- Changes to the list views - now using tabulator with filtering and sorting, client-side pagination, ...

- Adaptation of all list views in the app
This commit is contained in:
Josako
2025-07-14 18:58:54 +02:00
parent acad28b623
commit 000636a229
50 changed files with 2162 additions and 2174 deletions

View File

@@ -105,6 +105,12 @@ class Document(db.Model):
# Relations # Relations
versions = db.relationship('DocumentVersion', backref='document', lazy=True) versions = db.relationship('DocumentVersion', backref='document', lazy=True)
@property
def latest_version(self):
"""Returns the latest document version (the one with highest id)"""
from sqlalchemy import desc
return DocumentVersion.query.filter_by(doc_id=self.id).order_by(desc(DocumentVersion.id)).first()
def __repr__(self): def __repr__(self):
return f"<Document {self.id}: {self.name}>" return f"<Document {self.id}: {self.name}>"

View File

@@ -12,7 +12,16 @@ def prefixed_url_for(endpoint, **values):
if external: if external:
path, query, fragment = urlsplit(generated_url)[2:5] path, query, fragment = urlsplit(generated_url)[2:5]
new_path = prefix + path # Check if the prefix is already present in the path
if prefix and not path.startswith(prefix):
new_path = prefix + path
else:
new_path = path
return urlunsplit((scheme, host, new_path, query, fragment)) return urlunsplit((scheme, host, new_path, query, fragment))
else: else:
return prefix + generated_url # Check if the prefix is already present in the generated URL
if prefix and not generated_url.startswith(prefix):
return prefix + generated_url
else:
return generated_url

View File

@@ -96,9 +96,9 @@
/* Style for the table header */ /* Style for the table header */
.ordered-list-editor .tabulator-header { .ordered-list-editor .tabulator-header {
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%); /* Match JSE gradient */ background: #e1e1e1;
border-bottom: 2px solid var(--bs-secondary); /* Secondary color for border */ border-bottom: 2px solid var(--bs-secondary); /* Secondary color for border */
color: #ffffff; /* White text for better contrast on gradient */ color: #ffffff;
} }
/* Style for the headers container */ /* Style for the headers container */
@@ -108,14 +108,14 @@
/* Style for the header cells */ /* Style for the header cells */
.ordered-list-editor .tabulator-col { .ordered-list-editor .tabulator-col {
background: transparent; /* Let the header gradient show through */ background: transparent;
padding: 8px; padding: 8px;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
display: table-cell !important; /* Force display as table cell */ display: table-cell !important; /* Force display as table cell */
box-sizing: border-box !important; /* Include padding in width calculation */ box-sizing: border-box !important; /* Include padding in width calculation */
position: relative !important; /* Ensure proper positioning */ position: relative !important; /* Ensure proper positioning */
color: #ffffff; /* White text for better contrast on gradient */ color: #ffffff;
} }
/* Override any inline styles that might hide column headers */ /* Override any inline styles that might hide column headers */
@@ -135,7 +135,7 @@
word-break: break-word; /* Break words to prevent horizontal overflow */ word-break: break-word; /* Break words to prevent horizontal overflow */
font-weight: bold; font-weight: bold;
font-size: 0.85rem; /* Kleinere font grootte, dezelfde als in de rijen */ font-size: 0.85rem; /* Kleinere font grootte, dezelfde als in de rijen */
color: #ffffff; /* White text for better contrast on gradient */ color: #ffffff;
} }
/* Style for the table rows */ /* Style for the table rows */
@@ -385,7 +385,7 @@
} }
/* Extra specifieke override stijlen voor tabulator paginator en footer */ /* Extra specifieke override stijlen voor tabulator paginator en footer */
.tabulator .tabulator-footer .tabulator-paginator { .tabulator .tabulator-footer .tabulator-paginator {
background: transparent !important; /* Transparante achtergrond, want de footer heeft al gradient */ background: transparent !important;
border-top: none !important; border-top: none !important;
padding: 0 !important; padding: 0 !important;
} }
@@ -407,14 +407,14 @@
border-radius: 4px !important; border-radius: 4px !important;
border: 1px solid white !important; border: 1px solid white !important;
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
color: white !important; color: #67518c !important;
font-size: 0.85rem !important; font-size: 0.85rem !important;
font-family: inherit !important; font-family: inherit !important;
} }
/* Styling voor de paginator container */ /* Styling voor de paginator container */
.tabulator .tabulator-footer .tabulator-paginator label { .tabulator .tabulator-footer .tabulator-paginator label {
color: white !important; /* Witte tekst voor betere leesbaarheid op donkere gradient */ color: #67518c !important;
font-size: 0.85rem !important; font-size: 0.85rem !important;
font-weight: normal !important; font-weight: normal !important;
margin: 0 5px !important; margin: 0 5px !important;
@@ -465,7 +465,7 @@
} }
/* Styling voor de tabulator paginator -------------------------------------------------------------- */ /* Styling voor de tabulator paginator -------------------------------------------------------------- */
.tabulator-paginator { .tabulator-paginator {
background: linear-gradient(90deg, var(--bs-secondary) 0%, var(--bs-primary) 100%) !important; /* Omgekeerde gradient van de header */ background: #e1e1e1;
padding: 10px; padding: 10px;
border-bottom-left-radius: 0.375rem; border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem; border-bottom-right-radius: 0.375rem;
@@ -485,13 +485,13 @@
.tabulator-paginator .tabulator-page:hover { .tabulator-paginator .tabulator-page:hover {
background-color: var(--bs-secondary); background-color: var(--bs-secondary);
color: white; color: #67518c;
} }
.tabulator-paginator .tabulator-page.active, .tabulator-paginator .tabulator-page.active,
.tabulator .tabulator-footer .tabulator-page.active { .tabulator .tabulator-footer .tabulator-page.active {
background-color: var(--bs-primary) !important; background-color: var(--bs-primary) !important;
color: white !important; color: #67518c !important;
border-color: var(--bs-primary) !important; border-color: var(--bs-primary) !important;
} }
@@ -515,7 +515,7 @@
.tabulator-footer, .tabulator-footer,
.tabulator .tabulator-footer { .tabulator .tabulator-footer {
background: linear-gradient(90deg, var(--bs-secondary) 0%, var(--bs-primary) 100%) !important; /* Omgekeerde gradient van de header */ background: #e1e1e1;
border-top: 1px solid var(--bs-secondary) !important; border-top: 1px solid var(--bs-secondary) !important;
border-bottom-left-radius: 0.375rem !important; border-bottom-left-radius: 0.375rem !important;
border-bottom-right-radius: 0.375rem !important; border-bottom-right-radius: 0.375rem !important;
@@ -548,7 +548,7 @@
/* Algemene tabulator header stijlen -------------------------------------------------------------- */ /* Algemene tabulator header stijlen -------------------------------------------------------------- */
.tabulator .tabulator-header { .tabulator .tabulator-header {
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%); background: #e1e1e1;
border-bottom: 2px solid var(--bs-secondary); border-bottom: 2px solid var(--bs-secondary);
color: #ffffff; color: #ffffff;
} }
@@ -579,7 +579,7 @@
} }
.tabulator .tabulator-header .tabulator-col-title { .tabulator .tabulator-header .tabulator-col-title {
color: #ffffff; color: #67518c;
font-weight: bold; font-weight: bold;
} }
@@ -588,7 +588,7 @@
} }
.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title { .tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title {
color: #ffffff; color: #67518c;
font-size: 0.85rem; /* Kleinere font grootte, dezelfde als in de rijen */ font-size: 0.85rem; /* Kleinere font grootte, dezelfde als in de rijen */
} }
@@ -607,7 +607,7 @@
/* Tabulator List View paginator stijlen */ /* Tabulator List View paginator stijlen */
.tabulator-list-view .tabulator-footer { .tabulator-list-view .tabulator-footer {
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%); /* Match EveAI gradient */ background: #e1e1e1;
padding: 8px; padding: 8px;
border-bottom-left-radius: 0.375rem; border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem; border-bottom-right-radius: 0.375rem;
@@ -635,20 +635,20 @@
/* Stijl voor de tabel header */ /* Stijl voor de tabel header */
.tabulator-list-view .tabulator-header { .tabulator-list-view .tabulator-header {
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%); /* Match EveAI gradient */ background: #e1e1e1;
border-bottom: 2px solid var(--bs-secondary); /* Secundaire kleur voor rand */ border-bottom: 2px solid var(--bs-secondary); /* Secundaire kleur voor rand */
color: #ffffff; /* Witte tekst voor beter contrast op gradient */ color: #ffffff;
} }
/* Stijl voor de header cellen */ /* Stijl voor de header cellen */
.tabulator-list-view .tabulator-col { .tabulator-list-view .tabulator-col {
background: transparent; /* Laat de header gradient doorschijnen */ background: transparent;
padding: 8px; padding: 8px;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
box-sizing: border-box !important; /* Padding meenemen in breedte berekening */ box-sizing: border-box !important; /* Padding meenemen in breedte berekening */
position: relative !important; /* Juiste positionering verzekeren */ position: relative !important; /* Juiste positionering verzekeren */
color: #ffffff; /* Witte tekst voor beter contrast op gradient */ color: #ffffff;
} }
/* Voorkom kleurverandering bij hover over kolomkoppen */ /* Voorkom kleurverandering bij hover over kolomkoppen */
@@ -662,7 +662,7 @@
word-break: break-word; /* Breek woorden om horizontale overflow te voorkomen */ word-break: break-word; /* Breek woorden om horizontale overflow te voorkomen */
font-weight: bold; font-weight: bold;
font-size: 0.85rem; /* Kleinere font grootte, dezelfde als in de rijen */ font-size: 0.85rem; /* Kleinere font grootte, dezelfde als in de rijen */
color: #ffffff; /* Witte tekst voor beter contrast op gradient */ color: #ffffff;
} }
/* Stijl voor de tabel rijen */ /* Stijl voor de tabel rijen */

View File

@@ -0,0 +1,183 @@
/**
* EveAI List View Component
* JavaScript functionaliteit voor het beheren van lijst-weergaven
*/
// Namespace aanmaken als deze nog niet bestaat
if (typeof window.EveAI === 'undefined') {
window.EveAI = {};
}
// List View namespace
window.EveAI.ListView = {
// Opslag voor lijst-view instanties
instances: {},
/**
* Initialiseer een Tabulator lijst-view
* @param {string} elementId - ID van het HTML element
* @param {object} config - Configuratie object voor Tabulator
* @returns {Tabulator} - Tabulator instantie
*/
initialize: function(elementId, config) {
// Combineer standaard configuratie met aangepaste configuratie
const defaultConfig = {
height: 600,
layout: "fitColumns",
selectable: true,
movableColumns: true,
pagination: "local",
paginationSize: 15,
paginationSizeSelector: [10, 15, 20, 50, 100],
};
const tableConfig = {...defaultConfig, ...config};
// Voeg rij selectie event toe
tableConfig.rowSelectionChanged = (data, rows) => {
console.log("Rij selectie gewijzigd:", rows.length, "rijen geselecteerd");
// Update de geselecteerde rij in onze instance
if (this.instances[elementId]) {
this.instances[elementId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
this.updateActionButtons(elementId);
}
};
// Initialiseer de Tabulator
try {
const table = new Tabulator(`#${elementId}`, tableConfig);
// Bewaar de instance
this.instances[elementId] = {
table: table,
config: config || {},
selectedRow: null
};
// Bij initialisatie, update de knoppen (standaard inactief voor requiresSelection=true)
setTimeout(() => {
this.updateActionButtons(elementId);
}, 0);
return table;
} catch (error) {
console.error(`Fout bij het initialiseren van Tabulator voor ${elementId}:`, error);
return null;
}
},
/**
* Afhandelen van actieknoppen
* @param {string} action - Actie identifier
* @param {boolean} requiresSelection - Of de actie een selectie vereist
* @param {string} tableId - ID van de tabel
* @returns {boolean} - Succes indicator
*/
/**
* Update actieknoppen op basis van geselecteerde rij
* @param {string} tableId - ID van de tabel
*/
updateActionButtons: function(tableId) {
const instance = this.instances[tableId];
if (!instance) return;
const container = document.getElementById(tableId);
if (!container) return;
const buttons = container.parentElement.querySelectorAll('button[onclick*="handleListViewAction"]');
buttons.forEach(button => {
// Parse de onclick attribuut om de actiewaarde te krijgen
const onclickAttr = button.getAttribute('onclick');
const match = onclickAttr.match(/handleListViewAction\('([^']+)'/);
if (match) {
const actionValue = match[1];
// Vind de actie in de configuratie
const action = instance.config.actions.find(a => a.value === actionValue);
if (action && action.requiresSelection === true) {
// Schakel de knop in/uit op basis van selectie
button.disabled = !instance.selectedRow;
}
}
});
// Update de verborgen input met geselecteerde rij data
this._updateSelectedRowInput(tableId);
},
/**
* Update de verborgen input met geselecteerde rij gegevens
* @param {string} tableId - ID van de tabel
* @private
*/
_updateSelectedRowInput: function(tableId) {
const instance = this.instances[tableId];
const hiddenInput = document.getElementById(`${tableId}-selected-row`);
if (hiddenInput && instance && instance.selectedRow) {
// Bewaar de geselecteerde rij-ID
hiddenInput.value = JSON.stringify({value: instance.selectedRow.id});
} else if (hiddenInput) {
hiddenInput.value = '';
}
},
handleAction: function(action, requiresSelection, tableId) {
const selectedRowInput = document.getElementById(`${tableId}-selected-row`);
const actionInput = document.getElementById(`${tableId}-action`);
const table = Tabulator.findTable(`#${tableId}`)[0];
if (!table) {
console.error(`Tabulator tabel met ID ${tableId} niet gevonden`);
return false;
}
// Als de actie een selectie vereist, controleer of er een rij is geselecteerd
if (requiresSelection) {
const selectedRows = table.getSelectedRows();
if (selectedRows.length === 0) {
alert('Selecteer a.u.b. eerst een item uit de lijst.');
return false;
}
// Haal de data van de geselecteerde rij op en sla deze op
const rowData = selectedRows[0].getData();
selectedRowInput.value = JSON.stringify({ value: rowData.id });
// Update de instance als deze bestaat
if (this.instances[tableId]) {
this.instances[tableId].selectedRow = rowData;
}
}
// Stel de actie in en verstuur het formulier
actionInput.value = action;
// Zoek het juiste formulier en verstuur het
const form = document.getElementById(`${tableId}-form`) ||
table.element.closest('form');
if (form) {
form.submit();
return true;
} else {
console.error(`Geen formulier gevonden voor tabel ${tableId}`);
return false;
}
}
};
// Functie om beschikbaar te maken in templates
function handleListViewAction(action, requiresSelection) {
// Vind het tableId op basis van de button die is aangeklikt
const target = event?.target || event?.srcElement;
// Vind het formulier en tableId op basis daarvan
const form = target ? target.closest('form') : null;
const tableId = form ? form.id.replace('-form', '') : 'unknown_table';
return window.EveAI.ListView.handleAction(action, requiresSelection, tableId);
}
console.log('EveAI List View component geladen');

View File

@@ -0,0 +1,83 @@
/**
* EveAI Tabulator Setup
* Standaard Tabulator configuratie voor consistente tabelweergaven
*/
document.addEventListener('DOMContentLoaded', function() {
// Controleer of Tabulator is geladen
if (typeof Tabulator !== 'function') {
console.warn('Tabulator is niet geladen - overslaan van initialisatie');
return;
}
// Zorg ervoor dat de modules correct zijn gedefinieerd
if (!Tabulator.modules) {
Tabulator.modules = {};
}
if (!Tabulator.modules.format) {
Tabulator.modules.format = { formatters: {} };
} else if (!Tabulator.modules.format.formatters) {
Tabulator.modules.format.formatters = {};
}
// Registreer algemene Tabulator opties en formatters
// Gebruik rechtstreekse toewijzing i.p.v. extendModule indien deze functie niet beschikbaar is
if (typeof Tabulator.extendModule === 'function') {
try {
Tabulator.extendModule("format", "formatters", {
// Aangepaste formatter voor boolean waarden met mooie iconen
"boolean": function(cell, formatterParams){
const value = cell.getValue();
if (value === true || value === 'true' || value === 1 || value === '1') {
return '<i class="fas fa-check text-success"></i>';
} else if (value === false || value === 'false' || value === 0 || value === '0') {
return '<i class="fas fa-times text-danger"></i>';
}
return ''; // Geef lege string terug voor null/undefined waarden
}
});
} catch (e) {
console.warn('Fout bij extendModule:', e);
// Fallback: rechtstreeks formatters toevoegen
Tabulator.modules.format.formatters.boolean = function(cell, formatterParams){
const value = cell.getValue();
if (value === true || value === 'true' || value === 1 || value === '1') {
return '<i class="fas fa-check text-success"></i>';
} else if (value === false || value === 'false' || value === 0 || value === '0') {
return '<i class="fas fa-times text-danger"></i>';
}
return '';
};
}
} else {
// Directe toewijzing als extendModule niet beschikbaar is
Tabulator.modules.format.formatters.boolean = function(cell, formatterParams){
const value = cell.getValue();
if (value === true || value === 'true' || value === 1 || value === '1') {
return '<i class="fas fa-check text-success"></i>';
} else if (value === false || value === 'false' || value === 0 || value === '0') {
return '<i class="fas fa-times text-danger"></i>';
}
return '';
};
}
// Definieer standaard tabelconfiguratie
Tabulator.defaultOptions = {
...Tabulator.defaultOptions,
layout: "fitColumns",
responsiveLayout: false,
pagination: "local",
paginationSize: 25,
paginationSizeSelector: [10, 25, 50, 100],
movableColumns: true,
tooltips: false,
placeholder: "No Data Available",
// Verbeterde virtuele DOM-instellingen voor betere prestaties
renderVerticalBuffer: 20,
virtualDomBuffer: 80
};
console.log('EveAI Tabulator Setup successfully loaded');
});

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" itemscope itemtype="http://schema.org/WebPage"> <html lang="en" itemscope itemtype="http://schema.org/WebPage">
{% include 'head.html' %} {% include 'head.html' %}
<body class="presentation-page bg-gray-200"> <body class="presentation-page bg-gray-200">

View File

@@ -1,27 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Documents{% endblock %}
{% block content_title %}Catalogs{% endblock %}
{% block content_description %}View Catalogs for Tenant{% 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('document_bp.handle_catalog_selection') }}" id="catalogsForm">
{{ render_selectable_table(headers=["Catalog ID", "Name", "Type"], rows=rows, selectable=True, id="catalogsTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="set_session_catalog" class="btn btn-primary" onclick="return validateTableSelection('catalogsForm')">Set Session Catalog</button>
<button type="submit" name="action" value="edit_catalog" class="btn btn-primary" onclick="return validateTableSelection('catalogsForm')">Edit Catalog</button>
</div>
<button type="submit" name="action" value="create_catalog" class="btn btn-success">Register Catalog</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.catalogs') }}
{% endblock %}

View File

@@ -1,27 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Document Versions{% endblock %}
{% block content_title %}Document Versions{% endblock %}
{% block content_description %}View Versions for Document <b>{{ document }}</b>{% 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('document_bp.handle_document_version_selection') }}" id="documentVersionsForm">
{{ render_selectable_table(headers=["ID", "File Type", "File Size", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.documents') }}
{% endblock %}

View File

@@ -1,94 +0,0 @@
{% 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 %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<!-- Filter Form -->
{% set filter_form %}
<form method="GET" action="{{ url_for('document_bp.document_versions_list') }}">
{{ 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')) }}
<button type="submit" class="btn btn-primary">Apply Filters</button>
</form>
{% endset %}
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
<div class="form-group mt-3">
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}" id="documentVersionsForm">
<!-- Document Versions Table -->
{{ 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
) }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.document_versions_list') }}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('documentVersionsTable');
const headers = table.querySelectorAll('th.sortable');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
let sortOrder = 'asc';
if (this.querySelector('.fa-sort-up')) {
sortOrder = 'desc';
} else if (this.querySelector('.fa-sort-down')) {
sortOrder = 'none';
}
window.location.href = updateQueryStringParameter(window.location.href, 'sort_by', sortBy);
window.location.href = updateQueryStringParameter(window.location.href, 'sort_order', sortOrder);
});
});
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
table.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
var selectedRow = event.target.closest('tr');
var documentVersionId = selectedRow.cells[1].textContent;
console.log('Selected Document Version ID:', documentVersionId);
}
});
});
</script>
{% endblock %}

