Introduction of Partner Model, adding code to Tenant model

This commit is contained in:
Josako
2025-04-03 14:07:23 +02:00
parent 1762b930bc
commit 9ad7c1aee9
93 changed files with 823 additions and 22 deletions

View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% from "macros.html" import render_field %}
{% block title %}Update Partner{% endblock %}
{% block content_title %}Update Partner{% endblock %}
{% block content_description %}Update partner{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = ['tenant', 'code'] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<button type="submit" class="btn btn-primary">Update Partner</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% from "macros.html" import render_field %}
{% block title %}Register Partner Service{% endblock %}
{% block content_title %}Register Partner Service{% endblock %}
{% block content_description %}Register Partner Service{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = ['type'] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<button type="submit" class="btn btn-primary">Register Partner Service</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% from "macros.html" import render_field %}
{% block title %}Register Partner Service{% endblock %}
{% block content_title %}Register Partner Service{% endblock %}
{% block content_description %}Register Partner Service{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = [] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<button type="submit" class="btn btn-primary">Register Partner Service</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Partner Services{% endblock %}
{% block content_title %}Partner Services{% endblock %}
{% block content_description %}View Partner Services for active Partner{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('administration_bp.handle_partner_service_selection') }}" id="partnerServicesForm">
{{ render_selectable_table(headers=["Partner Service ID", "Name", "Type"], rows=rows, selectable=True, id="retrieversTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_partner_service" class="btn btn-primary" onclick="return validateTableSelection('partnerServicesForm')">Edit Partner Service</button>
</div>
<button type="submit" name="action" value="create_partner_service" class="btn btn-success">Register Partner Service</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.retrievers') }}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Partners{% endblock %}
{% block content_title %}Partners{% endblock %}
{% block content_description %}View Partners{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('administration_bp.handle_partner_selection') }}" id="partnersForm">
{{ render_selectable_table(headers=["Partner ID", "Name"], rows=rows, selectable=True, id="partnersTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_partner" class="btn btn-primary" onclick="return validateTableSelection('partnersForm')">Edit Partner</button>
<button type="submit" name="action" value="set_session_partner" class="btn btn-primary" onclick="return validateTableSelection('partnersForm')">Set Session Partner</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.retrievers') }}
{% endblock %}

View File

@@ -8,6 +8,7 @@
<!-- Trigger action Form -->
<form method="POST" action="{{ url_for('administration_bp.handle_trigger_action') }}">
<div class="form-group mt-3">
<button type="submit" name="action" value="register_partner" class="btn btn-secondary">Register Partner</button>
<button type="submit" name="action" value="update_usages" class="btn btn-secondary">Update Usages</button>
</div>
</form>

View File

@@ -9,7 +9,7 @@
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = ['user_name', 'user_email', 'tenant_name'] %}
{% set disabled_fields = ['user_name', 'user_email', 'tenant_name', 'partner_name'] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}

View File

@@ -101,6 +101,8 @@
{'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']},
{'name': 'Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Partners', 'url': '/administration/partners', 'roles': ['Super User']},
{'name': 'Partner Services', 'url': '/administration/partner_services', 'roles': ['Super User']},
]) }}
{% endif %}
{% if current_user.is_authenticated %}
@@ -123,6 +125,13 @@
{% endif %}
</a>
</li>
{% if current_user.has_roles('Super User') and 'partner' in session %}
<li class="nav-item mt-2">
<a href="/session_defaults" class="btn btn-sm bg-gradient-success mb-0">
PARTNER {{ session['partner'].get('id', 'None') }}: {{ session['partner'].get('name', 'None') }}
</a>
</li>
{% endif %}
</ul>
{% endif %}
</div>

View File

@@ -9,7 +9,7 @@
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = ['name', 'llm_model'] %}
{% set disabled_fields = ['name', 'code', 'llm_model'] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}

View File

@@ -9,7 +9,7 @@
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = [] %}
{% set disabled_fields = ['code'] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}

View File

