- Introduction of PARTNER_RAG retriever, PARTNER_RAG_SPECIALIST and linked Agent and Task, to support documentation inquiries in the management app (eveai_app)

- Addition of a tenant_partner_services view to show partner services from the viewpoint of a tenant
- Addition of domain model diagrams
- Addition of license_periods views and form
This commit is contained in:
Josako
2025-07-16 21:24:08 +02:00
parent 000636a229
commit f3a243698c
30 changed files with 1566 additions and 356 deletions

View File

@@ -455,10 +455,10 @@ def add_url():
except EveAIException as e:
current_app.logger.error(f"Error adding document: {str(e)}")
flash(str(e), 'error')
flash(str(e), 'danger')
except Exception as e:
current_app.logger.error(f'Error adding document: {str(e)}')
flash('An error occurred while adding the document.', 'error')
flash('An error occurred while adding the document.', 'danger')
return render_template('document/add_url.html', form=form)

View File

@@ -5,6 +5,8 @@ from wtforms import (StringField, PasswordField, BooleanField, SubmitField, Emai
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError, InputRequired
import pytz
from common.models.entitlements import PeriodStatus
class LicenseTierForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
@@ -76,3 +78,14 @@ class LicenseForm(FlaskForm):
validators=[DataRequired(), NumberRange(min=0)],
default=0)
class EditPeriodForm(FlaskForm):
period_start = DateField('Period Start', id='form-control datepicker', validators=[DataRequired()])
period_end = DateField('Period End', id='form-control datepicker', validators=[DataRequired()])
status = SelectField('Status', choices=[], validators=[DataRequired()])
def __init__(self, *args, **kwargs):
super(EditPeriodForm, self).__init__(*args, **kwargs)
self.status.choices = [(item.name, item.value) for item in PeriodStatus]

View File