View File

@@ -1,95 +0,0 @@
{% 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_with_dict_headers %}
{% block title %}Documents{% endblock %}
{% block content_title %}Documents{% endblock %}
{% block content_description %}View Documents for Catalog <b>{% if session.catalog_name %}{{ session.catalog_name }}{% else %}No Catalog{% endif %}</b>{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<!-- Filter Form -->
{% set filter_form %}
<form method="GET" action="{{ url_for('document_bp.documents') }}">
{{ render_filter_field('validity', 'Validity', filter_options['validity'], filters.get('validity', [])) }}
<button type="submit" class="btn btn-primary">Apply Filters</button>
</form>
{% endset %}
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
<div class="form-group mt-3">
<form method="POST" action="{{ url_for('document_bp.handle_document_selection') }}" id="documentsForm">
<!-- Documents Table -->
{{ render_selectable_sortable_table_with_dict_headers(
headers=[
{"text": "ID", "sort": "id"},
{"text": "Name", "sort": "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
) }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_document" class="btn btn-primary" onclick="return validateTableSelection('documentsForm')">Edit Document</button>
<button type="submit" name="action" value="document_versions" class="btn btn-secondary" onclick="return validateTableSelection('documentsForm')">Show Document Versions</button>
<button type="submit" name="action" value="refresh_document" class="btn btn-secondary" onclick="return validateTableSelection('documentsForm')">Refresh Document (new version)</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.documents') }}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('documentsTable');
const headers = table.querySelectorAll('th.sortable');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
let sortOrder = 'asc';
if (this.querySelector('.fa-sort-up')) {
sortOrder = 'desc';
} else if (this.querySelector('.fa-sort-down')) {
sortOrder = 'none';
}
window.location.href = updateQueryStringParameter(window.location.href, 'sort_by', sortBy);
window.location.href = updateQueryStringParameter(window.location.href, 'sort_order', sortOrder);
});
});
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
table.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
var selectedRow = event.target.closest('tr');
var documentId = selectedRow.cells[1].textContent;
console.log('Selected Document ID:', documentId);
}
});
});
</script>
{% endblock %}

View File

