From 423505cdf54c52947482509329c2a1c8ece97c39 Mon Sep 17 00:00:00 2001 From: George Halkiadakis Date: Wed, 19 Apr 2023 05:22:22 +0300 Subject: admin back-office environment: lessons management --- Dockerfile | 3 +- docker/bin/docker-entrypoint.sh | 4 +- public/app/controllers/admin/Course_admin.php | 93 ++++++++ public/app/controllers/admin/Users_admin.php | 42 ++++ public/app/models/admin/Privilege_model.php | 42 ++++ public/app/models/cms/Course_model.php | 3 +- public/app/routes/backend.php | 81 +++++-- public/app/views/admin/admin-menu.php | 4 +- public/app/views/admin/categories.php | 16 +- public/app/views/admin/new_lesson.php | 144 +++++++++++++ public/app/views/admin/privileges.php | 93 ++++++++ public/assets/css/overides.css | 45 +++- public/assets/js/admin/categories.js | 4 +- public/assets/js/admin/privileges.js | 291 ++++++++++++++++++++++++++ public/cache/.gitkeep | 0 15 files changed, 828 insertions(+), 37 deletions(-) create mode 100644 public/app/controllers/admin/Course_admin.php create mode 100644 public/app/controllers/admin/Users_admin.php create mode 100644 public/app/models/admin/Privilege_model.php create mode 100644 public/app/views/admin/new_lesson.php create mode 100644 public/app/views/admin/privileges.php create mode 100644 public/assets/js/admin/privileges.js delete mode 100755 public/cache/.gitkeep diff --git a/Dockerfile b/Dockerfile index ef8e94e..6e76571 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,8 +78,7 @@ RUN locale-gen # Manage permitions in specific directories # ------------------------------------------------------------------------------ # (make local storage directories writable) -# RUN chown -R www-data:www-data /var/www -RUN chmod -R 757 /var/www/public/cache +RUN chown -R www-data:www-data /var/www RUN chmod -R 757 /var/www/storage diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index a01f3a3..f421d80 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -4,8 +4,6 @@ # ------------------------------------------------------------------------------ # grand to 'cache' directory writable permissions # set file-cache directory to apache's user ownership -chmod -R 755 /var/www/public/cache -chown -R www-data:www-data /var/www/public/cache chmod -R 755 /var/www/storage chown -R www-data:www-data /var/www/storage @@ -24,4 +22,4 @@ cp /var/www/docker/config/composer-naked.json /var/www/composer.json # start apache # ------------------------------------------------------------------------------ # End with running the original command -exec /usr/sbin/apache2ctl -D FOREGROUND \ No newline at end of file +exec /usr/sbin/apache2ctl -D FOREGROUND diff --git a/public/app/controllers/admin/Course_admin.php b/public/app/controllers/admin/Course_admin.php new file mode 100644 index 0000000..0e34e00 --- /dev/null +++ b/public/app/controllers/admin/Course_admin.php @@ -0,0 +1,93 @@ +query( + "INSERT INTO course (parent_id, label) VALUES (:parent, :label)", + [ + ':parent' => Registry::get('REQUEST')->POST['parent_id'], + ':label' => Registry::get('REQUEST')->POST['label'] + ] + )->lastInsertID(); + + $cache = self::update_courses_cache(); // update category caches + return $cache['breadcrumbs']; // return + } + + + /** update course + * --- + * @param void (get data from POST) + */ + public static function update_course() + { + Registry::use('database')->query( + "UPDATE course SET parent_id = :parent, label = :label + WHERE id = :id", + [ + ':id' => Registry::get('REQUEST')->POST['id'], + ':parent' => Registry::get('REQUEST')->POST['parent_id'], + ':label' => Registry::get('REQUEST')->POST['label'] + ] + ); + $cache = self::update_courses_cache(); + return $cache['breadcrumbs']; + } + + + /** update courses cache + * --- -- -- - - - + * Shall run if anything changes to courses + * + * @param void + * @return (array): the two course caches + */ + public static function update_courses_cache() + { + $tree = proxy( // cache-get categories + [\app\models\cms\Course_model::class, 'category_tree'], + [], CACHE_ROOT_TTL, PROXY_IGNORE_CACHE + ); + $breadcrumbs = proxy( // cache-get breadcrumbs + [\app\models\cms\Course_model::class, 'breadcrumbs'], + [$tree], CACHE_ROOT_TTL, PROXY_IGNORE_CACHE + ); + + return [ + 'tree' => $tree, + 'breadcrumbs' => $breadcrumbs + ]; + } + +} \ No newline at end of file diff --git a/public/app/controllers/admin/Users_admin.php b/public/app/controllers/admin/Users_admin.php new file mode 100644 index 0000000..4e335c1 --- /dev/null +++ b/public/app/controllers/admin/Users_admin.php @@ -0,0 +1,42 @@ +runQuery( + "SELECT * FROM privilege",[] + ); + } + + /** edit privileges + * --- + */ + public static function edit_privileges() + { + // TODO: ... + } + + + /** update course + * --- + */ + public static function update_course() + { + // TODO: ... + } + +} \ No newline at end of file diff --git a/public/app/models/admin/Privilege_model.php b/public/app/models/admin/Privilege_model.php new file mode 100644 index 0000000..c385484 --- /dev/null +++ b/public/app/models/admin/Privilege_model.php @@ -0,0 +1,42 @@ +runQuery( + "SELECT * FROM privilege",[] + ); + } + + /** edit privileges + * --- + */ + public static function edit_privileges() + { + // TODO: ... + } + + + /** update course + * --- + */ + public static function update_course() + { + // TODO: ... + } + +} \ No newline at end of file diff --git a/public/app/models/cms/Course_model.php b/public/app/models/cms/Course_model.php index 4fb49c0..4740a4f 100644 --- a/public/app/models/cms/Course_model.php +++ b/public/app/models/cms/Course_model.php @@ -14,7 +14,7 @@ class Course_model { public static function get_categories() { return Registry::use('database')->runQuery( - "SELECT * FROM course", + "SELECT * FROM course ORDER BY `order`", [] ); } @@ -74,6 +74,7 @@ class Course_model { 'rec' => [ 'id' => $rec['id'], 'label' => $rec['label'], + 'order' => $rec['order'], 'parent' => $rec['parent_id'] ], 'childs' => self::to_tree($dataset, $child) // recursively diff --git a/public/app/routes/backend.php b/public/app/routes/backend.php index 700fa14..5356407 100644 --- a/public/app/routes/backend.php +++ b/public/app/routes/backend.php @@ -1,7 +1,8 @@ 'Διαχείριση Δικαιωμάτων Πρόσβασης', + 'action' => 'privileges', + ]); +}); + +// ajax: get all privileges (courses; with breadcrumbs) +// use proxy (cache results); send as json +// --- +Route::add('/admin/api/privileges', function () { + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json( proxy( + [\app\models\admin\Privilege_model::class, 'privileges'], [], CACHE_ROOT_TTL - ); - - $breadcrumbs = proxy( - [\app\models\cms\Course_model::class, 'breadcrumbs'], - [$tree], CACHE_ROOT_TTL - ); - // $breadcrumbs = Course_model::breadcrumbs($tree); - Render::json($breadcrumbs); -}); \ No newline at end of file + )); +}); + +// add privilege + +// update privilege + + + +// new lesson +// ----------------------------------------------------------------------------- +Route::add('/admin/new_lesson', function () { + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::view('admin/skeleton', [ + 'title' => 'Νέο Μάθημα', + 'action' => 'new_lesson' + ]); +}); + diff --git a/public/app/views/admin/admin-menu.php b/public/app/views/admin/admin-menu.php index 813420d..d7e7b09 100644 --- a/public/app/views/admin/admin-menu.php +++ b/public/app/views/admin/admin-menu.php @@ -16,11 +16,11 @@ $admin_options = [ - 'new' => 'Νέο Μάθημα', + 'new_lesson' => 'Νέο Μάθημα', 'lessons' => 'Μαθήματα', 'categories' => 'Κατηγορίες', 'pages' => 'Σελίδες', - 'privileges' => 'Δικαιώματα', + 'privileges' => 'Πρόσβαση', 'users' => 'Χρήστες' ]; diff --git a/public/app/views/admin/categories.php b/public/app/views/admin/categories.php index e33d9b0..91acec1 100644 --- a/public/app/views/admin/categories.php +++ b/public/app/views/admin/categories.php @@ -1,9 +1,9 @@
-
Διαχείριση Κατηγοριών
+
Διαχείριση Κατηγοριών
- @@ -16,7 +16,7 @@
- +
@@ -48,25 +48,19 @@
Κατηγορία
+ + + + + + +
ΚωδικόςΕπίπεδο ΠρόσβασηςΠεριλαμβάνει
+ +
+ + +
+
+ + + + + + + diff --git a/public/assets/css/overides.css b/public/assets/css/overides.css index 172d24e..0709f4d 100644 --- a/public/assets/css/overides.css +++ b/public/assets/css/overides.css @@ -40,13 +40,13 @@ html, body { font-family: "Roboto Slab", serif; font-weight: 400; } .admin-container { font-size: .96em; } .admin-container .top-bar a:hover { background: #ddd; } -.admin-container .manage { padding: .5em .5em 1.5em .5em; } +.admin-container .manage { padding: 1em; } .admin-container .title-bar { display: flex; justify-content: space-between; align-items: center; - padding: 1em 8px 1em 16px; + padding: 1.5em 1em 1.5em 1em; } .admin-container .dt-table { font-size: 14px; } @@ -58,11 +58,48 @@ html, body { font-family: "Roboto Slab", serif; font-weight: 400; } } .admin-container .modal .form-group { padding: .35em 0;} -.admin-container .modal .form-control { padding: 1px 8px; border-radius: 4px; font-size: 15px; } +/*.admin-container .modal .form-control { padding: 1px 8px; border-radius: 4px; font-size: 15px; } */ .admin-container .table > :not(caption) > * > * { padding: .35rem .35rem; } .admin-container .table tbody tr:hover { background: #eee; } ul.select2-results__options li { font-size: 14px; padding: 4px 8px; } -.admin-container .modal-footer .form-actions { display: flex; } \ No newline at end of file +.admin-container .modal-footer .form-actions { display: flex; } + +.admin-container .title-bar h5 { margin-bottom: 0; color: #333; } + +.admin-container table.dataTable { margin-top: 1em !important; margin-bottom: 1em !important;} + +.admin-container .title-bar .btn { border: 1px solid #7772; } +.admin-container .title-bar .btn:hover { background: #ddd; } + +.admin-container form label { padding: 8px 1em 2px 1em; } + +.admin-container .form-control { border-radius: 4px; } + +.admin-container select[data-chosen='one'] { + font-weight: 700; + color: #000; +} + +/* hack select2 caret styling + * (like bootstrap5 select caret) + * ----------------------------------------------------------------------------- + */ + +.select2-container--default .select2-selection--single .select2-selection__arrow b { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + background-color: transparent; + background-size: contain; + border: none !important; + height: 16 !important; + width: 12px !important; + margin: auto !important; + top: 9px !important; + left: auto !important; + height: 14px; +} +.select2-container--default .select2-selection--single .select2-selection__arrow { width: 24px; } +.select2-container--default .select2-selection--single { height: 31px; } + \ No newline at end of file diff --git a/public/assets/js/admin/categories.js b/public/assets/js/admin/categories.js index 0ff2b85..90eede3 100644 --- a/public/assets/js/admin/categories.js +++ b/public/assets/js/admin/categories.js @@ -119,7 +119,7 @@ $(document).ready(function() { function init_table() { // get all categories from server (ajax) - $.getJSON( "/admin/api/categories?exclude=0") + $.getJSON( "/admin/api/categories") .done(function( json ) { // then ... @@ -272,7 +272,7 @@ $(document).ready(function() { }; // select action url (add or update) - var request = '/admin/category?action=' + ((data.id == 0) ? 'add' : 'update'); + var request = '/admin/api/category/' + ((data.id == 0) ? 'add' : 'update'); // send POST request $.post(request, data) diff --git a/public/assets/js/admin/privileges.js b/public/assets/js/admin/privileges.js new file mode 100644 index 0000000..e4355c5 --- /dev/null +++ b/public/assets/js/admin/privileges.js @@ -0,0 +1,291 @@ +// Globals +// ----------------------------------------------------------------------------- +var privileges = []; +var table; + + + +// Supplamentary functions +// ----------------------------------------------------------------------------- + +// array.indexOf polyfill +// --- -- -- - - - +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(elt /*, from*/) + { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) + ? Math.ceil(from) + : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) + { + if (from in this && + this[from] === elt) + return from; + } + return -1; + }; +} + +/* +// function for sorting an array by 'breadcrumb' attribute +// --- -- -- - - - +function compare_breadcrumb (a,b) { + if ( a.breadcrumb < b.breadcrumb ) { return -1; } + if ( a.breadcrumb > b.breadcrumb ) { return 1; } + return 0; +} +*/ + +// return category record by category-id +// --- -- -- - - - +function privilege_record(id) { + var record = 0; + privileges.forEach(el => { if (el.id == id) record = el; }); + + return record; +} + +/* +// return children of parent +// --- -- -- - - - +function childs_of_parents(list) { + // if 'list' is integer then make it an one-item list + parents = (Number.isInteger(list)) ? [ list ] : list; + + var childs = []; + + if (parents.length == 0) { return []; } + + parents.forEach( pid => { + categories.forEach( el => { if (el.parent == pid) childs.push(el.id)}); + }); + + var grandchilds = childs_of_parents(childs); + + return childs.concat(grandchilds); +} +*/ + +// create actions html (edit and delete buttons) +// --- -- -- - - - +function create_actions_html(id) { + + var del, edit; + + del = ""; + + edit = [ + '' + ].join('\n'); + + return edit +' '+ del; +} + + + +$(document).ready(function() { + + // When document is ready + // ------------------------------------------------------------------------- + + + /** init selec2 element (used to specify parent-category) + * --- -- -- - - - + */ + $("#included_id").select2({ + placeholder: 'Περιλαμβάνει τα δικαιώματα της', + dropdownParent: $('#managePrivileges'), + width: '100%', + // dropdownParent: "#manageCategory" // ref: https://github.com/select2/select2-bootstrap-theme/issues/41 + }); + + + /** init_table + * --- -- -- - - - + * get categories + * constuct categories array + * render dataTable + */ + function init_table() { + + // get all categories from server (ajax) + $.getJSON( "/admin/api/privileges") + .done(function( json ) { + // then ... + + // reset categories + privileges.length = 0; + + // construct (new) categories data + Object.keys(json).forEach( key => { + el = json[key]; + privileges.push({ + id: el.id, + label: el.label, + includes: ((el.includes == null) ? [] : el.includes), + included_id: ((el.included_id == null) ? 0 : el.included_id), + actions: create_actions_html(el.id) + }); + }); + + // sort by breadcrumb + // categories.sort( compare_breadcrumb ); + + // destroy previous datatable table instances + $('#dt-privileges').dataTable().fnClearTable(); + $('#dt-privileges').dataTable().fnDestroy(); + + // (re-)create categories datatable + table = $('#dt-privileges').DataTable({ + language: { url: '/libs/DataTables/localization/Greek.json' }, + data: privileges, + ordering: false, + columns: [ + { data: 'id' }, + { data: 'label' }, + { data: 'includes' }, + { data: 'actions' } + ] + }); + + }) + .fail(function( jqxhr, textStatus, error ) { + console.log( "Request Failed (" + error +")" ); + }); + + } + init_table(); // init table on first run + + + + /** When modal show-up + * ------------------------------------------------------------------------- + * ... prepare the manage-category form + * ... update select2 with possible parents + */ + $('#managePrivileges').on('show.bs.modal', event => { + var button = $(event.relatedTarget); // Button that triggered the modal + var id = parseInt( button.data('id') ); // category_id from data-id + // var modal = $(this); // modal object + + // update modal literature + // --- + $('#managePrivileges .modal-title').html( // modal title + (id) ? 'Ενημέρωση Επιπέδου Πρόσβασης' : 'Δημιουργία Επιπέδου Πρόσβασης' + ); + $('#managePrivileges form input[name=id]').val( id ); // category id (hidden) + $('#managePrivileges form input[name=label]').val( // category label + (id) ? privilege_record(id).label : "" + ); + prepare_included_element(id); // prepare + * -> choose selected parent + * @param may_included (array): array of possible included privileges + * @param selected (int): selected included privilege + */ + function set_included_options(may_included, selected) { + // sort parents by breadcrumb + // parents.sort( compare_breadcrumb ); + + // remove any olded options from select2 + $('#included_id option').each(function() { $(this).remove(); }); + + // first option is the root category + $('#included_id').append('') + + // add all other parent options + may_included.forEach( i => { + $('#included_id').append(``); + }); + + if (selected == -1) { // new category; remove any parent pre-selection + $('#included_id').val(0).trigger('change'); + + } else { // category exists; choose selected parent + $('#included_id').val(selected).trigger('change'); + } + } + + + + + // When form is submited + // ------------------------------------------------------------------------- + + $('#manageCategory form').submit( event => { + event.preventDefault(); + + // prepare data to POST + var data = { + id: parseInt($('#manageCategory form input[name=id]').val()), + label: $('#manageCategory form input[name=label]').val(), + parent_id: parseInt($('#parent_id').select2('data')[0].id) + }; + + // select action url (add or update) + var request = '/admin/api/category/' + ((data.id == 0) ? 'add' : 'update'); + + // send POST request + $.post(request, data) + .done(function( data ) { + + $('#manageCategory').modal('hide'); // when done, close modal + + init_table(); // reload table of categories + }); + + }); + +}); diff --git a/public/cache/.gitkeep b/public/cache/.gitkeep deleted file mode 100755 index e69de29..0000000 -- cgit v1.2.3