@@ -1,7 +1,46 @@
from flask import current_app
from flask_wtf import FlaskForm
from wtforms.fields.simple import SubmitField
from wtforms.fields.choices import SelectField
from wtforms.fields.simple import SubmitField, StringField, BooleanField, TextAreaField
from wtforms.validators import DataRequired, Optional, Length
from common.extensions import cache_manager
from common.utils.form_assistants import validate_json
from eveai_app.views.dynamic_form_base import DynamicFormBase
class TriggerActionForm(FlaskForm):
submit = SubmitField('Submit')
class EditPartnerForm(FlaskForm):
tenant = StringField('Tenant', validators=[DataRequired()], render_kw={'readonly': True})
code = StringField('Code', description="Referral Code", validators=[DataRequired()], render_kw={'readonly': True})
logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)])
active = BooleanField('Active', validators=[DataRequired()], default=True)
class PartnerServiceForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
type = SelectField('Partner Service Type', validators=[DataRequired()])
active = BooleanField('Active', validators=[DataRequired()], default=True)
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Dynamically populate the 'type' field
types_dict = cache_manager.partner_services_types_cache.get_types()
self.type.choices = [(key, value['name']) for key, value in types_dict.items()]
class EditPartnerServiceForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])
type = StringField('Partner Service Type', validators=[DataRequired()], render_kw={'readonly': True})
active = BooleanField('Active', validators=[DataRequired()], default=True)
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])

View File