@@ -1,114 +0,0 @@
{% 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_with_dict_headers %}
{% block title %}Complete Document Overview{% endblock %}
{% block content_title %}Complete Document Overview{% endblock %}
{% block content_description %}View Documents with Latest Version for Catalog <b>{% if session.catalog_name %}{{ session.catalog_name }}{% else %}No Catalog{% endif %}</b>{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<!-- Filter Form -->
{% set filter_form %}
<form method="GET" action="{{ url_for('document_bp.full_documents') }}">
{{ render_filter_field('validity', 'Validity', filter_options['validity'], filters.get('validity', [])) }}
{{ 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', [])) }}
<button type="submit" class="btn btn-primary">Apply Filters</button>
</form>
{% endset %}
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
<div class="form-group mt-3">
<form method="POST" action="{{ url_for('document_bp.handle_full_document_selection') }}" id="fullDocumentsForm">
<!-- Hidden field to store the selected version ID -->
<input type="hidden" name="version_id" id="selectedVersionId" value="">
<!-- Documents Table -->
{{ render_selectable_sortable_table_with_dict_headers(
headers=[
{"text": "Document ID", "sort": "id"},
{"text": "Name", "sort": "name"},
{"text": "Valid From", "sort": "valid_from"},
{"text": "Valid To", "sort": "valid_to"},
{"text": "Version ID", "sort": ""},
{"text": "File Type", "sort": "file_type"},
{"text": "Processing", "sort": "processing"},
{"text": "Error", "sort": "processing_error"}
],
rows=rows,
selectable=True,
id="fullDocumentsTable",
sort_by=sort_by,
sort_order=sort_order
) }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_document" class="btn btn-primary" onclick="return validateTableSelection('fullDocumentsForm')">Edit Document</button>
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('fullDocumentsForm')">Edit Document Version</button>
<button type="submit" name="action" value="document_versions" class="btn btn-secondary" onclick="return validateTableSelection('fullDocumentsForm')">Show All Document Versions</button>
<button type="submit" name="action" value="refresh_document" class="btn btn-secondary" onclick="return validateTableSelection('fullDocumentsForm')">Refresh Document (new version)</button>
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('fullDocumentsForm')">View Processed Document</button>
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('fullDocumentsForm')">Process Document Version</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.full_documents') }}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('fullDocumentsTable');
const headers = table.querySelectorAll('th.sortable');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
let sortOrder = 'asc';
if (this.querySelector('.fa-sort-up')) {
sortOrder = 'desc';
} else if (this.querySelector('.fa-sort-down')) {
sortOrder = 'none';
}
window.location.href = updateQueryStringParameter(window.location.href, 'sort_by', sortBy);
window.location.href = updateQueryStringParameter(window.location.href, 'sort_order', sortOrder);
});
});
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
table.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
var selectedRow = event.target.closest('tr');
var documentId = selectedRow.cells[1].textContent;
var versionId = selectedRow.cells[5].textContent;
console.log('Selected Document ID:', documentId, 'Version ID:', versionId);
// Update the hidden field with the version ID
document.getElementById('selectedVersionId').value = versionId;
}
});
});
</script>
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Processors{% endblock %}
{% block content_title %}Processors{% endblock %}
{% block content_description %}View Processors for Catalog <b>{% if session.catalog_name %}{{ session.catalog_name }}{% else %}No Catalog{% endif %}</b>{% 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('document_bp.handle_processor_selection') }}" id="processorsForm">
{{ render_selectable_table(headers=["Processor ID", "Name", "Type", "Active"], 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_processor" class="btn btn-primary" onclick="return validateTableSelection('processorsForm')">Edit Processor</button>
</div>
<button type="submit" name="action" value="create_processor" class="btn btn-success">Register Processor</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.processors') }}
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Retrievers{% endblock %}
{% block content_title %}Retrievers{% endblock %}
{% block content_description %}View Retrievers for Catalog <b>{% if session.catalog_name %}{{ session.catalog_name }}{% else %}No Catalog{% endif %}</b>{% 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('document_bp.handle_retriever_selection') }}" id="retrieversForm">
{{ render_selectable_table(headers=["Retriever 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_retriever" class="btn btn-primary" onclick="return validateTableSelection('retrieversForm')">Edit Retriever</button>
</div>
<button type="submit" name="action" value="create_retriever" class="btn btn-success">Register Retriever</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.retrievers') }}
{% endblock %}

View File

@@ -1,36 +0,0 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table, render_pagination, render_field %}
{% block title %}License Tier Selection{% endblock %}
{% block content_title %}Select a License Tier{% endblock %}
{% block content_description %}Select a License Tier to continue{% endblock %}
{% block content %}
<!-- License Tier Selection Form -->
<form method="POST" action="{{ url_for('entitlements_bp.handle_license_tier_selection') }}" id="licenseTiersForm">
{{ 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>
{% 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>
{% 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>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% endblock %}

View File

@@ -1,28 +0,0 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Licenses{% endblock %}
{% block content_title %}View Licenses{% endblock %}
{% block content_description %}View Licenses{% endblock %}
{% block content %}
<form action="{{ url_for('entitlements_bp.handle_license_selection') }}" method="POST" id="licensesForm">
{{ render_selectable_table(headers=["License ID", "Name", "Start Date", "Nr of Periods", "Active"], rows=rows, selectable=True, id="licensesTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_license" class="btn btn-primary" onclick="return validateTableSelection('licensesForm')">Edit License</button>
<button type="submit" name="action" value="view_periods" class="btn btn-info" onclick="return validateTableSelection('licensesForm')">View Periods</button>
</div>
<!-- Additional buttons can be added here for other actions -->
</div>
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'entitlements_bp.view_licenses') }}
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@@ -1,301 +1,493 @@
<script type="module"> <style>
window.EveAI = window.EveAI || {}; button.disabled, button:disabled {
window.EveAI.ListView = { opacity: 0.65;
instances: {}, cursor: not-allowed;
pointer-events: none;
}
</style>
<div class="container">
<input type="hidden" id="{{ table_id }}-selected-row" name="selected_row" value="">
<input type="hidden" id="{{ table_id }}-action" name="action" value="">
initialize: function(containerId, options = {}) { <div id="{{ table_id }}" class="tabulator-list-view"></div>
const container = document.getElementById(containerId);
if (!container) { <div class="row mt-3">
console.error(`Container ${containerId} not found`); {% set right_actions = actions|selectattr('position', 'equalto', 'right')|list %}
return null; <div class="{% if right_actions %}col{% else %}col-12{% endif %}">
{% for action in actions if action.position != 'right' %}
<button type="button"
onclick="handleListViewAction('{{ action.value }}', {{ action.requiresSelection|tojson }})"
class="btn {{ action.class|default('btn-primary') }} me-2 {% if action.requiresSelection %}requires-selection{% endif %}"
{% if action.requiresSelection %}disabled{% endif %}>
{{ action.text }}
</button>
{% endfor %}
</div>
{% if right_actions %}
<div class="col-auto text-end">
{% for action in actions if action.position == 'right' %}
<button type="button"
onclick="handleListViewAction('{{ action.value }}', {{ action.requiresSelection|tojson }})"
class="btn {{ action.class|default('btn-primary') }} ms-2 {% if action.requiresSelection %}requires-selection{% endif %}"
{% if action.requiresSelection %}disabled{% endif %}>
{{ action.text }}
</button>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
// Zorg ervoor dat de ListView-module beschikbaar is
window.EveAI = window.EveAI || {};
window.EveAI.ListView = window.EveAI.ListView || {};
/* ListView-module functionaliteit:
* - Initialiseert een Tabulator tabel met de gegeven configuratie
* - Beheert rijselectie (maximaal 1 rij tegelijk)
* - Zorgt ervoor dat knoppen met requiresSelection=true inactief zijn wanneer er geen rij is geselecteerd
* - Ondersteunt verschillende Tabulator versies (5.x en 6.x)
*/
// Direct alle buttons met requiresSelection uitschakelen bij laden pagina
document.querySelectorAll('button[onclick*="handleListViewAction"]').forEach(button => {
const onclickAttr = button.getAttribute('onclick');
const match = onclickAttr.match(/handleListViewAction\('([^']+)',\s*(true|false)\)/i);
if (match && match[2].toLowerCase() === 'true') {
button.disabled = true;
button.classList.add('disabled');
} }
});
// Default configuration // Voeg de benodigde functies toe als ze nog niet bestaan
const config = { if (!window.EveAI.ListView.initialize) {
data: [], window.EveAI.ListView.instances = {};
columns: [],
initialSort: [],
initialFilters: [],
filterableColumns: [],
selectable: true,
actions: [],
usePagination: true, // Nieuwe optie om paginering aan/uit te zetten
tableHeight: 700, // Standaard tabelhoogte als deze niet wordt gespecificeerd
...options
};
// Check if Tabulator is available // Initialize functie
if (typeof window.Tabulator !== 'function') { window.EveAI.ListView.initialize = function(containerId, options = {}) {
console.error('Tabulator not loaded (window.Tabulator missing).'); const container = document.getElementById(containerId);
container.innerHTML = `<div class="alert alert-danger"> if (!container) {
<strong>Error:</strong> Tabulator not loaded console.error(`Container ${containerId} not found`);
</div>`; return null;
return null; }
}
try { // Default configuration
const config = {
data: [],
columns: [],
initialSort: [],
initialFilters: [],
filterableColumns: [],
selectable: true,
actions: [],
usePagination: true,
tableHeight: 700,
...options
};
// Create Tabulator table // Check if Tabulator is available
const tableContainer = document.createElement('div'); if (typeof window.Tabulator !== 'function') {
tableContainer.className = 'tabulator-container tabulator-list-view mt-3'; console.error('Tabulator not loaded (window.Tabulator missing).');
container.appendChild(tableContainer); container.innerHTML = `<div class="alert alert-danger">
<strong>Error:</strong> Tabulator not loaded
</div>`;
return null;
}
// Basisinstellingen voor tabel configuratie try {
const tableConfig = { // Create Tabulator table
data: config.data, const tableContainer = document.createElement('div');
columns: this._buildColumns(config.columns, config.selectable), tableContainer.className = 'tabulator-container tabulator-list-view mt-3';
layout: "fitColumns", container.appendChild(tableContainer);
// Conditionele paginatie of progressieve lading // Basisinstellingen voor tabel configuratie
...(config.usePagination ? { const tableConfig = {
pagination: "local", data: config.data,
paginationSize: 25, columns: this._buildColumns(config.columns, config.selectable),
paginationSizeSelector: [10, 25, 50, 100] layout: "fitColumns",
} : {
progressiveLoad: "scroll" // Gebruik progressieve lading voor grote datasets wanneer paginatie uitstaat
}),
// Gemeenschappelijke configuratie voor beide modi // Conditionele paginatie of progressieve lading
movableColumns: true, ...(config.usePagination ? {
resizableRows: false, // Schakel dit uit om prestatieproblemen te voorkomen pagination: "local",
initialSort: config.initialSort, paginationSize: 25,
initialFilter: config.initialFilters, paginationSizeSelector: [10, 25, 50, 100]
selectable: 1, // Beperk tot maximaal 1 rij selectie } : {
selectableRangeMode: "click", progressiveLoad: "scroll" // Gebruik progressieve lading voor grote datasets wanneer paginatie uitstaat
selectableCheck: function() { return true; }, // Zorg ervoor dat alle cellen selecteerbaar zijn }),
rowClick: function(e, row) {
// Selecteer de rij bij klikken op willekeurige cel // Gemeenschappelijke configuratie voor beide modi
movableColumns: true,
resizableRows: false, // Schakel dit uit om prestatieproblemen te voorkomen
initialSort: config.initialSort,
initialFilter: config.initialFilters,
selectable: config.selectable ? 1 : false, // Beperk tot maximaal 1 rij selectie of schakel uit
selectableRangeMode: "click",
selectableCheck: function() { return true; }, // Zorg ervoor dat alle cellen selecteerbaar zijn
rowClick: function(e, row) {
// Selecteer de rij bij klikken op willekeurige cel
if (config.selectable) {
console.log('Row clicked!', row.getData());
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen als extra maatregel
const instance = EveAI.ListView.instances[containerId];
if (instance) {
// Expliciet de geselecteerde rij instellen
instance.selectedRow = row.getData();
// Direct UI updaten voor betere gebruikerservaring
console.log('Updating buttons from rowClick handler');
EveAI.ListView._updateActionButtons(containerId);
}
}
},
cellClick: function(e, cell) {
// Gebruik ook cellClick om ervoor te zorgen dat klikken op een cel de rij selecteert
if (config.selectable && !e.target.matches('a, input, button, select, .tabulator-cell-editing')) {
console.log('Cell clicked!', cell.getData());
const row = cell.getRow();
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen als extra maatregel
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
// Direct forceer een update van de buttons - ook als de rowSelectionChanged niet triggert
console.log('Updating buttons from cellClick handler');
setTimeout(() => EveAI.ListView._updateActionButtons(containerId), 0);
}
}
},
cellTap: function(e, cell) {
// Extra handler voor touch devices
if (config.selectable && !e.target.matches('a, input, button, select, .tabulator-cell-editing')) {
console.log('Cell tapped!', cell.getData());
const row = cell.getRow();
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
}
},
rowSelectionChanged: function(data, rows) {
console.log("Aantal geselecteerde rijen:", rows.length);
console.log("Geselecteerde rijen:", rows.map(r => r.getData()));
// Update de geselecteerde rij (enkelvoud)
const selectedData = rows.length > 0 ? rows[0].getData() : null;
console.log('rowSelectionChanged met data:', selectedData);
// Controleer of de instance bestaat
if (EveAI.ListView.instances[containerId]) {
EveAI.ListView.instances[containerId].selectedRow = selectedData;
// Met kleine timeout om zeker te zijn dat alles is bijgewerkt
setTimeout(() => {
console.log('Updating buttons from rowSelectionChanged handler');
EveAI.ListView._updateActionButtons(containerId);
}, 10);
} else {
console.warn(`Instance voor ${containerId} niet gevonden in rowSelectionChanged`);
}
},
rowFormatter: function(row) {
// Voeg cursor-style toe om aan te geven dat rijen klikbaar zijn
if (config.selectable) {
row.getElement().style.cursor = 'pointer';
}
},
placeholder: "No data available",
tooltips: false, // Schakel tooltips uit voor betere prestaties
responsiveLayout: false, // Schakel responsiveLayout uit om recursieve problemen te voorkomen
renderVerticalBuffer: 20, // Optimaliseer virtuele rendering
virtualDomBuffer: 80, // Optimaliseer virtuele DOM buffer
height: config.tableHeight // Gebruik de geconfigureerde tabel hoogte
};
// Maak Tabulator instantie met de geconfigureerde instellingen
const table = new Tabulator(tableContainer, tableConfig);
// Store instance - maar maak GEEN action buttons meer in JavaScript
this.instances[containerId] = {
table: table,
config: config,
selectedRow: null
};
// Direct bij initialisatie: zorg ervoor dat knoppen met requiresSelection=True inactief zijn
// Gebruik een groter timeout om te zorgen dat alle componenten zijn geladen
setTimeout(() => {
console.log('Initialiseren van action buttons...');
this._updateActionButtons(containerId);
}, 100);
// Registreer events met de Tabulator API voor betere compatibiliteit
table.on("rowClick", function(e, row){
console.log("Tabulator API: Row clicked!", row.getData());
if (config.selectable) { if (config.selectable) {
console.log('Row clicked!', row.getData()); // Stop event propagation om te voorkomen dat andere handlers interfereren
e.stopPropagation();
row.getTable().deselectRow(); row.getTable().deselectRow();
row.select(); row.select();
// Handmatig de rowSelectionChanged event aanroepen als extra maatregel
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
} }
}, });
cellClick: function(e, cell) {
// Gebruik ook cellClick om ervoor te zorgen dat klikken op een cel de rij selecteert // Row selection event met de Tabulator API
if (config.selectable && !e.target.matches('a, input, button, select, .tabulator-cell-editing')) { table.on("rowSelectionChanged", function(data, rows){
console.log('Cell clicked!', cell.getData()); console.log("Tabulator API: Aantal geselecteerde rijen:", rows.length);
const row = cell.getRow(); console.log("Tabulator API: Geselecteerde rijen:", rows.map(r => r.getData()));
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen als extra maatregel
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
}
},
cellTap: function(e, cell) {
// Extra handler voor touch devices
if (config.selectable && !e.target.matches('a, input, button, select, .tabulator-cell-editing')) {
console.log('Cell tapped!', cell.getData());
const row = cell.getRow();
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
}
},
rowSelectionChanged: function(data, rows) {
console.log("Aantal geselecteerde rijen:", rows.length);
console.log("Geselecteerde rijen:", rows.map(r => r.getData()));
// Update de geselecteerde rij (enkelvoud) // Update de geselecteerde rij (enkelvoud)
EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null; EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
EveAI.ListView._updateActionButtons(containerId); EveAI.ListView._updateActionButtons(containerId);
}, });
rowFormatter: function(row) {
// Voeg cursor-style toe om aan te geven dat rijen klikbaar zijn return table;
if (config.selectable) { } catch (error) {
row.getElement().style.cursor = 'pointer'; console.error('Error initializing ListView:', error);
container.innerHTML = `<div class="alert alert-danger">
<strong>Error loading data:</strong> ${error.message}
</div>`;
return null;
}
};
// Bouw kolommen functie
window.EveAI.ListView._buildColumns = function(columns, selectable) {
const tabulatorColumns = [];
// Detecteer Tabulator versie (6.x heeft een andere aanpak voor column definities)
const isTabulator6Plus = typeof Tabulator === 'function' && Tabulator.version &&
parseInt(Tabulator.version.split('.')[0]) >= 6;
console.log('Tabulator versie detectie:', isTabulator6Plus ? '6+' : 'Pre-6');
// Add data columns
columns.forEach(col => {
// Maak een nieuwe schone kolom met alleen geldige Tabulator-eigenschappen
// voor de gedetecteerde versie
const column = {
title: col.title,
field: col.field,
width: col.width,
hozAlign: col.hozAlign || 'left',
vertAlign: col.vertAlign || 'middle'
};
// Voeg sorteren toe volgens de juiste manier per versie
if (isTabulator6Plus) {
// Tabulator 6+ gebruikt sorteerbaarheid via kolom-opties
column.sorter = col.field === 'id' || col.field.endsWith('_id') || col.type === 'number' ?
'number' : 'string';
column.sorterParams = {};
column.sorteringActive = col.sortable !== false;
} else {
// Pre-6 versies gebruiken headerSort
column.headerSort = col.sortable !== false;
// Zorg voor juiste numerieke sortering voor ID velden
if (col.field === 'id' || col.field.endsWith('_id') || col.type === 'number') {
column.sorter = 'number';
} }
}, }
placeholder: "No data available",
tooltips: false, // Schakel tooltips uit voor betere prestaties
responsiveLayout: false, // Schakel responsiveLayout uit om recursieve problemen te voorkomen
renderVerticalBuffer: 20, // Optimaliseer virtuele rendering
virtualDomBuffer: 80, // Optimaliseer virtuele DOM buffer
height: config.tableHeight // Gebruik de geconfigureerde tabel hoogte
};
// Maak Tabulator instantie met de geconfigureerde instellingen // Voeg formattering toe volgens de juiste manier per versie
const table = new Tabulator(tableContainer, tableConfig); if (col.formatter) {
if (isTabulator6Plus) {
column.formatterParams = { formatter: col.formatter };
} else {
column.formatter = col.formatter;
}
}
// Create action buttons // Voeg filtering toe volgens de juiste manier per versie
if (config.actions.length > 0) { if (isTabulator6Plus) {
this._createActionButtons(container, config.actions, table); // Tabulator 6+ gebruikt verschillende eigenschappen voor filtering
column.filterable = col.filterable !== false;
column.filterParams = {};
if (col.type === 'date') {
column.filterParams.type = 'date';
} else if (col.type === 'number') {
column.filterParams.type = 'number';
} else if (col.filterValues) {
column.filterParams.values = col.filterValues;
}
} else {
// Pre-6 versies gebruiken headerFilter
column.headerFilter = col.filterable !== false ? 'input' : false;
// Set appropriate header filter based on the data type
if (col.type === 'date') {
column.headerFilter = "input";
column.headerFilterParams = {type: "date"};
} else if (col.type === 'number') {
column.headerFilter = "number";
} else if (col.filterValues) {
column.headerFilter = "select";
column.headerFilterParams = {values: col.filterValues};
}
}
tabulatorColumns.push(column);
});
return tabulatorColumns;
};
// Update action buttons functie
window.EveAI.ListView._updateActionButtons = function(containerId) {
const instance = this.instances[containerId];
if (!instance) {
console.warn(`Kan buttons niet updaten: geen instance voor ${containerId}`);
return;
} }
// Store instance // Zoek buttons in de volledige formulier context (niet alleen de container)
this.instances[containerId] = { const form = document.getElementById(`${containerId}-form`) || document.querySelector('form');
table: table, const buttons = form ? form.querySelectorAll('button[onclick*="handleListViewAction"]') :
config: config, document.querySelectorAll('button[onclick*="handleListViewAction"]');
selectedRow: null
};
// Registreer events met de Tabulator API voor betere compatibiliteit console.log(`Updating buttons voor ${containerId}, ${buttons.length} buttons gevonden, selectedRow:`, instance.selectedRow);
table.on("rowClick", function(e, row){
console.log("Tabulator API: Row clicked!", row.getData()); buttons.forEach(button => {
if (config.selectable) { // Parse the onclick attribute to get the action value and requiresSelection parameter
// Stop event propagation om te voorkomen dat andere handlers interfereren const onclickAttr = button.getAttribute('onclick');
e.stopPropagation(); const match = onclickAttr.match(/handleListViewAction\('([^']+)',\s*(true|false)\)/i);
row.getTable().deselectRow(); if (match) {
row.select(); const actionValue = match[1];
const requiresSelection = match[2].toLowerCase() === 'true';
// Direct toepassen van requiresSelection-check
if (requiresSelection) {
// Controleer of er een geselecteerde rij is
const isDisabled = !instance.selectedRow;
button.disabled = isDisabled;
// Voeg/verwijder disabled class voor styling
if (isDisabled) {
button.classList.add('disabled');
} else {
button.classList.remove('disabled');
}
console.log(`Button ${actionValue} updated: disabled=${isDisabled}`);
}
// Backup check op basis van actions in config (voor achterwaartse compatibiliteit)
const action = instance.config.actions.find(a => a.value === actionValue);
if (action && action.requiresSelection === true && !requiresSelection) {
// Ook controleren op basis van action config
const isDisabled = !instance.selectedRow;
button.disabled = isDisabled;
// Voeg/verwijder disabled class voor styling
if (isDisabled) {
button.classList.add('disabled');
} else {
button.classList.remove('disabled');
}
}
} }
}); });
// Row selection event met de Tabulator API // Update hidden input with selected row data
table.on("rowSelectionChanged", function(data, rows){ this._updateSelectedRowInput(containerId);
console.log("Tabulator API: Aantal geselecteerde rijen:", rows.length); };
console.log("Tabulator API: Geselecteerde rijen:", rows.map(r => r.getData()));
// Update de geselecteerde rij (enkelvoud) // Update selected row input functie
EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null; window.EveAI.ListView._updateSelectedRowInput = function(containerId) {
EveAI.ListView._updateActionButtons(containerId); const instance = this.instances[containerId];
}); let hiddenInput = document.getElementById(`${containerId}-selected-row`);
// Opmerking: row selection event wordt nu afgehandeld via de Tabulator API if (!hiddenInput) {
hiddenInput = document.createElement('input');
return table; hiddenInput.type = 'hidden';
} catch (error) { hiddenInput.name = 'selected_row';
console.error('Error initializing ListView:', error); hiddenInput.id = `${containerId}-selected-row`;
container.innerHTML = `<div class="alert alert-danger"> document.getElementById(containerId).appendChild(hiddenInput);
<strong>Error loading data:</strong> ${error.message}
</div>`;
return null;
}
},
_buildColumns: function(columns, selectable) {
const tabulatorColumns = [];
// Add selection column if needed
{#if (selectable) {#}
{# tabulatorColumns.push({#}
{# formatter: "rowSelection", #}
{# titleFormatter: "rowSelection", #}
{# hozAlign: "center", #}
{# headerSort: false, #}
{# width: 60#}
{# });#}
{#}#}
// Add data columns
columns.forEach(col => {
// Maak een nieuwe schone kolom met alleen geldige Tabulator-eigenschappen
const column = {
title: col.title,
field: col.field,
headerSort: col.sortable !== false,
headerFilter: col.filterable !== false,
formatter: col.formatter,
width: col.width,
hozAlign: col.hozAlign,
vertAlign: col.vertAlign
};
// Zorg voor juiste numerieke sortering voor ID velden
if (col.field === 'id' || col.field.endsWith('_id') || col.type === 'number') {
column.sorter = 'number';
} }
// Set appropriate header filter based on the data type if (instance.selectedRow) {
if (col.type === 'date') { // Bewaar de geselecteerde rij-ID
column.headerFilter = "input"; hiddenInput.value = JSON.stringify({value: instance.selectedRow.id});
column.headerFilterParams = {type: "date"};
} else if (col.type === 'number') {
column.headerFilter = "number";
} else if (col.filterValues) {
column.headerFilter = "select";
column.headerFilterParams = {values: col.filterValues};
} else { } else {
column.headerFilter = "input"; hiddenInput.value = '';
} }
};
tabulatorColumns.push(column);
});
return tabulatorColumns;
},
_createActionButtons: function(container, actions, table) {
const actionSection = document.createElement('div');
actionSection.className = 'mt-3 d-flex justify-content-between';
const leftActions = document.createElement('div');
const rightActions = document.createElement('div');
actions.forEach(action => {
const button = document.createElement('button');
button.type = 'submit';
button.name = 'action';
button.value = action.value;
button.className = `btn ${action.class || 'btn-primary'} me-2`;
button.textContent = action.text;
button.disabled = action.requiresSelection !== false; // Disabled by default if requires selection
if (action.position === 'right') {
rightActions.appendChild(button);
} else {
leftActions.appendChild(button);
}
});
actionSection.appendChild(leftActions);
actionSection.appendChild(rightActions);
container.appendChild(actionSection);
},
_updateActionButtons: function(containerId) {
const instance = this.instances[containerId];
const container = document.getElementById(containerId);
const buttons = container.querySelectorAll('button[name="action"]');
buttons.forEach(button => {
// Enable/disable based on selection requirement
const action = instance.config.actions.find(a => a.value === button.value);
if (action && action.requiresSelection !== false) {
// Controleer of er een geselecteerde rij is
button.disabled = !instance.selectedRow;
}
});
// Update hidden input with selected row data
this._updateSelectedRowInput(containerId);
},
_updateSelectedRowInput: function(containerId) {
const instance = this.instances[containerId];
let hiddenInput = document.getElementById(`${containerId}-selected-row`);
if (!hiddenInput) {
hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'selected_row';
hiddenInput.id = `${containerId}-selected-row`;
document.getElementById(containerId).appendChild(hiddenInput);
}
if (instance.selectedRow) {
// Bewaar de geselecteerde rij-ID
hiddenInput.value = JSON.stringify({value: instance.selectedRow.id});
} else {
hiddenInput.value = '';
}
} }
};
</script>
// Definieer de handleListViewAction functie als deze nog niet bestaat
if (typeof window.handleListViewAction !== 'function') {
window.handleListViewAction = function(action, requiresSelection, e) {
// Gebruik explicit de event parameter om de browser event warning te vermijden
const evt = e || window.event;
const target = evt?.target || evt?.srcElement;
// Voorkom acties vanuit gedisabled buttons
if (target && (target.disabled || target.classList.contains('disabled'))) {
console.log('Button actie geblokkeerd: button is disabled');
return false;
}
// Vind het tableId op basis van het formulier waarin we zitten
const form = target ? target.closest('form') : null;
const tableId = form ? form.id.replace('-form', '') : document.querySelector('.tabulator-list-view')?.id;
if (!tableId) {
console.error('Kan tableId niet bepalen voor action:', action);
return false;
}
// Controleer direct of de button disabled zou moeten zijn
if (requiresSelection === true) {
const instance = window.EveAI.ListView.instances[tableId];
if (!instance || !instance.selectedRow) {
console.log('Button actie geblokkeerd: geen rij geselecteerd');
return false;
}
}
// Als EveAI.ListView beschikbaar is, gebruik dan de handleAction functie
if (window.EveAI && window.EveAI.ListView && typeof window.EveAI.ListView.handleAction === 'function') {
return window.EveAI.ListView.handleAction(action, requiresSelection, tableId);
}
// Fallback naar de originele implementatie
const actionInput = document.getElementById(`${tableId}-action`);
if (actionInput) {
actionInput.value = action;
}
// Controleer of er een rij geselecteerd is indien nodig
if (requiresSelection) {
const selectedRowInput = document.getElementById(`${tableId}-selected-row`);
if (!selectedRowInput || !selectedRowInput.value) {
alert('Selecteer eerst een item uit de lijst.');
return false;
}
}
// Verstuur het formulier met behoud van de originele form action
if (form) {
// Controleer of de form action correct is ingesteld
if (!form.action || form.action === '' || form.action === window.location.href || form.action === window.location.pathname) {
console.warn('Form action is mogelijk niet correct ingesteld:', form.action);
// Als er geen action is ingesteld, gebruik dan de huidige URL
form.action = window.location.href;
}
console.log(`Form action is: ${form.action}`);
form.submit();
return true;
}
return false;
};
}
console.log('EveAI List View component geladen');
});
</script>

View File

@@ -1,97 +0,0 @@
{% 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_with_dict_headers %}
{% block title %}Assets{% endblock %}
{% block content_title %}Assets{% endblock %}
{% block content_description %}View Assets{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<!-- Filter Form -->
{% set filter_form %}
<form method="GET" action="{{ url_for('interaction_bp.assets') }}">
{{ render_filter_field('type', 'Type', filter_options['type'], filters.get('type', [])) }}
{{ render_filter_field('file_type', 'Bestandstype', filter_options['file_type'], filters.get('file_type', [])) }}
<button type="submit" class="btn btn-primary">Apply Filters</button>
</form>
{% endset %}
{{ render_collapsible_section('Filter', 'Filter Options', filter_form) }}
<div class="form-group mt-3">
<form method="POST" action="{{ url_for('interaction_bp.handle_asset_selection') }}" id="assetsForm">
<!-- Assets Table -->
{{ render_selectable_sortable_table_with_dict_headers(
headers=[
{"text": "ID", "sort": "id"},
{"text": "Naam", "sort": "name"},
{"text": "Type", "sort": "type"},
{"text": "Type Versie", "sort": "type_version"},
{"text": "Bestandstype", "sort": "file_type"},
{"text": "Laatst Gebruikt", "sort": "last_used_at"}
],
rows=rows,
selectable=True,
id="assetsTable",
sort_by=sort_by,
sort_order=sort_order
) }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_asset" class="btn btn-primary" onclick="return validateTableSelection('assetsForm')">Edit Asset</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'interaction_bp.assets') }}
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('assetsTable');
const headers = table.querySelectorAll('th.sortable');
headers.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.dataset.sort;
let sortOrder = 'asc';
if (this.querySelector('.fa-sort-up')) {
sortOrder = 'desc';
} else if (this.querySelector('.fa-sort-down')) {
sortOrder = 'none';
}
window.location.href = updateQueryStringParameter(window.location.href, 'sort_by', sortBy);
window.location.href = updateQueryStringParameter(window.location.href, 'sort_order', sortOrder);
});
});
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
table.addEventListener('change', function(event) {
if (event.target.type === 'radio') {
var selectedRow = event.target.closest('tr');
var assetId = selectedRow.cells[1].textContent;
console.log('Selected Asset ID:', assetId);
}
});
});
</script>
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Chat Sessions{% endblock %}
{% block content_title %}Chat Sessions{% endblock %}
{% block content_description %}View Chat Sessions for Tenant{% 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('interaction_bp.handle_chat_session_selection') }}" id="chatSessionsForm">
{{ render_selectable_table(headers=["ID", "Session ID", "Session Start", "Session End"], rows=rows, selectable=True, id="chatSessionsTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="view_chat_session" class="btn btn-primary" onclick="return validateTableSelection('chatSessionsForm')">View Chat Session</button>
<button type="submit" name="action" value="chat_session_interactions" class="btn btn-primary" onclick="return validateTableSelection('chatSessionsForm')">View Chat Session interactions</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'interaction_bp.chat_sessions') }}
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Specialist Magic Links{% endblock %}
{% block content_title %}Specialist Magic Links{% endblock %}
{% block content_description %}View Specialists Magic Links{% 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('interaction_bp.handle_specialist_magic_link_selection') }}" id="specialistMagicLinksForm">
{{ render_selectable_table(headers=["Specialist ML ID", "Name", "Magic Link Code"], rows=rows, selectable=True, id="specialistMagicLinksTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_specialist_magic_link" class="btn btn-primary" onclick="return validateTableSelection('specialistMagicLinksForm')">Edit Specialist Magic Link</button>
</div>
<button type="submit" name="action" value="create_specialist_magic_link" class="btn btn-success">Register Specialist Magic Link</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'interaction_bp.specialist_magic_links') }}
{% endblock %}

View File

@@ -1,77 +0,0 @@
{% extends 'base.html' %}
{% block title %}Specialists{% endblock %}
{% block content_title %}Specialists{% endblock %}
{% block content_description %}View Specialists for Tenant{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm" onsubmit="return validateSpecialistSelection()">
<div id="specialists-list-view" class="tabulator-list-view"></div>
</form>
</div>
<!-- Include the list view component -->
{% include 'eveai_list_view.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Laad data met een kleine vertraging om de pagina eerst te laten renderen
setTimeout(() => {
// Initialize the list view met gepagineerde data en optimalisaties
const tabulatorTable = EveAI.ListView.initialize('specialists-list-view', {
data: {{ specialists_data | tojson }},
columns: {{ columns | tojson }},
initialSort: {{ initial_sort | tojson }},
actions: {{ actions | tojson }},
selectable: true,
usePagination: true, // Gebruik paginatie in plaats van progressieve lading
tableHeight: 800 // Hogere tabel voor specialists view
});
// Extra debug event listeners
if (tabulatorTable) {
console.log('Tabulator tabel succesvol geïnitialiseerd');
// Voeg een klik-event toe aan de hele tabel container voor een betere gebruikerservaring
document.querySelector('.tabulator-container').addEventListener('click', function(e) {
console.log('Tabulator container click event');
// De specifieke row click events worden afgehandeld door Tabulator zelf
});
} else {
console.error('Tabulator tabel kon niet worden geïnitialiseerd');
}
}, 50);
});
// Validation function for form submission
function validateSpecialistSelection() {
const selectedRow = document.getElementById('specialists-list-view-selected-row');
const actionButton = document.activeElement;
// Als de actie geen selectie vereist, ga dan altijd door
if (actionButton && actionButton.classList.contains('btn-success')) {
return true;
}
if (!selectedRow || !selectedRow.value) {
alert('Selecteer a.u.b. eerst een specialist.');
return false;
}
try {
const selection = JSON.parse(selectedRow.value);
// Controleer of er een specialist is geselecteerd
if (!selection.value) {
alert('Selecteer a.u.b. eerst een specialist.');
return false;
}
} catch (e) {
alert('Er is een fout opgetreden bij het verwerken van de selectie.');
return false;
}
return true;
}
</script>
{% endblock %}

View File

@@ -0,0 +1,82 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}{{ title }}{% endblock %}
{% block content_description %}{{ description|default('') }}{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ form_action }}" id="{{ table_id }}-form">
{% include 'eveai_list_view.html' %}
</form>
</div>
<script>
// Wacht tot de pagina volledig geladen is
document.addEventListener('DOMContentLoaded', function() {
// Functie om knoppen met requiresSelection te beheren
const updateSelectionButtons = function(hasSelection) {
document.querySelectorAll('.requires-selection').forEach(button => {
if (hasSelection) {
button.disabled = false;
button.classList.remove('disabled');
} else {
button.disabled = true;
button.classList.add('disabled');
}
});
};
// Direct alle requires-selection buttons disablen bij start
updateSelectionButtons(false);
// Wacht tot de EveAI.ListView module beschikbaar is
function checkAndInitialize() {
if (window.EveAI && window.EveAI.ListView && window.EveAI.ListView.initialize) {
// Configureer de tabel
const tableConfig = {
data: {{ data | tojson }},
columns: {{ columns | tojson }},
initialSort: {{ initial_sort | tojson }},
actions: {{ actions | tojson }},
tableHeight: {{ table_height|default(600) }},
selectable: true
};
// Initialiseer de tabel
const tabulatorTable = window.EveAI.ListView.initialize('{{ table_id }}', tableConfig);
if (tabulatorTable) {
console.log('Tabulator tabel succesvol geïnitialiseerd');
// Luister naar selectie-events
tabulatorTable.on("rowSelectionChanged", function(data, rows) {
const hasSelection = rows.length > 0;
updateSelectionButtons(hasSelection);
});
// Luister naar deselect events
tabulatorTable.on("rowDeselected", function(row) {
const selectedRows = tabulatorTable.getSelectedRows();
updateSelectionButtons(selectedRows.length > 0);
});
// Luister naar select events
tabulatorTable.on("rowSelected", function(row) {
updateSelectionButtons(true);
});
} else {
console.error('Kon de Tabulator tabel niet initialiseren');
}
} else {
// Probeer opnieuw na een korte vertraging
setTimeout(checkAndInitialize, 100);
}
}
// Start de initialisatie
setTimeout(checkAndInitialize, 50);
});
</script>
{% endblock %}

View File

@@ -69,10 +69,9 @@
<ul class="navbar-nav navbar-nav-hover mx-auto"> <ul class="navbar-nav navbar-nav-hover mx-auto">
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{{ dropdown('Tenants', 'source_environment', [ {{ dropdown('Tenants', 'source_environment', [
{'name': 'Tenants', 'url': '/user/select_tenant', 'roles': ['Super User', 'Partner Admin']}, {'name': 'Tenants', 'url': '/user/tenants', 'roles': ['Super User', 'Partner Admin']},
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant 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']}, {'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Tenant Makes', 'url': '/user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Tenant Makes', 'url': '/user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
@@ -100,8 +99,7 @@
{'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Documents', 'url': '/document/documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Documents', 'url': '/document/documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Full Documents', 'url': '/document/full_documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Document Processing', 'url': '/document/documents_processing', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Document Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']}, {'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }} ]) }}
{% endif %} {% endif %}
@@ -115,9 +113,9 @@
{% endif %} {% endif %}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{{ dropdown('Entitlements', 'settings', [ {{ dropdown('Entitlements', 'settings', [
{'name': 'License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User', 'Partner Admin']}, {'name': 'License Tiers', 'url': '/entitlements/license_tiers', 'roles': ['Super User', 'Partner Admin']},
{'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']}, {'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']},
{'name': 'Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']}, {'name': 'Licenses', 'url': '/entitlements/licenses', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']},
{'name': 'Active Usage', 'url': '/entitlements/active_usage', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']}, {'name': 'Active Usage', 'url': '/entitlements/active_usage', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']},
]) }} ]) }}
{% endif %} {% endif %}

View File

@@ -1,27 +0,0 @@
{% 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('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>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.retrievers') }}
{% endblock %}

View File

@@ -1,27 +0,0 @@
{% 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('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>
<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>
<button type="submit" name="action" value="create_partner" class="btn btn-success">Register Partner for Tenant</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'document_bp.retrievers') }}
{% endblock %}

View File

@@ -1,14 +1,9 @@
{#dist/main.js contains all used javascript libraries#} {#dist/main.js contains all used javascript libraries#}
<script src="{{url_for('static', filename='dist/main.js')}}"></script> <script src="{{url_for('static', filename='dist/main.js')}}"></script>
<!-- Chat client bundle (alleen op chat paginas) -->
{% if 'chat' in request.endpoint or request.path.startswith('/chat') %}
<script src="{{url_for('static', filename='dist/chat-client.js')}}"></script>
{% endif %}
{% include 'eveai_json_editor.html' %} {% include 'eveai_json_editor.html' %}
{% include 'eveai_ordered_list_editor.html' %} {% include 'eveai_ordered_list_editor.html' %}
{% include 'eveai_list_view.html' %} <!-- ListView component is now included in base.html -->
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View File

@@ -1,55 +0,0 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table, render_pagination, render_field %}
{% block title %}Tenant Selection{% endblock %}
{% block content_title %}Select a Tenant{% endblock %}
{% block content_description %}Select the active tenant for the current session{% endblock %}
{% block content %}
<!-- Filter Form -->
<form method="POST" action="{{ url_for('user_bp.select_tenant') }}" class="mb-4">
{{ filter_form.hidden_tag() }}
<div class="row">
<div class="col-md-4">
{{ render_field(filter_form.types, class="select2") }}
</div>
<div class="col-md-4">
{{ render_field(filter_form.search) }}
</div>
<div class="col-md-4">
{{ filter_form.submit(class="btn btn-primary") }}
</div>
</div>
</form>
<!-- Tenant Selection Form -->
<form method="POST" action="{{ url_for('user_bp.handle_tenant_selection') }}">
{{ render_selectable_table(headers=["Tenant ID", "Tenant Name", "Website", "Type"], rows=rows, selectable=True, id="tenantsTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="select_tenant" class="btn btn-primary">Set Session Tenant</button>
<button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button>
</div>
<button type="submit" name="action" value="create_tenant" class="btn btn-success">Register Tenant</button>
</div>
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('.select2').select2({
placeholder: "Select tenant types",
allowClear: true,
minimumResultsForSearch: Infinity, // Hides the search box
dropdownCssClass: 'select2-dropdown-hidden', // Custom class for dropdown
containerCssClass: 'select2-container-hidden' // Custom class for container
});
});
</script>
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Tenant Makes{% endblock %}
{% block content_title %}Tenant Makes{% endblock %}
{% block content_description %}View Tenant Makes for Tenant{% 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('user_bp.handle_tenant_make_selection') }}" id="tenantMakesForm">
{{ render_selectable_table(headers=["Tenant Make ID", "Name", "Website", "Active"], rows=rows, selectable=True, id="tenantMakesTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_tenant_make" class="btn btn-primary" onclick="return validateTableSelection('tenantMakesForm')">Edit Tenant Make</button>
</div>
<button type="submit" name="action" value="create_tenant_make" class="btn btn-success">Register Tenant Make</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, "user_bp.tenant_makes") }}
{% endblock %}

View File

@@ -1,28 +0,0 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Documents{% endblock %}
{% block content_title %}Tenant Projects{% endblock %}
{% block content_description %}View Tenant Projects for Tenant{% 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('user_bp.handle_tenant_project_selection') }}" id="tenantProjectsForm">
{{ render_selectable_table(headers=["Tenant Project ID", "Name", "API Clue", "Responsible", "Active"], rows=rows, selectable=True, id="catalogsTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_tenant_project" class="btn btn-primary" onclick="return validateTableSelection('tenantProjectsForm')">Edit Tenant Project</button>
<button type="submit" name="action" value="invalidate_tenant_project" class="btn btn-primary" onclick="return validateTableSelection('tenantProjectsForm')">Invalidate Tenant Project</button>
<button type="submit" name="action" value="delete_tenant_project" class="btn btn-danger" onclick="return validateTableSelection('tenantProjectsForm')">Delete Tenant Project</button>
</div>
<button type="submit" name="action" value="create_tenant_project" class="btn btn-success">Register Project</button>
</div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, "user_bp.tenant_projects") }}
{% endblock %}

