generic user tracking

This commit is contained in:
jr-k 2024-06-03 00:01:01 +02:00
parent becb98cfc5
commit 70a9237951
58 changed files with 859 additions and 233 deletions

View File

@ -267,6 +267,10 @@ button.purple:hover {
align-self: stretch;
}
.panel.no-border {
border: none;
}
.panel h3 {
color: #fff;
}
@ -554,7 +558,7 @@ button.purple:hover {
margin: 0;
}
form {
.form {
display: flex;
flex-direction: column;
justify-content: flex-start;
@ -562,7 +566,7 @@ form {
padding: 20px;
}
form .form-group {
.form .form-group {
margin: 10px 20px 5px 20px;
display: flex;
flex-direction: row;
@ -572,14 +576,14 @@ form .form-group {
flex: 1;
}
form .form-group label {
.form .form-group label {
flex: 1;
padding: 10px;
text-align: right;
margin-right: 20px;
}
form .form-group .widget {
.form .form-group .widget {
flex: 3;
display: flex;
flex-direction: row;
@ -588,9 +592,9 @@ form .form-group .widget {
align-self: stretch;
}
form .form-group input,
form .form-group select,
form .form-group textarea {
.form .form-group input,
.form .form-group select,
.form .form-group textarea {
flex: 1;
padding: 10px 5px 10px 5px;
border: 1px solid #EEE;
@ -598,23 +602,23 @@ form .form-group textarea {
width: auto;
}
form .form-group input[type=checkbox] {
.form .form-group input[type=checkbox] {
flex: 0;
}
form .form-group .trigger {
.form .form-group .trigger {
margin-right: 10px;
}
form .form-group select.trigger {
.form .form-group select.trigger {
max-width: 120px;
}
form .form-group span {
.form .form-group span {
margin-left: 10px;
}
form .actions {
.form .actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
@ -623,25 +627,25 @@ form .actions {
align-self: stretch;
}
form .actions button.green,
form .actions button {
.form .actions button.green,
.form .actions button {
font-size: 18px;
}
form .actions button.green:hover {
.form .actions button.green:hover {
background: white;
color: rgb(14, 239, 95);
border-color: rgb(14, 239, 95);
}
form .actions button.btn-normal {
.form .actions button.btn-normal {
color: #999;
border-color: #999;
font-size: 18px;
margin: 0;
}
form .actions button.btn-normal:hover {
.form .actions button.btn-normal:hover {
color: #555;
border-color: #555;
}
@ -677,19 +681,19 @@ footer .version {
color: #333;
}
.card form {
.card .form {
padding: 0;
}
.card form .form-group {
.card .form .form-group {
margin: 0 0 30px 0;
padding: 0;
}
.card form .form-group .widget {
.card .form .form-group .widget {
flex: 1;
}
.card form .form-group label {
.card .form .form-group label {
text-align: left;
}
@ -757,4 +761,97 @@ a.badge:hover {
margin-left: 34px;
color: #999;
}
}
.explorer {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
align-self: stretch;
}
.explorer .left {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
align-self: stretch;
flex: 1;
}
.explorer .right {
flex: 3;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
.panel.panel-menu {
display: flex;
flex: 1;
flex-direction: column;
align-self: stretch;
margin-right: 0;
border-color: #692fbd;
}
.panel.panel-menu ul {
flex: 1;
max-width: 250px;
display: flex;
flex-direction: column;
align-self: stretch;
list-style: none;
margin: 0;
padding: 0;
}
.panel.panel-menu ul li {
margin: 3px 0;
}
.panel.panel-menu ul li a {
padding: 5px 15px 5px 15px;
color: inherit;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
flex: 1;
}
.panel.panel-menu ul li:hover {
color: #555;
font-weight: bold;
}
.panel.panel-menu ul li.active {
color: #692fbd;
background: #692fbd22;
border-radius: 4px;
font-weight: bold;
border: 1px solid #692fbd;
}
.explorer .panel-active {
background: white;
color: #AAA;
border-color: #BBB;
}
.explorer .panel-active h3 {
color: #0bc44e;
}
.explorer .panel-inactive {
background: white;
color: #AAA;
border-color: #BBB;
}

View File

@ -1,7 +1,6 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-users');
const $tableInactive = $('table.inactive-users');
const $modalsRoot = $('.modals');
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
@ -15,16 +14,6 @@ jQuery(document).ready(function ($) {
$(this).find('tr.empty-tr').addClass('hidden');
}
});
}
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const main = function () {
@ -72,10 +61,6 @@ jQuery(document).ready(function ($) {
;
});
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.user-add', function () {
showModal('modal-user-add');
$('.modal-user-add input:eq(0)').focus().select();
@ -108,11 +93,5 @@ jQuery(document).ready(function ($) {
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -0,0 +1,64 @@
jQuery(document).ready(function ($) {
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
};
const updateTable = function () {
$('table').each(function () {
if ($(this).find('tbody tr.node-player-group-item:visible').length === 0) {
$(this).find('tr.empty-tr').removeClass('hidden');
} else {
$(this).find('tr.empty-tr').addClass('hidden');
}
});
};
const main = function () {
};
$(document).on('change', '#node-player-group-add-type', function () {
const value = $(this).val();
const inputType = $(this).find('option').filter(function (i, el) {
return $(el).val() === value;
}).data('input');
$('.node-player-group-add-object-input')
.addClass('hidden')
.prop('disabled', true)
.filter('#node-player-group-add-object-input-' + inputType)
.removeClass('hidden')
.prop('disabled', false)
;
});
$(document).on('click', '.node-player-group-add', function () {
showModal('modal-node-player-group-add');
$('.modal-node-player-group-add input:eq(0)').focus().select();
});
$(document).on('click', '.node-player-group-edit', function () {
const nodePlayerGroup = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-node-player-group-edit');
$('.modal-node-player-group-edit input:visible:eq(0)').focus().select();
$('#node-player-group-edit-name').val(nodePlayerGroup.name);
$('#node-player-group-edit-id').val(nodePlayerGroup.id);
});
$(document).on('click', '.node-player-group-delete', function () {
if (confirm(l.js_fleet_node_player_delete_confirmation)) {
const $tr = $(this).parents('tr:eq(0)');
$tr.remove();
updateTable();
$.ajax({
method: 'DELETE',
url: '/fleet/node-player-group/delete',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this))}),
});
}
});
main();
});

View File

@ -1,7 +1,6 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-node-players');
const $tableInactive = $('table.inactive-node-players');
const $modalsRoot = $('.modals');
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
@ -18,16 +17,6 @@ jQuery(document).ready(function ($) {
updatePositions();
};
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const updatePositions = function (table, row) {
const positions = {};
$('.node-player-item').each(function (index) {
@ -83,9 +72,6 @@ jQuery(document).ready(function ($) {
;
});
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.node-player-add', function () {
showModal('modal-node-player-add');
@ -115,11 +101,5 @@ jQuery(document).ready(function ($) {
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -1,7 +1,6 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-node-studios');
const $tableInactive = $('table.inactive-node-studios');
const $modalsRoot = $('.modals');
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
@ -18,16 +17,6 @@ jQuery(document).ready(function ($) {
updatePositions();
};
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const updatePositions = function (table, row) {
const positions = {};
$('.node-studio-item').each(function (index) {
@ -83,10 +72,6 @@ jQuery(document).ready(function ($) {
;
});
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.node-studio-add', function () {
showModal('modal-node-studio-add');
$('.modal-node-studio-add input:eq(0)').focus().select();
@ -116,11 +101,5 @@ jQuery(document).ready(function ($) {
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -1,4 +1,25 @@
const $modalsRoot = $('.modals');
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
jQuery(document).ready(function ($) {
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
$(document).on('click', '.protected', function(e) {
e.preventDefault();
@ -15,5 +36,15 @@ jQuery(document).ready(function ($) {
}
return false;
})
});
$(document).on('click', '.item-utrack', function () {
const entity = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-entity-utrack');
$('#entity-utrack-created-by').val(entity.created_by);
$('#entity-utrack-updated-by').val(entity.updated_by);
$('#entity-utrack-created-at').val(prettyTimestamp(entity.created_at * 1000));
$('#entity-utrack-updated-at').val(prettyTimestamp(entity.updated_at * 1000));
});
});

View File

@ -1,7 +1,6 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-playlists');
const $tableInactive = $('table.inactive-playlists');
const $modalsRoot = $('.modals');
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
@ -17,16 +16,6 @@ jQuery(document).ready(function ($) {
});
};
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const main = function () {
};
@ -50,10 +39,6 @@ jQuery(document).ready(function ($) {
updateTable();
});
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.playlist-add', function () {
showModal('modal-playlist-add');
$('.modal-playlist-add input:eq(0)').focus().select();
@ -87,11 +72,5 @@ jQuery(document).ready(function ($) {
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -1,24 +1,8 @@
jQuery(document).ready(function ($) {
const $modalsRoot = $('.modals');
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const main = function () {
};
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.variable-edit', function () {
const variable = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
@ -42,11 +26,5 @@ jQuery(document).ready(function ($) {
$('#variable-edit-id').val(variable.id);
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -1,18 +1,12 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-slides');
const $tableInactive = $('table.inactive-slides');
const $modalsRoot = $('.modals');
const getCronDateTime = function(cronExpression) {
const [minutes, hours, day, month, _, year] = cronExpression.split(' ');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}`;
};
const prettyTimestamp = function(timestamp) {
const d = new Date(timestamp);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')} `
}
const loadDateTimePicker = function($els) {
$els.each(function() {
var $el = $(this);
@ -50,16 +44,6 @@ jQuery(document).ready(function ($) {
}
}).tableDnDUpdate();
updatePositions();
}
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const updatePositions = function (table, row) {
@ -178,10 +162,6 @@ jQuery(document).ready(function ($) {
$(document).on('change', '#slide-add-type', inputTypeUpdate);
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.slide-add', function () {
showModal('modal-slide-add');
loadDateTimePicker($('.modal-slide-add .datetimepicker'))
@ -190,15 +170,6 @@ jQuery(document).ready(function ($) {
$('.modal-slide-add input:eq(0)').focus().select();
});
$(document).on('click', '.slide-utrack', function () {
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-slide-utrack');
$('#slide-utrack-created-by').val(slide.created_by);
$('#slide-utrack-updated-by').val(slide.updated_by);
$('#slide-utrack-created-at').val(prettyTimestamp(slide.created_at * 1000));
$('#slide-utrack-updated-at').val(prettyTimestamp(slide.updated_at * 1000));
});
$(document).on('click', '.slide-edit', function () {
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-slide-edit');
@ -254,11 +225,5 @@ jQuery(document).ready(function ($) {
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -1,3 +1,8 @@
const prettyTimestamp = function(timestamp) {
const d = new Date(timestamp);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')} `
};
const validateCronDateTime = function(cronExpression) {
const pattern = /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\*\s+(\d+)$/;
return pattern.test(cronExpression);

View File

@ -59,6 +59,7 @@
"playlist_form_button_cancel": "Cancel",
"js_playlist_delete_confirmation": "Are you sure?",
"playlist_delete_has_slides": "Playlist has slides, please remove them before and retry",
"playlist_delete_has_node_player_groups": "Playlist has player groups, please remove them before and retry",
"fleet_node_studio_page_title": "Studios",
"fleet_node_studio_button_add": "Add a studio",
@ -99,6 +100,21 @@
"fleet_node_player_form_button_cancel": "Cancel",
"js_fleet_node_player_delete_confirmation": "Are you sure?",
"fleet_node_player_group_page_title": "Player Groups",
"fleet_node_player_group_button_add": "Add a player group",
"fleet_node_player_group_panel_active": "Active player groups",
"fleet_node_player_group_panel_empty": "Currently, there are no player groups. %link% now.",
"fleet_node_player_group_panel_th_name": "Name",
"fleet_node_player_group_panel_th_activity": "Activity",
"fleet_node_player_group_form_add_title": "Add Player Group",
"fleet_node_player_group_form_add_submit": "Add",
"fleet_node_player_group_form_edit_title": "Edit Player Group",
"fleet_node_player_group_form_edit_submit": "Save",
"fleet_node_player_group_form_label_name": "Name",
"fleet_node_player_group_form_button_cancel": "Cancel",
"js_fleet_node_player_group_delete_confirmation": "Are you sure?",
"node_player_group_delete_has_node_player": "Player group has players, please remove or unassign them before and retry",
"login_page_title": "Login",
"auth_page_title": "Users",
"auth_user_button_add": "Add a user",

View File

@ -59,6 +59,7 @@
"playlist_form_button_cancel": "Annuler",
"js_playlist_delete_confirmation": "Êtes-vous sûr ?",
"playlist_delete_has_slides": "La liste de lecture contient des slides, supprimez-les avant et réessayez",
"playlist_delete_has_node_player_groups": "La liste de lecture contient des groupes de lecteur, supprimez-les avant et réessayez",
"fleet_node_studio_page_title": "Studios",
"fleet_node_studio_button_add": "Ajouter un studio",
@ -81,24 +82,39 @@
"fleet_node_studio_form_button_cancel": "Annuler",
"js_fleet_node_studio_delete_confirmation": "Êtes-vous sûr ?",
"fleet_node_player_page_title": "Players",
"fleet_node_player_button_add": "Ajouter un player",
"fleet_node_player_page_title": "Lecteurs",
"fleet_node_player_button_add": "Ajouter un lecteur",
"fleet_node_player_panel_active": "Players actifs",
"fleet_node_player_panel_inactive": "Players inactifs",
"fleet_node_player_panel_empty": "Actuellement, il n'y a pas de players. %link% maintenant.",
"fleet_node_player_panel_empty": "Actuellement, il n'y a pas de lecteurs. %link% maintenant.",
"fleet_node_player_panel_th_name": "Nom",
"fleet_node_player_panel_th_host": "Hôte",
"fleet_node_player_panel_th_enabled": "Activé",
"fleet_node_player_panel_th_activity": "Options",
"fleet_node_player_form_add_title": "Ajout d'un player",
"fleet_node_player_form_add_title": "Ajout d'un lecteur",
"fleet_node_player_form_add_submit": "Ajouter",
"fleet_node_player_form_edit_title": "Modification d'un player",
"fleet_node_player_form_edit_title": "Modification d'un lecteur",
"fleet_node_player_form_edit_submit": "Enregistrer",
"fleet_node_player_form_label_name": "Nom",
"fleet_node_player_form_label_host": "Hôte",
"fleet_node_player_form_button_cancel": "Annuler",
"js_fleet_node_player_delete_confirmation": "Êtes-vous sûr ?",
"fleet_node_player_group_page_title": "Groupes de lecteurs",
"fleet_node_player_group_button_add": "Ajouter un groupe de lecteur",
"fleet_node_player_group_panel_active": "Groupes de lecteur",
"fleet_node_player_group_panel_empty": "Actuellement, il n'y a pas de groupes de lecteur. %link% maintenant.",
"fleet_node_player_group_panel_th_name": "Nom",
"fleet_node_player_group_panel_th_activity": "Options",
"fleet_node_player_group_form_add_title": "Ajout d'un groupe de lecteur",
"fleet_node_player_group_form_add_submit": "Ajouter",
"fleet_node_player_group_form_edit_title": "Modification d'un groupe de lecteur",
"fleet_node_player_group_form_edit_submit": "Enregistrer",
"fleet_node_player_group_form_label_name": "Nom",
"fleet_node_player_group_form_button_cancel": "Annuler",
"js_fleet_node_player_group_delete_confirmation": "Êtes-vous sûr ?",
"node_player_group_delete_has_node_player": "Le groupe de lecteur a des lecteurs, supprimez-les ou réassignez-les avant de le supprimer",
"login_page_title": "Connexion",
"auth_page_title": "Utilisateurs",
"auth_user_button_add": "Ajouter un utilisateur",

View File

@ -0,0 +1,50 @@
import json
from flask import Flask, render_template, redirect, request, url_for, jsonify
from src.service.ModelStore import ModelStore
from src.model.entity.NodePlayerGroup import NodePlayerGroup
from src.interface.ObController import ObController
class FleetNodePlayerGroupController(ObController):
def guard_fleet(self, f):
def decorated_function(*args, **kwargs):
if not self._model_store.variable().map().get('fleet_player_enabled').as_bool():
return redirect(url_for('manage'))
return f(*args, **kwargs)
return decorated_function
def register(self):
self._app.add_url_rule('/fleet/node-player-group/list', 'fleet_node_player_group_list', self.guard_fleet(self._auth(self.fleet_node_player_group_list)), methods=['GET'])
self._app.add_url_rule('/fleet/node-player-group/add', 'fleet_node_player_group_add', self.guard_fleet(self._auth(self.fleet_node_player_group_add)), methods=['POST'])
self._app.add_url_rule('/fleet/node-player-group/edit', 'fleet_node_player_group_edit', self.guard_fleet(self._auth(self.fleet_node_player_group_edit)), methods=['POST'])
self._app.add_url_rule('/fleet/node-player-group/delete', 'fleet_node_player_group_delete', self.guard_fleet(self._auth(self.fleet_node_player_group_delete)), methods=['DELETE'])
def fleet_node_player_group_list(self):
return render_template(
'fleet/player-group/list.jinja.html',
node_player_groups=self._model_store.node_player_group().get_all(),
)
def fleet_node_player_group_add(self):
self._model_store.node_player_group().add_form(NodePlayerGroup(
name=request.form['name'],
playlist_id=request.form['playlist_id'],
))
return redirect(url_for('fleet_node_player_group_list'))
def fleet_node_player_group_edit(self):
self._model_store.node_player_group().update_form(request.form['id'], request.form['name'], request.form['playlist_id'])
return redirect(url_for('fleet_node_player_group_list'))
def fleet_node_player_group_delete(self):
data = request.get_json()
id = data.get('id')
if self._model_store.node_player().count_node_players_for_group(id) > 0:
return jsonify({'status': 'error', 'message': self.t('node_player_group_delete_has_node_player')}), 400
self._model_store.playlist().delete(id)
return jsonify({'status': 'ok'})

View File

@ -64,7 +64,12 @@ class PlaylistController(ObController):
def playlist_delete(self):
data = request.get_json()
id = data.get('id')
if self._model_store.slide().count_slides_for_playlist(id) > 0:
return jsonify({'status': 'error', 'message': self.t('playlist_delete_has_slides')}), 400
if self._model_store.node_player_group().count_node_player_groups_for_playlist(id) > 0:
return jsonify({'status': 'error', 'message': self.t('playlist_delete_has_node_player_groups')}), 400
self._model_store.playlist().delete(id)
return jsonify({'status': 'ok'})

View File

@ -47,7 +47,7 @@ class SlideshowController(ObController):
name=request.form['name'],
type=str_to_enum(request.form['type'], SlideType),
duration=request.form['duration'],
playlist=request.form['playlist'] if 'playlist' in request.form else None,
playlist_id=request.form['playlist_id'] if 'playlist_id' in request.form else None,
cron_schedule=get_optional_string(request.form['cron_schedule']),
cron_schedule_end=get_optional_string(request.form['cron_schedule_end']),
)
@ -72,8 +72,8 @@ class SlideshowController(ObController):
self._model_store.slide().add_form(slide)
self._post_update()
if slide.playlist:
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist))
if slide.playlist_id:
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist_id))
return redirect(url_for('slideshow_slide_list'))
@ -88,8 +88,8 @@ class SlideshowController(ObController):
)
self._post_update()
if slide.playlist:
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist))
if slide.playlist_id:
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist_id))
return redirect(url_for('slideshow_slide_list'))

View File

@ -0,0 +1,111 @@
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.NodePlayerGroup import NodePlayerGroup
from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.manager.VariableManager import VariableManager
from src.service.ModelManager import ModelManager
class NodePlayerGroupManager(ModelManager):
TABLE_NAME = "fleet_player_group"
TABLE_MODEL = [
"name CHAR(255)",
"playlist_id INTEGER",
"created_by CHAR(255)",
"updated_by CHAR(255)",
"created_at INTEGER",
"updated_at INTEGER"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager, variable_manager: VariableManager):
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
def hydrate_object(self, raw_node_player_group: dict, id: Optional[int] = None) -> NodePlayerGroup:
if id:
raw_node_player_group['id'] = id
return NodePlayerGroup(**raw_node_player_group)
def hydrate_list(self, raw_node_player_groups: list) -> List[NodePlayerGroup]:
return [self.hydrate_object(raw_node_player_group) for raw_node_player_group in raw_node_player_groups]
def get(self, id: int) -> Optional[NodePlayerGroup]:
object = self._db.get_by_id(self.TABLE_NAME, id)
return self.hydrate_object(object, id) if object else None
def get_by(self, query, sort: Optional[str] = None) -> List[NodePlayerGroup]:
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort))
def get_one_by(self, query) -> Optional[NodePlayerGroup]:
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
if not object:
return None
return self.hydrate_object(object)
def get_all(self, sort: bool = False) -> List[NodePlayerGroup]:
return self.hydrate_list(self._db.get_all(self.TABLE_NAME, "name" if sort else None))
def get_node_players_groups(self, playlist_id: Optional[int] = None) -> List[NodePlayerGroup]:
query = ""
if playlist_id:
query = "{} {}".format(query, "AND playlist_id = {}".format(playlist_id))
else:
query = "{} {}".format(query, "AND playlist_id is NULL")
return self.get_by(query=query, sort="name")
def forget_user(self, user_id: int):
node_player_groups = self.get_by("created_by = '{}' or updated_by = '{}'".format(user_id, user_id))
edits_node_player_groups = self.user_manager.forget_user_for_entity(node_player_groups, user_id)
for node_player_group_id, edits in edits_node_player_groups.items():
self._db.update_by_id(self.TABLE_NAME, node_player_group_id, edits)
def pre_add(self, node_player_group: Dict) -> Dict:
self.user_manager.track_user_on_create(node_player_group)
self.user_manager.track_user_on_update(node_player_group)
return node_player_group
def pre_update(self, node_player_group: Dict) -> Dict:
self.user_manager.track_user_on_update(node_player_group)
return node_player_group
def pre_delete(self, node_player_group_id: str) -> str:
return node_player_group_id
def post_add(self, node_player_group_id: str) -> str:
return node_player_group_id
def post_update(self, node_player_group_id: str) -> str:
return node_player_group_id
def update_form(self, id: int, name: str, playlist_id: Optional[int]) -> None:
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update({"name": name, "playlist_id": playlist_id}))
self.post_update(id)
def add_form(self, node_player_group: Union[NodePlayerGroup, Dict]) -> None:
form = node_player_group
if not isinstance(node_player_group, dict):
form = node_player_group.to_dict()
del form['id']
self._db.add(self.TABLE_NAME, self.pre_add(form))
self.post_add(node_player_group.id)
def delete(self, id: int) -> None:
self.pre_delete(id)
self._db.delete_by_id(self.TABLE_NAME, id)
self.post_delete(id)
def to_dict(self, node_player_groups: List[NodePlayerGroup]) -> List[Dict]:
return [node_player_group.to_dict() for node_player_group in node_player_groups]
def count_node_player_groups_for_playlist(self, id: int) -> int:
return len(self.get_node_players_groups(playlist_id=id))

View File

@ -14,6 +14,7 @@ class NodePlayerManager(ModelManager):
TABLE_MODEL = [
"name CHAR(255)",
"enabled INTEGER DEFAULT 0",
"group_id INTEGER",
"position INTEGER",
"host CHAR(255)",
"created_by CHAR(255)",
@ -53,6 +54,15 @@ class NodePlayerManager(ModelManager):
def get_all(self, sort: bool = False) -> List[NodePlayer]:
return self.hydrate_list(self._db.get_all(self.TABLE_NAME, "position" if sort else None))
def get_node_players(self, group_id: Optional[int] = None) -> List[NodePlayer]:
query = "enabled = {}".format("1" if enabled else "0")
if group_id:
query = "{} {}".format(query, "AND group_id = {}".format(group_id))
else:
query = "{} {}".format(query, "AND group_id is NULL")
return self.get_by(query=query, sort="position")
def forget_user(self, user_id: int):
node_players = self.get_by("created_by = '{}' or updated_by = '{}'".format(user_id, user_id))
edits_node_players = self.user_manager.forget_user_for_entity(node_players, user_id)
@ -113,3 +123,6 @@ class NodePlayerManager(ModelManager):
def to_dict(self, node_players: List[NodePlayer]) -> List[Dict]:
return [node_player.to_dict() for node_player in node_players]
def count_node_players_for_group(self, id: int) -> int:
return len(self.get_node_players(group_id=id))

View File

@ -20,7 +20,7 @@ class SlideManager(ModelManager):
"name CHAR(255)",
"type CHAR(30)",
"enabled INTEGER DEFAULT 0",
"playlist INTEGER",
"playlist_id INTEGER",
"duration INTEGER",
"position INTEGER",
"location TEXT",
@ -78,9 +78,9 @@ class SlideManager(ModelManager):
def get_slides(self, playlist_id: Optional[int] = None, enabled: bool = True) -> List[Slide]:
query = "enabled = {}".format("1" if enabled else "0")
if playlist_id:
query = "{} {}".format(query, "AND playlist = {}".format(playlist_id))
query = "{} {}".format(query, "AND playlist_id = {}".format(playlist_id))
else:
query = "{} {}".format(query, "AND playlist is NULL")
query = "{} {}".format(query, "AND playlist_id is NULL")
return self.get_by(query=query, sort="position")
@ -170,4 +170,4 @@ class SlideManager(ModelManager):
return [slide.to_dict() for slide in slides]
def count_slides_for_playlist(self, id: int) -> int:
return len(self.get_slides(playlist_id=id))
return len(self.get_slides(playlist_id=id))

View File

@ -1,4 +1,5 @@
import json
import time
from typing import Optional, Union

View File

@ -6,8 +6,9 @@ from typing import Optional, Union
class NodePlayer:
def __init__(self, host: str = '', enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
def __init__(self, host: str = '', enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None, group_id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
self._id = id if id else None
self._group_id = group_id
self._host = host
self._enabled = enabled
self._name = name
@ -21,6 +22,14 @@ class NodePlayer:
def id(self) -> Optional[int]:
return self._id
@property
def group_id(self) -> Optional[int]:
return self._group_id
@group_id.setter
def group_id(self, value: Optional[int]):
self._group_id = value
@property
def host(self) -> str:
return self._host
@ -88,6 +97,7 @@ class NodePlayer:
def __str__(self) -> str:
return f"NodePlayer(" \
f"id='{self.id}',\n" \
f"group_id='{self.group_id}',\n" \
f"name='{self.name}',\n" \
f"enabled='{self.enabled}',\n" \
f"position='{self.position}',\n" \
@ -98,12 +108,18 @@ class NodePlayer:
f"updated_at='{self.updated_at}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_json(self, edits: dict = {}) -> str:
obj = self.to_dict()
for k, v in edits.items():
obj[k] = v
return json.dumps(obj)
def to_dict(self) -> dict:
return {
"id": self.id,
"group_id": self.group_id,
"name": self.name,
"enabled": self.enabled,
"position": self.position,

View File

@ -0,0 +1,101 @@
import json
import time
from typing import Optional, Union
class NodePlayerGroup:
def __init__(self, name: str = 'Untitled', playlist_id: Optional[int] = None, id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
self._id = id if id else None
self._playlist_id = playlist_id
self._name = name
self._created_by = created_by if created_by else None
self._updated_by = updated_by if updated_by else None
self._created_at = int(created_at if created_at else time.time())
self._updated_at = int(updated_at if updated_at else time.time())
@property
def id(self) -> Optional[int]:
return self._id
@property
def playlist_id(self) -> Optional[int]:
return self._playlist_id
@playlist_id.setter
def playlist_id(self, value: Optional[int]):
self._playlist_id = value
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
self._name = value
@property
def created_by(self) -> str:
return self._created_by
@created_by.setter
def created_by(self, value: str):
self._created_by = value
@property
def updated_by(self) -> str:
return self._updated_by
@updated_by.setter
def updated_by(self, value: str):
self._updated_by = value
@property
def created_at(self) -> int:
return self._created_at
@created_at.setter
def created_at(self, value: int):
self._created_at = value
@property
def updated_at(self) -> int:
return self._updated_at
@updated_at.setter
def updated_at(self, value: int):
self._updated_at = value
def __str__(self) -> str:
return f"NodePlayer(" \
f"id='{self.id}',\n" \
f"name='{self.name}',\n" \
f"playlist_id='{self.playlist_id}',\n" \
f"created_by='{self.created_by}',\n" \
f"updated_by='{self.updated_by}',\n" \
f"created_at='{self.created_at}',\n" \
f"updated_at='{self.updated_at}',\n" \
f")"
def to_json(self, edits: dict = {}) -> str:
obj = self.to_dict()
for k, v in edits.items():
obj[k] = v
return json.dumps(obj)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"playlist_id": self.playlist_id,
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
def is_root(self) -> bool:
return not self._playlist_id

View File

@ -108,8 +108,13 @@ class NodeStudio:
f"updated_at='{self.updated_at}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_json(self, edits: dict = {}) -> str:
obj = self.to_dict()
for k, v in edits.items():
obj[k] = v
return json.dumps(obj)
def to_dict(self) -> dict:
return {

View File

@ -101,7 +101,7 @@ class Playlist:
def to_json(self, edits: dict = {}) -> str:
obj = self.to_dict()
for k,v in edits.items():
for k, v in edits.items():
obj[k] = v
return json.dumps(obj)

View File

@ -8,10 +8,10 @@ from src.util.utils import str_to_enum
class Slide:
def __init__(self, location: str = '', playlist: Optional[int] = None, duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
def __init__(self, location: str = '', playlist_id: Optional[int] = None, duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
self._id = id if id else None
self._location = location
self._playlist = playlist
self._playlist_id = playlist_id
self._duration = duration
self._type = str_to_enum(type, SlideType) if isinstance(type, str) else type
self._enabled = enabled
@ -77,12 +77,12 @@ class Slide:
self._type = value
@property
def playlist(self) -> Optional[int]:
return self._playlist
def playlist_id(self) -> Optional[int]:
return self._playlist_id
@playlist.setter
def playlist(self, value: Optional[int]):
self._playlist = value
@playlist_id.setter
def playlist_id(self, value: Optional[int]):
self._playlist_id = value
@property
def cron_schedule(self) -> Optional[str]:
@ -145,7 +145,7 @@ class Slide:
f"updated_by='{self.updated_by}',\n" \
f"created_at='{self.created_at}',\n" \
f"updated_at='{self.updated_at}',\n" \
f"playlist='{self.playlist}',\n" \
f"playlist_id='{self.playlist_id}',\n" \
f"cron_schedule='{self.cron_schedule}',\n" \
f"cron_schedule_end='{self.cron_schedule_end}',\n" \
f")"
@ -153,7 +153,7 @@ class Slide:
def to_json(self, edits: dict = {}) -> str:
obj = self.to_dict(with_virtual=True)
for k,v in edits.items():
for k, v in edits.items():
obj[k] = v
return json.dumps(obj)
@ -171,7 +171,7 @@ class Slide:
"updated_by": self.updated_by,
"created_at": self.created_at,
"updated_at": self.updated_at,
"playlist": self.playlist,
"playlist_id": self.playlist_id,
"cron_schedule": self.cron_schedule,
"cron_schedule_end": self.cron_schedule_end,
}

View File

@ -87,8 +87,13 @@ class User:
f"updated_at='{self.updated_at}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_json(self, edits: dict = {}) -> str:
obj = self.to_dict()
for k, v in edits.items():
obj[k] = v
return json.dumps(obj)
def to_dict(self) -> dict:
return {

View File

@ -22,6 +22,11 @@ class HookType(Enum):
H_FLEET_NODE_PLAYER_CSS = 'h_fleet_node_player_css'
H_FLEET_NODE_PLAYER_JAVASCRIPT = 'h_fleet_node_player_javascript'
H_FLEET_NODE_PLAYER_GROUP_TOOLBAR_ACTIONS_START = 'h_fleet_node_player_group_toolbar_actions_start'
H_FLEET_NODE_PLAYER_GROUP_TOOLBAR_ACTIONS_END = 'h_fleet_node_player_group_toolbar_actions_end'
H_FLEET_NODE_PLAYER_GROUP_CSS = 'h_fleet_node_player_group_css'
H_FLEET_NODE_PLAYER_GROUP_JAVASCRIPT = 'h_fleet_node_player_group_javascript'
H_PLAYLIST_TOOLBAR_ACTIONS_START = 'h_playlist_toolbar_actions_start'
H_PLAYLIST_TOOLBAR_ACTIONS_END = 'h_playlist_toolbar_actions_end'
H_PLAYLIST_CSS = 'h_playlist_css'

View File

@ -5,6 +5,7 @@ from src.manager.SlideManager import SlideManager
from src.manager.FolderManager import FolderManager
from src.manager.NodeStudioManager import NodeStudioManager
from src.manager.NodePlayerManager import NodePlayerManager
from src.manager.NodePlayerGroupManager import NodePlayerGroupManager
from src.manager.UserManager import UserManager
from src.manager.VariableManager import VariableManager
from src.manager.LangManager import LangManager
@ -35,6 +36,7 @@ class ModelStore:
self._folder_manager = FolderManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._node_studio_manager = NodeStudioManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._node_player_manager = NodePlayerManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._node_player_group_manager = NodePlayerGroupManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._playlist_manager = PlaylistManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._variable_manager.reload()
@ -63,6 +65,9 @@ class ModelStore:
def node_player(self) -> NodePlayerManager:
return self._node_player_manager
def node_player_group(self) -> NodePlayerGroupManager:
return self._node_player_group_manager
def folder_manager(self) -> FolderManager:
return self._folder_manager
@ -78,6 +83,7 @@ class ModelStore:
def on_user_delete(self, user_id: int) -> None:
self._playlist_manager.forget_user(user_id)
self._folder_manager.forget_user(user_id)
self._node_player_group_manager.forget_user(user_id)
self._node_player_manager.forget_user(user_id)
self._node_studio_manager.forget_user(user_id)
self._slide_manager.forget_user(user_id)

View File

@ -12,6 +12,7 @@ from src.controller.PlayerController import PlayerController
from src.controller.SlideshowController import SlideshowController
from src.controller.FleetNodeStudioController import FleetNodeStudioController
from src.controller.FleetNodePlayerController import FleetNodePlayerController
from src.controller.FleetNodePlayerGroupController import FleetNodePlayerGroupController
from src.controller.PlaylistController import PlaylistController
from src.controller.AuthController import AuthController
from src.controller.SysinfoController import SysinfoController
@ -108,6 +109,7 @@ class WebServer:
SysinfoController(self, self._app, self.auth_required, self._model_store, self._template_renderer)
FleetNodeStudioController(self, self._app, self.auth_required, self._model_store, self._template_renderer)
FleetNodePlayerController(self, self._app, self.auth_required, self._model_store, self._template_renderer)
FleetNodePlayerGroupController(self, self._app, self.auth_required, self._model_store, self._template_renderer)
PlaylistController(self, self._app, self.auth_required, self._model_store, self._template_renderer)
AuthController(self, self._app, self.auth_required, self._model_store, self._template_renderer)

View File

@ -2,6 +2,11 @@
<thead>
<tr>
<th>{{ l.auth_user_panel_th_username }}</th>
{% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac">{{ l.auth_user_panel_th_enabled }}</th>
<th class="tac">{{ l.auth_user_panel_th_activity }}</th>
</tr>
@ -16,7 +21,7 @@
</td>
</tr>
{% for user in users %}
<tr class="user-item" data-level="{{ user.id }}" data-entity="{{ user.to_json() }}">
<tr class="user-item" data-level="{{ user.id }}" data-entity="{{ user.to_json({"created_by": track_created(user).username, "updated_by": track_updated(user).username}) }}">
<td class="infos">
<div class="inner">
<div class="badge"><i class="fa fa-key icon-left"></i> {{ user.id }}</div>
@ -25,6 +30,14 @@
{{ user.username }}
</div>
</td>
{% if AUTH_ENABLED %}
<td class="tac">
{% set creator = track_created(user) %}
<a href="javascript:void(0);" class="badge item-utrack user-utrack {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</a>
</td>
{% endif %}
<td class="tac">
<label class="pure-material-switch">
<input type="checkbox" {% if user.enabled %}checked="checked"{% endif %}><span></span>

View File

@ -15,7 +15,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.auth_page_title }}</h2>
<h2><i class="fa fa-user icon-left"></i>{{ l.auth_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_AUTH_TOOLBAR_ACTIONS_START) }}
@ -56,6 +56,7 @@
<div class="modals-inner">
{% include 'auth/modal/add.jinja.html' %}
{% include 'auth/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>

View File

@ -25,7 +25,7 @@
<h3>
{{ l.login_form_title }}
</h3>
<form action="{{ url_for('login') }}" method="post">
<form class="form" action="{{ url_for('login') }}" method="post">
<div class="form-group">
<label for="password">{{ l.login_form_username }}</label>
<div class="widget">

View File

@ -3,7 +3,7 @@
{{ l.auth_user_form_add_title }}
</h2>
<form action="/auth/user/add" method="POST" enctype="multipart/form-data">
<form class="form" action="/auth/user/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="user-add-name">{{ l.auth_user_form_label_username }}</label>
<div class="widget">

View File

@ -3,7 +3,7 @@
{{ l.auth_user_form_edit_title }}
</h2>
<form action="/auth/user/edit" method="POST">
<form class="form" action="/auth/user/edit" method="POST">
<input type="hidden" name="id" id="user-edit-id" />
<div class="form-group">

View File

@ -76,6 +76,11 @@
<i class="fa fa-tv"></i> {{ l.fleet_node_player_page_title }}
</a>
</li>
<li class="{{ 'active' if request.url_rule.endpoint == 'fleet_node_player_group_list' }}">
<a href="{{ url_for('fleet_node_player_group_list') }}">
<i class="fa fa-group"></i> {{ l.fleet_node_player_group_page_title }}
</a>
</li>
{% endif %}
{% if AUTH_ENABLED %}
<li class="{{ 'active' if request.url_rule.endpoint == 'auth_user_list' }}">
@ -132,6 +137,7 @@
'js_common_are_you_sure': '{{ l.common_are_you_sure }}',
'js_playlist_delete_confirmation': '{{ l.js_playlist_delete_confirmation }}',
'js_slideshow_slide_delete_confirmation': '{{ l.js_slideshow_slide_delete_confirmation }}',
'js_fleet_node_player_group_delete_confirmation': '{{ l.js_fleet_node_player_group_delete_confirmation }}',
'js_fleet_node_player_delete_confirmation': '{{ l.js_fleet_node_player_delete_confirmation }}',
'js_fleet_node_studio_delete_confirmation': '{{ l.js_fleet_node_studio_delete_confirmation }}',
'js_auth_user_delete_confirmation': '{{ l.js_auth_user_delete_confirmation }}',

View File

@ -1,35 +1,35 @@
<div class="modal modal-slide-utrack modal-slide">
<div class="modal modal-entity-utrack modal-entity">
<h2>
{{ l.utrack_title }}
</h2>
<form action="/slideshow/slide/add" method="POST" enctype="multipart/form-data">
<div class="form">
<div class="form-group">
<label>{{ l.created_by }}</label>
<div class="widget">
<input name="name" type="text" id="slide-utrack-created-by" disabled="disabled" />
<input name="name" type="text" id="entity-utrack-created-by" disabled="disabled" />
</div>
</div>
<div class="form-group">
<label>{{ l.updated_by }}</label>
<div class="widget">
<input name="name" type="text" id="slide-utrack-updated-by" disabled="disabled" />
<input name="name" type="text" id="entity-utrack-updated-by" disabled="disabled" />
</div>
</div>
<div class="form-group">
<label>{{ l.created_at }}</label>
<div class="widget">
<input name="name" type="text" id="slide-utrack-created-at" disabled="disabled" />
<input name="name" type="text" id="entity-utrack-created-at" disabled="disabled" />
</div>
</div>
<div class="form-group">
<label>{{ l.updated_at }}</label>
<div class="widget">
<input name="name" type="text" id="slide-utrack-updated-at" disabled="disabled" />
<input name="name" type="text" id="entity-utrack-updated-at" disabled="disabled" />
</div>
</div>
@ -39,5 +39,5 @@
{{ l.close }}
</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,55 @@
<table class="{{ tclass }}-node-players">
<thead>
<tr>
<th>{{ l.fleet_node_player_group_panel_th_name }}</th>
{% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac">{{ l.fleet_node_player_group_panel_th_activity }}</th>
</tr>
</thead>
<tbody>
<tr class="empty-tr {% if node_player_groups|length != 0 %}hidden{% endif %}">
<td colspan="4">
{{ l.fleet_node_player_group_panel_empty|replace(
'%link%',
('<a href="javascript:void(0);" class="item-add node-player-group-add">'~l.fleet_node_player_group_button_add~'</a>')|safe
) }}
</td>
</tr>
{% for node_player_group in node_player_groups %}
<tr class="node-player-group-item" data-level="{{ node_player_group.id }}" data-entity="{{ node_player_group.to_json({"created_by": track_created(node_player_group).username, "updated_by": track_updated(node_player_group).username}) }}">
<td class="infos">
<div class="inner">
<a href="javascript:void(0);" class="item-sort node-player-group-sort">
<i class="fa fa-sort icon-left"></i>
</a>
<div class="badge"><i class="fa fa-key icon-left"></i> {{ node_player_group.id }}</div>
<i class="fa fa-server icon-left"></i>
{{ node_player_group.name }}
</div>
</td>
{% if AUTH_ENABLED %}
<td class="tac">
{% set creator = track_created(node_player_group) %}
<a href="javascript:void(0);" class="badge item-utrack node-player-group-utrack {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</a>
</td>
{% endif %}
<td class="actions tac">
<a href="javascript:void(0);" class="item-edit node-player-group-edit">
<i class="fa fa-pencil"></i>
</a>
<a href="javascript:void(0);" class="item-delete node-player-group-delete">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,50 @@
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.fleet_node_player_group_page_title }}
{% endblock %}
{% block add_css %}
{{ HOOK(H_FLEET_NODE_PLAYER_GROUP_CSS) }}
{% endblock %}
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/fleet/node-player-groups.js"></script>
{{ HOOK(H_FLEET_NODE_PLAYER_GROUP_JAVASCRIPT) }}
{% endblock %}
{% block page %}
<div class="toolbar">
<h2><i class="fa fa-group icon-left"></i>{{ l.fleet_node_player_group_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_FLEET_NODE_PLAYER_GROUP_TOOLBAR_ACTIONS_START) }}
<button class="purple node-player-group-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.fleet_node_player_group_button_add }}</button>
{{ HOOK(H_FLEET_NODE_PLAYER_GROUP_TOOLBAR_ACTIONS_END) }}
</div>
</div>
<div class="panel">
<div class="panel-body">
<h3>{{ l.fleet_node_player_group_panel_active }}</h3>
{% with tclass='active', node_player_groups=node_player_groups %}
{% include 'fleet/player-group/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="modals hidden">
<div class="modals-outer">
<a href="javascript:void(0);" class="modal-close">
<i class="fa fa-close"></i>
</a>
<div class="modals-inner">
{% include 'fleet/player-group/modal/add.jinja.html' %}
{% include 'fleet/player-group/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
<div class="modal modal-node-player-group-add">
<h2>
{{ l.fleet_node_player_group_form_add_title }}
</h2>
<form class="form" action="/fleet/node-player-group/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="node-player-group-add-name">{{ l.fleet_node_player_group_form_label_name }}</label>
<div class="widget">
<input name="name" type="text" id="node-player-group-add-name" required="required" />
</div>
</div>
<div class="actions">
<button type="button" class="btn-normal modal-close">
{{ l.fleet_node_player_group_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-plus icon-left"></i> {{ l.fleet_node_player_group_form_add_submit }}
</button>
</div>
</form>
</div>

View File

@ -0,0 +1,26 @@
<div class="modal modal-node-player-group-edit hidden">
<h2>
{{ l.fleet_node_player_group_form_edit_title }}
</h2>
<form class="form" action="/fleet/node-player-group/edit" method="POST">
<input type="hidden" name="id" id="node-player-group-edit-id" />
<div class="form-group">
<label for="node-player-group-edit-name">{{ l.fleet_node_player_group_form_label_name }}</label>
<div class="widget">
<input type="text" name="name" id="node-player-group-edit-name" required="required" />
</div>
</div>
<div class="actions">
<button type="button" class="btn-normal modal-close">
{{ l.fleet_node_player_group_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-save icon-left"></i>{{ l.fleet_node_player_group_form_edit_submit }}
</button>
</div>
</form>
</div>

View File

@ -2,6 +2,11 @@
<thead>
<tr>
<th>{{ l.fleet_node_player_panel_th_name }}</th>
{% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac">{{ l.fleet_node_player_panel_th_host }}</th>
<th class="tac">{{ l.fleet_node_player_panel_th_enabled }}</th>
<th class="tac">{{ l.fleet_node_player_panel_th_activity }}</th>
@ -17,7 +22,7 @@
</td>
</tr>
{% for node_player in node_players %}
<tr class="node-player-item" data-level="{{ node_player.id }}" data-entity="{{ node_player.to_json() }}">
<tr class="node-player-item" data-level="{{ node_player.id }}" data-entity="{{ node_player.to_json({"created_by": track_created(node_player).username, "updated_by": track_updated(node_player).username}) }}">
<td class="infos">
<div class="inner">
<a href="javascript:void(0);" class="item-sort node-player-sort">
@ -30,6 +35,14 @@
{{ node_player.name }}
</div>
</td>
{% if AUTH_ENABLED %}
<td class="tac">
{% set creator = track_created(node_player) %}
<a href="javascript:void(0);" class="badge item-utrack node-player-utrack {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</a>
</td>
{% endif %}
<td class="tac">
{{ node_player.host }}
</td>

View File

@ -16,7 +16,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.fleet_node_player_page_title }}</h2>
<h2><i class="fa fa-tv icon-left"></i>{{ l.fleet_node_player_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_START) }}
@ -24,7 +24,9 @@
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_END) }}
</div>
</div>
<div class="panel">
<div class="panel">
<div class="panel-body">
<h3>{{ l.fleet_node_player_panel_active }}</h3>
@ -43,7 +45,6 @@
</div>
</div>
<div class="modals hidden">
<div class="modals-outer">
<a href="javascript:void(0);" class="modal-close">
@ -52,6 +53,7 @@
<div class="modals-inner">
{% include 'fleet/player/modal/add.jinja.html' %}
{% include 'fleet/player/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
{{ l.fleet_node_player_form_add_title }}
</h2>
<form action="/fleet/node-player/add" method="POST" enctype="multipart/form-data">
<form class="form" action="/fleet/node-player/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="node-player-add-name">{{ l.fleet_node_player_form_label_name }}</label>
<div class="widget">

View File

@ -3,7 +3,7 @@
{{ l.fleet_node_player_form_edit_title }}
</h2>
<form action="/fleet/node-player/edit" method="POST">
<form class="form" action="/fleet/node-player/edit" method="POST">
<input type="hidden" name="id" id="node-player-edit-id" />
<div class="form-group">

View File

@ -2,6 +2,11 @@
<thead>
<tr>
<th>{{ l.fleet_node_studio_panel_th_name }}</th>
{% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac">{{ l.fleet_node_studio_panel_th_host }}</th>
<th class="tac">{{ l.fleet_node_studio_panel_th_port }}</th>
<th class="tac">{{ l.fleet_node_studio_panel_th_enabled }}</th>
@ -18,7 +23,7 @@
</td>
</tr>
{% for node_studio in node_studios %}
<tr class="node-studio-item" data-level="{{ node_studio.id }}" data-entity="{{ node_studio.to_json() }}">
<tr class="node-studio-item" data-level="{{ node_studio.id }}" data-entity="{{ node_studio.to_json({"created_by": track_created(node_studio).username, "updated_by": track_updated(node_studio).username}) }}">
<td class="infos">
<div class="inner">
<a href="javascript:void(0);" class="item-sort node-studio-sort">
@ -31,6 +36,14 @@
{{ node_studio.name }}
</div>
</td>
{% if AUTH_ENABLED %}
<td class="tac">
{% set creator = track_created(node_studio) %}
<a href="javascript:void(0);" class="badge item-utrack node-studio-utrack {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</a>
</td>
{% endif %}
<td class="tac">
{{ node_studio.host }}
</td>

View File

@ -16,7 +16,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.fleet_node_studio_page_title }}</h2>
<h2><i class="fa fa-server icon-left"></i>{{ l.fleet_node_studio_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_FLEET_NODE_STUDIO_TOOLBAR_ACTIONS_START) }}
@ -53,6 +53,7 @@
<div class="modals-inner">
{% include 'fleet/studio/modal/add.jinja.html' %}
{% include 'fleet/studio/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
{{ l.fleet_node_studio_form_add_title }}
</h2>
<form action="/fleet/node-studio/add" method="POST" enctype="multipart/form-data">
<form class="form" action="/fleet/node-studio/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="node-studio-add-name">{{ l.fleet_node_studio_form_label_name }}</label>
<div class="widget">

View File

@ -3,7 +3,7 @@
{{ l.fleet_node_studio_form_edit_title }}
</h2>
<form action="/fleet/node-studio/edit" method="POST">
<form class="form" action="/fleet/node-studio/edit" method="POST">
<input type="hidden" name="id" id="node-studio-edit-id" />
<div class="form-group">

View File

@ -2,6 +2,11 @@
<thead>
<tr>
<th>{{ l.playlist_panel_th_name }}</th>
{% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac"><i class="fa fa-compass"></i></th>
<th class="tac">{{ l.playlist_panel_th_enabled }}</th>
<th class="tac">{{ l.playlist_panel_th_duration }}</th>
@ -18,7 +23,7 @@
</td>
</tr>
{% for playlist in playlists %}
<tr class="playlist-item" data-level="{{ playlist.id }}" data-entity="{{ playlist.to_json() }}">
<tr class="playlist-item" data-level="{{ playlist.id }}" data-entity="{{ playlist.to_json({"created_by": track_created(playlist).username, "updated_by": track_updated(playlist).username}) }}">
<td class="infos">
<div class="inner">
{% if playlist.id %}
@ -31,6 +36,16 @@
{{ playlist.name }}
</div>
</td>
{% if AUTH_ENABLED %}
<td class="tac">
{% if playlist.id %}
{% set creator = track_created(playlist) %}
<a href="javascript:void(0);" class="badge item-utrack playlist-utrack {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</a>
{% endif %}
</td>
{% endif %}
<td class="tac">
{% if playlist.time_sync %}

View File

@ -16,7 +16,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.playlist_page_title }}</h2>
<h2><i class="fa fa-bars-staggered icon-left"></i>{{ l.playlist_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_START) }}
@ -55,6 +55,7 @@
<div class="modals-inner">
{% include 'playlist/modal/add.jinja.html' %}
{% include 'playlist/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
{{ l.playlist_form_add_title }}
</h2>
<form action="/playlist/add" method="POST" enctype="multipart/form-data">
<form class="form" action="/playlist/add" method="POST" enctype="multipart/form-data">
<div class="form-group">

View File

@ -4,7 +4,7 @@
</h2>
<form action="/playlist/edit" method="POST">
<form class="form" action="/playlist/edit" method="POST">
<input type="hidden" name="id" id="playlist-edit-id" />

View File

@ -11,7 +11,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.settings_page_title }}</h2>
<h2><i class="fa fa-cogs icon-left"></i>{{ l.settings_page_title }}</h2>
</div>
<div class="panel">
<div class="panel-body">

View File

@ -3,7 +3,7 @@
{{ l.settings_variable_form_edit_title }}
</h2>
<form action="/settings/variable/edit" method="POST">
<form class="form" action="/settings/variable/edit" method="POST">
<input type="hidden" name="id" id="variable-edit-id" />
<div class="form-group">

View File

@ -23,10 +23,7 @@
</td>
</tr>
{% for slide in slides %}
<tr class="slide-item" data-level="{{ slide.id }}" data-entity="{{ slide.to_json({
"created_by": track_created(slide).username,
"updated_by": track_updated(slide).username
}) }}">
<tr class="slide-item" data-level="{{ slide.id }}" data-entity="{{ slide.to_json({"created_by": track_created(slide).username, "updated_by": track_updated(slide).username}) }}">
<td class="infos">
<div class="inner">
<a href="javascript:void(0);" class="item-sort slide-sort">

View File

@ -19,7 +19,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.slideshow_page_title }}</h2>
<h2><i class="fa-regular fa-clock icon-left"></i>{{ l.slideshow_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_SLIDESHOW_TOOLBAR_ACTIONS_START) }}
@ -91,7 +91,7 @@
<div class="modals-inner">
{% include 'slideshow/modal/add.jinja.html' %}
{% include 'slideshow/modal/edit.jinja.html' %}
{% include 'slideshow/modal/utrack.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>

View File

@ -3,14 +3,14 @@
{{ l.slideshow_slide_form_add_title }}
</h2>
<form action="/slideshow/slide/add" method="POST" enctype="multipart/form-data">
<form class="form" action="/slideshow/slide/add" method="POST" enctype="multipart/form-data">
<h3>
{{ l.slideshow_slide_form_section_content }}
</h3>
{% if current_playlist %}
<input name="playlist" type="hidden" id="slide-add-playlist" value="{{ current_playlist.id }}">
<input name="playlist_id" type="hidden" id="slide-add-playlist-id" value="{{ current_playlist.id }}">
{% endif %}
<div class="form-group">

View File

@ -4,7 +4,7 @@
</h2>
<form action="/slideshow/slide/edit" method="POST">
<form class="form" action="/slideshow/slide/edit" method="POST">
<h3>
{{ l.slideshow_slide_form_section_content }}

View File

@ -11,7 +11,7 @@
{% block page %}
<div class="toolbar">
<h2>{{ l.sysinfo_page_title }}</h2>
<h2><i class="fa fa-list-check icon-left"></i>{{ l.sysinfo_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_SYSINFO_TOOLBAR_ACTIONS_START) }}
<button class="purple sysinfo-restart"><i class="fa fa-refresh icon-left"></i>{{ l.sysinfo_panel_button_restart }}</button>