@@ -1,3 +1,4 @@
import ast
import uuid
from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
@@ -5,10 +6,15 @@ from flask_security import hash_password, roles_required, roles_accepted, curren
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db, cache_manager
from common.models.user import Partner, Tenant, PartnerService
from common.utils.celery_utils import current_celery
from common.utils.eveai_exceptions import EveAIException
from common.utils.log_utils import format_query_results
from common.utils.model_logging_utils import update_logging_information, set_logging_information
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.nginx_utils import prefixed_url_for
from .administration_forms import TriggerActionForm
from .administration_forms import TriggerActionForm, EditPartnerForm, PartnerServiceForm, EditPartnerServiceForm
administration_bp = Blueprint('administration_bp', __name__, url_prefix='/administration')
@@ -35,5 +41,215 @@ def handle_trigger_action():
except Exception as e:
current_app.logger.error(f"Failed to trigger usage update task: {str(e)}")
flash(f'Failed to trigger usage update: {str(e)}', 'danger')
case 'register_partner':
try:
partner_id = register_partner_from_tenant(session['tenant']['id'])
return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id, ))
except EveAIException as e:
current_app.logger.error(f'Error registering partner for tenant {session['tenant']['id']}: {str(e)}')
flash('Error Registering Partner for Selected Tenant', 'danger')
return redirect(prefixed_url_for('user_bp.select_tenant'))
return redirect(prefixed_url_for('administration_bp.trigger_actions'))
@administration_bp.route('/partner/<int:partner_id>', methods=['GET', 'POST'])
@roles_accepted('Super User')
def edit_partner(partner_id):
partner = Partner.query.get_or_404(partner_id) # This will return a 404 if no partner is found
tenant = Tenant.query.get_or_404(partner.tenant_id)
form = EditPartnerForm(obj=partner)
form.tenant.data = tenant.name
if form.validate_on_submit():
# Populate the user with form data
form.populate_obj(partner)
update_logging_information(partner, dt.now(tz.utc))
db.session.commit()
flash('Partner updated successfully.', 'success')
return redirect(
prefixed_url_for('administration_bp.edit_partner',
partner_id=partner.id)) # Assuming there's a user profile view to redirect to
else:
form_validation_failed(request, form)
return render_template('administration/edit_partner.html', form=form, partner_id=partner_id)
@administration_bp.route('/partners', methods=['GET', 'POST'])
@roles_accepted('Super User')
def partners():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = (db.session.query(
Partner.id,
Partner.code,
Partner.active,
Partner.logo_url,
# Include all needed Partner columns here
Tenant.name.label('name')
).join(Tenant, Partner.tenant_id == Tenant.id).order_by(Partner.id))
current_app.logger.debug(f'{format_query_results(query)}')
pagination = query.paginate(page=page, per_page=per_page)
the_partners = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_partners, [('id', ''), ('name', '')])
# Render the catalogs in a template
return render_template('administration/partners.html', rows=rows, pagination=pagination)
@administration_bp.route('/handle_partner_selection', methods=['POST'])
@roles_accepted('Super User')
def handle_partner_selection():
action = request.form['action']
partner_identification = request.form.get('selected_row')
partner_id = ast.literal_eval(partner_identification).get('value')
partner = Partner.query.get_or_404(partner_id)
if action == 'set_session_partner':
current_app.logger.info(f"Setting session partner: {partner.id}")
session['partner'] = partner.to_dict()
elif action == 'edit_partner':
return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id))
return redirect(prefixed_url_for('administration_bp.partners'))
@administration_bp.route('/partner_service', methods=['GET', 'POST'])
@roles_accepted('Super User')
def partner_service():
form = PartnerServiceForm()
if form.validate_on_submit():
partner_id = session.get('partner_id', None)
if not partner_id:
flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('administration_bp.partners'))
new_partner_service = PartnerService()
form.populate_obj(new_partner_service)
set_logging_information(new_partner_service, dt.now(tz.utc))
new_partner_service.partner_id = partner_id
try:
db.session.add(new_partner_service)
db.session.commit()
flash('Partner Service successfully added!', 'success')
current_app.logger.info(f"Partner Service {new_partner_service.name} added successfully for {partner_id}")
# Step 2 of the creation process (depending on type)
return redirect(prefixed_url_for('administration_bp.partner_service',
partner_service_id=new_partner_service.id))
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to add Partner Service: {str(e)}', 'danger')
current_app.logger.error(f"Failed to add Partner Service {new_partner_service.name} "
f"for partner {partner_id}. Error: {str(e)}")
return render_template('administration/partner_service.html', form=form)
@administration_bp.route('/partner_service/<int:partner_service_id>', methods=['GET', 'POST'])
@roles_accepted('Super User')
def edit_partner_service(partner_service_id):
partner_service = PartnerService.query.get_or_404(partner_service_id)
partner_id = session.get('partner_id', None)
form = EditPartnerServiceForm(obj=partner_service)
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
partner_service.type_version)
configuration_config = partner_service_config.get('configuration')
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
f"{configuration_config}")
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
if form.validate_on_submit():
form.populate_obj(partner_service)
partner_service.configuration = form.get_dynamic_data('configuration')
# update partner relationship
partner_service.partner_id = partner_id
update_logging_information(partner_service, dt.now(tz.utc))
try:
db.session.add(partner_service)
db.session.commit()
flash('Partner Service updated successfully.', 'success')
current_app.logger.info(f"Partner Service {partner_service.name} updated successfully! ")
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to update Partner Service: {str(e)}', 'danger')
current_app.logger.error(f"Failed to update Partner Service {partner_service.id} for partner {partner_id}. "
f"Error: {str(e)} ")
return render_template('administration/edit_partner_service.html', form=form,
partner_service_id=partner_service_id)
return redirect(prefixed_url_for('administration_bp.partner_services'))
else:
form_validation_failed(request, form)
return render_template('administration/edit_partner_service.html', form=form,
partner_service_id=partner_service_id)
@administration_bp.route('/partner_services', methods=['GET', 'POST'])
@roles_accepted('Super User')
def partner_services():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
partner_id = session.get('partner_id', None)
if not partner_id:
flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('administration_bp.partners'))
query = PartnerService.query.filter(PartnerService.partner_id == partner_id)
pagination = query.paginate(page=page, per_page=per_page)
the_partner_services = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_partner_services, [('id', ''), ('name', ''), ('type', '')])
return render_template('administration/partner_services.html', rows=rows, pagination=pagination)
@administration_bp.route('/handle_partner_service_selection', methods=['POST'])
@roles_accepted('Super User')
def handle_partner_service_selection():
action = request.form['action']
if action == 'create_partner_service':
return redirect(prefixed_url_for('administration_bp.partner_service'))
partner_service_identification = request.form.get('selected_row')
partner_service_id = ast.literal_eval(partner_service_identification).get('value')
if action == 'edit_partner_service':
return redirect(prefixed_url_for('administration_bp.edit_partner_service',
partner_service_id=partner_service_id))
return redirect(prefixed_url_for('administration_bp.partner_services'))
def register_partner_from_tenant(tenant_id):
# check if there is already a partner defined for the tenant
partner = Partner.query.filter_by(tenant_id=tenant_id).first()
if partner:
return partner.id
try:
partner = Partner(
tenant_id=tenant_id,
code=f"PART-{str(uuid.uuid4())}",
)
set_logging_information(partner, dt.now(tz.utc))
db.session.add(partner)
db.session.commit()
return partner.id
except SQLAlchemyError as e:
db.session.rollback()
raise EveAIException(f"Failed to register partner for tenant {tenant_id}. Error: {str(e)}")