View File

@@ -21,7 +21,7 @@
{% endblock %} {% endblock %}
{% block content_footer %} {% block content_footer %}
{{ render_pagination(pagination, 'user_bp.view_tenant_domains') }} {{ render_pagination(pagination, 'user_bp.tenant_domains') }}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@@ -1,27 +0,0 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Users{% endblock %}
{% block content_title %}Select a user{% endblock %}
{% block content_description %}Select the user you'd like to action upon{% endblock %}
{% block content %}
<form action="{{ url_for('user_bp.handle_user_action') }}" method="POST" id="usersForm">
{{ render_selectable_table(headers=["User ID", "User Name", "Email"], rows=rows, selectable=True, id="usersTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_user" class="btn btn-primary" onclick="return validateTableSelection('usersForm')">Edit User</button>
<button type="submit" name="action" value="resend_confirmation_email" class="btn btn-secondary" onclick="return validateTableSelection('usersForm')">Resend Confirmation</button>
<button type="submit" name="action" value="send_password_reset_email" class="btn btn-secondary" onclick="return validateTableSelection('usersForm')">Password Reset</button>
<button type="submit" name="action" value="reset_uniquifier" class="btn btn-secondary" onclick="return validateTableSelection('usersForm')">Reset Uniquifier</button>
</div>
<button type="submit" name="action" value="create_user" class="btn btn-success">Register User</button>
<!-- Additional buttons can be added here for other actions -->
</div>
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% endblock %}

View File

@@ -25,9 +25,10 @@ from common.utils.middleware import mw_before_request
from common.utils.celery_utils import current_celery from common.utils.celery_utils import current_celery
from common.utils.nginx_utils import prefixed_url_for from common.utils.nginx_utils import prefixed_url_for
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
from eveai_app.views.list_views.document_list_view import DocumentListView from eveai_app.views.list_views.list_view_utils import render_list_view
from eveai_app.views.list_views.document_version_list_view import DocumentVersionListView from eveai_app.views.list_views.document_list_views import get_catalogs_list_view, get_processors_list_view, \
from eveai_app.views.list_views.full_document_list_view import FullDocumentListView get_retrievers_list_view, get_documents_list_view, get_documents_processing_list_view
from eveai_app.views.list_views.list_view_utils import render_list_view
document_bp = Blueprint('document_bp', __name__, url_prefix='/document') document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
@@ -86,19 +87,9 @@ def catalog():
@document_bp.route('/catalogs', methods=['GET', 'POST']) @document_bp.route('/catalogs', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def catalogs(): def catalogs():
page = request.args.get('page', 1, type=int) # Haal configuratie op en render de lijst-weergave
per_page = request.args.get('per_page', 10, type=int) config = get_catalogs_list_view()
return render_list_view('list_view.html', **config)
query = Catalog.query.order_by(Catalog.id)
pagination = query.paginate(page=page, per_page=per_page)
the_catalogs = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_catalogs, [('id', ''), ('name', ''), ('type', '')])
# Render the catalogs in a template
return render_template('document/catalogs.html', rows=rows, pagination=pagination)
@document_bp.route('/handle_catalog_selection', methods=['POST']) @document_bp.route('/handle_catalog_selection', methods=['POST'])
@@ -234,25 +225,14 @@ def edit_processor(processor_id):
@document_bp.route('/processors', methods=['GET', 'POST']) @document_bp.route('/processors', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def processors(): def processors():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
catalog_id = session.get('catalog_id', None) catalog_id = session.get('catalog_id', None)
if not catalog_id: if not catalog_id:
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning') flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
return redirect(prefixed_url_for('document_bp.catalogs')) return redirect(prefixed_url_for('document_bp.catalogs'))
query = Processor.query.filter_by(catalog_id=catalog_id).order_by(Processor.id) # Get configuration and render the list view
config = get_processors_list_view(catalog_id)
pagination = query.paginate(page=page, per_page=per_page) return render_list_view('list_view.html', **config)
the_processors = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_processors,
[('id', ''), ('name', ''), ('type', ''), ('active', '')])
# Render the catalogs in a template
return render_template('document/processors.html', rows=rows, pagination=pagination)
@document_bp.route('/handle_processor_selection', methods=['POST']) @document_bp.route('/handle_processor_selection', methods=['POST'])
@@ -345,25 +325,14 @@ def edit_retriever(retriever_id):
@document_bp.route('/retrievers', methods=['GET', 'POST']) @document_bp.route('/retrievers', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def retrievers(): def retrievers():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
catalog_id = session.get('catalog_id', None) catalog_id = session.get('catalog_id', None)
if not catalog_id: if not catalog_id:
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning') flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
return redirect(prefixed_url_for('document_bp.catalogs')) return redirect(prefixed_url_for('document_bp.catalogs'))
query = Retriever.query.filter_by(catalog_id=catalog_id).order_by(Retriever.id) # Get configuration and render the list view
config = get_retrievers_list_view(catalog_id)
pagination = query.paginate(page=page, per_page=per_page) return render_list_view('list_view.html', **config)
the_retrievers = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_retrievers,
[('id', ''), ('name', ''), ('type', '')])
# Render the catalogs in a template
return render_template('document/retrievers.html', rows=rows, pagination=pagination)
@document_bp.route('/handle_retriever_selection', methods=['POST']) @document_bp.route('/handle_retriever_selection', methods=['POST'])
@@ -502,20 +471,20 @@ def documents():
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning') flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
return redirect(prefixed_url_for('document_bp.catalogs')) return redirect(prefixed_url_for('document_bp.catalogs'))
view = DocumentListView(Document, 'document/documents.html', per_page=10) config = get_documents_list_view(catalog_id)
return view.get() return render_list_view('list_view.html', **config)
@document_bp.route('/full_documents', methods=['GET', 'POST']) @document_bp.route('/documents_processing', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def full_documents(): def documents_processing():
catalog_id = session.get('catalog_id', None) catalog_id = session.get('catalog_id', None)
if not catalog_id: if not catalog_id:
flash('You need to set a Session Catalog before viewing Full Documents', 'warning') flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
return redirect(prefixed_url_for('document_bp.catalogs')) return redirect(prefixed_url_for('document_bp.catalogs'))
view = FullDocumentListView(Document, 'document/full_documents.html', per_page=10) config = get_documents_processing_list_view(catalog_id)
return view.get() return render_list_view('list_view.html', **config)
@document_bp.route('/handle_document_selection', methods=['POST']) @document_bp.route('/handle_document_selection', methods=['POST'])
@@ -536,14 +505,23 @@ def handle_document_selection():
match action: match action:
case 'edit_document': case 'edit_document':
return redirect(prefixed_url_for('document_bp.edit_document_view', document_id=doc_id)) return redirect(prefixed_url_for('document_bp.edit_document', document_id=doc_id))
case 'document_versions': case 'refresh':
return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_id))
case 'refresh_document':
refresh_document_view(doc_id) refresh_document_view(doc_id)
return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_id)) return redirect(prefixed_url_for('document_bp.documents', document_id=doc_id))
case 're_embed_latest_versions': case 're_process':
re_embed_latest_versions() document = Document.query.get_or_404(doc_id)
doc_vers_id = document.latest_version.id
process_version(doc_vers_id)
case 'view_document_markdown':
document = Document.query.get_or_404(doc_id)
doc_vers_id = document.latest_version.id
return redirect(prefixed_url_for('document_bp.view_document_version_markdown',
document_version_id=doc_vers_id))
case 'edit_document_version':
document = Document.query.get_or_404(doc_id)
doc_vers_id = document.latest_version.id
return redirect(prefixed_url_for('document_bp.edit_document_version', document_version_id=doc_vers_id))
# Add more conditions for other actions # Add more conditions for other actions
return redirect(prefixed_url_for('document_bp.documents')) return redirect(prefixed_url_for('document_bp.documents'))
@@ -551,7 +529,7 @@ def handle_document_selection():
@document_bp.route('/edit_document/<int:document_id>', methods=['GET', 'POST']) @document_bp.route('/edit_document/<int:document_id>', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_document_view(document_id): def edit_document(document_id):
# Use an alias for the Catalog to avoid column name conflicts # Use an alias for the Catalog to avoid column name conflicts
CatalogAlias = aliased(Catalog) CatalogAlias = aliased(Catalog)
@@ -592,7 +570,7 @@ def edit_document_view(document_id):
@document_bp.route('/edit_document_version/<int:document_version_id>', methods=['GET', 'POST']) @document_bp.route('/edit_document_version/<int:document_version_id>', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_document_version_view(document_version_id): def edit_document_version(document_version_id):
doc_vers = DocumentVersion.query.get_or_404(document_version_id) doc_vers = DocumentVersion.query.get_or_404(document_version_id)
form = EditDocumentVersionForm(request.form, obj=doc_vers) form = EditDocumentVersionForm(request.form, obj=doc_vers)
@@ -630,119 +608,6 @@ def edit_document_version_view(document_version_id):
doc_details=f'Document {doc_vers.document.name}') doc_details=f'Document {doc_vers.document.name}')
@document_bp.route('/document_versions/<int:document_id>', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def document_versions(document_id):
doc = Document.query.get_or_404(document_id)
doc_desc = f'{doc.name}'
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = (DocumentVersion.query.filter_by(doc_id=document_id)
.order_by(DocumentVersion.language)
.order_by(desc(DocumentVersion.id)))
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
doc_langs = pagination.items
rows = prepare_table_for_macro(doc_langs, [('id', ''), ('file_type', ''), ('file_size', ''),
('processing', ''), ('processing_started_at', ''),
('processing_finished_at', ''), ('processing_error', '')])
return render_template('document/document_versions.html', rows=rows, pagination=pagination, document=doc_desc)
@document_bp.route('/handle_document_version_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_document_version_selection():
document_version_identification = request.form['selected_row']
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']
match action:
case 'edit_document_version':
return redirect(prefixed_url_for('document_bp.edit_document_version_view', document_version_id=doc_vers_id))
case 'process_document_version':
process_version(doc_vers_id)
# Add more conditions for other actions
case 'view_document_version_markdown':
return redirect(prefixed_url_for('document_bp.view_document_version_markdown',
document_version_id=doc_vers_id))
doc_vers = DocumentVersion.query.get_or_404(doc_vers_id)
return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_vers.doc_id))
@document_bp.route('/handle_full_document_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_full_document_selection():
selected_row = request.form['selected_row']
action = request.form['action']
try:
# Parse the selected row to get document ID (first column) and version ID (fifth column)
row_data = ast.literal_eval(selected_row)
selected_doc_id = row_data.get('value')
# We need to retrieve the corresponding row data to get the version ID
# This is a bit complex with the current structure, so we'll use a different approach
if action in ['edit_document', 'document_versions', 'refresh_document']:
# Actions that need document ID
match action:
case 'edit_document':
return redirect(prefixed_url_for('document_bp.edit_document_view', document_id=selected_doc_id))
case 'document_versions':
return redirect(prefixed_url_for('document_bp.document_versions', document_id=selected_doc_id))
case 'refresh_document':
refresh_document_view(selected_doc_id)
return redirect(prefixed_url_for('document_bp.full_documents'))
else:
# Actions that need version ID
# We need to get the version ID from the selected row in the table
# We'll extract it from the form data and the version ID is in the 5th cell (index 4)
version_id_cell = int(request.form.get('version_id', 0))
# If we couldn't get a version ID, try to find the latest version for this document
if not version_id_cell:
doc_version = DocumentVersion.query.filter_by(doc_id=selected_doc_id).order_by(desc(DocumentVersion.id)).first()
if doc_version:
version_id_cell = doc_version.id
else:
flash('No document version found for this document.', 'error')
return redirect(prefixed_url_for('document_bp.full_documents'))
match action:
case 'edit_document_version':
return redirect(prefixed_url_for('document_bp.edit_document_version_view', document_version_id=version_id_cell))
case 'process_document_version':
process_version(version_id_cell)
return redirect(prefixed_url_for('document_bp.full_documents'))
case 'view_document_version_markdown':
return redirect(prefixed_url_for('document_bp.view_document_version_markdown', document_version_id=version_id_cell))
except (ValueError, AttributeError, KeyError) as e:
current_app.logger.error(f"Error processing full document selection: {str(e)}")
flash('Invalid selection or action. Please try again.', 'error')
return redirect(prefixed_url_for('document_bp.full_documents'))
@document_bp.route('/document_versions_list', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def document_versions_list():
view = DocumentVersionListView(DocumentVersion, 'document/document_versions_list_view.html', per_page=20)
return view.get()
@document_bp.route('/view_document_version_markdown/<int:document_version_id>', methods=['GET']) @document_bp.route('/view_document_version_markdown/<int:document_version_id>', methods=['GET'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_document_version_markdown(document_version_id): def view_document_version_markdown(document_version_id):

View File

@@ -17,6 +17,8 @@ from .entitlements_forms import LicenseTierForm, LicenseForm
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed 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.nginx_utils import prefixed_url_for
from common.utils.document_utils import set_logging_information, update_logging_information from common.utils.document_utils import set_logging_information, update_logging_information
from .list_views.entitlement_list_views import get_license_tiers_list_view, get_license_list_view
from .list_views.list_view_utils import render_list_view
entitlements_bp = Blueprint('entitlements_bp', __name__, url_prefix='/entitlements') entitlements_bp = Blueprint('entitlements_bp', __name__, url_prefix='/entitlements')
@@ -45,48 +47,23 @@ def license_tier():
current_app.logger.info(f"Successfully created license tier {new_license_tier.id}") current_app.logger.info(f"Successfully created license tier {new_license_tier.id}")
flash(f"Successfully created tenant license tier {new_license_tier.id}", 'success') flash(f"Successfully created tenant license tier {new_license_tier.id}", 'success')
return redirect(prefixed_url_for('entitlements_bp.view_license_tiers')) return redirect(prefixed_url_for('entitlements_bp.license_tiers'))
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
return render_template('entitlements/license_tier.html', form=form) return render_template('entitlements/license_tier.html', form=form)
@entitlements_bp.route('/view_license_tiers', methods=['GET', 'POST']) @entitlements_bp.route('/license_tiers', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin') @roles_accepted('Super User', 'Partner Admin')
def view_license_tiers(): def license_tiers():
page = request.args.get('page', 1, type=int) config = get_license_tiers_list_view()
per_page = request.args.get('per_page', 10, type=int)
today = dt.now(tz.utc)
query = LicenseTier.query.filter( # Check if there was an error in getting the configuration
or_( if config.get('error'):
LicenseTier.end_date == None, return render_template("index.html")
LicenseTier.end_date >= today
)
)
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) return render_list_view('list_view.html', **config)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
license_tiers = pagination.items
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,
can_assign_license=UserServices.can_user_assign_license())
@entitlements_bp.route('/handle_license_tier_selection', methods=['POST']) @entitlements_bp.route('/handle_license_tier_selection', methods=['POST'])
@@ -110,7 +87,7 @@ def handle_license_tier_selection():
LicenseTierServices.associate_license_tier_with_partner(license_tier_id) LicenseTierServices.associate_license_tier_with_partner(license_tier_id)
# Add more conditions for other actions # Add more conditions for other actions
return redirect(prefixed_url_for('entitlements_bp.view_license_tiers')) return redirect(prefixed_url_for('entitlements_bp.license_tiers'))
@entitlements_bp.route('/license_tier/<int:license_tier_id>', methods=['GET', 'POST']) @entitlements_bp.route('/license_tier/<int:license_tier_id>', methods=['GET', 'POST'])
@@ -253,79 +230,16 @@ def edit_license(license_id):
return render_template('entitlements/license.html', form=form, ext_readonly_fields=readonly_fields) return render_template('entitlements/license.html', form=form, ext_readonly_fields=readonly_fields)
@entitlements_bp.route('/view_usages') @entitlements_bp.route('/licenses')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_usages(): def licenses():
page = request.args.get('page', 1, type=int) config = get_license_list_view()
per_page = request.args.get('per_page', 10, type=int)
if not session.get('tenant', None): # Check if there was an error in getting the configuration
flash('You can only view usage for a Tenant. Select a Tenant to continue!', 'danger') if config.get('error'):
return redirect(prefixed_url_for('user_bp.select_tenant')) return render_template("index.html")
tenant_id = session.get('tenant').get('id') return render_list_view('list_view.html', **config)
query = LicenseUsage.query.filter_by(tenant_id=tenant_id).order_by(desc(LicenseUsage.id))
pagination = query.paginate(page=page, per_page=per_page)
lus = pagination.items
# prepare table data
rows = prepare_table_for_macro(lus, [('id', ''), ('period_start_date', ''), ('period_end_date', ''),
('storage_mb_used', ''), ('embedding_mb_used', ''),
('interaction_total_tokens_used', '')])
# Render the users in a template
return render_template('entitlements/view_usages.html', rows=rows, pagination=pagination)
@entitlements_bp.route('/handle_usage_selection', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_usage_selection():
usage_identification = request.form['selected_row']
usage_id = ast.literal_eval(usage_identification).get('value')
the_usage = LicenseUsage.query.get_or_404(usage_id)
action = request.form['action']
pass # Currently, no actions are defined
@entitlements_bp.route('/view_licenses')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_licenses():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
tenant_id = session.get('tenant').get('id')
# Get current date in UTC
current_date = dt.now(tz=tz.utc).date()
# Query licenses for the tenant, with ordering and active status
# TODO - Check validity
query = (
License.query
.join(LicenseTier) # Join with LicenseTier
.filter(License.tenant_id == tenant_id)
.add_columns(
License.id,
License.start_date,
License.nr_of_periods,
LicenseTier.name.label('license_tier_name'), # Access name through LicenseTier
(License.start_date <= current_date).label('active')
)
.order_by(License.start_date.desc())
)
pagination = query.paginate(page=page, per_page=per_page)
lics = pagination.items
# prepare table data
rows = prepare_table_for_macro(lics, [('id', ''), ('license_tier_name', ''), ('start_date', ''),
('nr_of_periods', ''), ('active', '')])
# Render the licenses in a template
return render_template('entitlements/view_licenses.html', rows=rows, pagination=pagination)
@entitlements_bp.route('/handle_license_selection', methods=['POST']) @entitlements_bp.route('/handle_license_selection', methods=['POST'])
@@ -343,7 +257,7 @@ def handle_license_selection():
case 'view_periods': case 'view_periods':
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id)) return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))
case _: case _:
return redirect(prefixed_url_for('entitlements_bp.view_licenses')) return redirect(prefixed_url_for('entitlements_bp.licenses'))
@entitlements_bp.route('/license/<int:license_id>/periods') @entitlements_bp.route('/license/<int:license_id>/periods')
@@ -356,7 +270,7 @@ def view_license_periods(license_id):
tenant_id = session.get('tenant').get('id') tenant_id = session.get('tenant').get('id')
if license.tenant_id != tenant_id: if license.tenant_id != tenant_id:
flash('Access denied to this license', 'danger') flash('Access denied to this license', 'danger')
return redirect(prefixed_url_for('entitlements_bp.view_licenses')) return redirect(prefixed_url_for('entitlements_bp.licenses'))
# Get all periods for this license # Get all periods for this license
periods = (LicensePeriod.query periods = (LicensePeriod.query
@@ -402,6 +316,13 @@ def transition_period_status(license_id, period_id):
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id)) return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))
@entitlements_bp.route('/view_licenses')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_licenses_redirect():
# Redirect to the new licenses route
return redirect(prefixed_url_for('entitlements_bp.licenses'))
@entitlements_bp.route('/active_usage') @entitlements_bp.route('/active_usage')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def active_license_usage(): def active_license_usage():
@@ -409,7 +330,7 @@ def active_license_usage():
tenant_id = session.get('tenant', {}).get('id') tenant_id = session.get('tenant', {}).get('id')
if not tenant_id: if not tenant_id:
flash('No active or pending license period found for this tenant', 'warning') flash('No active or pending license period found for this tenant', 'warning')
return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('user_bp.tenants'))
active_period = LicensePeriod.query \ active_period = LicensePeriod.query \
.join(License) \ .join(License) \