@@ -13,11 +13,11 @@ from common.services.user import PartnerServices
from common.services.user import UserServices
from common.utils.eveai_exceptions import EveAIException
from common.utils.security_utils import current_user_has_role
from .entitlements_forms import LicenseTierForm, LicenseForm
from .entitlements_forms import LicenseTierForm, LicenseForm, EditPeriodForm
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.nginx_utils import prefixed_url_for
from common.utils.document_utils import set_logging_information, update_logging_information
from .list_views.entitlement_list_views import get_license_tiers_list_view, get_license_list_view
from .list_views.entitlement_list_views import get_license_tiers_list_view, get_license_list_view, get_license_periods_list_view
from .list_views.list_view_utils import render_list_view
entitlements_bp = Blueprint('entitlements_bp', __name__, url_prefix='/entitlements')
@@ -255,14 +255,14 @@ def handle_license_selection():
case 'edit_license':
return redirect(prefixed_url_for('entitlements_bp.edit_license', license_id=license_id))
case 'view_periods':
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))
return redirect(prefixed_url_for('entitlements_bp.license_periods', license_id=license_id))
case _:
return redirect(prefixed_url_for('entitlements_bp.licenses'))
@entitlements_bp.route('/license/<int:license_id>/periods')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_license_periods(license_id):
def license_periods(license_id):
license = License.query.get_or_404(license_id)
# Verify user can access this license
@@ -272,48 +272,77 @@ def view_license_periods(license_id):
flash('Access denied to this license', 'danger')
return redirect(prefixed_url_for('entitlements_bp.licenses'))
# Get all periods for this license
periods = (LicensePeriod.query
.filter_by(license_id=license_id)
.order_by(LicensePeriod.period_number)
.all())
config = get_license_periods_list_view(license_id)
# Group related data for easy template access
usage_by_period = {}
payments_by_period = {}
invoices_by_period = {}
# Check if there was an error in getting the configuration
if config.get('error'):
return render_template("index.html")
for period in periods:
usage_by_period[period.id] = period.license_usage
payments_by_period[period.id] = list(period.payments)
invoices_by_period[period.id] = list(period.invoices)
return render_template('entitlements/license_periods.html',
license=license,
periods=periods,
usage_by_period=usage_by_period,
payments_by_period=payments_by_period,
invoices_by_period=invoices_by_period)
return render_list_view('list_view.html', **config)
@entitlements_bp.route('/license/<int:license_id>/periods/<int:period_id>/transition', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin')
def transition_period_status(license_id, period_id):
@entitlements_bp.route('/license_period/<int:period_id>', methods=['GET', 'POST'])
@roles_accepted('Super User')
def edit_license_period(period_id):
"""Handle status transitions for license periods"""
period = LicensePeriod.query.get_or_404(period_id)
new_status = request.form.get('new_status')
form = EditPeriodForm(obj=period)
try:
period.transition_status(PeriodStatus[new_status], current_user.id)
db.session.commit()
flash(f'Period {period.period_number} status updated to {new_status}', 'success')
except ValueError as e:
flash(f'Invalid status transition: {str(e)}', 'danger')
except Exception as e:
db.session.rollback()
flash(f'Error updating status: {str(e)}', 'danger')
if request.method == 'POST' and form.validate_on_submit():
form.populate_obj(period)
update_logging_information(period, dt.now(tz.utc))
match form.status.data:
case 'UPCOMING':
period.upcoming_at = dt.now(tz.utc)
case 'PENDING':
period.pending_at = dt.now(tz.utc)
case 'ACTIVE':
period.active_at = dt.now(tz.utc)
case 'COMPLETED':
period.completed_at = dt.now(tz.utc)
case 'INVOICED':
period.invoiced_at = dt.now(tz.utc)
case 'CLOSED':
period.closed_at = dt.now(tz.utc)
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))
try:
db.session.add(period)
db.session.commit()
flash('Period updated successfully.', 'success')
current_app.logger.info(f"Successfully updated period {period_id}")
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Error updating status: {str(e)}', 'danger')
current_app.logger.error(f"Error updating period {period_id}: {str(e)}")
return redirect(prefixed_url_for('entitlements_bp.license_periods', license_id=period.license_id))
return render_template('entitlements/edit_license_period.html', form=form)
@entitlements_bp.route('/license/<int:license_id>/handle_period_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_license_period_selection(license_id):
"""Handle actions for license periods"""
action = request.form['action']
# For actions that don't require a selection
if 'selected_row' not in request.form:
return redirect(prefixed_url_for('entitlements_bp.license_periods', license_id=license_id))
period_identification = request.form['selected_row']
period_id = ast.literal_eval(period_identification).get('value')
match action:
case 'view_period_details':
# TODO: Implement period details view if needed
flash('Period details view not yet implemented', 'info')
return redirect(prefixed_url_for('entitlements_bp.license_periods', license_id=license_id))
case 'edit_license_period':
# Display a form to choose the new status
return redirect(prefixed_url_for('entitlements_bp.edit_license_period', period_id=period_id))
case _:
return redirect(prefixed_url_for('entitlements_bp.license_periods', license_id=license_id))
@entitlements_bp.route('/view_licenses')

View File

@@ -3,7 +3,7 @@ from datetime import datetime as dt, timezone as tz
from flask import flash
from sqlalchemy import or_, desc
from common.models.entitlements import LicenseTier, License
from common.models.entitlements import LicenseTier, License, LicensePeriod
from common.services.user import PartnerServices, UserServices
from common.utils.eveai_exceptions import EveAIException
from common.utils.security_utils import current_user_has_role
@@ -77,10 +77,16 @@ def get_license_tiers_list_view():
'requiresSelection': False}
]
# Add assign license action if user has permission
# Add assign license actions if user has permission
current_app.logger.debug(f"Adding specific buttons")
if UserServices.can_user_assign_license():
actions.insert(1, {'value': 'assign_license', 'text': 'Assign License', 'class': 'btn-info',
'requiresSelection': True})
current_app.logger.debug(f"Adding Create License for Tenant")
actions.insert(1, {'value': 'create_license_for_tenant', 'text': 'Create License for Tenant',
'class': 'btn-secondary', 'requiresSelection': True})
if current_user_has_role('Super User'):
current_app.logger.debug(f"Adding Associate License Tier to Partner")
actions.insert(2, {'value': 'associate_license_tier_to_partner', 'text': 'Associate License Tier to Partner',
'class': 'btn-secondary','requiresSelection': True})
# Initial sort configuration
initial_sort = [{'column': 'start_date', 'dir': 'desc'}, {'column': 'id', 'dir': 'asc'}]
@@ -139,7 +145,7 @@ def get_license_list_view():
{'title': 'License Tier', 'field': 'license_tier_name'},
{'title': 'Start Date', 'field': 'start_date', 'width': 120},
{'title': 'Nr of Periods', 'field': 'nr_of_periods', 'width': 120},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross', 'width': 100}
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross', 'width': 80}
]
# Action definitions
@@ -162,3 +168,74 @@ def get_license_list_view():
'description': 'View and manage licenses',
'table_height': 700
}
def get_license_periods_list_view(license_id):
"""Generate the license periods list view configuration"""
# Get the license object
license = License.query.get_or_404(license_id)
# Get all periods for this license
periods = (LicensePeriod.query
.filter_by(license_id=license_id)
.order_by(LicensePeriod.period_number)
.all())
# Prepare data for Tabulator
data = []
for period in periods:
# Get usage data
usage = period.license_usage
storage_used = usage.storage_mb_used if usage else 0
embedding_used = usage.embedding_mb_used if usage else 0
interaction_used = usage.interaction_total_tokens_used if usage else 0
# Get payment status
prepaid_payment = period.prepaid_payment
prepaid_status = prepaid_payment.status.name if prepaid_payment else 'N/A'
# Get invoice status
prepaid_invoice = period.prepaid_invoice
invoice_status = prepaid_invoice.status.name if prepaid_invoice else 'N/A'
data.append({
'id': period.id,
'period_number': period.period_number,
'period_start': period.period_start.strftime('%Y-%m-%d'),
'period_end': period.period_end.strftime('%Y-%m-%d'),
'status': period.status.name,
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 60, 'type': 'number'},
{'title': 'Period', 'field': 'period_number'},
{'title': 'Start Date', 'field': 'period_start'},
{'title': 'End Date', 'field': 'period_end'},
{'title': 'Status', 'field': 'status'},
]
# Action definitions
actions = [
{'value': 'view_period_details', 'text': 'View Details', 'class': 'btn-primary', 'requiresSelection': True}
]
# If user has admin roles, add transition action
if current_user_has_role('Super User'):
actions.append({'value': 'edit_license_period', 'text': 'Edit Period', 'class': 'btn-secondary', 'requiresSelection': True})
# Initial sort configuration
initial_sort = [{'column': 'period_number', 'dir': 'asc'}]
return {
'title': 'License Periods',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'license_periods_table',
'form_action': url_for('entitlements_bp.handle_license_period_selection', license_id=license_id),
'description': f'View and manage periods for License {license_id}',
'table_height': 700,
'license': license
}

