- Partner model additions

- menu changes to allow for partners
- partner views and forms now in partner_forms.py and partner_views.py
- Introduction of services layer
- Allow all configuration to handle partner configurations, and adaptation of caching to allow for this
This commit is contained in:
Josako
2025-05-02 13:10:59 +02:00
parent 9652d0bff9
commit 6ef025363d
72 changed files with 1342 additions and 228 deletions

View File

@@ -141,8 +141,8 @@ def register_blueprints(app):
app.register_blueprint(interaction_bp)
from .views.entitlements_views import entitlements_bp
app.register_blueprint(entitlements_bp)
from .views.administration_views import administration_bp
app.register_blueprint(administration_bp)
from .views.partner_views import partner_bp
app.register_blueprint(partner_bp)
from .views.healthz_views import healthz_bp, init_healtz
app.register_blueprint(healthz_bp)
init_healtz(app)

View File

@@ -56,13 +56,13 @@ def attribute_error_handler(error):
# Handle the SQLAlchemy relationship error specifically
if "'str' object has no attribute '_sa_instance_state'" in error_msg:
flash('Database relationship error. Please check your form inputs and try again.', 'error')
return render_template('errors/500.html',
return render_template('error/500.html',
error_type="Relationship Error",
error_details="A string value was provided where a database object was expected."), 500
# Handle other AttributeErrors
flash('An application error occurred. The technical team has been notified.', 'error')
return render_template('errors/500.html',
return render_template('error/500.html',
error_type="Attribute Error",
error_details=error_msg), 500
@@ -70,7 +70,7 @@ def attribute_error_handler(error):
def general_exception(e):
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
flash('An application error occurred. The technical team has been notified.', 'error')
return render_template('errors/500.html',
return render_template('error/500.html',
error_type=type(e).__name__,
error_details=str(e)), 500

View File

@@ -11,7 +11,7 @@
{{ form.hidden_tag() }}
{% set main_fields = ['start_date', 'end_date', 'currency', 'yearly_payment', 'basic_fee'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=['currency'], include_fields=main_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields + ['currency'], include_fields=main_fields) }}
{% endfor %}
<!-- Nav Tabs -->
<div class="row mt-5">
@@ -40,21 +40,21 @@
<div class="tab-pane fade show active" id="storage-tab" role="tabpanel">
{% set storage_fields = ['max_storage_tokens', 'additional_storage_token_price', 'additional_storage_bucket'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=[], include_fields=storage_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=storage_fields) }}
{% endfor %}
</div>
<!-- Embedding Tab -->
<div class="tab-pane fade" id="embedding-tab" role="tabpanel">
{% set embedding_fields = ['included_embedding_tokens', 'additional_embedding_token_price', 'additional_embedding_bucket'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=[], include_fields=embedding_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=embedding_fields) }}
{% endfor %}
</div>
<!-- Interaction Tab -->
<div class="tab-pane fade" id="interaction-tab" role="tabpanel">
{% set interaction_fields = ['included_interaction_tokens', 'additional_interaction_token_price', 'additional_interaction_bucket'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=[], include_fields=interaction_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=interaction_fields) }}
{% endfor %}
</div>
</div>

View File

@@ -11,7 +11,7 @@
{{ form.hidden_tag() }}
{% set main_fields = ['start_date', 'end_date', 'currency', 'yearly_payment', 'basic_fee'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=ext_disabled_fields + ['currency'], include_fields=main_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields + ['currency'], include_fields=main_fields) }}
{% endfor %}
<!-- Nav Tabs -->
<div class="row mt-5">
@@ -40,21 +40,21 @@
<div class="tab-pane fade show active" id="storage-tab" role="tabpanel">
{% set storage_fields = ['max_storage_mb', 'additional_storage_price', 'additional_storage_bucket'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=ext_disabled_fields, include_fields=storage_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=storage_fields) }}
{% endfor %}
</div>
<!-- Embedding Tab -->
<div class="tab-pane fade" id="embedding-tab" role="tabpanel">
{% set embedding_fields = ['included_embedding_mb', 'additional_embedding_price', 'additional_embedding_bucket', 'overage_embedding'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=ext_disabled_fields, include_fields=embedding_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=embedding_fields) }}
{% endfor %}
</div>
<!-- Interaction Tab -->
<div class="tab-pane fade" id="interaction-tab" role="tabpanel">
{% set interaction_fields = ['included_interaction_tokens', 'additional_interaction_token_price', 'additional_interaction_bucket', 'overage_interaction'] %}
{% for field in form %}
{{ render_included_field(field, disabled_fields=ext_disabled_fields, include_fields=interaction_fields) }}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=interaction_fields) }}
{% endfor %}
</div>
</div>