View File

@@ -31,6 +31,10 @@ from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAge
EditEveAIToolForm, ExecuteSpecialistForm, EditEveAIToolForm, ExecuteSpecialistForm,
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm) SpecialistMagicLinkForm, EditSpecialistMagicLinkForm)
from eveai_app.views.list_views.interaction_list_views import (get_specialists_list_view, get_assets_list_view,
get_magic_links_list_view, get_chat_sessions_list_view)
from eveai_app.views.list_views.list_view_utils import render_list_view
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction') interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
@@ -56,17 +60,9 @@ def before_request():
@interaction_bp.route('/chat_sessions', methods=['GET', 'POST']) @interaction_bp.route('/chat_sessions', methods=['GET', 'POST'])
def chat_sessions(): def chat_sessions():
page = request.args.get('page', 1, type=int) # Get configuration and render the list view
per_page = request.args.get('per_page', 10, type=int) config = get_chat_sessions_list_view()
return render_list_view('list_view.html', **config)
query = ChatSession.query.order_by(desc(ChatSession.session_start))
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
docs = pagination.items
rows = prepare_table_for_macro(docs, [('id', ''), ('session_id', ''), ('session_start', ''), ('session_end', '')])
return render_template('interaction/chat_sessions.html', rows=rows, pagination=pagination)
@interaction_bp.route('/handle_chat_session_selection', methods=['POST']) @interaction_bp.route('/handle_chat_session_selection', methods=['POST'])
@@ -288,45 +284,10 @@ def edit_specialist(specialist_id):
@interaction_bp.route('/specialists', methods=['GET', 'POST']) @interaction_bp.route('/specialists', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialists(): def specialists():
# Get all specialists (no pagination needed for client-side)
specialists_query = Specialist.query.order_by(Specialist.id)
all_specialists = specialists_query.all()
# Prepare data for Tabulator # Get configuration and render the list view
specialists_data = [] config = get_specialists_list_view()
for specialist in all_specialists: return render_list_view('list_view.html', **config)
specialists_data.append({
'id': specialist.id,
'name': specialist.name,
'type': specialist.type,
'type_version': specialist.type_version,
'active': specialist.active
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
{'title': 'Type Version', 'field': 'type_version'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]
# Action definitions
actions = [
{'value': 'edit_specialist', 'text': 'Edit Specialist', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'execute_specialist', 'text': 'Execute Specialist', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_specialist', 'text': 'Register Specialist', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False}
]
# Initial sort configuration
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return render_template('interaction/specialists.html',
specialists_data=specialists_data,
columns=columns,
actions=actions,
initial_sort=initial_sort)
@interaction_bp.route('/handle_specialist_selection', methods=['POST']) @interaction_bp.route('/handle_specialist_selection', methods=['POST'])
@@ -763,19 +724,9 @@ def edit_specialist_magic_link(specialist_magic_link_id):
@interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST']) @interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialist_magic_links(): def specialist_magic_links():
page = request.args.get('page', 1, type=int) # Get configuration and render the list view
per_page = request.args.get('per_page', 10, type=int) config = get_magic_links_list_view()
return render_list_view('list_view.html', **config)
query = SpecialistMagicLink.query.order_by(SpecialistMagicLink.id)
pagination = query.paginate(page=page, per_page=per_page)
the_specialist_magic_links = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_specialist_magic_links, [('id', ''), ('name', ''), ('magic_link_code', ''),])
# Render the catalogs in a template
return render_template('interaction/specialist_magic_links.html', rows=rows, pagination=pagination)
@interaction_bp.route('/handle_specialist_magic_link_selection', methods=['POST']) @interaction_bp.route('/handle_specialist_magic_link_selection', methods=['POST'])
@@ -798,19 +749,15 @@ def handle_specialist_magic_link_selection():
# Routes for Asset Management --------------------------------------------------------------------- # Routes for Asset Management ---------------------------------------------------------------------
@interaction_bp.route('/assets', methods=['GET', 'POST']) @interaction_bp.route('/assets', methods=['GET', 'POST'])
def assets(): def assets():
from eveai_app.views.list_views.assets_list_view import AssetsListView config = get_assets_list_view()
view = AssetsListView( return render_list_view('list_view.html', **config)
model=EveAIAsset,
template='interaction/assets.html',
per_page=10
)
return view.get()
@interaction_bp.route('/handle_asset_selection', methods=['POST']) @interaction_bp.route('/handle_asset_selection', methods=['POST'])
def handle_asset_selection(): def handle_asset_selection():
action = request.form.get('action') action = request.form.get('action')
asset_id = request.form.get('selected_row') asset_identification = request.form.get('selected_row')
asset_id = ast.literal_eval(asset_identification).get('value')
if action == 'edit_asset': if action == 'edit_asset':
return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id)) return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id))