View File

@@ -18,6 +18,9 @@ class SessionDefaultsForm(FlaskForm):
tenant_name = StringField('Tenant Name', validators=[DataRequired()])
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# Partner Defaults
partner_name = StringField('Partner Name', validators=[DataRequired()])
# Default Catalog - initialize as a regular SelectField
catalog = SelectField('Catalog', choices=[], validators=[Optional()])
@@ -28,6 +31,10 @@ class SessionDefaultsForm(FlaskForm):
self.user_name.data = current_user.user_name
self.user_email.data = current_user.email
self.tenant_name.data = session.get('tenant').get('name')
if session.get('partner', None):
self.partner_name.data = session.get('partner').get('name')
else:
self.partner_name.data = ""
self.default_language.choices = [(lang, lang.lower()) for lang in
session.get('tenant').get('allowed_languages')]
self.default_language.data = session.get('default_language')

View File

@@ -10,21 +10,13 @@ from wtforms_sqlalchemy.fields import QuerySelectField
from common.extensions import cache_manager
from common.models.document import Catalog
from common.utils.form_assistants import validate_json
from config.type_defs.catalog_types import CATALOG_TYPES
from config.type_defs.processor_types import PROCESSOR_TYPES
from config.type_defs.retriever_types import RETRIEVER_TYPES
from .dynamic_form_base import DynamicFormBase
def validate_json(form, field):
if field.data:
try:
json.loads(field.data)
except json.JSONDecodeError:
raise ValidationError('Invalid JSON format')
class CatalogForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[Optional()])

View File

@@ -29,7 +29,6 @@ from common.utils.view_assistants import form_validation_failed, prepare_table_f
from .document_list_view import DocumentListView
from .document_version_list_view import DocumentVersionListView
from config.type_defs.catalog_types import CATALOG_TYPES
from config.type_defs.retriever_types import RETRIEVER_TYPES
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')

View File

@@ -63,7 +63,7 @@ class DynamicFormBase(FlaskForm):
# Required validator
if field_def.get('required', False):
validators_list.append(validators.InputRequired())
validators_list.append(validators.DataRequired())
else:
validators_list.append(validators.Optional())

View File

@@ -11,6 +11,7 @@ from config.type_defs.service_types import SERVICE_TYPES
class TenantForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=80)])
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
# language fields
@@ -125,3 +126,6 @@ class EditTenantProjectForm(FlaskForm):
super().__init__(*args, **kwargs)
# Initialize choices for the services field
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]

View File

@@ -8,7 +8,7 @@ from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
import ast
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, Partner
from common.extensions import db, security, minio_client, simple_encryption
from common.utils.security_utils import send_confirmation_email, send_reset_email
from config.type_defs.service_types import SERVICE_TYPES
@@ -18,6 +18,8 @@ from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key
from common.utils.nginx_utils import prefixed_url_for
from common.utils.eveai_exceptions import EveAIDoublePartner, EveAIException
from common.utils.document_utils import set_logging_information, update_logging_information
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -36,6 +38,10 @@ def log_after_request(response):
@roles_required('Super User')
def tenant():
form = TenantForm()
if request.method == 'GET':
code = f"TENANT-{str(uuid.uuid4())}"
form.code.data = code
if form.validate_on_submit():
# Handle the required attributes
new_tenant = Tenant()
@@ -244,6 +250,7 @@ def handle_tenant_selection():
return redirect(prefixed_url_for('user_bp.tenant_overview'))
case 'new_tenant':
return redirect(prefixed_url_for('user_bp.tenant'))
# Add more conditions for other actions
return redirect(prefixed_url_for('select_tenant'))