From: Jan Schneider Date: Wed, 29 Apr 2009 13:52:58 +0000 (+0200) Subject: Import skoli. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=d900291bf7e6be1468116e5806310668122b9095;p=horde.git Import skoli. --- 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 000000000..a01e3e666 Binary files /dev/null and b/skoli/locale/de_DE/LC_MESSAGES/skoli.mo differ 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 000000000..a2f261d10 Binary files /dev/null and b/skoli/themes/graphics/add.png differ diff --git a/skoli/themes/graphics/az.png b/skoli/themes/graphics/az.png new file mode 100644 index 000000000..2b646f94c Binary files /dev/null and b/skoli/themes/graphics/az.png differ diff --git a/skoli/themes/graphics/favicon.ico b/skoli/themes/graphics/favicon.ico new file mode 100644 index 000000000..c3b82e4f1 Binary files /dev/null and b/skoli/themes/graphics/favicon.ico differ diff --git a/skoli/themes/graphics/minus.png b/skoli/themes/graphics/minus.png new file mode 100644 index 000000000..32170460c Binary files /dev/null and b/skoli/themes/graphics/minus.png differ diff --git a/skoli/themes/graphics/plus.png b/skoli/themes/graphics/plus.png new file mode 100644 index 000000000..263e35690 Binary files /dev/null and b/skoli/themes/graphics/plus.png differ diff --git a/skoli/themes/graphics/redbox_spinner.gif b/skoli/themes/graphics/redbox_spinner.gif new file mode 100644 index 000000000..35218b31b Binary files /dev/null and b/skoli/themes/graphics/redbox_spinner.gif differ diff --git a/skoli/themes/graphics/search.png b/skoli/themes/graphics/search.png new file mode 100644 index 000000000..94c47d455 Binary files /dev/null and b/skoli/themes/graphics/search.png differ diff --git a/skoli/themes/graphics/skoli.png b/skoli/themes/graphics/skoli.png new file mode 100644 index 000000000..abba5279e Binary files /dev/null and b/skoli/themes/graphics/skoli.png differ diff --git a/skoli/themes/graphics/timetable.png b/skoli/themes/graphics/timetable.png new file mode 100644 index 000000000..3da6e7eb3 Binary files /dev/null and b/skoli/themes/graphics/timetable.png differ diff --git a/skoli/themes/graphics/za.png b/skoli/themes/graphics/za.png new file mode 100644 index 000000000..a154237b5 Binary files /dev/null and b/skoli/themes/graphics/za.png differ diff --git a/skoli/themes/screen.css b/skoli/themes/screen.css new file mode 100644 index 000000000..db458802b --- /dev/null +++ b/skoli/themes/screen.css @@ -0,0 +1,166 @@ +/** + * $Horde: skoli/themes/screen.css,v 0.1 $ + */ + +/* Menu bottom margin, added for BC. */ +#menu { + margin-bottom: 8px; +} + +/* Added for BC. */ +.hidden { + display: none; +} +.radio { + border: 0; + height: 14px; + width: 14px; + background: transparent; +} + +/* Image alignment, added for BC. */ +img { + vertical-align: middle; +} + +/* Table CSS, added for BC. */ +th { + color: #333; + font-size: 90%; + border-bottom: 1px solid #999; +} + +/* Sort arrow styles, added for BC. */ +.sortup { + background: #bbcbff url("graphics/za.png") center left no-repeat; + padding-left: 10px; +} +.sortdown { + background: #bbcbff url("graphics/az.png") center left no-repeat; + padding-left: 10px; +} + +#classes { + width: 100%; +} + +.QuickFinderNoMatch { + display: none; +} +#classes_empty { + padding: 4px; + font-style: italic; +} + +#quicksearch { + display: inline; +} +#quicksearch input { + background-image: url("graphics/search.png"); + background-repeat: no-repeat; + background-position: 2px center; + padding: 1px 0 1px 20px; +} +#quicksearch a { + display: inline-block; + padding: 2px 4px; +} + +/* Added for class list panel */ +body.rightPanel #page { + margin-right: 10em; +} +body #pageControlsInner { + display: none; +} +body #sbarShow, body #sbarHide { + padding-left: 18px; + white-space: nowrap; +} +body #sbarShow { + display: block; + background: transparent url("graphics/plus.png") center left no-repeat; +} +body #sbarHide { + display: none; + background: transparent url("graphics/minus.png") center left no-repeat; +} +body.rightPanel #pageControlsInner { + display: block; +} +body.rightPanel #sbarShow { + display: none; +} +body.rightPanel #sbarHide { + display: block; +} +body.rightPanel ul { + list-style-type: none; +} + +#pageControls { + position: absolute; + top: 52px; + right: 0; + background: #eee; + border: 1px solid #ccc; + padding: 4px; + -moz-border-radius-bottomleft: 10px; + -webkit-border-bottom-left-radius: 10px; +} +#pageControls #classSearch { + background-image: url("graphics/search.png"); + background-repeat: no-repeat; + background-position: 2px center; + border: 1px solid #ccc; + padding: 2px 0 2px 20px; +} +#pageControls h3 { + font-size: 100%; + margin-top: 1em; +} +#pageControls h4 { + font-size: 90%; + margin-top: .5em; +} + +/* Tables. */ +table#class-list { + width: 99%; + margin: 0 0 8px 5px; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} +table#class-list th { + padding: 3px; + background: #e9e9e9; + border-right: 1px solid #ccc; + text-align: left; +} +table#class-list td { + padding: 3px; + border-right: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} +table#class-list th.sortup { + background: #bbcbff url("graphics/za.png") center left no-repeat; + padding-left: 10px; +} +table#class-list th.sortdown { + background: #bbcbff url("graphics/az.png") center left no-repeat; + padding-left: 10px; +} + +#class-list-buttons { + padding: 1em; +} +#class-list-buttons form { + display: inline; +} +.class-list-icon { + width: 1%; +} + +.class-info { + cursor: pointer; +}
+ getImageDir('horde')) ?> + width="2%"> +   + width="2%"> +   + width="2%"> +   + width="2%"> +   + +   +