- 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:
@@ -105,6 +105,12 @@ class Document(db.Model):
|
||||
# Relations
|
||||
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):
|
||||
return f"<Document {self.id}: {self.name}>"
|
||||
|
||||
|
||||
@@ -12,7 +12,16 @@ def prefixed_url_for(endpoint, **values):
|
||||
|
||||
if external:
|
||||
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))
|
||||
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
|
||||
|
||||
|
||||
@@ -96,9 +96,9 @@
|
||||
|
||||
/* Style for the table 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 */
|
||||
color: #ffffff; /* White text for better contrast on gradient */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Style for the headers container */
|
||||
@@ -108,14 +108,14 @@
|
||||
|
||||
/* Style for the header cells */
|
||||
.ordered-list-editor .tabulator-col {
|
||||
background: transparent; /* Let the header gradient show through */
|
||||
background: transparent;
|
||||
padding: 8px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: table-cell !important; /* Force display as table cell */
|
||||
box-sizing: border-box !important; /* Include padding in width calculation */
|
||||
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 */
|
||||
@@ -135,7 +135,7 @@
|
||||
word-break: break-word; /* Break words to prevent horizontal overflow */
|
||||
font-weight: bold;
|
||||
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 */
|
||||
@@ -385,7 +385,7 @@
|
||||
}
|
||||
/* Extra specifieke override stijlen voor tabulator paginator en footer */
|
||||
.tabulator .tabulator-footer .tabulator-paginator {
|
||||
background: transparent !important; /* Transparante achtergrond, want de footer heeft al gradient */
|
||||
background: transparent !important;
|
||||
border-top: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -407,14 +407,14 @@
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid white !important;
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
color: #67518c !important;
|
||||
font-size: 0.85rem !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
/* Styling voor de paginator container */
|
||||
.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-weight: normal !important;
|
||||
margin: 0 5px !important;
|
||||
@@ -465,7 +465,7 @@
|
||||
}
|
||||
/* Styling voor de 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;
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
border-bottom-right-radius: 0.375rem;
|
||||
@@ -485,13 +485,13 @@
|
||||
|
||||
.tabulator-paginator .tabulator-page:hover {
|
||||
background-color: var(--bs-secondary);
|
||||
color: white;
|
||||
color: #67518c;
|
||||
}
|
||||
|
||||
.tabulator-paginator .tabulator-page.active,
|
||||
.tabulator .tabulator-footer .tabulator-page.active {
|
||||
background-color: var(--bs-primary) !important;
|
||||
color: white !important;
|
||||
color: #67518c !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@
|
||||
|
||||
.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-bottom-left-radius: 0.375rem !important;
|
||||
border-bottom-right-radius: 0.375rem !important;
|
||||
@@ -548,7 +548,7 @@
|
||||
|
||||
/* Algemene tabulator header stijlen -------------------------------------------------------------- */
|
||||
.tabulator .tabulator-header {
|
||||
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%);
|
||||
background: #e1e1e1;
|
||||
border-bottom: 2px solid var(--bs-secondary);
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -579,7 +579,7 @@
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col-title {
|
||||
color: #ffffff;
|
||||
color: #67518c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@
|
||||
}
|
||||
|
||||
.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 */
|
||||
}
|
||||
|
||||
@@ -607,7 +607,7 @@
|
||||
|
||||
/* Tabulator List View paginator stijlen */
|
||||
.tabulator-list-view .tabulator-footer {
|
||||
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%); /* Match EveAI gradient */
|
||||
background: #e1e1e1;
|
||||
padding: 8px;
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
border-bottom-right-radius: 0.375rem;
|
||||
@@ -635,20 +635,20 @@
|
||||
|
||||
/* Stijl voor de tabel 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 */
|
||||
color: #ffffff; /* Witte tekst voor beter contrast op gradient */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Stijl voor de header cellen */
|
||||
.tabulator-list-view .tabulator-col {
|
||||
background: transparent; /* Laat de header gradient doorschijnen */
|
||||
background: transparent;
|
||||
padding: 8px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
box-sizing: border-box !important; /* Padding meenemen in breedte berekening */
|
||||
position: relative !important; /* Juiste positionering verzekeren */
|
||||
color: #ffffff; /* Witte tekst voor beter contrast op gradient */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Voorkom kleurverandering bij hover over kolomkoppen */
|
||||
@@ -662,7 +662,7 @@
|
||||
word-break: break-word; /* Breek woorden om horizontale overflow te voorkomen */
|
||||
font-weight: bold;
|
||||
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 */
|
||||
|
||||
183
eveai_app/static/assets/js/eveai-list-view.js
Normal file
183
eveai_app/static/assets/js/eveai-list-view.js
Normal 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');
|
||||
83
eveai_app/static/assets/js/eveai-tabulator-setup.js
Normal file
83
eveai_app/static/assets/js/eveai-tabulator-setup.js
Normal 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');
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" itemscope itemtype="http://schema.org/WebPage">
|
||||
|
||||
|
||||
{% include 'head.html' %}
|
||||
|
||||
<body class="presentation-page bg-gray-200">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -1,301 +1,493 @@
|
||||
<script type="module">
|
||||
window.EveAI = window.EveAI || {};
|
||||
window.EveAI.ListView = {
|
||||
instances: {},
|
||||
<style>
|
||||
button.disabled, button:disabled {
|
||||
opacity: 0.65;
|
||||
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 = {}) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`Container ${containerId} not found`);
|
||||
return null;
|
||||
<div id="{{ table_id }}" class="tabulator-list-view"></div>
|
||||
|
||||
<div class="row mt-3">
|
||||
{% set right_actions = actions|selectattr('position', 'equalto', 'right')|list %}
|
||||
<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
|
||||
const config = {
|
||||
data: [],
|
||||
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
|
||||
};
|
||||
// Voeg de benodigde functies toe als ze nog niet bestaan
|
||||
if (!window.EveAI.ListView.initialize) {
|
||||
window.EveAI.ListView.instances = {};
|
||||
|
||||
// Check if Tabulator is available
|
||||
if (typeof window.Tabulator !== 'function') {
|
||||
console.error('Tabulator not loaded (window.Tabulator missing).');
|
||||
container.innerHTML = `<div class="alert alert-danger">
|
||||
<strong>Error:</strong> Tabulator not loaded
|
||||
</div>`;
|
||||
return null;
|
||||
}
|
||||
// Initialize functie
|
||||
window.EveAI.ListView.initialize = function(containerId, options = {}) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`Container ${containerId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Default configuration
|
||||
const config = {
|
||||
data: [],
|
||||
columns: [],
|
||||
initialSort: [],
|
||||
initialFilters: [],
|
||||
filterableColumns: [],
|
||||
selectable: true,
|
||||
actions: [],
|
||||
usePagination: true,
|
||||
tableHeight: 700,
|
||||
...options
|
||||
};
|
||||
|
||||
// Create Tabulator table
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'tabulator-container tabulator-list-view mt-3';
|
||||
container.appendChild(tableContainer);
|
||||
// Check if Tabulator is available
|
||||
if (typeof window.Tabulator !== 'function') {
|
||||
console.error('Tabulator not loaded (window.Tabulator missing).');
|
||||
container.innerHTML = `<div class="alert alert-danger">
|
||||
<strong>Error:</strong> Tabulator not loaded
|
||||
</div>`;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Basisinstellingen voor tabel configuratie
|
||||
const tableConfig = {
|
||||
data: config.data,
|
||||
columns: this._buildColumns(config.columns, config.selectable),
|
||||
layout: "fitColumns",
|
||||
try {
|
||||
// Create Tabulator table
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'tabulator-container tabulator-list-view mt-3';
|
||||
container.appendChild(tableContainer);
|
||||
|
||||
// Conditionele paginatie of progressieve lading
|
||||
...(config.usePagination ? {
|
||||
pagination: "local",
|
||||
paginationSize: 25,
|
||||
paginationSizeSelector: [10, 25, 50, 100]
|
||||
} : {
|
||||
progressiveLoad: "scroll" // Gebruik progressieve lading voor grote datasets wanneer paginatie uitstaat
|
||||
}),
|
||||
// Basisinstellingen voor tabel configuratie
|
||||
const tableConfig = {
|
||||
data: config.data,
|
||||
columns: this._buildColumns(config.columns, config.selectable),
|
||||
layout: "fitColumns",
|
||||
|
||||
// Gemeenschappelijke configuratie voor beide modi
|
||||
movableColumns: true,
|
||||
resizableRows: false, // Schakel dit uit om prestatieproblemen te voorkomen
|
||||
initialSort: config.initialSort,
|
||||
initialFilter: config.initialFilters,
|
||||
selectable: 1, // Beperk tot maximaal 1 rij selectie
|
||||
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
|
||||
// Conditionele paginatie of progressieve lading
|
||||
...(config.usePagination ? {
|
||||
pagination: "local",
|
||||
paginationSize: 25,
|
||||
paginationSizeSelector: [10, 25, 50, 100]
|
||||
} : {
|
||||
progressiveLoad: "scroll" // Gebruik progressieve lading voor grote datasets wanneer paginatie uitstaat
|
||||
}),
|
||||
|
||||
// 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) {
|
||||
console.log('Row clicked!', row.getData());
|
||||
// Stop event propagation om te voorkomen dat andere handlers interfereren
|
||||
e.stopPropagation();
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
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();
|
||||
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()));
|
||||
});
|
||||
|
||||
// Row selection event met de Tabulator API
|
||||
table.on("rowSelectionChanged", function(data, rows){
|
||||
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)
|
||||
EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
|
||||
EveAI.ListView._updateActionButtons(containerId);
|
||||
},
|
||||
rowFormatter: function(row) {
|
||||
// Voeg cursor-style toe om aan te geven dat rijen klikbaar zijn
|
||||
if (config.selectable) {
|
||||
row.getElement().style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
return table;
|
||||
} catch (error) {
|
||||
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
|
||||
const table = new Tabulator(tableContainer, tableConfig);
|
||||
// Voeg formattering toe volgens de juiste manier per versie
|
||||
if (col.formatter) {
|
||||
if (isTabulator6Plus) {
|
||||
column.formatterParams = { formatter: col.formatter };
|
||||
} else {
|
||||
column.formatter = col.formatter;
|
||||
}
|
||||
}
|
||||
|
||||
// Create action buttons
|
||||
if (config.actions.length > 0) {
|
||||
this._createActionButtons(container, config.actions, table);
|
||||
// Voeg filtering toe volgens de juiste manier per versie
|
||||
if (isTabulator6Plus) {
|
||||
// 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
|
||||
this.instances[containerId] = {
|
||||
table: table,
|
||||
config: config,
|
||||
selectedRow: null
|
||||
};
|
||||
// Zoek buttons in de volledige formulier context (niet alleen de container)
|
||||
const form = document.getElementById(`${containerId}-form`) || document.querySelector('form');
|
||||
const buttons = form ? form.querySelectorAll('button[onclick*="handleListViewAction"]') :
|
||||
document.querySelectorAll('button[onclick*="handleListViewAction"]');
|
||||
|
||||
// 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) {
|
||||
// Stop event propagation om te voorkomen dat andere handlers interfereren
|
||||
e.stopPropagation();
|
||||
row.getTable().deselectRow();
|
||||
row.select();
|
||||
console.log(`Updating buttons voor ${containerId}, ${buttons.length} buttons gevonden, selectedRow:`, instance.selectedRow);
|
||||
|
||||
buttons.forEach(button => {
|
||||
// Parse the onclick attribute to get the action value and requiresSelection parameter
|
||||
const onclickAttr = button.getAttribute('onclick');
|
||||
const match = onclickAttr.match(/handleListViewAction\('([^']+)',\s*(true|false)\)/i);
|
||||
if (match) {
|
||||
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
|
||||
table.on("rowSelectionChanged", function(data, rows){
|
||||
console.log("Tabulator API: Aantal geselecteerde rijen:", rows.length);
|
||||
console.log("Tabulator API: Geselecteerde rijen:", rows.map(r => r.getData()));
|
||||
// Update hidden input with selected row data
|
||||
this._updateSelectedRowInput(containerId);
|
||||
};
|
||||
|
||||
// Update de geselecteerde rij (enkelvoud)
|
||||
EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
|
||||
EveAI.ListView._updateActionButtons(containerId);
|
||||
});
|
||||
// Update selected row input functie
|
||||
window.EveAI.ListView._updateSelectedRowInput = function(containerId) {
|
||||
const instance = this.instances[containerId];
|
||||
let hiddenInput = document.getElementById(`${containerId}-selected-row`);
|
||||
|
||||
// Opmerking: row selection event wordt nu afgehandeld via de Tabulator API
|
||||
|
||||
return table;
|
||||
} catch (error) {
|
||||
console.error('Error initializing ListView:', error);
|
||||
container.innerHTML = `<div class="alert alert-danger">
|
||||
<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';
|
||||
if (!hiddenInput) {
|
||||
hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'selected_row';
|
||||
hiddenInput.id = `${containerId}-selected-row`;
|
||||
document.getElementById(containerId).appendChild(hiddenInput);
|
||||
}
|
||||
|
||||
// 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};
|
||||
if (instance.selectedRow) {
|
||||
// Bewaar de geselecteerde rij-ID
|
||||
hiddenInput.value = JSON.stringify({value: instance.selectedRow.id});
|
||||
} 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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
82
eveai_app/templates/list_view.html
Normal file
82
eveai_app/templates/list_view.html
Normal 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 %}
|
||||
@@ -69,10 +69,9 @@
|
||||
<ul class="navbar-nav navbar-nav-hover mx-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ 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': '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 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']},
|
||||
@@ -100,8 +99,7 @@
|
||||
{'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': '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 Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Document Processing', 'url': '/document/documents_processing', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
]) }}
|
||||
{% endif %}
|
||||
@@ -115,9 +113,9 @@
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ 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': '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']},
|
||||
]) }}
|
||||
{% endif %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -1,14 +1,9 @@
|
||||
{#dist/main.js contains all used javascript libraries#}
|
||||
<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_ordered_list_editor.html' %}
|
||||
{% include 'eveai_list_view.html' %}
|
||||
<!-- ListView component is now included in base.html -->
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -21,7 +21,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'user_bp.view_tenant_domains') }}
|
||||
{{ render_pagination(pagination, 'user_bp.tenant_domains') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -25,9 +25,10 @@ from common.utils.middleware import mw_before_request
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||
from eveai_app.views.list_views.document_list_view import DocumentListView
|
||||
from eveai_app.views.list_views.document_version_list_view import DocumentVersionListView
|
||||
from eveai_app.views.list_views.full_document_list_view import FullDocumentListView
|
||||
from eveai_app.views.list_views.list_view_utils import render_list_view
|
||||
from eveai_app.views.list_views.document_list_views import get_catalogs_list_view, get_processors_list_view, \
|
||||
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')
|
||||
|
||||
@@ -86,19 +87,9 @@ def catalog():
|
||||
@document_bp.route('/catalogs', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def catalogs():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
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)
|
||||
# Haal configuratie op en render de lijst-weergave
|
||||
config = get_catalogs_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@document_bp.route('/handle_catalog_selection', methods=['POST'])
|
||||
@@ -234,25 +225,14 @@ def edit_processor(processor_id):
|
||||
@document_bp.route('/processors', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
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)
|
||||
if not catalog_id:
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
|
||||
query = Processor.query.filter_by(catalog_id=catalog_id).order_by(Processor.id)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
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)
|
||||
# Get configuration and render the list view
|
||||
config = get_processors_list_view(catalog_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@document_bp.route('/handle_processor_selection', methods=['POST'])
|
||||
@@ -345,25 +325,14 @@ def edit_retriever(retriever_id):
|
||||
@document_bp.route('/retrievers', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
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)
|
||||
if not catalog_id:
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
|
||||
query = Retriever.query.filter_by(catalog_id=catalog_id).order_by(Retriever.id)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
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)
|
||||
# Get configuration and render the list view
|
||||
config = get_retrievers_list_view(catalog_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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')
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
|
||||
view = DocumentListView(Document, 'document/documents.html', per_page=10)
|
||||
return view.get()
|
||||
config = get_documents_list_view(catalog_id)
|
||||
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')
|
||||
def full_documents():
|
||||
def documents_processing():
|
||||
catalog_id = session.get('catalog_id', None)
|
||||
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'))
|
||||
|
||||
view = FullDocumentListView(Document, 'document/full_documents.html', per_page=10)
|
||||
return view.get()
|
||||
config = get_documents_processing_list_view(catalog_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@document_bp.route('/handle_document_selection', methods=['POST'])
|
||||
@@ -536,14 +505,23 @@ def handle_document_selection():
|
||||
|
||||
match action:
|
||||
case 'edit_document':
|
||||
return redirect(prefixed_url_for('document_bp.edit_document_view', document_id=doc_id))
|
||||
case 'document_versions':
|
||||
return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_id))
|
||||
case 'refresh_document':
|
||||
return redirect(prefixed_url_for('document_bp.edit_document', document_id=doc_id))
|
||||
case 'refresh':
|
||||
refresh_document_view(doc_id)
|
||||
return redirect(prefixed_url_for('document_bp.document_versions', document_id=doc_id))
|
||||
case 're_embed_latest_versions':
|
||||
re_embed_latest_versions()
|
||||
return redirect(prefixed_url_for('document_bp.documents', document_id=doc_id))
|
||||
case 're_process':
|
||||
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
|
||||
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'])
|
||||
@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
|
||||
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'])
|
||||
@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)
|
||||
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}')
|
||||
|
||||
|
||||
@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'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def view_document_version_markdown(document_version_id):
|
||||
|
||||
@@ -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.nginx_utils import prefixed_url_for
|
||||
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')
|
||||
|
||||
@@ -45,48 +47,23 @@ def license_tier():
|
||||
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')
|
||||
|
||||
return redirect(prefixed_url_for('entitlements_bp.view_license_tiers'))
|
||||
return redirect(prefixed_url_for('entitlements_bp.license_tiers'))
|
||||
else:
|
||||
form_validation_failed(request, 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')
|
||||
def view_license_tiers():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
today = dt.now(tz.utc)
|
||||
def license_tiers():
|
||||
config = get_license_tiers_list_view()
|
||||
|
||||
query = LicenseTier.query.filter(
|
||||
or_(
|
||||
LicenseTier.end_date == None,
|
||||
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))
|
||||
# Check if there was an error in getting the configuration
|
||||
if config.get('error'):
|
||||
return render_template("index.html")
|
||||
|
||||
query = query.order_by(LicenseTier.start_date.desc(), LicenseTier.id)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
license_tiers = pagination.items
|
||||
|
||||
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())
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
# 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'])
|
||||
@@ -253,79 +230,16 @@ def edit_license(license_id):
|
||||
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')
|
||||
def view_usages():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
def licenses():
|
||||
config = get_license_list_view()
|
||||
|
||||
if not session.get('tenant', None):
|
||||
flash('You can only view usage for a Tenant. Select a Tenant to continue!', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.select_tenant'))
|
||||
# Check if there was an error in getting the configuration
|
||||
if config.get('error'):
|
||||
return render_template("index.html")
|
||||
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
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)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@entitlements_bp.route('/handle_license_selection', methods=['POST'])
|
||||
@@ -343,7 +257,7 @@ def handle_license_selection():
|
||||
case 'view_periods':
|
||||
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))
|
||||
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')
|
||||
@@ -356,7 +270,7 @@ def view_license_periods(license_id):
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
if license.tenant_id != tenant_id:
|
||||
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
|
||||
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))
|
||||
|
||||
|
||||
@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')
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def active_license_usage():
|
||||
@@ -409,7 +330,7 @@ def active_license_usage():
|
||||
tenant_id = session.get('tenant', {}).get('id')
|
||||
if not tenant_id:
|
||||
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 \
|
||||
.join(License) \
|
||||
|
||||
@@ -31,6 +31,10 @@ from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAge
|
||||
EditEveAIToolForm, ExecuteSpecialistForm,
|
||||
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')
|
||||
|
||||
|
||||
@@ -56,17 +60,9 @@ def before_request():
|
||||
|
||||
@interaction_bp.route('/chat_sessions', methods=['GET', 'POST'])
|
||||
def chat_sessions():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
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)
|
||||
# Get configuration and render the list view
|
||||
config = get_chat_sessions_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
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
|
||||
specialists_data = []
|
||||
for specialist in all_specialists:
|
||||
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)
|
||||
# Get configuration and render the list view
|
||||
config = get_specialists_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def specialist_magic_links():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
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)
|
||||
# Get configuration and render the list view
|
||||
config = get_magic_links_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@interaction_bp.route('/handle_specialist_magic_link_selection', methods=['POST'])
|
||||
@@ -798,19 +749,15 @@ def handle_specialist_magic_link_selection():
|
||||
# Routes for Asset Management ---------------------------------------------------------------------
|
||||
@interaction_bp.route('/assets', methods=['GET', 'POST'])
|
||||
def assets():
|
||||
from eveai_app.views.list_views.assets_list_view import AssetsListView
|
||||
view = AssetsListView(
|
||||
model=EveAIAsset,
|
||||
template='interaction/assets.html',
|
||||
per_page=10
|
||||
)
|
||||
return view.get()
|
||||
config = get_assets_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@interaction_bp.route('/handle_asset_selection', methods=['POST'])
|
||||
def handle_asset_selection():
|
||||
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':
|
||||
return redirect(prefixed_url_for('interaction_bp.edit_asset', asset_id=asset_id))
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# List Views module initialization
|
||||
# This module contains utility functions for handling list views
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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')]
|
||||
}
|
||||
324
eveai_app/views/list_views/document_list_views.py
Normal file
324
eveai_app/views/list_views/document_list_views.py
Normal 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.'
|
||||
}
|
||||
@@ -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')]
|
||||
}
|
||||
164
eveai_app/views/list_views/entitlement_list_views.py
Normal file
164
eveai_app/views/list_views/entitlement_list_views.py
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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')]
|
||||
}
|
||||
198
eveai_app/views/list_views/interaction_list_views.py
Normal file
198
eveai_app/views/list_views/interaction_list_views.py
Normal 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
|
||||
}
|
||||
|
||||
92
eveai_app/views/list_views/list_view_utils.py
Normal file
92
eveai_app/views/list_views/list_view_utils.py
Normal 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)
|
||||
129
eveai_app/views/list_views/partner_list_views.py
Normal file
129
eveai_app/views/list_views/partner_list_views.py
Normal 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.'
|
||||
}
|
||||
234
eveai_app/views/list_views/user_list_views.py
Normal file
234
eveai_app/views/list_views/user_list_views.py
Normal 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}'
|
||||
}
|
||||
@@ -12,9 +12,11 @@ from common.utils.celery_utils import current_celery
|
||||
from common.utils.eveai_exceptions import EveAIException
|
||||
from common.utils.log_utils import format_query_results
|
||||
from common.utils.model_logging_utils import update_logging_information, set_logging_information
|
||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||
from common.utils.view_assistants import form_validation_failed
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
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')
|
||||
|
||||
@@ -75,26 +77,8 @@ def edit_partner(partner_id):
|
||||
@partner_bp.route('/partners', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User')
|
||||
def partners():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
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)
|
||||
config = get_partners_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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'])
|
||||
@roles_accepted('Super User')
|
||||
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)
|
||||
if not partner:
|
||||
flash('No partner has been selected. Set partner before adding services.', 'warning')
|
||||
return redirect(prefixed_url_for('partner_bp.partners'))
|
||||
partner_id = session['partner']['id']
|
||||
|
||||
query = PartnerService.query.filter(PartnerService.partner_id == partner_id)
|
||||
|
||||
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)
|
||||
config = get_partner_services_list_view(partner_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@partner_bp.route('/handle_partner_service_selection', methods=['POST'])
|
||||
|
||||
@@ -55,7 +55,7 @@ def login():
|
||||
current_app.logger.info(f'Login successful! Current User is {current_user.email}')
|
||||
db.session.commit()
|
||||
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:
|
||||
return redirect(prefixed_url_for('user_bp.tenant_overview'))
|
||||
else:
|
||||
|
||||
@@ -23,6 +23,10 @@ from common.services.user import TenantServices
|
||||
from common.services.user import UserServices
|
||||
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')
|
||||
|
||||
|
||||
@@ -43,7 +47,7 @@ def tenant():
|
||||
if not UserServices.can_user_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')
|
||||
return redirect(prefixed_url_for('user_bp.select_tenant'))
|
||||
return redirect(prefixed_url_for('user_bp.tenants'))
|
||||
form = TenantForm()
|
||||
if request.method == 'GET':
|
||||
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}")
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
@user_bp.route('/select_tenant', methods=['GET', 'POST'])
|
||||
@user_bp.route('/tenants', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin') # Allow both roles
|
||||
def select_tenant():
|
||||
filter_form = TenantSelectionForm(request.form)
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
# 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)
|
||||
def tenants():
|
||||
# Get configuration and render the list view
|
||||
config = get_tenants_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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):
|
||||
current_app.logger.info(f"User not authenticated to edit tenant {tenant_id}.")
|
||||
flash(f"You are not authenticated to manage tenant {tenant_id}", 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.select_tenant'))
|
||||
return redirect(prefixed_url_for('user_bp.tenants'))
|
||||
the_tenant = Tenant.query.get(tenant_id)
|
||||
|
||||
# set tenant information in the session
|
||||
@@ -217,7 +177,7 @@ def handle_tenant_selection():
|
||||
return redirect(prefixed_url_for('user_bp.tenant_overview'))
|
||||
|
||||
# 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'])
|
||||
@@ -342,21 +302,10 @@ def edit_user(user_id):
|
||||
@user_bp.route('/view_users')
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
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')
|
||||
query = User.query.filter_by(tenant_id=tenant_id).order_by(User.user_name)
|
||||
|
||||
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)
|
||||
config = get_users_list_view(tenant_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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'))
|
||||
|
||||
|
||||
# Tenant Domain Management ------------------------------------------------------------------------
|
||||
@user_bp.route('/view_tenant_domains')
|
||||
# Tenant Domain Management (Probably obsolete )------------------------------------------------------------------------
|
||||
@user_bp.route('/tenant_domains')
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def view_tenant_domains():
|
||||
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')
|
||||
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)
|
||||
def tenant_domains():
|
||||
tenant_id = session['tenant']['id']
|
||||
config = get_tenant_domains_list_view(tenant_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@user_bp.route('/handle_tenant_domain_action', methods=['POST'])
|
||||
@@ -418,7 +356,7 @@ def handle_tenant_domain_action():
|
||||
if action == 'edit_tenant_domain':
|
||||
return redirect(prefixed_url_for('user_bp.edit_tenant_domain', tenant_domain_id=tenant_domain_id))
|
||||
# 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'])
|
||||
@@ -470,7 +408,7 @@ def edit_tenant_domain(tenant_domain_id):
|
||||
f'for tenant {session["tenant"]["id"]}'
|
||||
f'Error: {str(e)}')
|
||||
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
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
@@ -535,21 +473,9 @@ def tenant_project():
|
||||
@user_bp.route('/tenant_projects', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
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']
|
||||
query = TenantProject.query.filter_by(tenant_id=tenant_id).order_by(TenantProject.id)
|
||||
|
||||
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)
|
||||
config = get_tenant_projects_list_view(tenant_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@user_bp.route('/handle_tenant_project_selection', methods=['POST'])
|
||||
@@ -677,21 +603,9 @@ def tenant_make():
|
||||
@user_bp.route('/tenant_makes', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
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']
|
||||
query = TenantMake.query.filter_by(tenant_id=tenant_id).order_by(TenantMake.id)
|
||||
|
||||
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)
|
||||
config = get_tenant_makes_list_view(tenant_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@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
|
||||
if 'tenant' in session:
|
||||
session['tenant'] = tenant.to_dict()
|
||||
return None
|
||||
return None
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
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)}')
|
||||
|
||||
return redirect(prefixed_url_for('user_bp.tenant_makes'))
|
||||
|
||||
return None
|
||||
# Altijd teruggaan naar de tenant_makes pagina
|
||||
return redirect(prefixed_url_for('user_bp.tenant_makes'))
|
||||
|
||||
|
||||
def reset_uniquifier(user):
|
||||
|
||||
@@ -43,8 +43,28 @@ import 'select2';
|
||||
import { createJSONEditor } from 'vanilla-jsoneditor';
|
||||
window.createJSONEditor = createJSONEditor;
|
||||
|
||||
// Importeer Tabulator en maak deze globaal beschikbaar
|
||||
import { TabulatorFull } from 'tabulator-tables';
|
||||
window.Tabulator = TabulatorFull;
|
||||
|
||||
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';
|
||||
window.Vue = { createApp };
|
||||
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
// 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
|
||||
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 || {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user