View File

@@ -10,10 +10,19 @@
{{ render_selectable_table(headers=["ID", "Name", "Version", "Start Date", "End Date"], rows=rows, selectable=True, id="licenseTierTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_license_tier" class="btn btn-primary" onclick="return validateTableSelection('licenseTiersForm')">Edit License Tier</button>
<button type="submit" name="action" value="create_license_for_tenant" class="btn btn-secondary" onclick="return validateTableSelection('licenseTiersForm')">Create License for Current Tenant</button>
{% if current_user.has_role('Super User') %}
<button type="submit" name="action" value="edit_license_tier" class="btn btn-primary" onclick="return validateTableSelection('licenseTiersForm')">Edit License Tier</button>
{% endif %}
{% if current_user.has_role('Super User') or (current_user.has_role('Partner Admin') and can_assign_license) %}
<button type="submit" name="action" value="create_license_for_tenant" class="btn btn-secondary" onclick="return validateTableSelection('licenseTiersForm')">Create Tenant License</button>
{% endif %}
{% if current_user.has_role('Super User') %}
<button type="submit" name="action" value="associate_license_tier_to_partner" class="btn btn-secondary" onclick="return validateTableSelection('licenseTiersForm')">Associate to Partner</button>
{% endif %}
</div>
<button type="submit" name="action" value="create_license_tier" class="btn btn-success">Register License Tier</button>
{% if current_user.has_role('Super User') %}
<button type="submit" name="action" value="create_license_tier" class="btn btn-success">Register License Tier</button>
{% endif %}
</div>
</form>

View File

@@ -1,8 +1,8 @@
{% macro render_field_content(field, disabled=False, class='') %}
{% macro render_field_content(field, disabled=False, readonly=False, class='') %}
{% if field.type == 'BooleanField' %}
<div class="form-group">
<div class="form-check form-switch">
{{ field(class="form-check-input " + class, disabled=disabled, required=False) }}
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}
{% if field.description %}
{{ field.label(class="form-check-label",
**{'data-bs-toggle': 'tooltip',
@@ -57,11 +57,11 @@
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
{{ field(class="form-control d-none " + class, disabled=disabled) }}
{{ field(class="form-control d-none " + class, disabled=disabled, readonly=readonly) }}
{% elif field.type == 'SelectField' %}
{{ field(class="form-control form-select " + class, disabled=disabled) }}
{{ field(class="form-control form-select " + class, disabled=disabled, readonly=readonly) }}
{% else %}
{{ field(class="form-control " + class, disabled=disabled) }}
{{ field(class="form-control " + class, disabled=disabled, readonly=readonly) }}
{% endif %}
{% if field.errors %}
@@ -76,21 +76,23 @@
{% endmacro %}
{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %}
{% macro render_field(field, disabled_fields=[], readonly_fields=[], exclude_fields=[], class='') %}
<!-- Debug info -->
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
{% set disabled = field.name in disabled_fields %}
{% set readonly = field.name in readonly_fields %}
{% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}
{% if field.name not in exclude_fields %}
{{ render_field_content(field, disabled, class) }}
{{ render_field_content(field, disabled, readonly, class) }}
{% endif %}
{% endmacro %}
{% macro render_included_field(field, disabled_fields=[], include_fields=[], class='') %}
{% macro render_included_field(field, disabled_fields=[], readonly_fields=[], include_fields=[], class='') %}
{% set disabled = field.name in disabled_fields %}
{% set readonly = field.name in readonly_fields %}
{% if field.name in include_fields %}
{{ render_field_content(field, disabled, class) }}
{{ render_field_content(field, disabled, readonly, class) }}
{% endif %}
{% endmacro %}

View File

@@ -68,7 +68,7 @@
<div class="collapse navbar-collapse w-100 pt-3 pb-2 py-lg-0" id="navigation">
<ul class="navbar-nav navbar-nav-hover mx-auto">
{% if current_user.is_authenticated %}
{{ dropdown('Tenant Configuration', 'source_environment', [
{{ dropdown('Tenants', 'source_environment', [
{'name': 'Tenants', 'url': '/user/select_tenant', 'roles': ['Super User', 'Partner 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']},
@@ -78,7 +78,14 @@
]) }}
{% endif %}
{% if current_user.is_authenticated %}
{{ dropdown('Document Mgmt', 'note_stack', [
{{ dropdown('Partners', 'partner_exchange', [
{'name': 'Partners', 'url': '/partner/partners', 'roles': ['Super User']},
{'name': 'Partner Services', 'url': '/partner/partner_services', 'roles': ['Super User']},
{'name': 'Edit Partner', 'url': '/partner/partner/' ~ session['partner'].get('id'), 'roles': ['Super User', 'Partner Admin']},
]) }}
{% endif %}
{% if current_user.is_authenticated %}
{{ dropdown('Documents', 'note_stack', [
{'name': 'Catalogs', 'url': '/document/catalogs', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Processors', 'url': '/document/processors', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Retrievers', 'url': '/document/retrievers', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
@@ -96,13 +103,11 @@
]) }}
{% endif %}
{% if current_user.is_authenticated %}
{{ dropdown('Administration', 'settings', [
{{ dropdown('Entitlements', 'settings', [
{'name': 'License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User', 'Partner Admin']},
{'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']},
{'name': 'Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']},
{'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin', 'Partner 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 %}

View File

@@ -9,11 +9,12 @@
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('administration_bp.handle_partner_service_selection') }}" id="partnerServicesForm">
<form method="POST" action="{{ url_for('partner_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>
<button type="submit" name="action" value="add_partner_service_for_tenant" class="btn btn-primary" onclick="return validateTableSelection('partnerServicesForm')">Add Partner Service for Tenant</button>
</div>
<button type="submit" name="action" value="create_partner_service" class="btn btn-success">Register Partner Service</button>
</div>

View File

@@ -9,7 +9,7 @@
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('administration_bp.handle_partner_selection') }}" id="partnersForm">
<form method="POST" action="{{ url_for('partner_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>

View File

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

View File

@@ -28,13 +28,13 @@ class LicenseTierForm(FlaskForm):
validators=[InputRequired(), NumberRange(min=0)])
additional_embedding_bucket = IntegerField('Additional Embedding Bucket Size (MiB)',
validators=[DataRequired(), NumberRange(min=1)])
included_interaction_tokens = IntegerField('Included Embedding Tokens',
included_interaction_tokens = IntegerField('Included Embedding Tokens (M Tokens)',
validators=[DataRequired(), NumberRange(min=1)])
additional_interaction_token_price_d = FloatField('Additional Interaction Token Fee ($)',
validators=[InputRequired(), NumberRange(min=0)])
additional_interaction_token_price_e = FloatField('Additional Interaction Token Fee (€)',
validators=[InputRequired(), NumberRange(min=0)])
additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size',
additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size (M Tokens)',
validators=[DataRequired(), NumberRange(min=1)])
standard_overage_embedding = FloatField('Standard Overage Embedding (%)',
validators=[DataRequired(), NumberRange(min=0)],
@@ -61,11 +61,11 @@ class LicenseForm(FlaskForm):
validators=[InputRequired(), NumberRange(min=0)])
additional_embedding_bucket = IntegerField('Additional Embedding Bucket Size (MiB)',
validators=[DataRequired(), NumberRange(min=1)])
included_interaction_tokens = IntegerField('Included Interaction Tokens',
included_interaction_tokens = IntegerField('Included Interaction Tokens (M Tokens)',
validators=[DataRequired(), NumberRange(min=1)])
additional_interaction_token_price = FloatField('Additional Interaction Token Fee',
validators=[InputRequired(), NumberRange(min=0)])
additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size',
additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size (M Tokens)',
validators=[DataRequired(), NumberRange(min=1)])
overage_embedding = FloatField('Overage Embedding (%)',
validators=[DataRequired(), NumberRange(min=0)],

View File

@@ -8,9 +8,17 @@ import ast
from common.models.entitlements import License, LicenseTier, LicenseUsage, BusinessEventLog
from common.extensions import db, security, minio_client, simple_encryption
from common.services.entitlement_services import EntitlementServices
from common.services.partner_services import PartnerServices
from common.services.tenant_services import TenantServices
from common.services.user_services 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 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
entitlements_bp = Blueprint('entitlements_bp', __name__, url_prefix='/entitlements')
@@ -25,6 +33,8 @@ def license_tier():
new_license_tier = LicenseTier()
form.populate_obj(new_license_tier)
set_logging_information(new_license_tier, dt.now(tz.utc))
try:
db.session.add(new_license_tier)
db.session.commit()
@@ -45,7 +55,7 @@ def license_tier():
@entitlements_bp.route('/view_license_tiers', methods=['GET', 'POST'])
@roles_accepted('Super User')
@roles_accepted('Super User', 'Partner Admin')
def view_license_tiers():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -56,7 +66,18 @@ def view_license_tiers():
LicenseTier.end_date == None,
LicenseTier.end_date >= today
)
).order_by(LicenseTier.start_date.desc(), LicenseTier.id)
)
if current_user_has_role('Partner Admin'):
try:
license_tier_ids = PartnerServices.get_allowed_license_tier_ids()
except EveAIException as e:
flash(f"Cannot retrieve License Tiers: {str(e)}", 'danger')
current_app.logger.error(f'Cannot retrieve License Tiers for partner: {str(e)}')
return render_template("index.html")
if license_tier_ids and len(license_tier_ids) > 0:
query = query.filter(LicenseTier.id.in_(license_tier_ids))
query = query.order_by(LicenseTier.start_date.desc(), LicenseTier.id)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
license_tiers = pagination.items
@@ -64,11 +85,14 @@ def view_license_tiers():
rows = prepare_table_for_macro(license_tiers, [('id', ''), ('name', ''), ('version', ''), ('start_date', ''),
('end_date', '')])
return render_template('entitlements/view_license_tiers.html', rows=rows, pagination=pagination)
return render_template('entitlements/view_license_tiers.html',
rows=rows,
pagination=pagination,
can_assign_license=UserServices.can_user_assign_license())
@entitlements_bp.route('/handle_license_tier_selection', methods=['POST'])
@roles_accepted('Super User')
@roles_accepted('Super User', 'Partner Admin')
def handle_license_tier_selection():
action = request.form['action']
if action == 'create_license_tier':
@@ -76,7 +100,6 @@ def handle_license_tier_selection():
license_tier_identification = request.form['selected_row']
license_tier_id = ast.literal_eval(license_tier_identification).get('value')
the_license_tier = LicenseTier.query.get(license_tier_id)
match action:
case 'edit_license_tier':
@@ -85,6 +108,9 @@ def handle_license_tier_selection():
case 'create_license_for_tenant':
return redirect(prefixed_url_for('entitlements_bp.create_license',
license_tier_id=license_tier_id))
case 'associate_license_tier_to_partner':
EntitlementServices.associate_license_tier_with_partner(license_tier_id)
# Add more conditions for other actions
return redirect(prefixed_url_for('entitlements_bp.view_license_tiers'))
@@ -99,6 +125,8 @@ def edit_license_tier(license_tier_id):
# Populate the license_tier with form data
form.populate_obj(license_tier)
update_logging_information(license_tier, dt.now(tz.utc))
try:
db.session.add(license_tier)
db.session.commit()
@@ -118,12 +146,15 @@ def edit_license_tier(license_tier_id):
@entitlements_bp.route('/create_license/<int:license_tier_id>', methods=['GET', 'POST'])
@roles_accepted('Super User')
@roles_accepted('Super User', 'Partner Admin')
def create_license(license_tier_id):
form = LicenseForm()
tenant_id = session.get('tenant').get('id')
currency = session.get('tenant').get('currency')
if current_user_has_role("Partner Admin"): # The Partner Admin can only set the end date
readonly_fields = [field.name for field in form if (field.name != 'end_date' and field.name != 'start_date')]
if request.method == 'GET':
# Fetch the LicenseTier
license_tier = LicenseTier.query.get_or_404(license_tier_id)
@@ -167,6 +198,8 @@ def create_license(license_tier_id):
# Currency is added here again, as a form doesn't include disabled fields when passing it in the request
new_license.currency = currency
set_logging_information(new_license, dt.now(tz.utc))
try:
db.session.add(new_license)
db.session.commit()
@@ -178,23 +211,27 @@ def create_license(license_tier_id):
else:
form_validation_failed(request, form)
return render_template('entitlements/license.html', form=form, ext_disabled_fields=[])
return render_template('entitlements/license.html', form=form, ext_readonly_fields=readonly_fields)
@entitlements_bp.route('/license/<int:license_id>', methods=['GET', 'POST'])
@roles_accepted('Super User')
@roles_accepted('Super User', 'Partner Admin')
def edit_license(license_id):
license = License.query.get_or_404(license_id) # This will return a 404 if no license tier is found
form = LicenseForm(obj=license)
disabled_fields = []
readonly_fields = []
if len(license.usages) > 0: # There already are usage records linked to this license
# Define which fields should be disabled
disabled_fields = [field.name for field in form if field.name != 'end_date']
readonly_fields = [field.name for field in form if field.name != 'end_date']
if current_user_has_role("Partner Admin"): # The Partner Admin can only set the end date
readonly_fields = [field.name for field in form if field.name != 'end_date']
if form.validate_on_submit():
# Populate the license with form data
form.populate_obj(license)
update_logging_information(license, dt.now(tz.utc))
try:
db.session.add(license)
db.session.commit()
@@ -210,7 +247,7 @@ def edit_license(license_id):
else:
form_validation_failed(request, form)
return render_template('entitlements/license.html', form=form, ext_disabled_fields=disabled_fields)
return render_template('entitlements/license.html', form=form, ext_readonly_fields=readonly_fields)
@entitlements_bp.route('/view_usages')

View File

@@ -7,26 +7,26 @@ 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.models.user import Partner, Tenant, PartnerService, PartnerTenant
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, EditPartnerForm, PartnerServiceForm, EditPartnerServiceForm
from .partner_forms import TriggerActionForm, EditPartnerForm, PartnerServiceForm, EditPartnerServiceForm
administration_bp = Blueprint('administration_bp', __name__, url_prefix='/administration')
partner_bp = Blueprint('partner_bp', __name__, url_prefix='/partner')
@administration_bp.route('/trigger_actions', methods=['GET'])
@partner_bp.route('/trigger_actions', methods=['GET'])
@roles_accepted('Super User')
def trigger_actions():
form = TriggerActionForm()
return render_template('administration/trigger_actions.html', form=form)
return render_template('partner/trigger_actions.html', form=form)
@administration_bp.route('/handle_trigger_action', methods=['POST'])
@partner_bp.route('/handle_trigger_action', methods=['POST'])
@roles_accepted('Super User')
def handle_trigger_action():
action = request.form['action']
@@ -42,10 +42,10 @@ def handle_trigger_action():
current_app.logger.error(f"Failed to trigger usage update task: {str(e)}")
flash(f'Failed to trigger usage update: {str(e)}', 'danger')
return redirect(prefixed_url_for('administration_bp.trigger_actions'))
return redirect(prefixed_url_for('partner_bp.trigger_actions'))
@administration_bp.route('/partner/<int:partner_id>', methods=['GET', 'POST'])
@partner_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
@@ -63,15 +63,15 @@ def edit_partner(partner_id):
db.session.commit()
flash('Partner updated successfully.', 'success')
return redirect(
prefixed_url_for('administration_bp.edit_partner',
prefixed_url_for('partner_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)
return render_template('partner/edit_partner.html', form=form, partner_id=partner_id)
@administration_bp.route('/partners', methods=['GET', 'POST'])
@partner_bp.route('/partners', methods=['GET', 'POST'])
@roles_accepted('Super User')
def partners():
page = request.args.get('page', 1, type=int)
@@ -95,21 +95,21 @@ def partners():
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)
return render_template('partner/partners.html', rows=rows, pagination=pagination)
@administration_bp.route('/handle_partner_selection', methods=['POST'])
@partner_bp.route('/handle_partner_selection', methods=['POST'])
@roles_accepted('Super User')
def handle_partner_selection():
action = request.form['action']
if action == 'create_partner':
try:
partner_id = register_partner_from_tenant(session['tenant']['id'])
return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id, ))
return redirect(prefixed_url_for('partner_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('administration_bp.partners'))
return redirect(prefixed_url_for('partner_bp.partners'))
partner_identification = request.form.get('selected_row')
partner_id = ast.literal_eval(partner_identification).get('value')
partner = Partner.query.get_or_404(partner_id)
@@ -118,12 +118,12 @@ def handle_partner_selection():
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('partner_bp.edit_partner', partner_id=partner_id))
return redirect(prefixed_url_for('administration_bp.partners'))
return redirect(prefixed_url_for('partner_bp.partners'))
@administration_bp.route('/partner_service', methods=['GET', 'POST'])
@partner_bp.route('/partner_service', methods=['GET', 'POST'])
@roles_accepted('Super User')
def partner_service():
form = PartnerServiceForm()
@@ -132,7 +132,7 @@ def partner_service():
partner = session.get('partner', None)
if not partner:
flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('administration_bp.partners'))
return redirect(prefixed_url_for('partner_bp.partners'))
partner_id = partner['id']
new_partner_service = PartnerService()
form.populate_obj(new_partner_service)
@@ -145,17 +145,17 @@ def partner_service():
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',
return redirect(prefixed_url_for('partner_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)
return render_template('partner/partner_service.html', form=form)
@administration_bp.route('/partner_service/<int:partner_service_id>', methods=['GET', 'POST'])
@partner_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)
@@ -200,18 +200,18 @@ def edit_partner_service(partner_service_id):
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,
return render_template('partner/edit_partner_service.html', form=form,
partner_service_id=partner_service_id)
return redirect(prefixed_url_for('administration_bp.partner_services'))
return redirect(prefixed_url_for('partner_bp.partner_services'))
else:
form_validation_failed(request, form)
return render_template('administration/edit_partner_service.html', form=form,
return render_template('partner/edit_partner_service.html', form=form,
partner_service_id=partner_service_id)
@administration_bp.route('/partner_services', methods=['GET', 'POST'])
@partner_bp.route('/partner_services', methods=['GET', 'POST'])
@roles_accepted('Super User')
def partner_services():
page = request.args.get('page', 1, type=int)
@@ -219,7 +219,7 @@ def partner_services():
partner = session.get('partner', None)
if not partner:
flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('administration_bp.partners'))
return redirect(prefixed_url_for('partner_bp.partners'))
partner_id = session['partner']['id']
query = PartnerService.query.filter(PartnerService.partner_id == partner_id)
@@ -230,24 +230,28 @@ def partner_services():
# 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)
return render_template('partner/partner_services.html', rows=rows, pagination=pagination)
@administration_bp.route('/handle_partner_service_selection', methods=['POST'])
@partner_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'))
return redirect(prefixed_url_for('partner_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',
return redirect(prefixed_url_for('partner_bp.edit_partner_service',
partner_service_id=partner_service_id))
elif action == 'add_partner_service_for_tenant':
add_partner_service_for_tenant(partner_service_id)
return redirect(prefixed_url_for('partner_bp.edit_partner_service',
partner_service_id=partner_service_id))
return redirect(prefixed_url_for('administration_bp.partner_services'))
return redirect(prefixed_url_for('partner_bp.partner_services'))
def register_partner_from_tenant(tenant_id):
@@ -270,3 +274,67 @@ def register_partner_from_tenant(tenant_id):
raise EveAIException(f"Failed to register partner for tenant {tenant_id}. Error: {str(e)}")
def add_partner_service_for_tenant(partner_service_id):
"""
Associate a partner service with the current tenant
Args:
partner_service_id: ID of the partner service to associate
Returns:
Redirect to appropriate page based on result
"""
tenant = session.get('tenant', None)
if not tenant:
flash('No tenant has been selected. Set tenant before adding services.', 'warning')
return redirect(prefixed_url_for('user_bp.tenants'))
tenant_id = tenant['id']
try:
# Check if the partner service exists
partner_service = PartnerService.query.get(partner_service_id)
if not partner_service:
flash(f'Partner service with ID {partner_service_id} not found.', 'danger')
return redirect(prefixed_url_for('partner_bp.partner_services'))
# Check if the association already exists
existing = PartnerTenant.query.filter_by(
tenant_id=tenant_id,
partner_service_id=partner_service_id
).first()
if existing:
flash(f'This tenant already has access to this partner service.', 'warning')
return redirect(prefixed_url_for('partner_bp.partner_services'))
# Create new association
new_partner_tenant = PartnerTenant(
tenant_id=tenant_id,
partner_service_id=partner_service_id,
# Add any additional fields needed for your model
)
# Set logging information
set_logging_information(new_partner_tenant, dt.now(tz.utc))
# Save to database
db.session.add(new_partner_tenant)
db.session.commit()
# Get partner name for the flash message
partner = Partner.query.get(partner_service.partner_id)
partner_tenant = Tenant.query.get(partner.tenant_id) if partner else None
partner_name = partner_tenant.name if partner_tenant else 'Unknown Partner'
flash(f'Successfully added {partner_service.type} service from {partner_name} to this tenant.', 'success')
return redirect(prefixed_url_for('partner_bp.partner_services'))
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Database error adding partner service: {str(e)}")
flash(f'Error adding partner service: {str(e)}', 'danger')
return redirect(prefixed_url_for('partner_bp.partner_services'))

View File

@@ -7,7 +7,7 @@ import pytz
from flask_security import current_user
from common.models.user import Role
from common.services.user_service import UserService
from common.services.user_services import UserServices
from config.type_defs.service_types import SERVICE_TYPES
@@ -62,7 +62,7 @@ class BaseUserForm(FlaskForm):
def __init__(self, *args, **kwargs):
super(BaseUserForm, self).__init__(*args, **kwargs)
self.roles.choices = UserService.get_assignable_roles()
self.roles.choices = UserServices.get_assignable_roles()
class CreateUserForm(BaseUserForm):

View File

@@ -8,7 +8,7 @@ import ast
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
from common.extensions import db, security, minio_client, simple_encryption
from common.services.user_service import UserService
from common.services.user_services import UserServices
from common.utils.security_utils import send_confirmation_email, send_reset_email
from config.type_defs.service_types import SERVICE_TYPES
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
@@ -19,7 +19,8 @@ from common.utils.simple_encryption import generate_api_key
from common.utils.nginx_utils import prefixed_url_for
from common.utils.eveai_exceptions import EveAIException
from common.utils.document_utils import set_logging_information, update_logging_information
from common.services.tenant_service import TenantService
from common.services.tenant_services import TenantServices
from common.services.user_services import UserServices
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -37,6 +38,10 @@ def log_after_request(response):
@user_bp.route('/tenant', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin')
def tenant():
if not current_user.has_roles('Partner Admin') and UserServices.can_user_create_tenant():
current_app.logger.error(f'User {current_user.email} cannot create tenant in the current user')
flash(f"You don't have the appropriate permissions to create a tenant", 'danger')
return redirect(prefixed_url_for('user_bp.select_tenant'))
form = TenantForm()
if request.method == 'GET':
code = f"TENANT-{str(uuid.uuid4())}"
@@ -58,10 +63,10 @@ def tenant():
if current_user.has_roles('Partner Admin') and 'partner' in session:
# Always associate with the partner for Partner Admins
TenantService.associate_tenant_with_partner(new_tenant.id)
TenantServices.associate_tenant_with_partner(new_tenant.id)
elif current_user.has_roles('Super User') and form.assign_to_partner.data and 'partner' in session:
# Super User chose to associate with partner
TenantService.associate_tenant_with_partner(new_tenant.id)
TenantServices.associate_tenant_with_partner(new_tenant.id)
except SQLAlchemyError as e:
current_app.logger.error(f'Failed to add tenant to database. Error: {str(e)}')
@@ -183,7 +188,7 @@ def edit_user(user_id):
# Update roles
current_roles = set(role.id for role in user.roles)
selected_roles = set(form.roles.data)
if UserService.validate_role_assignments(selected_roles):
if UserServices.validate_role_assignments(selected_roles):
# Add new roles
for role_id in selected_roles - current_roles:
role = Role.query.get(role_id)
@@ -273,10 +278,10 @@ def handle_tenant_selection():
tenant_identification = request.form['selected_row']
tenant_id = ast.literal_eval(tenant_identification).get('value')
if not TenantService.can_user_edit_tenant(tenant_id):
if not UserServices.can_user_edit_tenant(tenant_id):
current_app.logger.info(f"User not authenticated to edit tenant {tenant_id}.")
flash(f"You are not authenticated to manage tenant {tenant_id}", 'danger')
return redirect(prefixed_url_for('select_tenant'))
return redirect(prefixed_url_for('user_bp.select_tenant'))
the_tenant = Tenant.query.get(tenant_id)
# set tenant information in the session