- Show markdown when signing a document

- Introduce consent history
- Centralise consent and content services and config
This commit is contained in:
Josako
2025-10-17 14:06:51 +02:00
parent eeb76d57b7
commit 5501061dd1
12 changed files with 162 additions and 49 deletions

View File

@@ -9,8 +9,9 @@ import ast
from wtforms import BooleanField
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.services.utils.version_services import VersionServices
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 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, \
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 common.services.user.consent_service import ConsentService
from common.services.user.consent_services import ConsentServices
from common.models.user import ConsentStatus
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -791,8 +792,8 @@ def tenant_consent():
if not tenant_id:
flash('No tenant context.', 'danger')
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
types = ConsentService.get_required_consent_types()
statuses = [ConsentService.evaluate_type_status(tenant_id, t) for t in types]
types = ConsentServices.get_required_consent_types()
statuses = [ConsentServices.evaluate_type_status(tenant_id, t) for t in types]
if current_app.jinja_env.loader:
return render_template('user/tenant_consent.html', statuses=statuses, tenant_id=tenant_id)
# Fallback text if no templates
@@ -810,8 +811,8 @@ def no_consent():
def tenant_consent_renewal():
# Show renewal statuses only
tenant_id = session.get('tenant', {}).get('id') or getattr(current_user, 'tenant_id', None)
types = ConsentService.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]
types = ConsentServices.get_required_consent_types()
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:
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])
@@ -827,12 +828,12 @@ def consent_renewal():
@roles_accepted('Super User', 'Partner Admin')
def view_tenant_consents(tenant_id: int):
# 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')):
flash('Not authorized to view consents for this tenant', 'danger')
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
types = ConsentService.get_required_consent_types()
statuses = [ConsentService.evaluate_type_status(tenant_id, t) for t in types]
types = ConsentServices.get_required_consent_types()
statuses = [ConsentServices.evaluate_type_status(tenant_id, t) for t in types]
if current_app.jinja_env.loader:
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])
@@ -842,7 +843,7 @@ def view_tenant_consents(tenant_id: int):
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def accept_tenant_consent(tenant_id: int, consent_type: str):
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')
except PermissionError:
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:
current_app.logger.debug(f"Rendering markdown for {consent_type} version {version}")
# 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:
for t in valid_types:
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)
# Map consent type to content_manager content_type
type_map = {
'Data Privacy Agreement': 'dpa',
'Terms & Conditions': 'terms',
}
type_map = current_app.config.get('CONSENT_TYPE_MAP', {})
content_type = type_map.get(consent_type)
if not content_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)
# Parse major.minor and patch from version (e.g., 1.2.3 -> 1.2 and 1.2.3)
parts = version.split('.')
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} ")
major_minor, patch = VersionServices.split_version(version)
# Use content_manager to read content
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:
current_app.logger.error(f"Failed to send API key notification email: {str(e)}")
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))