diff --git a/.idea/misc.xml b/.idea/misc.xml index 8136279..179cf2e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ - + + + \ No newline at end of file diff --git a/common/utils/filtered_list_view.py b/common/utils/filtered_list_view.py new file mode 100644 index 0000000..f373e1b --- /dev/null +++ b/common/utils/filtered_list_view.py @@ -0,0 +1,54 @@ +from flask import request, render_template, abort +from sqlalchemy import desc, asc + + +class FilteredListView: + def __init__(self, model, template, per_page=10): + self.model = model + self.template = template + self.per_page = per_page + + def get_query(self): + return self.model.query + + def apply_filters(self, query): + filters = request.args.get('filters', {}) + for key, value in filters.items(): + if hasattr(self.model, key): + column = getattr(self.model, key) + if value.startswith('like:'): + query = query.filter(column.like(f"%{value[5:]}%")) + else: + query = query.filter(column == value) + return query + + def apply_sorting(self, query): + sort_by = request.args.get('sort_by') + if sort_by and hasattr(self.model, sort_by): + sort_order = request.args.get('sort_order', 'asc') + column = getattr(self.model, sort_by) + if sort_order == 'desc': + query = query.order_by(desc(column)) + else: + query = query.order_by(asc(column)) + return query + + def paginate(self, query): + page = request.args.get('page', 1, type=int) + return query.paginate(page=page, per_page=self.per_page, error_out=False) + + def get(self): + query = self.get_query() + query = self.apply_filters(query) + query = self.apply_sorting(query) + pagination = self.paginate(query) + + context = { + 'items': pagination.items, + 'pagination': pagination, + 'model': self.model.__name__, + 'filters': request.args.get('filters', {}), + 'sort_by': request.args.get('sort_by'), + 'sort_order': request.args.get('sort_order', 'asc') + } + return render_template(self.template, **context) \ No newline at end of file diff --git a/common/utils/security_utils.py b/common/utils/security_utils.py index 937c213..4a3a897 100644 --- a/common/utils/security_utils.py +++ b/common/utils/security_utils.py @@ -5,9 +5,6 @@ from itsdangerous import URLSafeTimedSerializer from common.utils.nginx_utils import prefixed_url_for - - - def confirm_token(token, expiration=3600): serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) try: @@ -50,4 +47,3 @@ def send_reset_email(user): reset_url = prefixed_url_for('security_bp.reset_password', token=token, _external=True) html = render_template('email/reset_password.html', reset_url=reset_url) send_email(user.email, "Reset Your Password", html) - diff --git a/eveai_app/templates/document/document_versions_list_view.html b/eveai_app/templates/document/document_versions_list_view.html new file mode 100644 index 0000000..9298e5e --- /dev/null +++ b/eveai_app/templates/document/document_versions_list_view.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% from 'macros.html' import render_selectable_table, render_pagination, render_filter_field, render_date_filter_field, render_collapsible_section, render_selectable_sortable_table %} + +{% block title %}Documents{% endblock %} + +{% block content_title %}Document Versions{% endblock %} +{% block content_description %}View Document Versions for Tenant{% endblock %} +{% block content_class %}
{% endblock %} + +{% block content %} + + {% set filter_form %} +
+ {{ render_filter_field('file_type', 'File Type', filter_options['file_type'], filters.get('file_type')) }} + {{ render_filter_field('processing', 'Processing Status', filter_options['processing'], filters.get('processing')) }} + {{ render_filter_field('processing_error', 'Error Status', filter_options['processing_error'], filters.get('processing_error')) }} + {{ render_date_filter_field('start_date', 'Processing Start Date', filters.get('start_date')) }} + {{ render_date_filter_field('end_date', 'Processing End Date', filters.get('end_date')) }} + + +
+ {% endset %} + + {{ 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 + ) }} +{% endblock %} + +{% block content_footer %} + {{ render_pagination(pagination, 'document_bp.document_versions_list') }} +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index 41f0927..62a0598 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -135,6 +135,48 @@ {% endmacro %} +{% macro render_selectable_sortable_table(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 }} + {% if sort_by == header|lower|replace(' ', '_') %} + {% 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) %}
@@ -253,3 +295,41 @@ {% endmacro %} +{% macro render_filter_field(field_name, label, options, current_value) %} +
+ + +
+{% endmacro %} + +{% macro render_date_filter_field(field_name, label, current_value) %} +
+ + +
+{% endmacro %} + +{% macro render_collapsible_section(id, title, content) %} +
+
+
+ +
+
+
+ {{ content }} +
+
+
+
+{% endmacro %} + diff --git a/eveai_app/templates/navbar.html b/eveai_app/templates/navbar.html index d6b6801..fde2711 100644 --- a/eveai_app/templates/navbar.html +++ b/eveai_app/templates/navbar.html @@ -86,6 +86,7 @@ {'name': 'Add a list of URLs', 'url': '/document/add_urls', 'roles': ['Super User', 'Tenant Admin']}, {'name': 'Add Youtube Document' , 'url': '/document/add_youtube', 'roles': ['Super User', 'Tenant Admin']}, {'name': 'All Documents', 'url': '/document/documents', 'roles': ['Super User', 'Tenant Admin']}, + {'name': 'All Document Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Tenant Admin']}, {'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Tenant Admin']}, ]) }} {% endif %} diff --git a/eveai_app/templates/scripts.html b/eveai_app/templates/scripts.html index 732c509..c5fe94b 100644 --- a/eveai_app/templates/scripts.html +++ b/eveai_app/templates/scripts.html @@ -12,3 +12,4 @@ + diff --git a/eveai_app/views/document_version_list_view.py b/eveai_app/views/document_version_list_view.py new file mode 100644 index 0000000..f851ba2 --- /dev/null +++ b/eveai_app/views/document_version_list_view.py @@ -0,0 +1,83 @@ +from datetime import datetime +from flask import request, render_template, session +from sqlalchemy import desc, asc + +from common.models.document import DocumentVersion, Document +from common.utils.filtered_list_view import FilteredListView +from common.utils.view_assistants import prepare_table_for_macro + + +class DocumentVersionListView(FilteredListView): + allowed_filters = ['file_type', 'processing', 'processing_error'] + 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')) + + def apply_filters(self, query): + filters = request.args.to_dict() + + if filters.get('file_type'): + query = query.filter(DocumentVersion.file_type == filters['file_type']) + + if filters.get('processing'): + query = query.filter(DocumentVersion.processing == (filters['processing'] == 'true')) + + if filters.get('processing_error'): + if filters['processing_error'] == 'true': + query = query.filter(DocumentVersion.processing_error.isnot(None)) + elif filters['processing_error'] == 'false': + query = query.filter(DocumentVersion.processing_error.is_(None)) + + if filters.get('start_date'): + query = query.filter( + DocumentVersion.processing_started_at >= datetime.strptime(filters['start_date'], '%Y-%m-%d')) + + if filters.get('end_date'): + query = query.filter( + DocumentVersion.processing_finished_at <= datetime.strptime(filters['end_date'], '%Y-%m-%d')) + + 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: + column = getattr(DocumentVersion, 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) + + rows = prepare_table_for_macro( + pagination.items, + [('id', ''), ('file_type', ''), ('processing', ''), + ('processing_started_at', ''), ('processing_finished_at', ''), + ('processing_error', '')] + ) + + context = { + 'rows': rows, + 'pagination': pagination, + 'filters': request.args.to_dict(), + 'sort_by': request.args.get('sort_by', 'id'), + 'sort_order': request.args.get('sort_order', 'asc'), + 'filter_options': self.get_filter_options() + } + return render_template(self.template, **context) + + def get_filter_options(self): + return { + 'file_type': [('pdf', 'PDF'), ('docx', 'DOCX')], + 'processing': [('true', 'Processing'), ('false', 'Not Processing')], + 'processing_error': [('true', 'With Errors'), ('false', 'Without Errors')] + } \ No newline at end of file diff --git a/eveai_app/views/document_views.py b/eveai_app/views/document_views.py index e2b9611..47f84a1 100644 --- a/eveai_app/views/document_views.py +++ b/eveai_app/views/document_views.py @@ -24,6 +24,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_version_list_view import DocumentVersionListView document_bp = Blueprint('document_bp', __name__, url_prefix='/document') @@ -388,6 +389,15 @@ def handle_library_selection(): return redirect(prefixed_url_for('document_bp.library_operations')) +@document_bp.route('/document_versions_list', methods=['GET']) +@roles_accepted('Super User', 'Tenant Admin') +def document_versions_list(): + current_app.logger.debug('Getting document versions list') + view = DocumentVersionListView(DocumentVersion, 'document/document_versions_list_view.html', per_page=20) + current_app.logger.debug('Got document versions list') + return view.get() + + def refresh_all_documents(): for doc in Document.query.all(): refresh_document(doc.id)