diff --git a/common/models/user.py b/common/models/user.py index 083d358..d96eeff 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -244,6 +244,7 @@ class PartnerService(db.Model): # Dynamic configuration specific to this service - using JSONB like your other models configuration = db.Column(db.JSON, nullable=True) + permissions = db.Column(db.JSON, nullable=True) # For services that need to track shared resources system_metadata = db.Column(db.JSON, nullable=True) diff --git a/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml b/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml index 186004b..a2edf33 100644 --- a/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml +++ b/config/partner_services/MANAGEMENT_SERVICE/1.0.0.yaml @@ -1,13 +1,25 @@ version: "1.0.0" name: "Management Service" configuration: - admin_roles: - name: "Administrative Roles" - type: "enum" - description: "Administrative Roles that can be assigned to the partner" + billing_partner: + name: "Billing Partner" + type: "boolean" + description: "Billing of assigned Tenants is done through the partner." required: true - allowed_values: ["Partner Admin"] - default: "Partner Admin" + default: false +permissions: + can_create_tenant: + name: "Can Create Tenant" + type: "boolean" + description: "The Partner Admin can create new Tenants, linked to the partner" + required: true + default: false + can_assign_license: + name: "Can Assign License" + type: "boolean" + description: "The Partner Admin can assign licenses to Tenants, linked to the partner" + required: true + default: false metadata: author: "Josako" date_added: "2025-04-02" diff --git a/config/type_defs/partner_service_types.py b/config/type_defs/partner_service_types.py index 47e2b9f..a6fa34d 100644 --- a/config/type_defs/partner_service_types.py +++ b/config/type_defs/partner_service_types.py @@ -16,9 +16,5 @@ PARTNER_SERVICE_TYPES = { "name": "Management Service", "description": "Partner managing customer instances", }, - "WHITE_LABEL_SERVICE": { - "name": "White Label Service", - "description": "Partner reselling under their own brand", - } } diff --git a/eveai_app/templates/administration/edit_partner_service.html b/eveai_app/templates/administration/edit_partner_service.html index 04207fe..72319ef 100644 --- a/eveai_app/templates/administration/edit_partner_service.html +++ b/eveai_app/templates/administration/edit_partner_service.html @@ -10,9 +10,19 @@ {{ form.hidden_tag() }} {% set disabled_fields = ['type'] %} {% set exclude_fields = [] %} - {% for field in form %} + + {% for field in form.get_static_fields() %} {{ render_field(field, disabled_fields, exclude_fields) }} {% endfor %} + + {% for collection_name, fields in form.get_dynamic_fields().items() %} + {% if fields|length > 0 %} +

{{ collection_name }}

