Compare commits

..
Author SHA1 Message Date
adisposkofh 4058ecebdf displaying wahlname where applicable in old cis search results and profile 2026-10-09 11:43:48 +02:00
adisposkofh 5c0cbcd234 phrase fix wahlname -> wahlvorname 2026-10-08 17:38:38 +02:00
adisposkofh aa68181a3b displaying chosen name in mailing lists 2026-10-08 16:57:36 +02:00
adisposkofh cad122bef6 neu cis displaying wahlname 2026-10-08 15:02:12 +02:00
Andreas Österreicher 0c4d102850 Merge branch 'bug-83698/StudVW_Projektarbeit_Kontaktdaten' 2026-10-07 09:11:42 +02:00
Andreas Österreicher 2e15f2c5ab Edit Dialog von Kontaktdaten bei Projektbetreuer die Mitarbeiter sind wird nicht angezeigt 2026-10-07 09:10:51 +02:00
Harald Bamberger 784e5693b4 compat component add/remove css class via lifecycle hooks to apply compat specific rules 2026-10-06 15:22:16 +02:00
Andreas Österreicher 88b49b70e9 Fehlermeldung korrigiert beim Endupload wenn Zweitbetreuer keine E-Mail hinterlegt hat 2026-10-06 14:47:45 +02:00
Harald Bamberger 67a33f2ceb revert dashboard item overscroll-behavior to auto, so scrolling the page is not only possible on the page borders 2026-10-06 14:15:46 +02:00
Harald Bamberger 751445c199 Merge branch 'feature-83444/lvverwaltung_suche_nach_le_und_lv_inkl_router' 2026-10-06 10:59:19 +02:00
Harald Bamberger 916f5dd68e disable search for lv and le for the moment 2026-10-06 10:58:20 +02:00
Harald Bamberger efc8fa1eeb Merge branch 'master' into feature-83444/lvverwaltung_suche_nach_le_und_lv_inkl_router 2026-10-06 10:27:46 +02:00
Harald Bamberger d58edff92d fix propertyname for api response 2026-10-05 18:26:42 +02:00
Harald Bamberger 643929a0c6 add cis4 class to body to make it harder for cms content style sheets to override padding, add names to calendar events renderer components, show lvmenu not only for route MyLvPlan but e.g. also in Dashboard LvPlan Widget 2026-10-05 17:56:57 +02:00
Harald Bamberger 4ba2869632 Merge branch 'master' into bugfix-83059_incorrect-ical-semester-parameters 2026-10-05 11:22:59 +02:00
Andreas Österreicher 293bb77179 Merge branch 'bug-44023/EingabemaskeEntwicklungsteamNamenOhneUid2' 2026-10-02 13:23:02 +02:00
Andreas Österreicher 3fe10d715d Fixed Saving of Entwicklungsteam 2026-10-02 13:14:41 +02:00
Manuela Thamer 3034615455 fix pagelock of modals
prevent editing contact data of employees
2026-10-01 17:27:54 +02:00
Andreas Österreicher 36064afbba Fixed Testing Projektarbeit Seeds 2026-10-01 15:33:05 +02:00
Harald Bamberger f3d031ab16 cismenu: add cis-main-menu class to main nav, add li elements to ul, style cis-main-menu ul and li to prevent interferec with cms content styles, add draggable false to cis menu entries to improve menu scrolling especially on mobile devices 2026-10-01 11:48:51 +02:00
Harald Bamberger 3d8c4fa4d0 Merge branch 'feature-77757/WidgetsBerechtigung' 2026-09-30 15:48:00 +02:00
Harald Bamberger 272014bd78 remove odd phrases 2026-09-30 15:38:43 +02:00
Harald Bamberger 76a8e00edf Merge branch 'master' into feature-77757/WidgetsBerechtigung 2026-09-30 15:23:08 +02:00
ma0048 e369a8c03f LV-Verwaltung:
- Semester unter Orgform im Verbandsbaum
- Kombinierte LV/LE-Suche
- Semester/Orgform/Lehrform in Suchergebnissen
2026-09-28 08:42:37 +02:00
ma0048 faad2c4a9a lv auch anzeigen wenn keine lvteile in dem semester dran haengen
phrasen hinzugefuegt
2026-09-18 12:34:47 +02:00
ma0048 2ffaf36270 suche fuer lehreinheiten und lehrveranstaltungen in der lvverwaltung 2026-09-15 15:46:48 +02:00
Harald Bamberger c8c7d1566c Merge branch 'master' into bugfix-83059_incorrect-ical-semester-parameters 2026-09-11 11:43:23 +02:00
adisposkofh 9d4218bf38 implemented displayed semester/ical dates changes from my lv plan to other lv plan 2026-09-09 11:36:19 +02:00
adisposkofh 67f30c7765 fixed issue with wrong semester displayed and wrong ical dates used in my lv plan 2026-09-09 11:30:43 +02:00
Johann Hoffmann d40040efd8 added new fkey to tbl_widget for system.tbl_berechtigung; check for permission status during load & in widget picker; if widget was already picked/configured but not permitted anymore -> show a generalized "missing permission" screen via fallback component during widget init; 2026-07-21 18:14:26 +02:00
136 changed files with 1359 additions and 16230 deletions
-21
View File
@@ -1,21 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
// Preferred organisational unit for a new content.
// If the user is entitled for this unit, it wins.
// If not, the first entitled unit wins.
$config['default_oe_kurzbz'] = 'etw';
// Lifetime of an edit lock in hours, counted from tbl_contentlog.start. Must be > 0.
$config['lock_ttl_hours'] = 24;
// Switches the click statistics off completely. With false the admin tab stays hidden and
// the endpoint refuses every request, whatever the rights of the caller are.
$config['clickstats_enabled'] = true;
// Default period of the click statistics in months, when the caller sends none.
$config['clickstats_months'] = 12;
// Length of the ranking in the click statistics tab.
$config['clickstats_limit'] = 100;
+8 -6
View File
@@ -75,10 +75,6 @@ $route['Abgabetool/Student'] = 'Cis/Abgabetool/Student';
$route['Abgabetool/Student/(:any)'] = 'Cis/Abgabetool/Student/$1';
$route['Abgabetool/Deadlines'] = 'Cis/Abgabetool/Deadlines';
// CMS Admin App Routes
$route['cms'] = 'cms/index';
$route['cms/.*'] = 'cms/index';
// Studierendenverwaltung App Routes
$route['studvw'] = 'studentenverwaltung/index';
$route['studvw/.*'] = 'studentenverwaltung/index';
@@ -133,13 +129,19 @@ $route['api/frontend/v1/treemenudata/stv/.*'] = 'api/frontend/v1/stv/students/in
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByStg/$1/$2';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/semester/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByStg/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/semester/(:any)/.*'] = 'api/frontend/v1/Lehrveranstaltung/getByStg/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/orgform/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByStg/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/orgform/(:any)/.*'] = 'api/frontend/v1/Lehrveranstaltung/getByStg/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/orgform/(:any)/semester/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByStgOrgform/$1/$2/$3/$4';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/orgform/(:any)/semester/(:any)/.*'] = 'api/frontend/v1/Lehrveranstaltung/getByStgOrgform/$1/$2/$3/$4';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/orgform/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByStgOrgform/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/stg/(:any)/orgform/(:any)/.*'] = 'api/frontend/v1/Lehrveranstaltung/getByStgOrgform/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/emp/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByEmp/$1/$2';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/emp/(:any)/stg/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByEmp/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/emp/(:any)/stg/(:any)/.*'] = 'api/frontend/v1/Lehrveranstaltung/getByEmp/$1/$2/$3';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/le/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByLe/$1/$2';
$route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/lv/(:any)'] = 'api/frontend/v1/Lehrveranstaltung/getByLv/$1/$2';
// load routes from extensions, also look for environment-specific configs
$subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions'];
+44 -4
View File
@@ -26,6 +26,10 @@ $config['person'] = [
'comparison' => 'similar',
'field' => 'nachname'
],
'wahlname' => [
'comparison' => 'similar',
'field' => 'wahlname'
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)"
@@ -68,7 +72,7 @@ $config['person'] = [
'resultfields' => [
"ARRAY( SELECT uid FROM public.tbl_benutzer WHERE person_id = p.person_id ) AS uids",
"p.person_id",
"(p.vorname || ' ' || p.nachname) AS name",
"CASE WHEN p.wahlname IS NOT NULL THEN (p.wahlname || ' ' || p.nachname) ELSE (p.vorname || ' ' || p.nachname) END AS name",
"ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email",
"CASE
WHEN p.foto IS NOT NULL THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto
@@ -117,6 +121,20 @@ $config['student'] = [
]
]
],
'wahlname' => [
'comparison' => 'similar',
'field' => 'wahlname',
'join' => [
[
'table' => "public.tbl_prestudent",
'using' => "prestudent_id"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
@@ -196,7 +214,7 @@ $config['student'] = [
"s.student_uid AS uid",
"s.matrikelnr",
"p.person_id",
"(p.vorname || ' ' || p.nachname) AS name",
"CASE WHEN p.wahlname IS NOT NULL THEN (p.wahlname || ' ' || p.nachname) ELSE (p.vorname || ' ' || p.nachname) END AS name",
"(s.student_uid || '@" . DOMAIN . "') || ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email",
"CASE
WHEN p.foto IS NOT NULL THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto
@@ -257,6 +275,14 @@ $config['prestudent'] = [
'using' => "person_id"
]
],
'wahlname' => [
'comparison' => 'similar',
'field' => 'wahlname',
'join' => [
'table' => "public.tbl_person",
'using' => "person_id"
]
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
@@ -314,7 +340,7 @@ $config['prestudent'] = [
"s.matrikelnr",
"p.person_id",
"b.uid",
"(p.vorname || ' ' || p.nachname) AS name",
"CASE WHEN p.wahlname IS NOT NULL THEN (p.wahlname || ' ' || p.nachname) ELSE (p.vorname || ' ' || p.nachname) END AS name",
"(b.uid || '@" . DOMAIN . "') || ARRAY( SELECT kontakt FROM public.tbl_kontakt WHERE kontakttyp = 'email' AND person_id=p.person_id ) AS email",
"CASE
WHEN p.foto IS NOT NULL THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto
@@ -390,6 +416,20 @@ $config['employee'] = [
]
]
],
'wahlname' => [
'comparison' => 'similar',
'field' => "wahlname",
'join' => [
[
'table' => "public.tbl_benutzer",
'on' => "uid = mitarbeiter_uid"
],
[
'table' => "public.tbl_person",
'using' => "person_id"
]
]
],
'name' => [
'comparison' => 'similar',
'field' => "(vorname || ' ' || nachname)",
@@ -472,7 +512,7 @@ $config['employee'] = [
'resultfields' => [
"b.uid",
"p.person_id",
"(p.vorname || ' ' || p.nachname) AS name",
"CASE WHEN p.wahlname IS NOT NULL THEN (p.wahlname || ' ' || p.nachname) ELSE (p.vorname || ' ' || p.nachname) END AS name",
"ARRAY(
SELECT
'[' || ot.bezeichnung || '] ' || o.bezeichnung AS bezeichnung
+1 -1
View File
@@ -25,7 +25,7 @@ $config['student']['resultfields'] = [
"s.matrikelnr AS personenkennzeichen",
"p.person_id",
"p.matr_nr AS matrikelnummer",
"(p.vorname || ' ' || p.nachname) AS name",
"CASE WHEN p.wahlname IS NOT NULL THEN (p.wahlname || ' ' || p.nachname) ELSE (p.vorname || ' ' || p.nachname) END AS name",
"ARRAY[s.student_uid || '@' || '" . DOMAIN . "'] AS email",
"CASE
WHEN (p.foto_sperre = false AND p.foto IS NOT NULL) THEN 'data:image/jpeg' || CONVERT_FROM(DECODE('3b','hex'), 'UTF8') || 'base64,' || p.foto
+3 -2
View File
@@ -26,7 +26,8 @@ $config['root'] = [
'verband' => [
'group'
]
],
'orgform'
]
]
];
$config['root']['stg']['orgform'] = $config['root']['stg'];
+7 -10
View File
@@ -44,22 +44,19 @@ class Cms extends Auth_Controller
*
* @return void
*/
public function content($content_id = null, $version = null, $sprache = null, $sichtbar = true)
public function content($content_id, $version = null, $sprache = null, $sichtbar = true)
{
if (!is_numeric($content_id))
show_404();
// return early if the content_id for the content is missing
if (!isset($content_id))
$this->terminateWithError("content_id is missing");
$content = $this->ContentModel->load($content_id);
if (isError($content))
{
show_error('The content could not be loaded');
}
$this->terminateWithError(getError($content));
$content = getData($content);
if (NULL === $content) {
show_404();
}
if (NULL === $content)
$this->terminateWithError("Content not found");
$content = current($content);
-23
View File
@@ -1,23 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Cms extends Auth_Controller
{
public function __construct()
{
parent::__construct(['index' => 'basis/cms:r']);
$this->load->library('PermissionLib');
$this->config->load('cms');
$this->loadPhrases(['global', 'ui', 'cms']);
}
public function index()
{
// decide whether the click statistics tab is shown at all.
$this->load->view('Cms.php', [
'clickstats' => $this->config->item('clickstats_enabled')
&& $this->permissionlib->isBerechtigt('admin')
]);
}
}
@@ -1916,7 +1916,7 @@ class Abgabe extends FHCAPI_Controller
if (!$mailres)
{
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general');
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general');
}
}
@@ -50,6 +50,7 @@ class Cis4FhcApi extends FHCAPI_Controller
$viewData = array(
'uid' => getAuthUID(),
'name' => $personData->vorname,
'wahlname' => $personData->wahlname,
'person_id' => $personData->person_id,
'timezone' => $this->config->item('timezone'),
);
+6 -340
View File
@@ -25,10 +25,6 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
*/
class Cms extends FHCAPI_Controller
{
// Upper bound for the tiefe of oe-personen and dms-liste. A hierarchy this deep does
// not occur, and the cap keeps one marker from walking a whole tree.
const MAX_TIEFE = 10;
/**
* Object initialization
@@ -43,11 +39,6 @@ class Cms extends FHCAPI_Controller
'news' => self::PERM_LOGGED,
'getNewsRowCount' => self::PERM_LOGGED,
'getNews' => self::PERM_LOGGED,
'getOePersonen' => self::PERM_LOGGED,
'getDmsKategorie' => self::PERM_LOGGED,
'getContentChilds' => self::PERM_LOGGED,
'getPerson' => self::PERM_LOGGED,
'getDmsDokumente' => self::PERM_LOGGED,
]);
@@ -64,7 +55,11 @@ class Cms extends FHCAPI_Controller
]);
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
//------------------------------------------------------------------------------------------------------------------
// Public methods
@@ -85,10 +80,7 @@ class Cms extends FHCAPI_Controller
$sprache = $this->input->get("sprache",TRUE);
$sichtbar = $this->input->get("sichtbar",TRUE);
// cms admin preview renders a content without counting a view
$preview = $this->input->get("preview",TRUE);
$content = $this->cmslib->getContent($content_id, $version, $sprache, $sichtbar, !$preview);
$content = $this->cmslib->getContent($content_id, $version, $sprache, $sichtbar);
$content = $this->getDataOrTerminateWithError($content);
$this->terminateWithSuccess($content);
@@ -211,331 +203,5 @@ class Cms extends FHCAPI_Controller
}
/**
* Lists the people assigned to an organisation unit.
* Used by the CMS content component "oe-personen".
*/
public function getOePersonen()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('oe_kurzbz', 'Organisationseinheit', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel');
$this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel');
$oe_kurzbz = $this->input->get('oe_kurzbz', TRUE);
$mitFoto = $this->input->get('foto', TRUE) ? true : false;
// tiefe 0 asks about the one unit. Deeper takes the sub units with it.
$oeResult = $this->OrganisationseinheitModel->getMitUntereinheiten(
$oe_kurzbz, $this->_tiefe()
);
$oeRows = $this->getDataOrTerminateWithError($oeResult) ?: array();
$uids = array();
$funktionen = array();
foreach ($oeRows as $oe)
{
// 'oezuordnung' is the function that assigns an employee to an organisation
// unit. The model already filters datum_von and datum_bis.
$rows = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed(
'oezuordnung', $oe->oe_kurzbz
);
$rows = $this->getDataOrTerminateWithError($rows) ?: array();
foreach ($rows as $row)
{
// The model returns the aktiv flag but does not filter on it.
if (!$row->aktiv)
continue;
// One person can hold the assignment in several units of the walk. The
// first one wins, so the list keeps one block per person.
if (isset($funktionen[$row->uid]))
continue;
$uids[] = $row->uid;
// tbl_benutzerfunktion.bezeichnung is the free label of one assignment,
// for example "Bibliothekarin". tbl_funktion.beschreibung is the generic
// name.
$funktionen[$row->uid] = $row->bezeichnung ?: $row->beschreibung;
}
}
$this->terminateWithSuccess($this->_cmsPersonen($uids, $funktionen, $mitFoto));
}
/**
* Contact data of one person.
* Used by the CMS content component "person-block".
*/
public function getPerson()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('uid', 'UID', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$uid = $this->input->get('uid', TRUE);
$mitFoto = $this->input->get('foto', TRUE) ? true : false;
// The function label comes from the marker, not from the database. The component
// passes it through as a property.
$personen = $this->_cmsPersonen(array($uid), array(), $mitFoto);
$this->terminateWithSuccess($personen ? current($personen) : null);
}
/**
* Lists the published documents of one DMS category.
* Used by the CMS content component "dms-liste".
*/
public function getDmsKategorie()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('kategorie_kurzbz', 'DMS-Kategorie', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$kategorie_kurzbz = $this->input->get('kategorie_kurzbz', TRUE);
$erlaubt = $this->_dmsKategorieErlaubt($kategorie_kurzbz);
if ($erlaubt === null)
$this->terminateWithError('DMS category not found', self::ERROR_TYPE_404);
// a missing entitlement answers with success and an empty list, not with
// an error. The component sits inside a public content page. An error would raise a
// toast for every reader who is simply not in the group.
$dokumente = array();
if ($erlaubt)
{
// tiefe 0 asks about the one category. Deeper takes the sub categories with it.
$katResult = $this->DmsModel->getMitUnterkategorien(
$kategorie_kurzbz, $this->_tiefe()
);
$kategorien = $this->getDataOrTerminateWithError($katResult) ?: array();
foreach ($kategorien as $kat)
{
// Every category carries its own group rule, so a sub category can be
// closed while the parent is open. Ask for each one instead of letting the
// entitlement of the root stand for the whole walk. The root is already
// answered above.
if ($kat->kategorie_kurzbz !== $kategorie_kurzbz
&& $this->_dmsKategorieErlaubt($kat->kategorie_kurzbz) !== true)
continue;
$rows = $this->DmsModel->getKategorieDokumente($kat->kategorie_kurzbz);
$rows = $this->getDataOrTerminateWithError($rows) ?: array();
foreach ($rows as $row)
$dokumente[] = $row;
}
}
$this->terminateWithSuccess(array(
'zugriff' => $erlaubt,
'dokumente' => $dokumente
));
}
/**
* Lists named documents, in the order the caller asked for them.
* Used by the CMS content component "dms-dokumente".
*/
public function getDmsDokumente()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('dms_ids', 'DMS-IDs', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('content/Dms_model', 'DmsModel');
$ids = array();
foreach (explode(',', $this->input->get('dms_ids', TRUE)) as $teil)
{
$teil = trim($teil);
if (is_numeric($teil))
$ids[] = (int) $teil;
}
// An unbounded list would let one marker fetch the whole DMS.
$ids = array_slice(array_unique($ids), 0, 50);
$dokumente = $this->DmsModel->getDokumenteByIds($ids);
$dokumente = $this->getDataOrTerminateWithError($dokumente) ?: array();
// A hand picked list holds few categories, so deciding per category is cheap and
// reuses the rule of getDmsKategorie().
$erlaubte = array();
$nachId = array();
foreach ($dokumente as $dokument)
{
if (!array_key_exists($dokument->kategorie_kurzbz, $erlaubte))
$erlaubte[$dokument->kategorie_kurzbz] = $this->_dmsKategorieErlaubt($dokument->kategorie_kurzbz);
if ($erlaubte[$dokument->kategorie_kurzbz] === true)
$nachId[$dokument->dms_id] = $dokument;
}
// The editor chose the order. Keep it.
$sortiert = array();
foreach ($ids as $id)
{
if (isset($nachId[$id]))
$sortiert[] = $nachId[$id];
}
$this->terminateWithSuccess($sortiert);
}
/**
* Lists the child pages of a content, as a reader may see them.
* Used by the CMS content component "contentchild-menu".
*/
public function getContentChilds()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural_no_zero');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('content/Contentchild_model', 'ContentchildModel');
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
if (!$sprache)
$sprache = getUserLanguage();
$childs = $this->ContentchildModel->getChildsForReader($content_id, $sprache, getAuthUID());
$this->terminateWithSuccess($this->getDataOrTerminateWithError($childs) ?: array());
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
/**
* Builds the shape both person content components render.
*
* The assembly follows Cis/Cms/News/Xml/Address/Detailed.php, so a person reads the
* same in the news extras and on a content page.
*
* @param array $uids in the order the result should keep
* @param array $funktionen uid => function label, may be empty
* @param boolean $mitFoto
* @return array
*/
private function _cmsPersonen($uids, $funktionen, $mitFoto)
{
if (empty($uids))
return array();
$this->load->model('person/Person_model', 'PersonModel');
$kontakt = $this->PersonModel->getCmsKontakt($uids, $mitFoto);
$kontakt = $this->getDataOrTerminateWithError($kontakt) ?: array();
$nachUid = array();
foreach ($kontakt as $row)
$nachUid[$row->uid] = $row;
$personen = array();
// Loops over $uids, not over the result, so the caller keeps its order.
foreach ($uids as $uid)
{
if (!isset($nachUid[$uid]))
continue;
$row = $nachUid[$uid];
$name = trim(implode(' ', array_filter(array(
$row->titelpre, $row->vorname, $row->nachname, $row->titelpost
))));
$telefon = '';
if ($row->telefonklappe)
$telefon = trim(($row->kontakt ?: '') . ' - ' . $row->telefonklappe);
$email = '';
if (defined('DOMAIN'))
$email = ($row->alias ?: $row->uid) . '@' . DOMAIN;
$personen[] = array(
'uid' => $row->uid,
'name' => $name,
'funktion' => isset($funktionen[$uid]) ? $funktionen[$uid] : '',
'telefon' => $telefon,
'email' => $email,
'ort' => $row->planbezeichnung,
'foto' => $row->foto
);
}
return $personen;
}
/**
* Decides whether the current user may see a DMS category.
*
* Same rule as cms/dms.php: an unlocked category is open to everyone. A locked one
* needs a granted group, or the category permission together with basis/dms.
*
* @param string $kategorie_kurzbz
* @return boolean|null null when the category does not exist
*/
/**
* Reads the tiefe parameter of a content component. 0 keeps the component to the unit
* or category it names, which is what a marker without the attribute means.
* Capped, because the depth drives a recursive walk and one query per level found.
*
* @return int
*/
private function _tiefe()
{
$tiefe = (int) $this->input->get('tiefe', TRUE);
if ($tiefe < 0)
return 0;
return min($tiefe, self::MAX_TIEFE);
}
private function _dmsKategorieErlaubt($kategorie_kurzbz)
{
$this->load->library('PermissionLib');
$this->load->model('content/Dms_model', 'DmsModel');
$zugriff = $this->DmsModel->getKategorieZugriff($kategorie_kurzbz, getAuthUID());
$zugriff = $this->getDataOrTerminateWithError($zugriff);
if (empty($zugriff))
return null;
$kategorie = current($zugriff);
if (!$kategorie->gesperrt || $kategorie->in_gruppe)
return true;
$erlaubt = $this->permissionlib->isBerechtigt('basis/dms');
if ($erlaubt && $kategorie->berechtigung_kurzbz != '')
$erlaubt = $this->permissionlib->isBerechtigt($kategorie->berechtigung_kurzbz);
return $erlaubt;
}
}
@@ -1,760 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CmsAdmin extends FHCAPI_Controller
{
const MAX_TREE_DEPTH = 50;
public function __construct()
{
parent::__construct([
'getTree' => ['basis/cms:r'],
'getSubtree' => ['basis/cms:r'],
'getContent' => ['basis/cms:r'],
'getContentsprache' => ['basis/cms:r'],
'getTemplates' => ['basis/cms:r'],
'getOrganisationseinheiten' => ['basis/cms:r'],
'getSprachen' => ['basis/cms:r'],
'getDmsKategorien' => ['basis/cms:r'],
'getMitarbeiter' => ['basis/cms:r'],
'getDmsKategorieDokumente' => ['basis/cms:r'],
'getUsage' => ['basis/cms:r'],
'getTreeMeta' => ['basis/cms:r'],
'getAncestors' => ['basis/cms:r'],
'getClickCount' => ['basis/cms:r'],
'getClickRanking' => ['basis/cms:r'],
'postContent' => ['basis/cms:rw'],
'postTranslation' => ['basis/cms:rw'],
'postVersion' => ['basis/cms:rw'],
'putProperties' => ['basis/cms:rw'],
'deleteContent' => ['basis/cms:rw'],
'deleteContentsprache' => ['basis/cms:rw']
]);
$this->load->library('CmsAdminLib');
$this->load->library('PermissionLib');
$this->load->model('content/Content_model', 'ContentModel');
$this->load->model('content/Contentsprache_model', 'ContentspracheModel');
$this->load->model('content/Contentchild_model', 'ContentchildModel');
$this->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
$this->load->model('content/Template_model', 'TemplateModel');
$this->loadPhrases(['global', 'cms']);
}
public function getTree()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('menu', 'Menu', 'required|in_list[content,news]');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$menu = $this->input->get('menu', TRUE);
$filter = $this->input->get('filter', TRUE);
$entitledResult = $this->cmsadminlib->getEntitledOe();
$entitledOe = isError($entitledResult) ? [] : getData($entitledResult) ?? [];
$nodes = [];
if ($menu === 'news')
{
$newsResult = $this->ContentModel->getNewsContent();
if (!isError($newsResult) && getData($newsResult))
{
foreach (getData($newsResult) as $row)
{
$titel = '';
$csResult = $this->ContentspracheModel->getOne(
(int) $row->content_id, DEFAULT_LANGUAGE
);
if (!isError($csResult))
$titel = getData($csResult)->titel;
$node = new stdClass();
$node->content_id = (int) $row->content_id;
$node->titel = mb_substr($titel, 0, 15) . ' '
. date('d.m.Y', strtotime($row->insertamum));
$node->template_kurzbz = $row->template_kurzbz;
$node->oe_kurzbz = $row->oe_kurzbz;
$node->aktiv = ($row->aktiv === 't' || $row->aktiv === true);
$node->entitled = false;
$node->groups = [];
$node->children = [];
$nodes[] = $node;
}
}
}
else
{
if (!empty($filter))
{
$clean = str_replace(['!', '.', '?', ','], '', $filter);
$searchItems = preg_split('/\s+/', $clean, -1, PREG_SPLIT_NO_EMPTY);
$rootsResult = $this->ContentModel->searchCms($searchItems);
}
else
{
$rootsResult = $this->ContentModel->getRootContent();
}
if (!isError($rootsResult) && getData($rootsResult))
{
$visited = [];
foreach (getData($rootsResult) as $row)
{
$node = $this->buildTreeNode(
(int) $row->content_id, 0, $visited
);
if ($node !== null)
$nodes[] = $node;
}
}
}
$allIds = [];
$this->collectContentIds($nodes, $allIds);
$groupMap = [];
if (!empty($allIds))
{
$gResult = $this->ContentgruppeModel->getGruppenForContents($allIds);
if (!isError($gResult))
$groupMap = getData($gResult);
}
$this->applyMeta($nodes, $entitledOe, $groupMap);
$result = [];
foreach ($nodes as $node)
{
$pruned = $this->pruneUnentitled($node);
if ($pruned !== null)
$result[] = $pruned;
}
$this->terminateWithSuccess($result);
}
// One subtree in the shape of a getTree root node. Lets the client replace a branch
// instead of rebuilding the whole tree after a change.
// The visited map starts empty here, so a node with several parents can show up twice
// until the next full reload. Rebuilding the whole tree on every change costs more.
public function getSubtree()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = (int) $this->input->get('content_id', TRUE);
$entitledResult = $this->cmsadminlib->getEntitledOe();
$entitledOe = isError($entitledResult) ? [] : getData($entitledResult);
$visited = [];
$node = $this->buildTreeNode($content_id, 0, $visited);
// Deleted meanwhile. The client drops the branch.
if ($node === null)
$this->terminateWithSuccess(null);
$nodes = [$node];
$allIds = [];
$this->collectContentIds($nodes, $allIds);
$groupMap = [];
if (!empty($allIds))
{
$gResult = $this->ContentgruppeModel->getGruppenForContents($allIds);
if (!isError($gResult))
$groupMap = getData($gResult);
}
$this->applyMeta($nodes, $entitledOe, $groupMap);
$this->terminateWithSuccess($this->pruneUnentitled($node));
}
public function getContent()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$entitledResult = $this->cmsadminlib->isEntitledForContent($content_id);
$entitled = $this->getDataOrTerminateWithError($entitledResult);
if (!$entitled)
$this->terminateWithError($this->p->t('cms', 'keineBerechtigungFuerDiesenEintrag'));
$this->ContentModel->resetQuery();
$contentResult = $this->ContentModel->load($content_id);
$contentData = $this->getDataOrTerminateWithError($contentResult);
if (empty($contentData))
$this->terminateWithError($this->p->t('cms', 'contentNichtGefunden'));
$content = $contentData[0];
$langResult = $this->ContentspracheModel->getLanguages($content_id);
$languages = $this->getDataOrTerminateWithError($langResult);
$versions = new stdClass();
foreach ($languages as $sprache)
{
$vResult = $this->ContentspracheModel->getVersions($content_id, $sprache);
if (!isError($vResult) && getData($vResult))
{
$versions->$sprache = array_map(function ($v) {
return (int) $v->version;
}, getData($vResult));
}
}
$result = new stdClass();
$result->content_id = (int) $content->content_id;
$result->template_kurzbz = $content->template_kurzbz;
$result->oe_kurzbz = $content->oe_kurzbz;
$result->aktiv = ($content->aktiv === 't' || $content->aktiv === true);
$result->menu_open = ($content->menu_open === 't' || $content->menu_open === true);
$result->beschreibung = $content->beschreibung ?: '';
$result->entitled = true;
$result->languages = $languages;
$result->versions = $versions;
// Tells the CIS4 preview whether this template renders there at all.
$cis4Result = $this->TemplateModel->hasCis4Stylesheet($content->template_kurzbz);
$result->has_cis4_stylesheet = !isError($cis4Result) && getData($cis4Result);
$this->terminateWithSuccess($result);
}
public function getContentsprache()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
$version = $this->input->get('version', TRUE);
$result = $this->ContentspracheModel->getOne($content_id, $sprache, $version);
if (isError($result))
$this->terminateWithError($this->p->t('cms', 'versionNichtGefunden'));
$data = getData($result);
$data->sichtbar = ($data->sichtbar === 't' || $data->sichtbar === true);
$this->terminateWithSuccess($data);
}
// The tree lists a search hit out of its place, so a generic title carries no context.
// This answers where each one sits. One walk for the whole list, not one per node.
const MAX_ANCESTOR_TIEFE = 12;
/**
* Ancestors of the given contents, keyed by content_id and ordered from the top down.
*/
public function getAncestors()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_ids', 'Content IDs', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$ids = [];
foreach (explode(',', $this->input->get('content_ids', TRUE)) as $teil)
{
$teil = trim($teil);
if (is_numeric($teil))
$ids[] = (int) $teil;
}
$ids = array_values(array_unique($ids));
if (empty($ids))
$this->terminateWithSuccess(new stdClass());
$result = $this->ContentchildModel->getAncestors(
$ids, self::MAX_ANCESTOR_TIEFE, DEFAULT_LANGUAGE
);
$rows = $this->getDataOrTerminateWithError($result);
// Same rule as the tree: an entry of an organisational unit the editor is not
// entitled for stays readable but cannot be opened.
$entitledResult = $this->cmsadminlib->getEntitledOe();
$entitledOe = isError($entitledResult) ? [] : getData($entitledResult) ?? [];
// The query orders by the distance, farthest first, so appending keeps the path
// reading from the top down.
$pfade = [];
foreach ((array) $rows as $row)
{
$start = (int) $row->start_id;
if (!isset($pfade[$start]))
$pfade[$start] = [];
$pfade[$start][] = [
'content_id' => (int) $row->content_id,
'titel' => $row->titel,
'tiefe' => (int) $row->tiefe,
'entitled' => in_array($row->oe_kurzbz, $entitledOe)
];
}
$this->terminateWithSuccess($pfade ?: new stdClass());
}
/**
* Per content metadata for the tree filter, keyed by content_id.
* One query for the whole tree, so the client filters and sorts without a round trip.
*/
public function getTreeMeta()
{
$result = $this->ContentModel->getTreeMeta(getAuthUID(), DEFAULT_LANGUAGE);
$rows = $this->getDataOrTerminateWithError($result);
$meta = [];
foreach ((array) $rows as $row)
{
$meta[(int) $row->content_id] = [
'childcount' => (int) $row->childcount,
'insertamum' => $row->insertamum,
'updateamum' => $row->updateamum,
'mine' => ($row->mine === 't' || $row->mine === true),
'contentlength' => (int) $row->contentlength
];
}
$this->terminateWithSuccess($meta);
}
/**
* Two gates guard every click statistics request: the config flag switches the feature
* off for everybody, the admin right decides who may read the numbers.
*/
private function checkClickstatsAccess()
{
$this->config->load('cms');
if (!$this->config->item('clickstats_enabled'))
$this->terminateWithError($this->p->t('cms', 'klickstatistikDeaktiviert'));
if (!$this->permissionlib->isBerechtigt('admin'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
}
/**
* Period of a click statistics request. 0 counts the whole log.
* Call it after checkClickstatsAccess, which loads the config.
* @return array months and the start date, null for the whole log
*/
private function clickstatsPeriod()
{
$months = $this->input->get('months');
if ($months === null || $months === '')
$months = $this->config->item('clickstats_months');
$months = (int) $months;
$since = ($months > 0)
? date('Y-m-d H:i:s', strtotime('-' . $months . ' months'))
: null;
return [$months, $since];
}
/**
* Views of one content. One counting query, so the tab loads it right away.
* Empty if LOG_CONTENT is off.
*/
public function getClickCount()
{
$this->checkClickstatsAccess();
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('months', 'Monate', 'is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('system/Webservicelog_model', 'WebservicelogModel');
list($months, $since) = $this->clickstatsPeriod();
$result = $this->WebservicelogModel->getContentClickCount(
(int) $this->input->get('content_id', TRUE), $since
);
$this->terminateWithSuccess([
'since' => $since,
'months' => $months,
'own' => $this->getDataOrTerminateWithError($result)
]);
}
/**
* The ranking of the most viewed contents. It groups the whole log and resolves a
* title per row, which takes seconds, so the tab fetches it only on request.
* Empty if LOG_CONTENT is off.
*/
public function getClickRanking()
{
$this->checkClickstatsAccess();
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('months', 'Monate', 'is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('system/Webservicelog_model', 'WebservicelogModel');
list($months, $since) = $this->clickstatsPeriod();
$sprache = $this->input->get('sprache', TRUE);
if (empty($sprache))
$sprache = DEFAULT_LANGUAGE;
$result = $this->WebservicelogModel->getContentClickCounts(
$since, $sprache, (int) $this->config->item('clickstats_limit')
);
$rows = $this->getDataOrTerminateWithError($result);
$ranked = [];
foreach ((array) $rows as $row)
{
$ranked[] = [
'content_id' => (int) $row->content_id,
'titel' => $row->titel,
'hits' => (int) $row->hits
];
}
$this->terminateWithSuccess([
'since' => $since,
'months' => $months,
'ranked' => $ranked
]);
}
public function getTemplates()
{
$this->TemplateModel->addSelect('template_kurzbz, bezeichnung');
$this->TemplateModel->addOrder('bezeichnung');
$result = $this->TemplateModel->loadWhere([]);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getOrganisationseinheiten()
{
$this->load->model(
'organisation/Organisationseinheit_model', 'OrganisationseinheitModel'
);
$this->OrganisationseinheitModel->addSelect(
'oe_kurzbz, bezeichnung, organisationseinheittyp_kurzbz, aktiv'
);
$this->OrganisationseinheitModel->addOrder('bezeichnung');
$result = $this->OrganisationseinheitModel->loadWhere([]);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getSprachen()
{
$this->load->model('system/Sprache_model', 'SpracheModel');
$this->SpracheModel->addSelect('sprache, bezeichnung');
$result = $this->SpracheModel->loadWhere(['content' => true]);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getUsage()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$result = $this->ContentModel->getUsage($content_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function postContent()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('parent_content_id', 'Parent', 'is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$parent = $this->input->post('parent_content_id');
if (empty($parent))
$parent = null;
$result = $this->cmsadminlib->createContent($parent);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function postTranslation()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
$this->form_validation->set_rules('target_sprache', 'Target Sprache', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$result = $this->cmsadminlib->createTranslation(
$this->input->post('content_id'),
$this->input->post('sprache'),
$this->input->post('version'),
$this->input->post('target_sprache')
);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function postVersion()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$result = $this->cmsadminlib->createVersion(
$this->input->post('content_id'),
$this->input->post('sprache')
);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function putProperties()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
$this->form_validation->set_rules('template_kurzbz', 'Template', 'required');
$this->form_validation->set_rules('oe_kurzbz', 'OE', 'required');
$this->form_validation->set_rules('titel', 'Titel', 'required|max_length[256]');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$daten = [
'content_id' => $this->input->post('content_id'),
'sprache' => $this->input->post('sprache'),
'version' => $this->input->post('version'),
'template_kurzbz' => $this->input->post('template_kurzbz'),
'oe_kurzbz' => $this->input->post('oe_kurzbz'),
'titel' => $this->input->post('titel'),
'beschreibung' => $this->input->post('beschreibung') ?: '',
'aktiv' => filter_var($this->input->post('aktiv'), FILTER_VALIDATE_BOOLEAN),
'menu_open' => filter_var($this->input->post('menu_open'), FILTER_VALIDATE_BOOLEAN),
'sichtbar' => filter_var($this->input->post('sichtbar'), FILTER_VALIDATE_BOOLEAN)
];
$result = $this->cmsadminlib->saveProperties($daten);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function deleteContent()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'd'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$result = $this->cmsadminlib->deleteContent($this->input->post('content_id'));
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function deleteContentsprache()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'd'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->validateContentSpracheVersion();
$result = $this->cmsadminlib->deleteVersion(
$this->input->post('content_id'),
$this->input->post('sprache'),
$this->input->post('version')
);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
// --- Private helpers ---
private function validateContentSpracheVersion()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
}
private function buildTreeNode($content_id, $depth, &$visited)
{
if ($depth > self::MAX_TREE_DEPTH || isset($visited[$content_id]))
return null;
$visited[$content_id] = true;
$this->ContentModel->resetQuery();
$contentResult = $this->ContentModel->load($content_id);
if (isError($contentResult) || empty(getData($contentResult)))
return null;
$content = getData($contentResult)[0];
$titel = '';
$csResult = $this->ContentspracheModel->getOne($content_id, DEFAULT_LANGUAGE);
if (!isError($csResult))
$titel = getData($csResult)->titel;
$childrenResult = $this->ContentchildModel->getChilds($content_id, DEFAULT_LANGUAGE);
$children = [];
if (!isError($childrenResult) && getData($childrenResult))
{
foreach (getData($childrenResult) as $child)
{
$childNode = $this->buildTreeNode(
(int) $child->child_content_id, $depth + 1, $visited
);
if ($childNode !== null)
$children[] = $childNode;
}
}
$node = new stdClass();
$node->content_id = (int) $content_id;
$node->titel = $titel;
$node->template_kurzbz = $content->template_kurzbz;
$node->oe_kurzbz = $content->oe_kurzbz;
$node->aktiv = ($content->aktiv === 't' || $content->aktiv === true);
$node->entitled = false;
$node->groups = [];
$node->children = $children;
return $node;
}
private function collectContentIds($nodes, &$ids)
{
foreach ($nodes as $node)
{
$ids[] = $node->content_id;
if (!empty($node->children))
$this->collectContentIds($node->children, $ids);
}
}
private function applyMeta(&$nodes, $entitledOe, $groupMap)
{
foreach ($nodes as $node)
{
$node->entitled = in_array($node->oe_kurzbz, $entitledOe);
$node->groups = isset($groupMap[$node->content_id])
? $groupMap[$node->content_id]
: [];
if (!empty($node->children))
$this->applyMeta($node->children, $entitledOe, $groupMap);
}
}
private function pruneUnentitled($node)
{
$kept = [];
foreach ($node->children as $child)
{
$pruned = $this->pruneUnentitled($child);
if ($pruned !== null)
$kept[] = $pruned;
}
$node->children = $kept;
if ($node->entitled || !empty($node->children))
return $node;
return null;
}
/**
* Lists the DMS categories for the contentcomponent insert dialog.
*/
public function getDmsKategorien()
{
$this->load->model('content/Dms_model', 'DmsModel');
$result = $this->DmsModel->getKategorien();
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
/**
* Lists active staff for the contentcomponent insert dialog.
* Names only. See Mitarbeiter_model::getAktivesPersonalNamen().
*/
public function getMitarbeiter()
{
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$result = $this->MitarbeiterModel->getAktivesPersonalNamen();
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
/**
* Lists the documents of one DMS category for the contentcomponent insert dialog.
*/
public function getDmsKategorieDokumente()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('kategorie_kurzbz', 'DMS-Kategorie', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('content/Dms_model', 'DmsModel');
$result = $this->DmsModel->getKategorieDokumenteAlle($this->input->get('kategorie_kurzbz', TRUE));
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result) ?: array());
}
}
@@ -1,195 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CmsAdminInhalt extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'getFormSchema' => ['basis/cms:r'],
'getFormData' => ['basis/cms:r'],
'getLock' => ['basis/cms:r'],
'getVersions' => ['basis/cms:r'],
'putFormData' => ['basis/cms:rw'],
'postLock' => ['basis/cms:rw'],
'deleteLock' => ['basis/cms:rw'],
'deleteLockForced' => ['basis/cms:rw']
]);
$this->load->library('CmsAdminLib');
$this->load->library('XsdSchemaLib');
$this->load->library('PermissionLib');
$this->load->model('content/Contentsprache_model', 'ContentspracheModel');
$this->load->model('content/Template_model', 'TemplateModel');
$this->loadPhrases(['global', 'cms']);
}
public function getFormSchema()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('template_kurzbz', 'Template', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$template_kurzbz = $this->input->get('template_kurzbz', TRUE);
$this->TemplateModel->resetQuery();
$templateResult = $this->TemplateModel->load($template_kurzbz);
$templateData = $this->getDataOrTerminateWithError($templateResult);
if (empty($templateData))
$this->terminateWithError($this->p->t('cms', 'vorlageNichtGefunden'));
$schemaResult = $this->xsdschemalib->parseSchema(
$templateData[0]->xsd, $template_kurzbz
);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($schemaResult));
}
public function getFormData()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
$version = $this->input->get('version', TRUE);
$versionResult = $this->ContentspracheModel->getOne($content_id, $sprache, $version);
$row = $this->getDataOrTerminateWithError($versionResult);
$this->load->model('content/Content_model', 'ContentModel');
$this->ContentModel->resetQuery();
$contentResult = $this->ContentModel->load($content_id);
$contentData = $this->getDataOrTerminateWithError($contentResult);
if (empty($contentData))
$this->terminateWithError($this->p->t('cms', 'contentNichtGefunden'));
$this->TemplateModel->resetQuery();
$templateResult = $this->TemplateModel->load($contentData[0]->template_kurzbz);
$templateData = $this->getDataOrTerminateWithError($templateResult);
if (empty($templateData))
$this->terminateWithError($this->p->t('cms', 'vorlageNichtGefunden'));
$schemaResult = $this->xsdschemalib->parseSchema(
$templateData[0]->xsd, $contentData[0]->template_kurzbz
);
$schema = $this->getDataOrTerminateWithError($schemaResult);
$valuesResult = $this->xsdschemalib->extractValues($row->content, $schema);
$values = $this->getDataOrTerminateWithError($valuesResult);
$lockResult = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
$lock = $this->getDataOrTerminateWithError($lockResult);
$this->terminateWithSuccess([
'schema' => $schema,
'values' => $values,
'sperre' => $lock
]);
}
public function getLock()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
$version = $this->input->get('version', TRUE);
$result = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getVersions()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
$result = $this->ContentspracheModel->getVersions($content_id, $sprache);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function putFormData()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->post('content_id');
$sprache = $this->input->post('sprache');
$version = $this->input->post('version');
$values = $this->input->post('values');
if (is_string($values))
$values = json_decode($values, true);
if (!is_array($values))
$values = [];
$lockResult = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
$lock = $this->getDataOrTerminateWithError($lockResult);
if (!$lock['own'])
$this->terminateWithError($this->p->t('cms', 'speichernNurMitSperre'));
$result = $this->cmsadminlib->saveContentXml($content_id, $sprache, $version, $values);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function postLock()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigungFuerDieseAktion'));
$contentsprache_id = $this->input->post('contentsprache_id');
$result = $this->cmsadminlib->lock($contentsprache_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function deleteLock()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigungFuerDieseAktion'));
$contentsprache_id = $this->input->post('contentsprache_id');
$result = $this->cmsadminlib->releaseOwnLock($contentsprache_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function deleteLockForced()
{
if (!$this->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigungFuerDieseAktion'));
$contentsprache_id = $this->input->post('contentsprache_id');
$result = $this->cmsadminlib->forceRelease($contentsprache_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
}
@@ -1,265 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CmsAdminStruktur extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'getGruppen' => ['basis/cms:r'],
'getAllGruppen' => ['basis/cms:r'],
'getChilds' => ['basis/cms:r'],
'getParents' => ['basis/cms:r'],
'getPossibleChilds' => ['basis/cms:r'],
'postGruppe' => ['basis/cms:rw'],
'deleteGruppe' => ['basis/cms:rw'],
'postChild' => ['basis/cms:rw'],
'deleteChild' => ['basis/cms:rw'],
'putChildSort' => ['basis/cms:rw'],
'putChildOrder' => ['basis/cms:rw']
]);
$this->load->library('PermissionLib');
$this->load->model('content/Content_model', 'ContentModel');
$this->load->model('content/Contentchild_model', 'ContentchildModel');
$this->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
$this->loadPhrases(['global', 'cms']);
}
public function getGruppen()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$result = $this->ContentgruppeModel->getGruppen($content_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getAllGruppen()
{
$result = $this->ContentgruppeModel->getAllGruppen();
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getChilds()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
if (empty($sprache))
$sprache = DEFAULT_LANGUAGE;
$result = $this->ContentchildModel->getChilds($content_id, $sprache);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getParents()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
if (empty($sprache))
$sprache = DEFAULT_LANGUAGE;
$result = $this->ContentchildModel->getParents($content_id, $sprache);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getPossibleChilds()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
if (empty($sprache))
$sprache = DEFAULT_LANGUAGE;
$result = $this->ContentModel->getPossibleChilds($content_id, $sprache);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function postGruppe()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('gruppe_kurzbz', 'Gruppe', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$result = $this->ContentgruppeModel->insert([
'content_id' => $this->input->post('content_id'),
'gruppe_kurzbz' => $this->input->post('gruppe_kurzbz'),
'insertamum' => date('Y-m-d H:i:s'),
'insertvon' => getAuthUID()
]);
if (isError($result))
{
$this->terminateWithError($this->p->t('cms', 'gruppeBereitsZugeordnet'));
}
$this->terminateWithSuccess(true);
}
public function deleteGruppe()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('gruppe_kurzbz', 'Gruppe', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$result = $this->ContentgruppeModel->delete([
$this->input->post('gruppe_kurzbz'),
$this->input->post('content_id')
]);
$this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(true);
}
public function postChild()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
$this->form_validation->set_rules('child_content_id', 'Child Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$content_id = $this->input->post('content_id');
$sortResult = $this->ContentchildModel->getMaxSort($content_id);
$sort = $this->getDataOrTerminateWithError($sortResult) + 1;
$result = $this->ContentchildModel->insert([
'content_id' => $content_id,
'child_content_id' => $this->input->post('child_content_id'),
'sort' => $sort,
'insertamum' => date('Y-m-d H:i:s'),
'insertvon' => getAuthUID()
]);
$this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(true);
}
public function deleteChild()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('contentchild_id', 'Contentchild ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$result = $this->ContentchildModel->delete($this->input->post('contentchild_id'));
$this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(true);
}
public function putChildSort()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('contentchild_id', 'Contentchild ID', 'required|is_natural');
$this->form_validation->set_rules('direction', 'Direction', 'required|in_list[up,down]');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$direction = $this->input->post('direction');
$result = $this->ContentchildModel->swapSort(
$this->input->post('contentchild_id'),
$direction
);
if (isError($result))
{
// The child already sits at the end it was moved towards. That is a normal
// answer, not a failure, and the direction decides the wording.
if (getError($result) === Contentchild_model::NO_NEIGHBOUR)
{
$this->terminateWithError($this->p->t('cms',
$direction === 'up' ? 'bereitsGanzOben' : 'bereitsGanzUnten'));
}
$this->terminateWithError($this->p->t('cms', 'sortierungFehlgeschlagen'));
}
$this->terminateWithSuccess(true);
}
// Writes the whole order at once. The drag and drop moves a child over any distance,
// and putChildSort would need one request per position.
public function putChildOrder()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
$this->load->library('form_validation');
$this->form_validation->set_data($_POST);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$ids = $this->input->post('contentchild_ids');
if (is_string($ids))
$ids = json_decode($ids, true);
if (!is_array($ids) || empty($ids))
$this->terminateWithError($this->p->t('cms', 'sortierungFehlgeschlagen'));
$result = $this->ContentchildModel->setSortOrder(
$this->input->post('content_id'),
$ids
);
if (isError($result))
$this->terminateWithError($this->p->t('cms', 'sortierungFehlgeschlagen'));
$this->terminateWithSuccess(true);
}
}
@@ -28,7 +28,10 @@ class Lehrveranstaltung extends FHCAPI_Controller
{
parent::__construct([
'getByEmp' => ['admin:r', 'assistenz:r'],
'getByLe' => ['admin:r', 'assistenz:r'],
'getByLv' => ['admin:r', 'assistenz:r'],
'getByStg' => ['admin:r', 'assistenz:r'],
'getByStgOrgform' => ['admin:r', 'assistenz:r'],
'loadByLV' => ['admin:r', 'assistenz:r'],
]);
@@ -77,41 +80,117 @@ class Lehrveranstaltung extends FHCAPI_Controller
$this->terminateWithSuccess($tree);
}
public function getByLe($studiensemester_kurzbz = null, $lehreinheit_id = null)
{
if (is_null($lehreinheit_id))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$lehreinheiten = $this->_ci->LehreinheitModel->getByLeStudiensemester($lehreinheit_id, $studiensemester_kurzbz);
$lehreinheiten_data = $this->getDataOrTerminateWithError($lehreinheiten);
$this->terminateWithSuccess($lehreinheiten_data);
}
public function getByLv($studiensemester_kurzbz = null, $lehrveranstaltung_id = null)
{
if (is_null($lehrveranstaltung_id))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$tree = [];
$this->_ci->LehrveranstaltungModel->addSelect('studiengang_kz');
$lv = $this->_ci->LehrveranstaltungModel->load($lehrveranstaltung_id);
$lv = $this->getDataOrTerminateWithError($lv);
if (empty($lv))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$lehrveranstaltungen_data = array();
foreach ($this->_loadLvRows(current($lv)->studiengang_kz, $studiensemester_kurzbz) as $row)
{
if ((string)$row->lehrveranstaltung_id === (string)$lehrveranstaltung_id)
{
$lehrveranstaltungen_data[] = $row;
break;
}
}
if (empty($lehrveranstaltungen_data))
{
$this->_ci->LehrveranstaltungModel->addSelect('
tbl_lehrveranstaltung.*,
tbl_lehrveranstaltung.kurzbz as lv_kurzbz,
lehrveranstaltung_id,
lehrtyp_kurzbz,
studiengang_kz as lv_studiengang_kz,
UPPER(CONCAT(tbl_studiengang.typ, tbl_studiengang.kurzbz)) as studiengang,
semester as semester,
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
ects as lv_ects,
lehreverzeichnis as lv_lehreverzeichnis,
planfaktor as lv_planfaktor,
planlektoren as lv_planlektoren,
planpersonalkosten as lv_planpersonalkosten,
plankostenprolektor as lv_plankostenprolektor,
tbl_lehrveranstaltung.orgform_kurzbz as orgform_kurzbz,
tbl_lehrveranstaltung.sprache as sprache,
lehrform_kurzbz as lv_lehrform_kurzbz,
tbl_lehrveranstaltung.bezeichnung_english as lv_bezeichnung_english,
semesterstunden as lv_semesterstunden,
');
$this->_ci->LehrveranstaltungModel->addJoin('public.tbl_studiengang', 'studiengang_kz');
$lehrveranstaltungen = $this->_ci->LehrveranstaltungModel->load($lehrveranstaltung_id);
$lehrveranstaltungen_data = $this->getDataOrTerminateWithError($lehrveranstaltungen);
}
foreach ($lehrveranstaltungen_data as $lehrveranstaltung)
{
$lehreinheiten = $this->_ci->LehreinheitModel->getByLvidStudiensemester($lehrveranstaltung->lehrveranstaltung_id, $studiensemester_kurzbz);
$lehreinheiten_data = $this->getDataOrTerminateWithError($lehreinheiten);
if (!isset($lehrveranstaltung->_children))
{
$lehrveranstaltung->_children = $lehreinheiten_data;
}
$tree[] = $lehrveranstaltung;
}
$counter = 0;
$this->assignUniqueIndex($tree, $counter);
$this->terminateWithSuccess($tree);
}
public function getByStg($studiensemester_kurzbz = null, $stg_kz = null, $semester = null)
{
$this->_getLvTree($studiensemester_kurzbz, $stg_kz, $semester, null);
}
public function getByStgOrgform($studiensemester_kurzbz = null, $stg_kz = null, $orgform = null, $semester = null)
{
$orgform_kurzbz = $this->getOrgformKurzbz($orgform);
$this->_getLvTree($studiensemester_kurzbz, $stg_kz, $semester, $orgform_kurzbz);
}
private function _getLvTree($studiensemester_kurzbz, $stg_kz, $semester, $orgform_kurzbz)
{
$studiengang_kz = $this->getStudiengangKz($stg_kz);
if (is_null($studiengang_kz) || !preg_match("/^(-?[1-9][0-9]*|0)$/", (string)$studiengang_kz))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$verband = null;
if (!is_null($semester) && !is_numeric($semester))
{
$verband = $this->getOrgformKurzbz($semester);
$semester = null;
}
$this->_ci->load->model('organisation/Studienplan_model', 'StudienplanModel');
if (!is_null($semester) && !ctype_digit((string)$semester))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$studienplan_data = $this->_ci->StudienplanModel->getStudienplaeneBySemester($studiengang_kz, $studiensemester_kurzbz, $semester, $verband);
$studienplan_ids = array();
$only_ids = array();
$placeholders = array();
if (hasData($studienplan_data))
{
foreach (getData($studienplan_data) as $studienplan) {
$placeholders[] = "(?, ?)";
$studienplan_ids[] = $studienplan->studienplan_id;
$studienplan_ids[] = $studienplan->semester;
$only_ids[] = $studienplan->studienplan_id;
}
}
$lehrveranstaltungen_data = $this->_ci->LehrveranstaltungModel->getLvsByStudiengang($studienplan_ids, $placeholders, $only_ids, $studiengang_kz, $studiensemester_kurzbz, $semester, $verband);
$lehrveranstaltungen_data = hasData($lehrveranstaltungen_data) ? getData($lehrveranstaltungen_data) : array();
$lehrveranstaltungen_data = $this->_loadLvRows($studiengang_kz, $studiensemester_kurzbz, $semester, $orgform_kurzbz);
$tree = [];
foreach ($lehrveranstaltungen_data as $row)
@@ -244,6 +323,31 @@ class Lehrveranstaltung extends FHCAPI_Controller
$this->LehreinheitModel->getLvsByFachbereich($fachbereich, $studiensemester_kurzbz, $mitarbeiter_uid);
}*/
private function _loadLvRows($studiengang_kz, $studiensemester_kurzbz, $semester = null, $orgform_kurzbz = null)
{
$this->_ci->load->model('organisation/Studienplan_model', 'StudienplanModel');
$studienplan_data = $this->_ci->StudienplanModel->getStudienplaeneBySemester($studiengang_kz, $studiensemester_kurzbz, $semester, $orgform_kurzbz);
$studienplan_ids = array();
$only_ids = array();
$placeholders = array();
if (hasData($studienplan_data))
{
foreach (getData($studienplan_data) as $studienplan) {
$placeholders[] = "(?, ?)";
$studienplan_ids[] = $studienplan->studienplan_id;
$studienplan_ids[] = $studienplan->semester;
$only_ids[] = $studienplan->studienplan_id;
}
}
$lehrveranstaltungen_data = $this->_ci->LehrveranstaltungModel->getLvsByStudiengang($studienplan_ids, $placeholders, $only_ids, $studiengang_kz, $studiensemester_kurzbz, $semester, $orgform_kurzbz);
return hasData($lehrveranstaltungen_data) ? getData($lehrveranstaltungen_data) : array();
}
private function _setAuthUID()
{
$this->_uid = getAuthUID();
@@ -309,8 +413,8 @@ class Lehrveranstaltung extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result);
if (!$data)
return $orgform_kurzbz;
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
return current($data)->orgform_kurzbz;
}
}
}
@@ -62,6 +62,7 @@ class OtherLvPlan extends FHCAPI_Controller
"foto" => $profileData->data->foto,
"vorname" => $profileData->data->vorname,
"nachname" => $profileData->data->nachname,
"wahlname" => $profileData->data->wahlname,
"titel" => $profileData->data->titel,
"postnomen" => $profileData->data->postnomen,
],
@@ -101,6 +101,9 @@ class Widget extends FHCAPI_Controller
$tmpsetup = json_decode($widget->setup);
$tmpsetup->file = absoluteJsImportUrl($tmpsetup->file);
$widget->setup = $tmpsetup;
$widget->permitted = empty($widget->berechtigung_kurzbz)
|| $this->permissionlib->isBerechtigt($widget->berechtigung_kurzbz);
return $widget;
}, $widgets);
@@ -28,7 +28,8 @@ class Studiensemester extends FHCAPI_Controller
'getStudienjahrByStudiensemester' => self::PERM_LOGGED,
'getAllStudiensemesterAndAktOrNext' => self::PERM_LOGGED,
'current' => self::PERM_LOGGED,
'set' => self::PERM_LOGGED
'set' => self::PERM_LOGGED,
'getContainingOrNearestByDateRange' => self::PERM_LOGGED,
)
);
// Load model StudiensemesterModel
@@ -203,4 +204,15 @@ class Studiensemester extends FHCAPI_Controller
$this->terminateWithSuccess(true);
}
public function getContainingOrNearestByDateRange()
{
$from = $this->input->get("from");
$to = $this->input->get("to");
$result = $this->StudiensemesterModel->getContainingOrNearestByDateRange($from, $to);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
}
}
@@ -574,6 +574,19 @@ class Kontakt extends FHCAPI_Controller
$person_id = $this->input->post('person_id');
$standort_id = $this->input->post('standort_id');
//check person_id: allow editing only of persons who are no mitarbeiter
if($person_id)
{
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->BenutzerModel->addJoin('public.tbl_mitarbeiter ma', 'ON (ma.mitarbeiter_uid = public.tbl_benutzer.uid)');
$result = $this->BenutzerModel->loadWhere([
'person_id' => $person_id
]);
if(hasData($result))
return $this->terminateWithError($this->p->t('ui', 'editingMitarbeiterPVonly'), self::ERROR_TYPE_GENERAL);
}
$result = $this->KontaktModel->update(
[
'kontakt_id' => $kontakt_id
-563
View File
@@ -1,563 +0,0 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class CmsAdminLib
{
private $ci;
public function __construct()
{
$this->ci =& get_instance();
$this->ci->load->model('content/Content_model', 'ContentModel');
$this->ci->load->model('content/Contentsprache_model', 'ContentspracheModel');
$this->ci->load->model('content/Contentchild_model', 'ContentchildModel');
$this->ci->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
$this->ci->load->model('content/Contentlog_model', 'ContentlogModel');
$this->ci->load->model('content/Template_model', 'TemplateModel');
$this->ci->load->library('XsdSchemaLib');
$this->ci->load->library('PermissionLib');
$this->ci->config->load('cms');
}
/**
* @return object success(array) entitled oe_kurzbz values
*/
public function getEntitledOe()
{
$oe = $this->ci->permissionlib->getOE_isEntitledFor('basis/cms');
if ($oe === false || !is_array($oe))
return success([]);
return success($oe);
}
/**
* @param int $content_id
* @return object success(bool)
*/
public function isEntitledForContent($content_id)
{
$oeResult = $this->ci->ContentModel->getOeKurzbz($content_id);
if (isError($oeResult))
return $oeResult;
$entitledResult = $this->getEntitledOe();
if (isError($entitledResult))
return $entitledResult;
return success(in_array(getData($oeResult), getData($entitledResult)));
}
/**
* @return object success(string) or error
*/
public function getDefaultOe()
{
$default = $this->ci->config->item('default_oe_kurzbz');
$entitledResult = $this->getEntitledOe();
if (isError($entitledResult))
return $entitledResult;
$entitled = getData($entitledResult);
if (empty($entitled))
return error($this->ci->p->t('cms', 'keineBerechtigteOe'));
if (in_array($default, $entitled))
return success($default);
return success($entitled[0]);
}
/**
* @param int|null $parent_content_id
* @return object success(array) with content_id, sprache, version
*/
public function createContent($parent_content_id = null)
{
$templatesResult = $this->ci->TemplateModel->loadWhere([]);
if (isError($templatesResult))
return $templatesResult;
$templates = getData($templatesResult);
if (empty($templates))
return error($this->ci->p->t('cms', 'keineVorlageVorhanden'));
$template = $templates[0];
$oeResult = $this->getDefaultOe();
if (isError($oeResult))
return $oeResult;
$oe = getData($oeResult);
$titel = $this->ci->p->t('cms', 'neuerEintrag');
$now = date('Y-m-d H:i:s');
$uid = getAuthUID();
$this->ci->db->trans_start();
$contentResult = $this->ci->ContentModel->insert([
'template_kurzbz' => $template->template_kurzbz,
'oe_kurzbz' => $oe,
'aktiv' => true,
'menu_open' => false,
'beschreibung' => '',
'insertamum' => $now,
'insertvon' => $uid
]);
if (isError($contentResult))
return $contentResult;
$content_id = getData($contentResult);
$spracheResult = $this->ci->ContentspracheModel->insert([
'content_id' => $content_id,
'sprache' => DEFAULT_LANGUAGE,
'version' => 1,
'sichtbar' => true,
'titel' => $titel,
'content' => '<?xml version="1.0" encoding="UTF-8" ?><content></content>',
'insertamum' => $now,
'insertvon' => $uid
]);
if (isError($spracheResult))
return $spracheResult;
if ($parent_content_id !== null)
{
$maxSortResult = $this->ci->ContentchildModel->getMaxSort($parent_content_id);
if (isError($maxSortResult))
return $maxSortResult;
$childResult = $this->ci->ContentchildModel->insert([
'content_id' => $parent_content_id,
'child_content_id' => $content_id,
'sort' => getData($maxSortResult) + 1,
'insertamum' => $now,
'insertvon' => $uid
]);
if (isError($childResult))
return $childResult;
}
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success([
'content_id' => $content_id,
'sprache' => DEFAULT_LANGUAGE,
'version' => 1
]);
}
/**
* @param int $content_id
* @param string $sprache source language
* @param int $version source version
* @param string $neueSprache target language
* @return object success(array) with contentsprache_id
*/
public function createTranslation($content_id, $sprache, $version, $neueSprache)
{
$existsResult = $this->ci->ContentspracheModel->exists($content_id, $neueSprache);
if (isError($existsResult))
return $existsResult;
if (getData($existsResult))
return error($this->ci->p->t('cms', 'uebersetzungExistiert'));
$sourceResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
if (isError($sourceResult))
return $sourceResult;
$source = getData($sourceResult);
$now = date('Y-m-d H:i:s');
$uid = getAuthUID();
$insertResult = $this->ci->ContentspracheModel->insert([
'content_id' => $content_id,
'sprache' => $neueSprache,
'version' => $source->version,
'sichtbar' => true,
'titel' => $source->titel,
'content' => $source->content,
// The review feature is gone from this admin, but the columns stay
'reviewvon' => null,
'reviewamum' => null,
'gesperrt_uid' => null,
'insertamum' => $now,
'insertvon' => $uid,
'updateamum' => $now,
'updatevon' => $uid
]);
if (isError($insertResult))
return $insertResult;
return success(['contentsprache_id' => getData($insertResult)]);
}
/**
* @param int $content_id
* @param string $sprache
* @return object success(array) with version
*/
public function createVersion($content_id, $sprache)
{
$sourceResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, null);
if (isError($sourceResult))
return $sourceResult;
$source = getData($sourceResult);
$maxResult = $this->ci->ContentspracheModel->getMaxVersion($content_id, $sprache);
if (isError($maxResult))
return $maxResult;
$newVersion = getData($maxResult) + 1;
$now = date('Y-m-d H:i:s');
$uid = getAuthUID();
$insertResult = $this->ci->ContentspracheModel->insert([
'content_id' => $content_id,
'sprache' => $sprache,
'version' => $newVersion,
'sichtbar' => false,
'titel' => $source->titel,
'content' => $source->content,
// The review feature is gone from this admin, but the columns stay
'reviewvon' => null,
'reviewamum' => null,
'gesperrt_uid' => null,
'insertamum' => $now,
'insertvon' => $uid,
'updateamum' => $now,
'updatevon' => $uid
]);
if (isError($insertResult))
return $insertResult;
return success(['version' => $newVersion]);
}
/**
* Writes the properties of one content version.
* @param array $daten content_id, sprache, version, template_kurzbz, oe_kurzbz,
* aktiv, menu_open, beschreibung, titel, sichtbar
* @return object success(true) or error
*/
public function saveProperties($daten)
{
$versionResult = $this->ci->ContentspracheModel->getOne(
$daten['content_id'], $daten['sprache'], $daten['version']
);
if (isError($versionResult))
return $versionResult;
$row = getData($versionResult);
$now = date('Y-m-d H:i:s');
$uid = getAuthUID();
$this->ci->db->trans_start();
$this->ci->ContentModel->update($daten['content_id'], [
'template_kurzbz' => $daten['template_kurzbz'],
'oe_kurzbz' => $daten['oe_kurzbz'],
'aktiv' => $daten['aktiv'],
'menu_open' => $daten['menu_open'],
'beschreibung' => $daten['beschreibung'],
'updateamum' => $now,
'updatevon' => $uid
]);
$this->ci->ContentspracheModel->update($row->contentsprache_id, [
'titel' => $daten['titel'],
'sichtbar' => $daten['sichtbar'],
'updateamum' => $now,
'updatevon' => $uid
]);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
// legacy cms checks neither the permission
// type nor the lock. The form appears only when the user holds the lock, but the POST
// itself is unprotected.
/**
* @param int $content_id
* @param string $sprache
* @param int $version
* @param array $values field name => value
* @return object success(true) or error
*/
public function saveContentXml($content_id, $sprache, $version, $values)
{
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
if (isError($versionResult))
return $versionResult;
$row = getData($versionResult);
$contentResult = $this->ci->ContentModel->load($content_id);
if (isError($contentResult))
return $contentResult;
$content = getData($contentResult);
if (empty($content))
return error($this->ci->p->t('cms', 'contentNichtGefunden'));
$templateResult = $this->ci->TemplateModel->load($content[0]->template_kurzbz);
if (isError($templateResult))
return $templateResult;
$template = getData($templateResult);
if (empty($template))
return error($this->ci->p->t('cms', 'keineVorlageVorhanden'));
$schemaResult = $this->ci->xsdschemalib->parseSchema(
$template[0]->xsd, $content[0]->template_kurzbz
);
if (isError($schemaResult))
return $schemaResult;
$xmlResult = $this->ci->xsdschemalib->buildXml(getData($schemaResult), $values);
if (isError($xmlResult))
return $xmlResult;
$updateResult = $this->ci->ContentspracheModel->update(
$row->contentsprache_id,
['content' => getData($xmlResult)]
);
if (isError($updateResult))
return $updateResult;
return success(true);
}
/**
* Locks a version for the current user, or takes over an expired lock.
* @param int $contentsprache_id
* @return object success(true) or error
*/
public function lock($contentsprache_id)
{
$uid = getAuthUID();
$rowResult = $this->ci->ContentspracheModel->load($contentsprache_id);
if (isError($rowResult))
return $rowResult;
$rowData = getData($rowResult);
if (empty($rowData))
return error($this->ci->p->t('cms', 'versionNichtGefunden'));
$holder = ($rowData[0]->gesperrt_uid === '') ? null : $rowData[0]->gesperrt_uid;
if ($holder !== null && $holder !== $uid)
{
$openResult = $this->ci->ContentlogModel->getOpenLock($contentsprache_id, $holder);
$openEntry = (!isError($openResult)) ? getData($openResult) : null;
if (!$this->isLockExpired($holder, $openEntry))
return error($this->ci->p->t('cms', 'bereitsGesperrt'));
}
$this->ci->db->trans_start();
$this->ci->ContentlogModel->closeOpenEntries($contentsprache_id);
$logResult = $this->ci->ContentlogModel->insert([
'uid' => $uid,
'contentsprache_id' => $contentsprache_id,
'start' => date('Y-m-d H:i:s')
]);
if (isError($logResult))
return $logResult;
$updateResult = $this->ci->ContentspracheModel->update($contentsprache_id, [
'gesperrt_uid' => $uid
]);
if (isError($updateResult))
return $updateResult;
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
// legacy released ALL locks of the user. Releasing now ends one lock.
// The uid predicate stops a user releasing a lock of somebody else.
/**
* Releases the lock of the current user on one version.
* @param int $contentsprache_id
* @return object success(true)
*/
public function releaseOwnLock($contentsprache_id)
{
$uid = getAuthUID();
$this->ci->db->trans_start();
$this->ci->ContentlogModel->closeOpenEntries($contentsprache_id, $uid);
$this->ci->ContentspracheModel->releaseLock($contentsprache_id, $uid);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
/**
* @param int $contentsprache_id
* @return object success(true)
*/
public function forceRelease($contentsprache_id)
{
$this->ci->db->trans_start();
$this->ci->ContentlogModel->closeOpenEntries($contentsprache_id);
$this->ci->ContentspracheModel->releaseLock($contentsprache_id);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
/**
* @param int $content_id
* @param string $sprache
* @param int $version
* @return object success(array) gesperrt_uid, start, expires, own, expired, may_force
*/
public function getLockState($content_id, $sprache, $version)
{
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
if (isError($versionResult))
return $versionResult;
$row = getData($versionResult);
// legacy admin.php inserts an empty string, releasing writes NULL.
// this code reports NULL.
$gesperrt_uid = ($row->gesperrt_uid === '') ? null : $row->gesperrt_uid;
$logEntry = null;
if ($gesperrt_uid !== null)
{
$lockResult = $this->ci->ContentlogModel->getOpenLock(
$row->contentsprache_id, $gesperrt_uid
);
$logEntry = (!isError($lockResult)) ? getData($lockResult) : null;
}
return success([
'contentsprache_id' => (int) $row->contentsprache_id,
'gesperrt_uid' => $gesperrt_uid,
'start' => $logEntry ? $logEntry->start : null,
'own' => ($gesperrt_uid !== null && $gesperrt_uid === getAuthUID()),
'expired' => $this->isLockExpired($gesperrt_uid, $logEntry),
'expires' => $this->lockExpiryDate($logEntry),
'may_force' => $this->ci->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su')
]);
}
/**
* End of the lock window, null if the entry carries no start.
* @param stdClass|null $logEntry
* @return string|null
*/
private function lockExpiryDate($logEntry)
{
if ($logEntry === null || empty($logEntry->start))
return null;
$ttlHours = (int) $this->ci->config->item('lock_ttl_hours');
// lock() writes start with PHP time, so stay on PHP time.
return date('Y-m-d H:i:s', strtotime($logEntry->start) + $ttlHours * 3600);
}
// Locks now age out after lock_ttl_hours.
/**
* A lock without a log entry has no age and counts as expired.
* @param string|null $gesperrt_uid
* @param stdClass|null $logEntry
* @return bool
*/
private function isLockExpired($gesperrt_uid, $logEntry)
{
if ($gesperrt_uid === null)
return false;
$expires = $this->lockExpiryDate($logEntry);
return $expires === null || strtotime($expires) < time();
}
/**
* @param int $content_id
* @return object success(true) or error
*/
public function deleteContent($content_id)
{
$this->ci->db->trans_start();
$this->ci->ContentchildModel->deleteByContent($content_id);
$this->ci->ContentlogModel->deleteByContent($content_id);
$this->ci->ContentspracheModel->deleteByContent($content_id);
$this->ci->ContentgruppeModel->deleteByContent($content_id);
$this->ci->ContentModel->delete($content_id);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'loeschenNichtMoeglichReferenzen'));
return success(true);
}
/**
* @param int $content_id
* @param string $sprache
* @param int $version
* @return object success(true) or error
*/
public function deleteVersion($content_id, $sprache, $version)
{
if ($sprache === DEFAULT_LANGUAGE)
{
$countResult = $this->ci->ContentspracheModel->getNumberOfVersions($content_id, $sprache);
if (isError($countResult))
return $countResult;
if (getData($countResult) === 1)
return error($this->ci->p->t('cms', 'letzteVersionNichtLoeschbar'));
}
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
if (isError($versionResult))
return $versionResult;
$row = getData($versionResult);
$this->ci->db->trans_start();
$this->ci->ContentlogModel->deleteByContentsprache($row->contentsprache_id);
$this->ci->ContentspracheModel->deleteVersion($content_id, $sprache, $version);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'loeschenFehlgeschlagen'));
return success(true);
}
}
+2 -4
View File
@@ -42,12 +42,10 @@ class CmsLib
* @param int $version
* @param string $sprache
* @param boolean $sichtbar
* @param boolean $log false suppresses the click log entry. The CMS admin
* preview renders a content without counting a view.
*
* @return stdClass
*/
public function getContent($content_id, $version = null, $sprache = null, $sichtbar = true, $log = true)
public function getContent($content_id, $version = null, $sprache = null, $sichtbar = true)
{
if (!is_numeric($content_id))
return error('ContentID ist ungueltig');
@@ -74,7 +72,7 @@ class CmsLib
return $content;
// Legt einen Logeintrag für die Klickstatistik an
if ($log && defined('LOG_CONTENT') && LOG_CONTENT) {
if (defined('LOG_CONTENT') && LOG_CONTENT) {
// Nur eingeloggte User werden geloggt, das sonst auch alle Infoscreenaufrufe und dgl. mitgeloggt werden
if (isLogged()) {
$request_data = 'content_id=' . $content_id;
+1 -1
View File
@@ -329,7 +329,7 @@ class ProfilLib{
private function getPersonInfo($uid, $geburtsInfo = null)
{
$this->ci->load->model("person/Benutzer_model","BenutzerModel");
$selectClause = ["foto", "foto_sperre", "anrede", "titelpost as postnomen", "titelpre as titel", "vorname", "nachname"];
$selectClause = ["foto", "foto_sperre", "anrede", "titelpost as postnomen", "titelpre as titel", "vorname", "nachname", "wahlname"];
/** @param integer $geburtsInfo */
if ($geburtsInfo) {
array_push($selectClause, "gebort");
+101 -4
View File
@@ -33,7 +33,7 @@ class SearchBarLib
const ERROR_NOT_AUTH = 'ERR005';
// List of allowed types of search
const ALLOWED_TYPES = ['mitarbeiter', 'mitarbeiter_ohne_zuordnung', 'organisationunit', 'raum', 'person', 'student','studentStv', 'prestudent', 'document', 'cms'];
const ALLOWED_TYPES = ['mitarbeiter', 'mitarbeiter_ohne_zuordnung', 'organisationunit', 'raum', 'person', 'student','studentStv', 'prestudent', 'document', 'cms', 'lehreinheit', 'lehrveranstaltung'];
const PHOTO_IMG_URL = '/cis/public/bild.php?src=person&person_id=';
@@ -180,10 +180,14 @@ class SearchBarLib
protected function buildSearchClause(DB_Model $dbModel, array $columns, $searchstr)
{
$searchstr = preg_replace('/[[:punct:]]/', ' ', $searchstr);
$words = preg_split('/\s+/', trim($searchstr), -1, PREG_SPLIT_NO_EMPTY);
if (empty($words)) return array();
$document = implode(' || \' \' || ', $columns);
$query = '\'' . implode(':* & ', explode(' ', trim($searchstr))) . ':*\'';
$reversequery = '\'*:' . implode(' & *:', explode(' ', trim($searchstr))) . '\'';
$nospacequery = '\'' . implode('', explode(' ', trim($searchstr))) . ':*\'';
$query = '\'' . implode(':* & ', $words) . ':*\'';
$reversequery = '\'*:' . implode(' & *:', $words) . '\'';
$nospacequery = '\'' . implode('', $words) . ':*\'';
$searchclause = <<<EOSC
to_tsvector(lower(regexp_replace({$document}, '[[:punct:]]', ' ', 'g'))) @@ to_tsquery(lower({$query}))
@@ -596,5 +600,98 @@ EOSC;
// Otherwise return an empty array
return array();
}
private function _lehreinheit($searchstr, $type)
{
$dbModel = new DB_Model();
$idwhere = '';
if (ctype_digit(trim($searchstr)))
{
$id = (int)trim($searchstr);
$idwhere = ' OR le.lehreinheit_id = ' . $id;
}
$lehreinheit = $dbModel->execReadOnlyQuery('
SELECT
\'teachingunit\' AS renderer,
\''.$type.'\' AS type,
le.lehreinheit_id AS id,
le.studiensemester_kurzbz,
lv.bezeichnung,
\'lv_table_icon icon-\' AS foto,
UPPER(stg.typ || stg.kurzbz) AS studiengang,
le.lehrform_kurzbz,
lv.semester
FROM lehre.tbl_lehreinheit le
JOIN lehre.tbl_lehrveranstaltung lv ON (lv.lehrveranstaltung_id = le.lehrveranstaltung_id)
JOIN public.tbl_studiengang stg ON (stg.studiengang_kz = lv.studiengang_kz)
JOIN public.tbl_studiensemester sem ON (sem.studiensemester_kurzbz = le.studiensemester_kurzbz)
WHERE (' .
$this->buildSearchClause(
$dbModel,
array(
'(stg.typ || stg.kurzbz)',
'lv.semester',
'lv.bezeichnung'
),
$searchstr
) . ')' . $idwhere . '
');
// If something has been found then return it
if (hasData($lehreinheit)) return getData($lehreinheit);
// Otherwise return an empty array
return array();
}
private function _lehrveranstaltung($searchstr, $type)
{
$dbModel = new DB_Model();
$idwhere = '';
if (ctype_digit(trim($searchstr)))
{
$id = (int)trim($searchstr);
$idwhere = ' OR tbl_lehrveranstaltung.lehrveranstaltung_id = ' . $id;
}
$lehreinheit = $dbModel->execReadOnlyQuery('
SELECT
\'teachingunit\' AS renderer,
\''.$type.'\' AS type,
lehrveranstaltung_id as id,
tbl_lehrveranstaltung.bezeichnung,
\'lv_table_icon icon-lv\' as foto,
UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as studiengang,
semester,
lehrform_kurzbz,
tbl_organisationseinheit.bezeichnung as oe_bezeichnung
FROM lehre.tbl_lehrveranstaltung
JOIN public.tbl_studiengang USING(studiengang_kz)
JOIN public.tbl_organisationseinheit ON tbl_lehrveranstaltung.oe_kurzbz = tbl_organisationseinheit.oe_kurzbz
WHERE (' .
$this->buildSearchClause(
$dbModel,
array(
'(tbl_studiengang.typ || tbl_studiengang.kurzbz)',
'semester',
'tbl_lehrveranstaltung.bezeichnung'
),
$searchstr
) . ')' . $idwhere . '
');
// If something has been found then return it
if (hasData($lehreinheit)) return getData($lehreinheit);
// Otherwise return an empty array
return array();
}
}
+2 -17
View File
@@ -7,7 +7,6 @@ use \DateTime as DateTime;
use \DateTimeZone as DateTimeZone;
use \DateInterval as DateInterval;
use \DatePeriod as DatePeriod;
use CI3_Events as Events;
class StundenplanLib
{
@@ -144,7 +143,7 @@ class StundenplanLib
return $stundenplan_data;
$stundenplan_data = getData($stundenplan_data) ?? [];
$function_error = $this->expandObjectInformation($stundenplan_data, true);
$function_error = $this->expandObjectInformation($stundenplan_data);
if ($function_error)
return $function_error;
@@ -304,7 +303,7 @@ class StundenplanLib
return success($lektoren);
}
public function expandObjectInformation($data, $callExtensionsEvent = false)
public function expandObjectInformation($data)
{
$this->_ci =& get_instance();
@@ -393,20 +392,6 @@ class StundenplanLib
$item->lektor = $lektor_obj_array;
}
// info retrieved from extensions might not be necessary for some stundenplan use cases so the event
// will be gated behind this flag
if($callExtensionsEvent) {
Events::trigger(
'extendStundenplanData',
function & () use (&$data)
{
return $data;
}
);
}
}
public function fetchFerienTageEvents($start_date, $end_date, $studiengang_kz)
-195
View File
@@ -1,195 +0,0 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
use \DOMDocument as DOMDocument;
class XsdSchemaLib
{
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Converts an XSD into a field schema.
* @param string $xsd the XSD as a string
* @param string $templateKurzbz short name of the template
* @return object success(array) with template_kurzbz, root, fields
*/
public function parseSchema($xsd, $templateKurzbz)
{
libxml_use_internal_errors(true);
$dom = new DOMDocument();
if (!$dom->loadXML($xsd))
{
libxml_clear_errors();
return error($this->ci->p->t('cms', 'xsdUngueltig'));
}
libxml_clear_errors();
$root = null;
$fields = array();
$this->_walkElements($dom->documentElement, $root, $fields);
return success(array(
'template_kurzbz' => $templateKurzbz,
'root' => $root,
'fields' => $fields
));
}
/**
* Reads the values of a content from the XML.
* @param string $xml the value of tbl_contentsprache.content
* @param array $schema the result of parseSchema
* @return object success(array) field name => value
*/
public function extractValues($xml, $schema)
{
$values = array();
foreach ($schema['fields'] as $field)
{
$values[$field['name']] = '';
}
if ($xml === null || trim($xml) === '')
{
return success($values);
}
libxml_use_internal_errors(true);
$dom = new DOMDocument();
if (!$dom->loadXML($xml))
{
libxml_clear_errors();
return success($values);
}
libxml_clear_errors();
foreach ($schema['fields'] as $field)
{
$nodes = $dom->getElementsByTagName($field['name']);
if ($nodes->length > 0)
{
$values[$field['name']] = $nodes->item(0)->nodeValue;
}
}
return success($values);
}
/**
* Builds the XML from the values.
* @param array $schema the result of parseSchema
* @param array $values field name => value
* @return object success(string) the XML
*/
public function buildXml($schema, $values)
{
$xml = '<?xml version="1.0" encoding="UTF-8"?>';
$xml .= "\n<" . $schema['root'] . '>';
foreach ($schema['fields'] as $field)
{
$value = isset($values[$field['name']]) ? $values[$field['name']] : '';
$xml .= "\n<" . $field['name'] . '><![CDATA[' . $this->_escapeCdata($value) . ']]></' . $field['name'] . '>';
}
$xml .= "\n</" . $schema['root'] . '>';
libxml_use_internal_errors(true);
$check = new DOMDocument();
if (!$check->loadXML($xml))
{
libxml_clear_errors();
return error($this->ci->p->t('cms', 'xmlUngueltig'));
}
libxml_clear_errors();
return success($xml);
}
// -------------------------------------------------------------------------
// Private methods
/**
* Walks xs:element nodes recursively.
* The first element without a type attribute is the root.
* Elements with a type attribute are fields.
* xs:complexType and xs:sequence are traversed and ignored.
*/
private function _walkElements($node, &$root, &$fields)
{
if ($node->nodeType !== XML_ELEMENT_NODE)
{
return;
}
$localName = $node->localName;
if ($localName === 'element')
{
$typeAttr = $node->getAttribute('type');
if ($typeAttr === '')
{
if ($root === null)
{
$root = $node->getAttribute('name');
}
}
else
{
$minOccurs = $node->getAttribute('minOccurs');
$required = ($minOccurs === '') ? true : ((int)$minOccurs > 0);
$fields[] = array(
'name' => $node->getAttribute('name'),
'type' => $this->_mapType($typeAttr),
'required' => $required
);
}
}
foreach ($node->childNodes as $child)
{
$this->_walkElements($child, $root, $fields);
}
}
/**
* Maps an XSD type to the field schema type.
*/
private function _mapType($xsdType)
{
$map = array(
'xs:string' => 'string',
'xs:decimal' => 'string',
'xs:integer' => 'string',
'xs:positiveInteger' => 'string',
'xs:date' => 'date',
'wysiwyg' => 'wysiwyg',
'file' => 'file',
'boolean' => 'boolean'
);
return isset($map[$xsdType]) ? $map[$xsdType] : 'string';
}
// Escapes ]]> in the value. The legacy code does not do this and writes broken XML.
private function _escapeCdata($value)
{
return str_replace(']]>', ']]]]><![CDATA[>', $value);
}
}
@@ -159,8 +159,6 @@ class LvVwLib
if (!$stg->mischform)
return [];
$this->_ci->StudiengangModel->db->select('TRUE AS leaf', false);
return $this->_ci->stglib->orgform($path_template, $stg->studiengang_kz);
}
@@ -323,266 +323,4 @@ class Content_model extends DB_Model
return success(isset($result[$root_content_id]) ? $result[$root_content_id] : null);
}
/**
* Root contents without a parent in tbl_contentchild. Excludes news.
* @return stdClass success with array of rows or error
*/
public function getRootContent()
{
$query = '
SELECT *
FROM (
SELECT DISTINCT ON (content_id) *
FROM campus.tbl_content
LEFT JOIN campus.tbl_contentchild USING (content_id)
WHERE tbl_content.template_kurzbz <> ?
AND content_id NOT IN (
SELECT child_content_id FROM campus.tbl_contentchild
WHERE child_content_id = tbl_content.content_id)
) AS a
ORDER BY sort, content_id
';
return $this->execReadOnlyQuery($query, ['news']);
}
/**
* Recent news from the last two months, max 100 rows.
* @return stdClass success with array of rows or error
*/
public function getNewsContent()
{
$query = "
SELECT *
FROM campus.tbl_content
JOIN campus.tbl_news USING (content_id)
WHERE tbl_news.datum >= now() - '2 month'::interval
ORDER BY datum DESC
LIMIT 100
";
return $this->execReadOnlyQuery($query);
}
/**
* Search contents by content_id or titel. Excludes news. Returns content_ids only.
* @param array $searchItems search terms
* @return stdClass success with array of rows or error
*/
public function searchCms($searchItems)
{
if (empty($searchItems))
return success([]);
$conditions = [];
$params = [];
foreach ($searchItems as $term)
{
$conditions[] = '(content_id::text = ? OR lower(titel) LIKE lower(?))';
$params[] = $term;
$params[] = '%' . $term . '%';
}
$query = '
SELECT DISTINCT tbl_content.content_id
FROM campus.tbl_contentsprache
JOIN campus.tbl_content USING (content_id)
WHERE tbl_content.template_kurzbz <> ?
AND (' . implode(' OR ', $conditions) . ')
ORDER BY content_id
';
array_unshift($params, 'news');
return $this->execReadOnlyQuery($query, $params);
}
/**
* Per content metadata for the tree filter. Excludes news, like getRootContent().
* updateamum is the last touch on the content or on any of its versions.
*
* contentlength counts the visible characters of the newest version, the way getOne()
* picks it: the default language first, then the highest version. The XML scaffolding,
* the CDATA markers, the HTML tags, the entities and every space drop out, so a content
* that nobody ever filled measures 0 whatever empty shape it carries. The title counts
* too, so a content with a title and no body measures short instead of zero.
*
* @param string $uid current user, decides the mine flag
* @param string $sprache preferred language of the measured version
* @return stdClass success with array of rows or error
*/
public function getTreeMeta($uid, $sprache)
{
$query = "
SELECT tbl_content.content_id,
(SELECT COUNT(*) FROM campus.tbl_contentchild
WHERE tbl_contentchild.content_id = tbl_content.content_id) AS childcount,
tbl_content.insertamum,
GREATEST(
tbl_content.updateamum,
(SELECT MAX(updateamum) FROM campus.tbl_contentsprache
WHERE tbl_contentsprache.content_id = tbl_content.content_id)
) AS updateamum,
COALESCE(
tbl_content.insertvon = ? OR tbl_content.updatevon = ? OR EXISTS (
SELECT 1 FROM campus.tbl_contentsprache
WHERE tbl_contentsprache.content_id = tbl_content.content_id
AND (tbl_contentsprache.insertvon = ?
OR tbl_contentsprache.updatevon = ?)
),
false
) AS mine,
(SELECT length(
regexp_replace(
regexp_replace(
regexp_replace(
replace(replace(cs.content::text, '<![CDATA[', ''), ']]>', ''),
'<[^>]*>', '', 'g'),
'&[a-zA-Z]+;|&#[0-9]+;', '', 'g'),
'\s+', '', 'g')
)
FROM campus.tbl_contentsprache cs
WHERE cs.content_id = tbl_content.content_id
ORDER BY (cs.sprache = ?) DESC, cs.version DESC
LIMIT 1) AS contentlength
FROM campus.tbl_content
WHERE tbl_content.template_kurzbz <> ?
";
return $this->execReadOnlyQuery(
$query, [$uid, $uid, $uid, $uid, $sprache, 'news']
);
}
/**
* All contents eligible as children: excludes ancestors, self, and news.
* @param int $content_id the content to find children for
* @param string $sprache language for the titel subselect
* @return stdClass success with array of rows or error
*/
public function getPossibleChilds($content_id, $sprache)
{
$query = '
SELECT content_id, oe_kurzbz, template_kurzbz,
(SELECT titel FROM campus.tbl_contentsprache
WHERE sprache = ? AND content_id = tbl_content.content_id
ORDER BY version LIMIT 1) AS titel
FROM campus.tbl_content
WHERE content_id NOT IN (
WITH RECURSIVE parents(content_id, child_content_id) AS (
SELECT content_id, child_content_id FROM campus.tbl_contentchild
WHERE child_content_id = ?
UNION ALL
SELECT cc.content_id, cc.child_content_id
FROM campus.tbl_contentchild cc, parents
WHERE cc.child_content_id = parents.content_id)
SELECT content_id FROM parents GROUP BY content_id)
AND content_id <> ?
AND template_kurzbz <> ?
ORDER BY titel
';
return $this->execReadOnlyQuery($query, [$sprache, $content_id, $content_id, 'news']);
}
/**
* Where a content is referenced. Returns [{table, label}] for the delete dialog.
* Tolerates missing addon/testtool schemas.
* @param int $content_id
* @return stdClass success with flat array or error
*/
public function getUsage($content_id)
{
$coreQuery = "
SELECT 'campus.tbl_infoscreen_content' AS \"table\",
infoscreen_id::text AS label
FROM campus.tbl_infoscreen_content WHERE content_id = ?
UNION ALL
SELECT 'campus.tbl_news', betreff
FROM campus.tbl_news WHERE content_id = ?
UNION ALL
SELECT 'public.tbl_ort', ort_kurzbz
FROM public.tbl_ort WHERE content_id = ?
UNION ALL
SELECT 'public.tbl_service', bezeichnung
FROM public.tbl_service WHERE content_id = ?
UNION ALL
SELECT 'public.tbl_statistik', bezeichnung
FROM public.tbl_statistik WHERE content_id = ?
";
$coreParams = [$content_id, $content_id, $content_id, $content_id, $content_id];
$coreResult = $this->execReadOnlyQuery($coreQuery, $coreParams);
$rows = [];
if (!isError($coreResult) && getData($coreResult))
$rows = getData($coreResult);
// Optional schemas: testtool and addon may not exist. A failing query still writes a
// db error into the response envelope, so check for the table before querying it.
$optionalQueries = [
[
'testtool', 'tbl_ablauf_vorgaben',
"SELECT 'testtool.tbl_ablauf_vorgaben' AS \"table\",
ablauf_vorgaben_id::text AS label
FROM testtool.tbl_ablauf_vorgaben WHERE content_id = ?"
],
[
'addon', 'tbl_software',
"SELECT 'addon.tbl_software' AS \"table\",
software_id::text AS label
FROM addon.tbl_software WHERE content_id = ?"
]
];
foreach ($optionalQueries as $opt)
{
if (!$this->tableExists($opt[0], $opt[1]))
continue;
$result = $this->execReadOnlyQuery($opt[2], [$content_id]);
if (!isError($result) && getData($result))
$rows = array_merge($rows, getData($result));
}
return success($rows);
}
/**
* True if a base table exists. Guards the optional testtool and addon schemas.
* @param string $schema
* @param string $table
* @return bool
*/
private function tableExists($schema, $table)
{
$query = "
SELECT 1 AS exists
FROM information_schema.tables
WHERE table_catalog = ? AND table_schema = ? AND table_name = ?
";
return hasData($this->execReadOnlyQuery($query, [DB_NAME, $schema, $table]));
}
/**
* Returns oe_kurzbz for a content as a string.
* @param int $content_id
* @return stdClass success with string or error
*/
public function getOeKurzbz($content_id)
{
$query = 'SELECT oe_kurzbz FROM campus.tbl_content WHERE content_id = ?';
$result = $this->execReadOnlyQuery($query, [$content_id]);
if (isError($result))
return $result;
$data = getData($result);
if (empty($data))
return error('Content not found');
return success($data[0]->oe_kurzbz);
}
}
@@ -1,9 +1,6 @@
<?php
class Contentchild_model extends DB_Model
{
// swapSort reports the two boundary cases with this. See swapSort().
const NO_NEIGHBOUR = 'no neighbour';
/**
* Constructor
@@ -14,345 +11,4 @@ class Contentchild_model extends DB_Model
$this->dbTable = 'campus.tbl_contentchild';
$this->pk = 'contentchild_id';
}
/**
* Children with titel, sorted by sort.
* @param int $content_id
* @param string $sprache
* @return stdClass success with array of rows or error
*/
public function getChilds($content_id, $sprache)
{
$query = '
SELECT cc.contentchild_id, cc.child_content_id, cc.sort,
(SELECT titel FROM campus.tbl_contentsprache
WHERE content_id = cc.child_content_id AND sprache = ?
ORDER BY version LIMIT 1) AS titel
FROM campus.tbl_contentchild cc
WHERE cc.content_id = ?
ORDER BY cc.sort, cc.contentchild_id
';
return $this->execReadOnlyQuery($query, [$sprache, $content_id]);
}
/**
* Parents of a content, with the title of each parent.
* A content can hang under more than one parent, so this returns a list.
* @param int $content_id
* @param string $sprache
* @return stdClass success with array of rows or error
*/
public function getParents($content_id, $sprache)
{
$query = '
SELECT cc.contentchild_id, cc.content_id, cc.sort,
(SELECT titel FROM campus.tbl_contentsprache
WHERE content_id = cc.content_id AND sprache = ?
ORDER BY version LIMIT 1) AS titel
FROM campus.tbl_contentchild cc
WHERE cc.child_content_id = ?
ORDER BY cc.content_id
';
return $this->execReadOnlyQuery($query, [$sprache, $content_id]);
}
/**
* Children of a content as a reader may see them.
*
* Differs from getChilds() on purpose. getChilds() serves the admin and returns every
* child with the lowest version. A reader needs the newest visible version, only active
* contents, and no page that a group rule keeps from them.
*
* The group rule follows CmsLib::getContent(): a content without a row in
* tbl_contentgruppe is open, a content with rows needs membership in one of them.
*
* @param int $content_id
* @param string $sprache
* @param string $uid
* @return stdClass success with array of rows or error
*/
public function getChildsForReader($content_id, $sprache, $uid)
{
$query = '
SELECT
cc.child_content_id AS content_id,
cc.sort,
COALESCE(
(SELECT cs.titel FROM campus.tbl_contentsprache cs
WHERE cs.content_id = cc.child_content_id
AND cs.sprache = ?
AND cs.sichtbar
AND LENGTH(cs.titel) > 0
ORDER BY cs.version DESC NULLS LAST LIMIT 1),
(SELECT cs.titel FROM campus.tbl_contentsprache cs
WHERE cs.content_id = cc.child_content_id
AND cs.sichtbar
AND LENGTH(cs.titel) > 0
ORDER BY cs.version DESC NULLS LAST LIMIT 1),
c.beschreibung
) AS titel
FROM campus.tbl_contentchild cc
JOIN campus.tbl_content c ON c.content_id = cc.child_content_id
WHERE cc.content_id = ?
AND c.aktiv
-- Without a visible version the reader cannot open the page at all.
AND EXISTS (
SELECT 1 FROM campus.tbl_contentsprache cs
WHERE cs.content_id = cc.child_content_id AND cs.sichtbar
)
AND (
NOT EXISTS (
SELECT 1 FROM campus.tbl_contentgruppe cg
WHERE cg.content_id = cc.child_content_id
)
OR EXISTS (
SELECT 1 FROM campus.tbl_contentgruppe cg
JOIN public.vw_gruppen vg ON vg.gruppe_kurzbz = cg.gruppe_kurzbz
WHERE cg.content_id = cc.child_content_id AND vg.uid = ?
)
)
ORDER BY cc.sort, cc.contentchild_id
';
return $this->execReadOnlyQuery($query, [$sprache, $content_id, $uid]);
}
/**
* Highest sort value. Returns 0 if no row exists.
* @param int $content_id
* @return stdClass success with int or error
*/
public function getMaxSort($content_id)
{
$query = '
SELECT COALESCE(MAX(sort), 0) AS max_sort
FROM campus.tbl_contentchild
WHERE content_id = ?
';
$result = $this->execReadOnlyQuery($query, [$content_id]);
if (isError($result))
return $result;
return success((int) getData($result)[0]->max_sort);
}
/**
* All descendant content_ids recursively. Returns a flat array.
* @param int $content_id
* @return stdClass success with array of ints or error
*/
public function getAllChildIds($content_id)
{
$query = '
WITH RECURSIVE childs(content_id, child_content_id) AS (
SELECT content_id, child_content_id FROM campus.tbl_contentchild WHERE content_id = ?
UNION ALL
SELECT cc.content_id, cc.child_content_id
FROM campus.tbl_contentchild cc, childs
WHERE cc.content_id = childs.child_content_id)
SELECT DISTINCT child_content_id FROM childs
';
$result = $this->execReadOnlyQuery($query, [$content_id]);
if (isError($result))
return $result;
$ids = [];
if (getData($result))
{
foreach (getData($result) as $row)
$ids[] = (int) $row->child_content_id;
}
return success($ids);
}
/**
* Swap sort value with the neighbour. Direction is 'up' or 'down'.
* @param int $contentchild_id
* @param string $direction 'up' or 'down'
* @return stdClass success or error
*/
public function swapSort($contentchild_id, $direction)
{
$currentResult = $this->load($contentchild_id);
if (isError($currentResult))
return $currentResult;
$current = getData($currentResult)[0];
if ($direction === 'up')
{
$neighbourQuery = '
SELECT contentchild_id, sort
FROM campus.tbl_contentchild
WHERE content_id = ? AND sort < ?
ORDER BY sort DESC
LIMIT 1
';
}
else
{
$neighbourQuery = '
SELECT contentchild_id, sort
FROM campus.tbl_contentchild
WHERE content_id = ? AND sort > ?
ORDER BY sort ASC
LIMIT 1
';
}
$neighbourResult = $this->execReadOnlyQuery($neighbourQuery, [$current->content_id, $current->sort]);
if (isError($neighbourResult))
return $neighbourResult;
$neighbourData = getData($neighbourResult);
// A marker, not a message. The controller owns the wording, because a model has no
// phrases. Returning a phrase key here put the key itself in front of the user.
if (empty($neighbourData))
return error(self::NO_NEIGHBOUR);
$neighbour = $neighbourData[0];
// Transaction wraps the swap; the legacy does not.
$this->db->trans_start();
$this->execQuery(
'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
[$neighbour->sort, $current->contentchild_id]
);
$this->execQuery(
'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
[$current->sort, $neighbour->contentchild_id]
);
$this->db->trans_complete();
if ($this->db->trans_status() === false)
return error('Sort swap failed');
return success(true);
}
/**
* Writes the whole order of one content in one transaction. Serves the drag and drop.
* The list must hold every child of the content exactly once, or the rows left out
* would keep a stale sort value.
* @param int $content_id
* @param array $contentchild_ids in the wanted order
* @return stdClass success(true) or error
*/
public function setSortOrder($content_id, $contentchild_ids)
{
$currentResult = $this->execReadOnlyQuery(
'SELECT contentchild_id FROM campus.tbl_contentchild WHERE content_id = ?',
[$content_id]
);
if (isError($currentResult))
return $currentResult;
$current = [];
foreach ((array) getData($currentResult) as $row)
$current[] = (int) $row->contentchild_id;
$wanted = array_map('intval', array_values($contentchild_ids));
$check = $wanted;
sort($current);
sort($check);
if ($current !== $check)
return error('Sort order does not match the children of the content');
$this->db->trans_start();
foreach ($wanted as $position => $contentchild_id)
{
$this->execQuery(
'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
[$position + 1, $contentchild_id]
);
}
$this->db->trans_complete();
if ($this->db->trans_status() === false)
return error('Sort order update failed');
return success(true);
}
/**
* Every ancestor of the given contents, with the distance to the content it belongs to.
*
* The tree shows a search result out of its place: a title like "Team" says nothing
* about which unit it belongs to. This delivers the way down to it.
*
* A content can hang under more than one parent, so a level can hold more than one
* ancestor. Every one of them comes back, with tiefe 1 for the direct parent. The
* caller decides how to draw that.
*
* tiefe also ends a cycle: a content whose parent chain leads back to itself would
* otherwise recurse forever.
*
* @param array $content_ids contents to walk up from
* @param int $maxTiefe how many levels to climb
* @param string $sprache language of the ancestor titles
* @return stdClass success with array of rows or error
*/
public function getAncestors($content_ids, $maxTiefe, $sprache)
{
if (empty($content_ids))
return success([]);
$platzhalter = implode(', ', array_fill(0, count($content_ids), '?'));
// The aggregate and the title sit in the final select. The recursive term names
// the working table once and carries no subquery, which is what Postgres allows.
$query = '
WITH RECURSIVE auf(start_id, content_id, tiefe) AS (
SELECT cc.child_content_id, cc.content_id, 1
FROM campus.tbl_contentchild cc
WHERE cc.child_content_id IN (' . $platzhalter . ')
UNION ALL
SELECT auf.start_id, cc.content_id, auf.tiefe + 1
FROM campus.tbl_contentchild cc
JOIN auf ON cc.child_content_id = auf.content_id
WHERE auf.tiefe < ?
)
SELECT auf.start_id, auf.content_id, MIN(auf.tiefe) AS tiefe,
(SELECT titel FROM campus.tbl_contentsprache cs
WHERE cs.content_id = auf.content_id AND cs.sprache = ?
ORDER BY cs.version LIMIT 1) AS titel,
-- The caller turns this into the entitled flag, so the path can offer a
-- link only for an ancestor the editor may open.
(SELECT c.oe_kurzbz FROM campus.tbl_content c
WHERE c.content_id = auf.content_id) AS oe_kurzbz
FROM auf
GROUP BY auf.start_id, auf.content_id
ORDER BY auf.start_id, MIN(auf.tiefe) DESC, auf.content_id
';
$params = array_map('intval', array_values($content_ids));
$params[] = (int) $maxTiefe;
$params[] = $sprache;
return $this->execReadOnlyQuery($query, $params);
}
/**
* Removes a content from the child table, as parent and as child.
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentchild
WHERE content_id = ? OR child_content_id = ?',
[$content_id, $content_id]
);
}
}
@@ -10,8 +10,6 @@ class Contentgruppe_model extends DB_Model
parent::__construct();
$this->dbTable = 'campus.tbl_contentgruppe';
$this->pk = array('gruppe_kurzbz', 'content_id');
// Composite key without a sequence, so insert() must skip insert_id() and its LASTVAL().
$this->hasSequence = false;
}
/**
@@ -20,7 +18,7 @@ class Contentgruppe_model extends DB_Model
*
* @param int $content_id
*
* @return stdClass success(true) wenn locked sonst success(false)
* @return stdClass success(true) wenn eingeschraenkt sonst success(false)
*/
public function islocked($content_id)
{
@@ -32,7 +30,7 @@ class Contentgruppe_model extends DB_Model
}
/**
* Prüft ob ein User die Berechtigung fuer das Anzeigen des
* Prueft ob ein User die Berechtigung fuer das Anzeigen des
* Contents besitzt
*
* @param int $content_id ID des Contents
@@ -58,89 +56,5 @@ class Contentgruppe_model extends DB_Model
return $result;
return success(!!getData($result));
}
/**
* Groups assigned to a content, with bezeichnung.
* @param int $content_id
* @return stdClass success with array of rows or error
*/
public function getGruppen($content_id)
{
$query = '
SELECT cg.gruppe_kurzbz, g.bezeichnung
FROM campus.tbl_contentgruppe cg
JOIN public.tbl_gruppe g USING (gruppe_kurzbz)
WHERE cg.content_id = ?
ORDER BY cg.gruppe_kurzbz
';
return $this->execReadOnlyQuery($query, [$content_id]);
}
/**
* All selectable groups. Returns gruppe_kurzbz and bezeichnung.
* @return stdClass success with array of rows or error
*/
public function getAllGruppen()
{
// content_visible marks a group as selectable for content rights. Generated groups
// carry FALSE. admin.php filters the same way via getgruppe(...,$content_visible=true).
$query = '
SELECT gruppe_kurzbz, bezeichnung
FROM public.tbl_gruppe
WHERE content_visible = TRUE
ORDER BY gruppe_kurzbz
';
return $this->execReadOnlyQuery($query);
}
/**
* Groups for multiple contents in one query. Returns content_id => [gruppe_kurzbz, ...].
* @param array $contentIds array of content_id values
* @return stdClass success with associative array or error
*/
public function getGruppenForContents($contentIds)
{
if (empty($contentIds))
return success([]);
$placeholders = implode(',', array_fill(0, count($contentIds), '?'));
$query = '
SELECT content_id, gruppe_kurzbz
FROM campus.tbl_contentgruppe
WHERE content_id IN (' . $placeholders . ')
ORDER BY content_id, gruppe_kurzbz
';
$result = $this->execReadOnlyQuery($query, array_values($contentIds));
if (isError($result))
return $result;
$map = [];
if (getData($result))
{
foreach (getData($result) as $row)
{
$id = (int) $row->content_id;
if (!isset($map[$id]))
$map[$id] = [];
$map[$id][] = $row->gruppe_kurzbz;
}
}
return success($map);
}
/**
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentgruppe WHERE content_id = ?',
[$content_id]
);
}
}
@@ -11,76 +11,4 @@ class Contentlog_model extends DB_Model
$this->dbTable = 'campus.tbl_contentlog';
$this->pk = 'contentlog_id';
}
/**
* The open lock entry of a user (ende IS NULL). Returns null if none exists.
* Match the holder and take the newest, otherwise the age belongs to an abandoned entry.
* @param int $contentsprache_id
* @param string $uid holder of the lock
* @return stdClass success with row or null, or error
*/
public function getOpenLock($contentsprache_id, $uid)
{
$query = '
SELECT contentlog_id, uid, contentsprache_id, start
FROM campus.tbl_contentlog
WHERE contentsprache_id = ? AND uid = ? AND ende IS NULL
ORDER BY start DESC
LIMIT 1
';
$result = $this->execReadOnlyQuery($query, [$contentsprache_id, $uid]);
if (isError($result))
return $result;
$data = getData($result);
return success(empty($data) ? null : $data[0]);
}
/**
* Ends every open lock entry of a version. Without a uid it ends the entry of every
* holder, which is what a take-over and a forced release need.
* @param int $contentsprache_id
* @param string|null $uid
* @return stdClass
*/
public function closeOpenEntries($contentsprache_id, $uid = null)
{
$query = 'UPDATE campus.tbl_contentlog SET ende = now()
WHERE contentsprache_id = ? AND ende IS NULL';
$params = [$contentsprache_id];
if ($uid !== null)
{
$query .= ' AND uid = ?';
$params[] = $uid;
}
return $this->execQuery($query, $params);
}
/**
* @param int $contentsprache_id
* @return stdClass
*/
public function deleteByContentsprache($contentsprache_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id = ?',
[$contentsprache_id]
);
}
/**
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id IN
(SELECT contentsprache_id FROM campus.tbl_contentsprache WHERE content_id = ?)',
[$content_id]
);
}
}
@@ -38,177 +38,5 @@ class Contentsprache_model extends DB_Model
return success(!!getData($result));
}
/**
* All versions of a language, ascending by version.
* @param int $content_id
* @param string $sprache
* @return stdClass success with array of rows or error
*/
public function getVersions($content_id, $sprache)
{
$query = '
SELECT contentsprache_id, version, sichtbar, titel,
insertamum, insertvon, updateamum, updatevon, gesperrt_uid
FROM campus.tbl_contentsprache
WHERE content_id = ? AND sprache = ?
ORDER BY version ASC
';
return $this->execReadOnlyQuery($query, [$content_id, $sprache]);
}
/**
* Distinct languages for a content. Returns a flat array of strings.
* @param int $content_id
* @return stdClass success with array of strings or error
*/
public function getLanguages($content_id)
{
$query = '
SELECT DISTINCT sprache
FROM campus.tbl_contentsprache
WHERE content_id = ?
ORDER BY sprache
';
$result = $this->execReadOnlyQuery($query, [$content_id]);
if (isError($result))
return $result;
$languages = [];
if (getData($result))
{
foreach (getData($result) as $row)
$languages[] = $row->sprache;
}
return success($languages);
}
/**
* Highest version number. Returns 0 if no row exists.
* @param int $content_id
* @param string $sprache
* @return stdClass success with int or error
*/
public function getMaxVersion($content_id, $sprache)
{
$query = '
SELECT COALESCE(MAX(version), 0) AS max_version
FROM campus.tbl_contentsprache
WHERE content_id = ? AND sprache = ?
';
$result = $this->execReadOnlyQuery($query, [$content_id, $sprache]);
if (isError($result))
return $result;
return success((int) getData($result)[0]->max_version);
}
/**
* Row count for a content+language. Used for the delete guard.
* @param int $content_id
* @param string $sprache
* @return stdClass success with int or error
*/
public function getNumberOfVersions($content_id, $sprache)
{
$query = '
SELECT COUNT(*) AS cnt
FROM campus.tbl_contentsprache
WHERE content_id = ? AND sprache = ?
';
$result = $this->execReadOnlyQuery($query, [$content_id, $sprache]);
if (isError($result))
return $result;
return success((int) getData($result)[0]->cnt);
}
/**
* One version, all columns. If $version is empty, returns the highest version.
* @param int $content_id
* @param string $sprache
* @param int|null $version
* @return stdClass success with single row or error
*/
public function getOne($content_id, $sprache, $version = null)
{
$params = [$content_id, $sprache];
$versionClause = '';
if (!empty($version))
{
$versionClause = ' AND version = ?';
$params[] = $version;
}
$query = '
SELECT * FROM campus.tbl_contentsprache
WHERE content_id = ? AND sprache = ?' . $versionClause . '
ORDER BY version DESC LIMIT 1
';
$result = $this->execReadOnlyQuery($query, $params);
if (isError($result))
return $result;
$data = getData($result);
if (empty($data))
return error('Version not found');
return success($data[0]);
}
/**
* Clears the lock of a version. Without a uid it clears whoever holds it, which is
* what a forced release needs.
* @param int $contentsprache_id
* @param string|null $uid
* @return stdClass
*/
public function releaseLock($contentsprache_id, $uid = null)
{
$query = 'UPDATE campus.tbl_contentsprache SET gesperrt_uid = NULL
WHERE contentsprache_id = ?';
$params = [$contentsprache_id];
if ($uid !== null)
{
$query .= ' AND gesperrt_uid = ?';
$params[] = $uid;
}
return $this->execQuery($query, $params);
}
/**
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentsprache WHERE content_id = ?',
[$content_id]
);
}
/**
* @param int $content_id
* @param string $sprache
* @param int $version
* @return stdClass
*/
public function deleteVersion($content_id, $sprache, $version)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentsprache
WHERE content_id = ? AND sprache = ? AND version = ?',
[$content_id, $sprache, $version]
);
}
}
-237
View File
@@ -30,241 +30,4 @@ class Dms_model extends DB_Model
return $returnArray;
}
/**
* Reads a DMS category and the access facts a listing needs.
*
* Mirrors the rule of cms/dms.php and dms.class.php at category level:
* a category is locked when any category in its parent chain grants a group or
* carries a berechtigung_kurzbz. The caller decides, this method only reports.
*
* @param string $kategorie_kurzbz
* gesperrt and in_gruppe come back as 0 or 1, not as a boolean. A PostgreSQL boolean
* reaches PHP as the string 'f' when the driver conversion does not run, and 'f' is
* truthy. The int cast is correct in both cases.
*
* @param string $uid
* @return object one row, or an empty result when the category does not exist
*/
public function getKategorieZugriff($kategorie_kurzbz, $uid)
{
// tiefe guards against a cyclic parent chain.
$query = '
WITH RECURSIVE chain(kategorie_kurzbz, parent_kategorie_kurzbz, berechtigung_kurzbz, tiefe) AS (
SELECT kategorie_kurzbz, parent_kategorie_kurzbz, berechtigung_kurzbz, 1
FROM campus.tbl_dms_kategorie
WHERE kategorie_kurzbz = ?
UNION ALL
SELECT k.kategorie_kurzbz, k.parent_kategorie_kurzbz, k.berechtigung_kurzbz, c.tiefe + 1
FROM campus.tbl_dms_kategorie k
JOIN chain c ON k.kategorie_kurzbz = c.parent_kategorie_kurzbz
WHERE c.tiefe < 32
)
SELECT
kat.kategorie_kurzbz,
kat.bezeichnung,
kat.beschreibung,
kat.berechtigung_kurzbz,
(
EXISTS (SELECT 1 FROM chain WHERE chain.berechtigung_kurzbz IS NOT NULL)
OR EXISTS (
SELECT 1
FROM campus.tbl_dms_kategorie_gruppe g
JOIN chain ON chain.kategorie_kurzbz = g.kategorie_kurzbz
)
)::int AS gesperrt,
(EXISTS (
SELECT 1
FROM campus.tbl_dms_kategorie_gruppe g
JOIN chain ON chain.kategorie_kurzbz = g.kategorie_kurzbz
JOIN public.tbl_benutzergruppe b ON b.gruppe_kurzbz = g.gruppe_kurzbz
WHERE b.uid = ?
))::int AS in_gruppe
FROM campus.tbl_dms_kategorie kat
WHERE kat.kategorie_kurzbz = ?
';
return $this->execReadOnlyQuery($query, array($kategorie_kurzbz, $uid, $kategorie_kurzbz));
}
/**
* Lists the newest version of every document in one DMS category.
*
* filename stays out of the result. The reader gets the document through
* cms/dms.php?id=<dms_id>, never through the name on disk.
*
* @param string $kategorie_kurzbz
* @return object
*/
public function getKategorieDokumente($kategorie_kurzbz)
{
$query = '
SELECT
d.dms_id,
v.version,
v.name,
v.beschreibung,
v.mimetype,
COALESCE(v.updateamum, v.insertamum) AS geaendert
FROM campus.tbl_dms d
JOIN campus.tbl_dms_version v ON v.dms_id = d.dms_id
WHERE d.kategorie_kurzbz = ?
-- cis_suche marks a document as published to CIS readers. A category also
-- holds working files, and those must not appear on a content page.
AND v.cis_suche
AND v.version = (
SELECT MAX(version) FROM campus.tbl_dms_version WHERE dms_id = d.dms_id
)
-- A project document carries its own rule in dms.class.php and does not
-- belong in a CMS category listing.
AND NOT EXISTS (
SELECT 1 FROM fue.tbl_projekt_dokument pd WHERE pd.dms_id = d.dms_id
)
ORDER BY LOWER(v.name)
';
return $this->execReadOnlyQuery($query, array($kategorie_kurzbz));
}
/**
* Newest version of the named documents, in no particular order.
*
* Unlike getKategorieDokumente() this does NOT filter on cis_suche. An editor who names
* a dms_id has chosen that document on purpose, and the flag only marks a document for
* the CIS search. The category rule still applies, and the caller enforces it.
*
* @param array $ids dms_id values
* @return object
*/
public function getDokumenteByIds($ids)
{
if (empty($ids))
return success(array());
$platzhalter = implode(', ', array_fill(0, count($ids), '?'));
$query = '
SELECT
d.dms_id,
d.kategorie_kurzbz,
v.version,
v.name,
v.beschreibung,
v.mimetype,
COALESCE(v.updateamum, v.insertamum) AS geaendert
FROM campus.tbl_dms d
JOIN campus.tbl_dms_version v ON v.dms_id = d.dms_id
WHERE d.dms_id IN (' . $platzhalter . ')
AND v.version = (
SELECT MAX(version) FROM campus.tbl_dms_version WHERE dms_id = d.dms_id
)
AND NOT EXISTS (
SELECT 1 FROM fue.tbl_projekt_dokument pd WHERE pd.dms_id = d.dms_id
)
';
return $this->execReadOnlyQuery($query, array_values($ids));
}
/**
* Every document of one category, for the editor picker.
*
* Unlike getKategorieDokumente() this does NOT filter on cis_suche, because
* dms-dokumente renders named documents regardless of that flag. The flag travels with
* the row so the picker can mark a document that no reader would find by search.
*
* @param string $kategorie_kurzbz
* @return object
*/
public function getKategorieDokumenteAlle($kategorie_kurzbz)
{
$query = '
SELECT
d.dms_id,
v.name,
v.cis_suche::int AS cis_suche
FROM campus.tbl_dms d
JOIN LATERAL (
SELECT v.name, v.cis_suche
FROM campus.tbl_dms_version v
WHERE v.dms_id = d.dms_id
ORDER BY v.version DESC
LIMIT 1
) v ON TRUE
WHERE d.kategorie_kurzbz = ?
AND NOT EXISTS (
SELECT 1 FROM fue.tbl_projekt_dokument pd WHERE pd.dms_id = d.dms_id
)
ORDER BY LOWER(v.name)
';
return $this->execReadOnlyQuery($query, array($kategorie_kurzbz));
}
/**
* Lists every DMS category with the number of documents it holds.
* Used by the insert dialog of the CMS content component "dms-liste".
*
* @return object
*/
public function getKategorien()
{
$query = '
SELECT
k.kategorie_kurzbz,
k.bezeichnung,
k.oe_kurzbz,
COALESCE(a.anzahl, 0) AS anzahl
FROM campus.tbl_dms_kategorie k
LEFT JOIN (
SELECT d.kategorie_kurzbz, COUNT(*) AS anzahl
FROM campus.tbl_dms d
JOIN (
SELECT DISTINCT ON (dms_id) dms_id, cis_suche
FROM campus.tbl_dms_version
ORDER BY dms_id, version DESC
) v ON v.dms_id = d.dms_id
WHERE v.cis_suche
GROUP BY d.kategorie_kurzbz
) a ON a.kategorie_kurzbz = k.kategorie_kurzbz
ORDER BY LOWER(COALESCE(k.bezeichnung, k.kategorie_kurzbz))
';
return $this->execReadOnlyQuery($query);
}
/**
* The category itself plus its sub categories, down to a depth.
*
* tiefe 0 returns only the category named. The depth bound also ends a cycle: a
* category whose parent chain leads back to itself would otherwise recurse forever.
*
* The caller must still check the entitlement of every category it gets back. A sub
* category carries its own group rule and may be closed while the parent is open.
*
* @param string $kategorie_kurzbz root of the walk
* @param int $tiefe how many levels below the root to include
* @return object success with array of rows or error
*/
public function getMitUnterkategorien($kategorie_kurzbz, $tiefe)
{
$query = '
WITH RECURSIVE unter(kategorie_kurzbz, tiefe) AS (
SELECT kategorie_kurzbz, 0
FROM campus.tbl_dms_kategorie
WHERE kategorie_kurzbz = ?
UNION ALL
SELECT k.kategorie_kurzbz, unter.tiefe + 1
FROM campus.tbl_dms_kategorie k
JOIN unter ON k.parent_kategorie_kurzbz = unter.kategorie_kurzbz
WHERE unter.tiefe < ?
)
SELECT kategorie_kurzbz, MIN(tiefe) AS tiefe
FROM unter
GROUP BY kategorie_kurzbz
ORDER BY MIN(tiefe), kategorie_kurzbz
';
return $this->execReadOnlyQuery($query, [$kategorie_kurzbz, (int) $tiefe]);
}
}
@@ -11,30 +11,4 @@ class Template_model extends DB_Model
$this->dbTable = 'campus.tbl_template';
$this->pk = 'template_kurzbz';
}
/**
* True if the template carries a CIS4 stylesheet of its own, which is not true for every template.
*
* @param string $template_kurzbz
* @return stdClass success(bool) or error
*/
public function hasCis4Stylesheet($template_kurzbz)
{
$query = '
SELECT (xslt_xhtml_c4 IS NOT NULL
AND xslt_xhtml_c4::text IS DISTINCT FROM xslt_xhtml::text) AS eigen
FROM campus.tbl_template
WHERE template_kurzbz = ?
';
$result = $this->execReadOnlyQuery($query, [$template_kurzbz]);
if (isError($result))
return $result;
$data = getData($result);
if (empty($data))
return success(false);
return success($data[0]->eigen === 't' || $data[0]->eigen === true);
}
}
@@ -340,6 +340,20 @@ EOSQL;
}
public function getByLeStudiensemester($lehreinheit_id, $studiensemester_kurzbz)
{
$qry = "WITH lehreinheiten AS (
SELECT *
FROM lehre.tbl_lehreinheit
WHERE lehreinheit_id = ?
AND studiensemester_kurzbz = ?
)," . $this->_getLeCTE();
$params = array($lehreinheit_id, $studiensemester_kurzbz);
return $this->execReadOnlyQuery($qry, $params);
}
public function getByLvidStudiensemester($lv_id, $studiensemester_kurzbz, $mitarbeiter_uid = null, $fachbereich_kurzbz = null)
{
$qry = "WITH lehreinheiten AS (
@@ -347,35 +361,7 @@ EOSQL;
FROM lehre.tbl_lehreinheit
WHERE lehrveranstaltung_id = ?
AND studiensemester_kurzbz = ?
),
". $this->_getGruppenCTE() . ",
". $this->_getLektorenCTE() . ",
". $this->_getFachbereichCTE() . ",
". $this->_getTagsCTE() . "
SELECT lehreinheiten.*,
lehreinheiten.lehrform_kurzbz as lv_lehrform_kurzbz,
tbl_lehrveranstaltung.kurzbz as lv_kurzbz,
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
COALESCE(tag_data_agg.tags, '[]'::json) AS tags,
gruppen.gruppen,
mitarbeiter.lektoren,
mitarbeiter.le_planstunden,
mitarbeiter.vorname,
mitarbeiter.nachname,
mitarbeiter.semesterstunden,
fachbereich.bezeichnung as fachbereich,
UPPER(CONCAT(tbl_studiengang.typ,tbl_studiengang.kurzbz)) as studiengang,
semester
FROM lehreinheiten
LEFT JOIN lehre.tbl_lehrveranstaltung ON tbl_lehrveranstaltung.lehrveranstaltung_id = lehreinheiten.lehrfach_id
LEFT JOIN public.tbl_studiengang USING(studiengang_kz)
LEFT JOIN tag_data_agg ON tag_data_agg.lehreinheit_id = lehreinheiten.lehreinheit_id
LEFT JOIN mitarbeiter ON lehreinheiten.lehreinheit_id = mitarbeiter.lehreinheit_id
LEFT JOIN fachbereich ON lehreinheiten.lehreinheit_id = fachbereich.lehreinheit_id
LEFT JOIN gruppen ON lehreinheiten.lehreinheit_id = gruppen.lehreinheit_id
WHERE true
";
)," . $this->_getLeCTE();
$params = array($lv_id, $studiensemester_kurzbz);
@@ -395,9 +381,83 @@ EOSQL;
return $this->execReadOnlyQuery($qry, $params);
}
private function getLVTmp($stg_kz = null)
public function getOffeneStunden($lehreinheit_ids)
{
$qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *,
if (!is_array($lehreinheit_ids))
$lehreinheit_ids = [$lehreinheit_ids];
if (empty($lehreinheit_ids))
return success([]);
$db = new DB_Model();
$placeholders = implode(',', array_fill(0, count($lehreinheit_ids), '?'));
$qry = "WITH eventdauer AS (
SELECT
tbl_kalender_lehreinheit.lehreinheit_id,
tbl_kalender.von,
tbl_kalender.bis,
tbl_kalender.kalender_id
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_kalender_lehreinheit USING(lehreinheit_id)
JOIN lehre.tbl_kalender USING(kalender_id)
WHERE tbl_lehreinheit.lehreinheit_id IN ($placeholders)
AND NOT EXISTS (SELECT 1 FROM lehre.tbl_kalender nachfolger
WHERE nachfolger.vorgaenger_kalender_id = tbl_kalender.kalender_id)
AND tbl_kalender.status_kurzbz NOT IN ('deleted', 'todelete', 'archived')
GROUP BY tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender.von, tbl_kalender.bis, tbl_kalender.kalender_id
),
raster AS (
SELECT
eventdauer.lehreinheit_id,
eventdauer.kalender_id,
eventdauer.von,
eventdauer.bis,
vonstunde.stunde AS vonstunde_nr,
bisstunde.stunde AS bisstunde_nr
FROM eventdauer
LEFT JOIN lehre.tbl_stunde vonstunde ON eventdauer.von::time = vonstunde.beginn
LEFT JOIN lehre.tbl_stunde bisstunde ON eventdauer.bis::time = bisstunde.ende
),
unterrichtsdauer AS (
SELECT
raster.lehreinheit_id,
raster.kalender_id,
sum(EXTRACT(EPOCH FROM (LEAST(stunde.ende, raster.bis::time) - GREATEST(stunde.beginn, raster.von::time))) / 60) AS minuten
FROM raster JOIN lehre.tbl_stunde stunde ON stunde.beginn < raster.bis::time AND stunde.ende > raster.von::time
WHERE raster.vonstunde_nr IS NULL OR raster.bisstunde_nr IS NULL
GROUP BY raster.lehreinheit_id, raster.kalender_id
),
verplantestunden AS (
SELECT raster.lehreinheit_id, raster.kalender_id,
CASE
WHEN raster.vonstunde_nr IS NOT NULL AND raster.bisstunde_nr IS NOT NULL
THEN (raster.bisstunde_nr - raster.vonstunde_nr + 1)
ELSE coalesce(unterrichtsdauer.minuten, 0) / 45
END AS verplantestunde
FROM raster LEFT JOIN unterrichtsdauer ON unterrichtsdauer.lehreinheit_id = raster.lehreinheit_id AND unterrichtsdauer.kalender_id = raster.kalender_id
),
verplant_gesamt AS (
SELECT lehreinheit_id, SUM(verplantestunde) AS verplant
FROM verplantestunden
GROUP BY lehreinheit_id
)
SELECT
lema.lehreinheit_id,
lema.mitarbeiter_uid,
lema.planstunden,
ROUND(coalesce(verplant_gesamt.verplant, 0), 2) AS verplant,
ROUND((lema.planstunden - coalesce(verplant_gesamt.verplant, 0)), 2) AS offenestunden
FROM lehre.tbl_lehreinheitmitarbeiter lema
LEFT JOIN verplant_gesamt ON verplant_gesamt.lehreinheit_id = lema.lehreinheit_id
WHERE lema.lehreinheit_id IN ($placeholders)";
return $db->execReadOnlyQuery($qry, array_merge($lehreinheit_ids, $lehreinheit_ids));
}
private function getLVTmp()
{
return "SELECT DISTINCT ON(lehrveranstaltung_id) *,
'' as stundenblockung,
'' as lehreinheit_id,
'' as wochenrythmus,
@@ -408,24 +468,33 @@ EOSQL;
'' as studienplan_beeichnung,
UPPER(CONCAT(vw_lehreinheit.stg_typ, vw_lehreinheit.stg_kurzbz)) as studiengang
FROM campus.vw_lehreinheit
WHERE mitarbeiter_uid = ?
AND studiensemester_kurzbz = ?";
if (!is_null($stg_kz)) {
$qry .= " AND lv_studiengang_kz = ?";
}
return $qry;
WHERE studiensemester_kurzbz = ? ";
}
public function getLvsByEmployee($mitarbeiter_uid, $studiensemester_kurzbz, $stg_kz = null)
public function getLvsById($lehrveranstaltung_id, $studiensemester_kurzbz)
{
$qry = "WITH lvs AS (" . $this->getLVTmp($stg_kz) . ")
$where = ' AND lehrveranstaltung_id = ?';
$qry = "WITH lvs AS (" . $this->getLVTmp() . $where .")
SELECT lvs.*
FROM lvs
";
";
$params = array($studiensemester_kurzbz, $lehrveranstaltung_id);
return $this->execReadOnlyQuery($qry, $params);
}
public function getLvsByEmployee($mitarbeiter_uid, $studiensemester_kurzbz, $stg_kz = null)
{
$where = ' AND mitarbeiter_uid = ?';
if ($stg_kz != null)
$where .= " AND lv_studiengang_kz = ?";
$qry = "WITH lvs AS (" . $this->getLVTmp() . $where .")
SELECT lvs.*
FROM lvs";
$params = array($studiensemester_kurzbz, $mitarbeiter_uid);
$params = array($mitarbeiter_uid, $studiensemester_kurzbz);
if (!is_null($stg_kz))
{
$params[] = $stg_kz;
@@ -632,6 +701,36 @@ EOSQL;
return success('Contract successfully updated.');
}
private function _getLeCTE()
{
return $this->_getGruppenCTE() . ",
". $this->_getLektorenCTE() . ",
". $this->_getFachbereichCTE() . ",
". $this->_getTagsCTE() . "
SELECT lehreinheiten.*,
lehreinheiten.lehrform_kurzbz as lv_lehrform_kurzbz,
tbl_lehrveranstaltung.kurzbz as lv_kurzbz,
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
COALESCE(tag_data_agg.tags, '[]'::json) AS tags,
gruppen.gruppen,
mitarbeiter.lektoren,
mitarbeiter.le_planstunden,
mitarbeiter.vorname,
mitarbeiter.nachname,
mitarbeiter.semesterstunden,
fachbereich.bezeichnung as fachbereich,
UPPER(CONCAT(tbl_studiengang.typ,tbl_studiengang.kurzbz)) as studiengang,
semester
FROM lehreinheiten
LEFT JOIN lehre.tbl_lehrveranstaltung ON tbl_lehrveranstaltung.lehrveranstaltung_id = lehreinheiten.lehrfach_id
LEFT JOIN public.tbl_studiengang USING(studiengang_kz)
LEFT JOIN tag_data_agg ON tag_data_agg.lehreinheit_id = lehreinheiten.lehreinheit_id
LEFT JOIN mitarbeiter ON lehreinheiten.lehreinheit_id = mitarbeiter.lehreinheit_id
LEFT JOIN fachbereich ON lehreinheiten.lehreinheit_id = fachbereich.lehreinheit_id
LEFT JOIN gruppen ON lehreinheiten.lehreinheit_id = gruppen.lehreinheit_id
WHERE true";
}
private function _getGruppenCTE()
{
return "gruppen AS (
@@ -259,37 +259,4 @@ class Organisationseinheit_model extends DB_Model
return $this->execReadOnlyQuery($qry, array($oe_kurzbz));
}
/**
* The unit itself plus its sub units, down to a depth.
*
* tiefe 0 returns only the unit named, which is what a caller without a depth wants.
* The depth bound also ends a cycle: a unit whose parent chain leads back to itself
* would otherwise recurse forever.
*
* @param string $oe_kurzbz root of the walk
* @param int $tiefe how many levels below the root to include
* @return object success with array of rows or error
*/
public function getMitUntereinheiten($oe_kurzbz, $tiefe)
{
$query = '
WITH RECURSIVE unter(oe_kurzbz, tiefe) AS (
SELECT oe_kurzbz, 0
FROM public.tbl_organisationseinheit
WHERE oe_kurzbz = ?
UNION ALL
SELECT o.oe_kurzbz, unter.tiefe + 1
FROM public.tbl_organisationseinheit o
JOIN unter ON o.oe_parent_kurzbz = unter.oe_kurzbz
WHERE unter.tiefe < ?
)
SELECT oe_kurzbz, MIN(tiefe) AS tiefe
FROM unter
GROUP BY oe_kurzbz
ORDER BY MIN(tiefe), oe_kurzbz
';
return $this->execReadOnlyQuery($query, [$oe_kurzbz, (int) $tiefe]);
}
}
@@ -292,13 +292,29 @@ class Studiensemester_model extends DB_Model
COALESCE(
(SELECT studiensemester_kurzbz FROM semester_interval_startdate),
(SELECT studiensemester_kurzbz FROM nearest_semester_interval_startdate)
) AS studiensemester_kurzbz
) AS studiensemester_kurzbz,
COALESCE(
(SELECT start FROM semester_interval_startdate),
(SELECT start FROM nearest_semester_interval_startdate)
) as start,
COALESCE(
(SELECT ende FROM semester_interval_startdate),
(SELECT ende FROM nearest_semester_interval_startdate)
) as ende
UNION
SELECT
COALESCE(
(SELECT studiensemester_kurzbz FROM semester_interval_enddate),
(SELECT studiensemester_kurzbz FROM nearest_semester_interval_enddate)
) AS studiensemester_kurzbz
) AS studiensemester_kurzbz,
COALESCE(
(SELECT start FROM semester_interval_enddate),
(SELECT start FROM nearest_semester_interval_enddate)
) as start,
COALESCE(
(SELECT ende FROM semester_interval_enddate),
(SELECT ende FROM nearest_semester_interval_enddate)
) as ende
EOSQL;
+1 -53
View File
@@ -243,61 +243,9 @@ class Person_model extends DB_Model
* @param $uid
* @return array
*/
/**
* Contact data of the named users, for the CMS content components.
*
* The joins follow Benutzerfunktion_model::getBenutzerFunktionenDetailed() and the view
* Cis/Cms/News/Xml/Address/Detailed.php, so a person reads the same everywhere.
*
* foto_sperre is honoured. A person who blocked their photo never gets one on a public
* content page, no matter what the page asks for.
*
* @param array $uids
* @param boolean $mitFoto photos are large, so they are read only on request
* @return object
*/
public function getCmsKontakt($uids, $mitFoto = false)
{
if (empty($uids))
return success(array());
$platzhalter = implode(', ', array_fill(0, count($uids), '?'));
$fotoSpalte = $mitFoto
? 'CASE WHEN p.foto_sperre THEN NULL ELSE p.foto END'
: 'NULL';
$query = '
SELECT
b.uid,
b.alias,
p.vorname,
p.nachname,
p.titelpre,
p.titelpost,
m.telefonklappe,
k.kontakt,
o.planbezeichnung,
' . $fotoSpalte . ' AS foto
FROM public.tbl_benutzer b
JOIN public.tbl_person p ON p.person_id = b.person_id
LEFT JOIN public.tbl_mitarbeiter m ON m.mitarbeiter_uid = b.uid
LEFT JOIN public.tbl_kontakt k
ON k.standort_id = m.standort_id AND k.kontakttyp = ?
LEFT JOIN public.tbl_ort o ON o.ort_kurzbz = m.ort_kurzbz
WHERE b.uid IN (' . $platzhalter . ')
AND b.aktiv
';
$parameter = array('telefon');
foreach ($uids as $uid)
$parameter[] = $uid;
return $this->execReadOnlyQuery($query, $parameter);
}
public function getByUid($uid)
{
$this->addSelect('vorname, nachname, gebdatum, person_id, bpk, matr_nr, foto');
$this->addSelect('vorname, nachname, wahlname, gebdatum, person_id, bpk, matr_nr, foto');
$this->addJoin('tbl_benutzer', 'person_id');
return $this->loadWhere(array('uid' => $uid));
@@ -98,31 +98,6 @@ class Mitarbeiter_model extends DB_Model
return $this->execQuery($qry, $params);
}
/**
* Active staff, names only, for a picker.
*
* getPersonal() returns tbl_person.* including svnr, bpk and
* gebdatum, which is not needed in a CMS endpoint.
*
* @return object
*/
public function getAktivesPersonalNamen()
{
$query = '
SELECT
b.uid,
p.vorname,
p.nachname
FROM public.tbl_benutzer b
JOIN public.tbl_mitarbeiter m ON m.mitarbeiter_uid = b.uid
JOIN public.tbl_person p ON p.person_id = b.person_id
WHERE b.aktiv
ORDER BY LOWER(p.nachname), LOWER(p.vorname)
';
return $this->execReadOnlyQuery($query);
}
/**
* gibt Personen mit Übersicht von Vertragsdaten aus
*
@@ -12,72 +12,4 @@ class Webservicelog_model extends DB_Model
$this->dbTable = 'system.tbl_webservicelog';
$this->pk = 'webservicelog_id';
}
/**
* The most viewed contents since a date, with the title of each one.
* request_id is a varchar, so the join casts the content_id to text. A cast of
* request_id to a number would fail on a row that holds no number.
* The join also drops a log row whose content is deleted.
* @param string|null $since timestamp in the form Y-m-d H:i:s, null for the whole log
* @param string $sprache language of the title
* @param int $limit length of the ranking
* @return stdClass success with array of rows or error
*/
public function getContentClickCounts($since, $sprache, $limit)
{
$params = [$sprache, 'content'];
$sinceCondition = '';
if ($since !== null)
{
$sinceCondition = ' AND w.execute_time >= ?';
$params[] = $since;
}
$params[] = $limit;
$query = '
SELECT c.content_id, COUNT(*) AS hits,
(SELECT titel FROM campus.tbl_contentsprache
WHERE content_id = c.content_id AND sprache = ?
ORDER BY version LIMIT 1) AS titel
FROM system.tbl_webservicelog w
JOIN campus.tbl_content c ON c.content_id::text = w.request_id
WHERE w.webservicetyp_kurzbz = ?' . $sinceCondition . '
GROUP BY c.content_id
ORDER BY COUNT(*) DESC, c.content_id
LIMIT ?
';
return $this->execReadOnlyQuery($query, $params);
}
/**
* Views of one content since a date. A content outside the ranking still needs its
* own number.
* @param int $content_id
* @param string|null $since timestamp in the form Y-m-d H:i:s, null for the whole log
* @return stdClass success with int or error
*/
public function getContentClickCount($content_id, $since)
{
$query = '
SELECT COUNT(*) AS hits
FROM system.tbl_webservicelog
WHERE webservicetyp_kurzbz = ? AND request_id = ?
';
$params = ['content', (string) $content_id];
if ($since !== null)
{
$query .= ' AND execute_time >= ?';
$params[] = $since;
}
$result = $this->execReadOnlyQuery($query, $params);
if (isError($result))
return $result;
return success((int) getData($result)[0]->hits);
}
}
@@ -23,7 +23,6 @@ $includesArray = array(
'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css',
'public/css/Cis4/Contentcomponents.css',
'public/css/Cis4/Studium.css'
),
'customJSs' => array(
-35
View File
@@ -1,35 +0,0 @@
<?php
$includesArray = array(
'title' => 'CMS',
'axios027' => true,
'bootstrap5' => true,
'fontawesome6' => true,
'vue3' => true,
'primevue3' => true,
'tabulator6' => true,
'tinymce5' => true,
'phrases' => array('global', 'ui', 'cms'),
'customCSSs' => [
'public/css/components/verticalsplit.css',
'public/css/components/horizontalsplit.css',
'public/css/Cms.css'
],
'customJSs' => [
'vendor/npm-asset/primevue/tree/tree.min.js',
'vendor/npm-asset/primevue/checkbox/checkbox.min.js',
'vendor/npm-asset/primevue/textarea/textarea.min.js',
'vendor/moment/luxonjs/luxon.min.js'
],
'customJSModules' => ['public/js/apps/Cms.js']
);
$this->load->view('templates/FHC-Header', $includesArray);
?>
<div id="main">
<router-view
cms-root="<?= site_url('cms'); ?>"
auth-uid="<?= getAuthUID(); ?>"
default-language="<?= defined('DEFAULT_LANGUAGE') ? DEFAULT_LANGUAGE : 'German'; ?>"
<?= !empty($clickstats) ? 'clickstats' : ''; ?>
></router-view>
</div>
<?php $this->load->view('templates/FHC-Footer', $includesArray); ?>
@@ -21,6 +21,7 @@ $this->load->view('templates/FHC-Header', $includesArray);
?>
<script type="text/javascript">
document.body.classList.add("cis4");
if (window.self !== window.top)
document.body.classList.add("in-frame");
</script>
+4 -3
View File
@@ -179,7 +179,8 @@ echo ' <title>' . $p->t('mailverteiler/personenImVerteiler') . '</title>
<body id="inhalt">';
$qry = "SELECT
uid, vorname, nachname, geschlecht, fixangestellt
uid, vorname, nachname, wahlname, geschlecht, fixangestellt,
CASE WHEN wahlname IS NOT NULL THEN wahlname ELSE vorname END AS orderVorname
FROM
campus.vw_benutzer
JOIN
@@ -202,7 +203,7 @@ $qry = "SELECT
}
$qry .= " ORDER BY
nachname, vorname";
nachname, orderVorname";
echo '<p>'.$p->t('mailverteiler/anleitungstextMailPersInGroup').'<p>';
echo '<a class="buttongreen" href="#" onclick="SendMail()" id="mailSendButton">' . $p->t('mailverteiler/mailAnAlleSenden') . '</a>';
@@ -236,7 +237,7 @@ if ($result = $db->db_query($qry))
echo '<tr>';
echo ' <td style="text-align: center"><input type="checkbox" class="chkbox" id="checkbox_'.$row->uid.'" name="checkbox['.$row->uid.']"></td>';
echo ' <td>'.$row->nachname.'</td>';
echo ' <td>'.$row->vorname.'</td>';
echo ' <td>'. ($row->wahlname ? $row->wahlname : $row->vorname) .'</td>';
echo ' <td>'.strtoupper($row->geschlecht).'</td>';
echo ' <td>'.($row->fixangestellt != '' ? ($row->fixangestellt == 't' ? 'Ja' : 'Nein') :'-').'</td>';
echo ' <td class="clm_email"><a href="mailto:'.$row->uid.'@' . DOMAIN . '" class="Item">'.$row->uid .'@' . DOMAIN . '</a></td>';
+3 -1
View File
@@ -256,7 +256,9 @@ echo '
'.$p->t('global/anrede').': '.$user->anrede.'<br>
'.$p->t('global/titel').': '.$user->titelpre.' <br>';
if (!$ansicht)
if ($user->wahlname)
echo $p->t('global/vorname').': '.$user->wahlname.' <br>';
else if (!$ansicht)
echo $p->t('global/vorname').': '.$user->vorname.' '.$user->vornamen.'<br>';
else
echo $p->t('global/vorname').': '.$user->vorname.' <br>';
+7 -5
View File
@@ -162,7 +162,8 @@ if (!isset($_GET['kz']))
if (isset($_GET['all']))
{
$qry = "SELECT
vorname, nachname, uid, geschlecht
vorname, nachname, wahlname, uid, geschlecht,
CASE WHEN wahlname IS NOT NULL THEN wahlname ELSE vorname END AS orderVorname
FROM
campus.vw_student
WHERE
@@ -170,12 +171,13 @@ if (isset($_GET['all']))
AND studiengang_kz=".$db->db_add_param($_GET['kz'])."
AND semester<10
AND semester>0
ORDER BY nachname, vorname";
ORDER BY nachname, orderVorname";
}
else
{
$qry = "SELECT
vorname, nachname, uid, geschlecht
vorname, nachname, wahlname, uid, geschlecht,
CASE WHEN wahlname IS NOT NULL THEN wahlname ELSE vorname END AS orderVorname
FROM
campus.vw_student
WHERE
@@ -191,7 +193,7 @@ else
if (isset($_GET['grp']))
$qry.=" AND gruppe=".$db->db_add_param($_GET['grp']);
$qry.= ' ORDER BY nachname, vorname';
$qry.= ' ORDER BY nachname, orderVorname';
}
echo '<p>'.$p->t('mailverteiler/anleitungstextMailPersInGroup').'<p>';
echo '<a class="buttongreen" href="#" onclick="SendMail()" id="mailSendButton">' . $p->t('mailverteiler/mailAnAlleSenden') . '</a>';
@@ -223,7 +225,7 @@ if ($result = $db->db_query($qry))
echo "<tr>";
echo ' <td style="text-align: center"><input type="checkbox" class="chkbox" id="checkbox_'.$row->uid.'" name="checkbox['.$row->uid.']"></td>';
echo " <td>$row->nachname</td>";
echo " <td>$row->vorname</td>";
echo " <td>" . ($row->wahlname ? $row->wahlname : $row->vorname) . "</td>";
echo ' <td>'.strtoupper($row->geschlecht).'</td>';
echo ' <td class="clm_email"><a href="mailto:'.$row->uid.'@' . DOMAIN . '" class="Item">'.$row->uid .'@' . DOMAIN . '</a></td>';
echo "</tr>";
+1 -1
View File
@@ -180,7 +180,7 @@ function searchPerson($searchItems)
echo '<td>',$row->anrede,'</td>';
if ($row->wahlname)
echo '<td>',$row->wahlname, ' ' ,$row->vorname, '</td>';
echo '<td>',$row->wahlname,'</td>';
else
echo '<td>',$row->vorname, '</td>';
-28
View File
@@ -83,40 +83,12 @@ if (! $rechte->isberechtigt('basis/dms', null, 's', null))
return confirm('Möchten Sie das File wirklich löschen?');
}
/*
* CIS4 bridge, ticket 78489.
*
* The TinyMCE 5 editor of the new CMS admin cannot receive a selection through the
* TinyMCE 3 API below, so this window posts the URL back to its opener instead.
*
* message: { typ: 'fhc-dms-selection', url: 'dms.php?id=<dms_id>' }
* target: window.opener, origin checked on both sides
*
* The path is chosen by window.name, which the opener sets and which survives the
* internal navigation of this browser.
*
* Receiving side: public/js/components/Cms/Form/FieldWysiwyg.js
*/
var FileBrowserDialog=
{
init: function(){
},
mySubmit : function (id) {
var URL = "dms.php?id="+id;
// CIS4 path: see the comment block above this function. Do not test for
// tinyMCEPopup, this page loads tiny_mce_popup.js itself and always defines it.
if (window.name === 'DMS_CIS4' && window.opener && !window.opener.closed) {
window.opener.postMessage({ typ: 'fhc-dms-selection', url: URL }, window.location.origin);
window.close();
return;
}
if (typeof tinyMCEPopup === 'undefined') {
alert('The calling window is no longer available.');
return;
}
var win = tinyMCEPopup.getWindowArg("window");
// insert information now
+63 -25
View File
@@ -21,6 +21,7 @@
* Manuela Thamer <[email protected]>
*/
require_once(dirname(__FILE__).'/basis_db.class.php');
require_once(dirname(__FILE__).'/mitarbeiter.class.php');
class entwicklungsteam extends basis_db
{
@@ -43,7 +44,6 @@ class entwicklungsteam extends basis_db
public $ext_id;
public $besqual;
public $studiengang_kz_old;
/**
* Konstruktor
@@ -74,7 +74,7 @@ class entwicklungsteam extends basis_db
$qry = "SELECT * FROM bis.tbl_entwicklungsteam JOIN bis.tbl_besqual USING(besqualcode)
WHERE entwicklungsteam_id=".$this->db_add_param($entwicklungsteam_id);
$qry.=";";
$qry.=";";
if($this->db_query($qry))
{
@@ -147,6 +147,11 @@ class entwicklungsteam extends basis_db
$this->errormsg = 'Es muss ein Mitarbeiter angegeben werden';
return false;
}
if(!$this->checkIfExistingUid($this->mitarbeiter_uid))
{
$this->errormsg = 'Bitte einen Eintrag aus dem Dropdown Personensuche auswählen!';
return false;
}
if($this->studiengang_kz=='')
{
$this->errormsg = 'Es muss ein Studiengang angegeben werden';
@@ -162,6 +167,26 @@ class entwicklungsteam extends basis_db
$this->errormsg = 'Endedatum darf nicht vor Anfangsdatum liegen';
return false;
}
if($this->beginn != '')
{
$beginnParts = explode('-', $this->beginn);
if (!checkdate((int)$beginnParts[1], (int)$beginnParts[2], (int)$beginnParts[0])) {
$this->errormsg = 'Anfangsdatum ist ungültig';
return false;
}
}
if($this->ende != '')
{
$endeParts = explode('-', $this->ende);
if (!checkdate((int)$endeParts[1], (int)$endeParts[2], (int)$endeParts[0]))
{
$this->errormsg = 'Endedatum ist ungültig';
return false;
}
}
return true;
}
@@ -183,30 +208,28 @@ class entwicklungsteam extends basis_db
//Neuen Datensatz anlegen
$qry = "INSERT INTO bis.tbl_entwicklungsteam (mitarbeiter_uid, studiengang_kz, besqualcode, beginn, ende,
updateamum, updatevon, insertamum, insertvon) VALUES (".
$this->db_add_param($this->mitarbeiter_uid).', '.
$this->db_add_param($this->studiengang_kz, FHC_INTEGER).', '.
$this->db_add_param($this->besqualcode, FHC_INTEGER).', '.
$this->db_add_param($this->beginn).', '.
$this->db_add_param($this->ende).', '.
$this->db_add_param($this->updateamum).', '.
$this->db_add_param($this->updatevon).', '.
$this->db_add_param($this->insertamum).', '.
$this->db_add_param($this->insertvon).');';
$this->db_add_param($this->mitarbeiter_uid).', '.
$this->db_add_param($this->studiengang_kz, FHC_INTEGER).', '.
$this->db_add_param($this->besqualcode, FHC_INTEGER).', '.
$this->db_add_param($this->beginn).', '.
$this->db_add_param($this->ende).', '.
$this->db_add_param($this->updateamum).', '.
$this->db_add_param($this->updatevon).', '.
$this->db_add_param($this->insertamum).', '.
$this->db_add_param($this->insertvon).');';
}
else
{
if($this->studiengang_kz_old=='')
$this->studiengang_kz_old = $this->studiengang_kz;
//Bestehenden Datensatz aktualisieren
$qry= "UPDATE bis.tbl_entwicklungsteam SET".
" besqualcode=".$this->db_add_param($this->besqualcode, FHC_INTEGER).",".
" beginn=".$this->db_add_param($this->beginn).",".
" studiengang_kz=".$this->db_add_param($this->studiengang_kz, FHC_INTEGER).",".
" ende=".$this->db_add_param($this->ende).",".
" updateamum=".$this->db_add_param($this->updateamum).",".
" updatevon=".$this->db_add_param($this->updatevon).
" WHERE entwicklungsteam_id=".$this->db_add_param($this->entwicklungsteam_id).";";
" besqualcode=".$this->db_add_param($this->besqualcode, FHC_INTEGER).",".
" beginn=".$this->db_add_param($this->beginn).",".
" studiengang_kz=".$this->db_add_param($this->studiengang_kz, FHC_INTEGER).",".
" ende=".$this->db_add_param($this->ende).",".
" mitarbeiter_uid=".$this->db_add_param($this->mitarbeiter_uid).",".
" updateamum=".$this->db_add_param($this->updateamum).",".
" updatevon=".$this->db_add_param($this->updatevon).
" WHERE entwicklungsteam_id=".$this->db_add_param($this->entwicklungsteam_id).";";
}
if($this->db_query($qry))
@@ -234,7 +257,7 @@ class entwicklungsteam extends basis_db
if($studiengang_kz!=null)
$qry.=" AND studiengang_kz=".$this->db_add_param($studiengang_kz);
$qry.=";";
$qry.=";";
if($this->db_query($qry))
{
@@ -316,7 +339,7 @@ class entwicklungsteam extends basis_db
if ($sort != null)
{
$qry .= " ORDER BY ".$sort;
$qry .= " ORDER BY ".$sort;
}
$qry .= ";";
@@ -364,7 +387,7 @@ class entwicklungsteam extends basis_db
$bismeldung_jahr = $datetime->format('Y');
//laden des Datensatzes
$qry = "SELECT tbl_entwicklungsteam.*, tbl_besqual.*, tbl_studiengang.studiengang_kz, tbl_studiengang.melderelevant
$qry = "SELECT tbl_entwicklungsteam.*, tbl_besqual.*, tbl_studiengang.studiengang_kz, tbl_studiengang.melderelevant
FROM bis.tbl_entwicklungsteam
JOIN bis.tbl_besqual USING(besqualcode)
JOIN public.tbl_studiengang USING(studiengang_kz)
@@ -375,7 +398,7 @@ class entwicklungsteam extends basis_db
if($studiengang_kz!=null)
$qry.=" AND studiengang_kz=".$this->db_add_param($studiengang_kz);
$qry.=";";
$qry.=";";
if($this->db_query($qry))
{
@@ -407,5 +430,20 @@ class entwicklungsteam extends basis_db
return false;
}
}
/*
* Überprüft, ob die übergebene Uid in der Tabelle tbl_mitarbeiter vorhanden ist
* @param String $uid UID des Mitarbeiters
* @return true wenn vorhanden, false wenn nicht
*/
private function checkIfExistingUid($uid)
{
$mitarbeiter = new mitarbeiter();
if(!$mitarbeiter->checkIfExistingUid($uid))
return false;
return true;
}
}
?>
+18
View File
@@ -1995,5 +1995,23 @@ class mitarbeiter extends benutzer
return true;
}
}
/**
* Prueft ob eine existierende ma_uid übergeben wurde
* @param String $mitarbeiter_uid mitarbeiter_uid
* @return bool True wenn mitarbeiter_uid vorhanden sonst false.
*/
public function checkIfExistingUid($mitarbeiter_uid)
{
$qry = "Select 1 from public.tbl_mitarbeiter where mitarbeiter_uid =".$this->db_add_param($mitarbeiter_uid);
if ($result = $this->db_query($qry))
{
if ($this->db_num_rows($result) > 0)
return true;
else
return false;
}
}
}
?>
+25 -7
View File
@@ -7,6 +7,11 @@ html {
font-size: 0.875em;
}
body.compat,
body.compat main#cis-main {
background-color: #fff;
}
.fhc-dark-bg{
background-color: var(--fhc-dark);
color:var(--fhc-light) !important;
@@ -454,8 +459,8 @@ html {
/* desktop */
@media (min-width: 992px) {
body {
display: flex;
body.cis4 {
display: flex;
padding-top: var(--fhc-cis-header-height);
width: calc(100svw - var(--scrollbar-width, 0px));
/* overflow: visible !important; */
@@ -760,7 +765,7 @@ html {
}
.dashboard-item {
overscroll-behavior: none;
overscroll-behavior: auto;
}
.abgabe-zieldatum-border-red,
@@ -840,11 +845,8 @@ html {
color: var(--fhc-light);
}
/* the bootstrap opacity 0.5 reaches 2.9:1 on the blue header, 0.75 reaches 4.5:1 (WCAG 1.4.11) */
#cis-main .modal-header .btn-close{
filter: invert(1);
--bs-btn-close-opacity: 0.75;
--bs-btn-close-hover-opacity: 1;
}
#cis-main .modal-footer {
@@ -856,4 +858,20 @@ html {
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
}
.cis-main-menu {
max-width: 100vw;
}
.cis-main-menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.cis-main-menu ul li {
margin: 0;
padding: 0;
width: 100%;
}
-196
View File
@@ -1,196 +0,0 @@
/* Contentcomponents in CIS4 content.
*
* A contentcomponent renders inside its marker div, below #fhc-cms-content. The
* components themselves carry Bootstrap utility classes only, so without this file they
* read as unstyled Bootstrap.
*
* The class names are the contract with the components. A root class follows the pattern
* fhc-contentcomponent-<name> and is set in the template of the component.
*
* Colours come from the theme. public/css/theme/default.css declares the semantic
* variables under :root.light and :root.dark, never on bare :root, so every var() below
* carries a fallback. The fallback repeats the light value.
*
* Two themes are covered: light and dark. purple takes the light values.
*/
:root {
--fhc-cc-muted: #6c757d;
--fhc-cc-accent: var(--fhc-primary, #006095);
--fhc-cc-surface: var(--fhc-secondary, #f6f6f6);
--fhc-cc-hover: var(--fhc-tertiary, #e9ecef);
--fhc-cc-border: var(--fhc-border, #dee2e6);
--fhc-cc-radius: 0.3rem;
}
/* Two values need a step of their own. #006095 and #6c757d both fall under 3:1 on the
dark background, which is too little even for secondary text and for an icon. */
:root.dark {
--fhc-cc-muted: var(--fhc-white-90, #aeb3b8);
--fhc-cc-accent: var(--fhc-blue-40, #61aed8);
}
/* --------------------------------------------------------------------------------- */
/* The marker */
/* --------------------------------------------------------------------------------- */
/* The marker stays in the DOM and wraps the component. scan.js empties it, Teleport
renders the component root into it. It is therefore the place for the outer spacing. */
.fhc-contentcomponent {
display: block;
margin: 1.25rem 0;
}
.fhc-contentcomponent:first-child {
margin-top: 0;
}
.fhc-contentcomponent:last-child {
margin-bottom: 0;
}
.fhc-contentcomponent .alert {
margin-bottom: 0;
}
/* --------------------------------------------------------------------------------- */
/* Person block */
/* --------------------------------------------------------------------------------- */
/* PersonBlock.js. It carries person-block and every row of oe-personen. */
.fhc-contentcomponent-person {
/* flow-root contains the floated photo. It spares the clearfix element, which the
component does not render. */
display: flow-root;
padding: 0.75rem 1rem;
border: 1px solid var(--fhc-cc-border);
border-radius: var(--fhc-cc-radius);
background-color: var(--fhc-cc-surface);
/* The block carries mb-3. The gap of the two lists below owns the spacing instead,
so the last block does not push the content under it. Bootstrap writes the utility
with !important, so this needs it as well. */
margin-bottom: 0 !important;
}
/* The photo carries its width and its object-fit as an inline style. No rule here can
beat that, so only the placement is set. */
.fhc-contentcomponent-person > img {
float: left;
margin-right: 1rem;
border-radius: var(--fhc-cc-radius);
}
/* The name. It is the first div, with or without a photo above it. */
.fhc-contentcomponent-person > div:first-of-type {
font-size: 1.05em;
font-weight: 600;
line-height: 1.4;
}
/* Function, phone, room and mail. The mail and phone links keep the link colour. */
.fhc-contentcomponent-person > div:not(:first-of-type) {
font-size: 0.95em;
color: var(--fhc-cc-muted);
}
/* --------------------------------------------------------------------------------- */
/* oe-personen, person-block */
/* --------------------------------------------------------------------------------- */
.fhc-contentcomponent-oepersonen,
.fhc-contentcomponent-personkontakt {
display: grid;
gap: 0.75rem;
}
/* --------------------------------------------------------------------------------- */
/* dms-liste, dms-dokumente */
/* --------------------------------------------------------------------------------- */
/* Both render DokumentListe.js: ul.list-unstyled with one li per document. */
.fhc-contentcomponent-dmsliste ul,
.fhc-contentcomponent-dmsdokumente ul {
border: 1px solid var(--fhc-cc-border);
border-radius: var(--fhc-cc-radius);
/* Keeps the first and the last row inside the rounded corners. */
overflow: hidden;
}
.fhc-contentcomponent-dmsliste li,
.fhc-contentcomponent-dmsdokumente li {
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--fhc-cc-border);
/* The row carries mb-2. The line above separates the rows instead. */
margin-bottom: 0 !important;
}
.fhc-contentcomponent-dmsliste li:first-child,
.fhc-contentcomponent-dmsdokumente li:first-child {
border-top: 0;
}
.fhc-contentcomponent-dmsliste li:hover,
.fhc-contentcomponent-dmsdokumente li:hover {
background-color: var(--fhc-cc-surface);
}
/* The file type icon sits inside the link and would take the link colour. A fixed width
lines the names up over every row, whatever the icon is. */
.fhc-contentcomponent-dmsliste li i,
.fhc-contentcomponent-dmsdokumente li i {
width: 1.25rem;
margin-right: 0.25rem;
text-align: center;
color: var(--fhc-cc-accent);
}
.fhc-contentcomponent-dmsliste li a,
.fhc-contentcomponent-dmsdokumente li a {
text-decoration: none;
}
.fhc-contentcomponent-dmsliste li a:hover,
.fhc-contentcomponent-dmsliste li a:focus,
.fhc-contentcomponent-dmsdokumente li a:hover,
.fhc-contentcomponent-dmsdokumente li a:focus {
text-decoration: underline;
}
/* The description is the only block element in the row. It starts below the icon, so it
takes the width of the icon as an indent and lines up with the document name. */
.fhc-contentcomponent-dmsliste li > div,
.fhc-contentcomponent-dmsdokumente li > div {
padding-left: 1.5rem;
}
/* --------------------------------------------------------------------------------- */
/* contentchild-menu */
/* --------------------------------------------------------------------------------- */
/* The successor of the legacy menubox. The accent rule replaces its grey background as
the mark that says: this is a menu, not running text. */
.fhc-contentcomponent-contentchildmenu ul {
padding: 0.5rem;
border-left: 3px solid var(--fhc-cc-accent);
border-radius: 0 var(--fhc-cc-radius) var(--fhc-cc-radius) 0;
background-color: var(--fhc-cc-surface);
}
.fhc-contentcomponent-contentchildmenu li {
/* The entry carries mb-1. The link padding owns the spacing instead. */
margin-bottom: 0 !important;
}
/* A block link makes the whole row the target, not the length of the title. */
.fhc-contentcomponent-contentchildmenu a {
display: block;
padding: 0.35rem 0.5rem;
border-radius: var(--fhc-cc-radius);
text-decoration: none;
}
.fhc-contentcomponent-contentchildmenu a:hover,
.fhc-contentcomponent-contentchildmenu a:focus {
background-color: var(--fhc-cc-hover);
text-decoration: underline;
}
-97
View File
@@ -1,97 +0,0 @@
/* Full-height shell. Without a height chain down to .cms-admin the page itself scrolls
and the tabs leave the screen while paging through the tree. */
html,
body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
overflow: hidden;
}
#main {
flex: 1 1 auto;
min-height: 0;
}
.cms-admin {
min-height: 0;
}
/* The split component only caps its height with max-height, so give it a definite one. */
.cms-admin .horizontalsplit-container {
height: 100%;
}
.cms-admin .horizontalsplitted {
min-width: 0;
overscroll-behavior: contain;
}
.cms-admin-main {
min-width: 0;
overflow: hidden;
}
.cms-admin-tab-content {
min-height: 0;
}
/* Anchor for the overlay that blocks the view during a content switch. */
.cms-admin {
position: relative;
}
.cms-admin-overlay {
position: absolute;
inset: 0;
z-index: 1050;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.6);
cursor: wait;
}
/* content locker to groups get a gold padlock - resembles legacy cms */
.cms-tree-lock {
color: #f0c000;
text-shadow: 0 0 1px #8a6000;
}
/* A frame swallows the mouse events that reach it, so dragging the split handle over the
preview ends the drag. Both frames go through for the length of the drag, so the events
reach the page underneath. Only a drag sets this class, so the preview stays clickable
the rest of the time. */
.cms-preview-dragging iframe {
pointer-events: none;
}
/* The path down to a search hit, and the subtree under it. Both are context, so they step
back and let the hit itself carry the normal weight. */
.cms-tree-ancestor {
font-size: 0.8em;
color: #6c757d;
line-height: 1.4;
}
/* The path is context and keeps the grey, but it leads somewhere, so it underlines on
hover like any other link. */
.cms-tree-ancestor a {
color: inherit;
text-decoration: none;
}
.cms-tree-ancestor a:hover,
.cms-tree-ancestor a:focus {
color: var(--bs-link-color, #0d6efd);
text-decoration: underline;
}
.cms-tree-context > div > a,
.cms-tree-context > div > span {
font-size: 0.85em;
color: #6c757d;
}
+1 -22
View File
@@ -121,9 +121,8 @@
background-color: #000;
}
/* contrast: #777 reaches 4.48:1 on the odd and 4:1 on the even rows (#f2f2f2), #6c6c6c 5.3:1 and 4.7:1 */
.tabulator-row.tabulator-unselectable:not(.tabulator-calcs) {
color: #6c6c6c !important;
color: #777 !important;
}
.tabulator-cell .btn {
@@ -157,23 +156,3 @@
color: #fff;
background: #1d68cd;
}
/* contrast (WCAG 1.4.11): tabulator_simple draws the inactive sort arrow in #bbb (1.9:1) and the
* placeholder in #ccc (1.6:1). #8f8f8f (3.2:1) is close to the grey of the active arrow, so the
* active arrow gets the primary color (6.8:1) */
.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow,
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-bottom-color: #8f8f8f;
}
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="ascending"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-bottom-color: var(--fhc-primary, #006095);
}
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="descending"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-top-color: var(--fhc-primary, #006095);
}
.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents {
color: #6c757d;
}
-81
View File
@@ -1,81 +0,0 @@
/* the panel of js/directives/inViewTooltip.js. place() sets its place, max width and max height
inside the viewport, so neither a scrolling container nor a modal body clips it */
.fhc-in-view-tooltip {
--fhc-in-view-tooltip-progress: var(--fhc-blue-primary, #0d6efd);
position: fixed;
z-index: 1080;
display: flex;
flex-direction: column;
width: max-content;
border: 1px solid var(--bs-border-color, #dee2e6);
border-radius: 0.375rem;
background-color: var(--bs-body-bg, #fff);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
color: var(--bs-body-color, #212529);
font-size: 0.875rem;
font-weight: normal;
line-height: 1.5;
text-align: left;
}
/* the dark primary reaches 2:1 on the dark panel, the highlight 3.9:1 */
html.dark .fhc-in-view-tooltip {
--fhc-in-view-tooltip-progress: var(--fhc-blue-primary-highlight, #6ea8fe);
}
/* the body scrolls, so the progress border stays on the panel edge */
.fhc-in-view-tooltip-body {
min-height: 0;
overflow-y: auto;
padding: 0.75rem 1rem;
white-space: pre-line; /* the text keeps its line breaks */
}
/* the hover timer of a sticky panel: after a pause the border fills up clockwise, then the panel sticks.
The 2 px stroke covers the 1 px border and 1 px inside it */
.fhc-in-view-tooltip-progress {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}
.fhc-in-view-tooltip-progress rect {
fill: none;
stroke: var(--fhc-in-view-tooltip-progress);
stroke-width: 2;
stroke-dasharray: var(--fhc-in-view-tooltip-perimeter);
stroke-dashoffset: var(--fhc-in-view-tooltip-perimeter);
}
.fhc-in-view-tooltip--holding .fhc-in-view-tooltip-progress rect {
animation: fhc-in-view-tooltip-hold var(--fhc-in-view-tooltip-hold-fill) ease-in var(--fhc-in-view-tooltip-hold-delay) forwards;
}
/* a click during the hover timer fills the rest of the border quickly */
.fhc-in-view-tooltip--pinned .fhc-in-view-tooltip-progress rect {
stroke-dashoffset: 0;
transition: stroke-dashoffset 0.15s linear;
}
@keyframes fhc-in-view-tooltip-hold {
to {
stroke-dashoffset: 0;
}
}
/* the ? icon of components/InViewHelp.js takes the color of its context, e.g. white in the blue
modal header. A class like text-muted overrides it */
.fhc-in-view-help-btn,
.fhc-in-view-help-btn:hover,
.fhc-in-view-help-btn:focus-visible,
.btn.fhc-in-view-help-btn:active {
color: inherit;
}
.fhc-in-view-help-btn {
line-height: 1;
text-decoration: none;
}
+3 -154
View File
@@ -188,9 +188,6 @@
--fhc-light: var(--fhc-white-50);
--fhc-cis-menu-lvl-1-color: var(--fhc-white-50);
/* input borders, 3:1 against the background and the tinted card headers (#f8f8f9) */
--fhc-input-border: #868e96;
}
:root.dark {
@@ -236,8 +233,6 @@
--fhc-dark: var(--fhc-black-50);
--fhc-light: var(--fhc-white-70);
/* input borders, 3:1 against the background */
--fhc-input-border: #7c8791;
}
:root.purple {
@@ -318,13 +313,12 @@ html.dark .border-primary {
border-color: var(--fhc-primary) !important;
}
/* fhc danger: --fhc-danger is a light red for text and borders on the dark background. White text
* on it reaches 1.2:1, the backgrounds use the dark red (6.5:1) */
/* fhc danger*/
html.dark .bg-danger,
html.dark .btn-danger,
html.dark .btn-outline-danger {
background-color: var(--fhc-red-60) !important;
border-color: var(--fhc-red-60) !important;
background-color: var(--fhc-danger) !important;
border-color: var(--fhc-danger) !important;
color: white;
}
@@ -333,25 +327,6 @@ html.dark .btn-danger:hover {
filter: brightness(90%);
}
/* the dark primevue theme draws p-button-danger (e.g. FhcAlert.confirmDelete) light pink with black text,
* use the dark red of .btn-danger */
html.dark .p-button.p-button-danger {
color: white;
background: var(--fhc-red-60);
border-color: var(--fhc-red-60);
}
html.dark .p-button.p-button-danger:enabled:hover,
html.dark .p-button.p-button-danger:enabled:active {
color: white;
background: var(--fhc-red-70);
border-color: var(--fhc-red-70);
}
html.dark .p-button.p-button-danger:enabled:focus {
box-shadow: 0 0 0 0.2rem rgba(225, 110, 120, 0.5);
}
html.dark .text-danger {
color: var(--fhc-danger) !important;
}
@@ -360,55 +335,6 @@ html.dark .border-danger {
border-color: var(--fhc-danger) !important;
}
/* bootstrap draws btn-outline-secondary in #6c757d: 4.3:1 on the light dialog footer (#f6f6f6).
* --fhc-gray-30 reaches 5.4:1 there, the white hover text 5.9:1 */
html.light .btn-outline-secondary {
--bs-btn-color: var(--fhc-gray-30);
--bs-btn-border-color: var(--fhc-gray-30);
--bs-btn-hover-bg: var(--fhc-gray-30);
--bs-btn-hover-border-color: var(--fhc-gray-30);
--bs-btn-active-bg: var(--fhc-gray-30);
--bs-btn-active-border-color: var(--fhc-gray-30);
}
/* bootstrap keeps #6c757d for btn-outline-secondary in the dark theme: 2.4:1 on the dark tables and dialogs */
html.dark .btn-outline-secondary {
--bs-btn-color: var(--fhc-white-90);
--bs-btn-border-color: var(--fhc-white-90);
--bs-btn-hover-color: var(--fhc-black-30);
--bs-btn-hover-bg: var(--fhc-white-90);
--bs-btn-hover-border-color: var(--fhc-white-90);
--bs-btn-active-color: var(--fhc-black-30);
--bs-btn-active-bg: var(--fhc-white-90);
--bs-btn-active-border-color: var(--fhc-white-90);
}
/* the primevue multiselect renders the check icon also in unchecked boxes and hides it by color:
* white on white in the light theme, but #151515 on #20262e in the dark theme */
html.dark .p-checkbox .p-checkbox-box:not(.p-highlight) .p-checkbox-icon {
color: transparent;
}
/* FhcAlert.confirm passes bootstrap classes to the primevue buttons. The primevue theme loads after
* bootstrap, its .p-button colors replace the .btn colors: every button blue */
.p-button.btn {
color: var(--bs-btn-color);
background: var(--bs-btn-bg);
border-color: var(--bs-btn-border-color);
}
.p-button.btn:enabled:hover {
color: var(--bs-btn-hover-color);
background: var(--bs-btn-hover-bg);
border-color: var(--bs-btn-hover-border-color);
}
.p-button.btn:enabled:active {
color: var(--bs-btn-active-color);
background: var(--bs-btn-active-bg);
border-color: var(--bs-btn-active-border-color);
}
html.dark .tabulator,
html.dark .tabulator .tabulator-header,
@@ -429,52 +355,6 @@ html.dark .tabulator .tabulator-header .tabulator-col:hover {
background-color: var(--fhc-tertiary) !important;
}
/* the active sort arrow of tabulator_simple (#666) reaches 2:1 on the dark header */
html.dark .tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="ascending"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-bottom-color: var(--fhc-text);
}
html.dark .tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="descending"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-top-color: var(--fhc-text);
}
@media (hover: hover) and (pointer: fine) {
html.dark .tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover {
border-bottom-color: var(--fhc-text);
border-top-color: var(--fhc-text);
}
}
/* the group toggle arrow of tabulator_simple (#666) reaches 2:1 on the dark rows */
html.dark .tabulator .tabulator-row.tabulator-group:not(.tabulator-group-visible) .tabulator-arrow {
border-left-color: var(--fhc-text);
}
html.dark .tabulator .tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow {
border-top-color: var(--fhc-text);
}
/* the footer stays white otherwise: the page buttons get the white button text of the dark color scheme */
html.dark .tabulator .tabulator-footer {
background-color: var(--fhc-tertiary);
border-top-color: var(--fhc-border);
color: var(--fhc-text);
}
html.dark .tabulator .tabulator-footer .tabulator-page {
background: transparent;
border-color: var(--fhc-white-90);
color: var(--fhc-text);
}
html.dark .tabulator .tabulator-footer .tabulator-page.active {
color: var(--fhc-red-30);
}
html.dark .tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents {
color: var(--fhc-white-90);
}
.p-paginator{
background-color: var(--fhc-background) !important;
}
@@ -496,35 +376,4 @@ html.dark .tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-pl
.p-link:focus {
box-shadow: 0 0 0 0.2rem rgba(var(--fhc-primary), 0.5) !important;
}
/* input borders: 3:1 against the background (WCAG 1.4.11), the bootstrap, primevue and datepicker defaults
* reach 1.3 - 1.9:1. Focus and checked states of the libraries stay */
.form-control,
.form-select,
.form-check-input,
.p-inputtext,
.p-inputtext:enabled:hover:not(:focus),
.p-dropdown,
.p-dropdown:not(.p-disabled):hover:not(.p-focus),
.p-multiselect,
.p-multiselect:not(.p-disabled):hover:not(.p-focus),
.p-checkbox .p-checkbox-box,
.p-radiobutton .p-radiobutton-box {
border-color: var(--fhc-input-border, #868e96);
}
/* the datepicker css loads after the theme, html raises the specificity */
html .dp__theme_light,
html .dp__theme_dark {
--dp-border-color: var(--fhc-input-border, #868e96);
--dp-border-color-hover: var(--fhc-border-highlight, #495057);
}
/* the light datepicker (also in the dark cis4 theme) draws the days of other months in #c0c4cc (1.8:1),
* the icons in #959595 (3:1) and the selected day in #f8f5f5 on #1976d2 (4.2:1) */
html .dp__theme_light {
--dp-secondary-color: #6c6c6c;
--dp-icon-color: #6c757d;
--dp-primary-text-color: #fff;
}
+2 -51
View File
@@ -16,8 +16,7 @@
*/
export default {
// preview flag to suppress click log entry. The CMS admin preview passes it.
content(content_id, version=null, sprache=null, sichtbar=null, preview=false) {
content(content_id, version=null, sprache=null, sichtbar=null) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/content',
@@ -25,8 +24,7 @@ export default {
content_id,
...(version ? { version } : {}),
...(sprache ? { sprache } : {}),
...(sichtbar ? { sichtbar } : {}),
...(preview ? { preview: 1 } : {})
...(sichtbar ? { sichtbar } : {})
}
};
},
@@ -56,53 +54,6 @@ export default {
url: '/api/frontend/v1/Cms/getNewsRowCount'
};
},
// Used by the CMS content component "oe-personen".
// tiefe 0 asks about the one unit, higher takes that many levels of sub units with it.
getOePersonen(oe_kurzbz, foto, tiefe) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getOePersonen',
params: {
oe_kurzbz,
...(foto ? { foto: 1 } : {}),
...(tiefe ? { tiefe } : {})
}
};
},
// Used by the CMS content component "person-block".
getPerson(uid, foto) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getPerson',
params: { uid, ...(foto ? { foto: 1 } : {}) }
};
},
// Used by the CMS content component "dms-dokumente".
getDmsDokumente(dms_ids) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getDmsDokumente',
params: { dms_ids }
};
},
// Used by the CMS content component "dms-liste".
// tiefe 0 asks about the one category, higher takes that many levels of sub categories
// with it. The server checks the entitlement of every category it reaches.
getDmsKategorie(kategorie_kurzbz, tiefe) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getDmsKategorie',
params: { kategorie_kurzbz, ...(tiefe ? { tiefe } : {}) }
};
},
// Used by the CMS content component "contentchild-menu".
getContentChilds(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getContentChilds',
params: { content_id, ...(sprache ? { sprache } : {}) }
};
},
getNewsExtra() {
// TODO(chris): seems to be called from nowhere?
return {
-296
View File
@@ -1,296 +0,0 @@
export default {
// --- CmsAdmin ---
getTree(menu, filter) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getTree',
params: { menu, ...(filter ? { filter } : {}) }
};
},
getSubtree(content_id) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getSubtree',
params: { content_id }
};
},
getContent(content_id) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getContent',
params: { content_id }
};
},
getContentsprache(content_id, sprache, version) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getContentsprache',
params: { content_id, sprache, version }
};
},
// Feeds the tree filter: child count, insertamum, updateamum and the own-edit flag.
getTreeMeta() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getTreeMeta'
};
},
// Where each content sits. The tree asks for the whole visible list at once, so the
// hierarchy toggle costs one request and not one per node.
getAncestors(content_ids) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getAncestors',
params: { content_ids: content_ids.join(',') }
};
},
// Views of one content. One counting query, so the tab loads it right away.
// The server gates both click endpoints on the config flag and on the admin right,
// so a caller without both gets an error instead of numbers.
getClickCount(content_id, months) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getClickCount',
params: { content_id, months }
};
},
// The ranking over the whole log. Takes seconds, so the tab asks for it on request.
getClickRanking(months, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getClickRanking',
params: { months, sprache }
};
},
getTemplates() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getTemplates'
};
},
getOrganisationseinheiten() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getOrganisationseinheiten'
};
},
getSprachen() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getSprachen'
};
},
// Fills the DMS category field of the contentcomponent insert dialog.
getDmsKategorien() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getDmsKategorien'
};
},
// Fills the person field of the contentcomponent insert dialog.
getMitarbeiter() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getMitarbeiter'
};
},
// Fills the document field of the contentcomponent insert dialog.
getDmsKategorieDokumente(kategorie_kurzbz) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getDmsKategorieDokumente',
params: { kategorie_kurzbz }
};
},
getUsage(content_id) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getUsage',
params: { content_id }
};
},
postContent(parent_content_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdmin/postContent',
params: { parent_content_id }
};
},
postTranslation(content_id, sprache, version, target_sprache) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdmin/postTranslation',
params: { content_id, sprache, version, target_sprache }
};
},
postVersion(content_id, sprache) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdmin/postVersion',
params: { content_id, sprache }
};
},
putProperties(content_id, sprache, version, template_kurzbz, oe_kurzbz, aktiv, menu_open, beschreibung, titel, sichtbar) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdmin/putProperties',
params: { content_id, sprache, version, template_kurzbz, oe_kurzbz, aktiv, menu_open, beschreibung, titel, sichtbar }
};
},
deleteContent(content_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdmin/deleteContent',
params: { content_id }
};
},
deleteContentsprache(content_id, sprache, version) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdmin/deleteContentsprache',
params: { content_id, sprache, version }
};
},
// --- CmsAdminInhalt ---
getFormSchema(template_kurzbz) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminInhalt/getFormSchema',
params: { template_kurzbz }
};
},
getFormData(content_id, sprache, version) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminInhalt/getFormData',
params: { content_id, sprache, version }
};
},
getLock(content_id, sprache, version) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminInhalt/getLock',
params: { content_id, sprache, version }
};
},
getVersions(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminInhalt/getVersions',
params: { content_id, sprache }
};
},
putFormData(content_id, sprache, version, values) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminInhalt/putFormData',
params: { content_id, sprache, version, values }
};
},
postLock(contentsprache_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminInhalt/postLock',
params: { contentsprache_id }
};
},
deleteLock(contentsprache_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminInhalt/deleteLock',
params: { contentsprache_id }
};
},
deleteLockForced(contentsprache_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminInhalt/deleteLockForced',
params: { contentsprache_id }
};
},
// --- CmsAdminStruktur ---
getGruppen(content_id) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminStruktur/getGruppen',
params: { content_id }
};
},
getAllGruppen() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminStruktur/getAllGruppen'
};
},
getChilds(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminStruktur/getChilds',
params: { content_id, sprache }
};
},
// The hierarchy tab lists them above the children. A content can have several parents.
getParents(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminStruktur/getParents',
params: { content_id, sprache }
};
},
getPossibleChilds(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminStruktur/getPossibleChilds',
params: { content_id, sprache }
};
},
postGruppe(content_id, gruppe_kurzbz) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/postGruppe',
params: { content_id, gruppe_kurzbz }
};
},
deleteGruppe(content_id, gruppe_kurzbz) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/deleteGruppe',
params: { content_id, gruppe_kurzbz }
};
},
postChild(content_id, child_content_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/postChild',
params: { content_id, child_content_id }
};
},
deleteChild(contentchild_id) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/deleteChild',
params: { contentchild_id }
};
},
putChildSort(contentchild_id, direction) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/putChildSort',
params: { contentchild_id, direction }
};
},
// Writes the whole order after a drag and drop. The ids carry the wanted order.
putChildOrder(content_id, contentchild_ids) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/putChildOrder',
params: { content_id, contentchild_ids }
};
}
};
+11 -1
View File
@@ -44,5 +44,15 @@ export default {
studiensemester_kurzbz
}
};
}
},
getContainingOrNearestByDateRange(from, to) {
return {
method: 'get',
url: 'api/frontend/v1/organisation/studiensemester/getContainingOrNearestByDateRange',
params: {
from,
to,
}
};
},
};
+1 -6
View File
@@ -150,8 +150,7 @@ const router = VueRouter.createRouter({
}
},
{
// optional version for cms admin preview
path: `/CisVue/Cms/Content/:content_id/:version?`,
path: `/CisVue/Cms/Content/:content_id`,
name: 'Content',
component: CmsContent,
props: true
@@ -351,9 +350,5 @@ app.directive('contrast', contrast);
app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
// dont log cms admin preview
if (to.query.preview === '1')
return;
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
});
-44
View File
@@ -1,44 +0,0 @@
import CmsAdmin from "../components/Cms/CmsAdmin.js";
import PluginsPhrasen from "../plugins/Phrasen.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '')
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory('/' + ciPath),
routes: [
{
name: 'index',
path: '/cms',
component: CmsAdmin,
children: [
{
// Only content_id is required. CmsAdmin fills the missing parts
// from the content itself and rewrites the address.
name: 'content',
path: 'content/:content_id/:sprache?/:version?/:tab?',
component: CmsAdmin
}
]
},
{ path: '/:pathMatch(.*)*', redirect: { name: 'index' } }
]
});
FhcApps.router.makeExtendable(router);
const app = Vue.createApp({
name: 'CmsApp'
});
FhcApps.makeExtendable(app);
app
.use(router)
.use(primevue.config.default, {
zIndex: {
overlay: 1100
}
})
.use(PluginsPhrasen)
.mount('#main');
+2
View File
@@ -17,6 +17,8 @@ const router = VueRouter.createRouter({
component: LVVerwaltung,
children: [
{ name: 'emp', path: 'emp/:emp/:treemenu(.*)*', component: LVVerwaltung },
{ name: 'le', path: 'le/:le', component: LVVerwaltung },
{ name: 'lv', path: 'lv/:lv', component: LVVerwaltung },
{ name: 'treemenu', path: ':treemenu(.*)*', component: LVVerwaltung }
]
},
+9 -2
View File
@@ -73,7 +73,6 @@ export default {
}
},
computed: {
// opacity 0.5 reaches 2.4:1 on the blue cis4 modal header, 0.75 reaches 3.6:1 (WCAG 1.4.11)
getExpandButtonStyles() {
const hovered = this.expandBtnHovered;
const focused = this.expandBtnFocused;
@@ -86,11 +85,12 @@ export default {
border: 0;
background: transparent;
font-size: 1em;
opacity: 0.5;
color: inherit;
cursor: pointer;
line-height: 1;
transition: opacity 0.15s ease;
opacity: ${focused || hovered ? '1' : '0.75'};
opacity: ${focused ? '1' : hovered ? '0.75' : '0.5'};
outline: ${focused ? '1px solid currentColor' : 'none'};
outline-offset: 2px;`
}
@@ -103,6 +103,13 @@ export default {
keyboard: this.keyboard
});
},
//to avoid pagelocks if more than one instance
beforeUnmount() {
if (this.modal) {
this.modal.dispose();
this.modal = null;
}
},
popup(body, options, title, footer) {
const BsModal = this,
slots = {};
+2 -10
View File
@@ -38,9 +38,7 @@ export default {
methods: {
fetchContent(){
this.$api
.call(ApiCms.content(
this.content_id_internal, this.version, this.sprache, this.sichtbar, this.preview
))
.call(ApiCms.content(this.content_id_internal, this.version, this.sprache, this.sichtbar))
.then(res => {
this.$nextTick(function() {
this.content = res.data.content;
@@ -59,14 +57,8 @@ export default {
}
},
computed: {
// The viewer language, unless the address names one. The CMS admin previews a
// chosen language, which is not always the language of the editor interface.
sprache(){
return this.$route?.query?.sprache || this.$p.user_language.value;
},
// A preview renders the content without counting a view.
preview(){
return this.$route?.query?.preview === '1';
return this.$p.user_language.value;
},
computeContentType: function () {
switch (this.content_type) {
@@ -1,5 +1,4 @@
import { replaceRelativeLegacyLink } from "../../../../helpers/LegacyLinkReplaceHelper.js"
import ContentcomponentHost from "../../../Contentcomponents/ContentcomponentHost.js";
export default {
name: "GeneralComponent",
props:{
@@ -7,13 +6,7 @@ export default {
type:String,
required:true,
},
content_id:{
type: [Number, String],
},
},
components: {
ContentcomponentHost
},
methods: {
sanitizeLegacyTables(table) {
@@ -131,7 +124,7 @@ export default {
template: /*html*/ `
<!-- div that contains the content -->
<div v-if="content" class="container" style="max-width: 100%;"><div class="row"><div class="col">
<contentcomponent-host :content="content" :content-id="content_id"></contentcomponent-host>
<div v-html="content" ></div>
</div></div></div>
<p v-else>Content was not found</p>
`,
@@ -134,7 +134,6 @@ export default {
template: /*html*/ `
<!-- div that contains the content -->
<div v-if="content" class="container" style="max-width: 100%;"><div class="row"><div class="col">
<!-- No contentcomponent-host on purpose: news contents stay static by convention. -->
<div v-html="content" ></div>
</div></div></div>
<p v-else>Content was not found</p>
@@ -1,4 +1,4 @@
import ContentcomponentHost from "../../../Contentcomponents/ContentcomponentHost.js";
export default {
name: "RaumComponent",
data() {
@@ -15,9 +15,6 @@ export default {
type: [Number, String],
}
},
components: {
ContentcomponentHost
},
methods: {
sanitizeLegacyTables(table) {
@@ -138,7 +135,7 @@ export default {
<!-- div that contains the content -->
<!-- TODO: test with more img content from cms-->
<div v-if="imgContent"><img v-bind="imgContent"/></div>
<contentcomponent-host v-else-if="content" :content="content" :content-id="content_id"></contentcomponent-host>
<div v-html="content" v-else-if="content" ></div>
<p v-else>Content was not found</p>
`,
};
-1
View File
@@ -117,7 +117,6 @@ export default {
></pagination>
<div class="container-fluid mt-4">
<div class="row">
<!-- No contentcomponent-host on purpose: news stays static by convention. -->
<div class="col" v-html="content">
</div>
<div class="col-auto">
+6
View File
@@ -16,9 +16,15 @@ export default {
return this.mode + '#' + this.path + '#' + this.query_string;
}
},
beforeMount: function() {
document.body.classList.add('compat');
},
mounted: function() {
this.srcUrl = this.buildSrcUrl();
},
beforeUnmount: function() {
document.body.classList.remove('compat');
},
watch: {
propsWatchHelper: function() {
if(this.lastLoadediFrameURL === '') {
+30 -19
View File
@@ -1,6 +1,7 @@
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiStudienSemester from '../../../api/factory/studiensemester.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
@@ -16,13 +17,12 @@ export default {
},
data() {
return {
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
startSemester: null,
endSemester: null,
};
},
inject: ["isMobile"],
@@ -48,9 +48,12 @@ export default {
return this.propsViewData?.mode;
},
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
const startDate = this.startSemester?.start;
const endDate = this.endSemester ? this.endSemester.ende : this.startSemester?.ende;
if (!startDate || !endDate || !this.uid)
return false;
let type = false;
type = this.isStudent ? 'student' : type;
type = this.isMitarbeiter ? 'lektor' : type;
@@ -61,10 +64,10 @@ export default {
const opts = { zone: this.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.fromISO(startDate, opts)
.toUnixInteger();
const ende = luxon.DateTime
.fromISO(this.studiensemester_ende, opts)
.fromISO(endDate, opts)
.toUnixInteger();
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
@@ -109,16 +112,21 @@ export default {
}
});
},
updateRange(rangeInterval) {
this.$api
.call(ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf('week').toISODate()
))
.then(res => {
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
async updateRange(rangeInterval) {
const semesterResponse = await this.$api.call(
ApiStudienSemester.getContainingOrNearestByDateRange(
rangeInterval.s.toISO().slice(0, 10),
rangeInterval.e.toISO().slice(0, 10),
),
);
this.startSemester =
semesterResponse.data.length
? semesterResponse.data[0]
: null;
this.endSemester =
semesterResponse.data.length > 1
? semesterResponse.data[1]
: null;
},
getPromiseFunc(start, end) {
return [
@@ -142,8 +150,11 @@ export default {
<div class="cis-lvplan-personal d-flex flex-column h-100">
<h2>
{{ $p.t('lehre/stundenplan') }}
<span v-if="studiensemester_kurzbz" class="ps-3">
{{ studiensemester_kurzbz }}
<span v-if="startSemester?.studiensemester_kurzbz">
{{ startSemester.studiensemester_kurzbz }}
<span v-if="endSemester?.studiensemester_kurzbz">
{{ "- " + endSemester.studiensemester_kurzbz }}
</span>
</span>
</h2>
<hr>
+33 -26
View File
@@ -5,6 +5,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from "../.././../api/factory/lvPlan.js";
import ApiOtherLvPlan from "../.././../api/factory/otherLvPlan.js";
import ApiAuthinfo from "../../../api/factory/authinfo.js";
import ApiStudienSemester from '../../../api/factory/studiensemester.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
@@ -22,9 +23,6 @@ export default {
data() {
return {
localProps: {},
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
isOtherPersonMitarbeiter: false,
isOtherPersonStudent: false,
currentStgBezeichnung: null,
@@ -36,6 +34,8 @@ export default {
photo: "",
},
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
startSemester: null,
endSemester: null,
};
},
inject: ["isMobile"],
@@ -67,11 +67,10 @@ export default {
// do not show download links in otherLvPlan Mode
return false;
if (
!this.studiensemester_start ||
!this.studiensemester_ende ||
!this.propsViewData.otherUid
)
const startDate = this.startSemester?.start;
const endDate = this.endSemester ? this.endSemester.ende : this.startSemester?.ende;
if (!startDate || !endDate || !this.uid)
return false;
const type = this.isOtherPersonStudent
@@ -84,11 +83,11 @@ export default {
const opts = { zone: this.timezone };
const start = luxon.DateTime.fromISO(
this.studiensemester_start,
startDate,
opts,
).toUnixInteger();
const ende = luxon.DateTime.fromISO(
this.studiensemester_ende,
endDate,
opts,
).toUnixInteger();
@@ -166,19 +165,21 @@ export default {
},
});
},
updateRange(rangeInterval) {
this.$api
.call(
ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf("week").toISODate(),
),
)
.then((res) => {
this.studiensemester_kurzbz =
res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
async updateRange(rangeInterval) {
const semesterResponse = await this.$api.call(
ApiStudienSemester.getContainingOrNearestByDateRange(
rangeInterval.s.toISO().slice(0, 10),
rangeInterval.e.toISO().slice(0, 10),
),
);
this.startSemester =
semesterResponse.data.length
? semesterResponse.data[0]
: null;
this.endSemester =
semesterResponse.data.length > 1
? semesterResponse.data[1]
: null;
},
getPromiseFunc(start, end) {
return [
@@ -212,7 +213,7 @@ export default {
this.isOtherPersonStudent = !!viewData?.user_data?.is_student;
let fullNameWithTitleFragments = [
viewData?.user_data?.titel,
viewData?.user_data?.vorname,
viewData?.user_data?.wahlname ?? viewData?.user_data?.vorname,
viewData?.user_data?.nachname,
viewData?.user_data?.postnomen,
];
@@ -250,8 +251,14 @@ export default {
<div class="d-flex flex-column h-100">
<h2>
<div class="d-flex flex-row justify-content-between align-items-center">
<span>
{{ $p.t('lehre/stundenplan') + (studiensemester_kurzbz ? " " + studiensemester_kurzbz : "") }}
<span>
{{ $p.t('lehre/stundenplan') }}
<span v-if="startSemester?.studiensemester_kurzbz">
{{ startSemester.studiensemester_kurzbz }}
<span v-if="endSemester?.studiensemester_kurzbz">
{{ "- " + endSemester.studiensemester_kurzbz }}
</span>
</span>
</span>
<div @click="this.$router.push({name: 'ProfilView', params: {uid: propsViewData.otherUid}})" type="button" class="d-flex flex-row align-items-center gap-3">
<span v-if="otherPersonData.fullNameWithTitle?.length">
+1 -1
View File
@@ -186,7 +186,7 @@ export default {
</div>
</div>
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<nav id="nav-main" class="cis-main-menu offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<div id="nav-main-sticky">
<div class="d-flex flex-row h-100">
<div class="offcanvas-body p-0">
+5 -1
View File
@@ -162,6 +162,7 @@ export default {
<div class="btn-group w-100">
<a :target="target"
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
draggable="false"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
@@ -180,12 +181,15 @@ export default {
</div>
<ul ref="children"
class="nav w-100 collapse">
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
<li>
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
</li>
</ul>
</template>
<a v-else
:href="entry.url"
:target="target"
draggable="false"
:class="{
'btn btn-default rounded-0 w-100 text-start': true,
['btn-level-' + level]: true,
@@ -73,7 +73,7 @@ export default {
// check if the lv has lvplan entries for this studiensemester
if (this.studiensemester && this.event) {
return this.$fhcApi.factory.studium.getLvPlanForStudiensemester(this.studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id)
.then(data => data.data)
.then(data => data.data?.retval)
.then(res => {
if (Array.isArray(res) && res.length > 0) {
this.hasLvPlanEintraege = true;
@@ -114,4 +114,4 @@ export default {
`,
};
};
@@ -274,7 +274,7 @@ export default {
}
return {
Vorname: this.data.vorname,
Vorname: this.data.wahlname ?? this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
@@ -160,7 +160,7 @@ export default {
}
return {
Vorname: this.data.vorname,
Vorname: this.data.wahlname ?? this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
@@ -204,7 +204,7 @@ export default {
}
return {
Vorname: this.data.vorname,
Vorname: this.data.wahlname ?? this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
@@ -33,7 +33,7 @@ export default {
}
return {
Vorname: this.data.vorname,
Vorname: this.data.wahlname ?? this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
@@ -1,5 +1,6 @@
export default {
name: 'Cis-Renderer-Feiertag-CalendarEvent',
props: {
event: {
type: Object,
@@ -1,6 +1,7 @@
import { formatDate } from "../../../../helpers/DateHelpers.js"
export default {
name: 'Cis-Renderer-Feiertag-ModalContent',
props:{
event: {
type: Object,
@@ -1,4 +1,5 @@
export default {
name: 'Cis-Renderer-Feiertag-ModalTitle',
props:{
event: {
type: Object,
@@ -1,4 +1,5 @@
export default {
name: 'Cis-Renderer-Lehreinheit-CalendarEvent',
props:{
event: {
type: Object,
@@ -66,24 +67,6 @@ export default {
return "d-none d-xl-grid";
}
},
// link into the FHC-Core-Anwesenheiten tool
// digi_anw_data is attached per event by the extension's `extendStundenplanData` listener
anwLink() {
const d = this.event && this.event.digi_anw_data;
if (!d || !d.stg_kz || !d.lv_id || !d.sem_kurzbz)
return null;
const base = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/extensions/FHC-Core-Anwesenheiten/';
return base
+ `?stg_kz=${encodeURIComponent(d.stg_kz)}`
+ `&sem=${encodeURIComponent(d.sem ?? '')}`
+ `&lvid=${encodeURIComponent(d.lv_id)}`
+ `&sem_kurzbz=${encodeURIComponent(d.sem_kurzbz)}`;
},
anwLinkTitle() {
return this.$p.t('global/anwKontrolleOeffnen');
},
},
template: /*html*/`
<div
@@ -114,21 +97,6 @@ export default {
... +{{ event.lektor.length - 3 }}
</span>
</div>
<a
v-if="anwLink"
:href="anwLink"
target="_blank"
rel="noopener"
class="d-flex align-items-center flex-shrink-0"
style="margin-right: 4px"
:title="anwLinkTitle"
:aria-label="anwLinkTitle"
draggable="false"
@click.stop
@mousedown.stop
>
<i class="fa fa-arrow-up-right-from-square fs-5" aria-hidden="true"></i>
</a>
</div>
`,
}
@@ -5,6 +5,7 @@ import ApiLvPlan from '../../../../api/factory/lvPlan.js';
import ApiAddons from '../../../../api/factory/addons.js';
export default {
name: 'Cis-Renderer-Lehreinheit-ModalContent',
components:{
LvMenu,
},
@@ -50,19 +51,15 @@ export default {
}
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
},
// Deep link into the attendance tool; only present when the anw extension wrote into digi_anw_data
anwLink: function () {
const d = this.event && this.event.digi_anw_data;
if (!d || !d.stg_kz || !d.lv_id || !d.sem_kurzbz)
return null;
const base = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/extensions/FHC-Core-Anwesenheiten/';
return base
+ `?stg_kz=${encodeURIComponent(d.stg_kz)}`
+ `&sem=${encodeURIComponent(d.sem ?? '')}`
+ `&lvid=${encodeURIComponent(d.lv_id)}`
+ `&sem_kurzbz=${encodeURIComponent(d.sem_kurzbz)}`;
showLvMenu: function() {
const hideOnRoutes = [
'OtherLvPlan',
'StgOrgLvPlan'
];
if(hideOnRoutes.includes(this.$route.name)) {
return false;
}
return true;
}
},
watch: {
@@ -174,18 +171,8 @@ export default {
}}</th>
<td>{{event.organisationseinheit}}</td>
</tr>
<tr v-if="anwLink">
<th>{{
$p.t('global','anwKontrolle')?
$p.t('global','anwKontrolle')+':'
:''
}}</th>
<td>
<a :href="anwLink" target="_blank" rel="noopener" :aria-label="$p.t('global','anwKontrolleOeffnen')" :title="$p.t('global','anwKontrolleOeffnen')"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
</td>
</tr>
</tbody>
</table>
<lv-menu v-if="lvMenu.length && $route.name === 'MyLvPlan'" :containerStyles="['p-0']" :rowStyles="['m-0']" :menu="lvMenu" />
<lv-menu v-if="lvMenu.length && showLvMenu" :containerStyles="['p-0']" :rowStyles="['m-0']" :menu="lvMenu" />
</div>`,
}
@@ -1,4 +1,5 @@
export default {
name: 'Cis-Renderer-Lehreinheit-ModalTitle',
props:{
event: {
type: Object,
@@ -1,4 +1,5 @@
export default {
name: 'Cis-Renderer-Reservierung-CalendarEvent',
props: {
event: {
type: Object,
@@ -2,6 +2,7 @@ import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
import LvMenu from "../../Mylv/LvMenu.js";
export default {
name: 'Cis-Renderer-Reservierung-ModalContent',
props:{
event: {
type: Object,
@@ -1,4 +1,5 @@
export default {
name: 'Cis-Renderer-Reservierung-ModalTitle',
props:{
event: {
type: Object,
-303
View File
@@ -1,303 +0,0 @@
import ApiCmsAdmin from '../../api/factory/cmsadmin.js';
import ContentTree from './Tree/ContentTree.js';
import ContentHeader from './Header/ContentHeader.js';
import Properties from './Tabs/Properties.js';
import ContentTab from './Tabs/Content.js';
import Permissions from './Tabs/Permissions.js';
import Hierarchy from './Tabs/Hierarchy.js';
import History from './Tabs/History.js';
import Delete from './Tabs/Delete.js';
import Clickstats from './Tabs/Clickstats.js';
import HorizontalSplit from '../horizontalsplit/horizontalsplit.js';
const TAB_COMPONENTS = {
properties: Properties,
content: ContentTab,
permissions: Permissions,
hierarchy: Hierarchy,
history: History,
delete: Delete
};
const TABS = [
{ key: 'properties', phrase: 'cms/tabEigenschaften' },
{ key: 'content', phrase: 'cms/tabInhalt' },
{ key: 'permissions', phrase: 'cms/tabRechte' },
{ key: 'hierarchy', phrase: 'cms/tabHierarchie' },
{ key: 'history', phrase: 'cms/tabHistory' },
{ key: 'delete', phrase: 'cms/tabLoeschen' }
];
// The view renders the clickstats attribute only for an admin, and only with the config
// flag set.
const CLICKSTATS_TAB = { key: 'clickstats', phrase: 'cms/tabKlickstatistik' };
export default {
name: 'CmsAdmin',
components: {
'cms-tree': ContentTree,
'cms-content-header': ContentHeader,
'horizontal-split': HorizontalSplit,
...TAB_COMPONENTS
},
props: {
cmsRoot: String,
authUid: String,
defaultLanguage: { type: String, default: 'German' },
clickstats: { type: Boolean, default: false }
},
data() {
return {
contentId: null,
sprache: null,
version: null,
tab: 'properties',
contentInfo: null,
pending: 0
};
},
computed: {
tabs() {
return this.clickstats ? TABS.concat([CLICKSTATS_TAB]) : TABS;
},
tabComponents() {
return this.clickstats
? { ...TAB_COMPONENTS, clickstats: Clickstats }
: TAB_COMPONENTS;
},
activeTabComponent() {
return this.tabComponents[this.tab] || null;
},
busy() {
return this.pending > 0;
}
},
watch: {
'$route.params': {
handler(params) {
const id = params.content_id ? Number(params.content_id) : null;
const sprache = params.sprache || null;
const version = params.version ? Number(params.version) : null;
// unknown tab fallback
const tab = this.tabComponents[params.tab] ? params.tab : 'properties';
if (id !== this.contentId || sprache !== this.sprache
|| version !== this.version || tab !== this.tab) {
this.contentId = id;
this.sprache = sprache;
this.version = version;
this.tab = tab;
if (id === null) return;
if (this.contentInfo?.content_id !== id) {
this.loadContentInfo().then(() => { this.applyRouteDefaults(); });
} else {
this.applyRouteDefaults();
}
}
},
immediate: true
}
},
methods: {
loadContentInfo() {
if (this.contentId === null) {
this.contentInfo = null;
return Promise.resolve();
}
this.pending++;
return this.$api
.call(ApiCmsAdmin.getContent(this.contentId))
.then(result => {
this.contentInfo = result.data;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.pending--; });
},
// Language, version and tab are optional in the address. Fill in what the address
// leaves out, and replace a value that the content does not have. Then write the
// complete address back, so a reload and a copied link both work.
applyRouteDefaults() {
if (this.contentId === null || !this.contentInfo) return;
const languages = this.contentInfo.languages || [];
const versions = this.contentInfo.versions || {};
let lang = this.sprache;
if (lang === null || !languages.includes(lang)) {
lang = languages.includes(this.defaultLanguage)
? this.defaultLanguage
: languages[0] || null;
}
const langVersions = (lang && versions[lang]) ? versions[lang] : [];
let ver = this.version;
if (ver === null || !langVersions.includes(ver)) {
ver = langVersions.length ? Math.max(...langVersions) : null;
}
if (lang === this.sprache && ver === this.version) return;
this.sprache = lang;
this.version = ver;
// Replace, or the short address stays in the history and the back button
// walks through both forms of the same page.
this.pushRoute(true);
},
selectContent(content_id) {
if (content_id === null) {
this.contentId = null;
this.sprache = null;
this.version = null;
this.tab = 'properties';
this.contentInfo = null;
this.$router.push({ name: 'index' });
return;
}
// Set id, language and version in one flush, or the tabs query the new content
// with the version of the old one.
this.pending++;
this.$api
.call(ApiCmsAdmin.getContent(content_id))
.then(result => {
this.contentId = content_id;
this.contentInfo = result.data;
this.preselectLanguageVersion();
this.pushRoute();
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.pending--; });
},
selectTab(tab) {
this.tab = tab;
this.pushRoute();
},
selectLanguageVersion(sprache, version) {
this.sprache = sprache;
this.version = version;
this.pushRoute();
},
// Three grades of tree update. Pick the cheapest one that stays correct.
// A full reload drops the expanded branches and refetches the filter meta,
// so keep it for changes that move nodes between branches.
reloadTree() {
if (this.$refs.tree) this.$refs.tree.reload();
},
refreshTreeNode(contentId) {
if (this.$refs.tree) this.$refs.tree.refreshNode(contentId);
},
patchTreeNode(payload) {
if (this.$refs.tree) this.$refs.tree.patchNode(payload.content_id, payload.patch);
},
preselectLanguageVersion() {
if (!this.contentInfo) return;
const languages = this.contentInfo.languages || [];
const versions = this.contentInfo.versions || {};
let lang = languages.includes(this.defaultLanguage)
? this.defaultLanguage
: languages[0] || null;
let ver = null;
if (lang && versions[lang] && versions[lang].length) {
ver = Math.max(...versions[lang]);
}
this.sprache = lang;
this.version = ver;
},
pushRoute(replace = false) {
if (this.contentId === null) {
this.$router.push({ name: 'index' });
return;
}
// The optional parts are positional. Stop at the first missing one, or the
// address carries an empty segment.
const params = { content_id: this.contentId };
if (this.sprache !== null) {
params.sprache = this.sprache;
if (this.version !== null) {
params.version = this.version;
params.tab = this.tab;
}
}
const target = { name: 'content', params };
if (replace) {
this.$router.replace(target);
} else {
this.$router.push(target);
}
}
},
template: `
<div class="cms-admin h-100">
<horizontal-split :default-ratio="[25, 75]">
<template #left>
<cms-tree
ref="tree"
:active-content-id="contentId"
@select-content="selectContent"
></cms-tree>
</template>
<template #right>
<div class="cms-admin-main h-100 d-flex flex-column" v-if="contentId !== null">
<cms-content-header
:content-id="contentId"
:sprache="sprache"
:version="version"
:contentInfo="contentInfo"
@select-language-version="selectLanguageVersion"
@reload-content-info="loadContentInfo"
></cms-content-header>
<ul class="nav nav-tabs px-3">
<li class="nav-item" v-for="t in tabs" :key="t.key">
<a class="nav-link"
:class="{ active: tab === t.key }"
href="#"
@click.prevent="selectTab(t.key)"
>{{ $p.t(t.phrase) }}</a>
</li>
</ul>
<div class="cms-admin-tab-content flex-grow-1 overflow-auto">
<component
:is="activeTabComponent"
:content-id="contentId"
:sprache="sprache"
:version="version"
:contentInfo="contentInfo"
@reload-content-info="loadContentInfo"
@reload-tree="reloadTree"
@refresh-tree-node="refreshTreeNode"
@patch-tree-node="patchTreeNode"
@select-content="selectContent"
></component>
</div>
</div>
<div class="cms-admin-main h-100 d-flex align-items-center justify-content-center text-muted" v-else>
{{ $p.t('cms/keinEintragGewaehlt') }}
</div>
</template>
</horizontal-split>
<div v-if="busy" class="cms-admin-overlay">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{ $p.t('ui/loading') }}</span>
</div>
</div>
</div>
`
};
@@ -1,381 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
import {
catalog,
getDescriptor,
CONTENTCOMPONENT_CLASS,
CONTENTCOMPONENT_ATTR
} from '../../Contentcomponents/catalog.js';
/**
* The insert and edit dialogs for a contentcomponent marker.
*
* Lives apart from FieldWysiwyg so that file keeps to the editor itself. Everything that
* knows about the catalog, the marker markup and the dialogs is here.
*/
// Helper fields a picker adds to a dialog. They must not collide with a prop name, and
// markerHtml ignores them because it walks the catalog, not the dialog data.
const FELD_KATEGORIE = '__kategorie';
const FELD_DOKUMENT = '__dokument';
// Filled in createContentcomponentDialog. The factory is no Vue component, so the
// phrases plugin is handed in instead of read from an instance.
let t = key => key;
// A function, not a constant: the text is translated when the dialog is built.
const leereAuswahl = () => ({ value: '', text: t('ccAuswaehlen') });
// A prop source fills a dialog field with a list from the server. The key is the
// "source" value in the catalog. A prop without a known source gets a plain text field.
const SOURCES = {
oe: {
request: () => ApiCmsAdmin.getOrganisationseinheiten(),
item: oe => ({
value: oe.oe_kurzbz,
text: (oe.bezeichnung || oe.oe_kurzbz)
+ (oe.aktiv ? '' : ' (' + t('inaktiv') + ')')
})
},
dmskategorie: {
request: () => ApiCmsAdmin.getDmsKategorien(),
item: kat => ({
value: kat.kategorie_kurzbz,
text: (kat.bezeichnung || kat.kategorie_kurzbz) + ' (' + kat.anzahl + ')'
})
},
// Surname first
person: {
request: () => ApiCmsAdmin.getMitarbeiter(),
item: person => ({
value: person.uid,
text: person.nachname + ' ' + person.vorname + ' (' + person.uid + ')'
})
}
};
function escapeAttr(value)
{
return String(value)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Builds the marker. It stays empty: CIS4 renders the component into it, and every other
// renderer shows nothing. NOTE: if a marker disappears on save, check the TinyMCE
// serializer first. noneditable keeps the node, but an empty block is the exposed part.
function markerHtml(name, descriptor, data)
{
let attrs = '';
for (const propName of Object.keys(descriptor.props || {}))
{
const value = data[propName];
// false is an unchecked box. The absent attribute already means false.
if (value === '' || value === false || value === undefined || value === null)
continue;
attrs += ' data-' + propName + '="' + escapeAttr(value) + '"';
}
return '<div class="' + CONTENTCOMPONENT_CLASS + '" '
+ CONTENTCOMPONENT_ATTR + '="' + name + '"' + attrs + '></div>';
}
// Reads an existing marker back into dialog values.
function readMarker(node)
{
const name = node.getAttribute(CONTENTCOMPONENT_ATTR);
const descriptor = getDescriptor(name);
if (!descriptor)
return null;
const values = {};
for (const propName of Object.keys(descriptor.props || {}))
values[propName] = node.getAttribute('data-' + propName) || '';
return { name: name, values: values };
}
/**
* @param {object} options { api: the $api plugin, onChanged: called after a write }
*/
export function createContentcomponentDialog(options)
{
const api = options.api;
const onChanged = options.onChanged || function () {};
t = (key, args) => options.p.t('cms/' + key, args);
const sourceItems = {};
const sourceRequests = {};
// These lists are large and rarely needed. Load each one once, on demand.
function loadSource(key)
{
if (sourceRequests[key])
return sourceRequests[key];
const source = SOURCES[key];
sourceRequests[key] = api
.call(source.request())
.then(res => { sourceItems[key] = (res.data || []).map(source.item); })
.catch(() => { sourceItems[key] = []; });
return sourceRequests[key];
}
// Step one: pick the component. A second dialog then asks for its properties.
// Two dialogs avoid the redial logic a single changing dialog would need.
function openPicker(editor)
{
if (!catalog.length)
return;
editor.windowManager.open({
title: t('ccDialogTitel'),
initialData: { component: catalog[0].name },
body: {
type: 'panel',
items: [{
type: 'selectbox',
name: 'component',
label: t('ccContentcomponent'),
items: catalog.map(entry => ({
value: entry.name,
text: t(entry.labelPhrase)
}))
}]
},
buttons: [
{ type: 'cancel', text: t('abbrechen') },
{ type: 'submit', text: t('weiter'), primary: true }
],
onSubmit(dialog) {
const name = dialog.getData().component;
dialog.close();
openProperties(editor, name, null, null);
}
});
}
// Step two. node is null for an insert and the marker element for an edit.
function openProperties(editor, name, values, node)
{
const descriptor = getDescriptor(name);
if (!descriptor)
return;
const specs = Object.entries(descriptor.props || {});
const sources = specs
.map(([, spec]) => spec.source)
.filter((key, i, all) => SOURCES[key] && all.indexOf(key) === i);
// A document picker needs the category list even though no prop declares it.
if (specs.some(([, spec]) => spec.picker === 'dms') && sources.indexOf('dmskategorie') === -1)
sources.push('dmskategorie');
Promise.all(sources.map(loadSource)).then(() => {
// Documents of the category currently chosen in the dialog. A category change
// refills this and redials, because the items of a selectbox are fixed once
// the dialog is built.
let dokumente = [];
// A stored value that is no longer in the list, for example a person who left.
// Without this extra option the select would fall back to its first entry and
// silently replace the value on save. slice() keeps the cache clean.
function listeFuer(propName, spec)
{
const liste = (sourceItems[spec.source] || []).slice();
const wert = values && values[propName];
if (wert && !liste.some(eintrag => eintrag.value === wert))
liste.unshift({ value: wert, text: t('ccNichtInDerAuswahl', [wert]) });
return liste;
}
function itemsBauen()
{
return specs.reduce((alle, [propName, spec]) => {
if (SOURCES[spec.source])
{
alle.push({
type: 'selectbox',
name: propName,
label: spec.labelPhrase ? t(spec.labelPhrase) : propName,
items: listeFuer(propName, spec)
});
return alle;
}
if (spec.type === 'boolean')
{
alle.push({
type: 'checkbox',
name: propName,
label: spec.labelPhrase ? t(spec.labelPhrase) : propName
});
return alle;
}
alle.push({
type: 'input',
name: propName,
label: spec.labelPhrase ? t(spec.labelPhrase) : propName
});
if (spec.picker === 'dms')
{
alle.push({
type: 'selectbox',
name: propName + FELD_KATEGORIE,
label: t('ccKategorie'),
items: [leereAuswahl()].concat(sourceItems.dmskategorie || [])
});
alle.push({
type: 'selectbox',
name: propName + FELD_DOKUMENT,
label: t('ccDokumentHinzufuegen'),
items: [leereAuswahl()].concat(dokumente)
});
}
return alle;
}, []);
}
function konfigBauen(daten)
{
return {
title: t(descriptor.labelPhrase),
initialData: daten,
body: { type: 'panel', items: itemsBauen() },
onChange(dialog, details) {
aufAenderung(dialog, details.name);
},
buttons: [
{ type: 'cancel', text: t('abbrechen') },
{ type: 'submit', text: t('uebernehmen'), primary: true }
],
onSubmit(dialog) {
const html = markerHtml(name, descriptor, dialog.getData());
dialog.close();
if (node)
editor.dom.setOuterHTML(node, html);
else
editor.insertContent(html);
onChanged();
}
};
}
function aufAenderung(dialog, feld)
{
// A new category means a new document list, and only redial can replace
// the options of a selectbox.
if (feld.endsWith(FELD_KATEGORIE))
{
const ziel = feld.slice(0, -FELD_KATEGORIE.length);
const daten = dialog.getData();
const kategorie = daten[feld];
daten[ziel + FELD_DOKUMENT] = '';
if (!kategorie)
{
dokumente = [];
dialog.redial(konfigBauen(daten));
return;
}
api
.call(ApiCmsAdmin.getDmsKategorieDokumente(kategorie))
.then(res => {
dokumente = (res.data || []).map(dokument => ({
value: String(dokument.dms_id),
// A document outside the CIS search still renders here, so
// the editor has to see which one that is.
text: dokument.cis_suche
? dokument.name
: t('ccNichtInCisSuche', [dokument.name])
}));
})
.catch(() => { dokumente = []; })
.then(() => { dialog.redial(konfigBauen(daten)); });
return;
}
// Choosing a document appends it and clears the select, so the next one can
// be picked straight away. The reset fires this handler again with an empty
// value, which the guard below drops.
if (feld.endsWith(FELD_DOKUMENT))
{
const ziel = feld.slice(0, -FELD_DOKUMENT.length);
const daten = dialog.getData();
const wert = daten[feld];
if (!wert)
return;
const ids = String(daten[ziel] || '')
.split(',')
.map(teil => teil.trim())
.filter(teil => teil);
if (ids.indexOf(wert) === -1)
ids.push(wert);
const patch = {};
patch[ziel] = ids.join(', ');
patch[feld] = '';
dialog.setData(patch);
}
}
// Every field needs a start value, otherwise the dialog reports it as unknown.
const startdaten = {};
for (const [propName, spec] of specs)
{
if (spec.type === 'boolean')
{
startdaten[propName] = !!values && values[propName] === 'true';
continue;
}
const liste = sourceItems[spec.source];
startdaten[propName] = (values && values[propName])
|| (liste && liste.length ? liste[0].value : '');
if (spec.picker === 'dms')
{
startdaten[propName + FELD_KATEGORIE] = '';
startdaten[propName + FELD_DOKUMENT] = '';
}
}
editor.windowManager.open(konfigBauen(startdaten));
});
}
// Opens the property dialog of an existing marker.
function openMarker(editor, node)
{
const marker = readMarker(node);
if (marker)
openProperties(editor, marker.name, marker.values, node);
}
return {
openPicker: openPicker,
openMarker: openMarker
};
}
@@ -1,25 +0,0 @@
export default {
name: 'FieldString',
props: {
modelValue: { type: String, default: '' },
label: String,
required: Boolean,
disabled: Boolean,
type: { type: String, default: 'text' }
},
emits: ['update:modelValue'],
template: `
<div class="mb-3">
<label class="form-label">
{{ label }}<span v-if="required" class="text-danger"> *</span>
</label>
<input
class="form-control"
:type="type"
:value="modelValue"
:disabled="disabled"
@input="$emit('update:modelValue', $event.target.value)"
>
</div>
`
};
@@ -1,162 +0,0 @@
import { CONTENTCOMPONENT_CLASS, CONTENTCOMPONENT_ATTR } from '../../Contentcomponents/catalog.js';
import { createContentcomponentDialog } from './ContentcomponentDialog.js';
export default {
name: 'FieldWysiwyg',
props: {
modelValue: { type: String, default: '' },
label: String,
required: Boolean,
disabled: Boolean
},
emits: ['update:modelValue'],
data() {
return {
editor: null,
editorId: 'cms-wysiwyg-' + Math.random().toString(36).substring(2, 11)
};
},
methods: {
getContent() {
return this.editor ? this.editor.getContent() : this.modelValue;
},
// Opens the legacy DMS browser and resolves with the relative URL it posts back.
// Used for images and links, where the editor needs the file preview of that
// browser. The contentcomponent dialog picks documents from a list instead, without
// leaving the dialog. The bridge in cms/tinymce_dms.php sends { typ, url } to
// window.opener and closes itself.
openDmsBrowser() {
const vm = this;
return new Promise(resolve => {
if (vm._dmsListener)
window.removeEventListener('message', vm._dmsListener);
const url = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cms/tinymce_dms.php';
// The window name selects the bridge path in cms/tinymce_dms.php and survives
// its internal navigation. The legacy admin opens the same page as 'DMS'.
window.open(url, 'DMS_CIS4', 'width=1400,height=850,scrollbars=1,resizable=1');
const onMessage = (event) => {
if (event.origin !== window.location.origin) return;
if (!event.data || event.data.typ !== 'fhc-dms-selection') return;
window.removeEventListener('message', onMessage);
vm._dmsListener = null;
resolve(event.data.url);
};
vm._dmsListener = onMessage;
window.addEventListener('message', onMessage);
});
}
},
watch: {
modelValue(newVal) {
if (this.editor && this.editor.getContent() !== newVal) {
this.editor.setContent(newVal || '');
}
}
},
mounted() {
const vm = this;
// The dialogs live in their own module. This file stays with the editor itself.
this.contentcomponentDialog = createContentcomponentDialog({
api: this.$api,
// The dialog module is no component, so it cannot reach the phrases itself.
p: this.$p,
onChanged: () => vm.$emit('update:modelValue', vm.editor.getContent())
});
tinymce.init({
target: this.$refs.editorArea,
height: 500,
language: 'de',
// valid_elements: '*[*]' permits every tag and attribute. Existing content
// contains audio, video and source tags. A stricter setting deletes them silently.
valid_elements: '*[*]',
// A contentcomponent marker is one atomic block. The editor must not type into it.
noneditable_noneditable_class: CONTENTCOMPONENT_CLASS,
// The marker renders nothing here, so the editor has to show that it is there.
content_style: '.' + CONTENTCOMPONENT_CLASS + '{'
+ 'display:block;margin:.5rem 0;padding:.5rem;min-height:2.5rem;'
+ 'border:1px dashed #7a7a7a;background:#f2f2f2;cursor:pointer;}'
+ '.' + CONTENTCOMPONENT_CLASS + '::before{'
+ 'content:"Contentcomponent: " attr(' + CONTENTCOMPONENT_ATTR + ');'
+ 'font:12px monospace;color:#555;}',
forced_root_block: '',
// No style_formats, and no styleselect in the toolbar below. The six formats
// of the legacy admin are gone, and none of them earned a port.
//
// Three wrote a div, while every stylesheet targets a td: skin/styles/tw.css
// declares td.cmscontent, td.menubox and td.teambox. The live content agrees,
// it carries those classes on a td about 1400 times and on a div twice.
// Tablesorter wrote tablesorter_col_0, which no file in this repository
// reads, and skin/style.css.php never serves skin/tablesort.css.
//
// cmstable and screenshot_boxshadow did work, but only in the legacy
// renderer. public/css/Cis4/Cms.css binds its copies to #cms, and CIS4
// renders into #fhc-cms-content. CIS4 is the target, so they went too.
//
// A styling option for editors is a feature of its own. See
// docs/cms-darstellung-entwurf.md.
plugins: 'code lists link image table fullscreen paste searchreplace charmap directionality visualblocks anchor hr noneditable',
toolbar: [
'code | contentcomponent | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | fontsizeselect',
'pastetext | searchreplace | bullist numlist | outdent indent blockquote | undo redo | link unlink anchor image | forecolor backcolor',
'table | hr removeformat visualblocks | subscript superscript | charmap | ltr rtl | fullscreen'
],
// The relative form dms.php?id=N is mandatory. The legacy view runs under /cms/
// and resolves it directly. The CIS4 view replaces it in CmsLib::getContent
// with APP_ROOT . 'cms/dms.php'. An absolute URL breaks both paths.
file_picker_callback: (callback, value, meta) => {
vm.openDmsBrowser().then(callback);
},
setup: (editor) => {
vm.editor = editor;
editor.ui.registry.addButton('contentcomponent', {
text: vm.$p.t('cms/ccContentcomponent'),
tooltip: vm.$p.t('cms/ccDialogTitel'),
onAction: () => vm.contentcomponentDialog.openPicker(editor)
});
// A double click on a marker reopens its property dialog.
editor.on('dblclick', (event) => {
const node = editor.dom.getParent(event.target, '.' + CONTENTCOMPONENT_CLASS);
if (node)
vm.contentcomponentDialog.openMarker(editor, node);
});
editor.on('init', () => {
editor.setContent(vm.modelValue || '');
if (vm.disabled) editor.mode.set('readonly');
});
editor.on('change input', () => {
vm.$emit('update:modelValue', editor.getContent());
});
}
});
},
beforeUnmount() {
if (this._dmsListener) {
window.removeEventListener('message', this._dmsListener);
this._dmsListener = null;
}
if (this.editor && tinymce.get(this.editor.id)) {
tinymce.get(this.editor.id).remove();
this.editor = null;
}
},
template: `
<div class="mb-3">
<label class="form-label">
{{ label }}<span v-if="required" class="text-danger"> *</span>
</label>
<textarea ref="editorArea" :id="editorId"></textarea>
</div>
`
};
-72
View File
@@ -1,72 +0,0 @@
import FieldString from './FieldString.js';
import FieldWysiwyg from './FieldWysiwyg.js';
export default {
name: 'XsdForm',
components: {
'field-string': FieldString,
'field-wysiwyg': FieldWysiwyg
},
props: {
schema: Object,
modelValue: Object,
disabled: Boolean
},
emits: ['update:modelValue'],
methods: {
updateField(name, value) {
this.$emit('update:modelValue', { ...this.modelValue, [name]: value });
},
collectValues() {
const vals = { ...this.modelValue };
if (!this.schema?.fields) return vals;
for (const field of this.schema.fields) {
if (field.type === 'wysiwyg') {
const ref = this.$refs['wysiwyg-' + field.name];
const comp = Array.isArray(ref) ? ref[0] : ref;
if (comp?.getContent) vals[field.name] = comp.getContent();
}
}
return vals;
}
},
template: `
<div v-if="schema && schema.fields">
<template v-for="field in schema.fields" :key="field.name">
<field-wysiwyg
v-if="field.type === 'wysiwyg'"
:ref="'wysiwyg-' + field.name"
:modelValue="modelValue[field.name] || ''"
:label="field.name"
:required="field.required"
:disabled="disabled"
@update:modelValue="updateField(field.name, $event)"
></field-wysiwyg>
<div v-else-if="field.type === 'boolean'" class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
:checked="!!modelValue[field.name]"
:disabled="disabled"
@change="updateField(field.name, $event.target.checked)"
:id="'field-' + field.name"
>
<label class="form-check-label" :for="'field-' + field.name">
{{ field.name }}<span v-if="field.required" class="text-danger"> *</span>
</label>
</div>
</div>
<field-string
v-else
:modelValue="modelValue[field.name] || ''"
:label="field.name"
:required="field.required"
:disabled="disabled"
:type="field.type === 'date' ? 'date' : 'text'"
@update:modelValue="updateField(field.name, $event)"
></field-string>
</template>
</div>
`
};
@@ -1,180 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
import VersionSelect from './VersionSelect.js';
import LanguageSelect from './LanguageSelect.js';
export default {
name: 'CmsContentHeader',
components: {
'version-select': VersionSelect,
'sprache-select': LanguageSelect
},
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['select-language-version', 'reload-content-info'],
data() {
return {
titel: '',
allSprachen: [],
versionDetails: [],
// Target of the translation dropdown, used once the list grows past a pair.
neueSprache: ''
};
},
computed: {
languages() {
return this.contentInfo?.languages || [];
},
versions() {
if (!this.contentInfo?.versions || !this.sprache) return [];
return this.contentInfo.versions[this.sprache] || [];
},
availableSprachen() {
return this.allSprachen.filter(
s => !this.languages.includes(s.sprache)
);
}
},
watch: {
contentId() {
this.loadTitel();
this.loadVersionDetails();
},
sprache() {
this.loadTitel();
this.loadVersionDetails();
},
version() {
this.loadTitel();
}
},
methods: {
loadTitel() {
if (this.contentId == null || !this.sprache || !this.version) return;
this.$api
.call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
.then(result => {
this.titel = result.data?.titel || '';
});
},
loadVersionDetails() {
if (this.contentId == null || !this.sprache) {
this.versionDetails = [];
return;
}
this.$api
.call(ApiCmsAdmin.getVersions(this.contentId, this.sprache))
.then(result => {
this.versionDetails = result.data || [];
});
},
loadSprachen() {
this.$api
.call(ApiCmsAdmin.getSprachen())
.then(result => {
this.allSprachen = result.data || [];
});
},
selectVersion(ver) {
this.$emit('select-language-version', this.sprache, ver);
},
selectLanguage(lang) {
const versions = this.contentInfo?.versions?.[lang] || [];
const maxVersion = versions.length ? Math.max(...versions) : 1;
this.$emit('select-language-version', lang, maxVersion);
},
createVersion() {
this.$api
.call(ApiCmsAdmin.postVersion(this.contentId, this.sprache))
.then(result => {
const newVersion = result.data.version;
this.$fhcAlert.alertSuccess(this.$p.t('cms/neueVersionAngelegt'));
this.$fhcAlert.alertInfo(this.$p.t('cms/neueVersionIstUnsichtbar'));
this.$emit('reload-content-info');
this.$emit('select-language-version', this.sprache, newVersion);
})
.catch(this.$fhcAlert.handleSystemError);
},
createTranslation(newSprache) {
if (!newSprache) return;
this.neueSprache = '';
this.$api
.call(ApiCmsAdmin.postTranslation(
this.contentId, this.sprache, this.version, newSprache
))
.then(() => {
this.$fhcAlert.alertSuccess(this.$p.t('cms/uebersetzungAngelegt'));
this.$emit('reload-content-info');
this.$emit('select-language-version', newSprache, this.version);
})
.catch(this.$fhcAlert.handleSystemError);
}
},
mounted() {
this.loadSprachen();
this.loadTitel();
this.loadVersionDetails();
},
template: `
<div class="px-3 pt-2 pb-2 border-bottom" v-if="contentInfo">
<div class="mb-2 fw-bold">
{{ $p.t('cms/contentId') }}: {{ contentId }}
| {{ $p.t('cms/version') }}: {{ version }}
| {{ $p.t('cms/sprache') }}: {{ sprache }}
| {{ $p.t('cms/titel') }}: {{ titel }}
</div>
<div class="d-flex align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<version-select
:versions="versions"
:version-details="versionDetails"
:version="version"
@select-version="selectVersion"
></version-select>
<sprache-select
:languages="languages"
:sprache="sprache"
@select-language="selectLanguage"
></sprache-select>
</div>
<div>
<button class="btn btn-sm btn-outline-primary"
@click="createVersion"
>{{ $p.t('cms/neueVersionAnlegen') }}</button>
</div>
<div class="d-flex align-items-center gap-1">
<!-- One button per missing language is fine for a pair. An institution
with more content languages would get a wall of them, each
carrying the full label. -->
<template v-if="availableSprachen.length > 2">
<select class="form-select form-select-sm w-auto"
v-model="neueSprache">
<option value="">{{ $p.t('cms/spracheWaehlen') }}</option>
<option v-for="s in availableSprachen" :key="s.sprache"
:value="s.sprache">{{ s.sprache }}</option>
</select>
<button class="btn btn-sm btn-outline-primary"
:disabled="!neueSprache"
@click="createTranslation(neueSprache)"
>{{ $p.t('cms/uebersetzungAnlegen') }}</button>
</template>
<template v-else>
<button v-for="s in availableSprachen" :key="s.sprache"
class="btn btn-sm btn-outline-primary"
@click="createTranslation(s.sprache)"
>{{ $p.t('cms/uebersetzungAnlegenIn', [s.sprache]) }}</button>
</template>
</div>
</div>
</div>
`
};
@@ -1,36 +0,0 @@
export default {
name: 'SpracheSelect',
props: {
languages: { type: Array, default: () => [] },
sprache: String
},
emits: ['select-language'],
computed: {
// Two languages read faster as a pair of buttons, and that is the case here. Other
// institutions run this software with more content languages, and there the row of
// buttons pushes the rest of the header off the line.
alsAuswahl() {
return this.languages.length > 2;
}
},
template: `
<select v-if="alsAuswahl"
class="form-select form-select-sm w-auto cms-sprache-select"
:title="$p.t('cms/sprache')"
:value="sprache"
@change="$emit('select-language', $event.target.value)"
>
<option v-for="lang in languages" :key="lang" :value="lang">{{ lang }}</option>
</select>
<div v-else class="btn-group btn-group-sm cms-sprache-select" role="group">
<button
v-for="lang in languages"
:key="lang"
type="button"
class="btn"
:class="lang === sprache ? 'btn-primary' : 'btn-outline-secondary'"
@click="$emit('select-language', lang)"
>{{ lang }}</button>
</div>
`
};
@@ -1,81 +0,0 @@
// Versions shown on each side of the open one.
const WINDOW = 3;
export default {
name: 'VersionSelect',
props: {
versions: { type: Array, default: () => [] },
versionDetails: { type: Array, default: () => [] },
version: Number
},
emits: ['select-version'],
computed: {
// A content can carry dozens of versions, and one button each breaks the header.
// Show the open version with three neighbours on each side. The first and the last
// stay reachable in one click, and a gap marks what the window leaves out.
// Below the threshold the window would hide nothing, so show every version.
items() {
const all = this.versions;
const active = all.indexOf(this.version);
if (active === -1 || all.length <= WINDOW * 2 + 3)
return all.map(value => ({ type: 'version', value }));
const from = Math.max(0, active - WINDOW);
const to = Math.min(all.length - 1, active + WINDOW);
const items = [];
if (from > 0) {
items.push({ type: 'version', value: all[0] });
// One version between the first and the window needs no gap marker.
if (from > 1) items.push({ type: 'gap', count: from - 1 });
}
for (let i = from; i <= to; i++)
items.push({ type: 'version', value: all[i] });
if (to < all.length - 1) {
if (to < all.length - 2)
items.push({ type: 'gap', count: all.length - 2 - to });
items.push({ type: 'version', value: all[all.length - 1] });
}
return items;
},
sichtbarMap() {
const map = {};
for (const v of this.versionDetails) {
map[v.version] = v.sichtbar;
}
return map;
}
},
methods: {
isSichtbar(ver) {
return this.sichtbarMap[ver] !== false;
}
},
template: `
<div class="btn-group btn-group-sm" role="group">
<template v-for="(item, idx) in items" :key="item.type + idx">
<button
v-if="item.type === 'version'"
type="button"
class="btn"
:class="[
item.value === version ? 'btn-primary' : 'btn-outline-secondary',
{ 'opacity-50': !isSichtbar(item.value) }
]"
@click="$emit('select-version', item.value)"
>{{ item.value }}</button>
<button
v-else
type="button"
class="btn btn-outline-secondary"
disabled
:title="$p.t('cms/weitereVersionen', [item.count])"
>&hellip;</button>
</template>
</div>
`
};
-308
View File
@@ -1,308 +0,0 @@
// Safety net for the spinner.
const CIS4_LOAD_TIMEOUT = 10000;
// Cis.js reads window.innerWidth and switches to its mobile layout below 767px. In a frame
// that is the width of the frame, so these widths drive the real layout and not a zoom.
// Its resize listener reacts on its own, which means no reload and no second long wait.
const CIS4_WIDTHS = {
mobile: 375,
desktop: null
};
export default {
name: 'CmsPreview',
props: {
contentId: Number,
sprache: String,
version: Number,
templateKurzbz: String,
// The legacy renderer runs no Vue, so it draws nothing for a contentcomponent
// marker. The CIS4 view mounts them, which is the whole point of switching.
hasContentcomponents: Boolean,
// False for a template whose xslt_xhtml_c4 is a copy of the legacy stylesheet.
// Such a template emits a whole HTML document, and the CIS4 renderer writes that
// into a div, so the layout is lost. Defaults true, so an older payload warns
// about nothing.
hasCis4Stylesheet: { type: Boolean, default: true },
sichtbar: Boolean
},
data() {
// The legacy renderer stays the default, so nothing changes until an editor asks
// for the other one. The choice survives a page switch, like the menu switch.
const stored = localStorage.getItem('cms/previewMode');
const mode = (stored === 'legacy' || stored === 'cis4') ? stored : 'legacy';
const width = localStorage.getItem('cms/previewWidth');
return {
mode: mode,
// True while the split handle is dragged. See onSplitterDown.
dragging: false,
cis4Width: CIS4_WIDTHS.hasOwnProperty(width) ? width : 'desktop',
// The CIS4 shell pulls its whole stack before it shows anything, which takes a
// while. Start covered when the frame is the one on screen.
cis4Loading: mode === 'cis4'
};
},
watch: {
mode(value) {
if (value === 'cis4') this.startCis4Load();
else this.stopCis4Poll();
},
// A new content, language or version reloads the frame.
cis4PreviewUrl() {
if (this.mode === 'cis4') this.startCis4Load();
}
},
computed: {
// The preview uses the legacy renderer cms/content.php. It reads xslt_xhtml, which
// holds a complete HTML document for all eleven templates. The CIS4 route reads
// xslt_xhtml_c4, where four templates hold a whole document instead of a fragment.
previewUrl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'cms/content.php?content_id=' + this.contentId
+ '&version=' + this.version
+ '&sprache=' + this.sprache
+ '&sichtbar=';
},
// The CIS4 route takes content_id only. Its Vue route is
// /CisVue/Cms/Content/:content_id, and extra segments hit the catch-all, which
// redirects to the dashboard. CIS4 picks the version itself and the language from
// the viewer profile.
cis4Url() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/CisVue/Cms/content/' + this.contentId;
},
// The second preview renders the content the way CIS4 does: the route loads the
// CIS4 shell, which brings its stylesheets, its Vue app and the contentcomponents.
// CISVUE-Header sets the body class in-frame by itself, and Cis.css hides the CIS4
// header for it, so the frame shows the content and no navigation.
// version travels as a route segment, because Cms/Content.js already declares that
// prop and the route passes props. sprache and preview travel as query parameters,
// because neither is a prop there.
cis4PreviewUrl() {
let url = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/CisVue/Cms/content/' + this.contentId;
if (this.version !== null)
url += '/' + this.version;
url += '?preview=1';
if (this.sprache)
url += '&sprache=' + encodeURIComponent(this.sprache);
return url;
},
// The overlay sits on the wrapper, so the wrapper carries the width. Otherwise the
// spinner would cover the whole pane while the frame is only 375px wide.
cis4WrapStyle() {
const px = CIS4_WIDTHS[this.cis4Width];
// content-box on purpose: the frame reads its own window.innerWidth, and with
// the usual border-box the border would eat two pixels. The preset is meant
// to be the width the CIS4 app sees, so it must not depend on the frame.
return px === null
? ''
: 'width: ' + px + 'px; box-sizing: content-box; max-width: 100%;'
+ ' margin: 0 auto;';
},
// The redirect template navigates away with JavaScript. In an iframe this can close
// the admin page. Therefore this case uses a link and not an iframe.
isRedirect() {
return this.templateKurzbz === 'redirect';
}
},
methods: {
selectMode(value) {
localStorage.setItem('cms/previewMode', value);
this.mode = value;
},
// The split handle sits right next to the frame. As soon as the pointer crosses
// into the frame, the frame document takes the mousemove and the mouseup, the
// listeners HorizontalSplit put on the parent window never fire, and the drag
// stops halfway. Turning pointer events off in the frame for the length of the
// drag lets those events through to the page underneath.
//
// Capture phase on purpose: HorizontalSplit.dragStart calls stopPropagation, so a
// listener that waits for the event to bubble back up never sees it at all.
onSplitterDown(event) {
const target = event.target;
if (target && target.closest && target.closest('.horizontalsplitter'))
this.dragging = true;
},
onSplitterUp() {
this.dragging = false;
},
selectWidth(value) {
localStorage.setItem('cms/previewWidth', value);
this.cis4Width = value;
// A frame resized by its parent gets no resize event of its own, so Cis.js
// would keep the window width it read at load and stay on that layout. Same
// origin, so tell the frame. That spares a reload of the whole CIS4 stack.
this.$nextTick(() => {
try {
const frame = this.$refs.cis4Frame;
const win = frame && frame.contentWindow;
if (win) win.dispatchEvent(new win.Event('resize'));
} catch (e) {
// Not reachable. The frame keeps the layout it loaded with.
}
});
},
startCis4Load() {
this.stopCis4Poll();
this.cis4Loading = true;
},
// The frame fires load once the CIS4 shell is there, but the shell then fetches the
// content over the API, so the load event alone clears the spinner too early and
// leaves the placeholder text on screen. The frame is same origin, so wait for the
// element the CIS4 renderer writes once it holds the content.
onCis4Load() {
this.stopCis4Poll();
const started = Date.now();
this.cis4Poll = setInterval(() => {
let done = false;
try {
const doc = this.$refs.cis4Frame && this.$refs.cis4Frame.contentDocument;
done = !!(doc && doc.getElementById('fhc-cms-content'));
} catch (e) {
// Not readable after all. Stop waiting rather than spin forever.
done = true;
}
if (done || Date.now() - started > CIS4_LOAD_TIMEOUT) {
this.stopCis4Poll();
this.cis4Loading = false;
}
}, 120);
},
stopCis4Poll() {
if (this.cis4Poll) {
clearInterval(this.cis4Poll);
this.cis4Poll = null;
}
},
neuLaden() {
if (this.$refs.previewFrame) {
this.$refs.previewFrame.src = this.previewUrl + '&t=' + Date.now();
}
if (this.$refs.cis4Frame) {
this.startCis4Load();
this.$refs.cis4Frame.src = this.cis4PreviewUrl + '&t=' + Date.now();
}
}
},
mounted() {
// On the window, not the document: HorizontalSplit ends the drag from a listener
// it puts on the window, and a mouseup aimed at the window never reaches a
// listener on the document. The window is the first node of the capture path, so
// it sees everything the document would and that case as well.
window.addEventListener('mousedown', this.onSplitterDown, true);
window.addEventListener('mouseup', this.onSplitterUp, true);
// Backstop. A missed mouseup would leave the frame unclickable.
window.addEventListener('blur', this.onSplitterUp);
},
beforeUnmount() {
window.removeEventListener('mousedown', this.onSplitterDown, true);
window.removeEventListener('mouseup', this.onSplitterUp, true);
window.removeEventListener('blur', this.onSplitterUp);
this.stopCis4Poll();
},
template: `
<div class="mt-3" :class="{ 'cms-preview-dragging': dragging }">
<div v-if="sichtbar === false" class="text-danger fw-bold mb-2">
{{ $p.t('cms/unsichtbarImLivesystem') }}
</div>
<div class="mb-2">
<a :href="cis4Url" target="_blank">{{ $p.t('cms/inCis4Ansehen') }}</a>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn"
:class="mode === 'legacy' ? 'btn-primary' : 'btn-outline-secondary'"
@click="selectMode('legacy')"
>{{ $p.t('cms/vorschauLegacy') }}</button>
<button type="button" class="btn"
:class="mode === 'cis4' ? 'btn-primary' : 'btn-outline-secondary'"
@click="selectMode('cis4')"
>{{ $p.t('cms/vorschauCis4') }}</button>
</div>
<div v-if="mode === 'cis4' && !isRedirect"
class="btn-group btn-group-sm" role="group">
<button type="button" class="btn"
:class="cis4Width === 'desktop' ? 'btn-primary' : 'btn-outline-secondary'"
:title="$p.t('cms/breiteDesktop')"
@click="selectWidth('desktop')"
><i class="fa-solid fa-display"></i></button>
<button type="button" class="btn"
:class="cis4Width === 'mobile' ? 'btn-primary' : 'btn-outline-secondary'"
:title="$p.t('cms/breiteMobil')"
@click="selectWidth('mobile')"
><i class="fa-solid fa-mobile-screen-button"></i></button>
</div>
</div>
<template v-if="mode === 'legacy'">
<div v-if="hasContentcomponents" class="alert alert-info py-2">
{{ $p.t('cms/contentcomponentsNurCis4') }}
</div>
<template v-if="isRedirect">
<a :href="previewUrl" target="_blank">
{{ $p.t('cms/vorschauInEigenemFenster') }}
</a>
</template>
<template v-else>
<iframe
ref="previewFrame"
:src="previewUrl"
style="width: 100%; min-height: 500px; border: 1px solid #ccc;"
></iframe>
</template>
</template>
<template v-else>
<div v-if="!hasCis4Stylesheet" class="alert alert-warning py-2">
{{ $p.t('cms/keinCis4Stylesheet') }}
</div>
<template v-if="isRedirect">
<a :href="cis4PreviewUrl" target="_blank">
{{ $p.t('cms/vorschauInEigenemFenster') }}
</a>
</template>
<template v-else>
<div class="position-relative border" :style="cis4WrapStyle">
<iframe
ref="cis4Frame"
:src="cis4PreviewUrl"
@load="onCis4Load"
style="width: 100%; min-height: 500px; border: 0;
display: block;"
></iframe>
<div v-if="cis4Loading"
class="position-absolute top-0 start-0 w-100 h-100 d-flex
flex-column align-items-center justify-content-center"
style="background: rgba(255, 255, 255, 0.85);">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{ $p.t('ui/loading') }}</span>
</div>
<div class="mt-2 text-muted small">
{{ $p.t('cms/vorschauLaedt') }}
</div>
</div>
</div>
</template>
</template>
</div>
`
};
-153
View File
@@ -1,153 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsClickstats',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['select-content'],
data() {
return {
own: null,
ranked: [],
// The ranking stays absent until the editor asks for it, so an empty table and
// a table that was never fetched stay apart.
rankingLoaded: false,
since: null,
months: 12,
loadingOwn: false,
loadingRanking: false
};
},
watch: {
contentId() { this.loadOwn(); },
sprache() { this.loadOwn(); }
},
methods: {
// One counting query. Cheap enough to run whenever the tab or the period opens.
loadOwn() {
if (this.contentId == null) return;
this.loadingOwn = true;
this.$api
.call(ApiCmsAdmin.getClickCount(this.contentId, this.months))
.then(result => {
this.own = result.data.own;
this.since = result.data.since;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.loadingOwn = false; });
},
// Groups the whole log, which takes seconds. Only on request.
loadRanking() {
this.loadingRanking = true;
this.$api
.call(ApiCmsAdmin.getClickRanking(this.months, this.sprache))
.then(result => {
this.ranked = result.data.ranked || [];
this.since = result.data.since;
this.rankingLoaded = true;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.loadingRanking = false; });
},
// A new period always refreshes the cheap number. The ranking follows only once it
// is on screen, so changing the period never costs the long wait unasked.
onPeriodChange() {
this.loadOwn();
if (this.rankingLoaded) this.loadRanking();
},
// since arrives as an SQL timestamp. Format it the way the other tabs do.
formatDate(value) {
if (!value) return '';
const dt = luxon.DateTime.fromSQL(value);
return dt.isValid ? dt.toFormat('dd.MM.yyyy') : value;
},
isCurrent(row) {
return row.content_id === this.contentId;
},
onContentClick(content_id) {
this.$emit('select-content', content_id);
}
},
created() {
this.monthOptions = [3, 6, 12, 24, 0];
},
mounted() {
this.loadOwn();
},
template: `
<div class="p-3" v-if="contentId != null">
<div class="d-flex gap-2 align-items-center mb-3">
<label class="form-label mb-0">{{ $p.t('cms/zeitraum') }}</label>
<select class="form-select form-select-sm"
style="max-width: 220px"
v-model.number="months"
:disabled="loadingOwn || loadingRanking"
@change="onPeriodChange">
<option v-for="m in monthOptions" :key="m" :value="m">
{{ m ? $p.t('cms/letzteNMonate', [m]) : $p.t('cms/gesamterZeitraum') }}
</option>
</select>
<i v-if="loadingOwn" class="fa-solid fa-spinner fa-spin"></i>
</div>
<div class="form-text mb-3">
{{ since
? $p.t('cms/klicksSeit', [formatDate(since)])
: $p.t('cms/klicksGesamt') }}
</div>
<div class="mb-4">
<h5>{{ $p.t('cms/aufrufeDiesesEintrags') }}</h5>
<span class="fs-4">{{ own === null ? '-' : own }}</span>
</div>
<h5>{{ $p.t('cms/meistAufgerufeneEintraege') }}</h5>
<button v-if="!rankingLoaded"
class="btn btn-outline-primary"
:disabled="loadingRanking"
@click="loadRanking">
<i v-if="loadingRanking" class="fa-solid fa-spinner fa-spin me-1"></i>
{{ $p.t('cms/alleKlickstatistikenLaden') }}
</button>
<table v-else-if="ranked.length > 0" class="table table-sm table-striped">
<thead>
<tr>
<th>{{ $p.t('cms/rang') }}</th>
<th>ID</th>
<th>{{ $p.t('cms/titel') }}</th>
<th>{{ $p.t('cms/aufrufe') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in ranked" :key="row.content_id"
:class="isCurrent(row) ? 'table-primary' : ''">
<td>{{ idx + 1 }}</td>
<td>{{ row.content_id }}</td>
<td>
<a href="#" @click.prevent="onContentClick(row.content_id)">
{{ row.titel || $p.t('cms/ohneTitel') }}
</a>
</td>
<td>{{ row.hits }}</td>
</tr>
</tbody>
</table>
<div v-else class="text-muted">
{{ $p.t('cms/keineAufrufeImZeitraum') }}
</div>
</div>
`
};
-199
View File
@@ -1,199 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
import XsdForm from '../Form/XsdForm.js';
import CmsPreview from '../Preview.js';
import { CONTENTCOMPONENT_ATTR } from '../../Contentcomponents/catalog.js';
export default {
name: 'CmsContent',
components: {
'xsd-form': XsdForm,
'cms-preview': CmsPreview
},
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'refresh-tree-node', 'select-content'],
data() {
return {
schema: null,
values: {},
sperre: null,
sichtbar: true,
loading: false,
saving: false
};
},
computed: {
// The legacy renderer runs no Vue and draws nothing for a marker. The preview says
// so, but only while the legacy view is the one on screen.
hasContentcomponents() {
return Object.values(this.values || {}).some(
value => typeof value === 'string' && value.indexOf(CONTENTCOMPONENT_ATTR) !== -1
);
},
// The redirect template carries a URL, and its preview opens in a new window
// instead of an iframe. So without the lock nothing on this tab shows the values.
// Read only fields spare the editor a lock that blocks the content for 24 hours.
isRedirect() {
return this.contentInfo?.template_kurzbz === 'redirect';
},
lockState() {
if (!this.sperre || this.sperre.gesperrt_uid === null) return 'free';
if (this.sperre.own) return 'own';
if (this.sperre.expired) return 'expired';
return 'foreign';
},
lockInfo() {
if (!this.sperre || this.sperre.gesperrt_uid === null) return '';
const start = this.formatDateTime(this.sperre.start);
const ende = this.formatDateTime(this.sperre.expires);
if (this.sperre.own)
return this.$p.t('cms/eigeneSperre', [start, ende]);
const uid = this.sperre.gesperrt_uid;
if (this.sperre.expired)
return this.$p.t('cms/gesperrtVonSeitAbgelaufen', [uid, start, ende]);
return this.$p.t('cms/gesperrtVonSeit', [uid, start, ende]);
}
},
watch: {
contentId() { this.loadFormData(); },
sprache() { this.loadFormData(); },
version() { this.loadFormData(); }
},
methods: {
formatDateTime(dateStr) {
if (!dateStr) return this.$p.t('cms/unbekannt');
const dt = luxon.DateTime.fromSQL(dateStr);
return dt.isValid ? dt.toFormat('dd.MM.yyyy HH:mm') : dateStr;
},
loadFormData() {
if (this.contentId == null || !this.sprache || !this.version) return;
this.loading = true;
Promise.all([
this.$api.call(ApiCmsAdmin.getFormData(this.contentId, this.sprache, this.version)),
this.$api.call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
])
.then(([formResult, spracheResult]) => {
const d = formResult.data;
this.schema = d.schema;
this.values = d.values || {};
this.sperre = d.sperre;
this.sichtbar = !!spracheResult.data.sichtbar;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.loading = false; });
},
lock() {
this.$api
.call(ApiCmsAdmin.postLock(this.sperre.contentsprache_id))
.then(() => { this.loadFormData(); })
.catch(this.$fhcAlert.handleSystemError);
},
unlock() {
this.$api
.call(ApiCmsAdmin.deleteLock(this.sperre.contentsprache_id))
.then(() => { this.loadFormData(); })
.catch(this.$fhcAlert.handleSystemError);
},
forceUnlock() {
this.$api
.call(ApiCmsAdmin.deleteLockForced(this.sperre.contentsprache_id))
.then(() => { this.loadFormData(); })
.catch(this.$fhcAlert.handleSystemError);
},
save() {
const freshValues = this.$refs.xsdForm
? this.$refs.xsdForm.collectValues()
: this.values;
this.saving = true;
this.$api
.call(ApiCmsAdmin.putFormData(
this.contentId, this.sprache, this.version, freshValues
))
.then(() => {
this.$fhcAlert.alertSuccess(this.$p.t('cms/gespeichert'));
// The form holds the title, and the tree shows it.
this.$emit('refresh-tree-node', this.contentId);
if (this.$refs.preview) this.$refs.preview.neuLaden();
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.saving = false; });
}
},
mounted() {
this.loadFormData();
},
template: `
<div class="p-3" v-if="contentId != null">
<template v-if="!loading && sperre">
<div v-if="sperre.gesperrt_uid !== null"
class="alert"
:class="lockState === 'expired' ? 'alert-warning' : 'alert-info'">
{{ lockInfo }}
</div>
<template v-if="lockState === 'free'">
<button class="btn btn-primary mb-3" @click="lock">
{{ $p.t('cms/zurBearbeitungSperren') }}
</button>
</template>
<template v-else-if="lockState === 'own'">
<button class="btn btn-outline-secondary mb-3" @click="unlock">
{{ $p.t('cms/sperreFreigeben') }}
</button>
<xsd-form
ref="xsdForm"
:schema="schema"
v-model="values"
:disabled="false"
></xsd-form>
<button class="btn btn-primary mt-2 mb-3" @click="save" :disabled="saving">
{{ $p.t('cms/speichern') }}
</button>
</template>
<template v-else-if="lockState === 'expired'">
<button class="btn btn-warning mb-3" @click="lock">
{{ $p.t('cms/sperreUebernehmen') }}
</button>
</template>
<template v-else>
<button v-if="sperre.may_force" class="btn btn-danger mb-3" @click="forceUnlock">
{{ $p.t('cms/freigabeErzwingen') }}
</button>
</template>
<xsd-form
v-if="isRedirect && lockState !== 'own'"
:schema="schema"
:modelValue="values"
:disabled="true"
></xsd-form>
<cms-preview
ref="preview"
:content-id="contentId"
:sprache="sprache"
:version="version"
:template-kurzbz="contentInfo?.template_kurzbz"
:has-cis4-stylesheet="contentInfo?.has_cis4_stylesheet !== false"
:has-contentcomponents="hasContentcomponents"
:sichtbar="sichtbar"
></cms-preview>
</template>
</div>
`
};
-154
View File
@@ -1,154 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsDelete',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'reload-tree', 'refresh-tree-node', 'select-content'],
data() {
return {
usages: [],
loadingUsage: true
};
},
computed: {
hasRestrict() {
return this.usages.some(u => u.table !== 'campus.tbl_infoscreen_content');
},
// forward defaultLanguage prop to tabs. Using 'German' as fallback.
isLastVersion() {
if (!this.contentInfo?.versions || !this.sprache) return false;
const defaultLang = 'German';
const versions = this.contentInfo.versions[this.sprache];
return this.sprache === defaultLang && versions && versions.length === 1;
},
totalVersionCount() {
if (!this.contentInfo?.versions) return 0;
return Object.values(this.contentInfo.versions)
.reduce((sum, vers) => sum + vers.length, 0);
}
},
watch: {
contentId() { this.loadUsage(); }
},
methods: {
loadUsage() {
if (this.contentId == null) return;
this.loadingUsage = true;
this.$api
.call(ApiCmsAdmin.getUsage(this.contentId))
.then(result => {
this.usages = result.data || [];
})
.finally(() => { this.loadingUsage = false; });
},
deleteVersion() {
this.$fhcAlert.confirm({
message: this.$p.t('cms/versionLoeschenBestaetigen'),
acceptLabel: this.$p.t('ui/loeschen'),
acceptClass: 'p-button-danger'
}).then(confirmed => {
if (!confirmed) return;
return this.$api.call(
ApiCmsAdmin.deleteContentsprache(this.contentId, this.sprache, this.version)
);
}).then(result => {
if (!result) return;
this.$fhcAlert.alertSuccess(this.$p.t('cms/versionGeloescht'));
this.$emit('reload-content-info');
// The tree shows the title of the newest version, so the label can change.
this.$emit('refresh-tree-node', this.contentId);
}).catch(this.$fhcAlert.handleSystemError);
},
deleteContent() {
this.$fhcAlert.confirm({
message: this.$p.t('cms/contentLoeschenBestaetigen', [
this.contentInfo.languages.length,
this.totalVersionCount
]),
acceptLabel: this.$p.t('ui/loeschen'),
acceptClass: 'p-button-danger'
}).then(confirmed => {
if (!confirmed) return;
return this.$api.call(ApiCmsAdmin.deleteContent(this.contentId));
}).then(result => {
if (!result) return;
this.$fhcAlert.alertSuccess(this.$p.t('cms/contentGeloescht'));
// Children of the deleted entry can reappear under another parent, so the
// whole tree has to be rebuilt here.
this.$emit('reload-tree');
this.$nextTick(() => {
this.$emit('select-content', null);
});
}).catch(this.$fhcAlert.handleSystemError);
}
},
mounted() {
this.loadUsage();
},
// Five tables prevent the deletion with ON DELETE RESTRICT. tbl_infoscreen_content
// deletes its own rows with CASCADE. The legacy code shows neither case and hides the
// error. See Q5 in the contract.
template: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/dieseVersionLoeschen') }}</h5>
<div v-if="isLastVersion" class="alert alert-warning mb-3">
{{ $p.t('cms/letzteVersionNichtLoeschbar') }}
</div>
<div v-else class="mb-3">
<button class="btn btn-outline-danger" @click="deleteVersion">
{{ $p.t('cms/dieseVersionLoeschen') }}
</button>
</div>
<hr>
<h5>{{ $p.t('cms/ganzenContentLoeschen') }}</h5>
<div v-if="loadingUsage" class="mb-3">
<div class="spinner-border spinner-border-sm" role="status"></div>
</div>
<template v-else>
<div v-if="usages.length === 0" class="alert alert-info mb-3">
{{ $p.t('cms/keineVerwendungGefunden') }}
</div>
<div v-else class="mb-3">
<div class="alert alert-warning">
{{ $p.t('cms/contentWirdVerwendetIn') }}
</div>
<ul class="list-group mb-3">
<li v-for="(u, i) in usages" :key="i"
class="list-group-item d-flex justify-content-between
align-items-center gap-3">
<span class="me-auto">
<span class="d-block">{{ u.label || '—' }}</span>
<!-- The value alone says nothing about where the entry
sits. The table is a database name and stays as it is. -->
<code class="small text-muted">{{ u.table }}</code>
</span>
<span v-if="u.table === 'campus.tbl_infoscreen_content'"
class="badge bg-danger text-nowrap">
{{ $p.t('cms/wirdMitgeloescht') }}
</span>
<span v-else class="badge bg-warning text-dark text-nowrap">
{{ $p.t('cms/verhindertLoeschen') }}
</span>
</li>
</ul>
</div>
</template>
<button class="btn btn-danger" @click="deleteContent"
:disabled="hasRestrict || loadingUsage">
{{ $p.t('cms/ganzenContentLoeschen') }}
</button>
</div>
`
};
-332
View File
@@ -1,332 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsHierarchy',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'reload-tree', 'refresh-tree-node', 'select-content'],
data() {
return {
parents: [],
childs: [],
possibleChilds: [],
selectedChild: '',
dragIndex: null,
orderBefore: null,
dropped: false,
savingOrder: false
};
},
computed: {
// tbl_contentchild has no unique index. The server permits a duplicate assignment.
// Therefore the select list hides the children that are already assigned.
availableChilds() {
const assigned = new Set(this.childs.map(c => c.child_content_id));
return this.possibleChilds.filter(c => !assigned.has(c.content_id));
}
},
watch: {
contentId() { this.loadAll(); },
sprache() { this.loadAll(); }
},
methods: {
loadAll() {
this.loadParents();
this.loadChilds();
this.loadPossibleChilds();
},
loadParents() {
if (this.contentId == null || !this.sprache) return;
this.$api
.call(ApiCmsAdmin.getParents(this.contentId, this.sprache))
.then(result => { this.parents = result.data || []; })
.catch(this.$fhcAlert.handleSystemError);
},
loadChilds() {
if (this.contentId == null || !this.sprache) return;
this.$api
.call(ApiCmsAdmin.getChilds(this.contentId, this.sprache))
.then(result => { this.childs = result.data || []; })
.catch(this.$fhcAlert.handleSystemError);
},
loadPossibleChilds() {
if (this.contentId == null || !this.sprache) return;
this.$api
.call(ApiCmsAdmin.getPossibleChilds(this.contentId, this.sprache))
.then(result => {
this.possibleChilds = result.data || [];
this.selectedChild = '';
})
.catch(this.$fhcAlert.handleSystemError);
},
childLabel(c) {
const titel = c.titel || this.$p.t('cms/ohneTitel');
return titel + ' (' + c.content_id + ')';
},
async addChild() {
if (!this.selectedChild) return;
try {
await this.$api.call(
ApiCmsAdmin.postChild(this.contentId, this.selectedChild)
);
this.loadAll();
// Only this branch changes, so refresh this branch.
this.$emit('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/childHinzugefuegt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
async removeChild(contentchild_id) {
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('cms/zuordnungEntfernenBestaetigen')
});
if (!confirmed) return;
try {
await this.$api.call(ApiCmsAdmin.deleteChild(contentchild_id));
this.loadAll();
this.$emit('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/childEntfernt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
// Detaches this content from one parent. That is the same row as a child
// assignment, seen from the other end, so it uses the same endpoint.
async removeParent(contentchild_id) {
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('cms/zuordnungEntfernenBestaetigen')
});
if (!confirmed) return;
try {
await this.$api.call(ApiCmsAdmin.deleteChild(contentchild_id));
this.loadParents();
// The content leaves one branch and may become a root, so rebuild the tree.
this.$emit('reload-tree');
this.$fhcAlert.alertSuccess(this.$p.t('cms/childEntfernt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
async moveSort(contentchild_id, direction) {
try {
await this.$api.call(
ApiCmsAdmin.putChildSort(contentchild_id, direction)
);
this.loadChilds();
this.$emit('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/sortierungGespeichert'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
// --- Drag and drop ---
// The dragged row follows the cursor: every row it enters swaps places with it.
// The drop then only has to write the order that already stands on screen.
onDragStart(index, event) {
this.dragIndex = index;
this.dropped = false;
this.orderBefore = this.childs.map(c => c.contentchild_id);
event.dataTransfer.effectAllowed = 'move';
// Firefox starts no drag without a payload.
event.dataTransfer.setData('text/plain', String(index));
},
onDragEnter(index) {
if (this.dragIndex === null || this.dragIndex === index) return;
const moved = this.childs.splice(this.dragIndex, 1)[0];
this.childs.splice(index, 0, moved);
this.dragIndex = index;
},
onDrop() {
this.dropped = true;
},
onDragEnd() {
if (this.dragIndex === null) return;
const before = this.orderBefore;
this.dragIndex = null;
this.orderBefore = null;
// Escape cancels the drag. Put the rows back.
if (!this.dropped) {
this.applyOrder(before);
return;
}
if (this.orderDiffers(before)) this.saveOrder(before);
},
applyOrder(contentchild_ids) {
if (!contentchild_ids) return;
const byId = new Map(this.childs.map(c => [c.contentchild_id, c]));
this.childs = contentchild_ids.map(id => byId.get(id)).filter(c => c);
},
orderDiffers(contentchild_ids) {
if (!contentchild_ids) return false;
return this.childs.some((c, i) => c.contentchild_id !== contentchild_ids[i]);
},
// One request for the whole order. A child moves over any distance, and the arrow
// endpoint swaps two neighbours, so it would need one request per position.
async saveOrder(before) {
this.savingOrder = true;
try {
await this.$api.call(ApiCmsAdmin.putChildOrder(
this.contentId,
this.childs.map(c => c.contentchild_id)
));
this.loadChilds();
this.$emit('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/sortierungGespeichert'));
} catch (e) {
this.applyOrder(before);
this.$fhcAlert.handleSystemError(e);
} finally {
this.savingOrder = false;
}
},
onContentClick(content_id) {
this.$emit('select-content', content_id);
},
isFirst(index) {
return index === 0;
},
isLast(index) {
return index === this.childs.length - 1;
}
},
mounted() {
this.loadAll();
},
template: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/folgendeEintraegeSindUebergeordnet') }}</h5>
<table v-if="parents.length > 0" class="table table-sm table-striped">
<thead>
<tr>
<th>ID</th>
<th>{{ $p.t('cms/titel') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="parent in parents" :key="parent.contentchild_id">
<td>{{ parent.content_id }}</td>
<td>
<a href="#" @click.prevent="onContentClick(parent.content_id)">
{{ parent.titel || $p.t('cms/ohneTitel') }}
</a>
</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="removeParent(parent.contentchild_id)">
{{ $p.t('cms/entfernen') }}
</button>
</td>
</tr>
</tbody>
</table>
<div v-else class="text-muted">
{{ $p.t('cms/keineUebergeordnetenEintraege') }}
</div>
<h5 class="mt-4">{{ $p.t('cms/folgendeEintraegeSindUntergeordnet') }}</h5>
<template v-if="childs.length > 0">
<div class="form-text mb-1">
<i v-if="savingOrder" class="fa-solid fa-spinner fa-spin me-1"></i>
{{ $p.t('cms/ziehenZumSortieren') }}
</div>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>{{ $p.t('cms/sortierung') }}</th>
<th>ID</th>
<th>{{ $p.t('cms/titel') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(child, idx) in childs" :key="child.contentchild_id"
draggable="true"
:style="dragIndex === idx ? 'opacity:0.4' : ''"
@dragstart="onDragStart(idx, $event)"
@dragenter.prevent="onDragEnter(idx)"
@dragover.prevent
@drop.prevent="onDrop"
@dragend="onDragEnd">
<td class="text-nowrap">
<i class="fa-solid fa-grip-vertical text-muted me-1"
style="cursor:grab"
:title="$p.t('cms/ziehenZumSortieren')"></i>
{{ idx + 1 }}
<button class="btn btn-sm btn-link p-0 ms-1"
:disabled="isFirst(idx)"
@click="moveSort(child.contentchild_id, 'up')">
<i class="fa-solid fa-arrow-up"></i>
</button>
<button class="btn btn-sm btn-link p-0 ms-1"
:disabled="isLast(idx)"
@click="moveSort(child.contentchild_id, 'down')">
<i class="fa-solid fa-arrow-down"></i>
</button>
</td>
<td>{{ child.child_content_id }}</td>
<td>
<a href="#" draggable="false"
@click.prevent="onContentClick(child.child_content_id)">
{{ child.titel || $p.t('cms/ohneTitel') }}
</a>
</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="removeChild(child.contentchild_id)">
{{ $p.t('cms/entfernen') }}
</button>
</td>
</tr>
</tbody>
</table>
</template>
<div v-else class="text-muted">
{{ $p.t('cms/keineUntereintraege') }}
</div>
<div class="d-flex gap-2 align-items-end mt-3">
<select class="form-select" v-model="selectedChild" style="max-width: 500px;">
<option value="" disabled></option>
<option v-for="c in availableChilds" :key="c.content_id"
:value="c.content_id">
{{ childLabel(c) }}
</option>
</select>
<button class="btn btn-primary" @click="addChild" :disabled="!selectedChild">
{{ $p.t('cms/hinzufuegen') }}
</button>
</div>
</div>
`
};
-65
View File
@@ -1,65 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsHistory',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'reload-tree', 'select-content'],
data() {
return {
versions: []
};
},
watch: {
contentId() { this.loadVersions(); },
sprache() { this.loadVersions(); }
},
methods: {
loadVersions() {
if (this.contentId == null || !this.sprache) return;
this.$api
.call(ApiCmsAdmin.getVersions(this.contentId, this.sprache))
.then(result => {
this.versions = result.data || [];
})
.catch(this.$fhcAlert.handleSystemError);
},
formatDate(dateStr) {
if (!dateStr) return '';
try {
const dt = luxon.DateTime.fromSQL(dateStr);
return dt.isValid ? dt.toFormat('dd.MM.yyyy') : dateStr;
} catch (e) {
return dateStr;
}
}
},
mounted() {
this.loadVersions();
},
template: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/versionen') }}</h5>
<div v-for="v in versions" :key="v.version"
class="mb-3 p-2 border rounded"
:class="{ 'border-primary border-2': v.version === version }">
<div class="fw-bold">
{{ $p.t('cms/version') }} {{ v.version }}
<span v-if="!v.sichtbar" class="badge bg-secondary ms-2">
{{ $p.t('cms/unsichtbar') }}
</span>
</div>
<div>{{ $p.t('cms/erstelltAmVon', [formatDate(v.insertamum), v.insertvon]) }}</div>
<div v-if="v.updatevon">
{{ $p.t('cms/letzteAenderungVonAm', [formatDate(v.updateamum), v.updatevon]) }}
</div>
</div>
</div>
`
};
@@ -1,143 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsPermissions',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'patch-tree-node', 'select-content'],
data() {
return {
gruppen: [],
allGruppen: [],
selectedGruppe: '',
allGruppenLoaded: false
};
},
computed: {
availableGruppen() {
const assigned = new Set(this.gruppen.map(g => g.gruppe_kurzbz));
return this.allGruppen.filter(g => !assigned.has(g.gruppe_kurzbz));
}
},
watch: {
contentId() { this.loadGruppen(); }
},
methods: {
loadGruppen() {
if (this.contentId == null) return Promise.resolve();
return this.$api
.call(ApiCmsAdmin.getGruppen(this.contentId))
.then(result => {
this.gruppen = result.data || [];
this.selectedGruppe = '';
})
.catch(this.$fhcAlert.handleSystemError);
},
// A group only draws the lock icon. It never moves a node, so the tree needs no
// request here.
patchTree() {
this.$emit('patch-tree-node', {
content_id: this.contentId,
patch: { groups: this.gruppen.map(g => g.gruppe_kurzbz) }
});
},
loadAllGruppen() {
if (this.allGruppenLoaded) return;
this.$api
.call(ApiCmsAdmin.getAllGruppen())
.then(result => {
this.allGruppen = result.data || [];
this.allGruppenLoaded = true;
})
.catch(this.$fhcAlert.handleSystemError);
},
async addGruppe() {
if (!this.selectedGruppe) return;
try {
await this.$api.call(
ApiCmsAdmin.postGruppe(this.contentId, this.selectedGruppe)
);
await this.loadGruppen();
this.patchTree();
this.$fhcAlert.alertSuccess(this.$p.t('cms/gruppeHinzugefuegt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
async removeGruppe(gruppe_kurzbz) {
if (this.gruppen.length <= 1) {
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('cms/letzteGruppeEntfernenBestaetigen')
});
if (!confirmed) return;
}
try {
await this.$api.call(
ApiCmsAdmin.deleteGruppe(this.contentId, gruppe_kurzbz)
);
await this.loadGruppen();
this.patchTree();
this.$fhcAlert.alertSuccess(this.$p.t('cms/gruppeEntfernt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
}
},
mounted() {
this.loadAllGruppen();
this.loadGruppen();
},
template: `
<div class="p-3" v-if="contentId != null">
<template v-if="gruppen.length > 0">
<h5>{{ $p.t('cms/folgendeGruppenDuerfenAnsehen') }}</h5>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>{{ $p.t('cms/gruppe') }}</th>
<th>{{ $p.t('cms/bezeichnung') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="g in gruppen" :key="g.gruppe_kurzbz">
<td>{{ g.gruppe_kurzbz }}</td>
<td>{{ g.bezeichnung }}</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="removeGruppe(g.gruppe_kurzbz)">
{{ $p.t('cms/entfernen') }}
</button>
</td>
</tr>
</tbody>
</table>
</template>
<div v-else class="alert alert-warning">
{{ $p.t('cms/seiteDarfVonAllenAngezeigtWerden') }}
</div>
<div class="d-flex gap-2 align-items-end mt-3">
<select class="form-select" v-model="selectedGruppe" style="max-width: 400px;">
<option value="" disabled></option>
<option v-for="g in availableGruppen" :key="g.gruppe_kurzbz"
:value="g.gruppe_kurzbz">
{{ g.gruppe_kurzbz }} — {{ g.bezeichnung }}
</option>
</select>
<button class="btn btn-primary" @click="addGruppe" :disabled="!selectedGruppe">
{{ $p.t('cms/hinzufuegen') }}
</button>
</div>
</div>
`
};

Some files were not shown because too many files have changed in this diff Show More