diff --git a/common/models/user.py b/common/models/user.py index f703e59..995a0d1 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -34,32 +34,32 @@ class Tenant(db.Model): embedding_model = db.Column(db.String(50), nullable=True) llm_model = db.Column(db.String(50), nullable=True) - # Embedding variables ==> To be removed once all migrations (dev + prod) have been done - html_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']) - html_end_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'li']) - html_included_elements = db.Column(ARRAY(sa.String(50)), nullable=True) - html_excluded_elements = db.Column(ARRAY(sa.String(50)), nullable=True) - html_excluded_classes = db.Column(ARRAY(sa.String(200)), nullable=True) - - min_chunk_size = db.Column(db.Integer, nullable=True, default=2000) - max_chunk_size = db.Column(db.Integer, nullable=True, default=3000) - - # Embedding search variables - es_k = db.Column(db.Integer, nullable=True, default=5) - es_similarity_threshold = db.Column(db.Float, nullable=True, default=0.7) - - # Chat variables - chat_RAG_temperature = db.Column(db.Float, nullable=True, default=0.3) - chat_no_RAG_temperature = db.Column(db.Float, nullable=True, default=0.5) + # # Embedding variables ==> To be removed once all migrations (dev + prod) have been done + # html_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']) + # html_end_tags = db.Column(ARRAY(sa.String(10)), nullable=True, default=['p', 'li']) + # html_included_elements = db.Column(ARRAY(sa.String(50)), nullable=True) + # html_excluded_elements = db.Column(ARRAY(sa.String(50)), nullable=True) + # html_excluded_classes = db.Column(ARRAY(sa.String(200)), nullable=True) + # + # min_chunk_size = db.Column(db.Integer, nullable=True, default=2000) + # max_chunk_size = db.Column(db.Integer, nullable=True, default=3000) + # + # # Embedding search variables + # es_k = db.Column(db.Integer, nullable=True, default=5) + # es_similarity_threshold = db.Column(db.Float, nullable=True, default=0.7) + # + # # Chat variables + # chat_RAG_temperature = db.Column(db.Float, nullable=True, default=0.3) + # chat_no_RAG_temperature = db.Column(db.Float, nullable=True, default=0.5) fallback_algorithms = db.Column(ARRAY(sa.String(50)), nullable=True) # Licensing Information encrypted_chat_api_key = db.Column(db.String(500), nullable=True) encrypted_api_key = db.Column(db.String(500), nullable=True) - # Tuning enablers - embed_tuning = db.Column(db.Boolean, nullable=True, default=False) - rag_tuning = db.Column(db.Boolean, nullable=True, default=False) + # # Tuning enablers + # embed_tuning = db.Column(db.Boolean, nullable=True, default=False) + # rag_tuning = db.Column(db.Boolean, nullable=True, default=False) # Entitlements currency = db.Column(db.String(20), nullable=True) diff --git a/common/utils/document_utils.py b/common/utils/document_utils.py index 3ac4dbd..2014a23 100644 --- a/common/utils/document_utils.py +++ b/common/utils/document_utils.py @@ -319,9 +319,9 @@ def refresh_document_with_info(doc_id, tenant_id, api_input): response.raise_for_status() file_content = response.content - upload_file_for_version(new_doc_vers, file_content, extension, doc.tenant_id) + upload_file_for_version(new_doc_vers, file_content, extension, tenant_id) - task = current_celery.send_task('create_embeddings', args=[doc.tenant_id, new_doc_vers.id,], queue='embeddings') + task = current_celery.send_task('create_embeddings', args=[tenant_id, new_doc_vers.id,], queue='embeddings') current_app.logger.info(f'Embedding creation started for document {doc_id} on version {new_doc_vers.id} ' f'with task id: {task.id}.') diff --git a/config/logging_config.py b/config/logging_config.py index 0378ee7..6415b22 100644 --- a/config/logging_config.py +++ b/config/logging_config.py @@ -37,7 +37,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_app.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -45,7 +45,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_workers.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -53,7 +53,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_chat.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -61,7 +61,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_chat_workers.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -69,7 +69,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_api.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -77,7 +77,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_beat.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -85,7 +85,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/eveai_entitlements.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -93,7 +93,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/sqlalchemy.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -101,7 +101,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/mailman.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -109,7 +109,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/security.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -117,7 +117,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/rag_tuning.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -125,7 +125,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/embed_tuning.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, @@ -133,7 +133,7 @@ LOGGING = { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'logs/business_events.log', - 'maxBytes': 1024 * 1024 * 5, # 5MB + 'maxBytes': 1024 * 1024 * 1, # 1MB 'backupCount': 10, 'formatter': 'standard', }, diff --git a/eveai_app/templates/document/document_versions_list_view.html b/eveai_app/templates/document/document_versions_list_view.html index 9298e5e..149bafe 100644 --- a/eveai_app/templates/document/document_versions_list_view.html +++ b/eveai_app/templates/document/document_versions_list_view.html @@ -23,15 +23,23 @@ {{ render_collapsible_section('Filter', 'Filter Options', filter_form) }} - - {{ render_selectable_sortable_table( - headers=["ID", "File Type", "Processing", "Processing Start", "Processing Finish", "Processing Error"], - rows=rows, - selectable=True, - id="documentVersionsTable", - sort_by=sort_by, - sort_order=sort_order - ) }} +
+
+ + {{ render_selectable_sortable_table( + headers=["ID", "File Type", "Processing", "Processing Start", "Processing Finish", "Processing Error"], + rows=rows, + selectable=True, + id="documentVersionsTable", + sort_by=sort_by, + sort_order=sort_order + ) }} +
+ + +
+
+
{% endblock %} {% block content_footer %} diff --git a/eveai_app/templates/document/documents.html b/eveai_app/templates/document/documents.html index 89da53a..ed11e6b 100644 --- a/eveai_app/templates/document/documents.html +++ b/eveai_app/templates/document/documents.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% from 'macros.html' import render_selectable_table, render_pagination %} +{% from 'macros.html' import render_selectable_table, render_pagination, render_filter_field, render_date_filter_field, render_collapsible_section, render_selectable_sortable_table_with_dict_headers %} {% block title %}Documents{% endblock %} @@ -8,18 +8,88 @@ {% block content_class %}
{% endblock %} {% block content %} -
-
- {{ render_selectable_table(headers=["Document ID", "Name", "Valid From", "Valid To"], rows=rows, selectable=True, id="documentsTable") }} -
- - - -
-
-
+ + {% set filter_form %} +
+ {{ render_filter_field('catalog_id', 'Catalog', filter_options['catalog_id'], filters.get('catalog_id', [])) }} + {{ render_filter_field('validity', 'Validity', filter_options['validity'], filters.get('validity', [])) }} + + +
+ {% endset %} + + {{ render_collapsible_section('Filter', 'Filter Options', filter_form) }} + +
+
+ + {{ render_selectable_sortable_table_with_dict_headers( + headers=[ + {"text": "ID", "sort": "id"}, + {"text": "Name", "sort": "name"}, + {"text": "Catalog", "sort": "catalog_name"}, + {"text": "Valid From", "sort": "valid_from"}, + {"text": "Valid To", "sort": "valid_to"} + ], + rows=rows, + selectable=True, + id="documentsTable", + sort_by=sort_by, + sort_order=sort_order + ) }} +
+ + + +
+
+
{% endblock %} {% block content_footer %} {{ render_pagination(pagination, 'document_bp.documents') }} +{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/document/edit_document.html b/eveai_app/templates/document/edit_document.html index 7f8626a..5d52c8b 100644 --- a/eveai_app/templates/document/edit_document.html +++ b/eveai_app/templates/document/edit_document.html @@ -8,11 +8,17 @@ {% block content %}
{{ form.hidden_tag() }} - {% set disabled_fields = [] %} - {% set exclude_fields = [] %} - {% for field in form %} - {{ render_field(field, disabled_fields, exclude_fields) }} - {% endfor %} + {% set disabled_fields = [] %} + {% set exclude_fields = [] %} + + {{ render_field(form.name, disabled_fields, exclude_fields) }} + {{ render_field(form.valid_from, disabled_fields, exclude_fields) }} + {{ render_field(form.valid_to, disabled_fields, exclude_fields) }} + +
+ + +
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/header.html b/eveai_app/templates/header.html index 868b1be..91812d0 100644 --- a/eveai_app/templates/header.html +++ b/eveai_app/templates/header.html @@ -1,5 +1,5 @@
-
\ No newline at end of file + diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index c45beea..94e3896 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -177,6 +177,48 @@ {% endmacro %} +{% macro render_selectable_sortable_table_with_dict_headers(headers, rows, selectable, id, sort_by, sort_order) %} +
+
+ + + + {% if selectable %} + + {% endif %} + {% for header in headers %} + + {% endfor %} + + + + {% for row in rows %} + + {% if selectable %} + + {% endif %} + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
Select + {{ header['text'] }} + {% if sort_by == header['sort'] %} + {% if sort_order == 'asc' %} + + {% elif sort_order == 'desc' %} + + {% endif %} + {% else %} + + {% endif %} +
{{ cell.value }}
+
+
+{% endmacro %} + {% macro render_accordion(accordion_id, accordion_items, header_title, header_description) %}
diff --git a/eveai_app/views/document_list_view.py b/eveai_app/views/document_list_view.py new file mode 100644 index 0000000..5ad3490 --- /dev/null +++ b/eveai_app/views/document_list_view.py @@ -0,0 +1,102 @@ +from datetime import datetime +from flask import request, render_template, session +from sqlalchemy import desc, asc, or_, and_, cast, Integer +from common.models.document import Document, Catalog +from common.utils.filtered_list_view import FilteredListView +from common.utils.view_assistants import prepare_table_for_macro + + +class DocumentListView(FilteredListView): + allowed_filters = ['catalog_id', 'validity'] + allowed_sorts = ['id', 'name', 'catalog_name', 'valid_from', 'valid_to'] + + def get_query(self): + return Document.query.join(Catalog).add_columns( + Document.id, + Document.name, + Catalog.name.label('catalog_name'), + Document.valid_from, + Document.valid_to + ) + + def apply_filters(self, query): + filters = request.args.to_dict(flat=False) + + if 'catalog_id' in filters: + catalog_ids = filters['catalog_id'] + if catalog_ids: + # Convert catalog_ids to a list of integers + catalog_ids = [int(cid) for cid in catalog_ids if cid.isdigit()] + if catalog_ids: + query = query.filter(Document.catalog_id.in_(catalog_ids)) + + if 'validity' in filters: + now = datetime.utcnow().date() + if 'valid' in filters['validity']: + query = query.filter( + and_( + or_(Document.valid_from.is_(None), Document.valid_from <= now), + or_(Document.valid_to.is_(None), Document.valid_to >= now) + ) + ) + + return query + + def apply_sorting(self, query): + sort_by = request.args.get('sort_by', 'id') + sort_order = request.args.get('sort_order', 'asc') + + if sort_by in self.allowed_sorts: + if sort_by == 'catalog_name': + column = Catalog.name + else: + column = getattr(Document, sort_by) + + if sort_order == 'asc': + query = query.order_by(asc(column)) + elif sort_order == 'desc': + query = query.order_by(desc(column)) + + return query + + def get(self): + query = self.get_query() + query = self.apply_filters(query) + query = self.apply_sorting(query) + pagination = self.paginate(query) + + def format_date(date): + if isinstance(date, datetime): + return date.strftime('%Y-%m-%d') + elif isinstance(date, str): + return date + else: + return '' + + rows = [ + [ + {'value': item.id, 'class': '', 'type': 'text'}, + {'value': item.name, 'class': '', 'type': 'text'}, + {'value': item.catalog_name, 'class': '', 'type': 'text'}, + {'value': format_date(item.valid_from), 'class': '', 'type': 'text'}, + {'value': format_date(item.valid_to), 'class': '', 'type': 'text'} + ] for item in pagination.items + ] + + catalogs = Catalog.query.all() + + context = { + 'rows': rows, + 'pagination': pagination, + 'filters': request.args.to_dict(flat=False), + 'sort_by': request.args.get('sort_by', 'id'), + 'sort_order': request.args.get('sort_order', 'asc'), + 'filter_options': self.get_filter_options(catalogs) + } + return render_template(self.template, **context) + + def get_filter_options(self, catalogs): + return { + 'catalog_id': [(str(cat.id), cat.name) for cat in catalogs], + 'validity': [('valid', 'Valid'), ('all', 'All')] + } \ No newline at end of file diff --git a/eveai_app/views/document_version_list_view.py b/eveai_app/views/document_version_list_view.py index f851ba2..4a5ee5d 100644 --- a/eveai_app/views/document_version_list_view.py +++ b/eveai_app/views/document_version_list_view.py @@ -12,7 +12,7 @@ class DocumentVersionListView(FilteredListView): allowed_sorts = ['id', 'processing_started_at', 'processing_finished_at', 'processing_error'] def get_query(self): - return DocumentVersion.query.join(Document).filter(Document.tenant_id == session.get('tenant', {}).get('id')) + return DocumentVersion.query.join(Document) def apply_filters(self, query): filters = request.args.to_dict() diff --git a/eveai_app/views/document_views.py b/eveai_app/views/document_views.py index 76060e8..a45a48e 100644 --- a/eveai_app/views/document_views.py +++ b/eveai_app/views/document_views.py @@ -5,6 +5,7 @@ from babel.messages.setuptools_frontend import update_catalog from flask import request, redirect, flash, render_template, Blueprint, session, current_app from flask_security import roles_accepted, current_user from sqlalchemy import desc +from sqlalchemy.orm import aliased from werkzeug.utils import secure_filename from sqlalchemy.exc import SQLAlchemyError import requests @@ -26,6 +27,7 @@ from common.utils.middleware import mw_before_request from common.utils.celery_utils import current_celery from common.utils.nginx_utils import prefixed_url_for from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro, form_to_dict +from .document_list_view import DocumentListView from .document_version_list_view import DocumentVersionListView document_bp = Blueprint('document_bp', __name__, url_prefix='/document') @@ -286,22 +288,23 @@ def add_urls(): @document_bp.route('/documents', methods=['GET', 'POST']) @roles_accepted('Super User', 'Tenant Admin') def documents(): - page = request.args.get('page', 1, type=int) - per_page = request.args.get('per_page', 10, type=int) - - pagination = get_documents_list(page, per_page) - docs = pagination.items - - rows = prepare_table_for_macro(docs, [('id', ''), ('name', ''), ('valid_from', ''), ('valid_to', '')]) - - return render_template('document/documents.html', rows=rows, pagination=pagination) + view = DocumentListView(Document, 'document/documents.html', per_page=10) + return view.get() @document_bp.route('/handle_document_selection', methods=['POST']) @roles_accepted('Super User', 'Tenant Admin') def handle_document_selection(): document_identification = request.form['selected_row'] - doc_id = ast.literal_eval(document_identification).get('value') + if isinstance(document_identification, int) or document_identification.isdigit(): + doc_id = int(document_identification) + else: + # If it's not an integer, assume it's a string representation of a dictionary + try: + doc_id = ast.literal_eval(document_identification).get('value') + except (ValueError, AttributeError): + flash('Invalid document selection.', 'error') + return redirect(prefixed_url_for('document_bp.documents')) action = request.form['action'] @@ -323,9 +326,25 @@ def handle_document_selection(): @document_bp.route('/edit_document/', methods=['GET', 'POST']) @roles_accepted('Super User', 'Tenant Admin') def edit_document_view(document_id): - doc = Document.query.get_or_404(document_id) + # Use an alias for the Catalog to avoid column name conflicts + CatalogAlias = aliased(Catalog) + + # Query for the document and its catalog + result = db.session.query(Document, CatalogAlias.name.label('catalog_name')) \ + .join(CatalogAlias, Document.catalog_id == CatalogAlias.id) \ + .filter(Document.id == document_id) \ + .first_or_404() + + doc, catalog_name = result + form = EditDocumentForm(obj=doc) + if request.method == 'GET': + # Populate form with current values + form.name.data = doc.name + form.valid_from.data = doc.valid_from + form.valid_to.data = doc.valid_to + if form.validate_on_submit(): updated_doc, error = edit_document( document_id, @@ -341,7 +360,7 @@ def edit_document_view(document_id): else: form_validation_failed(request, form) - return render_template('document/edit_document.html', form=form, document_id=document_id) + return render_template('document/edit_document.html', form=form, document_id=document_id, catalog_name=catalog_name) @document_bp.route('/edit_document_version/', methods=['GET', 'POST']) @@ -395,7 +414,15 @@ def document_versions(document_id): @roles_accepted('Super User', 'Tenant Admin') def handle_document_version_selection(): document_version_identification = request.form['selected_row'] - doc_vers_id = ast.literal_eval(document_version_identification).get('value') + if isinstance(document_version_identification, int) or document_version_identification.isdigit(): + doc_vers_id = int(document_version_identification) + else: + # If it's not an integer, assume it's a string representation of a dictionary + try: + doc_vers_id = ast.literal_eval(document_version_identification).get('value') + except (ValueError, AttributeError): + flash('Invalid document version selection.', 'error') + return redirect(prefixed_url_for('document_bp.document_versions_list')) action = request.form['action'] @@ -447,7 +474,7 @@ def refresh_all_documents(): def refresh_document_view(document_id): - new_version, result = refresh_document(document_id) + new_version, result = refresh_document(document_id, session['tenant']['id']) if new_version: flash(f'Document refreshed. New version: {new_version.id}. Task ID: {result}', 'success') else: diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index 16b97ea..523c8bb 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -48,22 +48,6 @@ def tenant(): new_tenant = Tenant() form.populate_obj(new_tenant) - # Handle Embedding Variables - new_tenant.html_tags = [tag.strip() for tag in form.html_tags.data.split(',')] if form.html_tags.data else [] - new_tenant.html_end_tags = [tag.strip() for tag in form.html_end_tags.data.split(',')] \ - if form.html_end_tags.data else [] - new_tenant.html_included_elements = [tag.strip() for tag in form.html_included_elements.data.split(',')] \ - if form.html_included_elements.data else [] - new_tenant.html_excluded_elements = [tag.strip() for tag in form.html_excluded_elements.data.split(',')] \ - if form.html_excluded_elements.data else [] - new_tenant.html_excluded_classes = [cls.strip() for cls in form.html_excluded_classes.data.split(',')] \ - if form.html_excluded_classes.data else [] - - current_app.logger.debug(f'html_tags: {new_tenant.html_tags},' - f'html_end_tags: {new_tenant.html_end_tags},' - f'html_included_elements: {new_tenant.html_included_elements},' - f'html_excluded_elements: {new_tenant.html_excluded_elements}') - # Handle Timestamps timestamp = dt.now(tz.utc) new_tenant.created_at = timestamp @@ -105,30 +89,11 @@ def edit_tenant(tenant_id): if request.method == 'GET': # Populate the form with tenant data form.populate_obj(tenant) - if tenant.html_tags: - form.html_tags.data = ', '.join(tenant.html_tags) - if tenant.html_end_tags: - form.html_end_tags.data = ', '.join(tenant.html_end_tags) - if tenant.html_included_elements: - form.html_included_elements.data = ', '.join(tenant.html_included_elements) - if tenant.html_excluded_elements: - form.html_excluded_elements.data = ', '.join(tenant.html_excluded_elements) - if tenant.html_excluded_classes: - form.html_excluded_classes.data = ', '.join(tenant.html_excluded_classes) if form.validate_on_submit(): current_app.logger.debug(f'Updating tenant {tenant_id}') # Populate the tenant with form data form.populate_obj(tenant) - # Then handle the special fields manually - tenant.html_tags = [tag.strip() for tag in form.html_tags.data.split(',') if tag.strip()] - tenant.html_end_tags = [tag.strip() for tag in form.html_end_tags.data.split(',') if tag.strip()] - tenant.html_included_elements = [elem.strip() for elem in form.html_included_elements.data.split(',') if - elem.strip()] - tenant.html_excluded_elements = [elem.strip() for elem in form.html_excluded_elements.data.split(',') if - elem.strip()] - tenant.html_excluded_classes = [elem.strip() for elem in form.html_excluded_classes.data.split(',') if - elem.strip()] db.session.commit() flash('Tenant updated successfully.', 'success')