+ {% endif %} + {% for field in fields %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + {% endfor %} {% endblock %} diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index 59996e7..9c901a8 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -2,7 +2,7 @@ {% if field.type == 'BooleanField' %}
- {{ field(class="form-check-input " + class, disabled=disabled) }} + {{ field(class="form-check-input " + class, disabled=disabled, required=False) }} {% if field.description %} {{ field.label(class="form-check-label", **{'data-bs-toggle': 'tooltip', diff --git a/eveai_app/views/administration_forms.py b/eveai_app/views/administration_forms.py index 1f572ae..3440618 100644 --- a/eveai_app/views/administration_forms.py +++ b/eveai_app/views/administration_forms.py @@ -17,14 +17,14 @@ 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) + active = BooleanField('Active', validators=[Optional()], 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) + active = BooleanField('Active', validators=[Optional()], default=True) user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json]) @@ -40,7 +40,7 @@ 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) + active = BooleanField('Active', validators=[Optional()], default=True) user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json]) diff --git a/eveai_app/views/administration_views.py b/eveai_app/views/administration_views.py index f0014d6..3018fae 100644 --- a/eveai_app/views/administration_views.py +++ b/eveai_app/views/administration_views.py @@ -51,10 +51,12 @@ 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) + if request.method == 'GET': form.tenant.data = tenant.name if form.validate_on_submit(): + current_app.logger.debug(f"Form data for Partner: {form.data}") # Populate the user with form data form.populate_obj(partner) update_logging_information(partner, dt.now(tz.utc)) @@ -127,10 +129,11 @@ def partner_service(): form = PartnerServiceForm() if form.validate_on_submit(): - partner_id = session.get('partner_id', None) - if not partner_id: + 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')) + partner_id = partner['id'] new_partner_service = PartnerService() form.populate_obj(new_partner_service) set_logging_information(new_partner_service, dt.now(tz.utc)) @@ -156,7 +159,8 @@ def partner_service(): @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) + partner = session.get('partner', None) + partner_id = session['partner']['id'] form = EditPartnerServiceForm(obj=partner_service) partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type, @@ -165,10 +169,21 @@ def edit_partner_service(partner_service_id): 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) + permissions_config = partner_service_config.get('permissions') + current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: " + f"{permissions_config}") + form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions) if form.validate_on_submit(): + current_app.logger.debug(f"Form returned: {form.data}") + raw_form_data = request.form.to_dict() + current_app.logger.debug(f"Raw form data: {raw_form_data}") + form.populate_obj(partner_service) partner_service.configuration = form.get_dynamic_data('configuration') + partner_service.permissions = form.get_dynamic_data('permissions') + current_app.logger.debug(f"Partner Service configuration: {partner_service.configuration}") + current_app.logger.debug(f"Partner Service permissions: {partner_service.permissions}") # update partner relationship partner_service.partner_id = partner_id @@ -201,10 +216,11 @@ def edit_partner_service(partner_service_id): 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: + 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')) + partner_id = session['partner']['id'] query = PartnerService.query.filter(PartnerService.partner_id == partner_id) diff --git a/eveai_app/views/dynamic_form_base.py b/eveai_app/views/dynamic_form_base.py index 32fda28..dfbba38 100644 --- a/eveai_app/views/dynamic_form_base.py +++ b/eveai_app/views/dynamic_form_base.py @@ -1,7 +1,7 @@ from flask_wtf import FlaskForm from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField, validators, ValidationError) -from flask import current_app +from flask import current_app, request import json from wtforms.fields.choices import SelectField @@ -56,13 +56,14 @@ class DynamicFormBase(FlaskForm): self.dynamic_fields = {} # Store formdata for later use self.formdata = formdata + self.raw_formdata = request.form.to_dict() def _create_field_validators(self, field_def): """Create validators based on field definition""" validators_list = [] # Required validator - if field_def.get('required', False): + if field_def.get('required', False) and field_def.get('type', None) != 'boolean': validators_list.append(validators.DataRequired()) else: validators_list.append(validators.Optional()) @@ -343,12 +344,16 @@ class DynamicFormBase(FlaskForm): data = {} if collection_name not in self.dynamic_fields: return data + prefix_length = len(collection_name) + 1 # +1 for the underscore for full_field_name in self.dynamic_fields[collection_name]: original_field_name = full_field_name[prefix_length:] field = getattr(self, full_field_name) # Parse JSON for special field types - if isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField)) and field.data: + if field.type == 'BooleanField': + data[original_field_name] = full_field_name in self.raw_formdata + current_app.logger.debug(f"Value for {original_field_name} is {data[original_field_name]}") + elif isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField)) and field.data: try: data[original_field_name] = json.loads(field.data) except json.JSONDecodeError: diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py index b282f79..c2e2635 100644 --- a/eveai_app/views/user_forms.py +++ b/eveai_app/views/user_forms.py @@ -106,7 +106,7 @@ class TenantProjectForm(FlaskForm): services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()]) unencrypted_api_key = StringField('Unencrypted API Key', validators=[DataRequired()], render_kw={'readonly': True}) visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True}) - active = BooleanField('Active', validators=[DataRequired()], default=True) + active = BooleanField('Active', validators=[Optional()], default=True) responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()]) def __init__(self, *args, **kwargs): @@ -120,7 +120,7 @@ class EditTenantProjectForm(FlaskForm): description = TextAreaField('Description', validators=[Optional()]) services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()]) visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True}) - active = BooleanField('Active', validators=[DataRequired()], default=True) + active = BooleanField('Active', validators=[Optional()], default=True) responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()]) def __init__(self, *args, **kwargs): diff --git a/migrations/public/versions/cfee2c5bcd7a_add_permissions_to_parterservice.py b/migrations/public/versions/cfee2c5bcd7a_add_permissions_to_parterservice.py new file mode 100644 index 0000000..65abb94 --- /dev/null +++ b/migrations/public/versions/cfee2c5bcd7a_add_permissions_to_parterservice.py @@ -0,0 +1,31 @@ +"""Add Permissions to ParterService + +Revision ID: cfee2c5bcd7a +Revises: 9ac89fc67661 +Create Date: 2025-04-11 07:56:56.802824 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cfee2c5bcd7a' +down_revision = '9ac89fc67661' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('partner_service', schema=None) as batch_op: + batch_op.add_column(sa.Column('permissions', sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('partner_service', schema=None) as batch_op: + batch_op.drop_column('permissions') + + # ### end Alembic commands ###