- Show markdown when signing a document
- Introduce consent history - Centralise consent and content services and config
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -56,3 +56,5 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
|||||||
/nginx/.parcel-cache/
|
/nginx/.parcel-cache/
|
||||||
/nginx/static/
|
/nginx/static/
|
||||||
/docker/build_logs/
|
/docker/build_logs/
|
||||||
|
/content/.Ulysses-Group.plist
|
||||||
|
/content/.Ulysses-Settings.plist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from common.services.user.user_services import UserServices
|
from common.services.user.user_services import UserServices
|
||||||
from common.services.user.partner_services import PartnerServices
|
from common.services.user.partner_services import PartnerServices
|
||||||
from common.services.user.tenant_services import TenantServices
|
from common.services.user.tenant_services import TenantServices
|
||||||
from common.services.user.consent_service import ConsentService
|
from common.services.user.consent_services import ConsentServices
|
||||||
|
|
||||||
__all__ = ['UserServices', 'PartnerServices', 'TenantServices', 'ConsentService']
|
__all__ = ['UserServices', 'PartnerServices', 'TenantServices', 'ConsentServices']
|
||||||
@@ -20,7 +20,7 @@ class TypeStatus:
|
|||||||
last_version: Optional[str]
|
last_version: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class ConsentService:
|
class ConsentServices:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_required_consent_types() -> List[str]:
|
def get_required_consent_types() -> List[str]:
|
||||||
return list(current_app.config.get("CONSENT_TYPES", []))
|
return list(current_app.config.get("CONSENT_TYPES", []))
|
||||||
@@ -50,12 +50,12 @@ class ConsentService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def evaluate_type_status(tenant_id: int, consent_type: str) -> TypeStatus:
|
def evaluate_type_status(tenant_id: int, consent_type: str) -> TypeStatus:
|
||||||
active = ConsentService.get_active_consent_version(consent_type)
|
active = ConsentServices.get_active_consent_version(consent_type)
|
||||||
if not active:
|
if not active:
|
||||||
current_app.logger.error(f"No active ConsentVersion found for type {consent_type}")
|
current_app.logger.error(f"No active ConsentVersion found for type {consent_type}")
|
||||||
return TypeStatus(consent_type, ConsentStatus.UNKNOWN_CONSENT_VERSION, None, None)
|
return TypeStatus(consent_type, ConsentStatus.UNKNOWN_CONSENT_VERSION, None, None)
|
||||||
|
|
||||||
last = ConsentService.get_tenant_last_consent(tenant_id, consent_type)
|
last = ConsentServices.get_tenant_last_consent(tenant_id, consent_type)
|
||||||
if not last:
|
if not last:
|
||||||
return TypeStatus(consent_type, ConsentStatus.NOT_CONSENTED, active.consent_version, None)
|
return TypeStatus(consent_type, ConsentStatus.NOT_CONSENTED, active.consent_version, None)
|
||||||
|
|
||||||
@@ -98,8 +98,8 @@ class ConsentService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_consent_status(tenant_id: int) -> ConsentStatus:
|
def get_consent_status(tenant_id: int) -> ConsentStatus:
|
||||||
statuses = [ConsentService.evaluate_type_status(tenant_id, ct) for ct in ConsentService.get_required_consent_types()]
|
statuses = [ConsentServices.evaluate_type_status(tenant_id, ct) for ct in ConsentServices.get_required_consent_types()]
|
||||||
return ConsentService.aggregate_status(statuses)
|
return ConsentServices.aggregate_status(statuses)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_tenant_admin_for(tenant_id: int) -> bool:
|
def _is_tenant_admin_for(tenant_id: int) -> bool:
|
||||||
@@ -131,9 +131,9 @@ class ConsentService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def can_consent_on_behalf(tenant_id: int) -> Tuple[bool, str, Optional[int], Optional[int]]:
|
def can_consent_on_behalf(tenant_id: int) -> Tuple[bool, str, Optional[int], Optional[int]]:
|
||||||
# Returns: allowed, mode('tenant_admin'|'management_partner'), partner_id, partner_service_id
|
# Returns: allowed, mode('tenant_admin'|'management_partner'), partner_id, partner_service_id
|
||||||
if ConsentService._is_tenant_admin_for(tenant_id):
|
if ConsentServices._is_tenant_admin_for(tenant_id):
|
||||||
return True, 'tenant_admin', None, None
|
return True, 'tenant_admin', None, None
|
||||||
allowed, partner_id, partner_service_id = ConsentService._is_management_partner_for(tenant_id)
|
allowed, partner_id, partner_service_id = ConsentServices._is_management_partner_for(tenant_id)
|
||||||
if allowed:
|
if allowed:
|
||||||
return True, 'management_partner', partner_id, partner_service_id
|
return True, 'management_partner', partner_id, partner_service_id
|
||||||
return False, 'none', None, None
|
return False, 'none', None, None
|
||||||
@@ -195,13 +195,13 @@ class ConsentService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def record_consent(tenant_id: int, consent_type: str) -> TenantConsent:
|
def record_consent(tenant_id: int, consent_type: str) -> TenantConsent:
|
||||||
# Validate type
|
# Validate type
|
||||||
if consent_type not in ConsentService.get_required_consent_types():
|
if consent_type not in ConsentServices.get_required_consent_types():
|
||||||
raise ValueError(f"Unknown consent type: {consent_type}")
|
raise ValueError(f"Unknown consent type: {consent_type}")
|
||||||
active = ConsentService.get_active_consent_version(consent_type)
|
active = ConsentServices.get_active_consent_version(consent_type)
|
||||||
if not active:
|
if not active:
|
||||||
raise RuntimeError(f"No active ConsentVersion for type {consent_type}")
|
raise RuntimeError(f"No active ConsentVersion for type {consent_type}")
|
||||||
|
|
||||||
allowed, mode, partner_id, partner_service_id = ConsentService.can_consent_on_behalf(tenant_id)
|
allowed, mode, partner_id, partner_service_id = ConsentServices.can_consent_on_behalf(tenant_id)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
raise PermissionError("Not authorized to record consent for this tenant")
|
raise PermissionError("Not authorized to record consent for this tenant")
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ class ConsentService:
|
|||||||
ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip() or request.remote_addr or ''
|
ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip() or request.remote_addr or ''
|
||||||
ua = request.headers.get('User-Agent', '')
|
ua = request.headers.get('User-Agent', '')
|
||||||
locale = session.get('locale') or request.accept_languages.best or ''
|
locale = session.get('locale') or request.accept_languages.best or ''
|
||||||
content_meta = ConsentService._resolve_consent_content(consent_type, active.consent_version)
|
content_meta = ConsentServices._resolve_consent_content(consent_type, active.consent_version)
|
||||||
consent_data = {
|
consent_data = {
|
||||||
'source_ip': ip,
|
'source_ip': ip,
|
||||||
'user_agent': ua,
|
'user_agent': ua,
|
||||||
@@ -178,5 +178,5 @@ class TenantServices:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_consent_status(tenant_id: int) -> ConsentStatus:
|
def get_consent_status(tenant_id: int) -> ConsentStatus:
|
||||||
# Delegate to centralized ConsentService to ensure consistent logic
|
# Delegate to centralized ConsentService to ensure consistent logic
|
||||||
from common.services.user.consent_service import ConsentService
|
from common.services.user.consent_services import ConsentServices
|
||||||
return ConsentService.get_consent_status(tenant_id)
|
return ConsentServices.get_consent_status(tenant_id)
|
||||||
|
|||||||
14
common/services/utils/version_services.py
Normal file
14
common/services/utils/version_services.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
class VersionServices:
|
||||||
|
@staticmethod
|
||||||
|
def split_version(full_version: str) -> tuple[str, str]:
|
||||||
|
parts = full_version.split(".")
|
||||||
|
if len(parts) < 3:
|
||||||
|
major_minor = '.'.join(parts[:2]) if len(parts) >= 2 else full_version
|
||||||
|
patch = ''
|
||||||
|
else:
|
||||||
|
major_minor = '.'.join(parts[:2])
|
||||||
|
patch = parts[2]
|
||||||
|
|
||||||
|
return major_minor, patch
|
||||||
@@ -316,6 +316,11 @@ class Config(object):
|
|||||||
# Type Definitions ----------------------------------------------------------------------------
|
# Type Definitions ----------------------------------------------------------------------------
|
||||||
TENANT_TYPES = ['Active', 'Demo', 'Inactive', 'Test']
|
TENANT_TYPES = ['Active', 'Demo', 'Inactive', 'Test']
|
||||||
CONSENT_TYPES = ["Data Privacy Agreement", "Terms & Conditions"]
|
CONSENT_TYPES = ["Data Privacy Agreement", "Terms & Conditions"]
|
||||||
|
# CONSENT_TYPE_MAP maps names with the actual base folders the consent documents are stored in
|
||||||
|
CONSENT_TYPE_MAP = {
|
||||||
|
"Data Privacy Agreement": "dpa",
|
||||||
|
"Terms & Conditions": "terms",
|
||||||
|
}
|
||||||
|
|
||||||
# The maximum number of seconds allowed for audio compression (to save resources)
|
# The maximum number of seconds allowed for audio compression (to save resources)
|
||||||
MAX_COMPRESSION_DURATION = 60*10 # 10 minutes
|
MAX_COMPRESSION_DURATION = 60*10 # 10 minutes
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<div class="card">
|
<div class="card" id="content-viewer-section">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" id="showRaw">Show Raw</button>
|
<button class="btn btn-sm btn-outline-secondary" id="showRaw">Show Raw</button>
|
||||||
<button class="btn btn-sm btn-outline-primary active" id="showRendered">Show Rendered</button>
|
<button class="btn btn-sm btn-outline-primary active" id="showRendered">Show Rendered</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body" id="content-document-viewer">
|
||||||
<!-- Raw markdown view (hidden by default) -->
|
<!-- Raw markdown view (hidden by default) -->
|
||||||
<div id="rawMarkdown" class="code-wrapper" style="display: none;">
|
<div id="rawMarkdown" class="code-wrapper" style="display: none;">
|
||||||
<pre><code class="language-markdown">{{ markdown_content }}</code></pre>
|
<pre><code class="language-markdown">{{ markdown_content }}</code></pre>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
<!-- Gebundelde CSS (bevat nu al je CSS) -->
|
<!-- Gebundelde CSS (bevat nu al je CSS) -->
|
||||||
<link href="{{ asset_url('dist/main.css') }}" rel="stylesheet" />
|
<link href="{{ asset_url('dist/main.css') }}" rel="stylesheet" />
|
||||||
<!-- Consent viewer specific styles -->
|
<!-- Content viewer specific styles -->
|
||||||
<link href="{{ prefixed_url_for('static', filename='assets/css/eveai-consent-viewer.css') }}" rel="stylesheet" />
|
<link href="{{ url_for('static', filename='assets/css/eveai-content-viewer.css') }}" rel="stylesheet" />
|
||||||
<base href="/admin/">
|
<base href="/admin/">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,11 @@
|
|||||||
{'name': 'Consent Versions', 'url': 'user/consent_versions', 'roles': ['Super User']},
|
{'name': 'Consent Versions', 'url': 'user/consent_versions', 'roles': ['Super User']},
|
||||||
{'name': 'Tenant Overview', 'url': 'user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Overview', 'url': 'user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Edit Tenant', 'url': 'user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Edit Tenant', 'url': 'user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Consents', 'url': 'user/consent/tenant', 'roles': ['Tenant Admin']},
|
{'name': 'Tenant Consent History', 'url': 'user/tenant_consents_history', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
|
{'name': 'Tenant Consent Status', 'url': 'user/consent/tenant', 'roles': ['Tenant Admin']},
|
||||||
|
{'name': 'Tenant Consent Status', 'url': 'user/tenants/' ~ session['tenant'].get('id') ~ '/consents', 'roles': ['Super User', 'Partner Admin']},
|
||||||
{'name': 'Consent Renewal', 'url': 'user/consent/tenant_renewal', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Consent Renewal', 'url': 'user/consent/tenant_renewal', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Consents Overview', 'url': 'user/tenants/' ~ session['tenant'].get('id') ~ '/consents', 'roles': ['Super User', 'Partner Admin']},
|
|
||||||
{'name': 'Tenant Partner Services', 'url': 'user/tenant_partner_services', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Partner Services', 'url': 'user/tenant_partner_services', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Makes', 'url': 'user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Makes', 'url': 'user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Projects', 'url': 'user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Projects', 'url': 'user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ def view_content(content_type):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
content_type (str): Type content (eg. 'changelog', 'terms', 'dpa')
|
content_type (str): Type content (eg. 'changelog', 'terms', 'dpa')
|
||||||
|
Request Args:
|
||||||
|
version (str): Major.Minor version of the content to display
|
||||||
|
patch (str): Patch version of the content to display
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
major_minor = request.args.get('version')
|
major_minor = request.args.get('version')
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
import ast
|
import ast
|
||||||
|
|
||||||
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService, \
|
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService, \
|
||||||
ConsentVersion
|
ConsentVersion, TenantConsent, Partner
|
||||||
from common.services.user import UserServices, PartnerServices
|
from common.services.user import UserServices, PartnerServices
|
||||||
from common.utils.eveai_exceptions import EveAINoSessionPartner, EveAINoManagementPartnerService
|
from common.utils.eveai_exceptions import EveAINoSessionPartner, EveAINoManagementPartnerService
|
||||||
from common.utils.security_utils import current_user_has_role
|
from common.utils.security_utils import current_user_has_role
|
||||||
from eveai_app.views.list_views.list_view_utils import render_list_view
|
from eveai_app.views.list_views.list_view_utils import render_list_view
|
||||||
|
from common.extensions import db
|
||||||
|
|
||||||
# Tenant list view helper
|
# Tenant list view helper
|
||||||
def get_tenants_list_view():
|
def get_tenants_list_view():
|
||||||
@@ -376,3 +377,63 @@ def get_consent_versions_list_view():
|
|||||||
'description': f'Consent Versions'
|
'description': f'Consent Versions'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Tenant Consents history list view helper
|
||||||
|
|
||||||
|
def get_tenant_consents_list_view(tenant_id):
|
||||||
|
"""Generate the tenant consents history list view configuration for a specific tenant"""
|
||||||
|
# Build query joining optional partner->tenant for partner name and user for user name
|
||||||
|
q = db.session.query(
|
||||||
|
TenantConsent.id,
|
||||||
|
TenantConsent.consent_type,
|
||||||
|
TenantConsent.consent_date,
|
||||||
|
TenantConsent.consent_version,
|
||||||
|
User.user_name.label('user_name'),
|
||||||
|
Tenant.name.label('partner_name')
|
||||||
|
).join(User, User.id == TenantConsent.user_id)
|
||||||
|
# Left join Partner and its Tenant to get the partner tenant name
|
||||||
|
q = q.outerjoin(Partner, Partner.id == TenantConsent.partner_id)
|
||||||
|
q = q.outerjoin(Tenant, Tenant.id == Partner.tenant_id)
|
||||||
|
q = q.filter(TenantConsent.tenant_id == tenant_id).order_by(TenantConsent.consent_date.desc())
|
||||||
|
|
||||||
|
rows = q.all()
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for r in rows:
|
||||||
|
partner_name = r.partner_name if r.partner_name else '-'
|
||||||
|
data.append({
|
||||||
|
'id': r.id,
|
||||||
|
'consent_type': r.consent_type,
|
||||||
|
'consent_date': r.consent_date.isoformat() if hasattr(r.consent_date, 'isoformat') else str(r.consent_date),
|
||||||
|
'consent_version': r.consent_version,
|
||||||
|
'user_name': r.user_name,
|
||||||
|
'partner_name': partner_name,
|
||||||
|
})
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{'title': 'ID', 'field': 'id', 'width': 80},
|
||||||
|
{'title': 'Type', 'field': 'consent_type'},
|
||||||
|
{'title': 'Date', 'field': 'consent_date'},
|
||||||
|
{'title': 'Version', 'field': 'consent_version'},
|
||||||
|
{'title': 'User', 'field': 'user_name'},
|
||||||
|
{'title': 'Partner', 'field': 'partner_name'},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only a view action as records are immutable; handled via a POST -> redirect in the view handler
|
||||||
|
actions = [
|
||||||
|
{'value': 'view_consent_document', 'text': 'Bekijk document', 'class': 'btn-outline-secondary', 'requiresSelection': True},
|
||||||
|
]
|
||||||
|
|
||||||
|
initial_sort = [{'column': 'consent_date', 'dir': 'desc'}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': 'Tenant Consents History',
|
||||||
|
'data': data,
|
||||||
|
'columns': columns,
|
||||||
|
'actions': actions,
|
||||||
|
'initial_sort': initial_sort,
|
||||||
|
'table_id': 'tenant_consents_history_table',
|
||||||
|
'form_action': url_for('user_bp.handle_tenant_consents_history_selection'),
|
||||||
|
'description': f'Consent history for tenant {tenant_id}'
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import ast
|
|||||||
from wtforms import BooleanField
|
from wtforms import BooleanField
|
||||||
|
|
||||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake, \
|
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake, \
|
||||||
ConsentVersion
|
ConsentVersion, TenantConsent
|
||||||
from common.extensions import db, security, minio_client, simple_encryption, cache_manager, content_manager
|
from common.extensions import db, security, minio_client, simple_encryption, cache_manager, content_manager
|
||||||
|
from common.services.utils.version_services import VersionServices
|
||||||
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||||
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
@@ -29,9 +30,9 @@ from common.utils.mail_utils import send_email
|
|||||||
|
|
||||||
from eveai_app.views.list_views.user_list_views import get_tenants_list_view, get_users_list_view, \
|
from eveai_app.views.list_views.user_list_views import get_tenants_list_view, get_users_list_view, \
|
||||||
get_tenant_domains_list_view, get_tenant_projects_list_view, get_tenant_makes_list_view, \
|
get_tenant_domains_list_view, get_tenant_projects_list_view, get_tenant_makes_list_view, \
|
||||||
get_tenant_partner_services_list_view, get_consent_versions_list_view
|
get_tenant_partner_services_list_view, get_consent_versions_list_view, get_tenant_consents_list_view
|
||||||
from eveai_app.views.list_views.list_view_utils import render_list_view
|
from eveai_app.views.list_views.list_view_utils import render_list_view
|
||||||
from common.services.user.consent_service import ConsentService
|
from common.services.user.consent_services import ConsentServices
|
||||||
from common.models.user import ConsentStatus
|
from common.models.user import ConsentStatus
|
||||||
|
|
||||||
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
||||||
@@ -791,8 +792,8 @@ def tenant_consent():
|
|||||||
if not tenant_id:
|
if not tenant_id:
|
||||||
flash('No tenant context.', 'danger')
|
flash('No tenant context.', 'danger')
|
||||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||||
types = ConsentService.get_required_consent_types()
|
types = ConsentServices.get_required_consent_types()
|
||||||
statuses = [ConsentService.evaluate_type_status(tenant_id, t) for t in types]
|
statuses = [ConsentServices.evaluate_type_status(tenant_id, t) for t in types]
|
||||||
if current_app.jinja_env.loader:
|
if current_app.jinja_env.loader:
|
||||||
return render_template('user/tenant_consent.html', statuses=statuses, tenant_id=tenant_id)
|
return render_template('user/tenant_consent.html', statuses=statuses, tenant_id=tenant_id)
|
||||||
# Fallback text if no templates
|
# Fallback text if no templates
|
||||||
@@ -810,8 +811,8 @@ def no_consent():
|
|||||||
def tenant_consent_renewal():
|
def tenant_consent_renewal():
|
||||||
# Show renewal statuses only
|
# Show renewal statuses only
|
||||||
tenant_id = session.get('tenant', {}).get('id') or getattr(current_user, 'tenant_id', None)
|
tenant_id = session.get('tenant', {}).get('id') or getattr(current_user, 'tenant_id', None)
|
||||||
types = ConsentService.get_required_consent_types()
|
types = ConsentServices.get_required_consent_types()
|
||||||
statuses = [s for s in [ConsentService.evaluate_type_status(tenant_id, t) for t in types] if s.status != ConsentStatus.CONSENTED]
|
statuses = [s for s in [ConsentServices.evaluate_type_status(tenant_id, t) for t in types] if s.status != ConsentStatus.CONSENTED]
|
||||||
if current_app.jinja_env.loader:
|
if current_app.jinja_env.loader:
|
||||||
return render_template('user/tenant_consent_renewal.html', statuses=statuses, tenant_id=tenant_id)
|
return render_template('user/tenant_consent_renewal.html', statuses=statuses, tenant_id=tenant_id)
|
||||||
return "\n".join([f"{s.consent_type}: {s.status}" for s in statuses])
|
return "\n".join([f"{s.consent_type}: {s.status}" for s in statuses])
|
||||||
@@ -827,12 +828,12 @@ def consent_renewal():
|
|||||||
@roles_accepted('Super User', 'Partner Admin')
|
@roles_accepted('Super User', 'Partner Admin')
|
||||||
def view_tenant_consents(tenant_id: int):
|
def view_tenant_consents(tenant_id: int):
|
||||||
# Authorization: Tenant Admin for own tenant or Management Partner
|
# Authorization: Tenant Admin for own tenant or Management Partner
|
||||||
allowed, mode, _, _ = ConsentService.can_consent_on_behalf(tenant_id)
|
allowed, mode, _, _ = ConsentServices.can_consent_on_behalf(tenant_id)
|
||||||
if not (allowed or current_user.has_roles('Super User')):
|
if not (allowed or current_user.has_roles('Super User')):
|
||||||
flash('Not authorized to view consents for this tenant', 'danger')
|
flash('Not authorized to view consents for this tenant', 'danger')
|
||||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||||
types = ConsentService.get_required_consent_types()
|
types = ConsentServices.get_required_consent_types()
|
||||||
statuses = [ConsentService.evaluate_type_status(tenant_id, t) for t in types]
|
statuses = [ConsentServices.evaluate_type_status(tenant_id, t) for t in types]
|
||||||
if current_app.jinja_env.loader:
|
if current_app.jinja_env.loader:
|
||||||
return render_template('user/tenant_consents_overview.html', statuses=statuses, tenant_id=tenant_id)
|
return render_template('user/tenant_consents_overview.html', statuses=statuses, tenant_id=tenant_id)
|
||||||
return "\n".join([f"{s.consent_type}: {s.status}" for s in statuses])
|
return "\n".join([f"{s.consent_type}: {s.status}" for s in statuses])
|
||||||
@@ -842,7 +843,7 @@ def view_tenant_consents(tenant_id: int):
|
|||||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
def accept_tenant_consent(tenant_id: int, consent_type: str):
|
def accept_tenant_consent(tenant_id: int, consent_type: str):
|
||||||
try:
|
try:
|
||||||
tc = ConsentService.record_consent(tenant_id, consent_type)
|
tc = ConsentServices.record_consent(tenant_id, consent_type)
|
||||||
flash(f"Consent for {consent_type} recorded (version {tc.consent_version})", 'success')
|
flash(f"Consent for {consent_type} recorded (version {tc.consent_version})", 'success')
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
flash('Not authorized to accept this consent for the tenant', 'danger')
|
flash('Not authorized to accept this consent for the tenant', 'danger')
|
||||||
@@ -862,7 +863,7 @@ def view_consent_markdown(consent_type: str, version: str):
|
|||||||
try:
|
try:
|
||||||
current_app.logger.debug(f"Rendering markdown for {consent_type} version {version}")
|
current_app.logger.debug(f"Rendering markdown for {consent_type} version {version}")
|
||||||
# Validate type against config
|
# Validate type against config
|
||||||
valid_types = set(ConsentService.get_required_consent_types())
|
valid_types = set(ConsentServices.get_required_consent_types())
|
||||||
if consent_type not in valid_types:
|
if consent_type not in valid_types:
|
||||||
for t in valid_types:
|
for t in valid_types:
|
||||||
if t.lower() == consent_type.lower():
|
if t.lower() == consent_type.lower():
|
||||||
@@ -879,27 +880,14 @@ def view_consent_markdown(consent_type: str, version: str):
|
|||||||
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Document not found for version {version}"), 404)
|
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Document not found for version {version}"), 404)
|
||||||
|
|
||||||
# Map consent type to content_manager content_type
|
# Map consent type to content_manager content_type
|
||||||
type_map = {
|
type_map = current_app.config.get('CONSENT_TYPE_MAP', {})
|
||||||
'Data Privacy Agreement': 'dpa',
|
|
||||||
'Terms & Conditions': 'terms',
|
|
||||||
}
|
|
||||||
content_type = type_map.get(consent_type)
|
content_type = type_map.get(consent_type)
|
||||||
if not content_type:
|
if not content_type:
|
||||||
current_app.logger.warning(f"No content_type mapping for consent type {consent_type}")
|
current_app.logger.warning(f"No content_type mapping for consent type {consent_type}")
|
||||||
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Unknown content mapping for {consent_type}"), 404)
|
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Unknown content mapping for {consent_type}"), 404)
|
||||||
|
|
||||||
# Parse major.minor and patch from version (e.g., 1.2.3 -> 1.2 and 1.2.3)
|
# Parse major.minor and patch from version (e.g., 1.2.3 -> 1.2 and 1.2.3)
|
||||||
parts = version.split('.')
|
major_minor, patch = VersionServices.split_version(version)
|
||||||
current_app.logger.debug(f"Parts of version {version}: {parts}")
|
|
||||||
if len(parts) < 3:
|
|
||||||
current_app.logger.warning(f"Version does not follow a.b.c pattern: {version}")
|
|
||||||
major_minor = '.'.join(parts[:2]) if len(parts) >= 2 else version
|
|
||||||
patch = ''
|
|
||||||
else:
|
|
||||||
major_minor = '.'.join(parts[:2])
|
|
||||||
patch = parts[2]
|
|
||||||
|
|
||||||
current_app.logger.debug(f"Retrieving markdown for {consent_type}/{content_type} for major.minor={major_minor} and patch={patch} ")
|
|
||||||
|
|
||||||
# Use content_manager to read content
|
# Use content_manager to read content
|
||||||
content_data = content_manager.read_content(content_type, major_minor, patch)
|
content_data = content_manager.read_content(content_type, major_minor, patch)
|
||||||
@@ -977,3 +965,41 @@ def send_api_key_notification(tenant_id, tenant_name, project_name, api_key, ser
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Failed to send API key notification email: {str(e)}")
|
current_app.logger.error(f"Failed to send API key notification email: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_consents_history', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_consents_history():
|
||||||
|
tenant_id = session['tenant']['id']
|
||||||
|
config = get_tenant_consents_list_view(tenant_id)
|
||||||
|
return render_list_view('list_view.html', **config)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/handle_tenant_consents_history_selection', methods=['POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def handle_tenant_consents_history_selection():
|
||||||
|
action = request.form.get('action')
|
||||||
|
if action == 'view_consent_document':
|
||||||
|
tenant_consent_identification = request.form.get('selected_row')
|
||||||
|
if not tenant_consent_identification:
|
||||||
|
flash('No consent selected', 'warning')
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_consents_history', for_redirect=True))
|
||||||
|
try:
|
||||||
|
consent_id = ast.literal_eval(tenant_consent_identification).get('value')
|
||||||
|
except Exception:
|
||||||
|
flash('Invalid selection', 'danger')
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_consents_history', for_redirect=True))
|
||||||
|
tc = TenantConsent.query.get_or_404(consent_id)
|
||||||
|
type_map = current_app.config.get('CONSENT_TYPE_MAP', {})
|
||||||
|
consent_type_dir = type_map.get(tc.consent_type)
|
||||||
|
major_minor, patch = VersionServices.split_version(tc.consent_version)
|
||||||
|
# Redirect to the fragment view; the template will render the fragment response as a full page if opened
|
||||||
|
return redirect(prefixed_url_for(
|
||||||
|
'basic_bp.view_content',
|
||||||
|
content_type=consent_type_dir,
|
||||||
|
version=major_minor,
|
||||||
|
patch=patch,
|
||||||
|
for_redirect=True
|
||||||
|
))
|
||||||
|
# Default: back to the history page
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_consents_history', for_redirect=True))
|
||||||
|
|||||||
Reference in New Issue
Block a user