From d900291bf7e6be1468116e5806310668122b9095 Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Wed, 29 Apr 2009 15:52:58 +0200 Subject: [PATCH] Import skoli. --- skoli/LICENSE | 48 ++ skoli/README | 89 +++ skoli/add.php | 44 ++ skoli/classes/create.php | 51 ++ skoli/classes/delete.php | 54 ++ skoli/classes/edit.php | 81 +++ skoli/classes/index.php | 41 ++ skoli/config/conf.xml | 62 ++ skoli/config/prefs.php.dist | 207 ++++++ skoli/config/schools.php.dist | 116 ++++ skoli/data.php | 185 +++++ skoli/docs/CHANGES | 5 + skoli/docs/CREDITS | 17 + skoli/docs/INSTALL | 243 +++++++ skoli/docs/RELEASE_NOTES | 52 ++ skoli/docs/TODO | 15 + skoli/entry.php | 121 ++++ skoli/index.php | 24 + skoli/lib/Block/tree_menu.php | 57 ++ skoli/lib/Driver.php | 153 +++++ skoli/lib/Driver/sql.php | 694 +++++++++++++++++++ skoli/lib/Forms/CreateClass.php | 236 +++++++ skoli/lib/Forms/DeleteClass.php | 82 +++ skoli/lib/Forms/EditClass.php | 176 +++++ skoli/lib/Forms/Entry.php | 184 +++++ skoli/lib/School.php | 288 ++++++++ skoli/lib/Skoli.php | 1083 ++++++++++++++++++++++++++++++ skoli/lib/base.php | 52 ++ skoli/lib/version.php | 1 + skoli/list.php | 165 +++++ skoli/locale/de_DE/LC_MESSAGES/skoli.mo | Bin 0 -> 176332 bytes skoli/locale/de_DE/help.xml | 15 + skoli/locale/en_US/help.xml | 17 + skoli/po/README | 1 + skoli/po/de_DE.po | 997 +++++++++++++++++++++++++++ skoli/po/skoli.pot | 1006 +++++++++++++++++++++++++++ skoli/pref_api.php | 71 ++ skoli/scripts/.htaccess | 1 + skoli/scripts/sql/skoli.sql | 83 +++ skoli/search.php | 184 +++++ skoli/templates/classes/list.php | 35 + skoli/templates/common-header.inc | 38 ++ skoli/templates/data/export.inc | 34 + skoli/templates/entry/delete.inc | 10 + skoli/templates/list/classes.inc | 48 ++ skoli/templates/list/empty.inc | 3 + skoli/templates/list/footers.inc | 6 + skoli/templates/list/header.inc | 10 + skoli/templates/list/headers.inc | 96 +++ skoli/templates/list/students.inc | 51 ++ skoli/templates/menu.inc | 4 + skoli/templates/panel.inc | 77 +++ skoli/templates/search/criteria.inc | 44 ++ skoli/templates/search/empty.inc | 3 + skoli/templates/search/entries.inc | 27 + skoli/templates/search/footers.inc | 6 + skoli/templates/search/header.inc | 11 + skoli/templates/search/headers.inc | 50 ++ skoli/themes/categoryCSS.php | 36 + skoli/themes/graphics/add.png | Bin 0 -> 3895 bytes skoli/themes/graphics/az.png | Bin 0 -> 117 bytes skoli/themes/graphics/favicon.ico | Bin 0 -> 792 bytes skoli/themes/graphics/minus.png | Bin 0 -> 203 bytes skoli/themes/graphics/plus.png | Bin 0 -> 229 bytes skoli/themes/graphics/redbox_spinner.gif | Bin 0 -> 6820 bytes skoli/themes/graphics/search.png | Bin 0 -> 794 bytes skoli/themes/graphics/skoli.png | Bin 0 -> 3921 bytes skoli/themes/graphics/timetable.png | Bin 0 -> 501 bytes skoli/themes/graphics/za.png | Bin 0 -> 119 bytes skoli/themes/screen.css | 166 +++++ 70 files changed, 7756 insertions(+) create mode 100644 skoli/LICENSE create mode 100644 skoli/README create mode 100644 skoli/add.php create mode 100644 skoli/classes/create.php create mode 100644 skoli/classes/delete.php create mode 100644 skoli/classes/edit.php create mode 100644 skoli/classes/index.php create mode 100644 skoli/config/conf.xml create mode 100644 skoli/config/prefs.php.dist create mode 100644 skoli/config/schools.php.dist create mode 100644 skoli/data.php create mode 100644 skoli/docs/CHANGES create mode 100644 skoli/docs/CREDITS create mode 100644 skoli/docs/INSTALL create mode 100644 skoli/docs/RELEASE_NOTES create mode 100644 skoli/docs/TODO create mode 100644 skoli/entry.php create mode 100644 skoli/index.php create mode 100644 skoli/lib/Block/tree_menu.php create mode 100644 skoli/lib/Driver.php create mode 100644 skoli/lib/Driver/sql.php create mode 100644 skoli/lib/Forms/CreateClass.php create mode 100644 skoli/lib/Forms/DeleteClass.php create mode 100644 skoli/lib/Forms/EditClass.php create mode 100644 skoli/lib/Forms/Entry.php create mode 100644 skoli/lib/School.php create mode 100644 skoli/lib/Skoli.php create mode 100644 skoli/lib/base.php create mode 100644 skoli/lib/version.php create mode 100644 skoli/list.php create mode 100644 skoli/locale/de_DE/LC_MESSAGES/skoli.mo create mode 100644 skoli/locale/de_DE/help.xml create mode 100644 skoli/locale/en_US/help.xml create mode 100644 skoli/po/README create mode 100644 skoli/po/de_DE.po create mode 100644 skoli/po/skoli.pot create mode 100644 skoli/pref_api.php create mode 100755 skoli/scripts/.htaccess create mode 100755 skoli/scripts/sql/skoli.sql create mode 100644 skoli/search.php create mode 100644 skoli/templates/classes/list.php create mode 100644 skoli/templates/common-header.inc create mode 100644 skoli/templates/data/export.inc create mode 100644 skoli/templates/entry/delete.inc create mode 100644 skoli/templates/list/classes.inc create mode 100644 skoli/templates/list/empty.inc create mode 100644 skoli/templates/list/footers.inc create mode 100644 skoli/templates/list/header.inc create mode 100644 skoli/templates/list/headers.inc create mode 100644 skoli/templates/list/students.inc create mode 100644 skoli/templates/menu.inc create mode 100644 skoli/templates/panel.inc create mode 100644 skoli/templates/search/criteria.inc create mode 100644 skoli/templates/search/empty.inc create mode 100644 skoli/templates/search/entries.inc create mode 100644 skoli/templates/search/footers.inc create mode 100644 skoli/templates/search/header.inc create mode 100644 skoli/templates/search/headers.inc create mode 100644 skoli/themes/categoryCSS.php create mode 100644 skoli/themes/graphics/add.png create mode 100644 skoli/themes/graphics/az.png create mode 100644 skoli/themes/graphics/favicon.ico create mode 100644 skoli/themes/graphics/minus.png create mode 100644 skoli/themes/graphics/plus.png create mode 100644 skoli/themes/graphics/redbox_spinner.gif create mode 100644 skoli/themes/graphics/search.png create mode 100644 skoli/themes/graphics/skoli.png create mode 100644 skoli/themes/graphics/timetable.png create mode 100644 skoli/themes/graphics/za.png create mode 100644 skoli/themes/screen.css diff --git a/skoli/LICENSE b/skoli/LICENSE new file mode 100644 index 000000000..da074b87a --- /dev/null +++ b/skoli/LICENSE @@ -0,0 +1,48 @@ +Version 1.0 + +Copyright (c) 2002-2005 The Horde Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. The end-user documentation included with the redistribution, if +any, must include the following acknowledgment: + + "This product includes software developed by the Horde Project + (http://www.horde.org/)." + +Alternately, this acknowledgment may appear in the software itself, if +and wherever such third-party acknowledgments normally appear. + +4. The names "Horde", "The Horde Project", and "Mnemo" must not be +used to endorse or promote products derived from this software without +prior written permission. For written permission, please contact +core@horde.org. + +5. Products derived from this software may not be called "Horde" or +"Mnemo", nor may "Horde" or "Mnemo" appear in their name, without +prior written permission of the Horde Project. + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE HORDE PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +This software consists of voluntary contributions made by many +individuals on behalf of the Horde Project. For more information on +the Horde Project, please see . diff --git a/skoli/README b/skoli/README new file mode 100644 index 000000000..970529f86 --- /dev/null +++ b/skoli/README @@ -0,0 +1,89 @@ +What is Skoli? +============== + +:Last update: $Date: $ +:Revision: $Revision: 0.1 $ +:Contact: horde@lists.horde.org + +.. contents:: Contents +.. section-numbering:: + +Skoli is a simple administrative module for teachers. Several contacts +(students) will be summarized to a class. To each student one can then +enter marks, objectives, outcomes and absences. Besides offering a search +function Skoli also offers an export option in the CSV and TSV formats. + +This software is OSI Certified Open Source Software. OSI Certified is a +certification mark of the `Open Source Initiative`_. + +.. _`Open Source Initiative`: http://www.opensource.org/ + + +Obtaining Skoli +--------------- + +Further information on Skoli and the latest version can be obtained at + + http://www.horde.org/skoli/ + + +Documentation +------------- + +The following documentation is available in the Skoli distribution: + +:README_: This file +:LICENSE_: Copyright and license information +:`docs/CHANGES`_: Changes by release +:`docs/CREDITS`_: Project developers +:`docs/INSTALL`_: Installation instructions and notes +:`docs/TODO`_: Development TODO list +:`docs/UPGRADING`_: Pointers on upgrading from previous Skoli versions + + +Installation +------------ + +Instructions for installing Skoli can be found in the file INSTALL_ in the +``docs/`` directory of the Skoli distribution. + + +Assistance +---------- + +If you encounter problems with Skoli, help is available! + +The Horde Frequently Asked Questions List (FAQ), available on the Web at + + http://www.horde.org/faq/ + +The Horde Project runs a number of mailing lists, for individual applications +and for issues relating to the project as a whole. Information, archives, and +subscription information can be found at + + http://www.horde.org/mail/ + +Lastly, Horde developers, contributors and users also make occasional +appearances on IRC, on the channel #horde on the freenode Network +(irc.freenode.net). + + +Licensing +--------- + +For licensing and copyright information, please see the file LICENSE_ +in the Skoli distribution. + +Thanks, + +The Skoli team + + +.. _README: ?f=README.html +.. _LICENSE: http://www.horde.org/licenses/asl.php +.. _docs/CHANGES: ?f=CHANGES.html +.. _docs/CREDITS: ?f=CREDITS.html +.. _INSTALL: +.. _docs/INSTALL: ?f=INSTALL.html +.. _docs/TODO: ?f=TODO.html +.. _docs/UPGRADING: ?f=UPGRADING.html diff --git a/skoli/add.php b/skoli/add.php new file mode 100644 index 000000000..4ce9cf456 --- /dev/null +++ b/skoli/add.php @@ -0,0 +1,44 @@ + + */ + +@define('SKOLI_BASE', dirname(__FILE__)); +require_once SKOLI_BASE . '/lib/base.php'; +require_once SKOLI_BASE . '/lib/Forms/Entry.php'; + +/* Redirect to create a new class if we don't have access to any class */ +if (count(Skoli::listClasses(false, PERMS_EDIT)) == 0 && Auth::getAuth()) { + $notification->push(_("Please create a new Class first."), 'horde.message'); + header('Location: ' . Horde::applicationUrl('classes/create.php', true)); + exit; +} + +$vars = Variables::getDefaultVariables(); +$form = new Skoli_EntryForm($vars); + +// Execute if the form is valid. +if ($form->validate($vars)) { + $result = $form->execute(); + if (is_a($result, 'PEAR_Error')) { + $notification->push($result, 'horde.error'); + } else { + $notification->push(sprintf(_("The new entry for \"%s\" has been added."), $result), 'horde.success'); + } + + header('Location: ' . Horde::applicationUrl(Util::addParameter('add.php', 'class', $vars->get('class_id')), true)); + exit; +} + +$title = $form->getTitle(); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +echo $form->renderActive($form->getRenderer(), $vars, 'add.php', 'post'); +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/classes/create.php b/skoli/classes/create.php new file mode 100644 index 000000000..328b949fa --- /dev/null +++ b/skoli/classes/create.php @@ -0,0 +1,51 @@ +push(_("You don't have access to any valid addressbook."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} + +$vars = Variables::getDefaultVariables(); +$form = new Skoli_CreateClassForm($vars); + +// Execute if the form is valid. +if ($form->validate($vars)) { + $result = $form->execute(); + if (is_a($result, 'PEAR_Error')) { + $notification->push($result, 'horde.error'); + } else { + $notification->push(sprintf(_("The class \"%s\" has been created."), $vars->get('name')), 'horde.success'); + $GLOBALS['display_classes'][] = $form->shareid; + $prefs->setValue('display_classes', serialize($GLOBALS['display_classes'])); + } + + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} + +$title = $form->getTitle(); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +echo $form->renderActive($form->getRenderer(), $vars, 'create.php', 'post'); +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/classes/delete.php b/skoli/classes/delete.php new file mode 100644 index 000000000..3db6caf48 --- /dev/null +++ b/skoli/classes/delete.php @@ -0,0 +1,54 @@ +get('c'); + +$class = $skoli_shares->getShare($class_id); +if (is_a($class, 'PEAR_Error')) { + $notification->push($class, 'horde.error'); + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} elseif (!$class->hasPermission(Auth::getAuth(), PERMS_DELETE)) { + $notification->push(_("You are not allowed to delete this class."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} + +$form = new Skoli_DeleteClassForm($vars, $class); + +// Execute if the form is valid (must pass with POST variables only). +if ($form->validate(new Variables($_POST))) { + $result = $form->execute(); + if (is_a($result, 'PEAR_Error')) { + $notification->push($result, 'horde.error'); + } elseif ($result) { + $notification->push(sprintf(_("The class \"%s\" has been deleted."), $class->get('name')), 'horde.success'); + } + + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} + +$title = $form->getTitle(); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +echo $form->renderActive($form->getRenderer(), $vars, 'delete.php', 'post'); +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/classes/edit.php b/skoli/classes/edit.php new file mode 100644 index 000000000..2c594e7cb --- /dev/null +++ b/skoli/classes/edit.php @@ -0,0 +1,81 @@ +getShare($vars->get('c')); +if (is_a($class, 'PEAR_Error')) { + $notification->push($class, 'horde.error'); + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} elseif (!$class->hasPermission(Auth::getAuth(), PERMS_EDIT)) { + $notification->push(_("You are not allowed to change this class."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} +$vars->set('school', $class->get('school')); +if (!$vars->exists('marks')) { + $vars->set('marks', $class->get('marks')); +} +if (!$vars->exists('address_book')) { + $vars->set('address_book', $class->get('address_book')); +} + +$form = new Skoli_EditClassForm($vars, $class); + +// Execute if the form is valid. +if ($form->validate($vars)) { + $original_name = $class->get('name'); + $result = $form->execute(); + if (is_a($result, 'PEAR_Error')) { + $notification->push($result, 'horde.error'); + } else { + if ($class->get('name') != $original_name) { + $notification->push(sprintf(_("The class \"%s\" has been renamed to \"%s\"."), $original_name, $class->get('name')), 'horde.success'); + } else { + $notification->push(sprintf(_("The class \"%s\" has been saved."), $original_name), 'horde.success'); + } + } + + header('Location: ' . Horde::applicationUrl('classes/', true)); + exit; +} + +if (!$vars->exists('name')) { + $vars->set('name', $class->get('name')); + $vars->set('description', $class->get('desc')); + $vars->set('category', $class->get('category')); + foreach ($form->_schoolproperties as $name) { + if ($name != 'marks') { + $vars->set($name, $class->get($name)); + } + } + $studentslist = current(Skoli::listStudents($vars->get('c'))); + $studentsvars = array(); + foreach ($studentslist['_students'] as $student) { + $studentsvars[] = $student['__key']; + } + $vars->set('students', $studentsvars); +} + +$title = $form->getTitle(); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +echo $form->renderActive($form->getRenderer(), $vars, 'edit.php', 'post'); +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/classes/index.php b/skoli/classes/index.php new file mode 100644 index 000000000..2bf6d1923 --- /dev/null +++ b/skoli/classes/index.php @@ -0,0 +1,41 @@ +get('webroot', 'horde') . '/services/shares/edit.php?app=skoli', true); +$delete_url_base = Horde::applicationUrl('classes/delete.php'); + +$classes = Skoli::listClasses(true); +$sorted_classes = array(); +foreach ($classes as $class) { + $sorted_classes[$class->getName()] = $class->get('name'); +} +asort($sorted_classes); + +$edit_img = Horde::img('edit.png', _("Edit"), null, $registry->getImageDir('horde')); +$perms_img = Horde::img('perms.png', _("Change Permissions"), null, $registry->getImageDir('horde')); +$delete_img = Horde::img('delete.png', _("Delete"), null, $registry->getImageDir('horde')); + +Horde::addScriptFile('popup.js', 'horde', true); +Horde::addScriptFile('tables.js', 'horde', true); +$title = _("Manage Classes"); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +require SKOLI_TEMPLATES . '/classes/list.php'; +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/config/conf.xml b/skoli/config/conf.xml new file mode 100644 index 000000000..e61238f0a --- /dev/null +++ b/skoli/config/conf.xml @@ -0,0 +1,62 @@ + + + + + + Storage System Settings + + sql + + + + + + + + + + + + Settings for new Objects + + true + true + true + true + + + + + Address settings + + ask + + + + localsql + + + name + user + + + + + + %c - %g - %s + + + + + + + Menu settings + + true + + + + + + + diff --git a/skoli/config/prefs.php.dist b/skoli/config/prefs.php.dist new file mode 100644 index 000000000..7a974ff10 --- /dev/null +++ b/skoli/config/prefs.php.dist @@ -0,0 +1,207 @@ + _("General Options"), + 'label' => _("Display Options"), + 'desc' => _("Change your sorting and display options."), + 'members' => array('initial_page', 'class_columns', 'sortby_class', 'sortdir_class', 'student_columns', 'sortby_student', 'sortdir_student', 'entry_details_wrap'), +); + +$prefGroups['contactlists'] = array( + 'column' => _("General Options"), + 'label' => _("Contact Lists"), + 'desc' => _("Change your settings for automatically create contact lists."), + 'members' => $GLOBALS['conf']['addresses']['contact_list'] == 'user' ? array('contact_list', 'contact_list_name') : array(), +); + +$prefGroups['marks'] = array( + 'column' => _("General Options"), + 'label' => _("Marks"), + 'desc' => _("Define a format for marks"), + 'members' => array('marks_roundby') +); + +// default view +$_prefs['initial_page'] = array( + 'value' => 'list', + 'locked' => false, + 'shared' => false, + 'type' => 'enum', + 'enum' => array('list' => _("List Classes"), + 'add' => _("New Entry"), + 'search' => _("Search")), + 'desc' => _("Select the view to display after login:") +); + +// Load constants from lib/Skoli.php +require_once dirname(__FILE__) . '/../lib/Skoli.php'; + +// columns in the class list view +$_prefs['class_columns'] = array( + 'value' => 'a:6:{i:0;s:13:"semesterstart";i:1;s:11:"semesterend";i:2;s:5:"grade";i:3;s:8:"semester";i:4;s:8:"location";i:5;s:8:"category";}', + 'locked' => false, + 'shared' => false, + 'type' => 'multienum', + 'enum' => array(SKOLI_SORT_SEMESTERSTART => _("Semester Start"), + SKOLI_SORT_SEMESTEREND => _("Semester End"), + SKOLI_SORT_GRADE => _("Grade"), + SKOLI_SORT_SEMESTER => _("Semester"), + SKOLI_SORT_LOCATION => _("Location"), + SKOLI_SORT_CATEGORY => _("Category")), + 'desc' => _("Select the columns that should be shown in the class list view:") +); + +// user preferred sorting column for classes +$_prefs['sortby_class'] = array( + 'value' => SKOLI_SORT_SEMESTERSTART, + 'locked' => false, + 'shared' => false, + 'type' => 'enum', + 'enum' => array(SKOLI_SORT_SEMESTERSTART => _("Semester Start"), + SKOLI_SORT_SEMESTEREND => _("Semester End"), + SKOLI_SORT_NAME => _("Name"), + SKOLI_SORT_GRADE => _("Grade"), + SKOLI_SORT_SEMESTER => _("Semester"), + SKOLI_SORT_LOCATION => _("Location"), + SKOLI_SORT_CATEGORY => _("Category")), + 'desc' => _("Sort classes by:"), +); + +// user preferred sorting direction for classes +$_prefs['sortdir_class'] = array( + 'value' => SKOLI_SORT_ASCEND, + 'locked' => false, + 'shared' => false, + 'type' => 'enum', + 'enum' => array(SKOLI_SORT_ASCEND => _("Ascending"), + SKOLI_SORT_DESCEND => _("Descending")), + 'desc' => _("Sort direction for classes:"), +); + +// columns in the student list view +$_prefs['student_columns'] = array( + 'value' => 'a:3:{i:0;s:9:"lastentry";i:1;s:8:"summarks";i:2;s:11:"sumabsences";}', + 'locked' => false, + 'shared' => false, + 'type' => 'multienum', + 'enum' => array(SKOLI_SORT_LASTENTRY => _("Last Entry"), + SKOLI_SORT_SUMMARKS => _("Mark average"), + SKOLI_SORT_SUMABSENCES => _("Absences")), + 'desc' => _("Select the columns that should be shown in the student list view:") +); + +// user preferred sorting column for students +$_prefs['sortby_student'] = array( + 'value' => SKOLI_SORT_NAME, + 'locked' => false, + 'shared' => false, + 'type' => 'enum', + 'enum' => array(SKOLI_SORT_NAME => _("Name"), + SKOLI_SORT_LASTENTRY => _("Last Entry"), + SKOLI_SORT_SUMMARKS => _("Mark average"), + SKOLI_SORT_SUMABSENCES => _("Absences")), + 'desc' => _("Sort students by:"), +); + +// user preferred sorting direction for students +$_prefs['sortdir_student'] = array( + 'value' => SKOLI_SORT_ASCEND, + 'locked' => false, + 'shared' => false, + 'type' => 'enum', + 'enum' => array(SKOLI_SORT_ASCEND => _("Ascending"), + SKOLI_SORT_DESCEND => _("Descending")), + 'desc' => _("Sort direction for students:"), +); + +// preference for wrapping the details for an entry. +$_prefs['entry_details_wrap'] = array( + 'value' => 100, + 'locked' => false, + 'shared' => false, + 'type' => 'number', + 'desc' => _("How many characters of the entry details in search view should we allow to see?") +); + +// preference for contact lists. +$_prefs['contact_list'] = array( + 'value' => 'ask', + 'locked' => false, + 'shared' => false, + 'type' => 'enum', + 'enum' => array('ask' => _("Ask every time"), + 'none' => _("Don't create contact lists"), + 'auto' => _("Automatically create a new contact list")), + 'desc' => _("When a new class is created should we also create a new contact list?") +); + +// template for new contact lists. +$_prefs['contact_list_name'] = array( + 'value' => "%c - %g - %s", + 'locked' => false, + 'shared' => false, + 'type' => 'text', + 'desc' => _("Enter a default name for new contact lists.
NOTE: You can use %c, %g or %s as substitution for the class, grade respectively semester name.") +); + +// preference for rounding marks. +$_prefs['marks_roundby'] = array( + 'value' => 2, + 'locked' => false, + 'shared' => false, + 'type' => 'number', + 'desc' => _("How many decimal digits should we round marks to?") +); + +// custom settings for marks +$_prefs['marks_format_custom'] = array( + 'value' => "6, 5.5, 5, 4.5, 4, 3.5, 3, 2.5, 2, 1.5, 1", + 'locked' => false, + 'shared' => false, + 'type' => 'text', + 'desc' => _("Enter some custom marks and separate them by comma (best mark first).
NOTE: You also need to choose \"Custom settings\" above.") +); + +/** + * Hidden preferences + */ + +// show the class list options panel? +// a value of 0 = no, 1 = yes +$_prefs['show_panel'] = array( + 'value' => 1, + 'locked' => false, + 'shared' => false, + 'type' => 'checkbox', + 'desc' => _("Show class list options panel?") +); + +// show students in the class list view? +$_prefs['show_students'] = array( + 'value' => 1, + 'locked' => false, + 'shared' => false, + 'type' => 'checkbox', + 'desc' => _("Show students in the class list?"), +); + +// store the class lists to diplay +$_prefs['display_classes'] = array( + 'value' => 'a:0:{}', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); + +// store the last object format when adding a new entry +$_prefs['default_objects_format'] = array( + 'value' => 'mark', + 'locked' => false, + 'shared' => false, + 'type' => 'implicit' +); diff --git a/skoli/config/schools.php.dist b/skoli/config/schools.php.dist new file mode 100644 index 000000000..2573fa14e --- /dev/null +++ b/skoli/config/schools.php.dist @@ -0,0 +1,116 @@ + _("Custom school") +); + +/** + * The following school may be used for primary schools in Bern, Switzerland. + */ +$cfgSchools['prim_be'] = array( + 'title' => _("Sample school"), + 'grade' => array( + _("1. class"), + _("2. class"), + _("3. class"), + _("4. class"), + _("5. class"), + _("6. class") + ), + 'semester' => array( + array( + 'name' => _("1. term"), + 'start' => 'W33-1', + 'end' => 'W05-5' + ), + array( + 'name' => _("2. term"), + 'start' => 'W07-1', + 'end' => 'W27-5' + ) + ), + 'location' => array( + _("Schoolhouse 1"), + _("Schoolhouse 2") + ), + 'marks' => '6, 5.5, 5, 4.5, 4, 3.5, 3, 2.5, 2, 1.5, 1', + 'subjects' => array( + _("German") => array( + _("Hearing and Talking"), + _("Reading"), + _("Writing"), + ), + _("Mathematics") => array( + _("Imagination"), + _("Skills"), + _("Appliance"), + _("Problem solving behavior"), + ), + _("Nature-Human-Environment"), + _("Music"), + _("Sport"), + _("Construct"), + _("French") => array( + _("Hearing"), + _("Talking"), + _("Reading"), + _("Writing"), + ), + _("English") => array( + _("Hearing"), + _("Talking"), + _("Reading"), + _("Writing"), + ), + ), + 'objectives' => array( + _("Motivation to learn and dedication"), + _("Concentration, attention, perseverance"), + _("Exercise processing"), + _("Teamwork and autonomy"), + ), +); diff --git a/skoli/data.php b/skoli/data.php new file mode 100644 index 000000000..145890eca --- /dev/null +++ b/skoli/data.php @@ -0,0 +1,185 @@ + + */ + +require_once dirname(__FILE__) . '/lib/base.php'; +require_once 'Horde/Data.php'; + +if (!$conf['menu']['export']) { + header('Location: ' . Horde::applicationUrl('list.php', true)); + exit; +} + +$classes = Skoli::listClasses(); + +/* If there are no valid classes, abort. */ +if (count($classes) == 0) { + $notification->push(_("No classes are currently available. Export is disabled."), 'horde.error'); + require SKOLI_TEMPLATES . '/common-header.inc'; + require SKOLI_TEMPLATES . '/menu.inc'; + require $registry->get('templates', 'horde') . '/common-footer.inc'; + exit; +} + +$class_options = array(); +foreach ($classes as $key=>$class) { + $class_options[] = '\n"; +} + +$wholeclass_option = '\n"; +$student_options = array(); +$student_options[] = $wholeclass_option; +if (Util::getFormData('class') != '') { + $class = Util::getFormData('class'); +} else { + reset($classes); + $class = key($classes); +} +$export_class = current(Skoli::listStudents($class, SKOLI_SORT_NAME, SKOLI_SORT_ASCEND)); +foreach ($export_class['_students'] as $address) { + $student_options[] = '\n"; +} + +$actionID = Util::getFormData('actionID'); + +/* Loop through the action handlers. */ +switch ($actionID) { +case 'export': + $data = array(); + $driver = &Skoli_Driver::singleton($class); + if (Util::getFormData('student') == 'all') { + /* Export whole class. */ + $subjects = $driver->getSubjects('mark'); + foreach ($export_class['_students'] as $student) { + $row = array(); + $row[_("Class")] = $export_class['name']; + $row[_("Firstname")] = $student['firstname']; + $row[_("Lastname")] = $student['lastname']; + + /* Absences */ + $absences = Skoli::sumAbsences($class, $student['student_id']); + $row[_("Excused absences")] = $absences[0]; + $row[_("Absences without valid excuse")] = $absences[1]; + + /* Marks */ + foreach ($subjects as $subject) { + $row[$subject] = Skoli::sumMarks($class, $student['student_id'], $subject); + } + + /* Outcomes */ + $outcomes = Skoli::sumOutcomes($class, $student['student_id']); + $row[_("Completed outcomes")] = $outcomes[0]; + $row[_("Open outcomes")] = $outcomes[1]; + + $data[] = $row; + } + /* Make sure that only columns with data are exportet. */ + if (count($data)) { + foreach ($data[0] as $key=>$value) { + $emptycolumn = true; + foreach ($data as $row) { + if ($row[$key] !== '') { + $emptycolumn = false; + break; + } + } + if ($emptycolumn) { + foreach ($data as $rowkey=>$row) { + unset($data[$rowkey][$key]); + } + } + } + } + } else { + /* Export all entries for the selected student. */ + $data[] = array(_("Marks")); + $subjects = $driver->getSubjects('mark'); + foreach ($subjects as $subject) { + $params = array(array('name' => 'subject', 'value' => $subject, 'strict' => 1)); + $marks = Skoli::listEntries($class, Util::getFormData('student'), 'mark', $params, SKOLI_SORT_DATE, SKOLI_SORT_DESCEND); + foreach ($marks as $mark) { + $data[] = array($subject, $mark['date'], $mark['title'], Skoli::convertNumber($mark['mark']), Skoli::convertNumber($mark['weight'])); + } + } + + $data[] = array(_("Objectives")); + $subjects = $driver->getSubjects('objective'); + foreach ($subjects as $subject) { + $params = array(array('name' => 'subject', 'value' => $subject, 'strict' => 1)); + $objectives = Skoli::listEntries($class, Util::getFormData('student'), 'objective', $params, SKOLI_SORT_DATE, SKOLI_SORT_DESCEND); + foreach ($objectives as $objective) { + $data[] = array($subject, $objective['date'], $objective['category'], $objective['objective']); + } + } + + $data[] = array(_("Outcomes")); + $outcomes = Skoli::listEntries($class, Util::getFormData('student'), 'outcome', null, SKOLI_SORT_DATE, SKOLI_SORT_DESCEND); + foreach ($outcomes as $outcome) { + $completed = isset($outcome['completed']) && $outcome['completed'] != '' ? _("Completed") : _("Open"); + $comment = isset($outcome['comment']) ? $outcome['comment'] : ''; + $data[] = array($outcome['date'], $outcome['outcome'], $completed, $comment); + } + + $data[] = array(_("Absences")); + $absences = Skoli::listEntries($class, Util::getFormData('student'), 'absence', null, SKOLI_SORT_DATE, SKOLI_SORT_DESCEND); + foreach ($absences as $absence) { + $excused = isset($absence['excused']) && $absence['excused'] != '' ? _("Excused") : _("Not excused"); + $comment = isset($absence['comment']) ? $absence['comment'] : ''; + $data[] = array($absence['date'], Skoli::convertNumber($absence['absence']), $excused, $comment); + } + + /* Make sure that all rows have the same number of columns. */ + $maxcols = 0; + for ($i=0; $i < count($data); $i++) { + if (count($data[$i]) > $maxcols) { + $maxcols = count($data[$i]); + } + } + for ($i=0; $i < count($data); $i++) { + for ($irow=0; $irow < $maxcols; $irow++) { + if (!isset($data[$i][$irow])) { + $data[$i][$irow] = ''; + } + } + } + } + if (!count($data)) { + $notification->push(_("There were no entries to export."), 'horde.message'); + break; + } + + switch (Util::getFormData('exportID')) { + case EXPORT_CSV: + $csv = &Horde_Data::singleton('csv'); + $csv->exportFile(_("class.csv"), $data, (Util::getFormData('student') == 'all')); + exit; + + case EXPORT_TSV: + $tsv = &Horde_Data::singleton('tsv'); + $tsv->exportFile(_("class.tsv"), $data, (Util::getFormData('student') == 'all')); + exit; + + } + break; +} + +$title = _("Export Classes"); + +Horde::addScriptFile('prototype.js', 'horde', true); +Horde::addScriptFile('effects.js', 'horde', true); +Horde::addScriptFile('redbox.js', 'horde', true); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +require SKOLI_TEMPLATES . '/data/export.inc'; +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/docs/CHANGES b/skoli/docs/CHANGES new file mode 100644 index 000000000..7e31cfce8 --- /dev/null +++ b/skoli/docs/CHANGES @@ -0,0 +1,5 @@ +---- +v0.1 +---- + +[xyz] Initial Release diff --git a/skoli/docs/CREDITS b/skoli/docs/CREDITS new file mode 100644 index 000000000..a7551824b --- /dev/null +++ b/skoli/docs/CREDITS @@ -0,0 +1,17 @@ +=========================== + Skoli Development Team +=========================== + + +Core Developers +=============== + +- Martin Blumenthal + + +Localization +============ + +===================== ====================================================== +German Martin Blumenthal +===================== ====================================================== diff --git a/skoli/docs/INSTALL b/skoli/docs/INSTALL new file mode 100644 index 000000000..9cf25ee14 --- /dev/null +++ b/skoli/docs/INSTALL @@ -0,0 +1,243 @@ +====================== + Installing Skoli 0.1 +====================== + +:Last update: $Date: $ +:Revision: $Revision: 0.1 $ +:Contact: horde@lists.horde.org + +.. contents:: Contents +.. section-numbering:: + +This document contains instructions for installing the Skoli administrative +application for teachers on your system. + +For information on the capabilities and features of Skoli, see the file +README_ in the top-level directory of the Skoli distribution. + + +Obtaining Skoli +================== + +Skoli can be obtained from the Horde website and FTP server, at + + http://www.horde.org/skoli/ + + ftp://ftp.horde.org/pub/skoli/ + +Or use the mirror closest to you: + + http://www.horde.org/mirrors.php + +Bleeding-edge development versions of Skoli are available via CVS; see the +file `docs/HACKING`_ in the Horde distribution, or the website +http://www.horde.org/source/, for information on accessing the Horde CVS +repository. + + +Prerequisites +============= + +To function properly, Skoli **requires** the following: + +1. A working Horde installation. + + Skoli runs within the `Horde Application Framework`_, a set of common + tools for Web applications written in PHP. You must install Horde before + installing Skoli. + + .. Important:: Skoli 0.1 requires version 3.0+ of the Horde Framework - + earlier versions of Horde will **not** work. + + .. _`Horde Application Framework`: http://www.horde.org/horde/ + + The Horde Framework can be obtained from the Horde website and FTP server, + at + + http://www.horde.org/horde/ + + ftp://ftp.horde.org/pub/horde/ + + Many of Skoli's prerequisites are also Horde prerequisites. + + .. Important:: Be sure to have completed all of the steps in the + `horde/docs/INSTALL`_ file for the Horde Framework before + installing Skoli. + +2. SQL support in PHP. + + Skoli stores its data in an SQL database. Build PHP with whichever SQL + driver you require; see the Horde INSTALL_ file for details. + +3. Turba, the Horde contacts manager. + + Turba is the Horde contact management application, designed to be + integrated with other Horde applications to provide a unified interface to + contact management throughout the Horde suite. + + Turba is available from: + + http://www.horde.org/turba/ + + ftp://ftp.horde.org/pub/turba/ + + Turba provides a local address book and an LDAP directory search function + to IMP. + + You must use the 2.x branch of Turba with Skoli 0.1. + + +Installing Skoli +================ + +Skoli is written in PHP, and must be installed in a web-accessible +directory. The precise location of this directory will differ from system to +system. Conventionally, Skoli is installed directly underneath Horde in the +web server's document tree. + +Since Skoli is written in PHP, there is no compilation necessary; simply +expand the distribution where you want it to reside and rename the root +directory of the distribution to whatever you wish to appear in the URL. For +example, with the Apache web server's default document root of +``/usr/local/apache/htdocs``, you would type:: + + cd /usr/local/apache/htdocs/horde + tar zxvf /path/to/skoli-x.y.z.tar.gz + mv skoli-x.y.z skoli + +and would then find Skoli at the URL:: + + http://your-server/horde/skoli/ + + +Configuring Skoli +==================== + +1. Configuring Horde for Skoli + + a. Register the application + + In ``horde/config/registry.php``, find the ``applications['skoli']`` + stanza. The default settings here should be okay, but you can change + them if desired. If you have changed the location of Skoli relative + to Horde, either in the URL, in the filesystem or both, you must update + the ``fileroot`` and ``webroot`` settings to their correct values. + + If Skoli is not yet present in ``horde/config/registry.php`` you can + use something like: + + $this->applications['skoli'] = array( + 'fileroot' => dirname(__FILE__) . '/../skoli', + 'webroot' => $this->applications['horde']['webroot'] . '/skoli', + 'name' => _("School"), + 'status' => 'active', + 'menu_parent' => 'office' + ); + + $this->applications['skoli-menu'] = array( + 'status' => 'block', + 'app' => 'skoli', + 'blockname' => 'tree_menu', + 'menu_parent' => 'skoli', + ); + +2. Creating the database tables + + The specific steps to create Skoli's database tables depend on which + database you've chosen to use. + + First, look in ``scripts/sql/`` to see if a script already exists for your + database type. If so, you should be able to simply execute that script as + superuser in your database. (Note that executing the script as the "horde" + user will probably fail when granting privileges.) + + If such a script does not exist, you'll need to build your own, using the + file ``skoli.sql`` as a starting point. If you need assistance in + creating database tables, you may wish to let us know on the Skoli + mailing list. + +3. Configuring Skoli + + To configure Skoli, change to the ``config/`` directory of the installed + distribution, and make copies of all of the configuration ``dist`` files + without the ``dist`` suffix:: + + cd config/ + for foo in *.dist; do cp $foo `basename $foo .dist`; done + + Or on Windows:: + + copy *.dist *. + + Documentation on the format and purpose of those files can be found in each + file. You may edit these files if you wish to customize Skoli's + appearance and behavior. With one exception (``foo.php``) the defaults will + be correct for most sites. + + You must login to Horde as a Horde Administrator to finish the + configuration of Skoli. Use the Horde ``Administration`` menu item to + get to the administration page, and then click on the ``Configuration`` + icon to get the configuration page. Select ``Skoli Name`` from the + selection list of applications. Fill in or change any configuration values + as needed. When done click on ``Generate Skoli Name Configuration`` to + generate the ``conf.php`` file. If your web server doesn't have write + permissions to the Skoli configuration directory or file, it will not be + able to write the file. In this case, go back to ``Configuration`` and + choose one of the other methods to create the configuration file + ``skoli/config/conf.php``. + + Note for international users: Skoli uses GNU gettext to provide local + translations of text displayed by applications; the translations are found + in the ``po/`` directory. If a translation is not yet available for your + locale (and you wish to create one), see the ``horde/po/README`` file, or + if you're having trouble using a provided translation, please see the + `horde/docs/TRANSLATIONS`_ file for instructions. + +4. School templates + + To customize your school edit the file ``skoli/config/school.php``. It + contains some examples you can start from. + +5. Testing Skoli + + Use Skoli to create a class and add some entries. Test at least the + following: + + - Creating a new Class + - Adding a new entry for each desired type + - Modifying an entry + - Deleting an entry + + +Obtaining Support +================= + +If you encounter problems with Skoli, help is available! + +The Horde Frequently Asked Questions List (FAQ), available on the Web at + + http://www.horde.org/faq/ + +The Horde Project runs a number of mailing lists, for individual applications +and for issues relating to the project as a whole. Information, archives, and +subscription information can be found at + + http://www.horde.org/mail/ + +Lastly, Horde developers, contributors and users may also be found on IRC, +on the channel #horde on the Freenode Network (irc.freenode.net). + +Please keep in mind that Skoli is free software written by volunteers. +For information on reasonable support expectations, please read + + http://www.horde.org/support.php + +Thanks for using Skoli! + +The Skoli team + + +.. _README: ?f=README.html +.. _`horde/docs/HACKING`: ../../horde/docs/?f=HACKING.html +.. _`horde/docs/INSTALL`: ../../horde/docs/?f=INSTALL.html +.. _`horde/docs/TRANSLATIONS`: ../../horde/docs/?f=TRANSLATIONS.html diff --git a/skoli/docs/RELEASE_NOTES b/skoli/docs/RELEASE_NOTES new file mode 100644 index 000000000..c49e37af4 --- /dev/null +++ b/skoli/docs/RELEASE_NOTES @@ -0,0 +1,52 @@ +notes['fm']['focus'] = 4; + +/* Mailing list release notes. */ +$this->notes['ml']['changes'] = <<notes['fm']['changes'] = <<notes['name'] = 'Skoli'; +$this->notes['fm']['project'] = 'skoli'; +$this->notes['fm']['branch'] = 'Default'; diff --git a/skoli/docs/TODO b/skoli/docs/TODO new file mode 100644 index 000000000..9df0d65f7 --- /dev/null +++ b/skoli/docs/TODO @@ -0,0 +1,15 @@ +============================= + Skoli Development TODO List +============================= + +:Last update: $Date: $ +:Revision: $Revision: 0.1 $ +:Contact: horde@lists.horde.org + +- When adding new entries allow users to add different values for more than one student (e.g. adding marks for the whole class) + +- Tune up the search form with type dependent options (e.g. drop-down with used subjects). + +- Implement an easy form to create timetables in e.g. Kronolith. + +- Allow users to search for an address to add to classes (e.g. like the compose addressbook window in imp). diff --git a/skoli/entry.php b/skoli/entry.php new file mode 100644 index 000000000..780bae8c0 --- /dev/null +++ b/skoli/entry.php @@ -0,0 +1,121 @@ + + */ + +@define('SKOLI_BASE', dirname(__FILE__)); +require_once SKOLI_BASE . '/lib/base.php'; +require_once 'Horde/Variables.php'; +require_once 'Horde/UI/Tabs.php'; + +// Exit if this isn't an authenticated user. +if (!Auth::getAuth()) { + header('Location: ' . Horde::applicationUrl('list.php', true)); + exit; +} + +$vars = Variables::getDefaultVariables(); +$driver = &Skoli_Driver::singleton(''); +$entry = $driver->getEntry($vars->get('entry')); +if (is_a($entry, 'PEAR_Error') || !count($entry)) { + $notification->push(_("Entry not found."), 'horde.error'); + header('Location: ' . Horde::applicationUrl('search.php', true)); + exit; +} +$share = $GLOBALS['skoli_shares']->getShare($entry['class_id']); + +// Check permissions +if (!$share->hasPermission(Auth::getAuth(), PERMS_READ)) { + $notification->push(_("You are not allowed to view this entry."), 'horde.error'); + header('Location: ' . Util::addParameter(Horde::applicationUrl('search.php', true), 'actionID', 'search')); + exit; +} + +$studentdetails = Skoli::getStudent($share->get('address_book'), $entry['student_id']); + +// Get view. +$viewName = Util::getFormData('view', 'Entry'); + +if ($viewName != 'DeleteEntry') { + require_once SKOLI_BASE . '/lib/Forms/Entry.php'; + if (!$vars->exists('class_id')) { + foreach ($entry as $key=>$val) { + if (!is_array($val)) { + $vars->set($key, $val); + } + } + foreach ($entry['_attributes'] as $key=>$val) { + $vars->set('attribute_' . $key, $val); + } + } + $form = new Skoli_EntryForm($vars); + if ($viewName == 'EditEntry') { + if ($form->validate($vars)) { + $driver = &Skoli_Driver::singleton($vars->get('class_id')); + $result = $driver->updateEntry($entry['object_id'], $vars); + if (is_a($result, 'PEAR_Error')) { + $notification->push(sprintf(_("Couldn't update this entry: %s"), $result->getMessage()), 'horde.error'); + } else { + $notification->push(sprintf(_("The entry for \"%s\" has been saved."), $studentdetails[$conf['addresses']['name_field']]), 'horde.success'); + header('Location: ' . Util::addParameter(Horde::applicationUrl('search.php', true), 'actionID', 'search')); + exit; + } + } + } +} + +// Entry actions. +$actionID = Util::getFormData('actionID'); +if ($actionID == 'delete') { + if (is_a($deleted = $driver->deleteEntry($entry['object_id']), 'PEAR_Error')) { + $notification->push(sprintf(_("There was an error deleting this entry: %s"), $deleted->getMessage()), 'horde.error'); + } else { + $notification->push(sprintf(_("The entry for \"%s\" has been deleted."), $studentdetails[$conf['addresses']['name_field']]), 'horde.success'); + header('Location: ' . Util::addParameter(Horde::applicationUrl('search.php', true), 'actionID', 'search')); + exit; + } +} + +// Get tabs. +$url = Util::addParameter(Horde::applicationUrl('entry.php'), 'entry', $entry['object_id']); +$tabs = new Horde_UI_Tabs('view', $vars); +$tabs->addTab(_("View"), $url, array('tabname' => 'Entry', 'id' => 'tabEntry')); +if ($share->hasPermission(Auth::getAuth(), PERMS_EDIT)) { + $tabs->addTab(_("Edit"), $url, array('tabname' => 'EditEntry', 'id' => 'tabEditEntry')); +} +if ($share->hasPermission(Auth::getAuth(), PERMS_DELETE)) { + $tabs->addTab(_("Delete"), $url, array('tabname' => 'DeleteEntry', 'id' => 'tabDeleteEntry')); +} + +$title = _("Edit Entry"); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; + +echo '
'; +echo $tabs->render($viewName); +echo '

