Import skoli.
authorJan Schneider <jan@horde.org>
Wed, 29 Apr 2009 13:52:58 +0000 (15:52 +0200)
committerJan Schneider <jan@horde.org>
Wed, 29 Apr 2009 13:53:47 +0000 (15:53 +0200)
70 files changed:
skoli/LICENSE [new file with mode: 0644]
skoli/README [new file with mode: 0644]
skoli/add.php [new file with mode: 0644]
skoli/classes/create.php [new file with mode: 0644]
skoli/classes/delete.php [new file with mode: 0644]
skoli/classes/edit.php [new file with mode: 0644]
skoli/classes/index.php [new file with mode: 0644]
skoli/config/conf.xml [new file with mode: 0644]
skoli/config/prefs.php.dist [new file with mode: 0644]
skoli/config/schools.php.dist [new file with mode: 0644]
skoli/data.php [new file with mode: 0644]
skoli/docs/CHANGES [new file with mode: 0644]
skoli/docs/CREDITS [new file with mode: 0644]
skoli/docs/INSTALL [new file with mode: 0644]
skoli/docs/RELEASE_NOTES [new file with mode: 0644]
skoli/docs/TODO [new file with mode: 0644]
skoli/entry.php [new file with mode: 0644]
skoli/index.php [new file with mode: 0644]
skoli/lib/Block/tree_menu.php [new file with mode: 0644]
skoli/lib/Driver.php [new file with mode: 0644]
skoli/lib/Driver/sql.php [new file with mode: 0644]
skoli/lib/Forms/CreateClass.php [new file with mode: 0644]
skoli/lib/Forms/DeleteClass.php [new file with mode: 0644]
skoli/lib/Forms/EditClass.php [new file with mode: 0644]
skoli/lib/Forms/Entry.php [new file with mode: 0644]
skoli/lib/School.php [new file with mode: 0644]
skoli/lib/Skoli.php [new file with mode: 0644]
skoli/lib/base.php [new file with mode: 0644]
skoli/lib/version.php [new file with mode: 0644]
skoli/list.php [new file with mode: 0644]
skoli/locale/de_DE/LC_MESSAGES/skoli.mo [new file with mode: 0644]
skoli/locale/de_DE/help.xml [new file with mode: 0644]
skoli/locale/en_US/help.xml [new file with mode: 0644]
skoli/po/README [new file with mode: 0644]
skoli/po/de_DE.po [new file with mode: 0644]
skoli/po/skoli.pot [new file with mode: 0644]
skoli/pref_api.php [new file with mode: 0644]
skoli/scripts/.htaccess [new file with mode: 0755]
skoli/scripts/sql/skoli.sql [new file with mode: 0755]
skoli/search.php [new file with mode: 0644]
skoli/templates/classes/list.php [new file with mode: 0644]
skoli/templates/common-header.inc [new file with mode: 0644]
skoli/templates/data/export.inc [new file with mode: 0644]
skoli/templates/entry/delete.inc [new file with mode: 0644]
skoli/templates/list/classes.inc [new file with mode: 0644]
skoli/templates/list/empty.inc [new file with mode: 0644]
skoli/templates/list/footers.inc [new file with mode: 0644]
skoli/templates/list/header.inc [new file with mode: 0644]
skoli/templates/list/headers.inc [new file with mode: 0644]
skoli/templates/list/students.inc [new file with mode: 0644]
skoli/templates/menu.inc [new file with mode: 0644]
skoli/templates/panel.inc [new file with mode: 0644]
skoli/templates/search/criteria.inc [new file with mode: 0644]
skoli/templates/search/empty.inc [new file with mode: 0644]
skoli/templates/search/entries.inc [new file with mode: 0644]
skoli/templates/search/footers.inc [new file with mode: 0644]
skoli/templates/search/header.inc [new file with mode: 0644]
skoli/templates/search/headers.inc [new file with mode: 0644]
skoli/themes/categoryCSS.php [new file with mode: 0644]
skoli/themes/graphics/add.png [new file with mode: 0644]
skoli/themes/graphics/az.png [new file with mode: 0644]
skoli/themes/graphics/favicon.ico [new file with mode: 0644]
skoli/themes/graphics/minus.png [new file with mode: 0644]
skoli/themes/graphics/plus.png [new file with mode: 0644]
skoli/themes/graphics/redbox_spinner.gif [new file with mode: 0644]
skoli/themes/graphics/search.png [new file with mode: 0644]
skoli/themes/graphics/skoli.png [new file with mode: 0644]
skoli/themes/graphics/timetable.png [new file with mode: 0644]
skoli/themes/graphics/za.png [new file with mode: 0644]
skoli/themes/screen.css [new file with mode: 0644]