View File

@@ -10,9 +10,7 @@ def get_partners_list_view():
# Haal alle partners op met hun tenant informatie
query = (db.session.query(
Partner.id,
Partner.code,
Partner.active,
Partner.logo_url,
Tenant.name.label('name')
).join(Tenant, Partner.tenant_id == Tenant.id).order_by(Partner.id))
@@ -24,7 +22,6 @@ def get_partners_list_view():
for partner in all_partners:
data.append({
'id': partner.id,
'code': partner.code,
'name': partner.name,
'active': partner.active
})
@@ -32,7 +29,6 @@ def get_partners_list_view():
# Kolomdefinities
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Code', 'field': 'code'},
{'title': 'Name', 'field': 'name'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]

View File

@@ -3,7 +3,7 @@ from flask_security import roles_accepted
from sqlalchemy.exc import SQLAlchemyError
import ast
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService
from common.services.user import UserServices
from eveai_app.views.list_views.list_view_utils import render_list_view
@@ -232,3 +232,47 @@ def get_tenant_makes_list_view(tenant_id):
'form_action': url_for('user_bp.handle_tenant_make_selection'),
'description': f'Makes for tenant {tenant_id}'
}
# Tenant Partner Services list view helper
def get_tenant_partner_services_list_view(tenant_id):
"""Generate the tenant partner services list view configuration for a specific tenant"""
# Get partner services for the tenant through PartnerTenant association
query = PartnerService.query.join(PartnerTenant).filter(PartnerTenant.tenant_id == tenant_id)
partner_services = query.all()
# Prepare data for Tabulator
data = []
for service in partner_services:
data.append({
'id': service.id,
'name': service.name,
'type': service.type,
'type_version': service.type_version,
'active': service.active
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
{'title': 'Version', 'field': 'type_version'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross', 'width': 120}
]
# No actions needed as specified in requirements
actions = []
initial_sort = [{'column': 'name', 'dir': 'asc'}]
return {
'title': 'Partner Services',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'tenant_partner_services_table',
'form_action': url_for('user_bp.tenant_partner_services'),
'description': f'Partner Services for tenant {tenant_id}'
}

View File

@@ -24,7 +24,8 @@ from common.services.user import UserServices
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_domains_list_view, get_tenant_projects_list_view, get_tenant_makes_list_view, \
get_tenant_partner_services_list_view
from eveai_app.views.list_views.list_view_utils import render_list_view
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -686,6 +687,15 @@ def handle_tenant_make_selection():
return redirect(prefixed_url_for('user_bp.tenant_makes'))
@user_bp.route('/tenant_partner_services', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_partner_services():
tenant_id = session['tenant']['id']
config = get_tenant_partner_services_list_view(tenant_id)
return render_list_view('list_view.html', **config)
def reset_uniquifier(user):
security.datastore.set_uniquifier(user)
db.session.add(user)