' . sprintf(_("Entry for \"%s\""), $studentdetails[$conf['addresses']['name_field']]) . '

'; + +// View output +switch ($viewName) { +case 'Entry': + echo $form->renderInactive($form->getRenderer(), $vars); + break; + +case 'EditEntry': + echo $form->renderActive($form->getRenderer(), $vars, 'entry.php', 'post'); + break; + +case 'DeleteEntry': + require SKOLI_TEMPLATES . '/entry/delete.inc'; + break; +} + +echo '
'; +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/index.php b/skoli/index.php new file mode 100644 index 000000000..fe0a3ae79 --- /dev/null +++ b/skoli/index.php @@ -0,0 +1,24 @@ + _('This file defines templates for new classes.'))); +} + +require_once SKOLI_BASE . '/lib/base.php'; +require SKOLI_BASE . '/' . $prefs->getValue('initial_page') . '.php'; diff --git a/skoli/lib/Block/tree_menu.php b/skoli/lib/Block/tree_menu.php new file mode 100644 index 000000000..0c8fcf013 --- /dev/null +++ b/skoli/lib/Block/tree_menu.php @@ -0,0 +1,57 @@ +getImageDir(); + + $classes = Skoli::listClasses(false, PERMS_EDIT); + if (count($classes) > 0) { + + $tree->addNode($parent . '__new', + $parent, + _("New Entry"), + $indent + 1, + false, + array('icon' => 'add.png', + 'icondir' => $icondir, + 'url' => $add)); + + foreach ($classes as $name => $class) { + $tree->addNode($parent . $name . '__new', + $parent . '__new', + sprintf(_("in %s"), $class->get('name')), + $indent + 2, + false, + array('icon' => 'add.png', + 'icondir' => $icondir, + 'url' => Util::addParameter($add, array('class' => $name)))); + } + $tree->addNode($parent . '__search', + $parent, + _("Search"), + $indent + 1, + false, + array('icon' => 'search.png', + 'icondir' => $registry->getImageDir('horde'), + 'url' => Horde::applicationUrl('search.php'))); + } + + } +} diff --git a/skoli/lib/Driver.php b/skoli/lib/Driver.php new file mode 100644 index 000000000..bbd30132f --- /dev/null +++ b/skoli/lib/Driver.php @@ -0,0 +1,153 @@ + + * @package Skoli + */ +class Skoli_Driver { + + /** + * String containing the current class name. + * + * @var string + */ + var $_class = ''; + + /** + * An error message to throw when something is wrong. + * + * @var string + */ + var $_errormsg; + + /** + * Constructor - All real work is done by initialize(). + */ + function Skoli_Driver($errormsg = null) + { + if (is_null($errormsg)) { + $this->_errormsg = _("The School backend is not currently available."); + } else { + $this->_errormsg = $errormsg; + } + } + + /** + * Attempts to return a concrete Skoli_Driver instance based on $driver. + * + * @param string $class The name of the class to load. + * + * @param string $driver The type of the concrete Skoli_Driver subclass + * to return. The class name is based on the + * storage driver ($driver). The code is + * dynamically included. + * + * @param array $params A hash containing any additional configuration + * or connection parameters a subclass might need. + * + * @return Skoli_Driver The newly created concrete Skoli_Driver + * instance, or false on an error. + */ + function &factory($class = '', $driver = null, $params = null) + { + /* Check if we have access to the given class */ + static $classes; + if (!is_array($classes)) { + $classes = Skoli::listClasses(); + } + if (!isset($classes[$class])) { + $class = &new Skoli_Driver(sprintf(_("Access for class \"%s\" is denied"), $class)); + return $class; + } + + if (is_null($driver)) { + $driver = $GLOBALS['conf']['storage']['driver']; + } + $driver = basename($driver); + + if (is_null($params)) { + $params = Horde::getDriverConfig('storage', $driver); + } + + require_once dirname(__FILE__) . '/Driver/' . $driver . '.php'; + $objclass = 'Skoli_Driver_' . $driver; + if (class_exists($objclass)) { + $class = &new $objclass($class, $params); + $result = $class->initialize(); + if (is_a($result, 'PEAR_Error')) { + $class = &new Skoli_Driver(sprintf(_("The School backend is not currently available: %s"), $result->getMessage())); + } + } else { + $class = &new Skoli_Driver(sprintf(_("Unable to load the definition of %s."), $objclass)); + } + + return $class; + } + + /** + * Attempts to return a reference to a concrete Skoli_Driver + * instance based on $driver. It will only create a new instance + * if no Skoli_Driver instance with the same parameters currently + * exists. + * + * This should be used if multiple storage sources are required. + * + * This method must be invoked as: $var = &Skoli_Driver::singleton() + * + * @param string $class The name of the class to load. + * + * @param string $driver The type of concrete Skoli_Driver subclass + * to return. The is based on the storage + * driver ($driver). The code is dynamically + * included. + * + * @param array $params (optional) A hash containing any additional + * configuration or connection parameters a + * subclass might need. + * + * @return mixed The created concrete Skoli_Driver instance, or false + * on error. + */ + function &singleton($class = '', $driver = null, $params = null) + { + static $instances = array(); + + if (is_null($driver)) { + $driver = $GLOBALS['conf']['storage']['driver']; + } + + if (is_null($params)) { + $params = Horde::getDriverConfig('storage', $driver); + } + + $signature = serialize(array($class, $driver, $params)); + if (!isset($instances[$signature])) { + $instances[$signature] = &Skoli_Driver::factory($class, $driver, $params); + } + + return $instances[$signature]; + } + + /** + * Generate a universal / unique identifier for an entry. This is + * NOT something that we expect to be able to parse into a + * entry list. + * + * @return string A nice unique string (should be 255 chars or less). + */ + function generateUID() + { + return date('YmdHis') . '.' + . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16) + . '@' . $GLOBALS['conf']['server']['name']; + } +} diff --git a/skoli/lib/Driver/sql.php b/skoli/lib/Driver/sql.php new file mode 100644 index 000000000..d6d7cc00f --- /dev/null +++ b/skoli/lib/Driver/sql.php @@ -0,0 +1,694 @@ + + * 'phptype' The database type (e.g. 'pgsql', 'mysql', etc.). + * 'charset' The database's internal charset. + * + * Required by some database implementations:
+ *   'hostspec'     The hostname of the database server.
+ *   'protocol'     The communication protocol ('tcp', 'unix', etc.).
+ *   'database'     The name of the database.
+ *   'username'     The username with which to connect to the database.
+ *   'password'     The password associated with 'username'.
+ *   'options'      Additional options to pass to the database.
+ *   'tty'          The TTY on which to connect to the database.
+ *   'port'         The port on which to connect to the database.
+ * + * Optional values when using separate reading and writing servers, for example + * in replication settings:
+ *   'splitread'   Boolean, whether to implement the separation or not.
+ *   'read'        Array containing the parameters which are different for
+ *                 the read database connection, currently supported
+ *                 only 'hostspec' and 'port' parameters.
+ * + * Optional parameters:
+ *   'objects_table'            The name of the objects table in 'database'.  
+ *                              Default is 'skoli_objects'.
+ *   'object_attributes_table'  The name of the attributes table in 'database'.
+ *                              Default ist 'skoli_object_attributes'.
+ *   'students_table'           The name of the students table in 'database'.  
+ *                              Default is 'skoli_classes_students'.
+ * + * The table structure can be created by the scripts/sql/skoli.sql script. + * + * $Horde: skoli/lib/Driver/sql.php,v 0.1 $ + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + * + * @author Martin Blumenthal + * @package Skoli + */ +class Skoli_Driver_sql extends Skoli_Driver { + + /** + * Handle for the current database connection. + * + * @var DB + */ + var $_db; + + /** + * Handle for the current database connection, used for writing. Defaults + * to the same handle as $_db if a separate write database is not required. + * + * @var DB + */ + var $_write_db; + + /** + * Constructs a new SQL storage object. + * + * @param string $classlist The classlist to load. + * @param array $params A hash containing connection parameters. + */ + function Skoli_Driver_sql($class, $params = array()) + { + $this->_class = $class; + $this->_params = $params; + } + + /** + * Attempts to open a connection to the SQL server. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function initialize() + { + Horde::assertDriverConfig($this->_params, 'storage', + array('phptype', 'charset')); + + if (!isset($this->_params['database'])) { + $this->_params['database'] = ''; + } + if (!isset($this->_params['username'])) { + $this->_params['username'] = ''; + } + if (!isset($this->_params['hostspec'])) { + $this->_params['hostspec'] = ''; + } + if (!isset($this->_params['objects_table'])) { + $this->_params['objects_table'] = 'skoli_objects'; + } + if (!isset($this->_params['object_attributes_table'])) { + $this->_params['object_attributes_table'] = 'skoli_object_attributes'; + } + if (!isset($this->_params['students_table'])) { + $this->_params['students_table'] = 'skoli_classes_students'; + } + + /* Connect to the SQL server using the supplied parameters. */ + $this->_write_db = &DB::connect($this->_params, + array('persistent' => !empty($this->_params['persistent']))); + if (is_a($this->_write_db, 'PEAR_Error')) { + return $this->_write_db; + } + + /* Set DB portability options. */ + switch ($this->_write_db->phptype) { + case 'mssql': + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + /* Check if we need to set up the read DB connection + * seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($this->_params, $this->_params['read']); + $this->_db = &DB::connect($params, + array('persistent' => !empty($params['persistent']))); + if (is_a($this->_db, 'PEAR_Error')) { + return $this->_db; + } + + /* Set DB portability options. */ + switch ($this->_db->phptype) { + case 'mssql': + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + } else { + /* Default to the same DB handle for the writer too. */ + $this->_db =& $this->_write_db; + } + + return true; + } + + /** + * Get all students from the backend storage. + * + * @return array List with all student IDs. + */ + function getStudents() + { + $query = 'SELECT student_id FROM ' . $this->_params['students_table'] . + ' WHERE class_id = ?'; + $values = array($this->_class); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getStudents(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $students = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($students, 'PEAR_Error')) { + Horde::logMessage($students, __FILE__, __LINE__, PEAR_LOG_ERR); + return $students; + } + + return $students; + } + + /** + * Add students to the backend storage. + * + * @param array $students List with students. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function addStudents($students) + { + /* Delete any existing Students */ + $query = 'DELETE FROM ' . $this->_params['students_table'] . + ' WHERE class_id=?'; + $result = $this->_write_db->query($query, array($this->_class)); + + foreach ($students as $addressid) { + $query = 'INSERT INTO ' . $this->_params['students_table'] . + ' (class_id, student_id)' . + ' VALUES (?, ?)'; + $values = array($this->_class, $addressid); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::addStudents(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the insertion query. */ + $result = $this->_write_db->query($query, $values); + + /* Return an error immediately if the query failed. */ + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + } + + return true; + } + + /** + * Get an entry from the backend storage. + * + * @param string $entryid The entry ID. + * + * @return array List with all entry fields. + */ + function getEntry($entryid) + { + $query = 'SELECT * FROM ' . $this->_params['objects_table'] . + ' WHERE object_id = ?'; + $values = array($entryid); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $entry = $this->_db->getRow($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($entry, 'PEAR_Error')) { + Horde::logMessage($entry, __FILE__, __LINE__, PEAR_LOG_ERR); + return $entry; + } else if (!is_array($entry)) { + return array(); + } + + $query = 'SELECT * FROM ' . $this->_params['object_attributes_table'] . + ' WHERE object_id = ?'; + $values = array($entryid); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $attributes = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($attributes, 'PEAR_Error')) { + Horde::logMessage($attributes, __FILE__, __LINE__, PEAR_LOG_ERR); + return $attributes; + } + + $entry['_attributes'] = array(); + foreach ($attributes as $attribute) { + $entry['_attributes'][$attribute['attr_name']] = $attribute['attr_value']; + } + + if (empty($this->_class)) { + $this->_class = $entry['class_id']; + } + + return $entry; + } + + /** + * Get all entries for the current class or student from the backend storage. + * + * @param string $studentid The student ID. + * + * @param string $type The entry type to search in. + * + * @param array $searchparams Some additional search parameters. + * + * @return array List with all entries. + */ + function getEntries($studentid = null, $type = null, $searchparams = array()) + { + if (is_null($studentid)) { + $students = $this->getStudents(); + } else { + $students = array(array('student_id' => $studentid)); + } + + foreach ($students as $studentkey=>$student) { + /* Build the search parameter */ + if (count($searchparams)) { + $where = ''; + $search_values = array(); + if (count($searchparams) === 1 && !is_array($searchparams[0])) { + /* search all attributes for the specified text */ + $where = ' AND a.attr_value LIKE ?'; + $search_values[] = '%' . $searchparams[0] . '%'; + } else { + /* search only in the specified fields */ + $where = ' AND '; + for ($i = 0; $i < count($searchparams); $i++) { + $strict = !empty($searchparams[$i]['strict']); + $where .= '(a.attr_name = ? AND a.attr_value ' . ($strict ? '=' : 'LIKE') . ' ?) OR '; + $search_values[] = $searchparams[$i]['name']; + if ($strict) { + $search_values[] = $searchparams[$i]['value']; + } else { + $search_values[] = '%' . $searchparams[$i]['value'] . '%'; + } + } + $where = substr($where, 0, -4); + } + $query = 'SELECT o.* FROM ' . $this->_params['object_attributes_table'] . ' AS a, ' . + $this->_params['objects_table'] . ' AS o' . + ' WHERE o.object_id = a.object_id AND o.class_id = ? AND o.student_id = ?' . (!is_null($type) ? ' AND o.object_type = ?' : '') . $where . + ' GROUP BY o.object_id'; + $values = array($this->_class, $student['student_id']); + if (!is_null($type)) { + $values[] = $type; + } + $values = array_merge($values, $search_values); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getEntries(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $entries = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($entries, 'PEAR_Error')) { + Horde::logMessage($entries, __FILE__, __LINE__, PEAR_LOG_ERR); + return $entries; + } + } else { + $query = 'SELECT * FROM ' . $this->_params['objects_table'] . + ' WHERE class_id = ? AND student_id = ?' . + (!is_null($type) ? ' AND object_type = ?' : ''); + $values = array($this->_class, $student['student_id']); + if (!is_null($type)) { + $values[] = $type; + } + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getEntries(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $entries = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($entries, 'PEAR_Error')) { + Horde::logMessage($entries, __FILE__, __LINE__, PEAR_LOG_ERR); + return $entries; + } + } + + $students[$studentkey]['_entries'] = $entries; + + foreach ($entries as $entrykey=>$entry) { + $query = 'SELECT * FROM ' . $this->_params['object_attributes_table'] . + ' WHERE object_id = ?'; + $values = array($entry['object_id']); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getEntries(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $attributes = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($attributes, 'PEAR_Error')) { + Horde::logMessage($attributes, __FILE__, __LINE__, PEAR_LOG_ERR); + return $attributes; + } + + $students[$studentkey]['_entries'][$entrykey]['_attributes'] = array(); + foreach ($attributes as $attribute) { + $students[$studentkey]['_entries'][$entrykey]['_attributes'][$attribute['attr_name']] = $attribute['attr_value']; + } + } + } + + return $students; + } + + /** + * Get the timestamp of the last entry for the given student. + * + * @param string $studentid The student ID. + * + * @return int The last entry. + */ + function lastEntry($studentid) + { + $query = 'SELECT object_time FROM ' . $this->_params['objects_table'] . + ' WHERE class_id = ? AND student_id = ? ORDER BY object_time DESC LIMIT 1'; + $values = array($this->_class, $studentid); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::lastEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $lastentry = $this->_db->getRow($query, $values, DB_FETCHMODE_ORDERED); + + /* Return an error immediately if the query failed. */ + if (is_a($lastentry, 'PEAR_Error')) { + Horde::logMessage($lastentry, __FILE__, __LINE__, PEAR_LOG_ERR); + return $lastentry; + } + + if (count($lastentry)) { + return $lastentry[0]; + } + + return null; + } + + /** + * Add or update a new entry to the backend storage. + * + * @param string $entryid The entry ID. + * + * @param Variables $vars List with form variables. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function updateEntry($entryid, $vars) + { + $attributes = array(); + foreach ($vars->_vars as $key=>$value) { + if (strpos($key, 'attribute_') === 0 && $value != '') { + $attribute = substr($key, 10); + $attributes[$attribute] = $value; + } + } + + $query = 'UPDATE ' . $this->_params['objects_table'] . ' SET' . + ' class_id = ?, student_id = ?, object_time = ?, object_type = ?' . + ' WHERE object_id = ?'; + require_once 'Horde/Date.php'; + $date = new Horde_Date($vars->get('object_time')); + $values = array($this->_class, $vars->get('student_id'), $date->datestamp(), $vars->get('object_type'), $entryid); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::updateEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the insertion query. */ + $result = $this->_write_db->query($query, $values); + + /* Return an error immediately if the query failed. */ + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $query = 'DELETE FROM ' . $this->_params['object_attributes_table'] . + ' WHERE object_id = ?'; + $values = array($entryid); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::updateEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the delete query. */ + $result = $this->_write_db->query($query, $values); + + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + foreach ($attributes as $attribute=>$value) { + $query = 'INSERT INTO ' . $this->_params['object_attributes_table'] . + ' (object_id, attr_name, attr_value)' . + ' VALUES (?, ?, ?)'; + $values = array($entryid, $attribute, $value); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::addEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the insertion query. */ + $result = $this->_write_db->query($query, $values); + + /* Return an error immediately if the query failed. */ + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + } + + return true; + } + + /** + * Add a new entry to the backend storage. + * + * @param Variables $vars List with form variables. + * + * @return Mixed Studentnames on success, PEAR_Error on failure. + */ + function addEntry($vars) + { + $names = ''; + $class = current(Skoli::listStudents($this->_class)); + + $attributes = array(); + foreach ($vars->_vars as $key=>$value) { + if (strpos($key, 'attribute_') === 0 && $value != '') { + $attribute = substr($key, 10); + $attributes[$attribute] = $value; + } + } + + require_once 'Horde/Date.php'; + foreach ($vars->get('student_id') as $studentid) { + $query = 'INSERT INTO ' . $this->_params['objects_table'] . + ' (object_id, object_owner, object_uid, class_id, student_id, object_time, object_type)' . + ' VALUES (?, ?, ?, ?, ?, ?, ?)'; + $entryId = md5(uniqid(mt_rand(), true)); + $date = new Horde_Date($vars->get('object_time')); + $values = array($entryId, Auth::getAuth(), $this->generateUID(), $this->_class, $studentid, $date->datestamp(), $vars->get('object_type')); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::addEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the insertion query. */ + $result = $this->_write_db->query($query, $values); + + /* Return an error immediately if the query failed. */ + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + foreach ($attributes as $attribute=>$value) { + $query = 'INSERT INTO ' . $this->_params['object_attributes_table'] . + ' (object_id, attr_name, attr_value)' . + ' VALUES (?, ?, ?)'; + $values = array($entryId, $attribute, $value); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::addEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the insertion query. */ + $result = $this->_write_db->query($query, $values); + + /* Return an error immediately if the query failed. */ + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + } + + $studentdetails = Skoli::getStudent($class['address_book'], $studentid); + $names .= $studentdetails[$GLOBALS['conf']['addresses']['name_field']] . ', '; + } + + return substr($names, 0, -2); + } + + /** + * Get all currently used subjects from the current class. + * + * @param string $type Get subjects only from this type. + * + * @return array List with all subjects. + */ + function getSubjects($type = null) + { + $where = !is_null($type) ? ' AND o.object_type = ?' : ''; + $query = 'SELECT DISTINCT a.attr_value FROM ' . $this->_params['object_attributes_table'] . ' AS a, ' . + $this->_params['objects_table'] . ' AS o' . + ' WHERE a.object_id = o.object_id AND o.class_id = ? AND a.attr_name = ?' . $where; + $values = array($this->_class, 'subject'); + if (!is_null($type)) { + $values[] = $type; + } + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::getSubjects(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $subjects = $this->_db->getAll($query, $values, DB_FETCHMODE_ORDERED); + + /* Return an error immediately if the query failed. */ + if (is_a($subjects, 'PEAR_Error')) { + Horde::logMessage($subjects, __FILE__, __LINE__, PEAR_LOG_ERR); + return $subjects; + } + + $subjectlist = array(); + foreach ($subjects as $subject) { + $subjectlist[] = $subject[0]; + } + + return $subjectlist; + } + + /** + * Deletes all data from the current class. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function deleteAll() + { + $query = 'DELETE FROM ' . $this->_params['students_table'] . + ' WHERE class_id = ?'; + $values = array($this->_class); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::deleteAll(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the delete query. */ + $result = $this->_write_db->query($query, $values); + + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $query = 'SELECT object_id FROM ' . $this->_params['objects_table'] . + ' WHERE class_id = ?'; + $values = array($this->_class); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::deleteAll(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the select query. */ + $entries = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + /* Return an error immediately if the query failed. */ + if (is_a($entries, 'PEAR_Error')) { + Horde::logMessage($entries, __FILE__, __LINE__, PEAR_LOG_ERR); + return $entries; + } + + foreach ($entries as $entry) { + $result = $this->deleteEntry($entry['object_id']); + + /* Return an error immediately if the query failed. */ + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + return true; + } + + /** + * Deletes an entry from the current class. + * + * @param string $object_id The entry ID to delete. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function deleteEntry($object_id) + { + $query = 'DELETE FROM ' . $this->_params['objects_table'] . + ' WHERE object_id = ? AND class_id = ?'; + $values = array($object_id, $this->_class); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::deleteEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the delete query. */ + $result = $this->_write_db->query($query, $values); + + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $query = 'DELETE FROM ' . $this->_params['object_attributes_table'] . + ' WHERE object_id = ?'; + $values = array($object_id); + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Skoli_Driver_sql::deleteEntry(): %s', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Attempt the delete query. */ + $result = $this->_write_db->query($query, $values); + + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + return true; + } +} diff --git a/skoli/lib/Forms/CreateClass.php b/skoli/lib/Forms/CreateClass.php new file mode 100644 index 000000000..049aecd9c --- /dev/null +++ b/skoli/lib/Forms/CreateClass.php @@ -0,0 +1,236 @@ + + * @package Skoli + */ +class Skoli_CreateClassForm extends Horde_Form { + + /** + * Name of the new share. + * + * @var int + */ + var $shareid; + + /** + * List of school properties. + * + * @var array + */ + var $_schoolproperties = array( + 'grade', + 'semester', + 'start', + 'end', + 'location', + 'marks'); + + + function Skoli_CreateClassForm(&$vars) + { + global $conf, $prefs, $registry; + + parent::Horde_Form($vars, _("Create Class")); + + $this->setSection('properties', _("Properties")); + + $this->addVariable(_("General Settings"), null, 'header', false); + + $this->addVariable(_("Name"), 'name', 'text', true); + $this->addVariable(_("Description"), 'description', 'longtext', false, false, null, array(4, 60)); + + $this->addVariable(_("Category"), 'category', 'category', false); + // A new category doesn't survive a reload action, so reset it + // @TODO: Could this be a bug? + if (strpos($this->_vars->get('category'), '*new*') !== false) { + $this->_vars->set('category', $this->_vars->get('new_category')); + } + + $this->addVariable(_("School Specific Settings"), null, 'header', false); + + // Load Skoli_School + require_once SKOLI_BASE . '/lib/School.php'; + + // List schools + $schoollist = Skoli_School::listSchools(); + $actionvariable = &$this->addVariable(_("Schools"), 'school', 'enum', true, count($schoollist)>1 ? false : true, null, array($schoollist, _("Choose:"))); + if (count($schoollist) > 1) { + require_once 'Horde/Form/Action.php'; + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + } else { + $this->_vars->set('school', key($schoollist)); + } + + // Load the selected school + if ($this->_vars->exists('school')) { + $school = new Skoli_School($this->_vars->get('school')); + foreach ($this->_schoolproperties as $name) { + $school->addFormVariable($this, $name); + } + } + + $this->setSection('students', _("Students")); + + $this->addVariable(_("Address Book"), null, 'header', false); + + $addressbooklist = Skoli_School::listAddressBooks(); + $actionvariable = &$this->addVariable(_("Address Book"), 'address_book', 'enum', true, count($addressbooklist)>1 ? false : true, null, array($addressbooklist, _("Choose:"))); + if (count($addressbooklist) > 1) { + require_once 'Horde/Form/Action.php'; + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + } else { + $this->_vars->set('address_book', key($addressbooklist)); + } + + $this->addVariable(_("Students"), null, 'header', false); + + if ($this->_vars->get('address_book') != '') { + $searchargs = array( + 'addresses' => array(''), + 'addressbooks' => array($this->_vars->get('address_book')), + 'fields' => array() + ); + if ($search_fields_pref = $prefs->getValue('search_fields')) { + foreach (explode("\n", $search_fields_pref) as $s) { + $s = trim($s); + $s = explode("\t", $s); + if (!empty($s[0]) && ($s[0] == $this->_vars->get('address_book'))) { + $searchargs['fields'][array_shift($s)] = $s; + break; + } + } + } + $resultstmp = $registry->call('contacts/search', $searchargs); + // contacts/search seems to return an array entry for each source. + $results = array(); + foreach ($resultstmp as $r) { + $results = array_merge($results, $r); + } + foreach ($results as $address) { + if (isset($address['__type']) && $address['__type'] == 'Object') { + $addresses[$address['__key']] = $address[$conf['addresses']['name_field']]; + } + } + } else { + $addresses = array(); + } + + $this->addVariable(_("Students"), 'students', 'multienum', false, false, null, array($addresses, 20)); + + if ($conf['addresses']['contact_list'] != 'none' && $prefs->getValue('contact_list') != 'none') { + $this->addVariable(_("Contact List"), null, 'header', false); + if ($conf['addresses']['contact_list'] == 'user' && $prefs->getValue('contact_list') == 'ask') { + $this->addVariable(_("Create Contact List?"), 'contact_list_create', 'boolean', true, false); + if (!$this->_vars->exists('contact_list')) { + $this->_vars->set('contact_list_create', true); + } + } + $this->addVariable(_("Name"), 'contact_list', 'text', false, + $conf['addresses']['contact_list'] == 'auto' || $prefs->getValue('contact_list') == 'auto' ? true : false, _("The substitutions %c, %g or %s will be replaced automatically by the class, grade respectively semester name.")); + if (!$this->_vars->exists('contact_list')) { + $contactlist = $conf['addresses']['contact_list'] == 'auto' ? $conf['addresses']['contact_list_name'] : $prefs->getValue('contact_list_name'); + } else { + $contactlist = $this->_vars->get('contact_list'); + } + $this->_vars->set('contact_list', Skoli_School::parseContactListName($contactlist, $this->_vars)); + } + + $this->setButtons(array(_("Create"))); + } + + function execute() + { + global $conf, $prefs, $registry, $notification; + + /* Add new category. */ + if (strpos($this->_vars->get('category'), '*new*') !== false || $this->_vars->get('category') == $this->_vars->get('new_category')) { + require_once 'Horde/Prefs/CategoryManager.php'; + $cManager = new Prefs_CategoryManager(); + $cManager->add($this->_vars->get('new_category')); + $this->_vars->set('category', $this->_vars->get('new_category')); + } + + // Create new share. + $this->shareid = md5(microtime()); + $class = $GLOBALS['skoli_shares']->newShare($this->shareid); + if (is_a($class, 'PEAR_Error')) { + return $class; + } + $class->set('name', $this->_vars->get('name')); + $class->set('desc', $this->_vars->get('description')); + $class->set('category', $this->_vars->get('category')); + $class->set('school', $this->_vars->get('school')); + $class->set('address_book', $this->_vars->get('address_book')); + + require_once 'Horde/Date.php'; + foreach ($this->_schoolproperties as $property) { + if ($property == 'start' || $property == 'end') { + $date = new Horde_Date($this->_vars->get($property)); + $this->_vars->set($property, $date->datestamp()); + } else if ($property == 'marks' && $this->_vars->get($property . '_custom') != '') { + $this->_vars->set($property, $this->_vars->get($property . '_custom')); + } + $class->set($property, $this->_vars->get($property) == '' ? null : $this->_vars->get($property)); + } + + // Save students + if ($this->_vars->exists('students')) { + $driver = &Skoli_Driver::singleton($this->shareid); + $result = $driver->addStudents($this->_vars->get('students')); + if (is_a($result, 'PEAR_Error')) { + $notification->push(_("Couldn't add the selected students to the class."), 'horde.warning'); + } + + // Add new contact list + if ($conf['addresses']['contact_list'] != 'none' && $prefs->getValue('contact_list') != 'none' && $this->_vars->get('contact_list') != '') { + $createlist = true; + if ($conf['addresses']['contact_list'] == 'user' && $prefs->getValue('contact_list') == 'ask' && $this->_vars->get('contact_list_create') == '') { + $createlist = false; + } + } else { + $createlist = false; + } + if ($createlist) { + $apiargs = array( + 'content' => array( + '__type' => 'Group', + '__members' => serialize($this->_vars->get('students')), + 'name' => Skoli_School::parseContactListName($this->_vars->get('contact_list'), $this->_vars, true), + ), + 'contentType' => 'array', + 'source' => $this->_vars->get('address_book') + ); + $result = $registry->call('contacts/import', $apiargs); + if ($result === false || is_a($result, 'PEAR_Error')) { + $notification->push(sprintf(_("Couldn't create the contact list \"%s\"."), $this->_vars->get('contact_list')), 'horde.warning'); + } + } + } + + return $GLOBALS['skoli_shares']->addShare($class); + } + +} diff --git a/skoli/lib/Forms/DeleteClass.php b/skoli/lib/Forms/DeleteClass.php new file mode 100644 index 000000000..7a0a9478d --- /dev/null +++ b/skoli/lib/Forms/DeleteClass.php @@ -0,0 +1,82 @@ + + * @since Skoli 2.2 + * @package Skoli + */ +class Skoli_DeleteClassForm extends Horde_Form { + + /** + * Class being deleted + */ + var $_class; + + function Skoli_DeleteClassForm(&$vars, &$class) + { + $this->_class = &$class; + parent::Horde_Form($vars, sprintf(_("Delete %s"), $class->get('name'))); + + $this->addHidden('', 'c', 'text', true); + $this->addVariable(sprintf(_("Really delete the class \"%s\"? This cannot be undone and all data on this class will be permanently removed."), $this->_class->get('name')), 'desc', 'description', false); + + $this->setButtons(array(_("Delete"), _("Cancel"))); + } + + function execute() + { + // If cancel was clicked, return false. + if ($this->_vars->get('submitbutton') == _("Cancel")) { + return false; + } + + if ($this->_class->get('owner') != Auth::getAuth()) { + return PEAR::raiseError(_("Permission denied")); + } + + // Delete the class. + $storage = &Skoli_Driver::singleton($this->_class->getName()); + $result = $storage->deleteAll(); + if (is_a($result, 'PEAR_Error')) { + return PEAR::raiseError(sprintf(_("Unable to delete \"%s\": %s"), $this->_class->get('name'), $result->getMessage())); + } else { + // Remove share and all groups/permissions. + $result = $GLOBALS['skoli_shares']->removeShare($this->_class); + if (is_a($result, 'PEAR_Error')) { + return $result; + } else { + // Remove class from the display list if it exists + $key = array_search($this->_class->getName(), $GLOBALS['display_classes']); + if ($key !== false) { + unset($GLOBALS['display_classes'][$key]); + $GLOBALS['prefs']->setValue('display_classes', serialize($GLOBALS['display_classes'])); + } + } + } + + return true; + } + +} diff --git a/skoli/lib/Forms/EditClass.php b/skoli/lib/Forms/EditClass.php new file mode 100644 index 000000000..cea9c127d --- /dev/null +++ b/skoli/lib/Forms/EditClass.php @@ -0,0 +1,176 @@ + + * @since Skoli 2.2 + * @package Skoli + */ +class Skoli_EditClassForm extends Horde_Form { + + /** + * Class being edited + */ + var $_class; + + /** + * List of school properties. + * + * @var array + */ + var $_schoolproperties = array( + 'grade', + 'semester', + 'start', + 'end', + 'location', + 'marks'); + + function Skoli_EditClassForm(&$vars, &$class) + { + global $conf, $prefs, $registry; + + $this->_class = &$class; + + parent::Horde_Form($vars, sprintf(_("Edit %s"), $class->get('name'))); + + $this->addHidden('', 'c', 'text', true); + + $this->setSection('properties', _("Properties")); + + $this->addVariable(_("General Settings"), null, 'header', false); + + $this->addVariable(_("Name"), 'name', 'text', true); + $this->addVariable(_("Description"), 'description', 'longtext', false, false, null, array(4, 60)); + + $this->addVariable(_("Category"), 'category', 'category', false); + + $this->addVariable(_("School Specific Settings"), null, 'header', false); + + // Load Skoli_School + require_once SKOLI_BASE . '/lib/School.php'; + + // List schools + $schoollist = Skoli_School::listSchools(); + $this->addVariable(_("School"), 'school', 'enum', true, true, null, array($schoollist, _("Choose:"))); + + $school = new Skoli_School($this->_vars->get('school')); + foreach ($this->_schoolproperties as $name) { + $school->addFormVariable($this, $name); + } + + $this->setSection('students', _("Students")); + + $this->addVariable(_("Address Book"), null, 'header', false); + + $addressbooklist = Skoli_School::listAddressBooks(true); + $actionvariable = &$this->addVariable(_("Address Book"), 'address_book', 'enum', true, count($addressbooklist)>1 ? false : true, null, array($addressbooklist, _("Choose:"))); + if (count($addressbooklist) > 1) { + require_once 'Horde/Form/Action.php'; + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + } else { + $this->_vars->set('address_book', key($addressbooklist)); + } + + $this->addVariable(_("Students"), null, 'header', false); + + if ($this->_vars->get('address_book') != '') { + $searchargs = array( + 'addresses' => array(''), + 'addressbooks' => array($this->_vars->get('address_book')), + 'fields' => array() + ); + if ($search_fields_pref = $prefs->getValue('search_fields')) { + foreach (explode("\n", $search_fields_pref) as $s) { + $s = trim($s); + $s = explode("\t", $s); + if (!empty($s[0]) && ($s[0] == $this->_vars->get('address_book'))) { + $searchargs['fields'][array_shift($s)] = $s; + break; + } + } + } + $resultstmp = $registry->call('contacts/search', $searchargs); + // contacts/search seems to return an array entry for each source. + $results = array(); + foreach ($resultstmp as $r) { + $results = array_merge($results, $r); + } + foreach ($results as $address) { + if (isset($address['__type']) && $address['__type'] == 'Object') { + $addresses[$address['__key']] = $address[$conf['addresses']['name_field']]; + } + } + } else { + $addresses = array(); + } + + $this->addVariable(_("Students"), 'students', 'multienum', false, false, null, array($addresses, 20)); + + $this->setButtons(array(_("Save"))); + } + + function execute() + { + global $conf, $prefs, $registry, $notification; + + /* Add new category. */ + if (strpos($this->_vars->get('category'), '*new*') !== false || $this->_vars->get('category') == $this->_vars->get('new_category')) { + require_once 'Horde/Prefs/CategoryManager.php'; + $cManager = new Prefs_CategoryManager(); + $cManager->add($this->_vars->get('new_category')); + $this->_vars->set('category', $this->_vars->get('new_category')); + } + + $this->_class->set('name', $this->_vars->get('name')); + $this->_class->set('desc', $this->_vars->get('description')); + $this->_class->set('category', $this->_vars->get('category')); + $this->_class->set('address_book', $this->_vars->get('address_book')); + + require_once 'Horde/Date.php'; + foreach ($this->_schoolproperties as $property) { + if ($property == 'start' || $property == 'end') { + $date = new Horde_Date($this->_vars->get($property)); + $this->_vars->set($property, $date->datestamp()); + } else if ($property == 'marks' && $this->_vars->get($property . '_custom') != '') { + $this->_vars->set($property, $this->_vars->get($property . '_custom')); + } + $this->_class->set($property, $this->_vars->get($property) == '' ? null : $this->_vars->get($property)); + } + + // Save students + $driver = &Skoli_Driver::singleton($this->_vars->get('c')); + $result = $driver->addStudents($this->_vars->get('students')); + if (is_a($result, 'PEAR_Error')) { + $notification->push(_("Couldn't add the selected students to the class."), 'horde.warning'); + } + + $result = $this->_class->save(); + if (is_a($result, 'PEAR_Error')) { + return PEAR::raiseError(sprintf(_("Unable to save class \"%s\": %s"), $id, $result->getMessage())); + } + return true; + } + +} diff --git a/skoli/lib/Forms/Entry.php b/skoli/lib/Forms/Entry.php new file mode 100644 index 000000000..6cc1f4d3a --- /dev/null +++ b/skoli/lib/Forms/Entry.php @@ -0,0 +1,184 @@ + + * @package Skoli + */ +class Skoli_EntryForm extends Horde_Form { + + function Skoli_EntryForm(&$vars) + { + global $conf, $prefs, $registry; + + $update = $vars->exists('entry') && $vars->exists('entry'); + + parent::Horde_Form($vars, $update ? _("Update Entry") : _("Add Entry")); + + if ($update) { + $this->addHidden('', 'entry', 'text', true); + $this->addHidden('', 'view', 'text', true); + } + + $classes = Skoli::listClasses(false, PERMS_EDIT); + $classes_enums = array(); + foreach ($classes as $class) { + if ($class->hasPermission(Auth::getAuth(), PERMS_EDIT)) { + $classes_enums[$class->getName()] = $class->get('name'); + } + } + + if (!$this->_vars->exists('class_id') && $vars->exists('class')) { + $this->_vars->set('class_id', $vars->get('class')); + if (!$this->_vars->exists('student_id') && $vars->exists('student')) { + $this->_vars->set('student_id', array($vars->get('student'))); + } else { + $this->_vars->set('student_id', array()); + } + } + + // List classes + $actionvariable = &$this->addVariable(_("Class"), 'class_id', 'enum', true, count($classes)>1 ? false : true, null, array($classes_enums, _("Choose:"))); + if (count($classes) > 1) { + require_once 'Horde/Form/Action.php'; + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + } else { + reset($classes); + $this->_vars->set('class_id', key($classes)); + } + + // Load the selected students + if ($this->_vars->get('class_id') != '') { + $class = current(Skoli::listStudents($vars->get('class_id'))); + foreach ($class['_students'] as $address) { + $addresses[$address['student_id']] = $address[$conf['addresses']['name_field']]; + } + if ($update) { + $this->addVariable(_("Student"), 'student_id', 'enum', true, false, null, array($addresses)); + } else { + $this->addVariable(_("Student"), 'student_id', 'multienum', true, false, null, array($addresses, 14)); + } + } else { + $addresses = array(); + } + + $this->addVariable(_("Date"), 'object_time', 'monthdayyear', true, false, null, array()); + if (!$this->_vars->exists('object_time')) { + $date = new Horde_Date(time()); + $this->_vars->set('object_time', array('month' => $date->month, 'day' => $date->mday, 'year' => $date->year)); + } + + // Load last type from preferences + if (!$this->_vars->exists('object_type')) { + $this->_vars->set('object_type', $prefs->getValue('default_objects_format')); + } + if ($conf['objects']['allow_marks']) { + $types['mark'] = _("Mark"); + } + if ($conf['objects']['allow_objectives']) { + $types['objective'] = _("Objective"); + } + if ($conf['objects']['allow_outcomes']) { + $types['outcome'] = _("Outcome"); + } + if ($conf['objects']['allow_absences']) { + $types['absence'] = _("Absence"); + } + $actionvariable = &$this->addVariable(_("Type"), 'object_type', 'radio', true, false, null, array($types)); + require_once 'Horde/Form/Action.php'; + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + + if ($this->_vars->get('object_type') != '' && $this->_vars->get('class_id') != '') { + switch ($this->_vars->get('object_type')) { + + case 'mark': + $this->addVariable(_("Title"), 'attribute_title', 'text', true, false); + switch ($class['marks']) { + case 'numbers': + $this->addVariable(_("Mark"), 'attribute_mark', 'number', true, false, _("Mark in numbers")); + break; + + case 'percent': + $this->addVariable(_("Mark"), 'attribute_mark', 'number', true, false, _("Mark in percent")); + break; + + default: + $marks_enums = preg_split('/\s*,\s*/', $class['marks']); + $this->addVariable(_("Mark"), 'attribute_mark', 'enum', true, false, null, array(array_combine($marks_enums, $marks_enums), _("Choose:"))); + } + // Load Skoli_School + require_once SKOLI_BASE . '/lib/School.php'; + $school = new Skoli_School($class['school']); + $school->addFormVariable($this, 'subject', array(true)); + $this->addVariable(_("Weight"), 'attribute_weight', 'number', true, false); + if (!$this->_vars->exists('attribute_weight')) { + $this->_vars->set('attribute_weight', 1); + } + break; + + case 'objective': + // Load Skoli_School + require_once SKOLI_BASE . '/lib/School.php'; + $school = new Skoli_School($class['school']); + $school->addFormVariable($this, 'subject', array(false, true)); + $school->addFormVariable($this, 'category', array($this->_vars->get('attribute_subject'))); + $this->addVariable(_("Objective"), 'attribute_objective', 'longtext', true, false, null, array(4, 60)); + break; + + case 'outcome': + $this->addVariable(_("Outcome"), 'attribute_outcome', 'longtext', true, false, null, array(2, 60)); + $this->addVariable(_("Completed?"), 'attribute_completed', 'boolean', true, false); + $this->addVariable(_("Comment"), 'attribute_comment', 'longtext', false, false, null, array(4, 60)); + break; + + case 'absence': + $this->addVariable(_("Absence"), 'attribute_absence', 'number', true, false, _("Absence in number of lessons")); + $this->addVariable(_("Excused?"), 'attribute_excused', 'boolean', true, false); + if (!$this->_vars->exists('attribute_absence')) { + $this->_vars->set('attribute_excused', true); + } + $this->addVariable(_("Comment"), 'attribute_comment', 'longtext', false, false, null, array(4, 60)); + break; + } + } + + $this->setButtons(array($update ? _("Save") : _("Add"))); + } + + function execute() + { + global $conf, $prefs, $registry, $notification; + + // Save last type to preferences + $prefs->setValue('default_objects_format', $this->_vars->get('object_type')); + + $driver = &Skoli_Driver::singleton($this->_vars->get('class_id')); + $result = $driver->addEntry($this->_vars); + if (is_a($result, 'PEAR_Error')) { + $notification->push(_("Couldn't add the new entry."), 'horde.warning'); + } + + return $result; + } +} diff --git a/skoli/lib/School.php b/skoli/lib/School.php new file mode 100644 index 000000000..52f1ebc24 --- /dev/null +++ b/skoli/lib/School.php @@ -0,0 +1,288 @@ + + * @package Skoli + */ +class Skoli_School { + + /** + * School list from template. + * + * @var array + */ + public static $schools; + + /** + * Current school from template. + * + * @var array + */ + var $school; + + /** + * Load the school list from template. + */ + function Skoli_School($schoolName) + { + self::_loadSchools(); + if (!isset(self::$schools[$schoolName]) || !is_array(self::$schools[$schoolName])) { + return PEAR::raiseError(sprintf(_("Error loading the school \"%s\" from template."), $schoolName)); + } else { + $this->school = self::$schools[$schoolName]; + } + } + + /** + * Adds a variable to the current form. + * + * @param Horde_Form $form The current form. + * + * @param string $property The property to add. + * + * @param array $params Property dependent parameters. + */ + function addFormVariable(&$form, $property, $params = array()) + { + switch ($property) { + case 'start': + case 'end': + $form->addVariable(_(ucfirst($property)), $property, 'monthdayyear', true, false, null, array()); + if ($form->_vars->exists('semester') && isset($this->school['semester']) && is_array($this->school['semester'])) { + foreach ($this->school['semester'] as $semester) { + if ($semester['name'] == $form->_vars->get('semester')) { + $activesemester = $semester; + break; + } + } + if (isset($activesemester[$property])) { + require_once 'Horde/Date.php'; + if ($property == 'start') { + $startdate = 0; + } else { + $startdate = new Horde_Date($form->_vars->get('start')); + $startdate = $startdate->datestamp(); + } + $date = new Horde_Date($this->_getSemesterTime($activesemester[$property], $startdate)); + $form->_vars->set($property, array('month' => $date->month, 'day' => $date->mday, 'year' => $date->year)); + } + } + break; + + case 'marks': + $marksformat = array( + 'numbers' => _("Format in numbers"), + 'percent' => _("Format in percent"), + 'custom' => _("Custom format:") + ); + if (isset($this->school[$property])) { + $form->_vars->set($property, $this->school[$property]); + if (!isset($marksformat[$this->school[$property]])) { + $marksformat['custom'] .= ' ' . $this->school[$property]; + $marksformat[$this->school[$property]] = $marksformat['custom']; + unset($marksformat['custom']); + } + $form->addVariable(_(ucfirst($property)), $property, 'enum', true, true, null, array($marksformat, _("Choose:"))); + } else { + require_once 'Horde/Form/Action.php'; + if ($form->_vars->exists($property) && !isset($marksformat[$form->_vars->get($property)])) { + $form->_vars->set($property . '_custom', $form->_vars->get($property)); + $form->_vars->set($property, 'custom'); + } + $actionvariable = &$form->addVariable(_(ucfirst($property)), $property, 'enum', true, false, null, array($marksformat, _("Choose:"))); + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + if ($form->_vars->get($property) == 'custom') { + $form->addVariable('', $property . '_custom', 'text', true, false, _("List with custom marks separated by comma (best mark first)")); + } + } + break; + + case 'subject': + $obligatory = isset($params[0]) ? $params[0] : true; + $onlywithobjectives = isset($params[1]) ? $params[1] : false; + if (isset($this->school['subjects'])) { + $values = array(); + foreach ($this->school['subjects'] as $key=>$value) { + if (!$onlywithobjectives || ($onlywithobjectives && is_array($value))) { + $subject = is_array($value) ? $key : $value; + $values[$subject] = $subject; + } + } + if ($onlywithobjectives) { + if (count($values) > 0) { + require_once 'Horde/Form/Action.php'; + $actionvariable = &$form->addVariable(_(ucfirst($property)), 'attribute_subject', 'enum', $obligatory, false, null, array(array_merge(array(_("Interdisciplinary")=>_("Interdisciplinary")), $values))); + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + } else { + $form->addVariable(_(ucfirst($property)), 'attribute_subject', 'text', true, true); + $form->_vars->set('attribute_subject', _("Interdisciplinary")); + } + } else { + $form->addVariable(_(ucfirst($property)), 'attribute_subject', 'enum', $obligatory, false, null, array($values, _("Choose:"))); + } + } else { + $form->addVariable(_(ucfirst($property)), $property, 'text', $obligatory, false); + } + break; + + case 'category': + $subject = !empty($params[0]) ? $params[0] : _("Interdisciplinary"); + if ($subject != _("Interdisciplinary") && isset($this->school['subjects'][$subject]) && is_array($this->school['subjects'][$subject])) { + $values = array(); + foreach ($this->school['subjects'][$subject] as $value) { + $values[$value] = $value; + } + $form->addVariable(_(ucfirst($property)), 'attribute_category', 'enum', true, false, null, array($values, _("Choose:"))); + } else if ($subject == _("Interdisciplinary") && isset($this->school['objectives'])) { + $values = array(); + foreach ($this->school['objectives'] as $value) { + $values[$value] = $value; + } + $form->addVariable(_(ucfirst($property)), 'attribute_category', 'enum', true, false, null, array($values, _("Choose:"))); + } else { + $form->addVariable(_(ucfirst($property)), 'attribute_category', 'text', true, false); + } + break; + + default: + if (isset($this->school[$property]) && is_array($this->school[$property])) { + if (count($this->school[$property]) > 1) { + $values = array(); + foreach ($this->school[$property] as $value) { + $key = is_array($value) ? $value['name'] : $value; + $values[$key] = $key; + } + if (is_array(current($this->school[$property]))) { + require_once 'Horde/Form/Action.php'; + $actionvariable = &$form->addVariable(_(ucfirst($property)), $property, 'enum', false, false, null, array($values, _("Choose:"))); + $actionvariable->setAction(Horde_Form_Action::factory('reload')); + } else { + $form->addVariable(_(ucfirst($property)), $property, 'enum', false, false, null, array($values, _("Choose:"))); + } + } else { + $form->addVariable(_(ucfirst($property)), $property, 'text', false, true); + $value = current($this->school[$property]); + $form->_vars->set($property, is_array($value) ? $value['name'] : $value); + } + } else { + $form->addVariable(_(ucfirst($property)), $property, 'text', false, false); + } + } + } + + /** + * Returns a timestamp for the specified semester start- or enddate. + * + * @param mixed The dateformat specified in conf/schools.php for this date. + * + * @return int The timestamp. + */ + private function _getSemesterTime($format, $startdate) + { + if (is_int($format)) { + // Timestamp format + $timestamp = $format; + } else if (preg_match('/^W([0-9]{2})\-[0-9]$/', $format, $m)) { + $year = date('Y'); + if (date('W') > $m[1]) { + $year++; + } + $timestamp = strtotime($year . '-' . $format); + } else { + $timestamp = strtotime($format); + } + if (is_int($timestamp) && $timestamp > 0) { + if ($startdate >= $timestamp) { + $timestamp = strtotime('+1 year', $timestamp); + } + return $timestamp; + } else { + return ''; + } + } + + /** + * Returns all schools specified in conf/schools.php. + * + * @return array The school list. + */ + public static function listSchools() + { + self::_loadSchools(); + $schools = array(); + foreach (self::$schools as $key=>$val) { + $schools[$key] = $val['title']; + } + return $schools; + } + + /** + * Loads the schools specified in conf/schools.php + */ + private static function _loadSchools() + { + if (!isset(self::$schools)) { + require_once SKOLI_BASE . '/config/schools.php'; + self::$schools = $cfgSchools; + } + } + + /** + * Returns all addressbooks skoli is defined to use. + * + * @param boolean $all If set to true return all addressbooks a user has access to. + * + * @return array The address book list. + */ + public static function listAddressBooks($all = false) + { + global $conf, $prefs, $registry; + + $addressbooks = $registry->call('contacts/sources'); + + if (!$all && $conf['addresses']['storage'] == 'custom') { + if (isset($addressbooks[$conf['addresses']['address_book']])) { + $addressbooks = array($conf['addresses']['address_book'] => $addressbooks[$conf['addresses']['address_book']]); + } else { + $addressbooks = array(); + } + } + + return $addressbooks; + } + + /** + * Returns the parsed contact list name. + * + * @param string $contactlist The contact list name to parse. + * + * @param Horde_Variables $vars The variables to use as replacement. + * + * @param boolean $force If set to true also replaces empty fields. + * + * @return string The parsed contact list name. + */ + public static function parseContactListName($contactlist, $vars, $force = false) + { + $contactlistsubs = array( + '%c' => 'name', + '%g' => 'grade', + '%s' => 'semester' + ); + foreach ($contactlistsubs as $pattern=>$field) { + if (strpos($contactlist, $pattern) !== false && ($vars->get($field) != '' || $force)) { + $contactlist = str_replace($pattern, $vars->get($field), $contactlist); + } + } + return $contactlist; + } + +} diff --git a/skoli/lib/Skoli.php b/skoli/lib/Skoli.php new file mode 100644 index 000000000..838633aa4 --- /dev/null +++ b/skoli/lib/Skoli.php @@ -0,0 +1,1083 @@ + + * @package Skoli + */ +class Skoli { + + /** + * Initial app setup code. + */ + function initialize() + { + // Update the preference for what classes to display. If the user + // doesn't have any selected class then do nothing. + $GLOBALS['display_classes'] = @unserialize($GLOBALS['prefs']->getValue('display_classes')); + if (!$GLOBALS['display_classes']) { + $GLOBALS['display_classes'] = array(); + } + if (($classId = Util::getFormData('display_class')) !== null) { + if (is_array($classId)) { + $GLOBALS['display_classes'] = $classId; + } else { + if (in_array($classId, $GLOBALS['display_classes'])) { + $key = array_search($classId, $GLOBALS['display_classes']); + unset($GLOBALS['display_classes'][$key]); + } else { + $GLOBALS['display_classes'][] = $classId; + } + } + $GLOBALS['prefs']->setValue('show_students', Util::getFormData('show_students') ? 1 : 0); + } + + $GLOBALS['prefs']->setValue('display_classes', serialize($GLOBALS['display_classes'])); + } + + /** + * Returns all classes a user has access to, according to several + * parameters/permission levels. + * + * @param boolean $owneronly Only return classes that this user owns? + * Defaults to false. + * @param integer $permission The permission to filter classes by. + * + * @return array The class list. + */ + function listClasses($owneronly = false, $permission = PERMS_SHOW) + { + global $registry; + + $classes = $GLOBALS['skoli_shares']->listShares(Auth::getAuth(), $permission, $owneronly ? Auth::getAuth() : null, 0, 0, 'name'); + if (is_a($classes, 'PEAR_Error')) { + Horde::logMessage($classes, __FILE__, __LINE__, PEAR_LOG_ERR); + return array(); + } + + // Check if we have access to the attached addressbook. + $addressbooks = $registry->call('contacts/sources'); + foreach ($classes as $key=>$val) { + if (!isset($addressbooks[$val->get('address_book')])) { + unset($classes[$key]); + } + } + + return $classes; + } + + /** + * Retrieves the current user's student list from storage. + * + * This function will also sort the resulting list, if requested. + * + * @param array $classes An array of classes to display, a + * single classname or null/empty to + * display classes $GLOBALS['display_classes']. + * @param string $sortby_student The field by which to sort + * (SKOLI_SORT_PRIORITY, SKOLI_SORT_NAME + * SKOLI_SORT_DUE, SKOLI_SORT_COMPLETION). + * @param integer $sortdir_student The direction by which to sort + * (SKOLI_SORT_ASCEND, SKOLI_SORT_DESCEND). + * @param string $sortby_class The field by which to sort + * (SKOLI_SORT_PRIORITY, SKOLI_SORT_NAME + * SKOLI_SORT_DUE, SKOLI_SORT_COMPLETION). + * @param integer $sortdir_class The direction by which to sort + * (SKOLI_SORT_ASCEND, SKOLI_SORT_DESCEND). + * + * @return array A list of the requested classes with students. + */ + function listStudents($classes = null, + $sortby_student = null, + $sortdir_student = null, + $sortby_class = null, + $sortdir_class = null) + { + global $prefs, $registry; + + if (is_null($classes)) { + $classes = $GLOBALS['display_classes']; + } else if (!is_array($classes)) { + $classes = array($classes); + } + if (is_null($sortby_student)) { + $sortby_student = $prefs->getValue('sortby_student'); + } + if (is_null($sortdir_student)) { + $sortdir_student = $prefs->getValue('sortdir_student'); + } + if (is_null($sortby_class)) { + $sortby_class = $prefs->getValue('sortby_class'); + } + if (is_null($sortdir_class)) { + $sortdir_class = $prefs->getValue('sortdir_class'); + } + + $list = array(); + $i = 0; + $addressbooks = $registry->call('contacts/sources'); + foreach ($classes as $class) { + /* Get all data about the shared class */ + $share = $GLOBALS['skoli_shares']->getShare($class); + + /* Check permissions */ + if (!$share->hasPermission(Auth::getAuth(), PERMS_READ) || !isset($addressbooks[$share->get('address_book')])) { + continue; + } + + $list[$i] = $share->datatreeObject->data; + $list[$i]['_id'] = $class; + $list[$i]['_edit'] = $share->hasPermission(Auth::getAuth(), PERMS_EDIT); + + /* Add all students to the list */ + $driver = &Skoli_Driver::singleton($class); + $list[$i]['_students'] = $driver->getStudents(); + $student_columns = @unserialize($prefs->getValue('student_columns')); + foreach ($list[$i]['_students'] as $key=>$student) { + $studentdetails = Skoli::getStudent($list[$i]['address_book'], $student['student_id']); + if (count($studentdetails) > 0) { + $list[$i]['_students'][$key] += $studentdetails; + if (in_array('lastentry', $student_columns)) { + $list[$i]['_students'][$key]['_lastentry'] = $driver->lastEntry($student['student_id']); + } + if (in_array('summarks', $student_columns)) { + $list[$i]['_students'][$key]['_summarks'] = Skoli::sumMarks($class, $student['student_id']); + } + if (in_array('sumabsences', $student_columns)) { + $list[$i]['_students'][$key]['_sumabsences'] = Skoli::sumAbsences($class, $student['student_id']); + } + } else { + unset($list[$i]['_students'][$key]); + } + } + $i++; + } + + /* Sort the array if we have a sort function defined for this + * field. */ + $prefix = ($sortdir_class == SKOLI_SORT_DESCEND) ? '_rsort' : '_sort'; + usort($list, array('Skoli', $prefix . '_class_' . $sortby_class)); + $prefix = ($sortdir_student == SKOLI_SORT_DESCEND) ? '_rsort' : '_sort'; + for ($i = 0; $i < count($list); $i++) { + usort($list[$i]['_students'], array('Skoli', $prefix . '_student_' . $sortby_student)); + } + + return $list; + } + + /** + * Retrieves all data about a student. + * + * @param string $addressbook The addressbook. + * + * @param string $id An ID from the student contact. + * + * @return array A list with the data from the requested student. + */ + function getStudent($addressbook, $id) + { + global $registry; + + $student = array(); + $apiargs = array( + 'source' => $addressbook, + 'objectId' => $id + ); + $result = $registry->call('contacts/getContact', $apiargs); + if ($result === false || is_a($result, 'PEAR_Error')) { + $notification->push(sprintf(_("Couldn't create the contact list \"%s\"."), $this->_vars->get('contact_list')), 'horde.info'); + } else { + $student = $result; + } + return $student; + } + + /** + * Retrieves a sorted entry list from storage. + * + * @param string $classid The class ID. + * + * @param string $studentid The student ID. + * + * @param string $type The entry type to search in. + * + * @param array $searchparams Some additional search parameters. + * + * @param string $sortby The field by which to sort + * (SKOLI_SORT_CLASS, SKOLI_SORT_STUDENT + * SKOLI_SORT_DATE, SKOLI_SORT_TYPE). + * @param integer $sortdir The direction by which to sort + * (SKOLI_SORT_ASCEND, SKOLI_SORT_DESCEND). + * + * @return array Sorted list with all entries. + */ + function listEntries($classid = null, + $studentid = null, + $type = null, + $searchparams = array(), + $sortby = null, + $sortdir = null) + { + global $conf, $prefs, $registry; + + $dateFormat = $prefs->getValue('date_format'); + $entryTypes = array( + 'mark' => _("Mark"), + 'objective' => _("Objective"), + 'outcome' => _("Outcome"), + 'absence' => _("Absence") + ); + + if (is_null($classid)) { + $classes = Skoli::listClasses(); + } else { + $share = $GLOBALS['skoli_shares']->getShare($classid); + $classes = array($classid => $share); + } + + if (is_null($sortby)) { + $sortby = SKOLI_SORT_CLASS; + } + if (is_null($sortdir)) { + $sortdir = SKOLI_SORT_ASCEND; + } + + $entrylist = array(); + $i = 0; + $addressbooks = $registry->call('contacts/sources'); + foreach ($classes as $class_id=>$share) { + /* Check permissions */ + if (!$share->hasPermission(Auth::getAuth(), PERMS_READ) || !isset($addressbooks[$share->get('address_book')])) { + continue; + } + + $share_permissions_edit = $share->hasPermission(Auth::getAuth(), PERMS_EDIT); + $share_permissions_delete = $share->hasPermission(Auth::getAuth(), PERMS_DELETE); + + $driver = &Skoli_Driver::singleton($class_id); + $entries = $driver->getEntries($studentid, $type, $searchparams); + foreach ($entries as $student) { + $studentdetails = Skoli::getStudent($share->get('address_book'), $student['student_id']); + foreach ($student['_entries'] as $entry) { + $entrylist[$i] = $entry['_attributes']; + $entrylist[$i]['class'] = $share->get('name'); + $entrylist[$i]['classid'] = $class_id; + $entrylist[$i]['student'] = $studentdetails[$conf['addresses']['name_field']]; + $entrylist[$i]['date'] = strftime($dateFormat, $entry['object_time']); + $entrylist[$i]['timestamp'] = $entry['object_time']; + $entrylist[$i]['typename'] = $entryTypes[$entry['object_type']]; + $entrylist[$i]['type'] = $entry['object_type']; + $entrylist[$i]['_edit'] = $share_permissions_edit; + $entrylist[$i]['_delete'] = $share_permissions_delete; + $entrylist[$i]['_id'] = $entry['object_id']; + $i++; + } + } + } + + /* Sort the array if we have a sort function defined for this + * field. */ + $prefix = ($sortdir == SKOLI_SORT_DESCEND) ? '_rsort' : '_sort'; + usort($entrylist, array('Skoli', $prefix . '_entry_' . $sortby)); + + return $entrylist; + } + + /** + * Sum up all excused and not excused absences for a given student. + * + * @param string $classid An ID from the class. + * + * @param string $studentid An ID from the student contact. + * + * @return array A list with the requested data. + */ + function sumAbsences($classid, $studentid) + { + $driver = &Skoli_Driver::singleton($classid); + $entries = current($driver->getEntries($studentid, 'absence')); + + $excused = 0; + $notexcused = 0; + foreach($entries['_entries'] as $entry) { + $entry['_attributes']['absence'] = Skoli::convertNumber($entry['_attributes']['absence']); + if (empty($entry['_attributes']['excused'])) { + $notexcused += $entry['_attributes']['absence']; + } else { + $excused += $entry['_attributes']['absence']; + } + } + + return array($excused, $notexcused, $excused + $notexcused); + } + + /** + * Sum up all completed and open outcomes for a given student. + * + * @param string $classid An ID from the class. + * + * @param string $studentid An ID from the student contact. + * + * @return array A list with the requested data. + */ + function sumOutcomes($classid, $studentid) + { + $driver = &Skoli_Driver::singleton($classid); + $entries = current($driver->getEntries($studentid, 'outcome')); + + $completed = 0; + $open = 0; + foreach($entries['_entries'] as $entry) { + if (empty($entry['_attributes']['completed'])) { + $open++; + } else { + $completed++; + } + } + + return array($completed, $open, $completed + $open); + } + + /** + * Sum up all marks for a given student. + * + * @param string $classid An ID from the class. + * + * @param string $studentid An ID from the student contact. + * + * @param string $subject Only sum up marks from this subject. + * + * @return float The requested data. + */ + function sumMarks($classid, $studentid, $subject = null) + { + global $prefs; + + $driver = &Skoli_Driver::singleton($classid); + if (!is_null($subject)) { + $params = array(array('name' => 'subject', 'value' => $subject, 'strict' => 1)); + } else { + $params = null; + } + $entries = current($driver->getEntries($studentid, 'mark', $params)); + + /* Count weights */ + $totalweight = 0; + foreach($entries['_entries'] as $entry) { + $totalweight += Skoli::convertNumber($entry['_attributes']['weight']); + } + + if ($totalweight <= 0) { + return ''; + } + + $sum = 0; + $weight = 100 / $totalweight; + foreach($entries['_entries'] as $entry) { + $sum += $weight * Skoli::convertNumber($entry['_attributes']['weight']) * Skoli::convertNumber($entry['_attributes']['mark']); + } + + if ($sum > 0) { + return round($sum / 100, $prefs->getValue('marks_roundby')); + } else { + return ''; + } + } + + /** + * Converts numbers with a comma to a valid php number. + * + * @param string $number The number to convert. + * + * @return string The converted number + */ + function convertNumber($number) + { + $number = str_replace(',', '.', $number); + return $number; + } + + /** + * Build Skoli's list of menu items. + */ + function getMenu($returnType = 'object') + { + global $conf, $registry, $browser, $print_link; + + require_once 'Horde/Menu.php'; + + $menu = new Menu(HORDE_MENU_MASK_ALL); + $menu->add(Horde::applicationUrl('list.php'), _("List Classes"), 'skoli.png', null, null, null, basename($_SERVER['PHP_SELF']) == 'index.php' ? 'current' : null); + if (count(Skoli::listClasses(false, PERMS_EDIT))) { + $menu->add(Horde::applicationUrl('add.php'), _("_New Entry"), 'add.png', null, null, null, Util::getFormData('entry') ? '__noselection' : null); + } + + /* Search. */ + $menu->add(Horde::applicationUrl('search.php'), _("_Search"), 'search.png', $registry->getImageDir('horde')); + + /* Import/Export. */ + if ($conf['menu']['export']) { + $menu->add(Horde::applicationUrl('data.php'), _("_Export"), 'data.png', $registry->getImageDir('horde')); + } + + // @TODO Implement an easy form to create timetables in e.g. Kronolith + /* Timetable. + * Show this item only if an application provides 'calendar/show' and we have permissions to view it. + $app = $registry->hasMethod('calendar/show'); + if ($app !== false && $registry->get('status', $app) != 'inactive' && $registry->hasPermission($app, PERMS_EDIT)) { + $menu->add(Horde::applicationUrl(Util::addParameter('timetable.php', 'actionID', 'new_timetable')), _("_New Timetable"), 'timetable.png'); + } + */ + + if ($returnType == 'object') { + return $menu; + } else { + return $menu->render(); + } + } + + /** + * Comparison function for sorting classes by semester start date. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_semesterstart($a, $b) + { + if ($a['start'] == $b['start'] ) { + return 0; + } + + // Treat empty start dates as farthest into the future. + if ($a['start'] == 0) { + return 1; + } + if ($b['start'] == 0) { + return -1; + } + + return ($a['start'] > $b['start']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting classes by semester start date. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater, + * 0 if they are equal. + */ + function _rsort_class_semesterstart($a, $b) + { + if ($a['start'] == $b['start']) { + return 0; + } + + // Treat empty start dates as farthest into the future. + if ($a['start'] == 0) { + return -1; + } + if ($b['start'] == 0) { + return 1; + } + + return ($a['start'] < $b['start']) ? 1 : -1; + } + + /** + * Comparison function for sorting classes by semester end date. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_semesterend($a, $b) + { + if ($a['end'] == $b['end'] ) { + return 0; + } + + // Treat empty end dates as farthest into the future. + if ($a['end'] == 0) { + return 1; + } + if ($b['end'] == 0) { + return -1; + } + + return ($a['end'] > $b['end']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting classes by semester end date. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater, + * 0 if they are equal. + */ + function _rsort_class_semesterend($a, $b) + { + if ($a['end'] == $b['end']) { + return 0; + } + + // Treat empty end dates as farthest into the future. + if ($a['end'] == 0) { + return -1; + } + if ($b['end'] == 0) { + return 1; + } + + return ($a['end'] < $b['end']) ? 1 : -1; + } + + /** + * Comparison function for sorting classes by name. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_name($a, $b) + { + return strcasecmp($a['name'], $b['name']); + } + + /** + * Comparison function for reverse sorting classes by name. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater; + * 0 if they are equal. + */ + function _rsort_class_name($a, $b) + { + return strcasecmp($b['name'], $a['name']); + } + + /** + * Comparison function for sorting classes by grade. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_grade($a, $b) + { + return strcasecmp($a['grade'], $b['grade']); + } + + /** + * Comparison function for reverse sorting classes by grade. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater; + * 0 if they are equal. + */ + function _rsort_class_grade($a, $b) + { + return strcasecmp($b['grade'], $a['grade']); + } + + /** + * Comparison function for sorting classes by semester. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_semester($a, $b) + { + return strcasecmp($a['semester'], $b['semester']); + } + + /** + * Comparison function for reverse sorting classes by semester. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater; + * 0 if they are equal. + */ + function _rsort_class_semester($a, $b) + { + return strcasecmp($b['semester'], $a['semester']); + } + + /** + * Comparison function for sorting classes by location. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_location($a, $b) + { + return strcasecmp($a['location'], $b['location']); + } + + /** + * Comparison function for reverse sorting classes by location. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater; + * 0 if they are equal. + */ + function _rsort_class_location($a, $b) + { + return strcasecmp($b['location'], $a['location']); + } + + /** + * Comparison function for sorting classes by category. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer 1 if class one is greater, -1 if class two is greater; + * 0 if they are equal. + */ + function _sort_class_category($a, $b) + { + return strcasecmp($a['category'] ? $a['category'] : _("Unfiled"), + $b['category'] ? $b['category'] : _("Unfiled")); + } + + /** + * Comparison function for reverse sorting classes by category. + * + * @param array $a Class one. + * @param array $b Class two. + * + * @return integer -1 if class one is greater, 1 if class two is greater; + * 0 if they are equal. + */ + function _rsort_class_category($a, $b) + { + return strcasecmp($b['category'] ? $b['category'] : _("Unfiled"), + $a['category'] ? $a['category'] : _("Unfiled")); + } + + /** + * Comparison function for sorting students by name. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer 1 if student one is greater, -1 if student two is greater; + * 0 if they are equal. + */ + function _sort_student_name($a, $b) + { + return strcasecmp($a['name'], $b['name']); + } + + /** + * Comparison function for reverse sorting students by name. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer -1 if student one is greater, 1 if student two is greater; + * 0 if they are equal. + */ + function _rsort_student_name($a, $b) + { + return strcasecmp($b['name'], $a['name']); + } + + /** + * Comparison function for sorting students by last entry date. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer 1 if student one is greater, -1 if student two is greater; + * 0 if they are equal. + */ + function _sort_student_lastentry($a, $b) + { + // Treat empty dates as farthest into the past. + if (!isset($a['_lastentry']) || $a['_lastentry'] == 0) { + return -1; + } + if (!isset($b['_lastentry']) || $b['_lastentry'] == 0) { + return 1; + } + + if ($a['_lastentry'] == $b['_lastentry'] ) { + return 0; + } + + return ($a['_lastentry'] > $b['_lastentry']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting students by last entry date. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer -1 if student one is greater, 1 if student two is greater, + * 0 if they are equal. + */ + function _rsort_student_lastentry($a, $b) + { + // Treat empty dates as farthest into the past. + if (!isset($a['_lastentry']) || $a['_lastentry'] == 0) { + return 1; + } + if (!isset($b['_lastentry']) || $b['_lastentry'] == 0) { + return -1; + } + + if ($a['_lastentry'] == $b['_lastentry']) { + return 0; + } + + return ($a['_lastentry'] < $b['_lastentry']) ? 1 : -1; + } + + /** + * Comparison function for sorting students by sumabsences. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer 1 if student one is greater, -1 if student two is greater; + * 0 if they are equal. + */ + function _sort_student_sumabsences($a, $b) + { + if ($a['_sumabsences'] == $b['_sumabsences'] ) { + return 0; + } + + return ($a['_sumabsences'] > $b['_sumabsences']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting students by sumabsences. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer -1 if student one is greater, 1 if student two is greater, + * 0 if they are equal. + */ + function _rsort_student_sumabsences($a, $b) + { + if ($a['_sumabsences'] == $b['_sumabsences']) { + return 0; + } + + return ($a['_sumabsences'] < $b['_sumabsences']) ? 1 : -1; + } + + /** + * Comparison function for sorting students by summarks. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer 1 if student one is greater, -1 if student two is greater; + * 0 if they are equal. + */ + function _sort_student_summarks($a, $b) + { + // Treat empty sums as lowest mark. + if ($a['_summarks'] == '') { + return -1; + } + if ($b['_summarks'] == '') { + return 1; + } + + if ($a['_summarks'] == $b['_summarks'] ) { + return 0; + } + + return ($a['_summarks'] > $b['_summarks']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting students by summarks. + * + * @param array $a Student one. + * @param array $b Student two. + * + * @return integer -1 if student one is greater, 1 if student two is greater, + * 0 if they are equal. + */ + function _rsort_student_summarks($a, $b) + { + // Treat empty sums as lowest mark. + if ($a['_summarks'] == '') { + return 1; + } + if ($b['_summarks'] == '') { + return -1; + } + + if ($a['_summarks'] == $b['_summarks']) { + return 0; + } + + return ($a['_summarks'] < $b['_summarks']) ? 1 : -1; + } + + /** + * Comparison function for sorting entries by date. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer 1 if entry one is greater, -1 if entry two is greater; + * 0 if they are equal. + */ + function _sort_entry_date($a, $b) + { + if ($a['timestamp'] == $b['timestamp'] ) { + return 0; + } + + return ($a['timestamp'] > $b['timestamp']) ? -1 : 1; + } + + /** + * Comparison function for reverse sorting entries by date. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer -1 if entry one is greater, 1 if entry two is greater, + * 0 if they are equal. + */ + function _rsort_entry_date($a, $b) + { + if ($a['timestamp'] == $b['timestamp']) { + return 0; + } + + return ($a['timestamp'] < $b['timestamp']) ? -1 : 1; + } + + /** + * Comparison function for sorting entries by class. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer 1 if entry one is greater, -1 if entry two is greater; + * 0 if they are equal. + */ + function _sort_entry_class($a, $b) + { + if ($a['class'] == $b['class'] ) { + return Skoli::_sort_entry_date($a, $b); + } + + return ($a['class'] > $b['class']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting entries by class. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer -1 if entry one is greater, 1 if entry two is greater, + * 0 if they are equal. + */ + function _rsort_entry_class($a, $b) + { + if ($a['class'] == $b['class']) { + return Skoli::_rsort_entry_date($a, $b); + } + + return ($a['class'] < $b['class']) ? 1 : -1; + } + + /** + * Comparison function for sorting entries by student. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer 1 if entry one is greater, -1 if entry two is greater; + * 0 if they are equal. + */ + function _sort_entry_student($a, $b) + { + if ($a['student'] == $b['student'] ) { + return Skoli::_sort_entry_date($a, $b); + } + + return ($a['student'] > $b['student']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting entries by student. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer -1 if entry one is greater, 1 if entry two is greater, + * 0 if they are equal. + */ + function _rsort_entry_student($a, $b) + { + if ($a['student'] == $b['student']) { + return Skoli::_rsort_entry_date($a, $b); + } + + return ($a['student'] < $b['student']) ? 1 : -1; + } + + /** + * Comparison function for sorting entries by type. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer 1 if entry one is greater, -1 if entry two is greater; + * 0 if they are equal. + */ + function _sort_entry_type($a, $b) + { + if ($a['type'] == $b['type'] ) { + return Skoli::_sort_entry_date($a, $b); + } + + return ($a['type'] > $b['type']) ? 1 : -1; + } + + /** + * Comparison function for reverse sorting entries by type. + * + * @param array $a Entry one. + * @param array $b Entry two. + * + * @return integer -1 if entry one is greater, 1 if entry two is greater, + * 0 if they are equal. + */ + function _rsort_entry_type($a, $b) + { + if ($a['type'] == $b['type']) { + return Skoli::_rsort_entry_date($a, $b); + } + + return ($a['type'] < $b['type']) ? 1 : -1; + } +} diff --git a/skoli/lib/base.php b/skoli/lib/base.php new file mode 100644 index 000000000..6d208e171 --- /dev/null +++ b/skoli/lib/base.php @@ -0,0 +1,52 @@ +pushApp('skoli', !defined('AUTH_HANDLER'))), 'PEAR_Error')) { + if ($pushed->getCode() == 'permission_denied') { + Horde::authenticationFailureRedirect(); + } + Horde::fatal($pushed, __FILE__, __LINE__, false); +} +$conf = &$GLOBALS['conf']; +@define('SKOLI_TEMPLATES', $registry->get('templates')); + +// Horde framework libraries. +require_once 'Horde/History.php'; + +// Notification system. +$notification = &Notification::singleton(); +$notification->attach('status'); + +// Define the base file path of Skoli. +@define('SKOLI_BASE', dirname(__FILE__) . '/..'); + +// Skoli base library +require_once SKOLI_BASE . '/lib/Skoli.php'; +require_once SKOLI_BASE . '/lib/Driver.php'; + +// Start output compression. +Horde::compressOutput(); + +// Create a share instance. +require_once 'Horde/Share.php'; +$GLOBALS['skoli_shares'] = &Horde_Share::singleton($registry->getApp()); + +Skoli::initialize(); diff --git a/skoli/lib/version.php b/skoli/lib/version.php new file mode 100644 index 000000000..5e6d33190 --- /dev/null +++ b/skoli/lib/version.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/skoli/list.php b/skoli/list.php new file mode 100644 index 000000000..65e080420 --- /dev/null +++ b/skoli/list.php @@ -0,0 +1,165 @@ + + */ + +@define('SKOLI_BASE', dirname(__FILE__)); +require_once SKOLI_BASE . '/lib/base.php'; + +$title = _("My Classes"); + +/* Get and set Variables */ +require_once 'Horde/Variables.php'; +$vars = Variables::getDefaultVariables(); + +/* Get the current action ID. */ +$actionID = Util::getFormData('actionID'); + +/* Sort out the sorting values */ +if (($sortby_class = Util::getFormData('sortby_class')) !== null) { + if ($sortby_class == $prefs->getValue('sortby_class')) { + $prefs->setValue('sortdir_class', !$prefs->getValue('sortdir_class')); + } else { + $prefs->setValue('sortdir_class', SKOLI_SORT_ASCEND); + } + $prefs->setValue('sortby_class', $sortby_class); + if ($sortby_class == 'name') { + if ($sortby_class == $prefs->getValue('sortby_student')) { + $prefs->setValue('sortdir_student', !$prefs->getValue('sortdir_student')); + } else { + $prefs->setValue('sortdir_student', SKOLI_SORT_ASCEND); + } + $prefs->setValue('sortby_student', $sortby_class); + } +} +if (($sortby_student = Util::getFormData('sortby_student')) !== null) { + $prefs->setValue('sortby_student', $sortby_student); + if ($sortby_student == $prefs->getValue('sortby_student')) { + $prefs->setValue('sortdir_student', !$prefs->getValue('sortdir_student')); + } else { + $prefs->setValue('sortdir_student', SKOLI_SORT_ASCEND); + } +} + +/* Check if we have access to an application who provides contacts/getContact */ +$app = $registry->hasMethod('contacts/getContact'); +if ($app == false || $registry->get('status', $app) == 'inactive' || !$registry->hasPermission($app, PERMS_SHOW)) { + $notification->push(_("Skoli needs an applications who provides contacts (e.g. turba)."), 'horde.warning'); +} + +/* Redirect to create a new class if we don't have access to any class */ +if (count(Skoli::listClasses()) == 0 && Auth::getAuth()) { + $notification->push(_("Please create a new Class first."), 'horde.message'); + header('Location: ' . Horde::applicationUrl('classes/create.php', true)); + exit; +} + +switch ($actionID) { +case 'search_classes': + /* Get the search parameters. */ + $search_pattern = Util::getFormData('search_pattern'); + + /* Get the full, sorted student list for all classes. */ + $list = Skoli::listStudents(null, + $prefs->getValue('sortby_student'), + $prefs->getValue('sortdir_student'), + $prefs->getValue('sortby_class'), + $prefs->getValue('sortdir_class')); + + if (!empty($search_pattern)) { + $pattern = '/' . preg_quote($search_pattern, '/') . '/i'; + $search_results = array(); + foreach ($list as $class) { + $search_results_students = array(); + if (($search_name && preg_match($pattern, $task->name)) || + ($search_desc && preg_match($pattern, $task->desc)) || + ($search_category && preg_match($pattern, $task->category))) { + $search_results->add($task); + } + } + + /* Reassign $list to the search result. */ + $list = $search_results; + $title = sprintf(_("Search: Results for \"%s\""), $search_pattern); + } + break; + +default: + /* Get the full, sorted list for all classes. */ + $list = Skoli::listStudents(null, + $prefs->getValue('sortby_student'), + $prefs->getValue('sortdir_student'), + $prefs->getValue('sortby_class'), + $prefs->getValue('sortdir_class')); + break; +} + +Horde::addScriptFile('popup.js', 'horde', true); +Horde::addScriptFile('tooltip.js', 'horde', true); +Horde::addScriptFile('prototype.js', 'horde', true); +Horde::addScriptFile('effects.js', 'horde', true); +Horde::addScriptFile('QuickFinder.js', 'horde', true); + +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +echo '
'; +require SKOLI_TEMPLATES . '/list/header.inc'; + +if (count($list) > 0) { + $sortby_class = $prefs->getValue('sortby_class'); + $sortdir_class = $prefs->getValue('sortdir_class'); + $sortby_student = $prefs->getValue('sortby_student'); + $sortdir_student = $prefs->getValue('sortdir_student'); + $dateFormat = $prefs->getValue('date_format'); + $class_columns = @unserialize($prefs->getValue('class_columns')); + $show_students = $prefs->getValue('show_students'); + $student_columns = $show_students ? @unserialize($prefs->getValue('student_columns')) : array(); + $dynamic_sort = true; + + $baseurl = 'list.php'; + if ($actionID == 'search_classes') { + $baseurl = Util::addParameter( + $baseurl, + array('actionID' => 'search_classes', + 'search_pattern' => $search_pattern)); + } + + require SKOLI_TEMPLATES . '/list/headers.inc'; + + foreach ($list as $class) { + $dynamic_sort &= !$show_students; + $style = 'linedRow'; + require SKOLI_TEMPLATES . '/list/classes.inc'; + + if ($show_students) { + $treedir = $registry->getImageDir('horde'); + $counter = 0; + foreach ($class['_students'] as $student) { + if (++$counter < count($class['_students'])) { + $treeIcon = Horde::img(empty($GLOBALS['nls']['rtl'][$GLOBALS['language']]) ? 'tree/join.png' : 'tree/rev-join.png', '+', '', $treedir); + } else { + $treeIcon = Horde::img(empty($GLOBALS['nls']['rtl'][$GLOBALS['language']]) ? 'tree/joinbottom.png' : 'tree/rev-joinbottom.png', '\\', '', $treedir); + } + require SKOLI_TEMPLATES . '/list/students.inc'; + } + } + } + + require SKOLI_TEMPLATES . '/list/footers.inc'; + + if ($dynamic_sort) { + Horde::addScriptFile('tables.js', 'horde', true); + } +} else { + require SKOLI_TEMPLATES . '/list/empty.inc'; +} + +require SKOLI_TEMPLATES . '/panel.inc'; +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/locale/de_DE/LC_MESSAGES/skoli.mo b/skoli/locale/de_DE/LC_MESSAGES/skoli.mo new file mode 100644 index 0000000000000000000000000000000000000000..a01e3e666ac5dbed3713e7b09283a1809c12ef22 GIT binary patch literal 176332 zcmZ792i(rp|M>CSZSOsjaoc!&Ul~qIiJsU@vVNxbEKM=!0TGNgoFaxG9lq=#)O1p zv#m`?_;^b~LKfVOS@8&F#vhR`34bBY6aK+Wm~m@DLMqIMIk6z-!zy?)-ik%>9;|@V z(0Sj%g7_sC$KS9F=G~T%a1%DhlGqEKXA%y_+1LtGd>H!OiWw+BfW|!rje8mz=fYV3 zDyF8qKDr4@Q{IZM>nu9XZUT=kIu_JoDN30(d9UdKn*|`2VdQP6j^!ReT zz9L?K8`Dz1GuH1#+Z{&ZJc$+YBId_}A15Tt#oB1TK1HveMf09-H`lNYcErh80}o+O z%=Ae@LU$a9?#~uXj|ZbC(0rUl5uuNE5jEtnD8U~24+ zUcUqFHvo-)2&Thn=sB8$8E_4j!1pi-Pon#u_-QzwMbZ1J4VvdZXdJ`P{u3}I&Op!C zY|MyD(e-H2xk~9Q&d3J%M@fg?N1( zwxhfmD`3XGVf;F1K00G2?1jcZ2+QLL%z}&2^{htMu?{oi7Bs(~q3il0)_;TM;~d)l zDmrf3&qFx}x*vtm`AbAApySm*=c|W#u_4;OJ7&dx=zI^N;%edS3sG7CXqZka9*)NA z4h8SQMwDlw*FTH(*$#*6HL)7?z0v$X9qZr3+?4mD=jaTU!4yZreO3;grvsM8(P%zi z!tA&UUH4H;g{RPcJBxYnuW06@ArHl)c?O{SITOv})6wV9 zc8k${el6CojP-A$<84OQvjg4dpV9elp!<{lt5Ba49lt1AUlxtOI=Zel=y)B_=Rps2 z{(wYwjS!lbtXntOZhnjFq9yTWu9wIB*a|&I{n0pwqH#`)&cy(RVzi{+2ec@D<< zlju495zR-!_aW}|XgNPRUa?qT8J)i|I&OP(-QCdjjEL9A#_}|DzGq_j#ptVOK3Aga zeFt6dC$at@mZW?lmeZUH*R!GX-;AEaGS~o{VnLjU#`_YQ{}pJv_tAYgjL!EBmcU

36_8%6@v(fe|(7b$sK1cqE_0`UX`kq*u`l)CfThQmhH|Y1|@0f(eehT?- zhGi&sN9UV}u5&S(zpdy#okHWkgfC;xbK&{50d4mgmd0Pv@$#P!9VG_QA zC2%|1?bU8O)5gU>0nP=BF2C!~0`-Jo-GC6U)oc z@i$^N+=i*}KrA0d$NMpsFQIXz`!$@;+~{~E&~|muI9j3k>xRZV9<$?A^m+0envXST zzfUm{zsA;h98+WI-$FbUu@dEKm=616HXIVmk78EJQ_=Y6qj4-k=Ua}>vki0LK1_+< zqvM^y4EPJ0kE>|^l)s00vZLjq=<}j7X2e_3^>vHoJ1{-vd(r(GfzCS}&D%oEj?2(- z-bLd+fac>ntd2L(@1bgcgm~Mc{rg~19D+@7HX7FfH12QFb^I9p9dl8>fySHj&ydH$ z=(^jZ^|zzz7=Xq<7>#dKygnNpe-V2Bz7osZ&~@%c*YPdd@0VDg;jgg%BItT5qw}^# zaaTjnMME^c2he;!gr1+NXx?T<7ozK2 zhVK7L%#CZ&yzN5AIe?y{uh4v)i1laDcE3iiMblmm^*PWy6hz~#fbMrIbX`5s{0v6N zor1jQ+@>9k7ZZ`H>2bJhE*}u^$Vh8q51)`6RjzzsB-)biXs-2zkth_A7zTR|6fd0XlEHSl=Cus}CB-2(;fA zwB7VrKNpSbd34_8Xur47{o0Kc@GzQ>)c=I<(Rz3b<&kK9*I@(O#d7Y16v^LDb)vm6 ziR+J|dHWx_AMc{)XkYX%bi4v7QY1fT8lmm_V;&rfWpNIAf4q-3arIOd_}`~`Hp6=*)* zN7sEAUC$3_`-`#sCpyk`biR~n!um3!{qmvfDTVeci|%g~H2wxy5Zj>R+>4I$0NVZ$ z^f@{qUY{GUFGS;c1zqo(Xr9)fHpBUx5oOf z(dWwdXq=bOc@xuz^Op@BrzARV1@ydCMaON3_G^j${^*3}e-zqoBHHgMG;ecb{aQ5M z577Rfq4_w1=I123u5)NSzoO5%f3P9e$dDrWxjzd1-gq6I=PPWFCt`i2jNyK}3;mv$ ziq5kUo%cg@zR%G097f|lf#&Nxx~?plLb(biQSKZai6tq|L(lVOEQ5#8@e?wK=VL~6 z{Gw=mS#+Kn=zY-yJ;$BUb$3PYlU~>ehu}T9GG5P>CFCVPI!40qmPKJkKA+`zS9#=gpKYw9kR&r5HMH1x&&^=y~mlo|_@)`IvxB@CkJOPp~?E zg_SXD_OO4A(EDgKnvWT1{BzN97h@fK3!CG4Y>icOg!4EK3sathN%$6;?=R4GUPs5z zk~6HY7`iVl(edxX(l`RWk6u9Yu^wH|ek_Hj(Qz{83inGxwwEt4{ zx&1EMelI%Dx9Iu59LsrfhjtawJU2t*Z->U)2R+Y^pm}->-G`~@xO33FFO9y5?%!Kz z`*)*TWBteI_w-)0-|6Uibe`X%m(lZi9lbx(zjq;Z~+?MN9eqJ(02RK_>W>OJb@)KZ{Bc@Z$anjh|Y5tI?o{VJ{^Jea2~oZ z`_cRM7j*ob`NDZAfX>qZ?cWrQuN}JIx1;MCiavM7pzD1S&EEnv|Eth(x1#avj^%ym z?}4M}I=?~p?R50lSbsg5`lc{$W=wwGqj6P@m?LI@}J%Q%^$5_6Aw)+e1pC*5ZH#?e_f@oe#q50{L=I>!NPmiJF%|P@1bgW;E z=4~^&{ype8hvW5}H;3_)&^(pJny+>^2=74G zzcXI{0&RaZ)*naXJ{QZE&^)Co6wHO@xj0tBM(8}l(ES*V#`i=l&q2p~CDyM-^R@|n zE^bH1JA(fC@elOeW-c6_=OxhnY>uv@FPg6h(0v+#u750=kIAuq29~5e7nA2h4#w-pV);_^2Kqgex=5I}1iG$D=(<{A zY3z%RGYy^ZX>^|dp?NtQy^fxn>_tN!bD`Ih&~;Zw`!_)I(FVOAJEP+diavn0AC2Z= zJetqxXupNAehIo?E70FX@5bxr&~x)UI$uJuu%0YveM$7WS`E#2w`gB(Bdd^Ow-^*vvaZ?o!&&jN4xg;8Y6*P_p=>E1u=f52t zuP=J;N5uMv(S4eL&a*IHe<_xiqT{}S=63`7{M>_{lW)-Pfy?oF))FB<#nEx=qj7gd z&(VWu9%rKSJs-=9(R{8#<9`pGX9v1}htYHNEgH{dw0)|Q;oN0MG?=sA2IjpIeEjmxk+oFTAMIZ)+6=wk6}><1Mt^TTg63m3 z7Q;8t{o9Mi`+Y3`g|^RFHpE*5`%!L(j<*&|;SMy97ou0u{YYOf{EjPvwr_~$xih+d zebIRz#B%rq`uk)Z8s9;5AAZ5+_%}AjM&(2PC!zhHK=0cZ(0SfO&({|8em{baa{|r( zSu`(ypywr3g>XF^n!o(fa_G4A(R0xTy$?I0`#lsLe>D1g^-(mQMdhd@!pTd`!G8Ho6wo1A4#wj_yOI>S6wZ=(xqv{5Or|A?P?W(C7DjbRXA6ccAM%jL!c( zdSBc?&r!}A!F*`Df@mCN(0o;j<@#tInxN}%jpnx#I&VKTj`3(drla#bhkoy@#3bAv zJ&m6CG&MtgW%PN{5sh;$y3SQ-yTjN4Q`8FgLnkzzQD{8l(730dd7pu+Z{&l%hOmLFW_J- zS})}HDQrpkb#xzpLDzc;U1y&9VVrX4_;t|whUmCm&^UUdaomgU>%-`Hub^?ifi-bM zEMGv^dj;LM^bJBDb6^R|&9O2L#wz#%R=~aJJlC)uX1pcD)e^104c*V)==m6i$xSlg2;PeiV-kLc4KZc&@H}pc)=xp7%gfNbZ9~t+QS^Cv8B1Y~ z7NK2Tv|U%Uekj^*9(q1jqwDw_y$`=f*Lxn#*OhoZUCVI20J<+F&^*VhY8@exrZw=pbmC*TbN8`Q=E8qj@d43UH=gVl`S73YG zgT{Yzn=oHR^uDZv{{HNP=Jio@-07Hv&!KtVh_?F}ZTAD3-`~;pS7UwpwqZZ>pn0x{ zb+887?*X*mI5f`5XnvoH^)JWzH_-L0N5|QSp3AN1x!aHCA^UB?{OJ8(3>~)&+P@l_ z-`cUhDZ09Gd@iXxty6>)nIKb2R!>tiOtmo4#FWmj@lc zINH7zdc9S&Ypm~wp8FB#{?9_6-cDdV!_{*a4)<^fPE!w{q zI_@AWkCV~7uZ`Dtpm{!k=J6<+zZ2;Fc?w;}H7tu+JA`(1(SD84ythR6rDH7jMDsrY z&F=$f`-y0LPevD@`?3r@w{M~O-j9xV6#YK<4qf-@cs;RW7%u}3pgubq-)OY|5-f?! zu{Q2Q_ceW|(5^W8J>4El;|#2h>(Tu*|?nL|F9lakN=OOgDJsw^Ee6-)&XumCJes;$40d${_qWgaqZGR)yr|S~t&xej* z3eA5dbpFQi`mN}B?h)$;qWKwyjyD67pU1Jh8jWXTtlx_6?^ozNr(^j7CZA9AJmu~h z&eP3kK9kV%ToaAICAz-eXgq_^{TPjY&%G3{zl)yB?Pz?vqhFxs;~O;Z-=gdM8GW8# zMxVO{x`lI4A4^gmg%$7xbl*S0D)v=I>8T!mK@lmC^nk(fuBSU2!4aia%p7tlKlRn~&WnZ$Zy-o?hYiNP8?wc_cdC z0`&ggiq3xln_>Fi;rDAhY(aS<&clz<{M>m*SjQx6O8E?S!g6<}NdET#AHzYEci}x) zxlf9Oski`_W1+rb-5;ZQy@GWx>8|iS(-Rw0o`A0FLv;Q^{lfqLxgPyKx`F1oc>fS@ zH!Mea8WzE~(S1CO=HU`n!^#7~?}q{Ce$GVW*n~szB-X|DcZc@V(fK|=zt>J-DNMX4 zMe;tCMaOM}=5>55zmCp-4Bh8z_&in^7~=jIUFY9u|N4W%Jj1X7<@tCweu=kX-Fw4x z=t=Zkt;34=Jvx4l`%)zTbNo%QC*|kR{rw$VVbb7mUhc&t%ImQz9z>sm>4t>(tD)=c zjm|R$9p?=+Ux(4})zm{%Bn-pmI2<>i`(66}6bTRDY+Q%`px+DY9ti8(gUKJ~xixBs_sVu;0jVeKR`#hv>Y!Fdcp# zuOC6!|R(S^$lwGCKY(X#d8T23wxBzYUM)Vyt zU)#~&p+BJe{S!KV?$Kes0_b@y9&Lu6%K_-Qehxiv+t73I3%ajK4~2cJjpn}>`aGG4 z#`z)^#|`NCU!&g}SJAlgJRIh!jY*V;L}#JxR-xx*H~M_M7VGmo65^I2zJ|tK;L%WSgh`b9qR*R&=sK38^KM7)({Iqc zUq#nb%1rad$)S=lqxwQ99_@pc>PcG++D|1Sb0)7e>E^C z<@V^j{V_WZL)%ZndiXZ_JMCArUAo7^^B_06&O&JX^)UmsK;!9%-tXOG{SY)i4@Kw1 z`jzN7@1oC{L(vmx`ya6srkotsUk=S*RdoHW(0(1zIC`MJYx-kDd=71Q1kKC0XuDs~ zet)BRNI50sCokH*0J`4d=zLYs^|VCS*&Dr&2cr8kFqWM3LKDWM) z*RxCwdA}JQuL3%MOY}L~9v5OK%!cRD`{p_tf1W2oUJ6G`qWvqN=cfjGo|>ZZw@24A z06php(EDQ=CgDc(`SBID#7o#58$KEOO~DqF7oqF>4!xg#M&~IqEj+&~pwFpB=(_tw z2cr2Kg6_jZX#OXn=Vl&y&KIHUcsZ8eK-aq#9d8pl{}yz-Pto}fqy4U69ZWGjJn!pa z8Orxz1$-8r??W`7AEWs{i9W}!qUZAF8DW3wp!?AVoxdGAZ)Y^#JJIp(M!%m1qwzg} zJ}1Ya_tQr7x$$Q--^}p*s29Bpjq?c{fNNuYo>`$?ee`-ibR9F%@3T$l^X4p;$IMR! z>tboj1F;l7jow%9qR){p(EBCr?6Cg&X!~y18ONjNvd-3q3!Za4_-@lqctP2qsaSj@}P%VsSi#&VLnMXYsk=`QHvpP#%R|UliSr z=JNuU!)(un@4@=$?}(x3`I>;J!PW1jfiJq&!(DRgH zVK`rDF&*VBXnkIEeFf2VRYuQY<5+Hmp5GqmIv+ygoEXc`p!dll^gO>3>tB!cZ$&qu z^L-f0AER;XL*qOe%cs!uegPf#Z#167=Y#3d@0V=odW)j>NdxqG)EFJFI~q?vG(Q8; zeiPAk%#FT?wto%1f8Rp)c@O&hI)V1PhORryq7YXhbo`R&{N>PjD#h!q(fhn(vr6!y6-7p4E=JU=dKG(fxTC9d`;A#RceoZ9?PvFqZeB z&+{YbdQYL_{S@mjpn1N6BUA7_^uLhj883zS7NOrStI_lF0h+&E=)Ubo$32GT?K|{b z{Ee<7_u{aw8d#NbZ#3T7Xx`q!GWb3EJjlEx{64IS)hKsG*E zTdak@VO=b{H2gXDZfrw&6`G&F(e>tdHAV7&-={tr_lIcQUtkjcfaWjlvQRFC+bFlf z#+c!?aK752`~EPx&lAx6&qU*W4PEy;=>ENr?%z(d{XR7QV`v^P#_Lz&^^~uN>sit3 zMbQ4`qczbyHA3U>7VGbgjzHJV!eGZNLWYw$Rp>f*qw#)?#`P=u+(=v*&Rqetz8<=s zwrIaT=>0o9UZ0NUc@cUaZ$R_>33{LIL-TtW-N*0I`!eOK@I23so~ugexv7tS|Bu89 z_zG6R{b(F1SBL(2&~kBf{95R~G{u{-9UAX_XuEOfdS;{RUW|UGurj85 zE0pV??OLJh>wxC9ceEdR{sy7>d=UMdXxy{WxaXtuz7(&oLgU|n=6x&L?#o#J4LZ*mbl?6)_dmniVV)#3Kef>OHbI|H zt&)wIsIsS>ZtG6M{+Y%kW13KP4(IIHN2hnr*SS-&*=Uas4eI0uK zKStYKLf4<}o$&XMS+PFl+pr-{kA8w>C}(&#rj3X9rrML zj&6QG+()G`iE?N3dw3)o-!tgCKF6|nC6f4M{dJTlt*A$+=(^uSFDPqw}(Gh_QGnEx8gJS z2lmIQJHqdepU`ou?F@1CMb~v7`sa!fvHlr!9~Pm%50|6)T7jOEjp+H@hxWUG<~QT6 zu&)Kt`@b@phZfP>&~dw<`R#{~;}~p!+|0;C|eH)GcOHBUr7&`x7@p_g|!k?Ebp!?bneNGNV<9`(GKN(BoGw8l= ziq}6x$K8$oemIV<|8g|To{*Q~Xr5}J^R&jC*aaPb5SpJ+XuHSJ`RAd}`xoN%H_`bw zqUV1bnvc)Wc86p6hj{%Lbf2%Gai;z>jGG_*zA6&S_0c$*qT_WypT9lP`39qT9)XVc z2paz+wEg^8zZgqVUXI4MKl%;2zO(4Om#`V;_$>T>?T*g(I6CgLSR7wR=lu*_-xuil z`wkuFAN2W_e{XnR*2K1yM`9V=hTb>l&^Ysa9@d$J#!~@ZPdzM&&CvZHjOKqVx{gV) z{1iIhJaoLZ(f81O-GRor4}IRBh~Bg>{5hd>bT*db`X2OLU5X~{59QWap85yT_+P?` z_z8NSTtUB&>mLa9L$EUCMd)~+qSyaKpEo7G2=`MztWNnAtb#|-I5T`1+BZX=DNlZr<@+Y=Z)^1Vb31xo??BJPaLk5d(dW!e^m(u}mN&)n0rYwL zV=VuJSt;i}9-bE^&~sTk+8CYhR`mJM9&LYDEDuM|^W*6J&!FEsFQe_=i`VyHN6JUh z^Ih&lh`%cO{Az(dH~XS-kHqleX#1XM zoPE*!+#kzR(R@6Meh_M`du z3XS6w`W*QgjW6T(@qD4}Tchjlgy!weSU&)re<(W640PQ2SOyoP{XRjzFAt&X_#3_d zvYiU+uZ-sLR`mSdj*c@V){jQ-i^=GDS&!x`&FK(dZZt22FbQj-_gQ!J{Eb1MGc(Y5 zwxa9&6g_uG(Y$?&&iflW{`2@wJS0L)U*ddY&FdzxNiPdE0{CM?a(O zviumvEs4fi5sklLEVqx>`=aX|gllj(F2JH^QzUG_P3Szse+uVkA-ayY(foXYj{h|} z?|C#Ism_J#dC>e9!sPD-bl>`67o38X@hkMc&Uij}3noz>j*W2+ny)XhE&hytU)R47 z&e>hjiD+C)(0yJWU5Cc;J{sS4G#`i1JpX{Mm+4ZTAz}?gn~4rT;m^Spi#7Zh)O}D)zvy z@J_7$OZc9di|r}z!bX_)*RX%LM<-xO>R(6C&F5GK|G}DA=C?3TU#vp;S#*DPVtKrT z-rvQ45BpLV&FeTcACJfK95hc0(dX+M=)SB%zgIS(>wXVi_bznYuVeXF^qi;uBjlqn zI(}KSUmbM3=4hU7L-%1gn)kAjOg>Q z7}~x}ELTPMxi;Fr1v+kL^j!8t@00OZ0bfM-bq_k;Av8b7a5!E-^U&w7P(K1acazce z%t!k#M(2GK&BMl6-hqz$MJyl1a+J@Y-={e*h5mWa`?Lu9eO(6~rxQB=o#^-CgJ`~{ zq5Jn9x^G+1ec6u5`9q(3XVCfoM)QzxIgFncy`F^jtA^(JHne|lG~ailaSTPrc?4be z_;`I98t*fhe4fyGR-yggM)zSGn)kivIOoy4{T8oZN7tX>N?1=(G>%ee9F=2zZ8ZN4 z(DT(ElW+uf!e`Ov*O_SjzeE3L(D@dk`C5vuV>vp{x_Etatlx#MV?VmDM`HO0wEqS4 zx%V5E#LQR2x~ik+r!LyR4JPj+nuj~ld=EzZPl@%5(fwG7&bJl4KlY;eJ&w+M3C-U% zbiWd>g?X~0>&u6J-xNcyH$m^;j%fdZXdFZ0^+(Woo{Z(^(0sgx=4TB$-zH3s3;n*> zi`_8e_3(SVAJ(S)0y@t@?1*2X&#&?~!uLymY)W}5nx{|Db$^ZS^F=gof1&eVNB24H zKOxWM&~hjAJ{TQ+9i9Ix^m(6?yQ>UVLi5rC{oOMF9e;AHpMmCMKAMMD(ED;7|JQYI zK=*4i_Q69q4r``JOkU?YG|%rwcVRBuq38J=n*WrEiOKizO_-E|KX0OOwo93q{O7v+ zZ~^6cXnYk?B_^Nq>S+8e(EN8q*WU+yEdy%^?zeC%9+z7Cja@b6W&UBAv*3UOv0;ZJO$DQ8=>P2h(3bFDL;jt z_jPF8d$9t3hvp$$x-f3}Xj3dreNXiIbp0lS9mO}I2AeOtL z^AASvkE!UmucG(YHZ%`kq381ZSpNrlo-=0%`;r5_pKn6rC=u)HqWjbsJ;!aMUE=lL z=y~do#&vJ3pNO9GC((K4L>Hjry@0OkHMIZxSOY&o&+8R*AJb<{O#b_j0_b_JgPzM4 zvD`J5`(bP9hokephn}|`==wiJ=Q)Vg@oOxC*)oOks-pYX812^;%|~x^9|mCyoPj0q z0D2!@M)OiMb698DXeD$V)zS0a49!ocSbraS-o~K$eg@sQW$1o>h{pX*^oQurXndE^ zdH=!em^Mq?KeWC?v;x|`1{z0wblh9fedvtl^G-CLq3FI$M&o)OjcYZUpFOdB0*&)3 z+CER#Fm8EtziY&DU34FtqWjV<+BbS1wxWI{I{y3UKJ7=>bvTyKqIv%rJ;%ATh4@OM z?doA&ybWFFlbHPcL+5=3-H(;%df!LK`84_sR-}9pD`UaziOIh|+oAo(qIsPXorU(F z7hQ%aDZhi}_r2)$=x6Bshwxtf0ln`#Z!bE|A*_VQ(0$I7Gt}or+Z9CjtpqwxJv8nn=sa!E@w=kq^+flvFOI|!XdZt= z+g*xYL(ff$Twy=+p!-u6ZC@YFLmMoKz0mu95<1VjXuA*4xIaVB#gSM(kKUJ;&^S}( z4tdOsr74%fGT0%O$6ylWSFiwXN5?;j?$W>_*$2N7wZS zy3RuRLR>A;e&f({KLvZ^T=e<>3%bwgZwlw8Fxsvtx}TNNyf;PTZjH`&JGxJO(RhYM zA42DQ49(9IXummVewM`gH=`S{7WLcEaj&5BrpO=mH5dB5RT`~ti9T0)qT`Q>PC)m4 z20G7zSidBeSD`47qw118wA&-5~{d)-Q|4giZ30>Da zXnsCJpFiiYH2#C;r*zS9PTQmL-iPkbc=S220F8e=j=~?&-;;M1OHBSfKMB2`-$VEF zG}`YnmcZP_!}+a;j^7JCFT-N}G^|c}IhM!c==WB}5@EmUqtAg>*bQGr_vIRTo)Sxj z=hIE-_$ARe>Y@A64m}s$(S7ZQ?)wn*oIDzxiS}O@bI0sqL z-(R`V_BEre(f#g?et+GM&i_C3{JoCe$E(qGY(d){LeFWM(qTVyp#752ILe~=t%{z* z_E-(yfYP%KON zAvE7Fqw&9uuJdy=o*&WY?_cQspS^r|t~5aBZ;Z}!D|#PvLB|`5#`_eyf3KqByn{Z6 z55)T4(EBM>h0re_8b>*_Uvu>N*BxEwh**9SJ&#M#I98+ieHT4PAE4*sTl9RKLEBwI z^O;aFjGq~;FO13GJF&hcny(&cKIWq9T!GHJ0ev2CL;IgX@AnJnxk_Cr)E7m|<P!OoLolpl)iE}e+AKailXPdJbJEcqx;qfJ*Vx^ymUt6 z?1AQ^ADZU}(f%{h{dz9euSWB@8-1Rf!%~>9N@DWgb2LKB4`Ek)DVG04zuya14R*yD zlxJaSys27SYS==?pdVf^~UIP z`zfrBOVRuu#YT7@&0mEYVc%+@4nBK0F%#itbYpKhXv?)d=t(0*63dJ934aZ zUq$njqh?rtU2ICZKRVxQXdLU%c;83Y`vtmh$I(2ULeJrO^m`+9t#FRZpzUg+anz6X zw?@08^9(@mzhUV3Gotg+{!7sN?X_6mh;1lu!DldI?Xa#F(Z3(sjGo^Dbwb{Yq38To zG|qu&-X@^=nv2QnjpcRdx!#2p@CcfRGUci{VIXhH$(5^j_5r1qInsK6>%#1z3?{L?i~6& zOW7cdQw-g&Ced5bym!K?I26s(Vl;p6qtCbPXuiHhkJuO8?+?n2jjKN{~SG@h~1X=vUTU@2UNp2NLp zyx*h!FQEJU2f9C5n}l^2!6eF6(EM~l_hAS&#z)b-Z$anz9!uhHv3yh0(7rU9k2$C`Q zc0t$oF#4RFj^=SLnzzO1I#;3l{&6fHLG%6_+V5{n-tU&7J~R5f%#Ds$9z8F0uq-x1 z_xS;|-6Zr}EJVjyjrQA!9dIX(#k{Q&lmB;;x#;uaM;wL~TZedG!V;8MVtM=moj2ju za9?Ib^Hd6JVl(udjYab|8(r5nG@jkq1W%ynqG+2iPZ{+2Q471^J!qcZL(juObRT~} z&qLm}VV-*Ey4zzJ?1#=X6V1!ZXg)qe<2Z%p{XBY}uA%29<89$SD25FvS3>hOG&%;| zmnYEmJ&lgHG}f;`?~k|9=lgr;efTB%yZJY?f5moTKWd@v8>4ZwMDx)tmhV8%%aG`p z=#%LB=A-x73N+4b=)8N;erM46{zT8?4RoKfwGZtopyRYa$LSp%jy~_FqIrEa*1wCc z`!h6OC(-Bn6)b``bqMF79@?)v+U`Dd-bt8*FQMaaMW5>@(D<*S`<$+0$j?paeO3%T zcekMP4@RF?Y8;yGidd?n+<)vtRYtiq6_tE=jD;nny=>2mMr{fi@fs;Fjc{ZWvdmC2A z)0l+$y9DcDcgnrd`)fVg?gzBr&0RyghUj|xqItRpJ@+HgI3}a(ni;RpMf1A^ZTEI` z2YP-EqW#XG`T7~XuP#Tkb_@9|iC(Xawy%TE(-hEDzm2y4 zAeIlH=i_TkzJJmEyN2#ln(kq~!sxjwg^tq{oxg3Y?~LZ-fq4BeK8_x}*i!o;CreNSTx%3q>+zWM&J zjyuu$pU0ti4sXXE4}|CGTiDoobiEaZB_=e&!FUg@z=tvC@DTS*97cJ2w8n$s?;n?8 zZR$^A66PNfz8~tL@%Kc37d#Q&fS%XWXnc7_hW#Cgk5j&c&O3HgV#0X*0}tZJ(c$~# z)`!BlcVkWJ=b(Anjjr!ctbpwv4$s+fXr8~st@sb#fmgSFA?4Y4jZN9Uh_K4+i8Z1^%J;yN_$4QT%@m>YLuCOn2dx6j7w zDIN=dE=q@fPiDgOcr&`b5?B%|qvLi*_q8uN{%kbf`Dp(Bhvsu9dO!bzp3m0f!~HN5 ztv`t7sqlnwE;?Z~%8#SZk9W}hJA>|bj)~!U(;D6Pv1t8k*cFeX`(J%huwV3Pbp8*} z@7WZOhx@PuHl%zjn%C**^K1v!z)R?PtuQ&vcPBdEbTsa5vHS;mKb4yj);|>OKM##_ z4;t4cbi5K%!+qKfy*>v^;|FNFGuQ%iKM~^YjmGy9`hD>gw#TebCMGg08FQ z%<#QA1N|O)5uJAvmcVP+28+%L=k@<7P(h-(u)lEZ#15g z^TTS_vLZ)ckLWB&Q)mpJ?Q;<2;GOw3&QiRDte#Yf&M;Q ziaxI|qtC}|&xL(#hMt#B=z5+(+x-vC+XhU9`=f`@c)mu@!B1ESFQa+OyD*&Rg6R5+ zqx(`G9lu7r-UM?|ZXLZ7eLjsq_jeUG!gtZ<)fKFW6`v2~JJ9jwp!2_kMeqakx$-TR z!EB4dJay6iXpgR^6WYH!8s7jk{-J2Q5orELqtD4Fu^k>kpZ}#^2=`$dOrm^0dY{k6 z61WR7wSu(>+XT2@F^^f8_<0@5$pd(^H%hw zP=5z{J|D(A@eTAjoPKe*Pa5JC%H7a-J1hz77=|4w&qecg8e3u3m%~2Xjt^5_h+fa} zN{FjI8vg+FK3{;&w*%Ya)mUz|H2e;H4C_$80iFLBbYHW*8pdgejVM2eK5y2c&$(Ub zetm`0@C>?d!uu$UoCbKQx}&uO}w|_bKl|&-JHh{!hhn#^s^D7@F65 z=zY`;o8nls{T58ZAMhbe`$o7Q$D{Y-A~e2t(ffG^dY<-U8a#}~dpy>kL7!U}&~>JI zGhEMyJ|~J`5>`dW>x`bqKInZv9KFBBq4(irOpkNWI9@>KTaNaBD_;K~`Z0PRABdhr z$NL41_j0WN2OT%dim(s)(DjzXl-LUW{%MQ$>xZ_#7t7!XEQ&9pac_<8LDzW*yLtcxb>%9fdQwKD@foNVIK<6KY&i@E{U(ZJG-)Ex>(DU;=8sAE^|N2+MfZ;5p?Ncg9`-{XClWFMhjeVpI`y!Z-8$l|8(x)%G~ z;pG2$y-eG=v0RA_`OBp6YRDo|@;*)fCn$f-xG(UY!ut!x>P5e!e9rD-d99&5fWA+2 z?G5I5Gsf^*oNpU#+PUGp)>GGzb~R~}i@L6ikwo7&@k{3Wo9hqpxnGR^L;7?|?#c5k znvwpC`CJ-PGsd7;FEi=CjrU)Su?AnI?s;PU_qB<+Msuwh&ZX}wwAmalK1RFW`CKi= z|8Jiiw5vp)?8H=uHW%r$o4GSlF2l8#Dc{29R*d-_=E48Ix=~(2n|ERyRp{gEBkC70 zS9QkvjrUOM2GT$2|C|3O`*VFJ{l?>7-fe03@2hBRKb5*4h_f~G}&tt+62~SgZgZ8cP7}x&$YRFi={^0sD`uzLq&WFEfyN~PB8G9N&gIDoh>QB)2 zUEWEo^H0jH>HqJmsC}5LBJY1+IpXyzjB%0rWtbl`P+ysGR?zS?>v%V|>&)kusBh0$ zzH-z4S>`DgiV0^ZKghMt%)f)WR$TM-T68sieU)OI?XG3&$_mSaQ4*h|}bc$_iX(e^3IpD@lD-nD4I zAjap|zIJiF0CC+O>)XW`m(V{qWA?+*#8{GcOR*7S+)Kau_&jaPGe>zor{Ve>+)v#p z+WGo2#&()EMY-lHYpe?;{!W-{`I!4_`fZ@!)Y#YS9hlqKy#LglVjf@Pqg!di&ya*u zyuYR$|Eg8Od(1JI&nIYe68YEY688Lm^Om9C1Jo^I?%K3j%I8_cn1cGWlzp|0IY|{` z54D_wcs)am??k+?gn4}RB9=}uKFgir+@%=jC}S4HQ}lfa`H7dn9~ToEFya|i z;s0KP7|&PZIKQ8Z$9z2%>%OJWTIv^5zm2(lji6se#(9;xPQT^UFXZiO2IU3JH<9aCsVhbMUbrldXPY^3&Nj4JO?_+H z@K1kV^ z&u3^`g6mWG%pchjCh@+)XJ5r=vzNB_Q$ECdC*_^Ei)$75?CXDl|NFoF`E)qWZDkSK zZ(z)x%+WE%J~77b^`CejS@7L*jKE`k>pI0z`37i(^t`M&|UWWg~XgM$CK3rc)pW<=t zL$DC#Isb`c8uR#CNBc=!Z;2&o^91!Pm?t-56rp?{pI@P_D(|gv{%vvGFapmb`WE4O zSL&P4Z!w<>GS}Vo??zpV*#1u1b?0*muAQQduLt?;-vjmL+8O$m;`)Bd{OjZiAIJG> zP(PdcS=6=V{VsiabL}PibmZ-yCx*thEg5${ZAQlN^U`)3rdb2%y|fRFz?5_|A=#L;rejO zJt)7gWj6ZSZK3|cMjU!O`qxiUcl#w%U5b*?8^Hq-oBR6 z_Wn3dNy-aiz1RNX+7)6t7O(e?*Pf?t3h#4VAJ5xY4a&aKM*aK|{YKDd6ES7OSE%cW zY3TC}@ieF2S59JmBF^zstn=>%d=;SmHLiUZbJ!=&=d~7G^YsgLk8u4hKJTXO!<5@n zev@&1&G=6r|Bk}fN#1>FTP2R`Ursz7bMyh%(*Ij;TiOjKu8quBI?nkW?NjouP5te( zok!m`tUU|9O#2J;JI~wKJldV0-B{x2AJ_L4?aEO8{6G0R$+fo_>rv*I&lvta#sS)_ zqV0Elew4b4Tpt$K(}>UizUnf^!C3b1UzYKHoN;cWZxz~q%JtXi@9R%oiS6+_u8*SM zMA{_s9!uF5|G9^R3B>jhc8UEK#cMT*^PX5Y84J>GF600ET1j0l-b1NB{+}4H(r-Ou z%%b6KvHcQ!G>-dk{~^@974v*|oQJ>0O@7U1OkX=GXJNbxTrWi1*Nvb0E7bWa5!-*p z*nKGbcRs#$bG;Jd=flb|h94L+Kka%^E=`-1v2QlU9>V8LjGc-$6PV|H+60qS0f z?HqR{*IQ6-!nFg8-5=kGYw-FVl%JwaQ`!unFaKGTg!Npn#^--um16AUsC%Ebr)e{Z z7>4oA$+fZc9Zlm0|1<9-+WG4BpSdj0q3^%1KF-Va9~d_!pYO!>l;>mD*l0atR^|Og zjKS(gyt7j7!kRboc{`ss)9<4=CshgkDSsIIT%i90|CzTv?Jm(~I{gRmIW_vSdn@Ml zm6rYm>2scIgDHQ+`*LhIo6j3~7pBdNbb8W`yt?89T+4hF7$+C=_aa9r;+TDD?<)s& z|Gswc`F#o>(dPi~EVO+#&a<5Qow02lVsV(Wu{@fwI?yg#Z0mS8sM}4Wm9%Y2yVcD9 zET6~G=4ZzKm^MGf{5Y0{etWH7|&OB+E-xA z!ExNuT)UrlMq(+!oBupc@~dwk;W*bOGEND~%dk4v-)5|4)PKq6mGr5IeHnKs^%JQ7 z_qCt$0s0i7;&*D}^| zn$4!o8Ls`wXNLH{S7GYL*oXIJ+Rpt?4nLr78ZGK9(^G)4uP2gQCUc*mq?d>pxwS|2LN5R8(Nj zo8sEvV4Q9L8RJrnZ58qKq`oZrx{LN{Dd*(+OQ9-ZGoK&heJ^wU#h96h?K%7^#&8!F zqwZJQ%%pz@+SQ=%-?)W$pZ~|(o50zXRrUTy-(SU61oepn4yVD;iKHq+cSDl~+DcU= zsqR!#q$&x`6mM0Xs=DM(+#yNjIpcsM;_wuo@NnQSKKbLoLs0~W=Tig`aRL++|0;^8 z3?ewR^8fzUTKk-Ht8R6=#rHp-x^?!j_Hg!Id+oK>UVCrC{)9Zg3Ya(Wegk1|;{ANy z4~F~9kAV3)@^}j6{w?m?c%LTjjl_MB_ha$@68Zlx+;5v?vq4u4GC%kkd=+zUxF8}+>zx1RTt zhn_bR{}mCo&HKyn|Bgpx*YJN{xX+NEo?oD>&j98d`TZO5&!yh!`DYxTOqoB=?|G5u zzf#UG0P|P;K1$d}@wa*ZS=8q*$+t;f-$I%p@Am=skCEXcfc-7pj{&}m`!Fzi9!8qq ziu|7p{LR$q@x+}0M$g~J`y)}tr}O?U(tHN*uLkyc(O!c6ocJ!ef08_27HQ;PraouM z{}TCa^44<<_=f}cWa82@BK|+&e<^8SgZuZ0e>8BH1LlpmzZUm(ev8Dvo4P-p-;3iL zluswz|IGUnqkdlu?49^O760Aj@%`j~ggkx-_X*s8!0)rD*PDSm#{0J^_pkXK1?B|5 zO~T(#+BXsRcKq|i>0!ysp8H7uO~8B+abLsxX9>T5u1y`DcJ?00#; zf-pT-DQkw`mlLk1i~DEz{WS3Z)q}F%rA~St;jird#6J)CFCzStqD~)AUA~KY>G>~_ z*E=cWAIRq?fv-QehW{17e+ltgU;PcgF+|vcf zbDj8S;{P?`)}w5Ne>ZSXApCZI8-zcXuwNzYR^>(5w^R40lcpPKKAYh8@Jml0*xv-^ z5&Y68oFPol7n9~7X}>+{<)QH95&vlFbcOiWQ%(WQ5dW{@|Av$i30_Owi}^j5upc09 z8yG#elg9`7{SxkTr2SLk((_fd^a}gKNF(=~sMCKZ-J_}RTLP}n`v)oGS%l5;o(C@H z{ZRaRz7}{rkBu_Em;4?d?Xezlf6e>xz+a>;zr_0|f&Wq9{vp2oY4IMC?^F4`68|@n z=1PRWg}jeak54Ae@AF$E{2nVc%icxYa|qLOjqv{~+#Uj7PJZto?Q5e>3V#VQ2H<2g~Mf5rc;_&<)fo)7T*I{crG|9_M2 z)%d@IeDpjw+UBFg{UYuLdHx!|?~68mC-5I1u(uQbGr;|mC$;}SAV1+R=cnh(@xPcf z-%MUlB<$m&P95Uj9x%U8{Of>wv!wvf@Tcc>z;{UdLINJg@3E2Q7pc!*MxEY8*m~rv zbhk$Py)x>luy4Wr&xC&#bT{2F1OOqm}dpSKeBdF1sj!run`BlxZ2K1=wk zBi$qM>-pU%NAB+;kB^YA?zKI_H)SZ z#|XO{*k|z5^AP-B0?b{+eFJ6Q%kK$qr-5=wADQWK@ z-EZ^0h4&J_Dd2vedi8i;1@=pM|03~^;iu;}dH+4}ujBVW$zw!b{{qYt`OOj5B@aCh zi#9q0{KJWRiRuFE0QZjo^C`goCPBx5yBqg@e*Xf@_XGbYk@h!$y@>n0yr0DTQBj8< zQd)j5=l8w*{(^jdmEV_=zGOxOnbzm(r`(hSJ=sl0FI{Uq|5!Tn@l|AKV&=d*!-EOGDT zcLnz?z`mW|zurAX zVIL3d_fW=X1NUvjJq!0^R2K37ML;~y0_J(-u`kL{*vq4g@4)?G%Kr`C_bGzs*Ma*9 z-akN^Poaz_;C?v2=MjDpzn+8qozF@L!w);{EZ2U!|^JL);Hg-ZtUur2Puq3jy<5+%Ev; zCBW!;Pn7wigna`QOd^yMX&b@_H_D-@$JN_uZuVD)RgU@_Z_JEs<8w5BV#5B=P&BECsz6_a6X% zAHRPi%^M@`y|_>F)AKh`=Kn((dLGLAef+*1_qR}o|IGV$@c%sU`$+#3@_Hs=-%owg z^ZH18j<{C?_rVmwdmgy&=6!(o-%{VtiaIBdOVJ)bNgn?HmsvdLdzNp&UIgB#+nQ z|G7x>2fTlld~PFtoAgfu=H-m*< zKb&-Z{GV!}{_h{74Ea8lyndKE{+;R<`F;g;`3Zi@he@eBKWs?2V-PL(+dH_4ztrei9PD zyo>lhC*3meBhu>mTHrSb|9tX!Xq5X=(tUlTeRYIgr>hy-@*Tzq`8Lw zn|P<^ZIS+afPG}tMR9)&{J-(j^ZyceGwEi@_Zx8Q`E=YLsljhWo=epIXG!;&#O1&| zlkki2Jq^NM!tX~ti)@H{0L)SRo22>aDDRP!^&N!kc^JQwr2n??e=GTYJ9YT=_^u_~ z?;z|QQO04u=l#T8C;X`qW*PDCedO^{%6u|$dVY=aF7Ug^?;oQbUK-`x9pCfgz+#lqUSg`6c{% zK1BK_5%y-@ze@N?>hvnY|B`zBKJNcP`Y$8=SAcm5Vd+^4$3uv3MICP8{W4$|NcTkE zx#08t`v&=XV|$P{HoFJ#&aAB!gTduuJKr~uzuCcI5b_PXz`CQ|?f1J^<=!p^!=mpI zjbWZWYTw{dd85@Y8tdD6ajiKR4m_+m$UEI(-pJRR{bFs{?Qc)-h`7>dHP`dCt!{Iz zm>gHM8_ibUSYPi`&2f;mZfB#pIqDbdIkg^c75S-df4#`h6`RzdENrmV=ok5Vzj>wT zTYWaVqt1F{HS8Bf-Y+`qMSnu<)duafzD@zdt`)G^?~Zzde6URmv?GgMK)s^hZVm>` zZf63d8f1;88W!uvXzA%}-}KRq#pC&CKq=jgJiLuot9zA@{qpbIcOcuhu6}8B^pb~< zA2!y8xk?|5tP?jzt=4u`-|guk0EsNa}ZqaW&Fu`!E*}2jjI%r_q zMpszCC*{QzQXW&LyONE1>mXA?(7lqZ7DXpt)5o`31sowjFSov_2nSh zWxl4P6tpq%y?*yfb6uz{x;-!goL_AYw;*xbYLG#<(`b=IEpLNjI@^E#%*wg*%PaG9 z2UChC?i)N=t)!~ehFh_IU=lQdN^l@Mm~V~-LzP~0(xi;rgEU~j*;sAGhZ4%i5ivDI zp)IDdd}=B?B1*^qEof|ZWLnKI7|6lI*v^g|WhR37p0K)SM{ds(H9ML>aTw^)+r~ig zD$7B3YXZEjbR0gCpYP}+!Pe^_#BGOf6Xl~6rn9Un~WF# z;}XOZm-@w(W_L8mW|)K1wOH#;h`vZvfuP z3l(Ans=Kx}>I-RSN(2(!G9pA@8`2xsK+{sa=EgQ?kg(s1V*cCqc_vN$;A4?@imUk; zN1%#PYaiCR=E5#xg15$(F$gwrfeK7kb;*S`pzCyDlG?})WgKA+)r)puQ}`4q_3=p-5~4T9}2tH>-HP{?TQ!8 zW)}0=R(G@x!Y}4?&Fj}&g?f zqWiGY#kDm!$xK|m7>i@MIR|j`MW+ci8S5#+cj3fx)fC{pagY4rdubu!i|UMcS3v!j^)kECZNpt{0s$5f#s7 z)*&nF>&&su)_T5F^4cBRV)uHp z)oSF+&Glln5kTEBrrtB8Zf#@zB_$cNZyBmYIq3LAZf&|X@#SE?3fj={e4)*sRHd=L z-V|3s_h}4`di5FD<(0j%o6heQ$%Tt(bvF%p;e=GUu&u&^=0I~R92haGY(6Gy=^gZV?opbeg$#-LOtD(FaoouEhg0V2rix_Wt{m+@( z+z*{)W=*k`oEWiW(+90A&9JyuI-5fg4FxOTRSInIMh3BWv!T$Ie)&u#4bUCuH7qRr zq=}T~tkBP+RcChNd-62qGitS<-ZiR?CMJ4hUV*k$kbRxV)5~?rw@4jt5qQc0634 z*lD!unT$={*-V>e62dGu+FhCr_7LG@6WlW%)>_FruBNmmC`M~>e9xC4Yjovswfymad_EMXaP##)`pUrf*d1<9DS;h2%%vTtw; zIItH~zi3kd=gyrHh?79`W9*-($)v>1!IlVN6k#++BTqd2V6-~r>Jug01cRF$nzfHS zqOsy{xUrdYi_5bM3sZOK3u(IkXqD)}8Y9)nzg)(*`EXWLvzZ&{pxHRI)DXU`T9&TM)+wKpZ(>g{IyJ(s>+l?Na2Q#mAjPeaJRYXtlBjm~B zAxe}2>sA8<;(rm+iTGBPOODPx7L;S&M9)NTWqw=NWdvz zQA3MbPkm0~bZm-!u<4@{PtV=&mw8;yw~XcVLpN^xf0r z!NN;%ISSrtv|;#o_a!wRZ@_XfYcDLHox0zkHRT}IStqz)cd~Gc|Uchpqw&A_m9JTcp18|(Vl!9TD#j+gV8j1Nm z-s}&zq^>$Hb&Y2Sv)8C`A2w0f$hJeG2-o1E6iX;>6z8ZwEMaCfmnQd%ThQ$6c(>KO z!VDoczTN0wCRKO9%x+8+0@(FpQ*=rIL%zK`9H9M_Z*&FTKIZb{{RRS0cD#=e0w=M+ zln;A&cCFz53hJe%){uyX2ekjR{V^P)PSMQg)F6XKt3hjwTANbh;pktc%=|=S(3OXY z_-vmZpt>Vctv9pTbF=(TLt8b$F*j<=EWruDU(YhTqXg9N@L4qcAE-(h)vr(Sx(u(F64r^ zxhkc{iVSo`dTnGi6qu~B2sOZSae3CL4OlD{F|ffBUlT*sw27#U5kGErJs&`(NmL!T zL6A@#4$DsqHcQ3&6qIVS0h7$wpP=w5(;8=6Rk&VDO<`!E(Y^R*lxa}jckCm_OM^{c zE1-i)BrVJ>2oambP(1l9kc>Xxfdp0N5>v$Z;NB5?P?~yIJosYjp7E?(f&^ghgMu(A z?_f>01hwur3oRpxB=9lfko$$fRm%&|*R`#bYQR9$G!os2L@5D1v^UbkSLFC$NrI6^ zv>@qXJ(7C2)-JRiiwxkg78@8LrbLq57bTWOU4;l3MA`FHqF3Y>SdrF9gGeCE%_2o@ zq9^C8gomdaEX-(_S?D&7?KUJODFkNMvRT%}@};eAhdhd%bo;gg1ky+hwV4eUSs9e3 z_$?#mM}1I)Oe=bSRLS~7i6a!8&lN*vA8JtxJA1XG+SWprprdQlJDc5Vtm~hsmA`7* zDvKsYI+uQ4O5g#fVWv6BzC8qfSFrbru9N_SqJUEadw|@gtC{E2O#PzA^hxVmPnobT zT4HtZuuv8Z3`NGeInBED`F3Yw$e5TlqYTYKuf>{@FG3|i#ah`VDlr~K0!lHZV%cql z!K5*_0x&8wotqf0h4G2lmex2N#Sk_oC`POZzM3UG|8E})F9DkxYX)k5qNy$pG=%nF z-J}Ul{}~U6Tj@7SMce-4&CS~t)$Ni>vw^#6>Vab^tWgfK zDkO0rDFmIgK5so*SPBIg9nVy6);fpqDQV7~wWTZjkIow=$(`#3gw%9_Qa|tWZU_#< zAcUf=Li3;g=T8|SWQSXXP_^>8&7$RvSUGKh$^Gi{`9{ALiRC>_m5FR>4Yg|>iZMWh z5XDG^)5uS)tSkwNk(tk+1!I8=b-o&&`mAk*$8{$1NYU@FvOI;Q(r{CS?i#G#{(B1e zsm|tsibAjw-{_%OpAAQ;k|m(qUe&~pgP};J^`&?in7&{rJp=*h3{XO#FQgjXHawnQ zZPs}T%@bm#P3=YTv&$F22SuWtj5live7O1eV0*)Php*b^Cc!hLo$Y?arB;!^d?y zHL4f^5(maLkfht}s?M9;gW|J%Ew8(gf>t1;a|qaLY8mO2N(fO-L5ZUi%b4VJ z&0{?&(<66i^hs5$&H%%Drd)=FfC2L1?<+E9JR>o~GWOw2$JPc|_cAijY*(zL$*7UK z-`PM*Z9@v(2OKUW2EwZm7rG65*uCSZ3Uj zo@ubmJ|OBj)T5;tS@Q-n(YZ>vD%O;iukqKlY$wFlvrW;9%QJJ(3~5*D(TFOL%T?qb zM%#{ZsZEkRYDJHwt{~b)d#xeC$IH;vTz+{iBad}yax~B|2H~R5QacBC&rDRwR+7Va zR4*+AtJ@gQjj_H;k+Y@Yet!o|OS&GjqvsJ?yMcmoi zV?EgF!t2o;%r~3cyS}b{8OdZWyds zGPJ5b-hvh>hM7kgd}*bD=DyusAHlBf6R8=?eU|`LM#Id~*s5C%)oaNo#_zyeV~EG= zXF<@m9t0BI@!a{bEG7M@$dYZH0&K5f+D*lP z<;)smWQ9eG#->`H1cBfaxZ1qAp-_}vl(I2G*Hd7GQ?z>Z=Is{}@z5d9%$^>rc?6H) zlNmt6 zY?~PI0p{{Ut#S5-+JP|UUP zpzA@j9R4YxR|r@p@Tgr>@>&-Hr{V!rLm7TC@xpJ8OoLhh$(~VZcT~wZY|E=Q%IZBc zs37*3utptO1@3B|P>%<`??9Z0yb&h4j$lV~@sXlQRo_V`r@2}mYpkK{MdBOkan8gu zxYEMA+QV8%l2a`~smxLfJNINnxi*I=UsF&i{SNDn>3mTZoVH%8=z$x3$fQivu~yKg zR)b88GAb+J%HFasyCoOUBLc13ginvQG}W7U|LxQ=kYE}`HVzP7MWi=2^xYl`Ar|2E zUD+%|QG%Mi@{l9LQ)3trG(7NxDfq^HPAIkScGWv9>vI}{xi(7uyv1kAWaNAfO*RYe)U zqQ&GCdy!^fW10$J9B5#4@C28*rowKXm@G)*_@u_cZ5;yIwFuCRrw% zo6F{w&*b~16WSWGhI;4_daV9*Z>u-mDTarfC>$y}hk8&9)N|9~!DJjdy}mllgx$wz zVbBr$fou-eeVhNohW!ge4$X#;84y{EV(&>ddk`<-SMV3{JYpj{UN5MtRz)nv^eS?TA&i#|(d6Ej zB6Ey%@?`{NTFRT%(qN?M!@#r1&4?%^u7*Skk;bhzLNT3GtY!8g0hRvBsK7~x2u#Xl zQk<(8!@Q#ga92uHQctK^YAI*8YlGRp;*eD_5<+hG=Eof#kjd6h*4*n@HnR+26w)+~ zfwdMgtx`akWEV15bkseBqKg@tNyl7K+MLMA;*d|Cu`C0qiwK5Y!NZwFgDq7sd^g39 z(eI+dJG9=R3O+_A?14+8Ud?-gkz2Nw!k2Qv(1)6M zNt*~8tRfLRknx<}rguGh+msB=4gE0%OTlMtPeBFd%&C8fZ@5X2e3=F0`&VX8Fz_Q^ zf+2L~?G9Xhc!u3MrUJK?dUkUa)lD;->l(Ma9z!5}_6@N`1VhG}F^=d9i3+Hsrn_dJ z)_^|vObbErP!@Gf^F6pLUw|zxP${`!?Dp#2Yc#pkVAAHygaIM}P)O7X$wd_(ER#{T!lsRo7mxJ^H9b{>xzE{s6uP#f7!9_8$&-2O481LuSuhIAW}vI zNY-X*2K~$qXe^Nsu$nhd(< zZAJ_-JRM#HO-(r-=N_UU;@iy3I5y$Gws@n9V2)XjsmGRlL~(Z7Jq)IwxZ2MTJ^9Sp zmHA_(ydXhg-x{lpn-TyZ8XzGY7Gq=R%izjN!)i@@E?Z_yFCrOObE@s+8n6{k#=17< zR70=OpjaCr^>629Fi3~7*wlpQkeGTE4u;iS3e$e6y6j4`k~xy}*b30WyqMmc&Tlcp zA~*j<{Ku3{;fzFzfXexo6@-v#L@Rn2W$)u!l3H!kTL_J|s+ z!S7ofytj?E+8EH_8$-M2jI@%fN#NuNo}-b?)ASH5-rDnH|AnunsYXr5m(*l7`u*k# zM$)!ad7yQHWaDX#L@U%be)ZsWNpz==5IXYd7?M2m(9;$E%$+Irn4kGJ+bUG%yayc1 zRt_jSsv4+qop<(m-D;$j{ubjZ)z_BfvDO6M$%d8X0;zolU0}kKja6k?7}-cvL5WhE zXk(hvNa5pE0YR;atPLq@$choV9P9<$;Ei!a3tzd_cuG%Vl9qBw>Y-Yk)xpcJXn<8X z^k%`X%!AUhlbvTG>-OBiq?P%ybG(>`&UcJH8fSB)+7mW!0Y}SyXzsuDFtr9XSQztw zoeA9D+vBzxtqtGr7Jk@=NXGohI#M=&tsxa>EMw7kW=6H1-i#C5;6h;@aTg9Kg{3U| zYiz;Pwm@x1r?H4*F-wWrU2Ife{I3c@Ix66f~r`lmNURlGhsBOOpo44XPT& zyJE;Z9)gQW@IuZ^y#eD<@zp|eE%Z66J5%w4<{P^E;~G8d&~(0rIusoGPt7Fn2Kpo1&u;P$ieic=KHJw7CxtOO1-OVI9nImMGfc7Ws`LN`h4Chm55g^>B$sE_U+$dFc0)}ae|IJ>e&4m2I;y^AlZosig zdzuh$C|TKAnL$^IVm2vl?=0fxT8y^T+S03V+IWIT6*?z?+3SS0U$;xe$K|xri@Jkl zV@>o>72A5N)ruiDk|KyHk%wqU8M8h%q{dElq+z@h#uFtODuvjjhz3l0Pc73p$0{{X z1Ab4sW*LxzXWjlL4D%R&cb7uJ1=}%Z+Z}22G6cauh6xs8;X`aa<dd0MY}r4oJ=b;96jpB4*oirFS2yf?O~NATiO#$ z8`m}1l7vP8rY8#LB@RqRt>MFzYa2rbChG`TM)IYZI{8ZBwyaw1I03b?e@5N26CC1^ z*L4dPU>3LLGNe8U%h}0JVslF7Crzeoaj1o4qe1CkPUgD~Ku6O6v-LHR0F^j8%aQ>UQN5~)5s_aQlVgN=l%~cHGFmJ>KmbvN4$yS#o=c?##cCyPU7cE|0#x>)yM}^!S zW;x-D1p+vF$xy!g0)xVn5;F9Ed!We*SAkB>UYBd8b&o$_LDW(6xQ$R;W<<-IDr-|6 zRI0Q!6j$Vd8ml4S!M)m!8i`;LTN?V>v=B$MSi)V~7vL9tTaV8v*sbz0{Oz#?Vsa*~ z|9BAjFqurwDfIc9_T@Iq-JnJ(Co)78fT%LUz8hy-|6r;yCBOx`4%DEw^{W!{l?drc zYuLCKj6j#}L-Z8asfKX9JUag+}%P+0Kt7GV;5X~m13Y2g?QA(VU%aCVR z*{jyUavwtEYr-3{p2pAv#hn85*a;bY5}sLnCukP1yi>o-+ux2cn#ivRwDN@ z$E*}oMS}25Xt>N%>IXf}ZCy{n{EsMrFiO9Nf4F2wYyb0$#VSg?@EtP#=NM;B1g~ec zmFW+3#grhT;fZ+!yDX2Cs@iFj*qGeT9Du*v2t9*TueCfq{Us~)(y~ccHU>=~M_a)P z=AgK=j$uMFv%dL;NkT{)OQF_9ktkAEQ>LY^VD>@aPK)$>U=Xe=P5OU;4izlBiaNvg z(!v9>jB;0%AJ!{uAZuZax!GhYdQkx79m|cKoje52r zwWXbll+8nb2c}otMGn@=2ozit`)M2BA$UuU1wy;W^vHC4Rg9$E{-A!c^PuTN;UIi zRX%DnW87AKRgPkmkN+>(cMA)l+oEFk2X0C07`W>vT!_B`c}0<9oE&HkgLQ7}CsdD~)dw65xf?2J8A zsde^ytz$0|XBiLx2Q6;5ezHf+E!=A{!xYr3HU>M;Hs%;+<~W;UL53lP9qLvtTr9wZ zpzQG%U%x|TNAp)2W=(~R6s-qaH)v?2h7wXKr!-B0b?z5Zlv|M-b|Qc}Rl&Qo*(npz zD<*6kM9hHFIxwq}>LetDkAlqvoy?MgQbrqmjTx&$q_oNcpwx@07)BZbaqK`7&Z|>* zrBWaZD@8xO-4sKCeR9AYk;XHUt`I3~OohcRW2i*h;O)BGnbQ|%?p^kw0L5WwZTL7$ zDNRii69FQL?OtJ17R~wqmN>0ODt|q#yMBy0iOiB6V-E3B{pJVWxKhTIPn9&RX7c#+ zbEl^B{paa)TR+d8bv#f3>|Q2I5-pS)I#EGB4brc#Df6yvp{$_T6*IQ8mSJexW+~cJ zrNp_wxG*J{ff`zsDn_=V3v*R|YvrC5kZIh=)TY!jsj8+0oOYAd2Sllq=8CKnaH>3OoMI2(&7VxzT5GRJqWHdPUlU z%}df(RKT>29qA&ryVtzbtkt>}RdQ?S9MeY;#G6J{)G$SNyqpS7?)mZWS75?LJ! zHT8WJ0^M+fUbovTiv#7{^3z5+$b>$uYE8xaLj?`{MD0gaSi>Xv&N*Bh6& z>>wRB#`IV%bY1XW**!&PyTSk1BJJ%09gWT5<3nLufSsbHgO}LvJixo1W3pxFVjh%j zP&PlkQ;`dE7JRzsY_exqA+`ruj`P|mXhGwLaB3w+M7uIpPMt+6ZQsuBX`&FirwOU0 z;LON}|3^Cx7kw;0YX(XAh|{!Sw8qtMM$pOLKyM};)jtSrEVqa(*#(({7lI`&`M|G3R66pIPuwP-;mtI_ckI;5 z*V7`U99l5uaM8KHAVVd0JIu&TDAxCW;Q;K>5-R(m@Kifg&e8xkmkV;p{tx@ zx!S>WwOnfl)sfncV!Di@e=I zYA{mTs-XROJ+Q_#upwPA?C*-S+8mz-QB=6C9mi}2lD&W(Z?T81fMq)sXE3?P^gb$W3gOP)zP;C+ zOKe;6z-4SK%$&@<=V9{d0g_4@Xx2GZb;Fh?eYBCSVq&ZR#5l1Te$-xh$l7T*Yt|$< z0*b+L?!Qrk3WM}X#6p2cAwC~3E}zU68*7cO z{uc-@O-T6)$a@0552AVRXtT?b*p)F-zSvl2PX@Z_KB+cYfM2AK?4N6!akd&)oA%n4 zFoYjL30%Wr!SIjF-x>l5LBp$Qt0lZ(YG!(WPfDH?#?FG(~FI3rEbSXFty0FBnpC_ee?_* zpveNK*jB~KwuDxv$sX`+mdctu5v+TQHM#2 z0+A3GJE~AIB?WxErJMJS>dW<$uy#tw&1)hFE$U%~ks} zqYi~vD@K4Ae>_YXXqDAf3?TE_+Mw0BrWM#b>Ol*lmet(xq)>wp$P&t{rV}4k4PL$q zo3w6bzHLz)_W;B-82N0mxyDJ0QgIy2PkWoFyPzS6Q z>Xal>^g-*pYG9pz675MDQq1zgIGHW!okE8<<1=%oBu^UE_VYN+jxkxCO0O7)-8p+` z>cKH7Hf7b0Zj&UM!qPD_7DjV)Ij&7hd?JAEww>E)>%}6drY0hC-LZ>F*nFdwD$U|| z_XSQ81q?OwEIGpuq4Z) z4y&e%Sa`D6L`ou2ZgN2bL$#qTNMtszF6x9u*#-lQOFXB$&e=MW1#Z`2JIq*l`B6Db%T{ygpqF3zGu!N{?Zir znX@Z?$3k|d0lUt>rq;O67!NbW%0;6yHP1x}3`8@UKLe#7WoL?~uJEr%5oZ|9MPI7; zLEVl1i<>LN^8vv0&8pRLlrYBTmLan0P;YMTBk^KrV`_76+dtVbFVrA`R4m-7|25b1Ly-yl-wvkh$7%t+9Ean_AIE_TUP zl4EV@vt(vkps4eSRjZjrEBt8%EDb9gawqyU=7HT{j&=$q`CGPObzlk6T25ec*0G92 zsbs_j{j2s~nJ;X^E$YeH8Ab}c#|Q*yO6O_+g$_18&NSfz&UE=>UoB#g9OWERD%X4n zisdtnt4>FO9HnC^Bu!JLdctydG*Q%93&A;OxP_kAfKeSFFN?yrAZZ?Cdm!oDFhifYS5oUa%d2ia0{cP}iRGfxs{@>$7o5_xmd6Bw*oE@fDyQFX}6$$Bu&`#Qyys5Ez*U?sgJ z>&f6$=*TtfE10qRb|AS`M_S=w+*S6j1$G%e9QDeSMPJJj%d(`~M;-KG>q0#fxFmSN z@VJV;+UuwC&5Ya--EQOQh&FP<1v<*O8U*AuVeBkhUu%)H3+$Vi6Cg=%l+Xl=rF&+V zb8g;n;Z8+_?Y7ugE^V@!%k&gnw)w7t+0(Z43UTn?Es%U z-YRJA^x=?WvvUbj0P|)gn}#iR2{AmZqby7~VcALrSK4rT2)h)tT$hHp0!4IoTx3uI zs6SjcX=RqeH{`p%T*xPMyjDKNn<1(QWHJC$L81JTE54{~tfT-Qr?Jyao+YQ5P|~p3 zDzhsWTW1}IyWboTfzF`7)%MrwNX^3s6iBnBqh#b22hd@q2kPm@Hf0zBq%8DGfszPq z$2kL+J+pyA!d%DgJZN*)t}Zh_(jkqip&R>t%%y|A>{2KnfFOMppl`38UuIq#FS0eT z-59>0&i)$i&_RL|#1-F#>Vjle$&<;hLzo}Bvs!GqQp9j9MbNM#sfB3$KD;hplxS?| zQ|?w^I<;zdVcCNuH)Y`Rva`9 z-_X*41C{KM@=Kmd()5Eova@ZLQ!oOs1I^4r7avo%8Mrt@=!Y+YxtItwq!#lgfoK`z z1cK|~2r}cU)-Ya4>2}3KKgU5N{~%fv+@}CRvx_0dcdNnznO&4l8#KVdR#R(0k*Z=+ zG>>UJnteegx^4e71JvzPJP_s|#VP|K(RW)NK8BJvo$~^3sQZYFp1Wq>^bqTLoCG9w z4WCK*&=^M2velgcu_MaY-d3l5c>9|T>@JKPjd{epbIx6a$+kB$MVS8|6K^bh4E_Bh zva{vK*l~_$y*cd?EJR>oJp7%8gA8hP9w=t1V$KZkShmFVG2=^R3580^KbmNM-} zHLGAb{O~!s%CQnhGKwwkU21a$J6!PTWY?qP8itM8?=%L`9M9MMvYI8#vG*l2oo=~DGAF!r|2wuE#u z%J^@~XJ4E`)z3cgCH17Agkw)n@(ugL>PrLEB^PI(MZ4agMT zu0?l!^oj?jqgvAb-z7i@rb{=K&b#!%Q2Z5jsQhK(86xZza7}Ij2A@u)A4`wz&3L18a=V}2g z#z5b21S^s)agiR)hCqBF*exyeyB1E7+Leo-A*XaRm>hK0X$DCZ-Mh<<1fs0&SohQ&E!DYgc8w2%Y*-Muohj)*3k{O-`yS5pe45 zU4X`s)|uvVPEYlfcw<>GKVv0o92|Y-y4FSRCgj8}b|j<)7!1(BIg`G~))JStRB*@xvKt8Jnl_X~iPz&M2O zK^NhJQzLB>#B`Rl)ZNvYZW8jSYJK)A!D)cX?ZZBXycmtT80TolT*L+Ulq9`>>Ou21vjjL%^* zX&XfqZ!Av0A4;`lwO@v<)(uIf+cjBXX2kr!Nt|7h%f!R}C-nnW9QAt$#J-{6DKH}a zjO9nHke(u2?d;t0OkS(Teh_Q5IXsY^W3RWQ?4+7DzQ8OV=IxkZbDoA&07;iSyWno| z{>C#{43!HHrm1Z$>)P(Ib0X^5IYzSmtLm#D@qfH^jYP~n(i7Z;DgvY|r>h!22N{Oq zPwz;i5pDW9!_Y`xF@c`QX)CHzQ%*7g+BE0M(lH5icR3mQ;qgpu;}x`2wu*LFimBg3 zVwr37im=tIbEB_mA0xXB#=ury9yy{V#Cex>nfOz*YxnaD41g+-xs#PfsRXX^UlSP9 zyhMjEASbYGtGv9m3-J_mNATuZHVeB!@}geP_s8K>SFc{3E_|o?G$O>IE$vr1;3<(9 zl>v$=VQ+Hl06mb`5A>`V@UfyfL7SrHwJg+Lb+_4JuvpdFo>~$9u=)axwH=RPEbn&1 z(p;TOJ1I@9kr5y?B9f>sd>-R`WDr!RBuwS5&PqN9-6c%aHKeL;+~6}gfi8M{ za4GhJJ|R>6syOVT(HDumZicPx6o(SQ`RR&l#!s8&qcvSFYlhXdm8yzL&Hv|#_YCj7 zI*n*Y7y~$t)eAbFeAPg%|=ID4Qc8_%?nSjpLyAqgk;RphJzr+`+yOpnq zUDRnijP9~-&2{bop;62r%ykZQsBxAoER7-}%-kF*RDI{VXd=&XbAt|=I0uPlR35qG zaB|x6WCZ&HVSZu3tw~}7IX%MQCfrNhI{|(QWKk=#Y;}m33rKUSf}%7r z_)b+57nU2k`H=sbDC1PXWm*A(StI#S61!8P4hYP7g<;f^5pL3a^u%8z!rc&ey zC%MlZ40PUu{8-F|TdE~D=3!?++((egbS_3S8p{(B-cbdmIL54P6|4NNdQAY@4MR10 ztr2Zpek&Ha<(C>emG(jS-QIrOvG&|3l0Ae)>=AQWObe<+XpaRaf(YSz(rPG-4O%Ig zQN>2Tz&bxFXC)!$n9Sy_VBlp_7=Z81Br^#Fz6X>E92ZBUGDlaLm_45ag9n{H_0QxS zga%GeWOD?Hp}H9>!T{i4o9qVBC0Vu4v#5zlyrCpRgBFmLv1F++c9#VOvK@>f4%l`= z=|B`$aE;|)8Bea0MNO!a?Xo4eSnS9fTBLDBQJJ-oB(s4O1%=v|ooO*W6Bk8MkfzOI z-BNB$Y1vj+9l$8a6m~hzGAkX7`Sr?!nHNI#GP*BOU8xTkvKk~F#^G+}5Z6&N+XY@1 z+A3-wvwa*~%WX%DF0*Sw#M)jyZD^WyDc;e+1hD|C$D<)#xkTLUW?JG^a& zv>eCPx*CfVZXd{o9{SL&@=}*ElxQohk?Z+l(8X7g<@ntLG;6u=yR2paA(AZs#vxt0OY6<1 z>J`Q5HR850L$hs$g4sJQYe6X#B|1LSlt6xIm`CY^C(P0&#zps%fs%#FpP^(#4$j|? zFUl+&@r@@3c4$){zYYU_b$I|4a-XDJ+P zKnaK#fFa*ybR9rnwSU3y$i&BIGB>9Vr0bznZaFmcq_O~7#hyx*%9Tycz&M!1t}&Y; z&FsM=ffyPfU3i zWT&jN29~*FihU&h5{F^(G?z4PZplzzUv2sd-51D0pbXwsCsyKJ6`d`Mmu_GXX*>Yy z3=9nd(-K$dswuSWwk=c*%jDjGE<-7GBj)mDY}5FN%P1ad3ljvORYhrCK(AmGYBsPs zzy;$G=d4ZGw##n9Se>mDs||LKF1N5LT$&oo@GfVe^UBuB3lTEhncp zn_S*@4iN1)=BcMj?Odf^XBy$MSWhAOAJIl420d{V*1%>OND`{@($s;gClJV~FmuKr ze@wpAWfuWHh8?QlnRGT^3YaAul!bI=K-l0eeRIZ#MwB1glK@W(+vjbgR~eLclB59S z8}K8NpJ5IuJ=HWEu_-{)v6eG;gXT6j?}+m;k&orQA{%ro)yRW5M%F4Uz?ILEFGXD< zdSYC<1t21gQL1SxEw=;{`J?U|gYf$?VQ2#d^~enjLlr@0phjws4H`rqas`hMn^*~Dn)3!NS|x+xrv zP7Crz6HMgV&g04n4PHBz(3bzPbfzNAeJEvaCN=%&|G~C?lOfRI_kAWCIgsO5;%u2uoTQpZbsZTg`I6i>r>5S9+N+9cL`(uup8`=I(7 zq};2(LcZ}KDZQ-yZC^dK2;w9A0Ln5uIp||7;Xt-4EppwIAU1B0#J1~gaEch*yqS|u zsL?nWC&j=bzygS;6#5%yXUR%QDJ@%#<)0Q_1ghHBiWYw+HNXSNg%uN2VZe^-3t+G9 zJVn}4yZc<85Fc4D#%F4oO~F2mr=J92$|s3^~YOnk30L ze#dELXHmLhdaC?3H-&~Ki?>zRw?;(BAVnp9B8B_RxUzh~x*}tLCcQ3>Ry8xd9cLmD zeLzw4l^WSyEIxxs^)&lgYk?;D(I?i(E1H#pth2lMop^O>`!x)(T7k zD=$~H{2*=0c23dCR<^R$Xwtq5%Ttd#ib1xwV+I&X=iYfHm0 zBmuFROjR&=P)UKgiLtB0;z(7|RfJtwRqm=zqDvjx?9`GKps26D%P4Rydu_hD*@~!f zkH?OMe6h~{7^mvGRjo#tEQMXLxHLa|`i9NtD#~Qol~ml7KvD{VUjGLSZcA2bAI;9p z9rQ~&E&=-v(?cZY+uKft$>Z=H*@{bHF&S{|VvFFXq1cHO%g&@+!pxUS101G0XttCo zaRPi&8%bqb1M}}~wAc<^rSP8GvvnRQblEtbpqzhWR;w3X#@T%1F)m@+fGeZjwkB|L z62MsAlK`+yhv3^D5M{l0QT3XjHZSm<;vkfJO2u)3+79FRRSt-;RQ02c zR8>_W_(u()J*%q7`>W=zywkUpIeI0=?PM7YWv@A#1lAfq7TQc~EGkX%n%ygG3+;Z$ z8l#?r)-+OBM9>14Ne&oRLBz|Zk>Qbc4j|bUs~Qq^1iyCkctRa4sL5a)hsIC@@;wo# z*89P_Bt106*lmpsjd$1yQ*T;CN|R&7^NW} z0B_6QY6!SM@!iHzDu;5=RR&bBe@L!nZ`8@CXHc-VVRb6Y^(3Z#S8u}-V|z)lprz0S zm9KU<2(R}Iv~O9$G15k4qZ4+mf(c8NPU;xtz`g;^Rix6aqh@gR9UudAAKIpc;TL)n z_-jyRjQ)W!XS$uKTA238=-ahjpHdCcVP?H_R~JQtmrC^e!3JAK@vgD$<70DTB@JkR zv(r~Ca~KI!S2^Trzc|}Z_T1GeUYd>j{SjK}AX~LUlmnz!C$`bnP9)l~Q{D8pG?-Py zY`hX&S1V zq?UJ4*nxTK7G-V=&zE|ey)<2W-C{Nyn20I;@h(FQxFaKcB8w=5X*_Mh*+fvLHE1m( zpq#$I_)dC69Vlm$q-A?Q1tH-A6+@W@;J+z`2QIemq%rAK z$?2PJT8U$e8RD#ta1hlH1huYHBSs%o!;{8g>x8y?u|NdV*A28ufL5w=8#`3W;2>X* zY`Y2vk9kOexlC1%b8xXIqa+3#SKwR6QBjFd*_8_%Ij;pviEL18h$|FUYltE=ED=cD z?puIpJOeOJtCInWkW&+^nnV}sx1Fb#kEhAQtGZ&ye5 zQKv+8lM8LKt*5fS(vXP$YUmUW)1BB36y`Nu8=iVjpX*8qNAz?FQcQV4RlznyjkfyQ z85Nz{YsAGq&dhZXst)X6h!{py{;BQ!I6Da$&@!l#@WmMV2Xax? z3czN2Ao>MqF`g+2lVpqjRQVZ5!bM~Y9R{nUX$P^7P@AQ-90r0hvXziwW|VjONN0pn*%gtm;J z*A0HmIM!l-x6^4ps(qjthdH|u&Wb0u8%dd zL7AH{2Dv=U7?`d!`&mE56zGIC@#>U6p28HyN@GOyel^BE!|lqU6b!7?9a`o+(v%xh zm1fVnmCNCV=RQ*G%=&#!TGQA`-8BAXlT&N_MDl+RJG|@bZ&->+7eY0ZT#%QEbw+t+ z_k@|eXzYuy%J`I>jZ=a;+gAc7o=qib;HHWuct2R%!5NmJW-u}C#^6nzMXY$QB-#7KWFl+q(2YP*>uV#mRl{{~ z)!Np;*3naJDM_GkV693Y!Mv4DDC!fUHAjT)$nE~&uVV`!>aY`-WhSf%$Fk=58I&K! zo+oJb^Dsx6_$#)|?+0W#AqJBs2ZY&8?VQWaGI*lR6bfS&=qem<_oFY2rVSm-R?zh#_~_MJ`JV-j|_( ze*jIS5-%=r`FPsf(d_hEYyU@z8MFsSopVTql_CFO<#DnsUCwezLIHi>h3&#fn z!2S$7BFBb>=Inub-o z!5tCeXJUgOqF(!iK5B(cXPo-q1!B#krCqJd(sT%mDN*D;~@LPj(D z0puPIR4Jv<(07;`!xV(NT(9hNMjm7vY(kF~`WDs~MPbs4SCzfjUN^SL2qU!_%G9ku zAkPd1jL&y$Vn9c@FIqEk0^){r@hGc(v^N=*tSB8aM0AqH7R84`&}4X&qR~3u`c&QXtyg?`mQiB{UrROp8FV;H;XcwcFpghx~G#u^rbTBXN_P*;rk~XX^P*j30hy*4buRN6wLrU1 zv{Id;8sR=V%!8ZP8^%>(mZ`%Ktk7NHlrDx1@mGd8n3dvk{ya@^147qJ$tJjynpgcargpRUqk>*?tYgeC~^KEw8si$?WwB?hA zi_-~_{@UC-&CUdwr=ofnX3WA#^|^6(|I#d|$kqyx4YJ(NA-)H6N_A)kZPtQ!QO+{O zSm|!+J1?^nam1S&kkAF-z*><1A*Q8`Y^eU(iI8(OR$}*T!9L+sMt^lU8dxn$ivrYK zM=Ppotf1XgL47n;)NTW7XP*#)NKfLBI+>S!;v-_%YrdIGEN#9_ifTghSp4`hTa zXF0*Y3!@g-bFPepUzypm>7Pae7HeBw^w>yF=c^$*$j(n+m}VTW7uOV{^}$lH#OQVk zRXdkUrx;2L1p<1mv1bwyLrf1UShK=nb~2-`^Bk$dKCkna`>4i?th(VV>~?poz3htD z&csqrFkMfN9hFX*uOF-y22hFax>}-^hQq^5!zUc^6@NFBhoEUuyDHobiE4-I+2^V* zCNWC5tk>lR;AV_50UkC|=0x>k3(nEBB5cV7j&XNUlRysbtWeyeYeFO^qm31!%`WLv z#v%}1P)v@s`0ny20PT^9v(L4!tCB`X?fN)}DEph{+2aoJUAbc1!o_y?^I6ad5K>2_ z?W`B~uO*@`v9^a<`sImB+xL$)%gYQC$o$$2-Jm}c^#~83J1>LqciOTJ9bvLIn zhv{r~-fl69mm#hN#9kSXHEW>&al>?>U%frw)9ggFUYY?=+SMqRBzD$F zjjW4sip;viOP2i+#va?5U!!IJkKsYQ@`I&)T;Yeov~bKc^_hoK+epVz8NaAG%u=_^ zABWP#Vq&R}jn!6**>O9!g`M@idY+WCW2EgiF>7~7Ws;Iijg66qkR#ZGhvV+$dE@wvs}4OE2k;8N}!=g5-?r= zrD(Cfu>&lDy1rV3MY?O+lYNMrk*s-p?5Tu5mMO|rG1=Hu1Yu^T&OICmu2d*QT{mbkjY&kIXpsI}6;9S5Ec0+_iLphDD1#fsOL!M50Zsmdau3~b z#G7pf*zso<*zkOv!!jDUbccvnC>RiKctRHzx{g@xl5GI@GYCyp*u+GJ14*SdZtf-p zimkS5%D41VizpvED>AeT@^53*&NbT?v}NiH+oR-ACo?GaUszb4UEsoR!mipsjb?dx z=TF}^c&2;iW6edwL#QzxN-K{C()`Lo>c6ymT85)$hjgLYM@`-_HW_L$(wJ!IX^eQc=p14zC3$wVQD2_oH=uT z=Je@%^AqRJE*i;`%3g`3_RlUtb{JRERGb)bz*b{-NUAn5W&oI7Eci=1nXV+`VYHmq zb4f`kWh88;RzS_T4J!+D9i|_5%O%qr?>=TIj|-5cFh=)bYJpf4^a7-?>vj&B9uegq zg=qB;b*LeFTu&oSDjG?)lBED!?M_n%HXlT+=F5;89K~fWA-U-Hzh5j`@Z}1%w==ED zr`wxxqkiK|q1r*#2POsHUWG0SrX}ySP^vFKQ+kiy_Y}z zYixD9HyaAy%bGUi<6fOq#eZN5v_%VX5zf6Jv=)%6Z8(kGck9@~kmfhd>87)mT;Lp# zX+Nj4lo$(u`$>PJw2+)Qt{voHh$%lZNsfyh zlY2m<{giuV$6}h@I%`c27i&jY#chDJl(=!R*;I(GN%6kYS1WQHO?`8-PrV00W$q$C zI@8uDTb&|Dx2MJSBlA{ zR_XvXBZP5Wtd$f@J>3)A2dt`+&T2quXNfVlb02eTg1!a>$k;qgbXG5T@#s z1(`9{vy5{z#vz^DO#oBRh-RiN!R-eCW%h&hmP_?B+;>?>SQsKwEk{#z1t$&@s^eg4 zG|f)nQ@XG{qqk*_q~inXc%flncVL7Gg6VD2L1f7~!x<3UV`b&{^Lg$Gs17B!JWfFC zjwLhBW1esQM1BKWFx;vR1+n6s+0bUSWFfzbZmDCN(GmfIWsDk5vx={;vh%gKReRgU zi8$?K!LVz0jg1?$I@G8X3Z5p8@|zX4-K4cD0L$t2r)!G%8YZL zCA~5dY%Ry;lmA~jCly|*z)~OBoDmpgx%VyFCDQMNi{vGr5q?KY<&lP%kCXbD#75je_a-J5 z`w%vS;c4GPHN!jE@YB{rcrb=BWuc4B%(7{;+#rLff2lYwzmJyV5DzPTkH~ZvgpI-A z3z(qzM)6rT#aLAR<0cgsO);UY@kSEiW&Oy>JEyg#8^E#Kc^Xzq1^>Bq6;HFx9>%Nq zx%Oi_d-^Gv-s(_$*e_`Xo|!#;k^5pe1aKC+A=-%mHU<~Gmd4JGBtU0-)zhN$L?)3? ztyHl*fh)vp)3PciI_#O*r5P->ILYhM?CFKjAJxGLC~(9ULRnaz<-`}Uf- z48Cu0*MSVxpnisSDoSSP>bzqaXZ^C())o-iNY&1Qw+<i0F|;YhRvC;m$k4!g(1?I<15B9zV8 zxHC>q4;9845-?rsdt!qmfKT{qO_wrU4Lxf-Rq&2&0zw=

  • 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 literal 0 HcmV?d00001 diff --git a/skoli/locale/de_DE/help.xml b/skoli/locale/de_DE/help.xml new file mode 100644 index 000000000..6bed3999f --- /dev/null +++ b/skoli/locale/de_DE/help.xml @@ -0,0 +1,15 @@ + + + + + Skoli Übersicht + Was ist Skoli? + + Skoli ist ein einfaches Verwaltungsmodul für Lehrpersonen. Mehrere + Kontakte (Studierende) werden zu einer Klasse zusammengefasst. Zu + jedem Studierenden können anschliessend Noten, Beobachtungen, Lernziele + und Absenzen eingetragen werden. Neben einer Suchfunktion bietet Skoli + auch eine Exportmöglichkeit im CSV und TSV Format. + + + diff --git a/skoli/locale/en_US/help.xml b/skoli/locale/en_US/help.xml new file mode 100644 index 000000000..276f04346 --- /dev/null +++ b/skoli/locale/en_US/help.xml @@ -0,0 +1,17 @@ + + + + + + Skoli Overview + What is Skoli? + + Skoli is a simple administrative module for teachers. Several + contacts (students) will be summarized to a class. To each + student one can then enter marks, objectives, outcomes and + absences. Besides offering a search function Skoli also offers + an export option in the CSV and TSV formats. + + + + diff --git a/skoli/po/README b/skoli/po/README new file mode 100644 index 000000000..a985e94aa --- /dev/null +++ b/skoli/po/README @@ -0,0 +1 @@ +see horde/po/README diff --git a/skoli/po/de_DE.po b/skoli/po/de_DE.po new file mode 100644 index 000000000..4fff403e5 --- /dev/null +++ b/skoli/po/de_DE.po @@ -0,0 +1,997 @@ +# German translations for Skoli package +# German messages for Skoli. +# Copyright (C) 2009 Horde Project +# This file is distributed under the same license as the Skoli package. +# Automatically generated, 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: Skoli 0.1-cvs\n" +"Report-Msgid-Bugs-To: dev@lists.horde.org\n" +"POT-Creation-Date: 2009-04-16 11:04+0200\n" +"PO-Revision-Date: 2009-04-16 11:04+0200\n" +"Last-Translator: Automatically generated\n" +"Language-Team: i18n@lists.horde.org\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ISO-8859-1\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: config/schools.php.dist:57 +msgid "1. class" +msgstr "1. Klasse" + +#: config/schools.php.dist:66 +msgid "1. term" +msgstr "1. Halbjahr" + +#: config/schools.php.dist:58 +msgid "2. class" +msgstr "2. Klasse" + +#: config/schools.php.dist:71 +msgid "2. term" +msgstr "2. Halbjahr" + +#: config/schools.php.dist:59 +msgid "3. class" +msgstr "3. Klasse" + +#: config/schools.php.dist:60 +msgid "4. class" +msgstr "4. Klasse" + +#: config/schools.php.dist:61 +msgid "5. class" +msgstr "5. Klasse" + +#: config/schools.php.dist:62 +msgid "6. class" +msgstr "6. Klasse" + +#: lib/Skoli.php:315 lib/Forms/Entry.php:106 lib/Forms/Entry.php:156 +msgid "Absence" +msgstr "Abwesenheit" + +#: lib/Forms/Entry.php:156 +msgid "Absence in number of lessons" +msgstr "Abwesenheit in Anzahl Lektionen" + +#: data.php:134 search.php:116 templates/list/headers.inc:68 +#: config/prefs.php.dist:94 config/prefs.php.dist:107 +msgid "Absences" +msgstr "Abwesenheiten" + +#: data.php:73 +msgid "Absences without valid excuse" +msgstr "Unentschuldigte Abwesenheiten" + +#: lib/Driver.php:68 +msgid "Access for class \"%s\" is denied" +msgstr "Zugriff auf die Klasse \"%s\" wurde verweigert." + +#: lib/Forms/Entry.php:166 +msgid "Add" +msgstr "Hinzufügen" + +#: lib/Forms/Entry.php:37 +msgid "Add Entry" +msgstr "Eintrag hinzufügen" + +#: lib/Forms/CreateClass.php:97 lib/Forms/CreateClass.php:100 +#: lib/Forms/EditClass.php:85 lib/Forms/EditClass.php:88 +msgid "Address Book" +msgstr "Adressbuch" + +#: search.php:101 +msgid "All Types" +msgstr "Alle Typen" + +#: search.php:65 +msgid "All classes" +msgstr "Alle Klassen" + +#: search.php:74 +msgid "All students" +msgstr "Alle Studenten" + +#: config/schools.php.dist:90 +msgid "Appliance" +msgstr "Anwenden" + +#: config/prefs.php.dist:81 config/prefs.php.dist:117 +msgid "Ascending" +msgstr "Aufsteigend" + +#: config/prefs.php.dist:137 +msgid "Ask every time" +msgstr "Jedes Mal nachfragen" + +#: config/prefs.php.dist:139 +msgid "Automatically create a new contact list" +msgstr "Erstelle automatisch eine neue Kontaktliste." + +#: lib/Forms/DeleteClass.php:45 lib/Forms/DeleteClass.php:51 +msgid "Cancel" +msgstr "Abbrechen" + +#: lib/Forms/CreateClass.php:65 lib/Forms/EditClass.php:67 +#: config/prefs.php.dist:55 config/prefs.php.dist:71 +#: templates/list/headers.inc:84 +msgid "Category" +msgstr "Kategorie" + +#: classes/index.php:32 templates/classes/list.php:28 +msgid "Change Permissions" +msgstr "Rechte Ändern" + +#: config/prefs.php.dist:18 +msgid "Change your settings for automatically create contact lists." +msgstr "" +"Ändern Sie die Einstellungen zum automatischen Erstellen einer Kontaktliste." + +#: config/prefs.php.dist:11 +msgid "Change your sorting and display options." +msgstr "Ändern Sie die Sortierreihenfolge und andere Anzeigeeinstellungen." + +#: lib/School.php:94 lib/School.php:101 lib/School.php:130 lib/School.php:144 +#: lib/School.php:150 lib/School.php:166 lib/School.php:169 +#: lib/Forms/CreateClass.php:79 lib/Forms/CreateClass.php:100 +#: lib/Forms/Entry.php:62 lib/Forms/Entry.php:128 lib/Forms/EditClass.php:76 +#: lib/Forms/EditClass.php:88 +msgid "Choose:" +msgstr "Auswählen:" + +#: data.php:66 lib/Forms/Entry.php:62 templates/classes/list.php:16 +#: templates/search/headers.inc:34 +msgid "Class" +msgstr "Klasse" + +#: templates/classes/list.php:13 +msgid "Class List" +msgstr "Klassenliste" + +#: templates/panel.inc:33 templates/panel.inc:34 +msgid "Classes" +msgstr "Klassen" + +#: templates/list/header.inc:7 templates/search/header.inc:8 +msgid "Close Search" +msgstr "Suche beenden" + +#: templates/data/export.inc:11 +msgid "Comma separated values (CSV)" +msgstr "Kommagetrennte Werte (CSV)" + +#: lib/Forms/Entry.php:152 lib/Forms/Entry.php:161 +msgid "Comment" +msgstr "Kommentar" + +#: data.php:129 search.php:159 +msgid "Completed" +msgstr "Erledigt" + +#: data.php:82 +msgid "Completed outcomes" +msgstr "Erledigte Lernziele" + +#: lib/Forms/Entry.php:151 +msgid "Completed?" +msgstr "Erledigt?" + +#: config/schools.php.dist:112 +msgid "Concentration, attention, perseverance" +msgstr "Konzentration, Aufmerksamkeit, Ausdauer" + +#: config/schools.php.dist:96 +msgid "Construct" +msgstr "Gestalten" + +#: lib/Forms/CreateClass.php:144 +msgid "Contact List" +msgstr "Kontaktliste" + +#: config/prefs.php.dist:17 +msgid "Contact Lists" +msgstr "Kontaktlisten" + +#: lib/Forms/Entry.php:179 +msgid "Couldn't add the new entry." +msgstr "Ein neuer Eintrag konnte nicht erstellt werden." + +#: lib/Forms/CreateClass.php:204 lib/Forms/EditClass.php:166 +msgid "Couldn't add the selected students to the class." +msgstr "" +"Die ausgewählten Studenten konnten nicht zur Klasse hinzugefügt werden." + +#: lib/Skoli.php:275 lib/Forms/CreateClass.php:228 +msgid "Couldn't create the contact list \"%s\"." +msgstr "Die Kontaktliste \"%s\" konnte nicht erstellt werden." + +#: entry.php:64 +msgid "Couldn't update this entry: %s" +msgstr "Der Eintrag konnte nicht aktualisiert werden. %s" + +#: lib/Forms/CreateClass.php:161 +msgid "Create" +msgstr "Erstellen" + +#: lib/Forms/CreateClass.php:56 +msgid "Create Class" +msgstr "Klasse erstellen" + +#: lib/Forms/CreateClass.php:146 +msgid "Create Contact List?" +msgstr "Kontaktliste erstellen?" + +#: templates/classes/list.php:8 +msgid "Create a new Class" +msgstr "Neue Klasse erstellen" + +#: lib/School.php:85 +msgid "Custom format:" +msgstr "Eigenes Format:" + +#: config/schools.php.dist:48 +msgid "Custom school" +msgstr "Eigene Schule" + +#: lib/Forms/Entry.php:86 templates/search/headers.inc:40 +msgid "Date" +msgstr "Datum" + +#: config/prefs.php.dist:25 +msgid "Define a format for marks" +msgstr "Formatdefinition für Noten" + +#: entry.php:94 classes/index.php:33 lib/Forms/DeleteClass.php:45 +#: templates/classes/list.php:30 templates/entry/delete.inc:8 +msgid "Delete" +msgstr "Löschen" + +#: lib/Forms/DeleteClass.php:40 +msgid "Delete %s" +msgstr "%s löschen" + +#: config/prefs.php.dist:82 config/prefs.php.dist:118 +msgid "Descending" +msgstr "Absteigend" + +#: lib/Forms/CreateClass.php:63 lib/Forms/EditClass.php:65 +msgid "Description" +msgstr "Beschreibung" + +#: config/prefs.php.dist:10 +msgid "Display Options" +msgstr "Anzeige-Einstellungen" + +#: config/prefs.php.dist:138 +msgid "Don't create contact lists" +msgstr "Kein Erstellen von Kontaktlisten" + +#: entry.php:91 classes/index.php:31 templates/classes/list.php:26 +msgid "Edit" +msgstr "Bearbeiten" + +#: templates/list/classes.inc:13 +msgid "Edit \"%s\"" +msgstr "\"%s\" bearbeiten" + +#: lib/Forms/EditClass.php:56 +msgid "Edit %s" +msgstr "%s bearbeiten" + +#: templates/list/headers.inc:44 templates/search/headers.inc:31 +msgid "Edit Class" +msgstr "Klasse bearbeiten" + +#: entry.php:97 templates/search/entries.inc:5 +msgid "Edit Entry" +msgstr "Eintrag bearbeiten" + +#: templates/list/headers.inc:89 +msgid "Edit categories and colors" +msgstr "Kategorien und Farben bearbeiten" + +#: config/schools.php.dist:103 +msgid "English" +msgstr "Englisch" + +#: config/prefs.php.dist:149 +msgid "" +"Enter a default name for new contact lists.
    NOTE: You can use %c, %g or " +"%s as substitution for the class, grade respectively semester name." +msgstr "" +"Eingabe eines Standardnamens für neue Kontaktlisten.
    HINWEIS: Sie " +"können %c, %g oder %s als Ersatz für die Klasse (class), Stufe (grade) und " +"Semester (semester) verwenden." + +#: config/prefs.php.dist:167 +msgid "" +"Enter some custom marks and separate them by comma (best mark first).
    NOTE: You also need to choose \"Custom settings\" above." +msgstr "" +"Eingabe eines eigenen Notenformats. Jede Note wird mit einem Komma getrennt " +"(beste Note zuerst).
    HINWEIS: Sie müssen auch den Punkt \"Eigene " +"Einstellungen\" weiter oben auswählen." + +#: templates/search/headers.inc:46 +msgid "Entry" +msgstr "Eintrag" + +#: entry.php:103 +msgid "Entry for \"%s\"" +msgstr "Eintrag für \"%s\"" + +#: entry.php:28 +msgid "Entry not found." +msgstr "Eintrag nicht gefunden." + +#: lib/School.php:39 +msgid "Error loading the school \"%s\" from template." +msgstr "Fehler beim Laden der Schule \"%s\" aus der Vorlage." + +#: data.php:137 search.php:164 +msgid "Excused" +msgstr "Entschuldigt" + +#: data.php:72 +msgid "Excused absences" +msgstr "Entschuldigte Abwesenheiten" + +#: lib/Forms/Entry.php:157 +msgid "Excused?" +msgstr "Entschuldigt?" + +#: config/schools.php.dist:113 +msgid "Exercise processing" +msgstr "Aufgabenbearbeitung" + +#: templates/data/export.inc:32 +msgid "Export" +msgstr "Exportieren" + +#: data.php:177 templates/data/export.inc:5 +msgid "Export Classes" +msgstr "Klassen exportieren" + +#: data.php:67 +msgid "Firstname" +msgstr "Vorname" + +#: lib/School.php:83 +msgid "Format in numbers" +msgstr "Format in Zahlen" + +#: lib/School.php:84 +msgid "Format in percent" +msgstr "Format in Prozent" + +#: config/schools.php.dist:97 +msgid "French" +msgstr "Französisch" + +#: config/prefs.php.dist:9 config/prefs.php.dist:16 config/prefs.php.dist:23 +msgid "General Options" +msgstr "Allgemeine Einstellungen" + +#: lib/Forms/CreateClass.php:60 lib/Forms/EditClass.php:62 +msgid "General Settings" +msgstr "Allgemeine Angaben" + +#: config/schools.php.dist:82 +msgid "German" +msgstr "Deutsch" + +#: templates/list/headers.inc:72 config/prefs.php.dist:52 +#: config/prefs.php.dist:68 +msgid "Grade" +msgstr "Stufe" + +#: config/schools.php.dist:98 config/schools.php.dist:104 +msgid "Hearing" +msgstr "Hörverstehen" + +#: config/schools.php.dist:83 +msgid "Hearing and Talking" +msgstr "Hören und Sprechen" + +#: config/prefs.php.dist:128 +msgid "" +"How many characters of the entry details in search view should we allow to " +"see?" +msgstr "" +"Wieviele Zeichen sollen bei den Eintragdetails in der Suchansicht gezeigt " +"werden?" + +#: config/prefs.php.dist:158 +msgid "How many decimal digits should we round marks to?" +msgstr "Auf wieviele Stellen sollen die Noten gerundet werden?" + +#: config/schools.php.dist:88 +msgid "Imagination" +msgstr "Vorstellungsvermögen" + +#: lib/School.php:123 lib/School.php:127 lib/School.php:138 lib/School.php:139 +#: lib/School.php:145 +msgid "Interdisciplinary" +msgstr "Fächerübergreifend" + +#: templates/list/headers.inc:60 config/prefs.php.dist:92 +#: config/prefs.php.dist:105 +msgid "Last Entry" +msgstr "Letzter Eintrag" + +#: data.php:68 +msgid "Lastname" +msgstr "Nachname" + +#: lib/Skoli.php:497 config/prefs.php.dist:35 +msgid "List Classes" +msgstr "Klassen anzeigen" + +#: lib/School.php:104 +msgid "List with custom marks separated by comma (best mark first)" +msgstr "Kommagetrennte Liste mit eigenen Noten (beste Note zuerst)" + +#: templates/list/headers.inc:80 config/prefs.php.dist:54 +#: config/prefs.php.dist:70 +msgid "Location" +msgstr "Ort" + +#: classes/index.php:37 templates/classes/list.php:2 +msgid "Manage Classes" +msgstr "Klassen-Verwaltung" + +#: lib/Skoli.php:312 lib/Forms/Entry.php:97 lib/Forms/Entry.php:119 +#: lib/Forms/Entry.php:123 lib/Forms/Entry.php:128 +msgid "Mark" +msgstr "Note" + +#: templates/list/headers.inc:64 config/prefs.php.dist:93 +#: config/prefs.php.dist:106 +msgid "Mark average" +msgstr "Notendurchschnitt" + +#: lib/Forms/Entry.php:119 +msgid "Mark in numbers" +msgstr "Note in Zahlen" + +#: lib/Forms/Entry.php:123 +msgid "Mark in percent" +msgstr "Note in Prozent" + +#: data.php:106 search.php:104 config/prefs.php.dist:24 +msgid "Marks" +msgstr "Noten" + +#: config/schools.php.dist:87 +msgid "Mathematics" +msgstr "Mathematik" + +#: lib/Block/tree_menu.php:3 +msgid "Menu List" +msgstr "Menüliste" + +#: config/schools.php.dist:111 +msgid "Motivation to learn and dedication" +msgstr "Lernmotivation und Einsatz" + +#: config/schools.php.dist:94 +msgid "Music" +msgstr "Musik" + +#: list.php:16 +msgid "My Classes" +msgstr "Meine Klassen" + +#: templates/panel.inc:54 +msgid "My Classes:" +msgstr "Meine Klassen:" + +#: lib/Forms/CreateClass.php:62 lib/Forms/CreateClass.php:151 +#: lib/Forms/EditClass.php:64 templates/list/headers.inc:56 +#: config/prefs.php.dist:67 config/prefs.php.dist:104 +msgid "Name" +msgstr "Name" + +#: config/schools.php.dist:93 +msgid "Nature-Human-Environment" +msgstr "Natur-Mensch-Mitwelt" + +#: lib/Block/tree_menu.php:29 templates/list/students.inc:5 +#: templates/list/classes.inc:5 templates/list/headers.inc:41 +#: config/prefs.php.dist:36 +msgid "New Entry" +msgstr "Neuer Eintrag" + +#: data.php:25 +msgid "No classes are currently available. Export is disabled." +msgstr "" +"Zur Zeit sind keine Klassen verfügbar. Export steht nicht zur Verfügung." + +#: search.php:19 +msgid "No classes are currently available. Searching is disabled." +msgstr "" +"Zur Zeit sind keine Klassen verfügbar. Die Suche steht nicht zur Verfügung." + +#: templates/list/footers.inc:4 +msgid "No classes match" +msgstr "Keine Treffer" + +#: templates/search/footers.inc:4 +msgid "No entries match" +msgstr "Keine Treffer" + +#: data.php:137 search.php:164 +msgid "Not excused" +msgstr "Unentschuldigt" + +#: lib/Skoli.php:313 lib/Forms/Entry.php:100 lib/Forms/Entry.php:146 +msgid "Objective" +msgstr "Beobachtung" + +#: data.php:116 search.php:108 +msgid "Objectives" +msgstr "Beobachtungen" + +#: data.php:129 search.php:159 +msgid "Open" +msgstr "Offen" + +#: data.php:83 +msgid "Open outcomes" +msgstr "Offene Lernziele" + +#: lib/Skoli.php:314 lib/Forms/Entry.php:103 lib/Forms/Entry.php:150 +msgid "Outcome" +msgstr "Lernziel" + +#: data.php:126 search.php:112 +msgid "Outcomes" +msgstr "Lernziele" + +#: templates/entry/delete.inc:7 +msgid "Permanently delete this entry?" +msgstr "Diesen Eintrag unwiederbringlich löschen?" + +#: lib/Forms/DeleteClass.php:56 +msgid "Permission denied" +msgstr "Zugriff verweigert" + +#: list.php:59 add.php:19 +msgid "Please create a new Class first." +msgstr "Bitte erstellen Sie zuerst eine neue Klasse." + +#: config/schools.php.dist:91 +msgid "Problem solving behavior" +msgstr "Problemlöseverhalten" + +#: lib/Forms/CreateClass.php:58 lib/Forms/EditClass.php:60 +msgid "Properties" +msgstr "Eigenschaften" + +#: config/schools.php.dist:84 config/schools.php.dist:100 +#: config/schools.php.dist:106 +msgid "Reading" +msgstr "Lesen" + +#: lib/Forms/DeleteClass.php:43 +msgid "" +"Really delete the class \"%s\"? This cannot be undone and all data on this " +"class will be permanently removed." +msgstr "" +"Die Klasse \"%s\" wirklich löschen? Dieser Vorgang kann nicht rückgängig " +"gemacht werden, und alle Daten in dieser Klasse werden endgültig gelöscht." + +#: templates/search/criteria.inc:39 +msgid "Reset to Defaults" +msgstr "Zurücksetzen" + +#: config/schools.php.dist:55 +msgid "Sample school" +msgstr "Beispiel Schule" + +#: lib/Forms/Entry.php:166 lib/Forms/EditClass.php:131 templates/panel.inc:72 +msgid "Save" +msgstr "Speichern" + +#: lib/Forms/EditClass.php:76 +msgid "School" +msgstr "Schule" + +#: lib/Forms/CreateClass.php:72 lib/Forms/EditClass.php:69 +msgid "School Specific Settings" +msgstr "Schulabhängige Einstellungen" + +#: config/schools.php.dist:77 +msgid "Schoolhouse 1" +msgstr "Schulhaus 1" + +#: config/schools.php.dist:78 +msgid "Schoolhouse 2" +msgstr "Schulhaus 2" + +#: lib/Forms/CreateClass.php:79 +msgid "Schools" +msgstr "Schulen" + +#: search.php:119 lib/Block/tree_menu.php:48 templates/list/header.inc:3 +#: templates/search/header.inc:4 templates/search/criteria.inc:38 +#: config/prefs.php.dist:37 +msgid "Search" +msgstr "Suche" + +#: templates/search/criteria.inc:9 +msgid "Search Criterias" +msgstr "Suchkriterien" + +#: templates/search/header.inc:3 +msgid "Search Result" +msgstr "Suchergebnisse" + +#: templates/panel.inc:39 +msgid "Search for Classes:" +msgstr "Nach Klassen suchen:" + +#: templates/search/criteria.inc:13 +msgid "Search in" +msgstr "Suchen in" + +#: list.php:90 +msgid "Search: Results for \"%s\"" +msgstr "Suche: Ergebnisse für \"%s\"" + +#: templates/data/export.inc:26 +msgid "Select a student or the whole class to export:" +msgstr "Wählen Sie einen Studenten oder die ganze Klasse zum Exportieren:" + +#: templates/data/export.inc:17 +msgid "Select the class to export from:" +msgstr "Wählen Sie die Klasse die exportiert werden soll" + +#: config/prefs.php.dist:56 +msgid "Select the columns that should be shown in the class list view:" +msgstr "" +"Wählen Sie die Spalten, die in der Listenansicht der Klassen angezeigt " +"werden sollen:" + +#: config/prefs.php.dist:95 +msgid "Select the columns that should be shown in the student list view:" +msgstr "" +"Wählen Sie die Spalten, die in der Listenansicht der Studierenden angezeigt " +"werden sollen:" + +#: templates/data/export.inc:9 +msgid "Select the export format:" +msgstr "Wählen Sie das Exportformat:" + +#: config/prefs.php.dist:38 +msgid "Select the view to display after login:" +msgstr "Wählen Sie die Ansicht aus, die beim Start angezeigt werden soll:" + +#: templates/list/headers.inc:76 config/prefs.php.dist:53 +#: config/prefs.php.dist:69 +msgid "Semester" +msgstr "Semester" + +#: templates/list/headers.inc:52 config/prefs.php.dist:51 +#: config/prefs.php.dist:66 +msgid "Semester End" +msgstr "Semesterende" + +#: templates/list/headers.inc:48 config/prefs.php.dist:50 +#: config/prefs.php.dist:65 +msgid "Semester Start" +msgstr "Semesterstart" + +#: templates/panel.inc:63 +msgid "Shared Classes:" +msgstr "Gemeinsame Klassen:" + +#: templates/list/students.inc:13 +msgid "Show \"%s\"" +msgstr "\"%s\" anzeigen" + +#: config/prefs.php.dist:181 +msgid "Show class list options panel?" +msgstr "Kasten mit Klassenlisteninstellungen anzeigen?" + +#: config/prefs.php.dist:190 +msgid "Show students in the class list?" +msgstr "Studierende in der Klassenliste anzeigen?" + +#: templates/panel.inc:44 +msgid "Show students?" +msgstr "Studierende anzeigen?" + +#: config/schools.php.dist:89 +msgid "Skills" +msgstr "Kenntnisse, Fertigkeiten" + +#: list.php:54 +msgid "Skoli needs an applications who provides contacts (e.g. turba)." +msgstr "Skoli benötigt eine Applikation mit Kontakten (z.B. turba)." + +#: templates/list/headers.inc:68 +msgid "Sort by Absences" +msgstr "Sortieren nach Abwesenheiten" + +#: templates/list/headers.inc:84 +msgid "Sort by Category" +msgstr "Sortieren nach Kategorie" + +#: templates/search/headers.inc:34 +msgid "Sort by Class" +msgstr "Sortieren nach Klasse" + +#: templates/search/headers.inc:40 +msgid "Sort by Date" +msgstr "Sortieren nach Datum" + +#: templates/list/headers.inc:72 +msgid "Sort by Grade" +msgstr "Sortieren nach Stufe" + +#: templates/list/headers.inc:60 +msgid "Sort by Last Entry" +msgstr "Sortieren nach letztem Eintrag" + +#: templates/list/headers.inc:80 +msgid "Sort by Location" +msgstr "Sortieren nach Ort" + +#: templates/list/headers.inc:64 +msgid "Sort by Mark" +msgstr "Sortieren nach Noten" + +#: templates/list/headers.inc:56 +msgid "Sort by Name" +msgstr "Sortieren nach Name" + +#: templates/list/headers.inc:76 +msgid "Sort by Semester" +msgstr "Sortieren nach Semester" + +#: templates/list/headers.inc:52 +msgid "Sort by Semester End Date" +msgstr "Sortieren nach Semesterenddatum" + +#: templates/list/headers.inc:48 +msgid "Sort by Semester Start Date" +msgstr "Sortieren nach Semesterstartdatum" + +#: templates/search/headers.inc:37 +msgid "Sort by Student Name" +msgstr "Sortieren nach Studierenden" + +#: templates/search/headers.inc:43 +msgid "Sort by Type" +msgstr "Sortieren nach Typ" + +#: config/prefs.php.dist:72 +msgid "Sort classes by:" +msgstr "Klassen sortieren nach:" + +#: config/prefs.php.dist:83 +msgid "Sort direction for classes:" +msgstr "Sortierrichtung für Klassen:" + +#: config/prefs.php.dist:119 +msgid "Sort direction for students:" +msgstr "Sortierrichtung für Studierende:" + +#: config/prefs.php.dist:108 +msgid "Sort students by:" +msgstr "Studierende sortieren nach:" + +#: config/schools.php.dist:95 +msgid "Sport" +msgstr "Sport" + +#: lib/Forms/Entry.php:78 lib/Forms/Entry.php:80 +#: templates/search/headers.inc:37 +msgid "Student" +msgstr "Studierender" + +#: lib/Forms/CreateClass.php:95 lib/Forms/CreateClass.php:108 +#: lib/Forms/CreateClass.php:141 lib/Forms/EditClass.php:83 +#: lib/Forms/EditClass.php:96 lib/Forms/EditClass.php:129 +msgid "Students" +msgstr "Studierende" + +#: templates/data/export.inc:12 +msgid "Tab separated values (TSV)" +msgstr "Tabgetrennte Werte (TSV)" + +#: config/schools.php.dist:99 config/schools.php.dist:105 +msgid "Talking" +msgstr "Sprechen" + +#: config/schools.php.dist:114 +msgid "Teamwork and autonomy" +msgstr "Zusammenarbeit und Selbstständigkeit" + +#: lib/Driver.php:38 +msgid "The School backend is not currently available." +msgstr "Der Schuleserver ist zur Zeit nicht verfügbar." + +#: lib/Driver.php:87 +msgid "The School backend is not currently available: %s" +msgstr "Der Schuleserver ist zur Zeit nicht verfügbar: %s" + +#: classes/create.php:38 +msgid "The class \"%s\" has been created." +msgstr "Die Klasse \"%s\" wurde erstellt." + +#: classes/delete.php:43 +msgid "The class \"%s\" has been deleted." +msgstr "Die Klasse \"%s\" wurde gelöscht." + +#: classes/edit.php:50 +msgid "The class \"%s\" has been renamed to \"%s\"." +msgstr "Die Klasse \"%s\" wurde nach \"%s\" umbenannt." + +#: classes/edit.php:52 +msgid "The class \"%s\" has been saved." +msgstr "Die Klasse \"%s\" wurde gespeichert." + +#: entry.php:80 +msgid "The entry for \"%s\" has been deleted." +msgstr "Der Eintrag \"%s\" wurde gelöscht." + +#: entry.php:66 +msgid "The entry for \"%s\" has been saved." +msgstr "Der Eintrag für \"%s\" wurde gespeichert." + +#: add.php:33 +msgid "The new entry for \"%s\" has been added." +msgstr "Der neue Eintrag für \"%s\" wurde hinzugefügt." + +#: lib/Forms/CreateClass.php:152 +msgid "" +"The substitutions %c, %g or %s will be replaced automatically by the class, " +"grade respectively semester name." +msgstr "" +"Die Substitutionen %c, %g oder %s werden automatisch mit der Klasse (class), " +"Stufe (grade) bzw. dem Semester (semester) ersetzt." + +#: templates/list/empty.inc:2 +msgid "There are no classes matching the current criteria." +msgstr "Es gibt keine Klassen, die den Suchkriterien entsprechen." + +#: templates/search/empty.inc:2 +msgid "There are no entries matching the current criteria." +msgstr "Es wurden keine passenden Einträge gefunden." + +#: entry.php:78 +msgid "There was an error deleting this entry: %s" +msgstr "Beim Löschen des Eintrags ist ein Fehler aufgetreten: %s" + +#: data.php:158 +msgid "There were no entries to export." +msgstr "Es konnten keine Einträge zum Exportieren gefunden werden." + +#: index.php:20 +msgid "This file defines templates for new classes." +msgstr "" + +#: lib/Forms/Entry.php:116 +msgid "Title" +msgstr "Titel" + +#: lib/Forms/Entry.php:108 templates/search/headers.inc:43 +msgid "Type" +msgstr "Typ" + +#: lib/Forms/DeleteClass.php:63 +msgid "Unable to delete \"%s\": %s" +msgstr "\"%s\" kann nicht gelöscht werden: %s" + +#: lib/Driver.php:90 +msgid "Unable to load the definition of %s." +msgstr "Der %s-Treiber konnte nicht geladen werden." + +#: lib/Forms/EditClass.php:171 +msgid "Unable to save class \"%s\": %s" +msgstr "Die Klasse \"%s\" kann nicht gespeichert werden: %s" + +#: lib/Skoli.php:753 lib/Skoli.php:754 lib/Skoli.php:768 lib/Skoli.php:769 +#: templates/list/classes.inc:46 +msgid "Unfiled" +msgstr "Nicht zugeordnet" + +#: lib/Forms/Entry.php:37 +msgid "Update Entry" +msgstr "Eintrag aktualisieren" + +#: entry.php:89 +msgid "View" +msgstr "Anzeigen" + +#: templates/list/students.inc:27 +msgid "View Entries for \"%s\"" +msgstr "Einträge anzeigen für \"%s\"" + +#: templates/list/classes.inc:25 +msgid "View Entries in \"%s\"" +msgstr "Einträge anzeigen in \"%s\"" + +#: templates/search/entries.inc:21 +msgid "View Entry" +msgstr "Eintrag anzeigen" + +#: lib/Forms/Entry.php:134 +msgid "Weight" +msgstr "Gewichtung" + +#: config/prefs.php.dist:140 +msgid "When a new class is created should we also create a new contact list?" +msgstr "" +"Soll beim Erstellen einer neuen Klasse auch eine neue Kontaktliste angelegt " +"werden?" + +#: data.php:39 +msgid "Whole class" +msgstr "Ganze Klasse" + +#: config/schools.php.dist:85 config/schools.php.dist:101 +#: config/schools.php.dist:107 +msgid "Writing" +msgstr "Schreiben" + +#: classes/edit.php:28 +msgid "You are not allowed to change this class." +msgstr "Sie dürfen diese Klasse nicht ändern." + +#: classes/delete.php:30 +msgid "You are not allowed to delete this class." +msgstr "Sie dürfen diese Klasse nicht löschen." + +#: entry.php:36 +msgid "You are not allowed to view this entry." +msgstr "Sie dürfen diesen Eintrag nicht ansehen." + +#: classes/create.php:24 +msgid "You don't have access to any valid addressbook." +msgstr "Sie haben keinen Zugriff auf ein gültiges Adressbuch." + +#: templates/panel.inc:49 +msgid "[Manage Classes]" +msgstr "[Klassen verwalten]" + +#: lib/Skoli.php:507 +msgid "_Export" +msgstr "_Exportieren" + +#: lib/Skoli.php:499 +msgid "_New Entry" +msgstr "_Neuer Eintrag" + +#: lib/Skoli.php:503 +msgid "_Search" +msgstr "_Suche" + +#: templates/search/criteria.inc:21 +msgid "and" +msgstr "und" + +#: templates/search/criteria.inc:35 +msgid "and for Entries with:" +msgstr "und für Einträge mit:" + +#: data.php:165 templates/data/export.inc:1 +msgid "class.csv" +msgstr "klasse.csv" + +#: data.php:170 +msgid "class.tsv" +msgstr "klasse.tsv" + +#: templates/search/criteria.inc:25 +msgid "for" +msgstr "für" + +#: lib/Block/tree_menu.php:39 +msgid "in %s" +msgstr "in %s" diff --git a/skoli/po/skoli.pot b/skoli/po/skoli.pot new file mode 100644 index 000000000..0995f61cc --- /dev/null +++ b/skoli/po/skoli.pot @@ -0,0 +1,1006 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Horde Project +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: dev@lists.horde.org\n" +"POT-Creation-Date: 2009-04-16 11:04+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: config/schools.php.dist:57 +msgid "1. class" +msgstr "" + +#: config/schools.php.dist:66 +msgid "1. term" +msgstr "" + +#: config/schools.php.dist:58 +msgid "2. class" +msgstr "" + +#: config/schools.php.dist:71 +msgid "2. term" +msgstr "" + +#: config/schools.php.dist:59 +msgid "3. class" +msgstr "" + +#: config/schools.php.dist:60 +msgid "4. class" +msgstr "" + +#: config/schools.php.dist:61 +msgid "5. class" +msgstr "" + +#: config/schools.php.dist:62 +msgid "6. class" +msgstr "" + +#: lib/Skoli.php:315 lib/Forms/Entry.php:106 lib/Forms/Entry.php:156 +msgid "Absence" +msgstr "" + +#: lib/Forms/Entry.php:156 +msgid "Absence in number of lessons" +msgstr "" + +#: data.php:134 search.php:116 templates/list/headers.inc:68 +#: config/prefs.php.dist:94 config/prefs.php.dist:107 +msgid "Absences" +msgstr "" + +#: data.php:73 +msgid "Absences without valid excuse" +msgstr "" + +#: lib/Driver.php:68 +#, php-format +msgid "Access for class \"%s.\" is denied" +msgstr "" + +#: lib/Forms/Entry.php:166 +msgid "Add" +msgstr "" + +#: lib/Forms/Entry.php:37 +msgid "Add Entry" +msgstr "" + +#: lib/Forms/CreateClass.php:97 lib/Forms/CreateClass.php:100 +#: lib/Forms/EditClass.php:85 lib/Forms/EditClass.php:88 +msgid "Address Book" +msgstr "" + +#: search.php:101 +msgid "All Types" +msgstr "" + +#: search.php:65 +msgid "All classes" +msgstr "" + +#: search.php:74 +msgid "All students" +msgstr "" + +#: config/schools.php.dist:90 +msgid "Appliance" +msgstr "" + +#: config/prefs.php.dist:81 config/prefs.php.dist:117 +msgid "Ascending" +msgstr "" + +#: config/prefs.php.dist:137 +msgid "Ask every time" +msgstr "" + +#: config/prefs.php.dist:139 +msgid "Automatically create a new contact list" +msgstr "" + +#: lib/Forms/DeleteClass.php:45 lib/Forms/DeleteClass.php:51 +msgid "Cancel" +msgstr "" + +#: templates/list/headers.inc:84 +msgid "Cat_egory" +msgstr "" + +#: lib/Forms/CreateClass.php:65 lib/Forms/EditClass.php:67 +#: config/prefs.php.dist:55 config/prefs.php.dist:71 +msgid "Category" +msgstr "" + +#: classes/index.php:32 templates/classes/list.php:28 +msgid "Change Permissions" +msgstr "" + +#: config/prefs.php.dist:18 +msgid "Change your settings for automatically create contact lists." +msgstr "" + +#: config/prefs.php.dist:11 +msgid "Change your sorting and display options." +msgstr "" + +#: lib/School.php:94 lib/School.php:101 lib/School.php:130 lib/School.php:144 +#: lib/School.php:150 lib/School.php:166 lib/School.php:169 +#: lib/Forms/CreateClass.php:79 lib/Forms/CreateClass.php:100 +#: lib/Forms/Entry.php:62 lib/Forms/Entry.php:128 lib/Forms/EditClass.php:76 +#: lib/Forms/EditClass.php:88 +msgid "Choose:" +msgstr "" + +#: data.php:66 lib/Forms/Entry.php:62 templates/classes/list.php:16 +#: templates/search/headers.inc:34 +msgid "Class" +msgstr "" + +#: templates/classes/list.php:13 +msgid "Class List" +msgstr "" + +#: templates/panel.inc:33 templates/panel.inc:34 +msgid "Classes" +msgstr "" + +#: templates/list/header.inc:7 templates/search/header.inc:8 +msgid "Close Search" +msgstr "" + +#: templates/data/export.inc:11 +msgid "Comma separated values (CSV)" +msgstr "" + +#: lib/Forms/Entry.php:152 lib/Forms/Entry.php:161 +msgid "Comment" +msgstr "" + +#: data.php:129 search.php:159 +msgid "Completed" +msgstr "" + +#: data.php:82 +msgid "Completed outcomes" +msgstr "" + +#: lib/Forms/Entry.php:151 +msgid "Completed?" +msgstr "" + +#: config/schools.php.dist:112 +msgid "Concentration, attention, perseverance" +msgstr "" + +#: config/schools.php.dist:96 +msgid "Construct" +msgstr "" + +#: lib/Forms/CreateClass.php:144 +msgid "Contact List" +msgstr "" + +#: config/prefs.php.dist:17 +msgid "Contact Lists" +msgstr "" + +#: lib/Forms/Entry.php:179 +msgid "Couldn't add the new entry." +msgstr "" + +#: lib/Forms/CreateClass.php:204 lib/Forms/EditClass.php:166 +msgid "Couldn't add the selected students to the class." +msgstr "" + +#: lib/Skoli.php:275 lib/Forms/CreateClass.php:228 +#, php-format +msgid "Couldn't create the contact list \"%s\"." +msgstr "" + +#: entry.php:64 +#, php-format +msgid "Couldn't update this entry: %s" +msgstr "" + +#: lib/Forms/CreateClass.php:161 +msgid "Create" +msgstr "" + +#: lib/Forms/CreateClass.php:56 +msgid "Create Class" +msgstr "" + +#: lib/Forms/CreateClass.php:146 +msgid "Create Contact List?" +msgstr "" + +#: templates/classes/list.php:8 +msgid "Create a new Class" +msgstr "" + +#: lib/School.php:85 +msgid "Custom format:" +msgstr "" + +#: config/schools.php.dist:48 +msgid "Custom school" +msgstr "" + +#: lib/Forms/Entry.php:86 templates/search/headers.inc:40 +msgid "Date" +msgstr "" + +#: config/prefs.php.dist:25 +msgid "Define a format for marks" +msgstr "" + +#: entry.php:94 classes/index.php:33 lib/Forms/DeleteClass.php:45 +#: templates/classes/list.php:30 templates/entry/delete.inc:8 +msgid "Delete" +msgstr "" + +#: lib/Forms/DeleteClass.php:40 +#, php-format +msgid "Delete %s" +msgstr "" + +#: config/prefs.php.dist:82 config/prefs.php.dist:118 +msgid "Descending" +msgstr "" + +#: lib/Forms/CreateClass.php:63 lib/Forms/EditClass.php:65 +msgid "Description" +msgstr "" + +#: config/prefs.php.dist:10 +msgid "Display Options" +msgstr "" + +#: config/prefs.php.dist:138 +msgid "Don't create contact lists" +msgstr "" + +#: entry.php:91 classes/index.php:31 templates/classes/list.php:26 +msgid "Edit" +msgstr "" + +#: templates/list/classes.inc:13 +#, php-format +msgid "Edit \"%s\"" +msgstr "" + +#: lib/Forms/EditClass.php:56 +#, php-format +msgid "Edit %s" +msgstr "" + +#: templates/list/headers.inc:44 templates/search/headers.inc:31 +msgid "Edit Class" +msgstr "" + +#: entry.php:97 templates/search/entries.inc:5 +msgid "Edit Entry" +msgstr "" + +#: templates/list/headers.inc:89 +msgid "Edit categories and colors" +msgstr "" + +#: config/schools.php.dist:103 +msgid "English" +msgstr "" + +#: config/prefs.php.dist:149 +msgid "" +"Enter a default name for new contact lists.
    NOTE: You can use %c, %g or " +"%s as substitution for the class, grade respectively semester name." +msgstr "" + +#: config/prefs.php.dist:167 +msgid "" +"Enter some custom marks and separate them by comma (best mark first).
    NOTE: You also need to choose \"Custom settings\" above." +msgstr "" + +#: templates/search/headers.inc:46 +msgid "Entry" +msgstr "" + +#: entry.php:103 +#, php-format +msgid "Entry for \"%s\"" +msgstr "" + +#: entry.php:28 +msgid "Entry not found." +msgstr "" + +#: lib/School.php:39 +#, php-format +msgid "Error loading the school \"%s\" from template." +msgstr "" + +#: data.php:137 search.php:164 +msgid "Excused" +msgstr "" + +#: data.php:72 +msgid "Excused absences" +msgstr "" + +#: lib/Forms/Entry.php:157 +msgid "Excused?" +msgstr "" + +#: config/schools.php.dist:113 +msgid "Exercise processing" +msgstr "" + +#: templates/data/export.inc:32 +msgid "Export" +msgstr "" + +#: data.php:177 templates/data/export.inc:5 +msgid "Export Classes" +msgstr "" + +#: data.php:67 +msgid "Firstname" +msgstr "" + +#: lib/School.php:83 +msgid "Format in numbers" +msgstr "" + +#: lib/School.php:84 +msgid "Format in percent" +msgstr "" + +#: config/schools.php.dist:97 +msgid "French" +msgstr "" + +#: config/prefs.php.dist:9 config/prefs.php.dist:16 config/prefs.php.dist:23 +msgid "General Options" +msgstr "" + +#: lib/Forms/CreateClass.php:60 lib/Forms/EditClass.php:62 +msgid "General Settings" +msgstr "" + +#: config/schools.php.dist:82 +msgid "German" +msgstr "" + +#: templates/list/headers.inc:72 config/prefs.php.dist:52 +#: config/prefs.php.dist:68 +msgid "Grade" +msgstr "" + +#: config/schools.php.dist:98 config/schools.php.dist:104 +msgid "Hearing" +msgstr "" + +#: config/schools.php.dist:83 +msgid "Hearing and Talking" +msgstr "" + +#: config/prefs.php.dist:128 +msgid "" +"How many characters of the entry details in search view should we allow to " +"see?" +msgstr "" + +#: config/prefs.php.dist:158 +msgid "How many decimal digits should we round marks to?" +msgstr "" + +#: config/schools.php.dist:88 +msgid "Imagination" +msgstr "" + +#: lib/School.php:123 lib/School.php:127 lib/School.php:138 lib/School.php:139 +#: lib/School.php:145 +msgid "Interdisciplinary" +msgstr "" + +#: templates/list/headers.inc:60 config/prefs.php.dist:92 +#: config/prefs.php.dist:105 +msgid "Last Entry" +msgstr "" + +#: data.php:68 +msgid "Lastname" +msgstr "" + +#: lib/Skoli.php:497 config/prefs.php.dist:35 +msgid "List Classes" +msgstr "" + +#: lib/School.php:104 +msgid "List with custom marks separated by comma (best mark first)" +msgstr "" + +#: templates/list/headers.inc:80 config/prefs.php.dist:54 +#: config/prefs.php.dist:70 +msgid "Location" +msgstr "" + +#: classes/index.php:37 templates/classes/list.php:2 +msgid "Manage Classes" +msgstr "" + +#: lib/Skoli.php:312 lib/Forms/Entry.php:97 lib/Forms/Entry.php:119 +#: lib/Forms/Entry.php:123 lib/Forms/Entry.php:128 +msgid "Mark" +msgstr "" + +#: templates/list/headers.inc:64 +msgid "Mark Average" +msgstr "" + +#: config/prefs.php.dist:93 config/prefs.php.dist:106 +msgid "Mark average" +msgstr "" + +#: lib/Forms/Entry.php:119 +msgid "Mark in numbers" +msgstr "" + +#: lib/Forms/Entry.php:123 +msgid "Mark in percent" +msgstr "" + +#: data.php:106 search.php:104 config/prefs.php.dist:24 +msgid "Marks" +msgstr "" + +#: config/schools.php.dist:87 +msgid "Mathematics" +msgstr "" + +#: lib/Block/tree_menu.php:3 +msgid "Menu List" +msgstr "" + +#: config/schools.php.dist:111 +msgid "Motivation to learn and dedication" +msgstr "" + +#: config/schools.php.dist:94 +msgid "Music" +msgstr "" + +#: list.php:16 +msgid "My Classes" +msgstr "" + +#: templates/panel.inc:54 +msgid "My Classes:" +msgstr "" + +#: lib/Forms/CreateClass.php:62 lib/Forms/CreateClass.php:151 +#: lib/Forms/EditClass.php:64 templates/list/headers.inc:56 +#: config/prefs.php.dist:67 config/prefs.php.dist:104 +msgid "Name" +msgstr "" + +#: config/schools.php.dist:93 +msgid "Nature-Human-Environment" +msgstr "" + +#: lib/Block/tree_menu.php:29 templates/list/students.inc:5 +#: templates/list/classes.inc:5 templates/list/headers.inc:41 +#: config/prefs.php.dist:36 +msgid "New Entry" +msgstr "" + +#: data.php:25 +msgid "No classes are currently available. Export is disabled." +msgstr "" + +#: search.php:19 +msgid "No classes are currently available. Searching is disabled." +msgstr "" + +#: templates/list/footers.inc:4 +msgid "No classes match" +msgstr "" + +#: templates/search/footers.inc:4 +msgid "No entries match" +msgstr "" + +#: data.php:137 search.php:164 +msgid "Not excused" +msgstr "" + +#: lib/Skoli.php:313 lib/Forms/Entry.php:100 lib/Forms/Entry.php:146 +msgid "Objective" +msgstr "" + +#: data.php:116 search.php:108 +msgid "Objectives" +msgstr "" + +#: data.php:129 search.php:159 +msgid "Open" +msgstr "" + +#: data.php:83 +msgid "Open outcomes" +msgstr "" + +#: lib/Skoli.php:314 lib/Forms/Entry.php:103 lib/Forms/Entry.php:150 +msgid "Outcome" +msgstr "" + +#: data.php:126 search.php:112 +msgid "Outcomes" +msgstr "" + +#: templates/entry/delete.inc:7 +msgid "Permanently delete this entry?" +msgstr "" + +#: lib/Forms/DeleteClass.php:56 +msgid "Permission denied" +msgstr "" + +#: list.php:59 add.php:19 +msgid "Please create a new Class first." +msgstr "" + +#: config/schools.php.dist:91 +msgid "Problem solving behavior" +msgstr "" + +#: lib/Forms/CreateClass.php:58 lib/Forms/EditClass.php:60 +msgid "Properties" +msgstr "" + +#: config/schools.php.dist:84 config/schools.php.dist:100 +#: config/schools.php.dist:106 +msgid "Reading" +msgstr "" + +#: lib/Forms/DeleteClass.php:43 +#, php-format +msgid "" +"Really delete the class \"%s\"? This cannot be undone and all data on this " +"class will be permanently removed." +msgstr "" + +#: templates/search/criteria.inc:39 +msgid "Reset to Defaults" +msgstr "" + +#: config/schools.php.dist:55 +msgid "Sample school" +msgstr "" + +#: lib/Forms/Entry.php:166 lib/Forms/EditClass.php:131 templates/panel.inc:72 +msgid "Save" +msgstr "" + +#: lib/Forms/EditClass.php:76 +msgid "School" +msgstr "" + +#: lib/Forms/CreateClass.php:72 lib/Forms/EditClass.php:69 +msgid "School Specific Settings" +msgstr "" + +#: config/schools.php.dist:77 +msgid "Schoolhouse 1" +msgstr "" + +#: config/schools.php.dist:78 +msgid "Schoolhouse 2" +msgstr "" + +#: lib/Forms/CreateClass.php:79 +msgid "Schools" +msgstr "" + +#: search.php:119 lib/Block/tree_menu.php:48 templates/list/header.inc:3 +#: templates/search/header.inc:4 templates/search/criteria.inc:38 +#: config/prefs.php.dist:37 +msgid "Search" +msgstr "" + +#: templates/search/criteria.inc:9 +msgid "Search Criterias" +msgstr "" + +#: templates/search/header.inc:3 +msgid "Search Result" +msgstr "" + +#: templates/panel.inc:39 +msgid "Search for Classes:" +msgstr "" + +#: templates/search/criteria.inc:13 +msgid "Search in" +msgstr "" + +#: list.php:90 +#, php-format +msgid "Search: Results for \"%s\"" +msgstr "" + +#: templates/data/export.inc:26 +msgid "Select a student or the whole class to export:" +msgstr "" + +#: templates/data/export.inc:17 +msgid "Select the class to export from:" +msgstr "" + +#: config/prefs.php.dist:56 +msgid "Select the columns that should be shown in the class list view:" +msgstr "" + +#: config/prefs.php.dist:95 +msgid "Select the columns that should be shown in the student list view:" +msgstr "" + +#: templates/data/export.inc:9 +msgid "Select the export format:" +msgstr "" + +#: config/prefs.php.dist:38 +msgid "Select the view to display after login:" +msgstr "" + +#: templates/list/headers.inc:76 config/prefs.php.dist:53 +#: config/prefs.php.dist:69 +msgid "Semester" +msgstr "" + +#: templates/list/headers.inc:52 config/prefs.php.dist:51 +#: config/prefs.php.dist:66 +msgid "Semester End" +msgstr "" + +#: templates/list/headers.inc:48 config/prefs.php.dist:50 +#: config/prefs.php.dist:65 +msgid "Semester Start" +msgstr "" + +#: templates/panel.inc:63 +msgid "Shared Classes:" +msgstr "" + +#: templates/list/students.inc:13 +#, php-format +msgid "Show \"%s\"" +msgstr "" + +#: config/prefs.php.dist:181 +msgid "Show class list options panel?" +msgstr "" + +#: config/prefs.php.dist:190 +msgid "Show students in the class list?" +msgstr "" + +#: templates/panel.inc:44 +msgid "Show students?" +msgstr "" + +#: config/schools.php.dist:89 +msgid "Skills" +msgstr "" + +#: list.php:54 +msgid "Skoli needs an applications who provides contacts (e.g. turba)." +msgstr "" + +#: templates/list/headers.inc:68 +msgid "Sort by Absences" +msgstr "" + +#: templates/list/headers.inc:84 +msgid "Sort by Category" +msgstr "" + +#: templates/search/headers.inc:34 +msgid "Sort by Class" +msgstr "" + +#: templates/search/headers.inc:40 +msgid "Sort by Date" +msgstr "" + +#: templates/list/headers.inc:72 +msgid "Sort by Grade" +msgstr "" + +#: templates/list/headers.inc:60 +msgid "Sort by Last Entry" +msgstr "" + +#: templates/list/headers.inc:80 +msgid "Sort by Location" +msgstr "" + +#: templates/list/headers.inc:64 +msgid "Sort by Mark" +msgstr "" + +#: templates/list/headers.inc:56 +msgid "Sort by Name" +msgstr "" + +#: templates/list/headers.inc:76 +msgid "Sort by Semester" +msgstr "" + +#: templates/list/headers.inc:52 +msgid "Sort by Semester End Date" +msgstr "" + +#: templates/list/headers.inc:48 +msgid "Sort by Semester Start Date" +msgstr "" + +#: templates/search/headers.inc:37 +msgid "Sort by Student Name" +msgstr "" + +#: templates/search/headers.inc:43 +msgid "Sort by Type" +msgstr "" + +#: config/prefs.php.dist:72 +msgid "Sort classes by:" +msgstr "" + +#: config/prefs.php.dist:83 +msgid "Sort direction for classes:" +msgstr "" + +#: config/prefs.php.dist:119 +msgid "Sort direction for students:" +msgstr "" + +#: config/prefs.php.dist:108 +msgid "Sort students by:" +msgstr "" + +#: config/schools.php.dist:95 +msgid "Sport" +msgstr "" + +#: lib/Forms/Entry.php:78 lib/Forms/Entry.php:80 +#: templates/search/headers.inc:37 +msgid "Student" +msgstr "" + +#: lib/Forms/CreateClass.php:95 lib/Forms/CreateClass.php:108 +#: lib/Forms/CreateClass.php:141 lib/Forms/EditClass.php:83 +#: lib/Forms/EditClass.php:96 lib/Forms/EditClass.php:129 +msgid "Students" +msgstr "" + +#: templates/data/export.inc:12 +msgid "Tab separated values (TSV)" +msgstr "" + +#: config/schools.php.dist:99 config/schools.php.dist:105 +msgid "Talking" +msgstr "" + +#: config/schools.php.dist:114 +msgid "Teamwork and autonomy" +msgstr "" + +#: lib/Driver.php:38 +msgid "The School backend is not currently available." +msgstr "" + +#: lib/Driver.php:87 +#, php-format +msgid "The School backend is not currently available: %s" +msgstr "" + +#: classes/create.php:38 +#, php-format +msgid "The class \"%s\" has been created." +msgstr "" + +#: classes/delete.php:43 +#, php-format +msgid "The class \"%s\" has been deleted." +msgstr "" + +#: classes/edit.php:50 +#, php-format +msgid "The class \"%s\" has been renamed to \"%s\"." +msgstr "" + +#: classes/edit.php:52 +#, php-format +msgid "The class \"%s\" has been saved." +msgstr "" + +#: entry.php:80 +#, php-format +msgid "The entry for \"%s\" has been deleted." +msgstr "" + +#: entry.php:66 +#, php-format +msgid "The entry for \"%s\" has been saved." +msgstr "" + +#: add.php:33 +#, php-format +msgid "The new entry for \"%s\" has been added." +msgstr "" + +#: lib/Forms/CreateClass.php:152 +msgid "" +"The substitutions %c, %g or %s will be replaced automatically by the class, " +"grade respectively semester name." +msgstr "" + +#: templates/list/empty.inc:2 +msgid "There are no classes matching the current criteria." +msgstr "" + +#: templates/search/empty.inc:2 +msgid "There are no entries matching the current criteria." +msgstr "" + +#: entry.php:78 +#, php-format +msgid "There was an error deleting this entry: %s" +msgstr "" + +#: data.php:158 +msgid "There were no entries to export." +msgstr "" + +#: index.php:20 +msgid "This file defines templates for new classes." +msgstr "" + +#: lib/Forms/Entry.php:116 +msgid "Title" +msgstr "" + +#: lib/Forms/Entry.php:108 templates/search/headers.inc:43 +msgid "Type" +msgstr "" + +#: lib/Forms/DeleteClass.php:63 +#, php-format +msgid "Unable to delete \"%s\": %s" +msgstr "" + +#: lib/Driver.php:90 +#, php-format +msgid "Unable to load the definition of %s." +msgstr "" + +#: lib/Forms/EditClass.php:171 +#, php-format +msgid "Unable to save class \"%s\": %s" +msgstr "" + +#: lib/Skoli.php:753 lib/Skoli.php:754 lib/Skoli.php:768 lib/Skoli.php:769 +#: templates/list/classes.inc:46 +msgid "Unfiled" +msgstr "" + +#: lib/Forms/Entry.php:37 +msgid "Update Entry" +msgstr "" + +#: entry.php:89 +msgid "View" +msgstr "" + +#: templates/list/students.inc:27 +#, php-format +msgid "View Entries for \"%s\"" +msgstr "" + +#: templates/list/classes.inc:25 +#, php-format +msgid "View Entries in \"%s\"" +msgstr "" + +#: templates/search/entries.inc:21 +msgid "View Entry" +msgstr "" + +#: lib/Forms/Entry.php:134 +msgid "Weight" +msgstr "" + +#: config/prefs.php.dist:140 +msgid "When a new class is created should we also create a new contact list?" +msgstr "" + +#: data.php:39 +msgid "Whole class" +msgstr "" + +#: config/schools.php.dist:85 config/schools.php.dist:101 +#: config/schools.php.dist:107 +msgid "Writing" +msgstr "" + +#: classes/edit.php:28 +msgid "You are not allowed to change this class." +msgstr "" + +#: classes/delete.php:30 +msgid "You are not allowed to delete this class." +msgstr "" + +#: entry.php:36 +msgid "You are not allowed to view this entry." +msgstr "" + +#: classes/create.php:24 +msgid "You don't have access to any valid addressbook." +msgstr "" + +#: templates/panel.inc:49 +msgid "[Manage Classes]" +msgstr "" + +#: lib/Skoli.php:507 +msgid "_Export" +msgstr "" + +#: lib/Skoli.php:499 +msgid "_New Entry" +msgstr "" + +#: lib/Skoli.php:503 +msgid "_Search" +msgstr "" + +#: templates/search/criteria.inc:21 +msgid "and" +msgstr "" + +#: templates/search/criteria.inc:35 +msgid "and for Entries with:" +msgstr "" + +#: data.php:165 templates/data/export.inc:1 +msgid "class.csv" +msgstr "" + +#: data.php:170 +msgid "class.tsv" +msgstr "" + +#: templates/search/criteria.inc:25 +msgid "for" +msgstr "" + +#: lib/Block/tree_menu.php:39 +#, php-format +msgid "in %s" +msgstr "" diff --git a/skoli/pref_api.php b/skoli/pref_api.php new file mode 100644 index 000000000..52c6dae91 --- /dev/null +++ b/skoli/pref_api.php @@ -0,0 +1,71 @@ + + */ + +@define('HORDE_BASE', dirname(dirname(__FILE__))); +require_once HORDE_BASE . '/lib/core.php'; + +$registry = &Registry::singleton(); + +/* Which application. */ +$app = Util::getFormData('app'); +if (!$app) { + echo '
      '; + foreach ($registry->listApps() as $app) { + echo '
    • ' . htmlspecialchars($app) . '
    • '; + } + echo '
    '; + exit; +} + +/* Load $app's base environment, but don't request that the app perform + * authentication beyond Horde's. */ +$authentication = 'none'; +$appbase = $registry->get('fileroot', $app); +require_once $appbase . '/lib/base.php'; + +/* Which preference. */ +$pref = Util::getFormData('pref'); +if (!$pref) { + $_prefs = array(); + if (is_callable(array('Horde', 'loadConfiguration'))) { + $result = Horde::loadConfiguration('prefs.php', array('_prefs'), $app); + if (is_a($result, 'PEAR_Error')) { + exit; + } + extract($result); + } elseif (file_exists($appbase . '/config/prefs.php')) { + require $appbase . '/config/prefs.php'; + } + + echo '
      '; + foreach ($_prefs as $pref => $params) { + switch ($params['type']) { + case 'special': + case 'link': + break; + + default: + echo '
    • ' . htmlspecialchars($pref) . '
    • '; + } + } + echo '
    '; +} + +/* Which action. */ +if (Util::getPost('pref') == $pref) { + /* POST for saving a pref. */ + $prefs->setValue($pref, Util::getPost('value')); +} + +/* GET returns the current value, POST returns the new value. */ +header('Content-type: text/plain'); +echo $prefs->getValue($pref); diff --git a/skoli/scripts/.htaccess b/skoli/scripts/.htaccess new file mode 100755 index 000000000..3a4288278 --- /dev/null +++ b/skoli/scripts/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/skoli/scripts/sql/skoli.sql b/skoli/scripts/sql/skoli.sql new file mode 100755 index 000000000..3339617b4 --- /dev/null +++ b/skoli/scripts/sql/skoli.sql @@ -0,0 +1,83 @@ +-- $Horde: skoli/scripts/sql/skoli.sql,v 0.1 $ + +CREATE TABLE skoli_classes_students ( + class_id VARCHAR(255) NOT NULL, + student_id VARCHAR(255) NOT NULL +); + +CREATE INDEX skoli_classlist_idx ON skoli_classes_students (class_id); +CREATE INDEX skoli_studentlist_idx ON skoli_classes_students (student_id); + +CREATE TABLE skoli_objects ( + object_id VARCHAR(32) NOT NULL, + object_owner VARCHAR(255) NOT NULL, + object_uid VARCHAR(255) NOT NULL, + class_id VARCHAR(255) NOT NULL, + student_id VARCHAR(255) NOT NULL, + object_time INT NOT NULL, + object_type VARCHAR(255) NOT NULL, + PRIMARY KEY (object_id) +); + +CREATE INDEX skoli_objectlist_idx ON skoli_objects (object_owner); +CREATE INDEX skoli_uid_idx ON skoli_objects (object_uid); +CREATE INDEX skoli_classlist_idx ON skoli_objects (class_id); +CREATE INDEX skoli_studentlist_idx ON skoli_objects (student_id); + +CREATE TABLE skoli_object_attributes ( + object_id VARCHAR(32) NOT NULL, + attr_name VARCHAR(50) NOT NULL, + attr_value VARCHAR(255), + PRIMARY KEY (object_id, attr_name) +); +CREATE INDEX skoli_object_attributes_object_idx ON skoli_object_attributes (object_id); + +CREATE TABLE skoli_shares ( + share_id INT NOT NULL, + share_name VARCHAR(255) NOT NULL, + share_owner VARCHAR(32) NOT NULL, + share_flags SMALLINT NOT NULL DEFAULT 0, + perm_creator SMALLINT NOT NULL DEFAULT 0, + perm_default SMALLINT NOT NULL DEFAULT 0, + perm_guest SMALLINT NOT NULL DEFAULT 0, + attribute_name VARCHAR(255) NOT NULL, + attribute_desc VARCHAR(255), + attribute_school VARCHAR(255) NOT NULL, + attribute_grade VARCHAR(255), + attribute_semester VARCHAR(255), + attribute_start INT NOT NULL, + attribute_end INT NOT NULL, + attribute_category VARCHAR(255) NULL, + attribute_location VARCHAR(255), + attribute_marks VARCHAR(255), + attribute_address_book VARCHAR(255) NOT NULL, + PRIMARY KEY (share_id) +); + +CREATE INDEX skoli_shares_share_name_idx ON skoli_shares (share_name); +CREATE INDEX skoli_shares_share_owner_idx ON skoli_shares (share_owner); +CREATE INDEX skoli_shares_perm_creator_idx ON skoli_shares (perm_creator); +CREATE INDEX skoli_shares_perm_default_idx ON skoli_shares (perm_default); +CREATE INDEX skoli_shares_perm_guest_idx ON skoli_shares (perm_guest); +CREATE INDEX skoli_shares_attribute_category_idx ON skoli_shares (attribute_category); +CREATE INDEX skoli_shares_attribute_address_book_idx ON skoli_shares (attribute_address_book); + +CREATE TABLE skoli_shares_groups ( + share_id INT NOT NULL, + group_uid VARCHAR(255) NOT NULL, + perm SMALLINT NOT NULL +); + +CREATE INDEX skoli_shares_groups_share_id_idx ON skoli_shares_groups (share_id); +CREATE INDEX skoli_shares_groups_group_uid_idx ON skoli_shares_groups (group_uid); +CREATE INDEX skoli_shares_groups_perm_idx ON skoli_shares_groups (perm); + +CREATE TABLE skoli_shares_users ( + share_id INT NOT NULL, + user_uid VARCHAR(255) NOT NULL, + perm SMALLINT NOT NULL +); + +CREATE INDEX skoli_shares_users_share_id_idx ON skoli_shares_users (share_id); +CREATE INDEX skoli_shares_users_user_uid_idx ON skoli_shares_users (user_uid); +CREATE INDEX skoli_shares_users_perm_idx ON skoli_shares_users (perm); diff --git a/skoli/search.php b/skoli/search.php new file mode 100644 index 000000000..3a646ed09 --- /dev/null +++ b/skoli/search.php @@ -0,0 +1,184 @@ + + */ + +require_once dirname(__FILE__) . '/lib/base.php'; + +$classes = Skoli::listClasses(); + +/* If there are no valid classes, abort. */ +if (count($classes) == 0) { + $notification->push(_("No classes are currently available. Searching is disabled."), 'horde.error'); + require SKOLI_TEMPLATES . '/common-header.inc'; + require SKOLI_TEMPLATES . '/menu.inc'; + require $registry->get('templates', 'horde') . '/common-footer.inc'; + exit; +} + +$actionID = Util::getFormData('actionID'); + +if (!isset($_SESSION['skoli'])) { + $_SESSION['skoli'] = array(); +} + +if (($classid = Util::getFormData('class')) !== null) { + $_SESSION['skoli']['search_classid'] = $classid; +} else if (isset($_SESSION['skoli']['search_classid'])) { + $classid = $_SESSION['skoli']['search_classid']; +} +if (($studentid = Util::getFormData('student')) !== null) { + $_SESSION['skoli']['search_studentid'] = $studentid; +} else if (isset($_SESSION['skoli']['search_studentid'])) { + $studentid = $_SESSION['skoli']['search_studentid']; +} +if (($type = Util::getFormData('type')) !== null) { + $_SESSION['skoli']['search_type'] = $type; +} else if (isset($_SESSION['skoli']['search_type'])) { + $type = $_SESSION['skoli']['search_type']; +} + +$search = Util::getFormData('stext'); + +/* Sort out the sorting values */ +$sortby = Util::getFormData('sortby'); +$sortdir = Util::getFormData('sortdir'); +if ($sortby === null) { + $sortby = SKOLI_SORT_CLASS; +} else if ($sortby == Util::getFormData('sortby')) { + $sortdir = !$sortdir; +} +if ($sortdir === null) { + $sortdir = SKOLI_SORT_ASCEND; +} + +$class_options = array(); +if (count($classes) > 1) { + $class_options[] = '\n"; +} +foreach ($classes as $key=>$class) { + $class_options[] = '\n"; +} + +$student_options = array(); +$student_options[] = '\n"; +if ($classid == '' || $classid == 'all') { + $studentslist = Skoli::listStudents(null, SKOLI_SORT_NAME, SKOLI_SORT_ASCEND); + $students = array(); + foreach ($studentslist as $val) { + $students = array_merge($students, $val['_students']); + } +} else { + $studentslist = current(Skoli::listStudents($classid, SKOLI_SORT_NAME, SKOLI_SORT_ASCEND)); + $students = $studentslist['_students']; +} +$foundstudent = false; +foreach ($students as $address) { + if ($studentid == $address['student_id']) { + $foundstudent = true; + } + $student_options[] = '\n"; +} +if (!$foundstudent && $studentid != 'all') { + $actionID = ''; + $studentid = ''; + $_SESSION['skoli']['search_studentid'] = $studentid; +} + +$type_options = array(); +$type_options[] = '\n"; +if ($conf['objects']['allow_marks']) { + $type_options[] = '\n"; +} +if ($conf['objects']['allow_objectives']) { + $type_options[] = '\n"; +} +if ($conf['objects']['allow_outcomes']) { + $type_options[] = '\n"; +} +if ($conf['objects']['allow_absences']) { + $type_options[] = '\n"; +} + +$title = _("Search"); +$notification->push('document.skoli_searchform.stext.focus();', 'javascript'); + +Horde::addScriptFile('prototype.js', 'horde', true); +Horde::addScriptFile('QuickFinder.js', 'horde', true); +Horde::addScriptFile('effects.js', 'horde', true); +Horde::addScriptFile('redbox.js', 'horde', true); +require SKOLI_TEMPLATES . '/common-header.inc'; +require SKOLI_TEMPLATES . '/menu.inc'; +reset($classes); +require SKOLI_TEMPLATES . '/search/criteria.inc'; + +if ($actionID == 'search') { + $params = array($search); + $list = Skoli::listEntries($classid == 'all' ? null : $classid, $studentid == 'all' ? null : $studentid, $type == 'all' ? null : $type, $params, $sortby, $sortdir); + + $dynamic_sort = false; + $params = array('actionID' => 'search'); + $baseurl = Util::addParameter('search.php', $params); + echo '
    '; + require SKOLI_TEMPLATES . '/search/header.inc'; + if (count($list) > 0) { + require SKOLI_TEMPLATES . '/search/headers.inc'; + foreach ($list as $entry) { + $style = 'linedRow'; + $details = ''; + + switch ($entry['type']) { + case 'mark': + $details = $entry['subject'] . ': ' . $entry['mark'] . + ($classes[$entry['classid']]->get('marks') == 'percent' ? '%' : '') . + ' (' . $entry['weight'] . '), ' . $entry['title']; + break; + + case 'objective': + $details = $entry['category'] . ' (' . $entry['subject'] . '): ' . $entry['objective']; + break; + + case 'outcome': + $details = $entry['outcome'] . ': ' . + (isset($entry['completed']) && $entry['completed'] != '' ? _("Completed") : _("Open")) . + (isset($entry['comment']) && $entry['comment'] != '' ? ', ' . $entry['comment'] : ''); + break; + + case 'absence': + $details = (isset($entry['excused']) && $entry['excused'] != '' ? _("Excused") : _("Not excused")) . + ': ' . $entry['absence'] . + (isset($entry['comment']) && $entry['comment'] != '' ? ', ' . $entry['comment'] : ''); + break; + } + $detailswrapped = String::wordwrap($details, $prefs->getValue('entry_details_wrap'), '
    ', true); + $entry['details'] = current(explode('
    ', $detailswrapped)); + require SKOLI_TEMPLATES . '/search/entries.inc'; + } + + require SKOLI_TEMPLATES . '/search/footers.inc'; + + if ($dynamic_sort) { + Horde::addScriptFile('tables.js', 'horde', true); + } + } else { + require SKOLI_TEMPLATES . '/search/empty.inc'; + } +} + +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/skoli/templates/classes/list.php b/skoli/templates/classes/list.php new file mode 100644 index 000000000..c8b7a0528 --- /dev/null +++ b/skoli/templates/classes/list.php @@ -0,0 +1,35 @@ +

    + +

    + +
    +
    + + " /> +
    +
    + + 0): ?> +" cellspacing="0" id="class-list" class="striped sortable"> + + + + + + + + + + + + + + + + + + + + +
     
    get('name')) ?>">">">
    + diff --git a/skoli/templates/common-header.inc b/skoli/templates/common-header.inc new file mode 100644 index 000000000..eac8f938d --- /dev/null +++ b/skoli/templates/common-header.inc @@ -0,0 +1,38 @@ + + + + + +' : '' ?> + +get('name'); +if (!empty($title)) $page_title .= ' :: ' . $title; +if (!empty($refresh_time) && ($refresh_time > 0) && !empty($refresh_url)) { + echo "\n"; +} + +Horde::includeScriptFiles(); + +$bc = Util::nonInputVar('bodyClass'); +if ($prefs->getValue('show_panel')) { + if ($bc) { + $bc .= ' '; + } + $bc .= 'rightPanel'; +} + +?> +<?php echo htmlspecialchars($page_title) ?> + + + + + +> diff --git a/skoli/templates/data/export.inc b/skoli/templates/data/export.inc new file mode 100644 index 000000000..575ef838d --- /dev/null +++ b/skoli/templates/data/export.inc @@ -0,0 +1,34 @@ +
    "> + + +

    + +

    + +
    +
    +
    + + 1): ?> +
    +
    +
    + + + +
    + +
    +
    +
    + + " class="button" /> +
    +
    diff --git a/skoli/templates/entry/delete.inc b/skoli/templates/entry/delete.inc new file mode 100644 index 000000000..d2db7787a --- /dev/null +++ b/skoli/templates/entry/delete.inc @@ -0,0 +1,10 @@ +
    + + + + +
    +

    + " /> +
    +
    diff --git a/skoli/templates/list/classes.inc b/skoli/templates/list/classes.inc new file mode 100644 index 000000000..b3906ec1e --- /dev/null +++ b/skoli/templates/list/classes.inc @@ -0,0 +1,48 @@ + + + getImageDir('skoli')) . ''; + } + ?> + + + getImageDir('horde')) . ''; + } + ?> + + +   + +   + + + 'search', + 'class' => $class['_id'] + ); + echo Horde::link(Util::addParameter(Horde::applicationUrl('search.php'), $params), $label) . htmlspecialchars($class['name']) . ''; + ?> + + +   + +   + +   + +   + +   + +   + + + + diff --git a/skoli/templates/list/empty.inc b/skoli/templates/list/empty.inc new file mode 100644 index 000000000..3e418aa2e --- /dev/null +++ b/skoli/templates/list/empty.inc @@ -0,0 +1,3 @@ +

    + +

    diff --git a/skoli/templates/list/footers.inc b/skoli/templates/list/footers.inc new file mode 100644 index 000000000..a5ef8c968 --- /dev/null +++ b/skoli/templates/list/footers.inc @@ -0,0 +1,6 @@ + + +

    + +
    +
    diff --git a/skoli/templates/list/header.inc b/skoli/templates/list/header.inc new file mode 100644 index 000000000..1cb21fe53 --- /dev/null +++ b/skoli/templates/list/header.inc @@ -0,0 +1,10 @@ + diff --git a/skoli/templates/list/headers.inc b/skoli/templates/list/headers.inc new file mode 100644 index 000000000..c6a9c42c8 --- /dev/null +++ b/skoli/templates/list/headers.inc @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skoli/templates/list/students.inc b/skoli/templates/list/students.inc new file mode 100644 index 000000000..c483559ca --- /dev/null +++ b/skoli/templates/list/students.inc @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skoli/templates/menu.inc b/skoli/templates/menu.inc new file mode 100644 index 000000000..72cecc822 --- /dev/null +++ b/skoli/templates/menu.inc @@ -0,0 +1,4 @@ + +notify(array('listeners' => 'status')) ?> diff --git a/skoli/templates/panel.inc b/skoli/templates/panel.inc new file mode 100644 index 000000000..3ea432cd3 --- /dev/null +++ b/skoli/templates/panel.inc @@ -0,0 +1,77 @@ + $cl) { + if ($cl->get('owner') == $current_user) { + $my_classes[$id] = $cl; + } else { + $shared_classes[$id] = $cl; + } +} +?> + +
    + + + + +
    +
    + +

    +

    + +

    +

    + +

    + + +

    + +

    + + + +

    +
      + $cl): ?> +
    • + +
    + + + +

    +
      + $cl): ?> +
    • + +
    + + +

    + " class="button" /> +

    + + +
    +
    diff --git a/skoli/templates/search/criteria.inc b/skoli/templates/search/criteria.inc new file mode 100644 index 000000000..b17bb8f2c --- /dev/null +++ b/skoli/templates/search/criteria.inc @@ -0,0 +1,44 @@ + + + + +

    + +

    + +
    + + 1): ?> + + + get('name') ?> + +  + +  +
    + +
    + +
    + getImageDir('skoli')) ?> + + getImageDir('horde')) ?> + width="2%"> +   + width="2%"> +   + > +   + width="2%"> +   + width="2%"> +   + width="2%"> +   + width="2%"> +   + width="2%"> +   + width="2%"> +   + width="10%"> +   + isLocked('categories') || + !$GLOBALS['prefs']->isLocked('category_colors'))) { + $categoryUrl = Util::addParameter(Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/services/prefs.php'), array('app' => 'horde', 'group' => 'categories')); + echo ' ' . Horde::link($categoryUrl, _("Edit categories and colors"), '', '_blank', 'popup(this.href); return false;') . Horde::img('colorpicker.png', _("Edit categories and colors"), '', $GLOBALS['registry']->getImageDir('horde')) . ''; + } + ?> +
    + $class['_id'], 'student'=>$student['__key'])), $label) . Horde::img('add.png', $label, null, $registry->getImageDir('skoli')) . ''; + } + ?> + + hasMethod('contacts/show')) { + $label = sprintf(_("Show \"%s\""), $student[$conf['addresses']['name_field']]); + $url = $registry->link('contacts/show', array('source' => $class['address_book'], + 'key' => $student['__key'])); + echo Horde::link($url, $label) . Horde::img('user.png', $label, null, $registry->getImageDir('horde')) . ''; + } + ?> +    + 'search', + 'class' => $class['_id'], + 'student' => $student['__key'] + ); + echo $treeIcon . ' ' . Horde::link(Util::addParameter(Horde::applicationUrl('search.php'), $params), $label) . htmlspecialchars($student[$conf['addresses']['name_field']]) . ''; + ?> +      
    + + + +
    + + + + " /> + " /> +
    +
  • + diff --git a/skoli/templates/search/empty.inc b/skoli/templates/search/empty.inc new file mode 100644 index 000000000..3121ac26f --- /dev/null +++ b/skoli/templates/search/empty.inc @@ -0,0 +1,3 @@ +

    + +

    diff --git a/skoli/templates/search/entries.inc b/skoli/templates/search/entries.inc new file mode 100644 index 000000000..7d85afef8 --- /dev/null +++ b/skoli/templates/search/entries.inc @@ -0,0 +1,27 @@ + + + 'EditEntry', + 'entry' => $entry['_id'] + ); + echo Horde::link(Util::addParameter(Horde::applicationUrl('entry.php'), $params), $label) . Horde::img('edit.png', $label, null, $registry->getImageDir('horde')) . ''; + } + ?> + +   + + 'Entry', + 'entry' => $entry['_id'] + ); + echo Horde::link(Util::addParameter(Horde::applicationUrl('entry.php'), $params), _("View Entry")) . htmlspecialchars($entry['student']) . ' '; + ?> + +   +   +   + diff --git a/skoli/templates/search/footers.inc b/skoli/templates/search/footers.inc new file mode 100644 index 000000000..bafb71ee3 --- /dev/null +++ b/skoli/templates/search/footers.inc @@ -0,0 +1,6 @@ + + +

    + +
    + diff --git a/skoli/templates/search/header.inc b/skoli/templates/search/header.inc new file mode 100644 index 000000000..585dab5ab --- /dev/null +++ b/skoli/templates/search/header.inc @@ -0,0 +1,11 @@ +
    + diff --git a/skoli/templates/search/headers.inc b/skoli/templates/search/headers.inc new file mode 100644 index 000000000..fc9cb5ac0 --- /dev/null +++ b/skoli/templates/search/headers.inc @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + diff --git a/skoli/themes/categoryCSS.php b/skoli/themes/categoryCSS.php new file mode 100644 index 000000000..fb45e11ed --- /dev/null +++ b/skoli/themes/categoryCSS.php @@ -0,0 +1,36 @@ + + */ + +@define('AUTH_HANDLER', true); +@define('SKOLI_BASE', dirname(__FILE__) . '/..'); +require_once SKOLI_BASE . '/lib/base.php'; +require_once 'Horde/Image.php'; +require_once 'Horde/Prefs/CategoryManager.php'; + +header('Content-Type: text/css'); + +$cManager = new Prefs_CategoryManager(); + +$colors = $cManager->colors(); +$fgColors = $cManager->fgColors(); +foreach ($colors as $category => $color) { + if ($category == '_unfiled_' || $category == '_default_') { + continue; + } + + $class = '.category' . md5($category); + + echo "$class, .linedRow td$class, .overdue td$class, .closed td$class { " + . 'color: ' . (isset($fgColors[$category]) ? $fgColors[$category] : $fgColors['_default_']) . '; ' + . 'background: ' . $color . '; ' + . "padding: 0 4px; }\n"; +} diff --git a/skoli/themes/graphics/add.png b/skoli/themes/graphics/add.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f261d10fccd9c1e0709ced8290274e42451239 GIT binary patch literal 3895 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 Iol`;+0JRbu(EtDd literal 0 HcmV?d00001 diff --git a/skoli/themes/graphics/favicon.ico b/skoli/themes/graphics/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c3b82e4f198b2b36c4b34b24336168405ba4510f GIT binary patch literal 792 zcmZvaOH(qH7%d_k@MjMlb}qeF%pyA-JB$uBfR?sv%h zhQm3)&B7YU*dZ&o57IUTKY?N$Qa&;|nqX)sMkEqtdwZL$tt~b;H`&5hKNN;a1fj|Ji-;dAdLy{ys9uF>; zOU%tldwV+$hXcFaF6P;S)oP`=xfzSaB4%pFXf$Fl7|`qWG&VM()9KJ^wba(uqS0um zuC7k*Rn+kxNFd3wBq$()-r(R6RMsTR?rit@`}(8jAE@qEOS1IR+3NQ81P4x56soI` zd9F9McwPs>S@*6Mi7Xf&Hd%DfdLkE6@^6dx*y|e}89Uot*{RpckR`3@vEjul&*jr+ z?&g(WlF&AqZI0TEW4YIhOVTbvL(>zJRe9q|!JX3MId70!o1YqD e+o3sks;)~c_HV)6Uh!`aqoW@F36jdu0e=AR*WE4v literal 0 HcmV?d00001 diff --git a/skoli/themes/graphics/minus.png b/skoli/themes/graphics/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..32170460cc24a1a1687786fb1e135b84ed80bd21 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!Wq?nJt4ZpT+GW?9SKZil=2`uU z>uqaqPS|{F(w19Ow%wk;@4?nH&$gX?cHr`hsoQUFIrDV;xo5jCKHqck`Tzg_WoKCg z01Xi?3GxdDa?t?8rrJ9kKxsWs7sn8ZsmTIP%seJS2@XsGjB<>QbqWujCAD#V_*k@I m^G2hM21P~**PYm>Gcg#2@~8j${jM6Qlfl!~&t;ucLK6T0fn84k literal 0 HcmV?d00001 diff --git a/skoli/themes/graphics/plus.png b/skoli/themes/graphics/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..263e356901817a5ace857c6416481a8cc89faedc GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!Wq?nJt4ZpT+GW?9SKZil=2`uU z>uqaqPS|{F(w19Ow%wk;@4?nH&$gX?cHr`hsoQUFIrDV;xo5jCKHqck`Tzg_WoKCg z01Xi?3GxdDa?t?8rrJ9kKxq$87sn8Z%gF*x%sdt>5)BNY4fE%VFixnOz;HxH;E|xf z6LzLNy|fh!^7`!7(%Qn@%IeHc+}hes+(!b!N}?k;cQW67Sy98yP|(Hueogq5a-f+E Mp00i_>zopr0454z8UO$Q literal 0 HcmV?d00001 diff --git a/skoli/themes/graphics/redbox_spinner.gif b/skoli/themes/graphics/redbox_spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..35218b31b5e09d98e59af8f8c17ab0c548b302b6 GIT binary patch literal 6820 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 literal 0 HcmV?d00001 diff --git a/skoli/themes/graphics/search.png b/skoli/themes/graphics/search.png new file mode 100644 index 0000000000000000000000000000000000000000..94c47d455e3eb1c758c79da1fd74f37831616196 GIT binary patch literal 794 zcmV+#1LgdQP)1V6Mdku2@=!r7%xX>^OL z4bEEPwX>aj!!QhRo@4V1vcX^w6#Wji;{!E1;n*IxQSpzntI-#4J_iS-2IpOw-i0Gb zv-ss=j{67X$rJ#OG)=a-xw#|F5SbG^wOYAK$RsEF-S?~YE<&bv-jErbm8EUdGg`Af zzqq(?sBUSVEdW}z>au3Fig8n$z^zU4%JE#PchwLER~eySR-vsF<2u_z?rwz&!2?0% zH=n5!}4wi%I##Uk79|0l8&reNFf~HjsG6j+6@hhUZ zNO<`K1a%T=Y^wKnbapTni)D}dTy2}yTfr;aEGa1|7l|>LoE!&j`(v=#Yn+a literal 0 HcmV?d00001 diff --git a/skoli/themes/graphics/skoli.png b/skoli/themes/graphics/skoli.png new file mode 100644 index 0000000000000000000000000000000000000000..abba5279e8a84c7e8b43e86886707b129e928a84 GIT binary patch literal 3921 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
    + getImageDir('horde')) ?> + width="2%"> +   + width="2%"> +   + width="2%"> +   + width="2%"> +   + +   +