diff --git a/skoli/LICENSE b/skoli/LICENSE
new file mode 100644 (file)
index 0000000..da074b8
--- /dev/null
@@ -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 <http://www.horde.org/>.
diff --git a/skoli/README b/skoli/README
new file mode 100644 (file)
index 0000000..970529f
--- /dev/null
@@ -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 (file)
index 0000000..4ce9cf4
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * $Horde: skoli/add.php,v 0.1 $
+ *
+ * Copyright 2001-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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 <tinu@humbapa.ch>
+ */
+
+@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 (file)
index 0000000..328b949
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * $Horde: skoli/classes/create.php,v 0.1 $
+ *
+ * Copyright 2002-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+@define('SKOLI_BASE', dirname(dirname(__FILE__)));
+require_once SKOLI_BASE . '/lib/base.php';
+require_once SKOLI_BASE . '/lib/Forms/CreateClass.php';
+
+// Exit if this isn't an authenticated user.
+if (!Auth::getAuth()) {
+    header('Location: ' . Horde::applicationUrl('list.php', true));
+    exit;
+}
+
+// Exit if we don't have access to addressbooks.
+require_once SKOLI_BASE . '/lib/School.php';
+if (!count(Skoli_School::listAddressBooks())) {
+    $notification->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 (file)
index 0000000..3db6caf
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * $Horde: skoli/classes/delete.php,v 0.1 $
+ *
+ * Copyright 2002-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+@define('SKOLI_BASE', dirname(dirname(__FILE__)));
+require_once SKOLI_BASE . '/lib/base.php';
+require_once SKOLI_BASE . '/lib/Forms/DeleteClass.php';
+
+// Exit if this isn't an authenticated user.
+if (!Auth::getAuth()) {
+    header('Location: ' . Horde::applicationUrl('list.php', true));
+    exit;
+}
+
+$vars = Variables::getDefaultVariables();
+$class_id = $vars->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 (file)
index 0000000..2c594e7
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * $Horde: skoli/classes/edit.php,v 0.1 $
+ *
+ * Copyright 2002-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+@define('SKOLI_BASE', dirname(dirname(__FILE__)));
+require_once SKOLI_BASE . '/lib/base.php';
+require_once SKOLI_BASE . '/lib/Forms/EditClass.php';
+
+// Exit if this isn't an authenticated user.
+if (!Auth::getAuth()) {
+    header('Location: ' . Horde::applicationUrl('list.php', true));
+    exit;
+}
+
+$vars = Variables::getDefaultVariables();
+$class = $skoli_shares->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 (file)
index 0000000..2bf6d19
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * $Horde: skoli/classes/index.php,v 0.1 $
+ *
+ * Copyright 2002-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+@define('SKOLI_BASE', dirname(dirname(__FILE__)));
+require_once SKOLI_BASE . '/lib/base.php';
+
+// Exit if this isn't an authenticated user.
+if (!Auth::getAuth()) {
+    header('Location: ' . Horde::applicationUrl('list.php', true));
+    exit;
+}
+
+$edit_url_base = Horde::applicationUrl('classes/edit.php');
+$perms_url_base = Horde::url($registry->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 (file)
index 0000000..e61238f
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<!-- $Horde: skoli/config/conf.xml,v 0.1 $ -->
+<configuration>
+ <configsection name="storage">
+  <configheader>
+   Storage System Settings
+  </configheader>
+  <configswitch name="driver" desc="What storage driver should we use?">sql
+   <case name="sql" desc="SQL">
+    <configsection name="params">
+     <configsql switchname="driverconfig">
+     </configsql>
+    </configsection>
+   </case>
+  </configswitch>
+ </configsection>
+
+ <configsection name="objects">
+  <configheader>
+   Settings for new Objects
+  </configheader>
+  <configboolean name="allow_marks" desc="Allow users to add marks?">true</configboolean>
+  <configboolean name="allow_objectives" desc="Allow users to add objectives?">true</configboolean>
+  <configboolean name="allow_outcomes" desc="Allow users to add outcomes?">true</configboolean>
+  <configboolean name="allow_absences" desc="Allow users to add absences?">true</configboolean>
+ </configsection>
+
+ <configsection name="addresses">
+  <configheader>
+   Address settings
+  </configheader>
+  <configswitch name="storage" desc="In what address book should we store students?">ask
+   <case name="ask" desc="Ask user">
+   </case>
+   <case name="custom" desc="One address book for all students">
+    <configstring name="address_book" desc="Name of a client address book. If this is a shared address book, use the share id, not the source name.">localsql</configstring>
+   </case>
+  </configswitch>
+  <configstring name="name_field" desc="Field name from the client address book to display as name field.">name</configstring>
+  <configswitch name="contact_list" desc="When a new class is created should we also create a new contact list?">user
+   <case name="user" desc="Per user preferences setting">
+   </case>
+   <case name="none"  desc="Don't create contact lists">
+   </case>
+   <case name="auto" desc="Automatically create a new contact list">
+    <configstring name="contact_list_name" desc="Enter a default name for new contact lists.&lt;br /&gt;NOTE: You can use %c, %g or %s as substitution for the class, grade respectively semester name.">%c - %g - %s</configstring>
+   </case>
+  </configswitch>
+ </configsection>
+
+ <configsection name="menu">
+  <configheader>
+   Menu settings
+  </configheader>
+  <configboolean name="export" desc="Should we display an Export link in the Horde application menus?">true</configboolean>
+  <configmultienum name="apps" desc="Select any applications that should be linked in Skoli's menu">
+   <values>
+    <configspecial name="list-horde-apps" />
+   </values>
+  </configmultienum>
+ </configsection>
+</configuration>
diff --git a/skoli/config/prefs.php.dist b/skoli/config/prefs.php.dist
new file mode 100644 (file)
index 0000000..7a974ff
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+/**
+ * $Horde: skoli/config/prefs.php.dist,v 0.1 $
+ *
+ * See horde/config/prefs.php for documentation on the structure of this file.
+ */
+
+$prefGroups['display'] = array(
+    'column' => _("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.<br />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).<br />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 (file)
index 0000000..2573fa1
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * $Horde: skoli/config/schools.php.dist,v 0.1 $
+ *
+ * This file is where you specify default informations needed to create a
+ * new class. It contains one EXAMPLE. Please remove or comment out that 
+ * example if YOU DON'T NEED IT. There are a number of properties that you 
+ * can set for each school, including:
+ *
+ * title:      The name of the school template.
+ *
+ * grade:      List of grades for this school.
+ *
+ * semester:   List of semesters. Specify each semester with:
+ *
+ *   name:  The semester name.
+ *
+ *   start: Semester startdate. Currently supported formats are:
+ *            timestamp  --  Timestamp
+ *            ISO week   --  ISO week date format: e.g. 'W33-2' is Tuesday 
+ *                           of week 33.
+ *            strtotime  --  A string who can be parsed by 'strtotime'. See
+ *                           http://www.php.net/strtotime for more informations.
+ *
+ *   end:   Semester enddate (same format as 'start').
+ *
+ * location:   List of locations, e.g. schoolhouses.
+ *
+ * marks:      The format to use for marks. Currently supported formats are:
+ *               numbers  --  In numbers: e.g. 4.4
+ *               percent  --  In percent: e.g. 74%
+ *               custom   --  List with custom marks separated by comma (best mark first):
+ *                            e.g. 6, 5.5, 5, 4.5, 4, 3.5, 3, 2.5, 2, 1.5, 1
+ *
+ * subjects:   List of subjects.
+ *
+ *   objectives: Each subject may have a list of objectives.
+ *
+ * objectives: List of objectives without a specific subject.
+ */
+
+/**
+ * If a property is not defined then the user will be able to enter the
+ * missing informations as free text. E.g. the following school template
+ * will accept any user input:
+ */
+$cfgSchools['custom'] = array(
+    'title' => _("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 (file)
index 0000000..145890e
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+/**
+ * $Horde: skoli/data.php,v 0.1 $
+ *
+ * Copyright 2001-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (ASL).  If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @author Martin Blumenthal <tinu@humbapa.ch>
+ */
+
+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[] = '<option value="' . htmlspecialchars($key) . '"' . (Util::getFormData('class') == $key ? ' selected="selected"' : '') . '>' .
+                       htmlspecialchars($class->get('name')) . "</option>\n";
+}
+
+$wholeclass_option = '<option value="all">' .
+                     htmlspecialchars(_("Whole class")) . "</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[] = '<option value="' . htmlspecialchars($address['student_id']) . '">' .
+                         htmlspecialchars($address[$conf['addresses']['name_field']]) . "</option>\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 (file)
index 0000000..7e31cfc
--- /dev/null
@@ -0,0 +1,5 @@
+----
+v0.1
+----
+
+[xyz] Initial Release
diff --git a/skoli/docs/CREDITS b/skoli/docs/CREDITS
new file mode 100644 (file)
index 0000000..a755182
--- /dev/null
@@ -0,0 +1,17 @@
+===========================
+ Skoli Development Team
+===========================
+
+
+Core Developers
+===============
+
+- Martin Blumenthal <tinu@humbapa.ch>
+
+
+Localization
+============
+
+=====================   ======================================================
+German                  Martin Blumenthal <tinu@humbapa.ch>
+=====================   ======================================================
diff --git a/skoli/docs/INSTALL b/skoli/docs/INSTALL
new file mode 100644 (file)
index 0000000..9cf25ee
--- /dev/null
@@ -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 (file)
index 0000000..c49e37a
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Release focus. Possible values:
+ * 0 - N/A
+ * 1 - Initial freshmeat announcement
+ * 2 - Documentation
+ * 3 - Code cleanup
+ * 4 - Minor feature enhancements
+ * 5 - Major feature enhancements
+ * 6 - Minor bugfixes
+ * 7 - Major bugfixes
+ * 8 - Minor security fixes
+ * 9 - Major security fixes
+ */
+$this->notes['fm']['focus'] = 4;
+
+/* Mailing list release notes. */
+$this->notes['ml']['changes'] = <<<ML
+The Horde Team is pleased to announce the [first release candidate|final
+release] of the Skoli Foo Bar Application version H3 (x.x).
+
+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.
+
+[For alpha/beta releases:
+This is a preview version that should not be used on production systems.  This
+version is considered feature complete but there might still be a few bugs.
+You should not use this preview version over existing production data.
+
+We encourage widespread testing and feedback via the mailing lists or our bug
+tracking system.  Updated translations are very welcome, though some strings
+might still change before the final release.]
+
+[For release candidates:
+Barring any problems, this code will be released as Skoli H3 (x.x).
+Testing is requested and comments are encouraged.
+Updated translations would also be great.]
+
+The major changes compared to the Skoli version H3 (x.x) are:
+[or: Changes in this release:]
+    * ...
+ML;
+
+/* Freshmeat release notes, not more than 600 characters. */
+$this->notes['fm']['changes'] = <<<FM
+FM;
+
+$this->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 (file)
index 0000000..9df0d65
--- /dev/null
@@ -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 (file)
index 0000000..780bae8
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * $Horde: skoli/entry.php,v 0.1 $
+ *
+ * Copyright 2000-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (ASL).  If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @author Martin Blumenthal <tinu@humbapa.ch>
+ */
+
+@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 '<div id="page">';
+echo $tabs->render($viewName);
+echo '<h1 class="header">' . sprintf(_("Entry for \"%s\""), $studentdetails[$conf['addresses']['name_field']]) . '</h1>';
+
+// 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 '</div>';
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/skoli/index.php b/skoli/index.php
new file mode 100644 (file)
index 0000000..fe0a3ae
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+/**
+ * $Horde: skoli/index.php,v 0.1 $
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+@define('SKOLI_BASE', dirname(__FILE__));
+$skoli_configured = (is_readable(SKOLI_BASE . '/config/conf.php') &&
+                     is_readable(SKOLI_BASE . '/config/prefs.php') &&
+                     is_readable(SKOLI_BASE . '/config/schools.php'));
+
+if (!$skoli_configured) {
+    require SKOLI_BASE . '/../lib/Test.php';
+    Horde_Test::configFilesMissing('Skoli', SKOLI_BASE,
+                                   array('conf.php', 'prefs.php'),
+                                   array('schools.php' => _('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 (file)
index 0000000..0c8fcf0
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+$block_name = _("Menu List");
+$block_type = 'tree';
+
+/**
+ * $Horde: skoli/lib/Block/tree_menu.php,v 0.1 $
+ *
+ * @package Horde_Block
+ */
+class Horde_Block_skoli_tree_menu extends Horde_Block {
+
+    var $_app = 'skoli';
+
+    function _buildTree(&$tree, $indent = 0, $parent = null)
+    {
+        global $registry;
+
+        require_once dirname(__FILE__) . '/../base.php';
+
+        $add = Horde::applicationUrl('add.php');
+        $icondir = $registry->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 (file)
index 0000000..bbd3013
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Skoli_Driver:: defines an API for implementing storage backends for
+ * Skoli.
+ *
+ * $Horde: skoli/lib/Driver.php,v 0.1 $
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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 <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..d6d7cc0
--- /dev/null
@@ -0,0 +1,694 @@
+<?php
+/**
+ * Skoli storage implementation for PHP's PEAR database abstraction layer.
+ *
+ * Required parameters:<pre>
+ *   'phptype'      The database type (e.g. 'pgsql', 'mysql', etc.).
+ *   'charset'      The database's internal charset.</pre>
+ *
+ * Required by some database implementations:<pre>
+ *   '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.</pre>
+ *
+ * Optional values when using separate reading and writing servers, for example
+ * in replication settings:<pre>
+ *   '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.</pre>
+ *
+ * Optional parameters:<pre>
+ *   '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'.</pre>
+ *
+ * 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 <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..049aecd
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+/**
+ * Horde_Form for creating classes.
+ *
+ * $Horde: skoli/lib/Forms/CreateClass.php,v 0.1 $
+ *
+ * See the enclosed file LICENSE for license information (ASL). If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @package Skoli
+ */
+
+/** Variables */
+require_once 'Horde/Variables.php';
+
+/** Horde_Form */
+require_once 'Horde/Form.php';
+
+/** Horde_Form_Renderer */
+require_once 'Horde/Form/Renderer.php';
+
+/**
+ * The Skoli_CreateClassForm class provides the form for
+ * creating a class.
+ *
+ * @author  Martin Blumenthal <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..7a0a947
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Horde_Form for deleting classs.
+ *
+ * $Horde: skoli/lib/Forms/DeleteClass.php,v 0.1 $
+ *
+ * See the enclosed file LICENSE for license information (ASL). If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @package Skoli
+ */
+
+/** Variables */
+require_once 'Horde/Variables.php';
+
+/** Horde_Form */
+require_once 'Horde/Form.php';
+
+/** Horde_Form_Renderer */
+require_once 'Horde/Form/Renderer.php';
+
+/**
+ * The Skoli_DeleteClassForm class provides the form for
+ * deleting a class.
+ *
+ * @author  Martin Blumenthal <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..cea9c12
--- /dev/null
@@ -0,0 +1,176 @@
+<?php
+/**
+ * Horde_Form for editing classs.
+ *
+ * $Horde: skoli/lib/Forms/EditClass.php,v 0.1 $
+ *
+ * See the enclosed file LICENSE for license information (ASL). If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @package Skoli
+ */
+
+/** Variables */
+require_once 'Horde/Variables.php';
+
+/** Horde_Form */
+require_once 'Horde/Form.php';
+
+/** Horde_Form_Renderer */
+require_once 'Horde/Form/Renderer.php';
+
+/**
+ * The Skoli_EditClassForm class provides the form for
+ * editing a class.
+ *
+ * @author  Martin Blumenthal <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..6cc1f4d
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+/**
+ * Horde_Form for adding and updateing entries.
+ *
+ * $Horde: skoli/lib/Forms/Entry.php,v 0.1 $
+ *
+ * See the enclosed file LICENSE for license information (ASL). If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @package Skoli
+ */
+
+/** Variables */
+require_once 'Horde/Variables.php';
+
+/** Horde_Form */
+require_once 'Horde/Form.php';
+
+/** Horde_Form_Renderer */
+require_once 'Horde/Form/Renderer.php';
+
+/**
+ * The Skoli_EntryForm class provides the form for
+ * adding and updateing a new entry.
+ *
+ * @author  Martin Blumenthal <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..52f1ebc
--- /dev/null
@@ -0,0 +1,288 @@
+<?php
+/**
+ * Skoli School Class.
+ *
+ * $Horde: skoli/lib/School.php,v 0.1 $
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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 <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..838633a
--- /dev/null
@@ -0,0 +1,1083 @@
+<?php
+/**
+ * Sort by student or class name.
+ */
+define('SKOLI_SORT_NAME', 'name');
+
+/**
+ * Sort by last entry date.
+ */
+define('SKOLI_SORT_LASTENTRY', 'lastentry');
+
+/**
+ * Sort by mark average.
+ */
+define('SKOLI_SORT_SUMMARKS', 'summarks');
+
+/**
+ * Sort by absences.
+ */
+define('SKOLI_SORT_SUMABSENCES', 'sumabsences');
+
+/**
+ * Sort by semester start date.
+ */
+define('SKOLI_SORT_SEMESTERSTART', 'semesterstart');
+
+/**
+ * Sort by semester end date.
+ */
+define('SKOLI_SORT_SEMESTEREND', 'semesterend');
+
+/**
+ * Sort by grade.
+ */
+define('SKOLI_SORT_GRADE', 'grade');
+
+/**
+ * Sort by semester.
+ */
+define('SKOLI_SORT_SEMESTER', 'semester');
+
+/**
+ * Sort by location.
+ */
+define('SKOLI_SORT_LOCATION', 'location');
+
+/**
+ * Sort by category.
+ */
+define('SKOLI_SORT_CATEGORY', 'category');
+
+/**
+ * Sort by entry class name.
+ */
+define('SKOLI_SORT_CLASS', 'class');
+
+/**
+ * Sort by entry student name.
+ */
+define('SKOLI_SORT_STUDENT', 'student');
+
+/**
+ * Sort by entry date.
+ */
+define('SKOLI_SORT_DATE', 'date');
+
+/**
+ * Sort by entry type.
+ */
+define('SKOLI_SORT_TYPE', 'type');
+
+/**
+ * Sort in ascending order.
+ */
+define('SKOLI_SORT_ASCEND', 0);
+
+/**
+ * Sort in descending order.
+ */
+define('SKOLI_SORT_DESCEND', 1);
+
+/**
+ * Skoli Base Class.
+ *
+ * $Horde: skoli/lib/Skoli.php,v 0.1 $
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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 <tinu@humbapa.ch>
+ * @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 (file)
index 0000000..6d208e1
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Skoli base application file.
+ *
+ * $Horde: skoli/lib/base.php,v 0.1 $
+ *
+ * This file brings in all of the dependencies that every Skoli script will
+ * need, and sets up objects that all scripts use.
+ */
+
+// Check for a prior definition of HORDE_BASE (perhaps by an auto_prepend_file
+// definition for site customization).
+if (!defined('HORDE_BASE')) {
+    @define('HORDE_BASE', dirname(__FILE__) . '/../..');
+}
+
+// Load the Horde Framework core, and set up inclusion paths.
+require_once HORDE_BASE . '/lib/core.php';
+
+// Registry.
+$registry = &Registry::singleton();
+if (is_a(($pushed = $registry->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 (file)
index 0000000..5e6d331
--- /dev/null
@@ -0,0 +1 @@
+<?php define('SKOLI_VERSION', '0.1-cvs') ?>
\ No newline at end of file
diff --git a/skoli/list.php b/skoli/list.php
new file mode 100644 (file)
index 0000000..65e0804
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+/**
+ * $Horde: skoli/list.php,v 0.1 $
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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 <tinu@humbapa.ch>
+ */
+
+@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 '<div id="page">';
+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 (file)
index 0000000..a01e3e6
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 (file)
index 0000000..6bed399
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- $Horde: skoli/locale/de_DE/help.xml,v 0.1 $ -->
+<help>
+  <entry id="Overview">
+    <title>Skoli Ãœbersicht</title>
+    <heading>Was ist Skoli?</heading>
+    <para>
+      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.
+    </para>
+  </entry>
+</help>
diff --git a/skoli/locale/en_US/help.xml b/skoli/locale/en_US/help.xml
new file mode 100644 (file)
index 0000000..276f043
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version='1.0'?>
+<!-- $Horde: skoli/locale/en_US/help.xml,v 0.1 $ -->
+<help>
+
+<entry id="Overview">
+    <title>Skoli Overview</title>
+    <heading>What is Skoli?</heading>
+    <para>
+       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.
+    </para>
+</entry>
+
+</help>
diff --git a/skoli/po/README b/skoli/po/README
new file mode 100644 (file)
index 0000000..a985e94
--- /dev/null
@@ -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 (file)
index 0000000..4fff403
--- /dev/null
@@ -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.<br />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.<br />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).<br /"
+">NOTE: You also need to choose \"Custom settings\" above."
+msgstr ""
+"Eingabe eines eigenen Notenformats. Jede Note wird mit einem Komma getrennt "
+"(beste Note zuerst).<br />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 (file)
index 0000000..0995f61
--- /dev/null
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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.<br />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).<br /"
+">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 (file)
index 0000000..52c6dae
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/**
+ * $Horde: skoli/pref_api.php,v 0.1 $
+ *
+ * Copyright 2006-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+@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 '<ul id="app">';
+    foreach ($registry->listApps() as $app) {
+        echo '<li>' . htmlspecialchars($app) . '</li>';
+    }
+    echo '</ul>';
+    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 '<ul id="pref">';
+    foreach ($_prefs as $pref => $params) {
+        switch ($params['type']) {
+        case 'special':
+        case 'link':
+            break;
+
+        default:
+            echo '<li preftype="' . htmlspecialchars($params['type']) . '">' . htmlspecialchars($pref) . '</li>';
+        }
+    }
+    echo '</ul>';
+}
+
+/* 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 (executable)
index 0000000..3a42882
--- /dev/null
@@ -0,0 +1 @@
+Deny from all
diff --git a/skoli/scripts/sql/skoli.sql b/skoli/scripts/sql/skoli.sql
new file mode 100755 (executable)
index 0000000..3339617
--- /dev/null
@@ -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 (file)
index 0000000..3a646ed
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+/**
+ * $Horde: skoli/search.php,v 0.1 $
+ *
+ * Copyright 2000-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file LICENSE for license information (ASL).  If you
+ * did not receive this file, see http://www.horde.org/licenses/asl.php.
+ *
+ * @author Martin Blumenthal <tinu@humbapa.ch>
+ */
+
+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[] = '<option value="all">' .
+                       htmlspecialchars(_("All classes")) . "</option>\n";
+}
+foreach ($classes as $key=>$class) {
+    $class_options[] = '<option value="' . htmlspecialchars($key) . '"' . ($classid == $key ? ' selected="selected"' : '') . '>' .
+                       htmlspecialchars($class->get('name')) . "</option>\n";
+}
+
+$student_options = array();
+$student_options[] = '<option value="all">' .
+                     htmlspecialchars(_("All students")) . "</option>\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[] = '<option value="' . htmlspecialchars($address['student_id']) . '"' . ($studentid == $address['student_id'] ? ' selected="selected"' : '') . '">' .
+                         htmlspecialchars($address[$conf['addresses']['name_field']]) . "</option>\n";
+}
+if (!$foundstudent && $studentid != 'all') {
+    $actionID = '';
+    $studentid = '';
+    $_SESSION['skoli']['search_studentid'] = $studentid;
+}
+
+$type_options = array();
+$type_options[] = '<option value="all">' .
+                  htmlspecialchars(_("All Types")) . "</option>\n";
+if ($conf['objects']['allow_marks']) {
+    $type_options[] = '<option value="mark"' . ($type == 'mark' ? ' selected="selected"' : '') . '>' .
+                      htmlspecialchars(_("Marks")) . "</option>\n";
+}
+if ($conf['objects']['allow_objectives']) {
+    $type_options[] = '<option value="objective"' . ($type == 'objective' ? ' selected="selected"' : '') . '>' .
+                      htmlspecialchars(_("Objectives")) . "</option>\n";
+}
+if ($conf['objects']['allow_outcomes']) {
+    $type_options[] = '<option value="outcome"' . ($type == 'outcome' ? ' selected="selected"' : '') . '>' .
+                      htmlspecialchars(_("Outcomes")) . "</option>\n";
+}
+if ($conf['objects']['allow_absences']) {
+    $type_options[] = '<option value="absence"' . ($type == 'absence' ? ' selected="selected"' : '') . '>' .
+                      htmlspecialchars(_("Absences")) . "</option>\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 '<div id="page">';
+    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'), '<br />', true);
+            $entry['details'] = current(explode('<br />', $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 (file)
index 0000000..c8b7a05
--- /dev/null
@@ -0,0 +1,35 @@
+<h1 class="header">
+ <?php echo _("Manage Classes") ?>
+</h1>
+
+<div id="class-list-buttons">
+ <form method="get" action="create.php">
+<?php echo Util::formInput() ?>
+  <input type="submit" class="button" value="<?php echo _("Create a new Class") ?>" />
+ </form>
+</div>
+
+<?php if (count($sorted_classes) > 0): ?>
+<table summary="<?php echo _("Class List") ?>" cellspacing="0" id="class-list" class="striped sortable">
+ <thead>
+  <tr>
+   <th class="sortdown"><?php echo _("Class") ?></th>
+   <th class="class-list-icon nosort" colspan="<?php echo empty($conf['share']['no_sharing']) ? 3 : 2 ?>">&nbsp;</th>
+  </tr>
+ </thead>
+
+ <tbody>
+<?php foreach (array_keys($sorted_classes) as $class_id): ?>
+ <?php $class = $classes[$class_id] ?>
+  <tr>
+   <td><?php echo htmlspecialchars($class->get('name')) ?></td>
+   <td><a href="<?php echo Util::addParameter($edit_url_base, 'c', $class->getName()) ?>" title="<?php echo _("Edit") ?>"><?php echo $edit_img ?></a></td>
+<?php if (empty($conf['share']['no_sharing'])): ?>
+   <td><a onclick="return !popup(this.href);" href="<?php echo Util::addParameter($perms_url_base, 'share', $class->getName()) ?>" target="_blank" title="<?php echo _("Change Permissions") ?>"><?php echo $perms_img ?></a></td>
+<?php endif; ?>
+   <td><a href="<?php echo Util::addParameter($delete_url_base, 'c', $class->getName()) ?>" title="<?php echo _("Delete") ?>"><?php echo $delete_img ?></a></td>
+  </tr>
+<?php endforeach; ?>
+ </tbody>
+</table>
+<?php endif; ?>
diff --git a/skoli/templates/common-header.inc b/skoli/templates/common-header.inc
new file mode 100644 (file)
index 0000000..eac8f93
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+if (isset($language)) {
+    header('Content-type: text/html; charset=' . NLS::getCharset());
+    header('Vary: Accept-Language');
+}
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<!-- Skoli: Copyright 2004-2008 The Horde Project.  Skoli is under a Horde license. -->
+<!--    Horde Project: http://www.horde.org/ | Skoli: http://www.horde.org/horde/   -->
+<!--                Horde Licenses: http://www.horde.org/licenses/                  -->
+<?php echo !empty($language) ? '<html lang="' . strtr($language, '_', '-') . '">' : '<html>' ?>
+<head>
+<?php
+
+$page_title = $registry->get('name');
+if (!empty($title)) $page_title .= ' :: ' . $title;
+if (!empty($refresh_time) && ($refresh_time > 0) && !empty($refresh_url)) {
+    echo "<meta http-equiv=\"refresh\" content=\"$refresh_time;url=$refresh_url\">\n";
+}
+
+Horde::includeScriptFiles();
+
+$bc = Util::nonInputVar('bodyClass');
+if ($prefs->getValue('show_panel')) {
+    if ($bc) {
+        $bc .= ' ';
+    }
+    $bc .= 'rightPanel';
+}
+
+?>
+<title><?php echo htmlspecialchars($page_title) ?></title>
+<link href="<?php echo $GLOBALS['registry']->getImageDir()?>/favicon.ico" rel="SHORTCUT ICON" />
+<?php echo Horde::stylesheetLink('skoli') ?>
+<link href="<?php echo Horde::applicationUrl('themes/categoryCSS.php') ?>" rel="stylesheet" type="text/css" />
+</head>
+
+<body<?php if ($bc) echo ' class="' . $bc . '"' ?><?php if ($bi = Util::nonInputVar('bodyId')) echo ' id="' . $bi . '"'; ?>>
diff --git a/skoli/templates/data/export.inc b/skoli/templates/data/export.inc
new file mode 100644 (file)
index 0000000..575ef83
--- /dev/null
@@ -0,0 +1,34 @@
+<form method="post" name="skoli_exportform" id="skoli_exportform" action="<?php echo Horde::downloadUrl(_("class.csv"), null, Horde::applicationUrl('data.php')) ?>">
+<input type="hidden" name="actionID" value="export" />
+
+<h1 class="header">
+ <?php echo _("Export Classes") ?>
+</h1>
+
+<div class="item" style="padding:1em">
+ <?php echo Horde::label('exportID', _("Select the export format:")) ?><br />
+ <select name="exportID" id="exportID">
+  <option value="<?php echo EXPORT_CSV ?>"<?php echo Util::getFormData('exportID') == EXPORT_CSV ? ' selected="selected"' : '' ?>><?php echo _("Comma separated values (CSV)") ?></option>
+  <option value="<?php echo EXPORT_TSV ?>"<?php echo Util::getFormData('exportID') == EXPORT_TSV ? ' selected="selected"' : '' ?>><?php echo _("Tab separated values (TSV)") ?></option>
+ </select><br />
+
+<?php if (count($classes) > 1): ?>
+ <br />
+ <?php echo Horde::label('class', _("Select the class to export from:")) ?><br />
+ <select name="class" id="class" onchange="if (this.value) { document.skoli_exportform.actionID.value=''; RedBox.loading(); document.skoli_exportform.submit() }">
+  <?php echo implode('', $class_options) ?>
+ </select><br />
+<?php else: ?>
+ <input type="hidden" name="class" value="<?php echo key($classes) ?>" />
+<?php endif; ?>
+ <br />
+
+ <?php echo Horde::label('student', _("Select a student or the whole class to export:")) ?><br />
+ <select name="student" id="student">
+  <?php echo implode('', $student_options) ?>
+ </select><br />
+ <br />
+
+ <input type="submit" value="<?php echo _("Export") ?>" class="button" />
+</div>
+</form>
diff --git a/skoli/templates/entry/delete.inc b/skoli/templates/entry/delete.inc
new file mode 100644 (file)
index 0000000..d2db778
--- /dev/null
@@ -0,0 +1,10 @@
+<form action="entry.php" method="post">
+<?php echo Util::formInput() ?>
+<input type="hidden" name="actionID" value="delete" />
+<input type="hidden" name="entry" value="<?php echo $entry['object_id'] ?>" />
+<input type="hidden" name="view" value="<?php echo $viewName ?>" />
+<div class="headerbox" style="padding: 8px">
+ <p><?php echo _("Permanently delete this entry?") ?></p>
+ <input type="submit" class="button" name="delete" value="<?php echo _("Delete") ?>" />
+</div>
+</form>
diff --git a/skoli/templates/list/classes.inc b/skoli/templates/list/classes.inc
new file mode 100644 (file)
index 0000000..b3906ec
--- /dev/null
@@ -0,0 +1,48 @@
+<tr class="<?php echo $style ?>">
+  <td>
+    <?php
+    if ($class['_edit']) {
+        $label = _("New Entry");
+        echo Horde::link(Util::addParameter(Horde::applicationUrl('add.php'), 'class', $class['_id']), $label) . Horde::img('add.png', $label, null, $registry->getImageDir('skoli')) . '</a>';
+    }
+    ?>
+  </td>
+  <td>
+    <?php
+    if ($class['_edit']) {
+        $label = sprintf(_("Edit \"%s\""), $class['name']);
+        echo Horde::link(Util::addParameter(Horde::applicationUrl('classes/edit.php'), 'c', $class['_id']), $label) . Horde::img('edit.png', $label, null, $registry->getImageDir('horde')) . '</a>';
+    }
+    ?>
+  </td>
+<?php if (in_array('semesterstart', $class_columns)): ?>
+  <td><?php echo strftime($dateFormat, $class['start']) ?>&nbsp;</td>
+<?php endif; if (in_array('semesterend', $class_columns)): ?>
+  <td><?php echo strftime($dateFormat, $class['end']) ?>&nbsp;</td>
+<?php endif; ?>
+  <td>
+    <?php
+    $label = sprintf(_("View Entries in \"%s\""), $class['name']);
+    $params = array(
+        'actionID' => 'search',
+        'class'    => $class['_id']
+    );
+    echo Horde::link(Util::addParameter(Horde::applicationUrl('search.php'), $params), $label) . htmlspecialchars($class['name']) . '</a>';
+    ?>
+  </td>
+<?php if (in_array('lastentry', $student_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('summarks', $student_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('sumabsences', $student_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('grade', $class_columns)): ?>
+  <td><?php echo htmlspecialchars($class['grade']) ?>&nbsp;</td>
+<?php endif; if (in_array('semester', $class_columns)): ?>
+  <td><?php echo htmlspecialchars($class['semester']) ?>&nbsp;</td>
+<?php endif; if (in_array('location', $class_columns)): ?>
+  <td><?php echo htmlspecialchars($class['location']) ?>&nbsp;</td>
+<?php endif; if (in_array('category', $class_columns)): ?>
+  <td class="category<?php echo md5($class['category']) ?>"><?php echo isset($class['category']) ? htmlspecialchars($class['category']) : _("Unfiled") ?></td>
+<?php endif; ?>
+</tr>
diff --git a/skoli/templates/list/empty.inc b/skoli/templates/list/empty.inc
new file mode 100644 (file)
index 0000000..3e418aa
--- /dev/null
@@ -0,0 +1,3 @@
+<p class="text">
+ <em><?php echo _("There are no classes matching the current criteria.") ?></em>
+<p>
diff --git a/skoli/templates/list/footers.inc b/skoli/templates/list/footers.inc
new file mode 100644 (file)
index 0000000..a5ef8c9
--- /dev/null
@@ -0,0 +1,6 @@
+</tbody>
+</table>
+<div id="classes_empty">
+ <?php echo _("No classes match") ?>
+</div>
+</div>
diff --git a/skoli/templates/list/header.inc b/skoli/templates/list/header.inc
new file mode 100644 (file)
index 0000000..1cb21fe
--- /dev/null
@@ -0,0 +1,10 @@
+<div class="header leftAlign">
+ <?php echo htmlspecialchars($title) ?>
+ <a id="quicksearchL" href="<?php echo Horde::applicationUrl('search.php') ?>" title="<?php echo _("Search") ?>" onclick="$('quicksearchL').hide(); $('quicksearch').show(); $('quicksearchT').focus(); return false;"><?php echo Horde::img('search.png', _("Search"), '', $registry->getImageDir('horde')) ?></a>
+ <div id="quicksearch" style="display:none">
+  <input type="text" name="quicksearchT" id="quicksearchT" for="classes-body" empty="classes_empty" />
+  <small>
+   <a title="<?php echo _("Close Search") ?>" href="#" onclick="$('quicksearch').hide(); $('quicksearchT').value = ''; QuickFinder.filter($('quicksearchT')); $('quicksearchL').show(); return false;">X</a>
+  </small>
+ </div>
+</div>
diff --git a/skoli/templates/list/headers.inc b/skoli/templates/list/headers.inc
new file mode 100644 (file)
index 0000000..c6a9c42
--- /dev/null
@@ -0,0 +1,96 @@
+<?php $sortdirclass_class = $sortdir_class ? 'sortup' : 'sortdown' ?>
+<?php $sortdirclass_student = $sortdir_student ? 'sortup' : 'sortdown' ?>
+
+<script type="text/javascript">
+
+var PREFS_UPDATE_TIMEOUT;
+
+function table_sortCallback(tableId, column, sortDown)
+{
+    if (typeof PREFS_UPDATE_TIMEOUT != "undefined") {
+        window.clearTimeout(PREFS_UPDATE_TIMEOUT);
+    }
+
+    PREFS_UPDATE_TIMEOUT = window.setTimeout('doPrefsUpdate("' + column + '", "' + sortDown + '")', 300);
+}
+
+function doPrefsUpdate(column, sortDown)
+{
+    baseurl = '<?php echo Horde::applicationUrl('pref_api.php', true) ?>';
+    try {
+        column = column.substring(1);
+        prefscope = 'class';
+        if (column == 'lastentry' || column == 'comment') {
+            prefscope = 'student';
+        }
+        new Ajax.Request(baseurl, { parameters: { app: 'skoli', pref: 'sortby_' + prefscope, value: encodeURIComponent(column) } });
+        new Ajax.Request(baseurl, { parameters: { app: 'skoli', pref: 'sortdir_' + prefscope, value: encodeURIComponent(sortDown) } });
+        if (column == 'name') {
+            new Ajax.Request(baseurl, { parameters: { app: 'skoli', pref: 'sortby_student', value: encodeURIComponent(column) } });
+            new Ajax.Request(baseurl, { parameters: { app: 'skoli', pref: 'sortdir_student', value: encodeURIComponent(sortDown) } });
+        }
+    } catch (e) {}
+}
+
+</script>
+
+<table id="classes" cellspacing="0" class="sortable nowrap">
+<thead>
+ <tr class="item leftAlign">
+  <th width="2%" class="nosort">
+   <?php echo Horde::img('add.png', _("New Entry"), null, $registry->getImageDir('skoli')) ?>
+  </th>
+  <th width="2%" class="nosort">
+   <?php echo Horde::img('edit.png', _("Edit Class"), null, $registry->getImageDir('horde')) ?>
+  </th>
+<?php if (in_array('semesterstart', $class_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_SEMESTERSTART ?>"<?php if ($sortby_class == SKOLI_SORT_SEMESTERSTART) echo ' class="' . $sortdirclass_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_SEMESTERSTART)), _("Sort by Semester Start Date"), 'sortlink', '', '', _("Semester Start")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('semesterend', $class_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_SEMESTEREND ?>"<?php if ($sortby_class == SKOLI_SORT_SEMESTEREND) echo ' class="' . $sortdirclass_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_SEMESTEREND)), _("Sort by Semester End Date"), 'sortlink', '', '', _("Semester End")) ?>&nbsp;
+  </th>
+<?php endif; ?>
+  <th id="s<?php echo SKOLI_SORT_NAME ?>"<?php if ($sortby_class == SKOLI_SORT_NAME) echo ' class="' . $sortdirclass_class . '"' ?>>
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_NAME)), _("Sort by Name"), 'sortlink', '', '', _("Name")) ?>&nbsp;
+  </th>
+<?php if (in_array('lastentry', $student_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_LASTENTRY ?>"<?php if ($sortby_student == SKOLI_SORT_LASTENTRY) echo ' class="' . $sortdirclass_student . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_student', SKOLI_SORT_LASTENTRY)), _("Sort by Last Entry"), 'sortlink', '', '', _("Last Entry")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('summarks', $student_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_SUMMARKS ?>"<?php if ($sortby_student == SKOLI_SORT_SUMMARKS) echo ' class="' . $sortdirclass_student . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_student', SKOLI_SORT_SUMMARKS)), _("Sort by Mark"), 'sortlink', '', '', _("Mark average")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('sumabsences', $student_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_SUMABSENCES ?>"<?php if ($sortby_student == SKOLI_SORT_SUMABSENCES) echo ' class="' . $sortdirclass_student . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_student', SKOLI_SORT_SUMABSENCES)), _("Sort by Absences"), 'sortlink', '', '', _("Absences")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('grade', $class_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_GRADE ?>"<?php if ($sortby_class == SKOLI_SORT_GRADE) echo ' class="' . $sortdirclass_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_GRADE)), _("Sort by Grade"), 'sortlink', '', '', _("Grade")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('semester', $class_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_SEMESTER ?>"<?php if ($sortby_class == SKOLI_SORT_SEMESTER) echo ' class="' . $sortdirclass_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_SEMESTER)), _("Sort by Semester"), 'sortlink', '', '', _("Semester")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('location', $class_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_LOCATION ?>"<?php if ($sortby_class == SKOLI_SORT_LOCATION) echo ' class="' . $sortdirclass_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_LOCATION)), _("Sort by Location"), 'sortlink', '', '', _("Location")) ?>&nbsp;
+  </th>
+<?php endif; if (in_array('category', $class_columns)): ?>
+  <th id="s<?php echo SKOLI_SORT_CATEGORY ?>"<?php if ($sortby_class == SKOLI_SORT_CATEGORY) echo ' class="' . $sortdirclass_class . '"' ?> width="10%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby_class', SKOLI_SORT_CATEGORY)), _("Sort by Category"), 'sortlink', '', '', _("Category")) ?>&nbsp;
+   <?php
+   if (Auth::getAuth() && (!$GLOBALS['prefs']->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')) . '</a>';
+   }
+   ?>
+  </th>
+<?php endif; ?>
+ </tr>
+</thead>
+<tbody id="classes-body">
diff --git a/skoli/templates/list/students.inc b/skoli/templates/list/students.inc
new file mode 100644 (file)
index 0000000..c483559
--- /dev/null
@@ -0,0 +1,51 @@
+<tr class="<?php echo $style ?>">
+  <td>
+    <?php
+    if ($class['_edit']) {
+        $label = _("New Entry");
+        echo Horde::link(Util::addParameter(Horde::applicationUrl('add.php'), array('class'=>$class['_id'], 'student'=>$student['__key'])), $label) . Horde::img('add.png', $label, null, $registry->getImageDir('skoli')) . '</a>';
+    }
+    ?>
+  </td>
+  <td>
+    <?php
+    if ($registry->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')) . '</a>';
+    }
+    ?>
+  </td>
+<?php if (in_array('semesterstart', $class_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('semesterend', $class_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; ?>
+  <td>
+    <?php
+    $label = sprintf(_("View Entries for \"%s\""), $student[$conf['addresses']['name_field']]);
+    $params = array(
+        'actionID' => 'search',
+        'class'    => $class['_id'],
+        'student'  => $student['__key']
+    );
+    echo $treeIcon . '&nbsp;' . Horde::link(Util::addParameter(Horde::applicationUrl('search.php'), $params), $label) . htmlspecialchars($student[$conf['addresses']['name_field']]) . '</a>';
+    ?>
+  </td>
+<?php if (in_array('lastentry', $student_columns)): ?>
+  <td><?php echo isset($student['_lastentry']) ? strftime($dateFormat, $student['_lastentry']) : '' ?>&nbsp;</td>
+<?php endif; if (in_array('summarks', $student_columns)): ?>
+  <td style="text-align: center;"><?php echo $student['_summarks'] != '' ? $student['_summarks'] : '&nbsp;' ?></td>
+<?php endif; if (in_array('sumabsences', $student_columns)): ?>
+  <td style="text-align: center;"><?php echo $student['_sumabsences'][2] ?></td>
+<?php endif; if (in_array('grade', $class_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('semester', $class_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('location', $class_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; if (in_array('category', $class_columns)): ?>
+  <td>&nbsp;</td>
+<?php endif; ?>
+</tr>
diff --git a/skoli/templates/menu.inc b/skoli/templates/menu.inc
new file mode 100644 (file)
index 0000000..72cecc8
--- /dev/null
@@ -0,0 +1,4 @@
+<div id="menu">
+ <?php echo Skoli::getMenu('string') ?>
+</div>
+<?php $GLOBALS['notification']->notify(array('listeners' => 'status')) ?>
diff --git a/skoli/templates/panel.inc b/skoli/templates/panel.inc
new file mode 100644 (file)
index 0000000..3ea432c
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+Horde::addScriptFile('prototype.js', 'horde', true);
+Horde::addScriptFile('QuickFinder.js', 'horde', true);
+
+$current_user = Auth::getAuth();
+$my_classes = array();
+$shared_classes = array();
+foreach (Skoli::listClasses() as $id => $cl) {
+    if ($cl->get('owner') == $current_user) {
+        $my_classes[$id] = $cl;
+    } else {
+        $shared_classes[$id] = $cl;
+    }
+}
+?>
+
+<div id="pageControls">
+<script type="text/javascript">
+function sbarToggle()
+{
+    var pref_value;
+    if (Element.hasClassName(document.body, 'rightPanel')) {
+        pref_value = 0;
+        Element.removeClassName(document.body, 'rightPanel');
+    } else {
+        pref_value = 1;
+        Element.addClassName(document.body, 'rightPanel');
+    }
+
+    new Ajax.Request('<?php echo Horde::applicationUrl('pref_api.php', true) ?>', { parameters: { app: 'skoli', pref: 'show_panel', value: pref_value } });
+}
+</script>
+<a id="sbarShow" href="#" onclick="sbarToggle(); return false;"><?php echo _("Classes") ?></a>
+<a id="sbarHide" href="#" onclick="sbarToggle(); return false;"><?php echo _("Classes") ?></a>
+
+<div id="pageControlsInner">
+<form action="<?php echo Horde::selfUrl() ?>" method="post">
+<?php echo Util::formInput() ?>
+ <h4><?php echo _("Search for Classes:") ?></h4>
+ <p>
+  <input id="classSearch" type="text" for="myclasses,sharedclasses" />
+ </p>
+ <p>
+  <label><input type="checkbox" class="checkbox" name="show_students" value="1"<?php echo ($prefs->getValue('show_students') ? ' checked="checked"' : '') . ' /> ' . _("Show students?") ?></label>
+ </p>
+
+<?php if (Auth::getAuth()): ?>
+ <p>
+  <a href="<?php echo Horde::applicationUrl('classes/') ?>"><?php echo _("[Manage Classes]") ?></a>
+ </p>
+<?php endif; ?>
+
+<?php if (count($my_classes)): ?>
+ <h4><?php echo _("My Classes:") ?></h4>
+ <ul id="myclasses">
+<?php foreach ($my_classes as $id => $cl): ?>
+  <li><label><input type="checkbox" class="checkbox" name="display_class[]" value="<?php echo htmlspecialchars($id) ?>"<?php echo (in_array($id, $display_classes) ? ' checked="checked"' : '') . ' /> ' . htmlspecialchars($cl->get('name')) ?></label></li>
+<?php endforeach; ?>
+ </ul>
+<?php endif; ?>
+
+<?php if (count($shared_classes)): ?>
+ <h4><?php echo _("Shared Classes:") ?></h4>
+ <ul id="sharedclasses">
+<?php foreach ($shared_classes as $id => $cl): ?>
+  <li><label><input type="checkbox" class="checkbox" name="display_class[]" value="<?php echo htmlspecialchars($id) ?>"<?php echo (in_array($id, $display_classes) ? ' checked="checked"' : '') . ' /> [' . htmlspecialchars(Auth::removeHook($cl->get('owner'))) . '] ' . htmlspecialchars($cl->get('name')) ?></label></li>
+<?php endforeach; ?>
+ </ul>
+<?php endif; ?>
+
+ <p>
+  <input type="submit" value="<?php echo _("Save") ?>" class="button" />
+ </p>
+</form>
+
+</div>
+</div>
diff --git a/skoli/templates/search/criteria.inc b/skoli/templates/search/criteria.inc
new file mode 100644 (file)
index 0000000..b17bb8f
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/* Build the directory sources select widget. */
+
+?>
+<form name="skoli_searchform" action="search.php" method="get" onsubmit="RedBox.loading(); return true;">
+<input type="hidden" name="actionID" value="search" />
+
+<h1 class="header">
+ <?php echo _("Search Criterias") ?>
+</h1>
+
+<div class="text" style="padding:1em">
+<strong><?php echo Horde::label('class', _("Search in")) ?></strong>
+<?php if (count($classes) > 1): ?>
+ <select name="class" id="class" onchange="if (this.value) { document.skoli_searchform.actionID.value='<?php echo $actionID ?>'; RedBox.loading(); document.skoli_searchform.submit() }">
+  <?php echo implode('', $class_options) ?>
+ </select>
+<?php else: ?>
+ <strong><?php echo current($classes)->get('name') ?></strong>
+<?php endif; ?>
+<strong>&nbsp;<?php echo Horde::label('student', _("and")) ?></strong>
+<select name="student" id="student">
+ <?php echo implode('', $student_options) ?>
+</select>
+<strong>&nbsp;<?php echo Horde::label('type', _("for")) ?></strong>
+<select name="type" id="type">
+ <?php echo implode('', $type_options) ?>
+</select><br />
+
+<br />
+
+<table cellspacing="0" cellpadding="0" width="100%">
+ <tr>
+  <td>
+   <strong><?php echo Horde::label('stext', _("and for Entries with:")) ?></strong>
+   <input type="text" size="30" id="stext" name="stext" value="<?php echo htmlspecialchars($search) ?>" />
+  <td style="text-align: center;">
+   <input type="submit" class="button" name="search" value="<?php echo _("Search") ?>" />
+   <input type="reset" class="button" name="reset" value="<?php echo _("Reset to Defaults") ?>" />
+  </td>
+ </tr>
+</table>
+</div>
+</form>
diff --git a/skoli/templates/search/empty.inc b/skoli/templates/search/empty.inc
new file mode 100644 (file)
index 0000000..3121ac2
--- /dev/null
@@ -0,0 +1,3 @@
+<p class="text">
+ <em><?php echo _("There are no entries matching the current criteria.") ?></em>
+<p>
diff --git a/skoli/templates/search/entries.inc b/skoli/templates/search/entries.inc
new file mode 100644 (file)
index 0000000..7d85afe
--- /dev/null
@@ -0,0 +1,27 @@
+<tr class="<?php echo $style ?>">
+  <td>
+    <?php
+    if ($entry['_edit']) {
+        $label = _("Edit Entry");
+        $params = array(
+            'view'  => 'EditEntry',
+            'entry' => $entry['_id']
+        );
+        echo Horde::link(Util::addParameter(Horde::applicationUrl('entry.php'), $params), $label) . Horde::img('edit.png', $label, null, $registry->getImageDir('horde')) . '</a>';
+    }
+    ?>
+  </td>
+  <td><?php echo htmlspecialchars($entry['class']) ?>&nbsp;</td>
+  <td>
+    <?php 
+    $params = array(
+        'view'  => 'Entry',
+        'entry' => $entry['_id']
+    );
+    echo Horde::link(Util::addParameter(Horde::applicationUrl('entry.php'), $params), _("View Entry")) . htmlspecialchars($entry['student']) . '</a>&nbsp;';
+    ?>
+ </td>
+  <td><?php echo htmlspecialchars($entry['date']) ?>&nbsp;</td>
+  <td><?php echo htmlspecialchars($entry['typename']) ?>&nbsp;</td>
+  <td><?php echo htmlspecialchars($entry['details']) ?>&nbsp;</td>
+</tr>
diff --git a/skoli/templates/search/footers.inc b/skoli/templates/search/footers.inc
new file mode 100644 (file)
index 0000000..bafb71e
--- /dev/null
@@ -0,0 +1,6 @@
+</tbody>
+</table>
+<div id="entries_empty">
+ <?php echo _("No entries match") ?>
+</div>
+</div>
diff --git a/skoli/templates/search/header.inc b/skoli/templates/search/header.inc
new file mode 100644 (file)
index 0000000..585dab5
--- /dev/null
@@ -0,0 +1,11 @@
+<br />
+<div class="header leftAlign">
+ <?php echo htmlspecialchars(_("Search Result")) ?>
+ <a id="quicksearchL" href="<?php echo Horde::applicationUrl('search.php') ?>" title="<?php echo _("Search") ?>" onclick="$('quicksearchL').hide(); $('quicksearch').show(); $('quicksearchT').focus(); return false;"><?php echo Horde::img('search.png', _("Search"), '', $registry->getImageDir('horde')) ?></a>
+ <div id="quicksearch" style="display:none">
+  <input type="text" name="quicksearchT" id="quicksearchT" for="entries-body" empty="entries_empty" />
+  <small>
+   <a title="<?php echo _("Close Search") ?>" href="#" onclick="$('quicksearch').hide(); $('quicksearchT').value = ''; QuickFinder.filter($('quicksearchT')); $('quicksearchL').show(); return false;">X</a>
+  </small>
+ </div>
+</div>
diff --git a/skoli/templates/search/headers.inc b/skoli/templates/search/headers.inc
new file mode 100644 (file)
index 0000000..fc9cb5a
--- /dev/null
@@ -0,0 +1,50 @@
+<?php $sortdir_class = $sortdir ? 'sortup' : 'sortdown' ?>
+
+<script type="text/javascript">
+
+var PREFS_UPDATE_TIMEOUT;
+
+function table_sortCallback(tableId, column, sortDown)
+{
+    if (typeof PREFS_UPDATE_TIMEOUT != "undefined") {
+        window.clearTimeout(PREFS_UPDATE_TIMEOUT);
+    }
+
+    PREFS_UPDATE_TIMEOUT = window.setTimeout('doPrefsUpdate("' + column + '", "' + sortDown + '")', 300);
+}
+
+function doPrefsUpdate(column, sortDown)
+{
+    baseurl = '<?php echo Horde::applicationUrl('pref_api.php', true) ?>';
+    try {
+        column = column.substring(1);
+        new Ajax.Request(baseurl, { parameters: { app: 'skoli', pref: 'sortby', value: encodeURIComponent(column) } });
+    } catch (e) {}
+}
+
+</script>
+
+<table id="entries" cellspacing="0" class="sortable nowrap">
+<thead>
+ <tr class="item leftAlign">
+  <th width="2%" class="nosort">
+   <?php echo Horde::img('edit.png', _("Edit Class"), null, $registry->getImageDir('horde')) ?>
+  </th>
+  <th id="s<?php echo SKOLI_SORT_CLASS ?>"<?php if ($sortby == SKOLI_SORT_CLASS) echo ' class="' . $sortdir_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby', SKOLI_SORT_CLASS)), _("Sort by Class"), 'sortlink', '', '', _("Class")) ?>&nbsp;
+  </th>
+  <th id="s<?php echo SKOLI_SORT_STUDENT ?>"<?php if ($sortby == SKOLI_SORT_STUDENT) echo ' class="' . $sortdir_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby', SKOLI_SORT_STUDENT)), _("Sort by Student Name"), 'sortlink', '', '', _("Student")) ?>&nbsp;
+  </th>
+  <th id="s<?php echo SKOLI_SORT_DATE ?>"<?php if ($sortby == SKOLI_SORT_DATE) echo ' class="' . $sortdir_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby', SKOLI_SORT_DATE)), _("Sort by Date"), 'sortlink', '', '', _("Date")) ?>&nbsp;
+  </th>
+  <th id="s<?php echo SKOLI_SORT_TYPE ?>"<?php if ($sortby == SKOLI_SORT_TYPE) echo ' class="' . $sortdir_class . '"' ?> width="2%">
+   <?php echo Horde::widget(Horde::applicationUrl(Util::addParameter($baseurl, 'sortby', SKOLI_SORT_TYPE)), _("Sort by Type"), 'sortlink', '', '', _("Type")) ?>&nbsp;
+  </th>
+  <th class="nosort">
+   <?php echo _("Entry") ?>&nbsp;
+  </th>
+ </tr>
+</thead>
+<tbody id="entries-body">
diff --git a/skoli/themes/categoryCSS.php b/skoli/themes/categoryCSS.php
new file mode 100644 (file)
index 0000000..fb45e11
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * $Horde: skoli/themes/categoryCSS.php,v 0.1 $
+ *
+ * Copyright 1999-2008 The Horde Project (http://www.horde.org/)
+ *
+ * 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 Chuck Hagenbuch <chuck@horde.org>
+ */
+
+@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 (file)
index 0000000..a2f261d
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 (file)
index 0000000..2b646f9
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 (file)
index 0000000..c3b82e4
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 (file)
index 0000000..3217046
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 (file)
index 0000000..263e356
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 (file)
index 0000000..35218b3
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 (file)
index 0000000..94c47d4
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 (file)
index 0000000..abba527
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 (file)
index 0000000..3da6e7e
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 (file)
index 0000000..a154237
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 (file)
index 0000000..db45880
--- /dev/null
@@ -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;
+}