- 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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'}
|
||||
]
|
||||
|
||||
@@ -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}'
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user