View File

@@ -0,0 +1,2 @@
# List Views module initialization
# This module contains utility functions for handling list views

View File

@@ -1,83 +0,0 @@
from datetime import datetime as dt, timezone as tz
from flask import request, render_template, current_app
from sqlalchemy import desc, asc
from common.models.interaction import EveAIAsset
from eveai_app.views.list_views.filtered_list_view import FilteredListView
class AssetsListView(FilteredListView):
allowed_filters = ['type', 'file_type']
allowed_sorts = ['id', 'last_used_at']
def get_query(self):
return EveAIAsset.query
def apply_filters(self, query):
filters = request.args.to_dict(flat=False)
if 'type' in filters and filters['type']:
query = query.filter(EveAIAsset.type.in_(filters['type']))
if 'file_type' in filters and filters['file_type']:
query = query.filter(EveAIAsset.file_type.in_(filters['file_type']))
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(EveAIAsset, 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, dt):
return date.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(date, str):
return date
else:
return ''
rows = [
[
{'value': item.id, 'class': '', 'type': 'text'},
{'value': item.name, 'class': '', 'type': 'text'},
{'value': item.type, 'class': '', 'type': 'text'},
{'value': item.type_version, 'class': '', 'type': 'text'},
{'value': item.file_type or '', 'class': '', 'type': 'text'},
{'value': format_date(item.last_used_at), 'class': '', 'type': 'text'}
] for item in pagination.items
]
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()
}
return render_template(self.template, **context)
def get_filter_options(self):
# Haal unieke waarden op voor filters
types = [t[0] for t in EveAIAsset.query.with_entities(EveAIAsset.type).distinct().all() if t[0]]
file_types = [f[0] for f in EveAIAsset.query.with_entities(EveAIAsset.file_type).distinct().all() if f[0]]
return {
'type': [(t, t) for t in types],
'file_type': [(f, f) for f in file_types]
}

View File

@@ -1,81 +0,0 @@
from datetime import datetime as dt, timezone as tz
from flask import request, render_template, session, current_app
from sqlalchemy import desc, asc, or_, and_
from common.models.document import Document
from eveai_app.views.list_views.filtered_list_view import FilteredListView
class DocumentListView(FilteredListView):
allowed_filters = ['validity']
allowed_sorts = ['id', 'name', 'valid_from', 'valid_to']
def get_query(self):
catalog_id = session.get('catalog_id')
return Document.query.filter_by(catalog_id=catalog_id)
def apply_filters(self, query):
filters = request.args.to_dict(flat=False)
if 'validity' in filters:
now = dt.now(tz.utc).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:
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, dt):
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': format_date(item.valid_from), 'class': '', 'type': 'text'},
{'value': format_date(item.valid_to), 'class': '', 'type': 'text'}
] for item in pagination.items
]
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()
}
return render_template(self.template, **context)
def get_filter_options(self):
return {
'validity': [('valid', 'Valid'), ('all', 'All')]
}

View File

@@ -0,0 +1,324 @@
from flask import current_app, url_for
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.document import Catalog, Document, DocumentVersion, Processor, Retriever
def get_catalogs_list_view():
"""Genereer de catalogi lijst-weergave configuratie"""
# Haal alle catalogi op (geen server-side filtering - wordt client-side afgehandeld)
catalog_query = Catalog.query.order_by(Catalog.id)
all_catalogs = catalog_query.all()
# Bereid data voor voor Tabulator
data = []
for catalog in all_catalogs:
data.append({
'id': catalog.id,
'name': catalog.name,
'type': catalog.type
})
# Kolomdefinities
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Naam', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
]
actions = [
{'value': 'set_session_catalog', 'text': 'Set Session Catalog', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'edit_catalog', 'text': 'Edit Catalog', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_catalog', 'text': 'Register Catalog', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Catalogi',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'catalogs_table',
'form_action': url_for('document_bp.handle_catalog_selection'),
'description': 'Bekijk en beheer catalogi'
}
def get_processors_list_view(catalog_id):
"""Generate the processors list view configuration"""
# Get all processors for this catalog
processor_query = Processor.query.filter_by(catalog_id=catalog_id).order_by(Processor.id)
all_processors = processor_query.all()
# Prepare data for Tabulator
data = []
for processor in all_processors:
data.append({
'id': processor.id,
'name': processor.name,
'type': processor.type,
'active': processor.active
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
{'title': 'Active', 'field': 'active'}
]
actions = [
{'value': 'edit_processor', 'text': 'Edit Processor', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_processor', 'text': 'Register Processor', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Processors',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'processors_table',
'form_action': url_for('document_bp.handle_processor_selection'),
'description': 'View and manage processors'
}
def get_retrievers_list_view(catalog_id):
"""Generate the retrievers list view configuration"""
# Get all retrievers for this catalog
retriever_query = Retriever.query.filter_by(catalog_id=catalog_id).order_by(Retriever.id)
all_retrievers = retriever_query.all()
# Prepare data for Tabulator
data = []
for retriever in all_retrievers:
data.append({
'id': retriever.id,
'name': retriever.name,
'type': retriever.type
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'}
]
actions = [
{'value': 'edit_retriever', 'text': 'Edit Retriever', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_retriever', 'text': 'Register Retriever', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Retrievers',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'retrievers_table',
'form_action': url_for('document_bp.handle_retriever_selection'),
'description': 'View and manage retrievers'
}
def get_documents_list_view(catalog_id):
# Query all documents for the given catalog_id, along with their latest version
# We use a subquery to get the latest document version for each document
from sqlalchemy import desc, func
from sqlalchemy.orm import aliased
# Subquery to get the max version id for each document
latest_version_subquery = db.session.query(
DocumentVersion.doc_id,
func.max(DocumentVersion.id).label('max_id')
).group_by(DocumentVersion.doc_id).subquery()
# Alias for the latest document version
LatestVersion = aliased(DocumentVersion)
# Main query with join to get documents with their latest version
document_query = db.session.query(
Document.id,
Document.name,
Document.valid_from,
Document.valid_to,
LatestVersion.file_type,
LatestVersion.file_size,
LatestVersion.processing
).join(
latest_version_subquery,
Document.id == latest_version_subquery.c.doc_id
).join(
LatestVersion,
(LatestVersion.doc_id == latest_version_subquery.c.doc_id) &
(LatestVersion.id == latest_version_subquery.c.max_id)
).filter(
Document.catalog_id == catalog_id
).order_by(Document.id)
# Execute the query
try:
documents_with_latest_versions = document_query.all()
# Prepare data for Tabulator
data = []
for doc in documents_with_latest_versions:
data.append({
'id': doc.id,
'name': doc.name,
'valid_from': doc.valid_from.strftime('%Y-%m-%d') if doc.valid_from else '',
'valid_to': doc.valid_to.strftime('%Y-%m-%d') if doc.valid_to else '',
'file_type': doc.file_type,
'file_size': f"{doc.file_size:.2f}" if doc.file_size else '',
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Valid From', 'field': 'valid_from'},
{'title': 'Valid To', 'field': 'valid_to'},
{'title': 'File Type', 'field': 'file_type'},
{'title': 'File Size', 'field': 'file_size'},
]
actions = [
{'value': 'edit_document', 'text': 'Edit Document', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'edit_document_version', 'text': 'Edit Document Version', 'class': 'btn-secondary',
'requiresSelection': True},
{'value': 'refresh', 'text': 'Refresh', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 're_process', 'text': 'Re-Process', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'view_document_markdown', 'text': 'View Document', 'class': 'btn-secondary', 'requiresSelection': True},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Documents',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'documents_table',
'form_action': url_for('document_bp.handle_document_selection'),
'description': 'Manage Documents and Document Versions'
}
except SQLAlchemyError as e:
current_app.logger.error(f"Error querying documents with latest versions: {str(e)}")
return {
'title': 'Documents',
'data': [],
'columns': [],
'actions': [],
'initial_sort': [],
'table_id': 'documents_table',
'form_action': url_for('document_bp.handle_document_selection'),
'description': 'There was an error while retrieving the documents. Please try again later.'
}
def get_documents_processing_list_view(catalog_id):
# Query all documents for the given catalog_id, along with their latest version
# We use a subquery to get the latest document version for each document
from sqlalchemy import desc, func
from sqlalchemy.orm import aliased
# Subquery to get the max version id for each document
latest_version_subquery = db.session.query(
DocumentVersion.doc_id,
func.max(DocumentVersion.id).label('max_id')
).group_by(DocumentVersion.doc_id).subquery()
# Alias for the latest document version
LatestVersion = aliased(DocumentVersion)
# Main query with join to get documents with their latest version
document_query = db.session.query(
Document.id,
Document.name,
LatestVersion.processing,
LatestVersion.processing_started_at,
LatestVersion.processing_finished_at,
LatestVersion.processing_error,
).join(
latest_version_subquery,
Document.id == latest_version_subquery.c.doc_id
).join(
LatestVersion,
(LatestVersion.doc_id == latest_version_subquery.c.doc_id) &
(LatestVersion.id == latest_version_subquery.c.max_id)
).filter(
Document.catalog_id == catalog_id
).order_by(Document.id)
# Execute the query
try:
documents_with_latest_versions = document_query.all()
# Prepare data for Tabulator
data = []
for doc in documents_with_latest_versions:
data.append({
'id': doc.id,
'name': doc.name,
'processing': doc.processing,
'processing_started_at': doc.processing_started_at.strftime('%Y-%m-%d %H:%M:%S') if doc.processing_started_at else '',
'processing_finished_at': doc.processing_finished_at.strftime('%Y-%m-%d %H:%M:%S') if doc.processing_finished_at else '',
'processing_error': doc.processing_error,
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Processing', 'field': 'processing', 'formatter': 'tickCross'},
{'title': 'Start', 'field': 'processing_started_at'},
{'title': 'Finish', 'field': 'processing_finished_at'},
{'title': 'Error', 'field': 'processing_error'},
]
actions = [
{'value': 'edit_document', 'text': 'Edit Document', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'edit_document_version', 'text': 'Edit Document Version', 'class': 'btn-secondary',
'requiresSelection': True},
{'value': 'refresh', 'text': 'Refresh', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 're_process', 'text': 'Re-Process', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'view_document_markdown', 'text': 'View Document', 'class': 'btn-secondary', 'requiresSelection': True},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Document Processing Status',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'documents_table',
'form_action': url_for('document_bp.handle_document_selection'),
'description': 'View Processing Status of Document Versions'
}
except SQLAlchemyError as e:
current_app.logger.error(f"Error querying documents with latest versions: {str(e)}")
return {
'title': 'Document Processing Status',
'data': [],
'columns': [],
'actions': [],
'initial_sort': [],
'table_id': 'documents_table',
'form_action': url_for('document_bp.handle_document_selection'),
'description': 'An error occurred while retrieving the documents. Please try again later.'
}

View File

@@ -1,83 +0,0 @@
from datetime import datetime
from flask import request, render_template
from sqlalchemy import desc, asc
from common.models.document import DocumentVersion, Document
from eveai_app.views.list_views.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)
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')]
}

View File

@@ -0,0 +1,164 @@
from flask import url_for, current_app, session
from datetime import datetime as dt, timezone as tz
from flask import flash
from sqlalchemy import or_, desc
from common.models.entitlements import LicenseTier, License
from common.services.user import PartnerServices, UserServices
from common.utils.eveai_exceptions import EveAIException
from common.utils.security_utils import current_user_has_role
def get_license_tiers_list_view():
"""Generate the license tiers list view configuration"""
today = dt.now(tz.utc)
# Build the query
query = LicenseTier.query.filter(
or_(
LicenseTier.end_date == None,
LicenseTier.end_date >= today
)
)
# Apply partner-specific filtering if needed
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 {
'title': 'License Tiers',
'data': [],
'columns': [],
'actions': [],
'initial_sort': [],
'table_id': 'license_tiers_table',
'form_action': url_for('entitlements_bp.handle_license_tier_selection'),
'description': 'View and manage license tiers',
'table_height': 700,
'error': True
}
if license_tier_ids and len(license_tier_ids) > 0:
query = query.filter(LicenseTier.id.in_(license_tier_ids))
# Order the results
query = query.order_by(LicenseTier.start_date.desc(), LicenseTier.id)
# Get all license tiers
license_tiers = query.all()
# Prepare data for Tabulator
data = []
for tier in license_tiers:
data.append({
'id': tier.id,
'name': tier.name,
'version': tier.version,
'start_date': tier.start_date.strftime('%Y-%m-%d') if tier.start_date else '',
'end_date': tier.end_date.strftime('%Y-%m-%d') if tier.end_date else ''
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80, 'type': 'number'},
{'title': 'Name', 'field': 'name'},
{'title': 'Version', 'field': 'version', 'width': 120},
{'title': 'Start Date', 'field': 'start_date', 'width': 120},
{'title': 'End Date', 'field': 'end_date', 'width': 120}
]
# Action definitions
actions = [
{'value': 'edit_license_tier', 'text': 'Edit License Tier', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_license_tier', 'text': 'Create License Tier', 'class': 'btn-secondary', 'position': 'right',
'requiresSelection': False}
]
# Add assign license action if user has permission
if UserServices.can_user_assign_license():
actions.insert(1, {'value': 'assign_license', 'text': 'Assign License', 'class': 'btn-info',
'requiresSelection': True})
# Initial sort configuration
initial_sort = [{'column': 'start_date', 'dir': 'desc'}, {'column': 'id', 'dir': 'asc'}]
return {
'title': 'License Tiers',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'license_tiers_table',
'form_action': url_for('entitlements_bp.handle_license_tier_selection'),
'description': 'View and manage license tiers',
'table_height': 700
}
def get_license_list_view():
"""Generate the licenses list view configuration"""
tenant_id = session.get('tenant').get('id')
# Get current date in UTC
current_date = dt.now(tz=tz.utc).date()
# Query licenses for the tenant, with ordering and active status
query = (
License.query
.join(LicenseTier) # Join with LicenseTier
.filter(License.tenant_id == tenant_id)
.add_columns(
License.id,
License.start_date,
License.nr_of_periods,
LicenseTier.name.label('license_tier_name'), # Access name through LicenseTier
(License.start_date <= current_date).label('active')
)
.order_by(License.start_date.desc())
)
# Get all licenses
licenses_list = query.all()
# Prepare data for Tabulator
data = []
for license in licenses_list:
data.append({
'id': license.id,
'license_tier_name': license.license_tier_name,
'start_date': license.start_date.strftime('%Y-%m-%d') if license.start_date else '',
'nr_of_periods': license.nr_of_periods,
'active': license.active
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80, 'type': 'number'},
{'title': 'License Tier', 'field': 'license_tier_name'},
{'title': 'Start Date', 'field': 'start_date', 'width': 120},
{'title': 'Nr of Periods', 'field': 'nr_of_periods', 'width': 120},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross', 'width': 100}
]
# Action definitions
actions = [
{'value': 'edit_license', 'text': 'Edit License', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'view_periods', 'text': 'View Periods', 'class': 'btn-secondary', 'requiresSelection': True}
]
# Initial sort configuration
initial_sort = [{'column': 'start_date', 'dir': 'desc'}]
return {
'title': 'Licenses',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'licenses_table',
'form_action': url_for('entitlements_bp.handle_license_selection'),
'description': 'View and manage licenses',
'table_height': 700
}

View File

@@ -1,54 +0,0 @@
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)

View File

@@ -1,178 +0,0 @@
from datetime import datetime as dt, timezone as tz
from flask import request, render_template, session, current_app
from sqlalchemy import desc, asc, or_, and_
from sqlalchemy.orm import aliased
from common.models.document import Document, DocumentVersion
from eveai_app.views.list_views.filtered_list_view import FilteredListView
from common.utils.view_assistants import prepare_table_for_macro
class FullDocumentListView(FilteredListView):
allowed_filters = ['validity', 'file_type', 'processing', 'processing_error']
allowed_sorts = ['id', 'name', 'valid_from', 'valid_to', 'file_type', 'processing_started_at',
'processing_finished_at', 'processing_error']
def __init__(self, model, template, per_page=10):
super().__init__(model, template, per_page)
self.version_alias = None
def get_query(self):
catalog_id = session.get('catalog_id')
# Fix: Selecteer alleen de id kolom in de subquery
latest_version_subquery = (
DocumentVersion.query
.with_entities(DocumentVersion.id, DocumentVersion.doc_id, DocumentVersion.url,
DocumentVersion.bucket_name, DocumentVersion.object_name,
DocumentVersion.file_type, DocumentVersion.sub_file_type,
DocumentVersion.file_size, DocumentVersion.language,
DocumentVersion.user_context, DocumentVersion.system_context,
DocumentVersion.user_metadata, DocumentVersion.system_metadata,
DocumentVersion.catalog_properties, DocumentVersion.created_at,
DocumentVersion.created_by, DocumentVersion.updated_at,
DocumentVersion.updated_by, DocumentVersion.processing,
DocumentVersion.processing_started_at, DocumentVersion.processing_finished_at,
DocumentVersion.processing_error)
.filter(DocumentVersion.id == (
DocumentVersion.query
.with_entities(DocumentVersion.id) # Selecteer alleen de id kolom
.filter(DocumentVersion.doc_id == Document.id)
.order_by(DocumentVersion.id.desc())
.limit(1)
.scalar_subquery()
))
.subquery()
)
self.version_alias = aliased(DocumentVersion, latest_version_subquery)
return Document.query.filter_by(catalog_id=catalog_id).outerjoin(
self.version_alias, Document.id == self.version_alias.doc_id
)
def apply_filters(self, query):
filters = request.args.to_dict(flat=False)
# Document filters
if 'validity' in filters:
now = dt.now(tz.utc).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)
)
)
# DocumentVersion filters - use the same alias from get_query
if filters.get('file_type') and self.version_alias is not None:
query = query.filter(self.version_alias.file_type == filters['file_type'][0])
if filters.get('processing') and self.version_alias is not None:
query = query.filter(self.version_alias.processing == (filters['processing'][0] == 'true'))
if filters.get('processing_error') and self.version_alias is not None:
if filters['processing_error'][0] == 'true':
query = query.filter(self.version_alias.processing_error.isnot(None))
elif filters['processing_error'][0] == 'false':
query = query.filter(self.version_alias.processing_error.is_(None))
# Controleer of start_date een waarde heeft voordat we proberen te parsen
if filters.get('start_date') and self.version_alias is not None and filters['start_date'][0].strip():
query = query.filter(
self.version_alias.processing_started_at >= dt.strptime(filters['start_date'][0], '%Y-%m-%d'))
# Controleer of end_date een waarde heeft voordat we proberen te parsen
if filters.get('end_date') and self.version_alias is not None and filters['end_date'][0].strip():
query = query.filter(
self.version_alias.processing_finished_at <= dt.strptime(filters['end_date'][0], '%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')
document_columns = ['id', 'name', 'valid_from', 'valid_to']
version_columns = ['file_type', 'processing', 'processing_started_at', 'processing_finished_at',
'processing_error']
if sort_by in self.allowed_sorts:
if sort_by in document_columns:
column = getattr(Document, sort_by)
elif sort_by in version_columns and self.version_alias is not None:
column = getattr(self.version_alias, sort_by)
else:
column = Document.id
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)
# Haal de laatste versies op voor elke document
items_with_versions = []
for doc in pagination.items:
latest_version = DocumentVersion.query.filter_by(doc_id=doc.id).order_by(desc(DocumentVersion.id)).first()
items_with_versions.append((doc, latest_version))
def format_date(date):
if isinstance(date, dt):
return date.strftime('%Y-%m-%d')
elif isinstance(date, str):
return date
else:
return ''
# Maak rijen voor de tabel met document en versie informatie
rows = []
for doc, version in items_with_versions:
if version:
row = [
{'value': doc.id, 'class': '', 'type': 'text'},
{'value': doc.name, 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_from), 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_to), 'class': '', 'type': 'text'},
{'value': version.id, 'class': '', 'type': 'text'},
{'value': version.file_type, 'class': '', 'type': 'text'},
{'value': 'Ja' if version.processing else 'Nee', 'class': '', 'type': 'text'},
{'value': version.processing_error or '', 'class': '', 'type': 'text'}
]
else:
row = [
{'value': doc.id, 'class': '', 'type': 'text'},
{'value': doc.name, 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_from), 'class': '', 'type': 'text'},
{'value': format_date(doc.valid_to), 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'},
{'value': '', 'class': '', 'type': 'text'}
]
rows.append(row)
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()
}
return render_template(self.template, **context)
def get_filter_options(self):
return {
'validity': [('valid', 'Valid'), ('all', 'All')],
'file_type': [('pdf', 'PDF'), ('docx', 'DOCX')],
'processing': [('true', 'Processing'), ('false', 'Not Processing')],
'processing_error': [('true', 'With Errors'), ('false', 'Without Errors')]
}

View File

@@ -0,0 +1,198 @@
from flask import redirect, flash, current_app, session, url_for
from flask_security import roles_accepted
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import desc
import ast
from common.models.interaction import Specialist, SpecialistMagicLink, ChatSession
from common.utils.nginx_utils import prefixed_url_for
from eveai_app.views.list_views.list_view_utils import render_list_view
# Specialists list view helper
def get_specialists_list_view():
"""Generate the specialists list view configuration"""
# Get all specialists
specialists_query = Specialist.query.order_by(Specialist.id)
all_specialists = specialists_query.all()
# Prepare data for Tabulator
data = []
for specialist in all_specialists:
data.append({
'id': specialist.id,
'name': specialist.name,
'type': specialist.type,
'type_version': specialist.type_version,
'active': specialist.active
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
{'title': 'Type Version', 'field': 'type_version'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]
# Action definitions
actions = [
{'value': 'edit_specialist', 'text': 'Edit Specialist', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'execute_specialist', 'text': 'Execute Specialist', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_specialist', 'text': 'Register Specialist', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False}
]
# Initial sort configuration
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Specialists',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'specialists_table',
'form_action': url_for('interaction_bp.handle_specialist_selection'),
'description': 'View and manage specialists',
'table_height': 800 # Hogere tabel voor specialists view
}
def get_assets_list_view():
"""Generate the assets list view configuration"""
# Get all assets
from common.models.interaction import EveAIAsset
assets_query = EveAIAsset.query.order_by(EveAIAsset.id)
all_assets = assets_query.all()
# Prepare data for Tabulator
data = []
for asset in all_assets:
data.append({
'id': asset.id,
'name': asset.name,
'type': asset.type,
'type_version': asset.type_version,
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
{'title': 'Type Version', 'field': 'type_version'}
]
# Action definitions
actions = [
{'value': 'edit_asset', 'text': 'Edit Asset', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_asset', 'text': 'Register Asset', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False}
]
# Initial sort configuration
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Assets',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'assets_table',
'form_action': url_for('interaction_bp.handle_asset_selection'),
'description': 'View and manage assets',
'table_height': 800
}
def get_magic_links_list_view():
"""Generate the specialist magic links list view configuration"""
# Get all specialist magic links
magic_links_query = SpecialistMagicLink.query.order_by(SpecialistMagicLink.id)
all_magic_links = magic_links_query.all()
# Prepare data for Tabulator
data = []
for magic_link in all_magic_links:
data.append({
'id': magic_link.id,
'name': magic_link.name,
'magic_link_code': magic_link.magic_link_code,
'specialist_id': magic_link.specialist_id
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Magic Link Code', 'field': 'magic_link_code'},
{'title': 'Specialist ID', 'field': 'specialist_id'}
]
# Action definitions
actions = [
{'value': 'edit_specialist_magic_link', 'text': 'Edit Magic Link', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_specialist_magic_link', 'text': 'Create Magic Link', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False}
]
# Initial sort configuration
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Specialist Magic Links',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'specialist_magic_links_table',
'form_action': url_for('interaction_bp.handle_specialist_magic_link_selection'),
'description': 'View and manage specialist magic links',
'table_height': 800
}
def get_chat_sessions_list_view():
"""Generate the chat sessions list view configuration"""
# Get all chat sessions ordered by session_start (descending)
chat_sessions_query = ChatSession.query.order_by(desc(ChatSession.session_start))
all_chat_sessions = chat_sessions_query.all()
# Prepare data for Tabulator
data = []
for chat_session in all_chat_sessions:
data.append({
'id': chat_session.id,
'session_id': chat_session.session_id,
'session_start': chat_session.session_start.strftime('%Y-%m-%d %H:%M:%S') if chat_session.session_start else '',
'session_end': chat_session.session_end.strftime('%Y-%m-%d %H:%M:%S') if chat_session.session_end else '',
})
# Column definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Session ID', 'field': 'session_id'},
{'title': 'Start Time', 'field': 'session_start'},
{'title': 'End Time', 'field': 'session_end'},
]
# Action definitions
actions = [
{'value': 'view_chat_session', 'text': 'View Details', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'chat_session_interactions', 'text': 'View Interactions', 'class': 'btn-secondary', 'requiresSelection': True}
]
# Initial sort configuration
initial_sort = [{'column': 'session_start', 'dir': 'desc'}]
return {
'title': 'Chat Sessions',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'chat_sessions_table',
'form_action': url_for('interaction_bp.handle_chat_session_selection'),
'description': 'View all chat sessions',
'table_height': 800
}

View File

@@ -0,0 +1,92 @@
from flask import render_template, current_app
def get_list_view_config(title, data, columns, actions, initial_sort=None, table_id=None, additional_config=None):
"""
Creates a standardized configuration dictionary for list views.
Args:
title (str): The title of the page
data (list): The data to display in the table
columns (list): Column definitions for the table
actions (list): Action button definitions
initial_sort (list, optional): Initial sort configuration
table_id (str, optional): Custom table ID, generated from title if not provided
additional_config (dict, optional): Any additional configuration to include
Returns:
dict: A standardized configuration dictionary
"""
# Generate table_id from title if not provided
if not table_id:
table_id = f"{title.lower().replace(' ', '_')}_table"
# Create the base configuration
config = {
'title': title,
'data': data, # Consistent data parameter name
'columns': columns,
'actions': actions,
'initial_sort': initial_sort or [],
'table_id': table_id
}
# Add any additional configuration
if additional_config:
config.update(additional_config)
return config
def render_list_view(template_name, title, data, columns, actions, form_action, initial_sort=None,
table_id=None, additional_config=None, **kwargs):
"""
Renders a list view template with standardized configuration.
Args:
template_name (str): The name of the template to render
title (str): The title of the page
data (list): The data to display in the table
columns (list): Column definitions for the table
actions (list): Action button definitions
form_action (str): Form action URL for the table
initial_sort (list, optional): Initial sort configuration
table_id (str, optional): Custom table ID
additional_config (dict, optional): Any additional configuration
**kwargs: Additional template variables
Returns:
str: The rendered template
"""
# Zorg ervoor dat table_id altijd een string is zonder spaties of speciale tekens
if not table_id:
table_id = f"{title.lower().replace(' ', '_').replace('-', '_')}_table"
# Zorg ervoor dat initial_sort altijd een lijst is
if initial_sort is None:
initial_sort = []
# Zorg ervoor dat actions altijd een lijst is
if actions is None:
actions = []
# Maak config dictionary
config = {
'title': title,
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': table_id,
'form_action': form_action
}
# Voeg extra configuratie toe indien aanwezig
if additional_config:
config.update(additional_config)
# Voeg eventuele extra template variabelen toe
config.update(kwargs)
current_app.logger.debug(f"List view config: {config}")
return render_template(template_name, **config)

View File

@@ -0,0 +1,129 @@
from flask import current_app, url_for
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.user import Partner, Tenant, PartnerService
def get_partners_list_view():
"""Genereer de partners lijst-weergave configuratie"""
# Haal alle partners op met hun tenant informatie
query = (db.session.query(
Partner.id,
Partner.code,
Partner.active,
Partner.logo_url,
Tenant.name.label('name')
).join(Tenant, Partner.tenant_id == Tenant.id).order_by(Partner.id))
try:
all_partners = query.all()
# Bereid data voor voor Tabulator
data = []
for partner in all_partners:
data.append({
'id': partner.id,
'code': partner.code,
'name': partner.name,
'active': partner.active
})
# Kolomdefinities
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Code', 'field': 'code'},
{'title': 'Name', 'field': 'name'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]
actions = [
{'value': 'set_session_partner', 'text': 'Set Session Partner', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'edit_partner', 'text': 'Edit Partner', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_partner', 'text': 'Register Partner', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Partners',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'partners_table',
'form_action': url_for('partner_bp.handle_partner_selection'),
'description': 'Manage partners'
}
except SQLAlchemyError as e:
current_app.logger.error(f"Error bij het ophalen van partners: {str(e)}")
return {
'title': 'Partners',
'data': [],
'columns': [],
'actions': [],
'initial_sort': [],
'table_id': 'partners_table',
'form_action': url_for('partner_bp.handle_partner_selection'),
'description': 'Er is een fout opgetreden bij het ophalen van partners. Probeer het later opnieuw.'
}
def get_partner_services_list_view(partner_id):
"""Genereer de partner services lijst-weergave configuratie"""
# Haal alle partner services op voor deze partner
query = PartnerService.query.filter(PartnerService.partner_id == partner_id)
try:
all_partner_services = query.all()
# Bereid data voor voor Tabulator
data = []
for service in all_partner_services:
data.append({
'id': service.id,
'name': service.name,
'type': service.type,
'type_version': service.type_version
})
# Kolomdefinities
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
{'title': 'Version', 'field': 'type_version'}
]
actions = [
{'value': 'edit_partner_service', 'text': 'Edit Service', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'add_partner_service_for_tenant', 'text': 'Assign to Tenant', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'create_partner_service', 'text': 'Register Service', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Partner Services',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'partner_services_table',
'form_action': url_for('partner_bp.handle_partner_service_selection'),
'description': 'Manage Partner Services'
}
except SQLAlchemyError as e:
current_app.logger.error(f"Error bij het ophalen van partner services: {str(e)}")
return {
'title': 'Partner Services',
'data': [],
'columns': [],
'actions': [],
'initial_sort': [],
'table_id': 'partner_services_table',
'form_action': url_for('partner_bp.handle_partner_service_selection'),
'description': 'Er is een fout opgetreden bij het ophalen van partner services. Probeer het later opnieuw.'
}

View File

@@ -0,0 +1,234 @@
from flask import redirect, flash, current_app, session, url_for
from flask_security import roles_accepted
from sqlalchemy.exc import SQLAlchemyError
import ast
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake
from common.services.user import UserServices
from eveai_app.views.list_views.list_view_utils import render_list_view
# Tenant list view helper
def get_tenants_list_view():
"""Generate the tenants list view configuration"""
# Get all tenants (no server side filtering - handled client-side)
tenant_query = Tenant.query.order_by(Tenant.id)
all_tenants = tenant_query.all()
# Prepare data for Tabulator
data = []
for tenant in all_tenants:
data.append({
'id': tenant.id,
'name': tenant.name,
'type': tenant.type
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Type', 'field': 'type'},
]
actions = [
{'value': 'select_tenant', 'text': 'Set Session Tenant', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'edit_tenant', 'text': 'Edit tenant', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_tenant', 'text': 'Register tenant', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Tenants',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'tenants_table',
'form_action': url_for('user_bp.handle_tenant_selection'),
'description': 'View and manage tenants'
}
# Users list view helper
def get_users_list_view(tenant_id):
"""Generate the users list view configuration for a specific tenant"""
# Get users for the tenant
query = User.query.filter_by(tenant_id=tenant_id).order_by(User.user_name)
users = query.all()
# Prepare data for Tabulator
data = []
for user in users:
data.append({
'id': user.id,
'user_name': user.user_name,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'active': user.active
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'User Name', 'field': 'user_name'},
{'title': 'Email', 'field': 'email'},
{'title': 'First Name', 'field': 'first_name'},
{'title': 'Last Name', 'field': 'last_name'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]
actions = [
{'value': 'edit_user', 'text': 'Edit User', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'resend_confirmation_email', 'text': 'Resend Confirmation', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'send_password_reset_email', 'text': 'Send Password Reset', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'reset_uniquifier', 'text': 'Reset Uniquifier', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'create_user', 'text': 'Register User', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'user_name', 'dir': 'asc'}]
return {
'title': 'Users',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'users_table',
'form_action': url_for('user_bp.handle_user_action'),
'description': f'Users for tenant {tenant_id}'
}
# Tenant Domains list view helper
def get_tenant_domains_list_view(tenant_id):
"""Generate the tenant domains list view configuration for a specific tenant"""
# Get domains for the tenant
query = TenantDomain.query.filter_by(tenant_id=tenant_id).order_by(TenantDomain.domain)
domains = query.all()
# Prepare data for Tabulator
data = []
for domain in domains:
data.append({
'id': domain.id,
'domain': domain.domain,
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Domain', 'field': 'domain'},
]
actions = [
{'value': 'edit_tenant_domain', 'text': 'Edit Domain', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_tenant_domain', 'text': 'Register Domain', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'domain', 'dir': 'asc'}]
return {
'title': 'Domains',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'tenant_domains_table',
'form_action': url_for('user_bp.handle_tenant_domain_action'),
'description': f'Domains for tenant {tenant_id}'
}
# Tenant Projects list view helper
def get_tenant_projects_list_view(tenant_id):
"""Generate the tenant projects list view configuration for a specific tenant"""
# Get projects for the tenant
query = TenantProject.query.filter_by(tenant_id=tenant_id).order_by(TenantProject.id)
projects = query.all()
# Prepare data for Tabulator
data = []
for project in projects:
data.append({
'id': project.id,
'name': project.name,
'visual_api_key': project.visual_api_key,
'responsible_email': project.responsible_email,
'active': project.active
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'API Key', 'field': 'visual_api_key'},
{'title': 'Responsible', 'field': 'responsible_email'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]
actions = [
{'value': 'edit_tenant_project', 'text': 'Edit Project', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'invalidate_tenant_project', 'text': 'Invalidate Project', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'delete_tenant_project', 'text': 'Delete Project', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'create_tenant_project', 'text': 'Register Project', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Projects',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'tenant_projects_table',
'form_action': url_for('user_bp.handle_tenant_project_selection'),
'description': f'Projects for tenant {tenant_id}'
}
# Tenant Makes list view helper
def get_tenant_makes_list_view(tenant_id):
"""Generate the tenant makes list view configuration for a specific tenant"""
# Get makes for the tenant
query = TenantMake.query.filter_by(tenant_id=tenant_id).order_by(TenantMake.id)
makes = query.all()
# Prepare data for Tabulator
data = []
for make in makes:
data.append({
'id': make.id,
'name': make.name,
'website': make.website,
'active': make.active
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Name', 'field': 'name'},
{'title': 'Website', 'field': 'website'},
{'title': 'Active', 'field': 'active', 'formatter': 'tickCross'}
]
actions = [
{'value': 'edit_tenant_make', 'text': 'Edit Make', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'set_as_default', 'text': 'Set as Default', 'class': 'btn-secondary', 'requiresSelection': True},
{'value': 'create_tenant_make', 'text': 'Create Make', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Makes',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'tenant_makes_table',
'form_action': url_for('user_bp.handle_tenant_make_selection'),
'description': f'Makes for tenant {tenant_id}'
}

View File

@@ -12,9 +12,11 @@ from common.utils.celery_utils import current_celery
from common.utils.eveai_exceptions import EveAIException from common.utils.eveai_exceptions import EveAIException
from common.utils.log_utils import format_query_results 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.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.view_assistants import form_validation_failed
from common.utils.nginx_utils import prefixed_url_for from common.utils.nginx_utils import prefixed_url_for
from .partner_forms import TriggerActionForm, EditPartnerForm, PartnerServiceForm, EditPartnerServiceForm from .partner_forms import TriggerActionForm, EditPartnerForm, PartnerServiceForm, EditPartnerServiceForm
from eveai_app.views.list_views.partner_list_views import get_partners_list_view, get_partner_services_list_view
from eveai_app.views.list_views.list_view_utils import render_list_view
partner_bp = Blueprint('partner_bp', __name__, url_prefix='/partner') partner_bp = Blueprint('partner_bp', __name__, url_prefix='/partner')
@@ -75,26 +77,8 @@ def edit_partner(partner_id):
@partner_bp.route('/partners', methods=['GET', 'POST']) @partner_bp.route('/partners', methods=['GET', 'POST'])
@roles_accepted('Super User') @roles_accepted('Super User')
def partners(): def partners():
page = request.args.get('page', 1, type=int) config = get_partners_list_view()
per_page = request.args.get('per_page', 10, type=int) return render_list_view('list_view.html', **config)
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))
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('partner/partners.html', rows=rows, pagination=pagination)
@partner_bp.route('/handle_partner_selection', methods=['POST']) @partner_bp.route('/handle_partner_selection', methods=['POST'])
@@ -199,23 +183,14 @@ def edit_partner_service(partner_service_id):
@partner_bp.route('/partner_services', methods=['GET', 'POST']) @partner_bp.route('/partner_services', methods=['GET', 'POST'])
@roles_accepted('Super User') @roles_accepted('Super User')
def partner_services(): def partner_services():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
partner = session.get('partner', None) partner = session.get('partner', None)
if not partner: if not partner:
flash('No partner has been selected. Set partner before adding services.', 'warning') flash('No partner has been selected. Set partner before adding services.', 'warning')
return redirect(prefixed_url_for('partner_bp.partners')) return redirect(prefixed_url_for('partner_bp.partners'))
partner_id = session['partner']['id'] partner_id = session['partner']['id']
query = PartnerService.query.filter(PartnerService.partner_id == partner_id) config = get_partner_services_list_view(partner_id)
return render_list_view('list_view.html', **config)
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('partner/partner_services.html', rows=rows, pagination=pagination)
@partner_bp.route('/handle_partner_service_selection', methods=['POST']) @partner_bp.route('/handle_partner_service_selection', methods=['POST'])

View File

@@ -55,7 +55,7 @@ def login():
current_app.logger.info(f'Login successful! Current User is {current_user.email}') current_app.logger.info(f'Login successful! Current User is {current_user.email}')
db.session.commit() db.session.commit()
if current_user.has_roles('Super User'): if current_user.has_roles('Super User'):
return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('user_bp.tenants'))
else: else:
return redirect(prefixed_url_for('user_bp.tenant_overview')) return redirect(prefixed_url_for('user_bp.tenant_overview'))
else: else:

View File

@@ -23,6 +23,10 @@ from common.services.user import TenantServices
from common.services.user import UserServices from common.services.user import UserServices
from common.utils.mail_utils import send_email from common.utils.mail_utils import send_email
from eveai_app.views.list_views.user_list_views import get_tenants_list_view, get_users_list_view, \
get_tenant_domains_list_view, get_tenant_projects_list_view, get_tenant_makes_list_view
from eveai_app.views.list_views.list_view_utils import render_list_view
user_bp = Blueprint('user_bp', __name__, url_prefix='/user') user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -43,7 +47,7 @@ def tenant():
if not UserServices.can_user_create_tenant(): if not UserServices.can_user_create_tenant():
current_app.logger.error(f'User {current_user.email} cannot create tenant') current_app.logger.error(f'User {current_user.email} cannot create tenant')
flash(f"You don't have the appropriate permissions to create a tenant", 'danger') flash(f"You don't have the appropriate permissions to create a tenant", 'danger')
return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('user_bp.tenants'))
form = TenantForm() form = TenantForm()
if request.method == 'GET': if request.method == 'GET':
code = f"TENANT-{str(uuid.uuid4())}" code = f"TENANT-{str(uuid.uuid4())}"
@@ -102,7 +106,7 @@ def tenant():
current_app.logger.info(f"Creating MinIO bucket for tenant {new_tenant.id}") current_app.logger.info(f"Creating MinIO bucket for tenant {new_tenant.id}")
minio_client.create_tenant_bucket(new_tenant.id) minio_client.create_tenant_bucket(new_tenant.id)
return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('user_bp.tenants'))
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
@@ -137,56 +141,12 @@ def edit_tenant(tenant_id):
return render_template('user/tenant.html', form=form, tenant_id=tenant_id) return render_template('user/tenant.html', form=form, tenant_id=tenant_id)
@user_bp.route('/select_tenant', methods=['GET', 'POST']) @user_bp.route('/tenants', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin') # Allow both roles @roles_accepted('Super User', 'Partner Admin') # Allow both roles
def select_tenant(): def tenants():
filter_form = TenantSelectionForm(request.form) # Get configuration and render the list view
page = request.args.get('page', 1, type=int) config = get_tenants_list_view()
per_page = request.args.get('per_page', 10, type=int) return render_list_view('list_view.html', **config)
# Start with a base query
query = Tenant.query
# Apply different filters based on user role
if current_user.has_roles('Partner Admin') and 'partner' in session:
# Get the partner's management service
management_service = next((service for service in session['partner']['services']
if service.get('type') == 'MANAGEMENT_SERVICE'), None)
if management_service:
# Get the partner's own tenant
partner_tenant_id = session['partner']['tenant_id']
# Get tenants managed by this partner through PartnerTenant relationships
managed_tenant_ids = db.session.query(PartnerTenant.tenant_id).filter_by(
partner_service_id=management_service['id']
).all()
# Convert list of tuples to flat list
managed_tenant_ids = [tenant_id for (tenant_id,) in managed_tenant_ids]
# Include partner's own tenant in the list
allowed_tenant_ids = [partner_tenant_id] + managed_tenant_ids
# Filter query to only show allowed tenants
query = query.filter(Tenant.id.in_(allowed_tenant_ids))
# Apply form filters (for both Super User and Partner Admin)
if filter_form.validate_on_submit():
if filter_form.types.data:
query = query.filter(Tenant.type.in_(filter_form.types.data))
if filter_form.search.data:
search = f"%{filter_form.search.data}%"
query = query.filter(Tenant.name.ilike(search))
# Finalize query
query = query.order_by(Tenant.name)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
tenants = pagination.items
rows = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', ''), ('type', '')])
return render_template('user/select_tenant.html', rows=rows, pagination=pagination, filter_form=filter_form)
@user_bp.route('/handle_tenant_selection', methods=['POST']) @user_bp.route('/handle_tenant_selection', methods=['POST'])
@@ -201,7 +161,7 @@ def handle_tenant_selection():
if not UserServices.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}.") 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') flash(f"You are not authenticated to manage tenant {tenant_id}", 'danger')
return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('user_bp.tenants'))
the_tenant = Tenant.query.get(tenant_id) the_tenant = Tenant.query.get(tenant_id)
# set tenant information in the session # set tenant information in the session
@@ -217,7 +177,7 @@ def handle_tenant_selection():
return redirect(prefixed_url_for('user_bp.tenant_overview')) return redirect(prefixed_url_for('user_bp.tenant_overview'))
# Add more conditions for other actions # Add more conditions for other actions
return redirect(prefixed_url_for('select_tenant')) return redirect(prefixed_url_for('tenants'))
@user_bp.route('/tenant_overview', methods=['GET']) @user_bp.route('/tenant_overview', methods=['GET'])
@@ -342,21 +302,10 @@ def edit_user(user_id):
@user_bp.route('/view_users') @user_bp.route('/view_users')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_users(): def view_users():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
tenant_id = session.get('tenant').get('id') tenant_id = session.get('tenant').get('id')
query = User.query.filter_by(tenant_id=tenant_id).order_by(User.user_name) config = get_users_list_view(tenant_id)
return render_list_view('list_view.html', **config)
pagination = query.paginate(page=page, per_page=per_page)
users = pagination.items
# prepare table data
rows = prepare_table_for_macro(users, [('id', ''), ('user_name', ''), ('email', '')])
# Render the users in a template
return render_template('user/view_users.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_user_action', methods=['POST']) @user_bp.route('/handle_user_action', methods=['POST'])
@@ -385,24 +334,13 @@ def handle_user_action():
return redirect(prefixed_url_for('user_bp.view_users')) return redirect(prefixed_url_for('user_bp.view_users'))
# Tenant Domain Management ------------------------------------------------------------------------ # Tenant Domain Management (Probably obsolete )------------------------------------------------------------------------
@user_bp.route('/view_tenant_domains') @user_bp.route('/tenant_domains')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_tenant_domains(): def tenant_domains():
page = request.args.get('page', 1, type=int) tenant_id = session['tenant']['id']
per_page = request.args.get('per_page', 10, type=int) config = get_tenant_domains_list_view(tenant_id)
return render_list_view('list_view.html', **config)
tenant_id = session.get('tenant').get('id')
query = TenantDomain.query.filter_by(tenant_id=tenant_id).order_by(TenantDomain.domain)
pagination = query.paginate(page=page, per_page=per_page)
tenant_domains = pagination.items
# prepare table data
rows = prepare_table_for_macro(tenant_domains, [('id', ''), ('domain', ''), ('valid_to', '')])
# Render the users in a template
return render_template('user/view_tenant_domains.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_tenant_domain_action', methods=['POST']) @user_bp.route('/handle_tenant_domain_action', methods=['POST'])
@@ -418,7 +356,7 @@ def handle_tenant_domain_action():
if action == 'edit_tenant_domain': if action == 'edit_tenant_domain':
return redirect(prefixed_url_for('user_bp.edit_tenant_domain', tenant_domain_id=tenant_domain_id)) return redirect(prefixed_url_for('user_bp.edit_tenant_domain', tenant_domain_id=tenant_domain_id))
# Add more conditions for other actions # Add more conditions for other actions
return redirect(prefixed_url_for('view_tenant_domains')) return redirect(prefixed_url_for('tenant_domains'))
@user_bp.route('/tenant_domain', methods=['GET', 'POST']) @user_bp.route('/tenant_domain', methods=['GET', 'POST'])
@@ -470,7 +408,7 @@ def edit_tenant_domain(tenant_domain_id):
f'for tenant {session["tenant"]["id"]}' f'for tenant {session["tenant"]["id"]}'
f'Error: {str(e)}') f'Error: {str(e)}')
return redirect( return redirect(
prefixed_url_for('user_bp.view_tenant_domains', prefixed_url_for('user_bp.tenant_domains',
tenant_id=session['tenant']['id'])) # Assuming there's a user profile view to redirect to tenant_id=session['tenant']['id'])) # Assuming there's a user profile view to redirect to
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
@@ -535,21 +473,9 @@ def tenant_project():
@user_bp.route('/tenant_projects', methods=['GET', 'POST']) @user_bp.route('/tenant_projects', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_projects(): def tenant_projects():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
query = TenantProject.query.filter_by(tenant_id=tenant_id).order_by(TenantProject.id) config = get_tenant_projects_list_view(tenant_id)
return render_list_view('list_view.html', **config)
pagination = query.paginate(page=page, per_page=per_page)
the_tenant_projects = pagination.items
# prepare table data
rows = prepare_table_for_macro(the_tenant_projects, [('id', ''), ('name', ''), ('visual_api_key', ''),
('responsible_email', ''), ('active', '')])
# Render the catalogs in a template
return render_template('user/tenant_projects.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_tenant_project_selection', methods=['POST']) @user_bp.route('/handle_tenant_project_selection', methods=['POST'])
@@ -677,21 +603,9 @@ def tenant_make():
@user_bp.route('/tenant_makes', methods=['GET', 'POST']) @user_bp.route('/tenant_makes', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_makes(): def tenant_makes():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
query = TenantMake.query.filter_by(tenant_id=tenant_id).order_by(TenantMake.id) config = get_tenant_makes_list_view(tenant_id)
return render_list_view('list_view.html', **config)
pagination = query.paginate(page=page, per_page=per_page)
tenant_makes = pagination.items
# prepare table data
rows = prepare_table_for_macro(tenant_makes,
[('id', ''), ('name', ''), ('website', ''), ('active', '')])
# Render the tenant makes in a template
return render_template('user/tenant_makes.html', rows=rows, pagination=pagination)
@user_bp.route('/tenant_make/<int:tenant_make_id>', methods=['GET', 'POST']) @user_bp.route('/tenant_make/<int:tenant_make_id>', methods=['GET', 'POST'])
@@ -763,16 +677,13 @@ def handle_tenant_make_selection():
# Update session data if necessary # Update session data if necessary
if 'tenant' in session: if 'tenant' in session:
session['tenant'] = tenant.to_dict() session['tenant'] = tenant.to_dict()
return None
return None
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.session.rollback() db.session.rollback()
flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger') flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update default tenant make. Error: {str(e)}') current_app.logger.error(f'Failed to update default tenant make. Error: {str(e)}')
return redirect(prefixed_url_for('user_bp.tenant_makes')) # Altijd teruggaan naar de tenant_makes pagina
return redirect(prefixed_url_for('user_bp.tenant_makes'))
return None
def reset_uniquifier(user): def reset_uniquifier(user):

View File

@@ -43,8 +43,28 @@ import 'select2';
import { createJSONEditor } from 'vanilla-jsoneditor'; import { createJSONEditor } from 'vanilla-jsoneditor';
window.createJSONEditor = createJSONEditor; window.createJSONEditor = createJSONEditor;
// Importeer Tabulator en maak deze globaal beschikbaar
import { TabulatorFull } from 'tabulator-tables';
window.Tabulator = TabulatorFull;
import './tabulator-setup.js'; import './tabulator-setup.js';
// Importeer extra tabulator setup script
import '../../../eveai_app/static/assets/js/eveai-tabulator-setup.js';
// Controleer of alle benodigde Tabulator modules correct zijn geladen
document.addEventListener('DOMContentLoaded', function() {
if (typeof window.Tabulator === 'function') {
console.log(`Tabulator versie: ${window.Tabulator.version || 'onbekend'}`);
// Zorg ervoor dat benodigde modules beschikbaar zijn
window.Tabulator.modules = window.Tabulator.modules || {};
window.Tabulator.modules.format = window.Tabulator.modules.format || { formatters: {} };
window.Tabulator.modules.sort = window.Tabulator.modules.sort || { sorters: {} };
window.Tabulator.modules.filter = window.Tabulator.modules.filter || { filters: {} };
}
});
import { createApp } from 'vue'; import { createApp } from 'vue';
window.Vue = { createApp }; window.Vue = { createApp };

View File

@@ -1,5 +1,30 @@
// JavaScript imports // JavaScript imports
import { TabulatorFull as Tabulator } from 'tabulator-tables'; import { TabulatorFull } from 'tabulator-tables';
/**
* Basis Tabulator Setup
* Dit bestand bevat configuratie voor Tabulator tabellen
*/
// Algemene instellingen voor Tabulator
document.addEventListener('DOMContentLoaded', function() {
if (typeof TabulatorFull === 'function') {
console.log('Tabulator bibliotheek is geladen en geconfigureerd');
} else {
console.error('Tabulator bibliotheek is niet beschikbaar');
}
});
// Maak Tabulator globaal beschikbaar // Maak Tabulator globaal beschikbaar
window.Tabulator = Tabulator; window.Tabulator = TabulatorFull;
// Zorg ervoor dat de formattermodule correct is geregistreerd
if (typeof TabulatorFull.prototype.moduleRegistered !== 'function' ||
!TabulatorFull.prototype.moduleRegistered('format')) {
console.warn('Format module niet gevonden in Tabulator, wordt toegevoegd');
// Basismodule definiëren indien niet aanwezig
TabulatorFull.prototype.moduleRegistered = function(name) {
return this.modules && this.modules[name];
};
TabulatorFull.modules = TabulatorFull.modules || {};
TabulatorFull.modules.format = TabulatorFull.modules.format || {};
TabulatorFull.modules.format.formatters = TabulatorFull.modules.format.formatters || {};
}