Seth is a "idea" organiser. master
authorKristian Kræmmer Nielsen <jkkn@tv2.dk>
Sun, 15 Aug 2010 01:51:47 +0000 (03:51 +0200)
committerKristian Kræmmer Nielsen <jkkn@tv2.dk>
Sun, 15 Aug 2010 01:51:47 +0000 (03:51 +0200)
It builds of a princip of keeping everything is simple objects and then
organising these using their properties.

This is done by allowing to create filters on the fly.
Dragging between groups and filters are allowed and will automatically
assign and attach properties to the elements.

Hope you will have fun using this interface :-)

/Kristian Kræmmer

23 files changed:
inc/Seth/Controller/Filter.php [new file with mode: 0644]
inc/Seth/Controller/Group.php [new file with mode: 0644]
inc/Seth/Controller/List.php [new file with mode: 0644]
inc/Seth/Controller/Page.php [new file with mode: 0644]
inc/Seth/Controller/View.php [new file with mode: 0644]
inc/Seth/Element/Filter.php [new file with mode: 0755]
inc/Seth/Model/Element.php [new file with mode: 0755]
inc/Seth/Model/Element/Descriptions.php [new file with mode: 0755]
inc/Seth/Model/Element/FilterOrder.php [new file with mode: 0644]
inc/Seth/Model/Filter.php [new file with mode: 0755]
inc/Seth/Model/Group.php [new file with mode: 0755]
inc/Seth/Model/View.php [new file with mode: 0755]
opdatering/addfilter.php [new file with mode: 0644]
opdatering/css/seth.css [new file with mode: 0644]
opdatering/edit.php [new file with mode: 0644]
opdatering/editfilter.php [new file with mode: 0644]
opdatering/index.php [new file with mode: 0644]
opdatering/js/group.js [new file with mode: 0644]
opdatering/js/seth.js [new file with mode: 0644]
opdatering/js/view.js [new file with mode: 0644]
opdatering/list.php [new file with mode: 0644]
opdatering/views.php [new file with mode: 0644]
scripts/importer/access.php [new file with mode: 0644]

diff --git a/inc/Seth/Controller/Filter.php b/inc/Seth/Controller/Filter.php
new file mode 100644 (file)
index 0000000..03efe4a
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+require_once 'Seth/Model/Filter.php';
+
+/**
+ * Controller for selecting/creating/editing a filter
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ */
+class Seth_Controller_Filter {
+
+    private $_groupId = null;
+
+    public function setGroupId($id) {
+        $this->_groupId = intval($id);
+    }
+
+    private function _printSelect($id, $values, $selected=null) {
+        printf('<select id="%s" name="%s">', htmlentities($id), htmlentities($id));
+        foreach ($values as $key => $value) {
+            printf('<option value="%d"%s>%s</option>',
+                $key,
+                (!is_null($selected) && $selected==$key) ? ' selected="selected"' : '',
+                htmlentities($value));
+        }
+        echo '</select>';
+    }
+
+    /** Return all groups
+    */
+    public static function getGroups() {
+        $db = DB_PDO::get('seth');
+        $groups = $db->getHash('SELECT id, title FROM `group` ORDER BY title',
+            array(), 'id', 'title');
+        return $groups;
+    }
+
+    /** Return all filters for a given group
+    */
+    public static function getFilters($groupId, Seth_Model_Filter $exclude) {
+        $db = DB_PDO::get('seth');
+        $filters = $db->getHash('SELECT id, title FROM filter WHERE group_id=? ORDER BY title',
+            array($groupId), 'id', 'title');
+        $filters = array(''=>'') + $filters;
+        if ($exclude && !$exclude->isNew()) {
+            unset($filters[$exclude->getId()]);
+        }
+        return $filters;
+    }
+
+    public function printGroupSelect() {
+        $groups = self::getGroups();
+        echo '<form method="GET"/>';
+        $this->_printSelect('group_id', $groups, $this->_groupId);
+        echo '</form>' . "\n";
+    }
+
+    /**
+     * Method inserts "edit" links into I2_GUI_List for filters
+     */
+    static public function insertEditLinks(array &$row, Seth_Model_Filter $filter) {
+        $row['title'] = sprintf('<a href="javascript:parent.SethManager.addListAndCloseDialog(%d,\'%s\')">%s</a>',
+                $filter->getId(), rawurlencode($filter->getTitle()),
+                htmlentities($filter->getTitle()));
+        $row['action'] =
+            '<div style="width:100%; text-align:right">'.
+            sprintf('<a href="editfilter.php?id=%d">Redigér</a> | ',
+                $filter->getId()).
+            sprintf('<a href="#" onclick="SethManager.removeFilter(%d, \'%s\'); event.stopPropagation();">Slet</a>',
+            $filter->getId(), htmlentities(rawurlencode($filter->getTitle())))
+            .'</div>';
+    }
+
+    static private function _param($key) {
+        return isset($_POST[$key]) ? $_POST[$key] : null;
+    }
+
+    /**
+     * Delete filter
+     */
+    private static function _remove($id) {
+        $delete = new Seth_Model_Filter(intval($id));
+        if (!$delete->isNew()) {
+            $delete->delete();
+        }
+        return array('ok'=>true);
+    }
+
+    /**
+     * Process callbacks
+     */
+    static public function process() {
+        $action = self::_param('action');
+
+        $rtn = null;
+        switch ($action) {
+            case 'remove':
+                $rtn = self::_remove(self::_param('id'));
+            default:
+                break;
+        }
+        
+        if (!is_null($rtn)) {
+            header('Content-Type: application/json');
+            echo json_encode($rtn);
+            die();
+        }
+    }
+
+}
+
+
+?>
diff --git a/inc/Seth/Controller/Group.php b/inc/Seth/Controller/Group.php
new file mode 100644 (file)
index 0000000..f145db1
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Controller for the group list
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+ */
+class Seth_Controller_Group {
+
+    // Insert actions into I2_GUI_List
+    static public function insertActions(array &$row, Seth_Model_Group $group) {
+        $row['actions'] =
+            '<div style="width:100%; text-align:right">'.
+            sprintf('<a href="#" onclick="SethGroup.rename(%d, \'%s\'); event.stopPropagation();">Omdøb</a> | ',
+            $group->getId(), htmlentities(rawurlencode($group->getTitle()))) .
+            sprintf('<a href="#" onclick="SethGroup.remove(%d, \'%s\'); event.stopPropagation();">Slet</a>',
+            $group->getId(), htmlentities(rawurlencode($group->getTitle()))) .
+            '</div>';
+    }
+
+    /* Create group */
+    private static function _new($title) {
+        $new = new Seth_Model_Group();
+        $new->setTitle($title);
+        $new->save();
+        return array('ok'=>true);
+    }
+
+    /* Rename group */
+    private static function _rename($id, $title) {
+        $rename = new Seth_Model_Group(intval($id));
+        if ($rename->isNew()) {
+            return array('err'=>'Group no longer exists');
+        }
+        $rename->setTitle($title);
+        $rename->save();
+        return array('ok'=>true);
+    }
+
+    /* Delete group */
+    private static function _remove($id) {
+        $delete = new Seth_Model_Group(intval($id));
+        if (!$delete->isNew()) {
+            $delete->delete();
+        }
+        return array('ok'=>true);
+    }
+
+
+    static private function _param($key) {
+        return isset($_POST[$key]) ? $_POST[$key] : null;
+    }
+
+    static public function process() {
+        $action = self::_param('action');
+
+        $rtn = null;
+        switch ($action) {
+            case 'new':
+                $rtn = self::_new(self::_param('title'));
+                break;
+            case 'rename':
+                $rtn = self::_rename(self::_param('id'), self::_param('title'));
+                break;
+            case 'remove':
+                $rtn = self::_remove(self::_param('id'));
+            default:
+                break;
+        }
+        
+        if (!is_null($rtn)) {
+            header('Content-Type: application/json');
+            echo json_encode($rtn);
+            die();
+        }
+    }
+
+}
diff --git a/inc/Seth/Controller/List.php b/inc/Seth/Controller/List.php
new file mode 100644 (file)
index 0000000..972cefe
--- /dev/null
@@ -0,0 +1,397 @@
+<?php
+require_Once 'Seth/Model/Element.php';
+require_Once 'Seth/Model/Element/Descriptions.php';
+require_Once 'Seth/Model/Filter.php';
+require_Once 'Seth/Model/Element/FilterOrder.php';
+require_Once 'Seth/Model/View.php';
+
+/**
+ * Contains all list callback actions for Seth
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+ */
+class Seth_Controller_List {
+
+    private $_viewId;
+
+    /**
+      * Initializes new List controller
+      *
+      * @param integer $view View we are displaying
+      */
+    public function __construct($viewId) {
+        $this->_viewId = $viewId;
+    }
+
+    /**
+      * Create new element
+      * @param $obj array('title'=>, 'filter_id'=>, 'group_defined_as'=>)
+      */
+    private function _newItem(array $obj) {
+        // create the new object
+        $elm = new Seth_Model_Element();
+        // fill in the entered fields from the interface
+        $elm->setTitle(utf8_decode($obj['title']));
+
+        /* We must also match the group parameters */
+        $gd = $obj['group_defined_as'];
+        if ($gd) {
+            foreach (unserialize(utf8_decode($gd)) as $key => $value) {
+                $elm->set($key, $value);
+            }
+        }
+
+        $filter = new Seth_Model_Filter(intval($obj['filter_id']));
+        if ($filter->isNew()) {
+            return array('err'=>'Filter no longer exists');
+        }
+
+        // Always set group id
+        $elm->setGroupId($filter->getGroupId());
+        $elm->save();
+
+        // Set order index to be at bottom at the beginning
+        $order = new Seth_Model_Element_FilterOrder($filter->getId(), $elm->getId());
+        $order->setOrderIndex(9223372036854775807); // max bigint
+        $order->save();
+
+        return array('new'=>true, 'list' => $this->_getList($filter));
+    }
+
+    /** Format a groupset title a bit nice */
+    static private function _formatGroupTitle($group) {
+        $title = '';
+        foreach ($group as $key => $value) {
+            if ($title != '') {
+                $title .= '; ';
+            }
+            // For now change field name by splitting _ and uppercasing first letters
+            $name = array();
+            foreach (split('_', $key) as $part) {
+                $name[] = ucfirst($part);
+            }
+            $name = join(' ', $name);
+            if (is_null($value)) {
+                $value = '(ikke sat)';
+            }
+            $title .= htmlentities(ucfirst($name)) . ': ' . htmlentities($value);
+        }
+        return utf8_encode($title);
+    }
+
+    /**
+     * Place here warning (NOT IMPLEMENTED)
+     */
+    static private function _formatPlaceHere($definition) {
+        return null;
+    }
+
+    /**
+     * Adds some information for mouseover
+     */
+    static private function _formatDescription(Seth_Model_Element $item,
+                                               $desc) {
+        // TODO: Implement a overlay for mouseover instead of <title/>:
+
+        $text = '';
+        if ($desc && $us=$desc->getUserStory()) {
+            $text .= $desc->getUserStory() . " \n\n";
+        }
+        $text .= 'Prioritet: ' . $item->getPriority() . " \n";
+        if ($cp=$item->getContactPerson()) {
+            $text .= 'Kontakt: ' . $cp . " \n";
+        }
+        if ($ar=$item->getArea()) {
+            $text .= 'Område: ' . $ar . " \n";
+        }
+        if ($as=$item->getAssignee()) {
+            $text .= 'Tildelt: ' . $as . " \n";
+        }
+
+        return $text;
+    }
+
+    /**
+     * Returns a group definition (including filter requirements)
+     *
+     * All = requirements from filter are copied, if other type
+     * exists we will only allow creating/drag if group overrides them
+     *
+     * This is used for new elements created in the group/dragged there
+     * @param array $group Group elements optional
+     * @return string Serializd array that defines group or null if not able to do so
+     */
+    static private function _getGroupDefinition(Seth_Model_filter $filter, array $group = array()) {
+        $define = array();
+        $conds = $filter->getFilterByIncludeInherited();
+        if ($conds) {
+            $conds = Seth_Model_Filter::cleanFilter($conds);
+            foreach ($conds as $cond) {
+                // all
+                if ($cond['cond'] == '=') {
+                    $define[$cond['field']] = $cond['value'];
+                } elseif ($cond['cond'] == 'IS NULL') {
+                    $define[$cond['field']] = null;
+                } else {
+                    // must be in group
+                    if (!isset($group[$cond['field']])) {
+                        return null; // we cannot guess this value - no drag/create here
+                    }
+                }
+            }
+        }
+        $define += $group; // add group by definition
+
+        $define['group_id'] = $filter->getGroupId();
+
+        return utf8_encode(serialize($define));
+    }
+
+    /** Returns a filtered list by id
+     * @param mixed $listid Id or Seth_Model_Filter object
+     */
+    static private function _getList($listid) {
+        // Start by finding the filter object
+        if (!$listid instanceof Seth_Model_Filter) {
+            $filter = new Seth_Model_Filter(intval($listid));
+            if ($filter->isNew()) {
+                // failed
+                return array('err'=>'Filter/view no longer exists');
+            }
+        } else {
+            $filter = $listid;
+        }
+
+        $items = Seth_Model_Element::find($filter->getQueryConditions());
+        $desc  = Seth_Model_Element_Descriptions::multiple(
+                    Object_Util::extract($items, 'id'), ORM::INDEX_BY_KEY);
+    
+        $groupby = $filter->getGroupByIncludeInherited();
+
+        $groups = array();
+        $curGroup = null;
+
+        // If we have no elements or are not grouping, items will appear in a
+        // standard group:
+        $define = self::_getGroupDefinition($filter);
+        $current = array(
+            'title' => utf8_encode(empty($conds) ? 'Alle elementer' : 'Filtrede elementer'),
+            'when_placing_here' => self::_formatPlaceHere($define),
+            'defined_as' => $define,
+            'elements' => array());
+
+        // Construct our response by grouping elements of the same group
+        foreach ($items as $no => $item) {
+            if ($groupby) {
+                $thisGroup = array();
+                foreach ($groupby as $gby) {
+                    $thisGroup[$gby] = $item->get($gby);
+                }
+
+                if ($thisGroup !== $curGroup) {
+                    if (!is_null($curGroup)) $groups[] = $current;
+                    $define = self::_getGroupDefinition($filter, $thisGroup);
+                    $current = array(
+                        'title' => self::_formatGroupTitle($thisGroup),
+                        'when_placing_here' => self::_formatPlaceHere($define),
+                        'defined_as' => $define,
+                        'elements' => array());
+                    $curGroup = $thisGroup;
+                }
+            }
+
+            $current['elements'][] = array(
+                'title' => utf8_encode($item->getTitle()),
+                'id'    => $item->getId(),
+                'description' => utf8_encode(self::_formatDescription($item,
+                    isset($desc[$no]) ? $desc[$no] : null)));
+        }
+
+        $groups[] = $current; // always at least one empty group
+        
+        return array('groups'=>$groups);
+    }
+
+    /**
+     * Stores new tab order (order of filters)
+     *
+     * @param integer $ViewId view_id
+     * @param array $order Ordered array of filter_ids
+     */
+    private function _setFilterOrder($viewId, $order) {
+        if (!is_array($order)) {
+            $order = array(); // empty view
+        }
+        $view = new Seth_Model_View($viewId);
+        if ($view->isNew()) {
+            // failed
+            return array('err'=>'Filter/view no longer exists');
+        }
+        $view->setFilterOrder($order);
+        $view->save();
+        return array('save'=>true);
+    }
+
+    /**
+     * Load filter order
+     *
+     * @param $viewId integer view id
+     * @return array of oredered filter ids as JSON
+     */
+    public function getFilterOrder(Seth_Model_View $view) {
+        // Get titles
+        $filters = ORM::multiple(Seth_Model_Filter::TYPE, $view->getFilterOrder());
+        $titles = array();
+        foreach ($filters as $filter) {
+            $titles[] = array('id' => $filter->getId(), 'title' => utf8_encode($filter->getTitle())); // JSON must be UTF-8
+        }
+        return json_encode($titles);
+    }
+
+    /**
+     * Move an element to another filtered list
+     *
+     * @param integer $elementId Element to move
+     * @param integer $filterId Id of filter to obey
+     */
+    static private function _moveToList($elementId, $filterId) {
+        $element = new Seth_Model_Element($elementId);
+        if ($element->isNew()) {
+            return array('err'=>'Element no longer exists');
+        }
+        $filter = new Seth_Model_Filter($filterId);
+        if ($filter->isNew()) {
+            return array('err'=>'Filter no longer exists');
+        }
+
+        // just copy all = rows from the filter - fail if there is non = conditions
+        $conds = $filter->getFilterByIncludeInherited();
+        if ($conds) {
+            $conds = Seth_Model_Filter::cleanFilter($conds);
+            foreach ($conds as $cond) {
+                if ($cond['cond'] != '=' && $cond['cond'] != 'IS NULL') {
+                    return array('err'=>'Kan ikke flyttes her, filtret benytter andet end "=" krav!');
+                }
+                if ($cond['cond'] == 'IS NULL') {
+                    $element->set($cond['field'], null);
+                } else {
+                    $element->set($cond['field'], $cond['value']);
+                }
+            }
+        }
+
+        // always copy group id
+        $element->setGroupId($filter->getGroupId());
+
+        // save
+        $element->save();
+
+        return array('save'=>true);
+    }
+
+    /**
+     * Moves an element to another location and/or group
+     *
+     * @param array $obj Array of ('element_id'=>, 'above_id'=>, 'below_id'=>, 'group_defined_as'=>, 'filter_id'=>)
+     */
+    private function _moveElement(array $obj) {
+
+        // 1) First check if all elements are alive
+        $element = new Seth_Model_Element(intval($obj['element_id']));
+        if ($element->isNew()) {
+            return array('err'=>'Element no longer exists');
+        }
+
+        $filter = new Seth_Model_Filter(intval($obj['filter_id']));
+        if ($filter->isNew()) {
+            return array('err'=>'Filter no longer exists');
+        }
+
+        // 2) Check and find above/below objects
+        $above = $below = null;
+        if (!empty($obj['above_id'])) {
+            $above = new Seth_Model_Element(intval($obj['above_id']));
+            if ($above->isNew()) $above = null;
+        }
+
+        if (!empty($obj['below_id'])) {
+            $below = new Seth_Model_Element(intval($obj['below_id']));
+            if ($below->isNew()) $below = null;
+        }
+
+        /* 3) Move element into the right group */
+        $gd = $obj['group_defined_as'];
+        if ($gd) {
+            foreach (unserialize(utf8_decode($gd)) as $key => $value) {
+                $element->set($key, $value);
+            }
+        }
+        $element->save();
+
+        /** 3) The differcult part - try to place it between the two elements
+          *    We only change the order_index in the filter many-to-many relation
+          */
+        Seth_Model_Element_FilterOrder::placeBetween(
+            $filter, $element, $above, $below);
+
+        return array('moved'=>true, 'list' => $this->_getList($filter));
+    }
+
+    /**
+     * Delete an element
+     * 
+     * @param integer $id Element id
+     */
+    static private function _deleteElement($id) {
+        $element = new Seth_Model_Element($id);
+        if ($element->isNew()) {
+            return array('err'=>'Element var allerede slettet.');
+        }
+        $element->delete();
+        return array('delete'=>true);
+    }
+
+    private function _param($key) {
+        return isset($_POST[$key]) ? $_POST[$key] : null;
+    }
+
+    public function process() {
+        $action = $this->_param('action');
+
+        $rtn = null;
+        switch ($action) {
+            case 'new':
+                // create new
+                $rtn = $this->_newItem($this->_param('obj'));
+                break;
+            case 'get':
+                // get a filtered list
+                $rtn = array('list' => $this->_getList($this->_param('listid')));
+                break;
+            case 'setfilterorder':
+                // store new filter order in view
+                $rtn = $this->_setFilterOrder($this->_param('view_id'), $this->_param('order'));
+                break;
+            case 'move_to_list';
+                // moves item to another list
+                $rtn = $this->_moveToList($this->_param('element_id'), $this->_param('filter_id'));
+                break;
+            case 'move_element';
+                // moves item to another location and/or group
+                $rtn = $this->_moveElement($this->_param('obj'));
+                break;
+            case 'delete_element';
+                // moves item to another list
+                $rtn = $this->_deleteElement($this->_param('element_id'));
+            default:
+                break;
+        }
+        
+        if (!is_null($rtn)) {
+            header('Content-Type: application/json');
+            echo json_encode($rtn);
+            die();
+        }
+    }
+
+}
diff --git a/inc/Seth/Controller/Page.php b/inc/Seth/Controller/Page.php
new file mode 100644 (file)
index 0000000..32d2817
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Makes up the general header for a Seth page
+ * @author Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+ */
+class Seth_Controller_Page {
+
+    static public function printTop($head='') {
+        $vendor = getSite('vendor');
+        echo <<<EOH
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
+<html xmlns="http://www.w3.org/1999/xhtml"> 
+<head>
+    <title>Seth - projekt organizer</title>
+    <link type="text/css" href="http://$vendor/css/jquery-ui/sunny/jquery-ui.css" rel="stylesheet"/>
+    <script type="text/javascript" src="http://$vendor/js/jquery.js"></script>
+    <script type="text/javascript" src="http://$vendor/js/jquery-ui.js"></script>
+    <script type="text/javascript" src="js/seth.js"></script>
+    <link type="text/css" href="css/seth.css" rel="stylesheet"/>
+    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+    $head
+</head>
+<body>
+EOH;
+    }
+
+    static public function printBottom() {
+        echo <<<EOF
+            </body>
+            </html>
+EOF;
+    }
+
+}
diff --git a/inc/Seth/Controller/View.php b/inc/Seth/Controller/View.php
new file mode 100644 (file)
index 0000000..589fb10
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Controller for the view list
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+ */
+class Seth_Controller_View {
+
+    // Insert actions into I2_GUI_List
+    static public function insertActions(array &$row, Seth_Model_View $view) {
+        $row['actions'] =
+            '<div style="width:100%; text-align:right">'.
+            sprintf('<a href="#" onclick="SethView.rename(%d, \'%s\'); event.stopPropagation();">Omdøb</a> | ',
+            $view->getId(), htmlentities(rawurlencode($view->getTitle()))) .
+            sprintf('<a href="#" onclick="SethView.remove(%d, \'%s\'); event.stopPropagation();">Slet</a>',
+            $view->getId(), htmlentities(rawurlencode($view->getTitle()))) .
+            '</div>';
+    }
+
+    /* Create view */
+    private static function _new($title, $groupId) {
+        $new = new Seth_Model_View();
+        $new->setTitle($title);
+        $new->setGroupId(intval($groupId));
+        $new->save();
+        return array('ok'=>true);
+    }
+
+    /* Rename view */
+    private static function _rename($id, $title) {
+        $rename = new Seth_Model_View(intval($id));
+        if ($rename->isNew()) {
+            return array('err'=>'View no longer exists');
+        }
+        $rename->setTitle($title);
+        $rename->save();
+        return array('ok'=>true);
+    }
+
+    /* Delete view */
+    private static function _remove($id) {
+        $delete = new Seth_Model_View(intval($id));
+        if (!$delete->isNew()) {
+            $delete->delete();
+        }
+        return array('ok'=>true);
+    }
+
+
+    static private function _param($key) {
+        return isset($_POST[$key]) ? $_POST[$key] : null;
+    }
+
+    static public function process() {
+        $action = self::_param('action');
+
+        $rtn = null;
+        switch ($action) {
+            case 'new':
+                $rtn = self::_new(self::_param('title'), self::_param('group_id'));
+                break;
+            case 'rename':
+                $rtn = self::_rename(self::_param('id'), self::_param('title'));
+                break;
+            case 'remove':
+                $rtn = self::_remove(self::_param('id'));
+            default:
+                break;
+        }
+        
+        if (!is_null($rtn)) {
+            header('Content-Type: application/json');
+            echo json_encode($rtn);
+            die();
+        }
+    }
+
+}
diff --git a/inc/Seth/Element/Filter.php b/inc/Seth/Element/Filter.php
new file mode 100755 (executable)
index 0000000..83bd5ae
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * This implements a select using three controls to add new part of
+ * a filter:
+ *
+ * [ field ]  [ condition ]  [ value ]
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+ */
+class Seth_Element_Filter extends StraightForm_Element_Container {
+
+    private $_title = '';
+    private $_elmField;
+    private $_elmCondition;
+    private $_elmValue;
+
+    /**
+     * New filter element
+     * @param string $title Title displayed above
+     * @param array $fields Array of fields to select
+     * @param array $conds Array of possible conditions
+     */
+    public function __construct($title, array $fields, array $conds) {
+        $this->_title = $title;
+        $this->_elmField = StraightForm_BasicElements::select(null, $fields, key($fields));
+        $this->_elmCondition = StraightForm_BasicElements::select(null, $conds, key($conds));
+        $this->_elmValue = StraightForm_BasicElements::text(null, 30);
+        $this->_idField = $this->addElement($this->_elmField);
+        $this->_idCond = $this->addElement($this->_elmCondition);
+        $this->_idValue = $this->addElement($this->_elmValue);
+    }
+
+    public function getHTML($id, $label = true) {
+
+        if ($label && !empty($this->_title)) {
+            $html = sprintf('<label for="%s">%s%s</label>',
+                htmlentities($id),
+                htmlentities($this->_title),
+                !$this->isValid() ? ' <span>*</span>':'') . "\n<br/>\n";
+        } else {
+            $html = '';
+        }
+        $html .= self::getSubHTML($this->_idField, $id, false);
+        $html .= self::getSubHTML($this->_idCond, $id, false);
+        $html .= self::getSubHTML($this->_idValue, $id, false);
+        $html .= '<br/>';
+
+        return $html;
+    }
+
+    public function isValid() {
+        if (!parent::isValid()) {
+            return false;
+        }
+        /* Value must not be set if field has not been selected */
+        if (!$this->_elmField->getValue() && $this->_elmValue->getValue() != '') {
+            return false;
+        }
+        return true;
+    }
+
+    public function getValue() {
+        if ($this->_elmField->getValue()) {
+            return
+                array('field' => $this->_elmField->getValue(),
+                    'cond' => $this->_elmCondition->getValue(),
+                    'value' => $this->_elmValue->getValue());
+        } else {
+            return null;
+        }
+    }
+
+    public function setValue($value) {
+        if (!is_null($value)) {
+            $this->_elmField->setValue($value['field']);
+            $this->_elmCondition->setValue($value['cond']);
+            $this->_elmValue->setValue($value['value']);
+        } else {
+            $this->_elmField->setValue(0);
+            $this->_elmConditon->setValue(0);
+            $this->_elmValue->setValue('');
+        }
+    }
+
+}
diff --git a/inc/Seth/Model/Element.php b/inc/Seth/Model/Element.php
new file mode 100755 (executable)
index 0000000..dc1ce23
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * This represents an element in a list
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ * @version $Revision$
+ * @access public
+ * @package Seth
+ */
+class Seth_Model_Element extends ORM_AbstractBase {
+
+    const TYPE = __CLASS__;
+
+    /**
+     * Create new and set defaults
+     */
+    public function __construct($element_id = null) {
+        parent::__construct($element_id);
+        if (is_null($element_id)) {
+            $this->_fields['priority'] = 100; // default priority
+            $this->_fields['created_at'] = date('c');
+        }
+    }
+
+    /**
+     * Configuration of the ORM object
+     *
+     * @param ORM_Configuration $config
+     */
+    public static function _ORMConfiguration(ORM_Configuration $config) {
+        $config->db = 'seth';
+        $config->table = 'element';
+        $config->autoIncrement = 'id';
+/*        $config->type['deadline'] = ORM::TYPE_DATE;
+        $config->type['created_at'] = ORM::TYPE_DATE;
+        $config->type['completed_date'] = ORM::TYPE_DATE;*/
+        $config->createGet(array('id'));
+        $config->createGetSet(array('title', 'group_id', 'priority', 'area',
+            'contact_person', 'estimate_design', 'estimate_development',
+            'actual_design', 'actual_development', 'approved', 'design',
+            'contact_person_email', 'completed', 'deleted', 'bug', 'deadline',
+            'created_at', 'sprint', 'completed_date', 'po_time', 'site',
+            'ressource', 'product_owner', 'assignee'));
+    }
+
+
+    /**
+     * Return array of fields that may be filtered
+     */
+    public static function getFilterFields() {
+        return
+            array('id', 'title', 'priority', 'area',
+                'contact_person', 'estimate_design', 'estimate_development',
+                'actual_design', 'actual_development', 'approved', 'design',
+                'contact_person_email', 'completed', 'deleted', 'bug', 'deadline',
+                'created_at', 'sprint', 'completed_date', 'po_time', 'site',
+                'ressource', 'product_owner', 'assignee');
+    }
+
+    /**
+     * Allows to return a specific field
+     * @param string $field Name of field
+     * @return mixed value of field
+     */
+    public function get($field) {
+        return $this->_fields[$field];
+    }
+
+    /**
+     * Allows to set a specific field
+     * @param string $field Name of field
+     * @param mixed value of field
+     */
+    public function set($field, $value) {
+        $this->_fields[$field] = $value;
+    }
+
+    /**
+     * Shortcut to find Seth_Element objects from database
+     * @param mixed $cond Optional SQL condition as string or ORM_Condition object
+     * @param array $params If condition is provided as string, optional array of parameters
+     * @return array Array of Seth_Element objects
+     */
+    static function find($cond = null, $params = array()) {
+        return ORM::find(self::TYPE, $cond, $params);
+    }
+
+    /**
+     * Shortcut to count number of Seth_Element objects in database
+     * @param mixed $cond Optional SQL condition as string or ORM_Condition object
+     * @param array $params If condition is provided as string, optional array of parameters
+     * @return integer Number of objects in database
+     */
+    static function count($cond = null, $params = array()) {
+        return ORM::count(self::TYPE, $cond, $params);
+    }
+
+    /**
+     * Shortcut to see if a Seth_Element object exists in database
+     * @param mixed $cond Optional SQL condition as string or ORM_Condition object
+     * @param array $params If condition is provided as string, optional array of parameters
+     * @return boolean Returns true if at least one object exists, otherwise false
+     */
+    static function exists($cond = null, $params = array()) {
+        return ORM::exists(self::TYPE, $cond, $params);
+    }
+
+    /**
+     * Shortcut to instantiate multiple Seth_Element objects by primary key
+     * @param array $pks Array of primary keys
+     * @param integer $type One of the ORM ordering constants
+     * @return array Array of Seth_Element objects
+     */
+    static function multiple(array $pks, $type = ORM::ORDERED) {
+        return ORM::multiple(self::TYPE, $pks, $type);
+    }
+
+}
+
+?>
diff --git a/inc/Seth/Model/Element/Descriptions.php b/inc/Seth/Model/Element/Descriptions.php
new file mode 100755 (executable)
index 0000000..0ec0d5c
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * This class contains extra description fields for an element in the list.
+ * The difference is that these fields are not searchable/filterable by default
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ * @version $Revision$
+ * @access public
+ * @package Seth_Element
+ */
+class Seth_Model_Element_Descriptions extends ORM_AbstractBase {
+
+    const TYPE = __CLASS__;
+
+    /**
+     * Constructor
+     * 
+     * Creates a new Seth_Element_Descriptions object or loads a Seth_Element_Descriptions object by primary key
+     * WARNING: No auto_increment column found, you need to always set primary keys explicit!
+     *
+     * @param integer $element_id primary key
+     * @return object returns Seth_Element_Descriptions object
+     */
+    public function __construct($element_id = null) {
+        parent::__construct($element_id);
+    }
+
+    /**
+     * Configuration of the ORM object
+     *
+     * @param ORM_Configuration $config
+     */
+    public static function _ORMConfiguration(ORM_Configuration $config) {
+        $config->db = 'seth';
+        $config->table = 'element_descriptions';
+        $config->pk = array('element_id');
+        $config->createGet(array('element_id'));
+        $config->createGetSet(array('user_story', 'specifications',
+            'description', 'url', 'material_in_shared_folder'));
+    }
+
+    /**
+     * Shortcut to find Seth_Element_Descriptions objects from database
+     * @param mixed $cond Optional SQL condition as string or ORM_Condition object
+     * @param array $params If condition is provided as string, optional array of parameters
+     * @return array Array of Seth_Element_Descriptions objects
+     */
+    static function find($cond = null, $params = array()) {
+        return ORM::find(self::TYPE, $cond, $params);
+    }
+
+    /**
+     * Shortcut to count number of Seth_Element_Descriptions objects in database
+     * @param mixed $cond Optional SQL condition as string or ORM_Condition object
+     * @param array $params If condition is provided as string, optional array of parameters
+     * @return integer Number of objects in database
+     */
+    static function count($cond = null, $params = array()) {
+        return ORM::count(self::TYPE, $cond, $params);
+    }
+
+    /**
+     * Shortcut to see if a Seth_Element_Descriptions object exists in database
+     * @param mixed $cond Optional SQL condition as string or ORM_Condition object
+     * @param array $params If condition is provided as string, optional array of parameters
+     * @return boolean Returns true if at least one object exists, otherwise false
+     */
+    static function exists($cond = null, $params = array()) {
+        return ORM::exists(self::TYPE, $cond, $params);
+    }
+
+    /**
+     * Shortcut to instantiate multiple Seth_Element_Descriptions objects by primary key
+     * @param array $pks Array of primary keys
+     * @param integer $type One of the ORM ordering constants
+     * @return array Array of Seth_Element_Descriptions objects
+     */
+    static function multiple(array $pks, $type = ORM::ORDERED) {
+        return ORM::multiple(self::TYPE, $pks, $type);
+    }
+
+}
+
+?>
diff --git a/inc/Seth/Model/Element/FilterOrder.php b/inc/Seth/Model/Element/FilterOrder.php
new file mode 100644 (file)
index 0000000..25bc7a1
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+require_once 'Seth/Model/Element.php';
+require_once 'Seth/Model/Filter.php';
+
+/**
+ * This class is used to keep a specific ordering with a given view of the elements
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ * @version $Revision$
+ * @access public
+ * @package Seth_Model_Element
+ */
+class Seth_Model_Element_FilterOrder extends ORM_AbstractBase {
+
+    const TYPE = __CLASS__;
+
+    /**
+     * Constructor
+     * 
+     * Creates a new Seth_Element_Filter_Order object or loads a Seth_Element_Filter_Order object by primary keys
+     * WARNING: No auto_increment column found, you need to always set primary keys explicit!
+     *
+     * @param integer $filter_id primary key
+     * @param integer $element_id primary key
+     * @return object returns Seth_Element_Filter_Order object
+     */
+    public function __construct($filter_id = null, $element_id = null) {
+        if (!is_null($filter_id) || !is_null($element_id)) {
+            parent::__construct(array($filter_id, $element_id));
+        } else {
+            parent::__construct();
+        }
+    }
+
+    /**
+     * Configuration of the ORM object
+     *
+     * @param ORM_Configuration $config
+     */
+    public static function _ORMConfiguration(ORM_Configuration $config) {
+        $config->db = 'seth';
+        $config->table = 'element_filter_order';
+        $config->pk = array('filter_id', 'element_id');
+        $config->createGet(array('filter_id', 'element_id'));
+        $config->createGetSet(array('order_index'));
+    }
+
+    /**
+     * Very naive way of placing an element between two elements
+     *
+     * We receive the entire ordering, then reorder all elements each time
+     * @param object $filter Filter displayed
+     * @param object $place Element to place
+     * @param object $above Element that should be above
+     * @param object $below Element that should be below
+     * TODO: Reimplement using a any other algorithm :-)
+     */
+    public static function placeBetween(Seth_Model_Filter $filter,
+                Seth_Model_Element $place, $above, $below) {
+        // Get all elements from list
+        $conds = $filter->getQueryConditions();
+        $elms = ORM::find(Seth_Model_Element::TYPE, $conds);
+        // Get current ordering
+        $ordering = ORM::find(self::TYPE, 'filter_id=?', $filter->getId());
+        // order by element
+        $orderByElm = array();
+        foreach ($ordering as $order) {
+            $orderByElm[$order->getElementId()] = $order;
+        }
+        $placeId = $place->getId();
+        $aboveId = !is_null($above) ? $above->getId() : null;
+        $belowId = !is_null($below) ? $below->getId() : null;
+
+        // reindex
+        $index = 0;
+        $newOrder = array();
+        $wasPlaced = false;
+        foreach ($elms as $elm) {
+            $id = $elm->getId();
+            
+            if ($id == $placeId) {
+                continue; // we skip the element we are going to place
+            }
+
+            $index++;
+            if (isset($orderByElm[$id])) {
+                $newOrder[] = $order = $orderByElm[$id];
+                unset($orderByElm[$id]);
+            } else {
+                $newOrder[] = $order = new self($filter->getId(), $id);
+            }
+            
+            // check to see if we should place our element above or below this one
+            if ($id == $belowId && !is_null($below) && !$wasPlaced) {
+                $placeIndex = ($index++);
+                $wasPlaced = true;
+            }
+
+            $order->setOrderIndex($index);
+
+            if ($id == $aboveId && !is_null($above) && !$wasPlaced) {
+                $placeIndex = (++$index);
+                $wasPlaced = true;
+            }
+        }
+
+        // Place item
+        if ($wasPlaced) {
+            if (isset($orderByElm[$placeId])) {
+                $newOrder[] = $order = $orderByElm[$placeId];
+                unset($orderByElm[$placeId]);
+            } else {
+                $newOrder[] = $order = new self($filter->getId(), $placeId);
+            }
+            $order->setOrderIndex($placeIndex);
+        }
+
+        // save all
+        ORM_Multiple::save($newOrder);
+        // delete unused
+        ORM_Multiple::delete($orderByElm);
+    }
+
+}
+
+?> 
diff --git a/inc/Seth/Model/Filter.php b/inc/Seth/Model/Filter.php
new file mode 100755 (executable)
index 0000000..ab42654
--- /dev/null
@@ -0,0 +1,230 @@
+<?php
+/**
+ *
+ * A Filter is used to create the nice difference lists used as tabs
+ * in Seth. It is just different "filters"/views on the same data
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ * @version $Revision$
+ * @access public
+ * @package Seth
+ */
+class Seth_Model_Filter extends ORM_AbstractBase {
+
+    const TYPE = __CLASS__;
+
+    /**
+     * Configuration of the ORM object
+     *
+     * @param ORM_Configuration $config
+     */
+    public static function _ORMConfiguration(ORM_Configuration $config) {
+        $config->db = 'seth';
+        $config->table = 'filter';
+        $config->autoIncrement = 'id';
+        $config->createGet(array('id'));
+        $config->createGetSet(array('title', 'group_id', 'description', 'inherit_id'));
+    }
+
+    /**
+     * Returns possible conditions
+     */
+    public static function getPossibleConditions() {
+        return array('=', '!=', '<', '<=', '>' ,'>=', 'LIKE', 'ILIKE', 'IS NULL', 'IS NOT NULL');
+    }
+
+    /**
+     * Sets what to filter by
+     * @param array $rules Array of rules, of field, cond, value
+     */
+    public function setFilterBy(array $rules) {
+        if (!empty($rules)) {
+            $this->_fields['filter_by'] = serialize($rules);
+        } else {
+            $this->_fields['filter_by'] = null;
+        }
+    }
+
+    /**
+     * Gets what to filter by
+     * @return array Array of rules, of field, cond and value
+     */
+    public function getFilterBy() {
+        if (isset($this->_fields['filter_by']) && !is_null($this->_fields['filter_by'])) {
+            return unserialize($this->_fields['filter_by']);
+        }
+        return null;
+    }
+
+    /**
+     * Sets list of fields to group by
+     *
+     * @param array $fields List or fields to group by or null
+     */
+    public function setGroupBy(array $fields) {
+        $this->_fields['group_by'] =
+            empty($fields) ? null : implode(',', $fields);
+    }
+
+    /**
+     * Returns list of fields to group by
+     *
+     * @return array Array of fields to group by or null
+     */
+    public function getGroupBy() {
+        if (!isset($this->_fields['group_by']) || is_null($this->_fields['group_by'])) {
+            return null;
+        }
+        return explode(',', $this->_fields['group_by']);
+    }
+
+    /**
+     * Sets list of fields to order by
+     *
+     * @param array $fields List or fields to order by or null
+     */
+    public function setOrderBy(array $fields) {
+        $this->_fields['order_by'] =
+            empty($fields) ? null : implode(',', $fields);
+    }
+
+    /**
+     * Returns list of fields to order by
+     *
+     * @return array Array of fields to order by or null
+     */
+    public function getOrderBy() {
+        if (!isset($this->_fields['order_by']) || is_null($this->_fields['order_by'])) {
+            return null;
+        }
+        return explode(',', $this->_fields['order_by']);
+    }
+
+    /**
+     * Returns all filters, including inherited
+     */
+    public function getFilterByIncludeInherited() {
+        $parentFilters = null;
+        // We will start by including all the conditions we inherit
+        if ($this->getInheritId()) {
+            $inherit = new Seth_Model_Filter($this->getInheritId());
+            if (!$inherit->isNew()) {
+                $parentFilters = $inherit->getFilterByIncludeInherited();
+            }
+        }
+        $myFilters = $this->getFilterBy();
+        if (!is_null($myFilters) && !is_null($parentFilters)) {
+            $filters = array_merge($parentFilters, $myFilters);
+        } else {
+            $filters = (!is_null($myFilters)) ? $myFilters : $parentFilters;
+        }
+        return $filters;
+    }
+
+    /**
+     * Filter out filters for more specific rules
+     *
+     * If a filter has a = or IS NULL condition these override any other condition
+     * Useful when determining what to set to match a filter
+     */
+    public static function cleanFilter(array $all) {
+        $ignore = array();
+        $filters = array();
+        foreach ($all as $filter) {
+            if ($filter['cond'] == '=' || $filter['cond'] == 'IS NULL') {
+                $ignore[] = $filter['field'];
+                $filters[] = $filter;
+            }
+        }
+        // now add rest to list (that are not overridden)
+        foreach ($all as $filter) {
+            if (!in_array($filter['field'], $ignore)) {
+                $filters[] = $filter; 
+            }
+        }
+        return $filters;
+    }
+
+    /**
+     * Returns all fields to group by
+     */
+    public function getGroupByIncludeInherited() {
+        $parentGB = null;
+        if ($this->getInheritId()) {
+            $inherit = new Seth_Model_Filter($this->getInheritId());
+            if (!$inherit->isNew()) {
+                $parentGB = $inherit->getGroupByIncludeInherited();
+            }
+        }
+        $myGB = $this->getGroupBy();
+        if (!is_null($myGB) && !is_null($parentGB)) {
+            $GB = array_merge($parentGB, $myGB);
+        } else {
+            $GB = (!is_null($myGB)) ? $myGB : $parentGB;
+        }
+        return $GB;
+    }
+
+
+
+    /**
+     * Returns a ORM_Condition object to query a list using this filter
+     * @param boolean $inherting include conditions for subfilters (that inherits this filter)
+     */
+    public function getQueryConditions($inheriting = false) {
+
+        $query = new ORM_Condition();
+
+        // We will start by all including all the conditions we inherit
+        if ($this->getInheritId()) {
+            $inherit = new Seth_Model_Filter($this->getInheritId());
+            if (!$inherit->isNew()) {
+                $query->merge($inherit->getQueryConditions(true));
+            }
+        }
+
+        // load filter options
+        $conds = $this->getFilterBy();
+        $orderby = $this->getOrderBy();
+        $groupby = $this->getGroupBy();
+
+        if ($conds) {
+            foreach ($conds as $cond) {
+                // Ignore value if IS NULL or IS NOT NULL
+                if ($cond['cond'] == 'IS NULL' || $cond['cond'] == 'IS NOT NULL') {
+                    $query->add(sprintf('e.%s %s', $cond['field'], $cond['cond']));
+                } else {
+                    $query->add(sprintf('e.%s %s ?', $cond['field'], $cond['cond']), $cond['value']);
+                }
+            }
+        }
+
+        // Construct our ordering of the elements
+        if ($groupby) {
+            foreach ($groupby as $order) {
+                $query->addOrderBy('e.'.$order);
+            }
+        }
+        if ($orderby) {
+            foreach ($orderby as $order) {
+                $query->addOrderBy('e.'.$order);
+            }
+        }
+
+        // Default conditions
+        if (!$inheriting) {
+            $query->add('e.group_id = ?', $this->getGroupId());
+            // Always left join and order by the filter's own ordering
+            $query->add('FROM element e '.
+                        'LEFT JOIN element_filter_order o ON e.id=o.element_id AND o.filter_id=? '.
+                        'WHERE e.id=element.id '.
+                        'ORDER BY o.order_index',
+                $this->getId());
+        }
+            
+        return $query;
+    }
+
+}
+
+?>
diff --git a/inc/Seth/Model/Group.php b/inc/Seth/Model/Group.php
new file mode 100755 (executable)
index 0000000..2d40119
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * This represents groups for seth, so difference groups of people can use
+ * the interface indepedent.
+ *
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ * @version $Revision$
+ * @access public
+ * @package Seth
+ */
+class Seth_Model_Group extends ORM_AbstractBase {
+
+    const TYPE = __CLASS__;
+
+    /**
+     * Configuration of the ORM object
+     *
+     * @param ORM_Configuration $config
+     */
+    public static function _ORMConfiguration(ORM_Configuration $config) {
+        $config->db = 'seth';
+        $config->table = '`group`';
+        $config->autoIncrement = 'id';
+        $config->createGet(array('id'));
+        $config->createGetSet(array('title'));
+    }
+
+}
+
+?>
diff --git a/inc/Seth/Model/View.php b/inc/Seth/Model/View.php
new file mode 100755 (executable)
index 0000000..6a8f99e
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ *
+ * A view is a set of tabs (lists) displayed
+ * 
+ * @author Kristian Kræmmer Nielsen <jkkn@jkkn.dk>
+ * @version $Revision$
+ * @access public
+ * @package Seth
+ */
+class Seth_Model_View extends ORM_AbstractBase {
+
+    const TYPE = __CLASS__;
+
+    /**
+     * Configuration of the ORM object
+     *
+     * @param ORM_Configuration $config
+     */
+    public static function _ORMConfiguration(ORM_Configuration $config) {
+        $config->db = 'seth';
+        $config->table = 'view';
+        $config->autoIncrement = 'id';
+        $config->createGet(array('id'));
+        $config->createGetSet(array('title', 'group_id'));
+    }
+
+    /**
+     * Sets list of filters to view
+     *
+     * @param array $fields List or fields to order by or null
+     */
+    public function setFilterOrder(array $filters) {
+        $this->_fields['filter_order'] =
+                implode(',', $filters);
+    }
+
+    /**
+     * Returns list of filters this view has
+     *
+     * @return array Array of filters in order
+     */
+    public function getFilterOrder() {
+        if (!isset($this->_fields['filter_order'])) {
+            return array();
+        }
+        return explode(',', $this->_fields['filter_order']);
+    }
+
+}
+?>
diff --git a/opdatering/addfilter.php b/opdatering/addfilter.php
new file mode 100644 (file)
index 0000000..f23b7e1
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+require_once 'Seth/Controller/Page.php';
+require_once 'Seth/Controller/Filter.php';
+// process callbacks
+Seth_Controller_Filter::process();
+
+I2_GUI_List::process();
+
+Seth_Controller_Page::printTop();
+
+$controller = new Seth_Controller_Filter();
+$groupId = 0;
+if (isset($_GET['group_id'])) {
+    $controller->setGroupId($groupId = intval($_GET['group_id']));
+} else {
+    echo 'Missing group_id';die();
+}
+
+echo <<<EOF
+<script type="text/javascript">
+$(function() {
+    SethManager.initAddFilter();
+});
+</script>
+EOF;
+
+// List of available groups to choose from
+echo '<h2>Gruppe:</h2>';
+$controller->printGroupSelect();
+
+echo '<h2>Vælg liste:</h2>';
+$list = new I2_GUI_List();
+$list->enableFeatures(I2_GUI_LIST_TOTALCOUNT);
+$list->setHeaders(array('id' => 'ID', 'title'=>'Navn på liste', 'action' => 'Muligheder'));
+$list->setLimit(25);
+$list->setDataORM(Seth_Model_Filter::TYPE, 'group_id=?', array($groupId));
+$list->addOrderBy('title');
+$list->setRowCallback('insertEditLinks', 'Seth_Controller_Filter');
+echo $list->getHTML();
+
+echo '<br/>';
+printf('<button onclick="location.href=\'editfilter.php?group_id=%d\'">Opret ny liste</button>', $groupId);
+
+
+Seth_Controller_Page::printBottom();
+
+?>
diff --git a/opdatering/css/seth.css b/opdatering/css/seth.css
new file mode 100644 (file)
index 0000000..e9d398e
--- /dev/null
@@ -0,0 +1,90 @@
+html,body,#page, #tabs {
+    height: 100%;
+}
+#page, #tabs {
+    position: relative;
+    overflow: none;
+}
+
+h1 {
+    font: 22pt Arial;
+    font-weight: bold;
+    margin: 0 0 10px 0;
+    text-align: center;
+}
+
+#page {
+    width: 90%;
+    margin: 2pt auto 2pt;
+    padding: 4pt;
+
+    border: 1px solid gray;
+    -moz-box-shadow: 10px 10px 5px #888;
+    -moz-border-radius: 10px;
+}
+
+.table li {
+    list-style-type:none;
+}
+
+ul.table {
+    margin: 0;
+    padding-left: 0;
+    min-height: 15px; /* allow drag'n'drop back */
+}
+
+.new input[type=text] {
+    width: 80%;
+    margin: 0 5px;
+}
+
+div.group-seperator {
+    margin-top: 16px;
+    font-size: 14px;
+    font-weight: bold;
+}
+
+#tabs-list {
+    font-size: 12px;
+}
+
+#actions {
+    font: 11pt verdana bold;
+    text-align: right;
+    padding: 0 10px;
+}
+
+#actions a {
+    color: darkblue;
+    text-decoration: none;
+}
+
+#actions a:hover {
+    background-color: #FFFFB0;
+    text-decoration: underline overline;
+}
+
+.filtered_list {
+    position:relative;
+    height: 80%;
+    overflow: scroll;
+}
+
+.filtered_list a:hover {
+    background-color: #FFFF88;
+}
+
+/* Dialogs */
+
+#addlist, #editelement {
+    padding: 0;
+    margin: 0;
+    overflow: hidden;
+}
+
+#addlistiframe, #editiframe {
+    width: 100%;
+    height: 100%;
+    border: 0;
+}
+
diff --git a/opdatering/edit.php b/opdatering/edit.php
new file mode 100644 (file)
index 0000000..ef527e6
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+require_once 'Seth/Controller/Page.php';
+require_once 'Seth/Controller/Filter.php';
+require_once 'Seth/Model/Element.php';
+require_once 'Seth/Model/Element/Descriptions.php';
+
+// Edit element
+$elementId = isset($_GET['id']) ? intval($_GET['id']) : 0;
+if (!$elementId) {
+    echo 'No element'; die();
+}
+
+$element = new Seth_Model_Element($elementId);
+if ($element->isNew()) {
+    echo 'Element no longer exists'; die();
+}
+
+$elemDescription = new Seth_Model_Element_Descriptions($elementId);
+
+// Fields directly mapped to table (ORM object):
+$ormControlElm = new StraightForm_ORMController($element,
+    array(
+    'title' => 'Overskrift',
+    'priority' => 'Prioritet (lavere tal er højest prioritet)',
+    'area' => 'Område',
+    'sprint' => 'Sprint',
+    'contact_person_email' => 'Kontakt person (email)',
+    'contact_person' => 'Kontakt person',
+    'assignee' => 'Opgaven tilhører',
+    'estimate_development' => 'Udviklings estimat',
+    'estimate_design' => 'Design estimat',
+    'actual_design' => 'Faktisk designtid',
+    'actual_development' => 'Faktisk udviklingstid',
+    'approved' => StraightForm_BasicElements::checkbox('Godkendt'),
+    'design' => StraightForm_BasicElements::checkbox('Design klar'),
+    'completed' => StraightForm_BasicElements::checkbox('Afsluttet'),
+    'deleted' => StraightForm_BasicElements::checkbox('Slettet'),
+    'bug' => StraightForm_BasicElements::checkbox('Bug/fejlrettelse'),
+    'deadline' => 'Deadline',
+    'created_at' => 'Oprettelsestidspunkt',
+    'completed_date' => 'Færdiggørelsesdag',
+    'po_time' => 'PO tid',
+    'site' => 'Site',
+    'ressource' => 'Ressource',
+    'product_owner' => 'Product Owner',
+    'group_id' =>  StraightForm_BasicElements::select('Gruppe:', Seth_Controller_Filter::getGroups()),
+    ));
+$ormControlElm->setParameters('title', array('text_regex' => '/^.+/'));
+$ormControlElm->setParameters('priority', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setParameters('estimate_design', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setParameters('estimate_development', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setParameters('actual_development', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setParameters('actual_design', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setParameters('sprint', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setParameters('po_time', array('text_regex' => '/^[0-9]*$/'));
+$ormControlElm->setNullify(array('assignee', 'area', 'contact_person', 'estimate_design',
+    'estimate_development', 'actual_design', 'actual_development', 'contact_person_email',
+    'deadline', 'sprint', 'completed_date', 'po_time', 'site', 'ressource', 'product_owner'));
+$ormControlElm->setAutoSave(false);
+
+$ormControlElmDesc = new StraightForm_ORMController($elemDescription,
+    array(
+       'user_story' =>  StraightForm_BasicElements::textarea('User Story'),
+       'specifications' => StraightForm_BasicElements::textarea('Specifikationer'),
+       'description' => StraightForm_BasicElements::textarea('Beskrivelse'),
+       'url' => 'URL',
+       'material_in_shared_folder' => StraightForm_BasicElements::checkbox('Findes yderligere materiale på share')
+       ));
+$ormControlElmDesc->setNullify(array('user_story', 'specifications',
+    'description', 'url', 'material_in_shared_folder'));
+$ormControlElmDesc->setAutoSave(false);
+
+// Form
+$form = new StraightForm(new StraightForm_ChainController($ormControlElmDesc, $ormControlElm));
+
+Seth_Controller_Page::printTop($form->getHead());
+
+// Saving
+if ($form->handleForm()) {
+    // post submit we will convert some fields back to what they really should be
+    $element->save();
+    $elemDescription->save();
+    // completed - return to list page
+    $newTitle = json_encode(utf8_encode($element->getTitle()));
+    echo '<script type="text/javascript">parent.SethManager.updateThenCloseEditDialog('.$element->getId().', '.$newTitle.'); //</script>';
+
+} else {
+    echo $form->getHTML();
+}
+
+Seth_Controller_Page::printBottom();
+
+?>
diff --git a/opdatering/editfilter.php b/opdatering/editfilter.php
new file mode 100644 (file)
index 0000000..2ee2f90
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+require_once 'Seth/Controller/Page.php';
+require_once 'Seth/Controller/Filter.php';
+require_once 'Seth/Element/Filter.php';
+require_once 'Seth/Model/Element.php';
+
+I2_GUI_List::process();
+
+// Require group_id
+$groupId = isset($_GET['group_id']) ? intval($_GET['group_id']) : 0;
+
+// My controller
+$con = new Seth_Controller_Filter();
+
+// Create new or edit filter
+$filterId = isset($_GET['id']) ? intval($_GET['id']) : 0;
+if ($filterId) {
+    $filter = new Seth_Model_Filter($filterId);
+    $groupId = $filter->getGroupId();
+} else {
+    // create new with group id as default
+    if (!$groupId) {
+        echo 'No group'; die();
+    }
+    $filter = new Seth_Model_Filter();
+    $filter->setGroupId($groupId);
+}
+
+// Fields directly mapped to table (ORM object):
+$ormController = new StraightForm_ORMController($filter,
+    array('title' => 'Navn til filtret:',
+          'group_id' =>  StraightForm_BasicElements::select('Gruppe:', Seth_Controller_Filter::getGroups()),
+          'inherit_id' =>  StraightForm_BasicElements::select('Byg ovenpå:', Seth_Controller_Filter::getFilters($groupId, $filter)),
+          'description' => StraightForm_BasicElements::textarea('Beskrivelse:')));
+
+$ormController->setParameters('title', array('text_regex' => '/^\w+/'));
+$ormController->setNullify(array('inherit_id'));
+$ormController->setAutoSave(false);
+
+// Form
+$form = new StraightForm($ormController);
+
+// Fields added for prettifying gui making it easier to add conditions:
+$fields = Seth_Model_Element::getFilterFields();
+sort($fields);
+$fields = array_combine($fields, $fields); // same keys/values
+$fields = array_merge(array(0=>''), $fields); // Empty default
+$default = 0;
+
+$conds = Seth_Model_Filter::getPossibleConditions();
+$conds = array_combine($conds, $conds); // same keys/values
+
+$formFilters = array();
+$rules = $filter->getFilterBy();
+if (!is_null($rules)) reset($rules);
+
+for ($i=0; $i<6; $i++) {
+    $formFilters[] = $f = $form->addElement('filter'. $i, new Seth_Element_Filter($i?null:'Filtre:', $fields, $conds));
+    if (!is_null($rules) && current($rules)) {
+        $f->setValue(current($rules));
+        next($rules);
+    }
+}
+
+// Add group by fields
+$formGroupBy = array();
+
+$groupBy = $filter->getGroupBy();
+if (!is_null($groupBy)) reset($groupBy);
+
+for ($i=0; $i<5; $i++) {
+    $formGroupBy[] = $f = $form->addElement('groupby'.$i,
+        StraightForm_BasicElements::select($i?null:'Gruppér efter:', $fields, $default));
+    if (!is_null($groupBy) && current($groupBy)) {
+        $f->setValue(current($groupBy));
+        next($groupBy);
+    }
+}
+
+// Add order by fields
+$formOrderBy = array();
+
+$orderBy = $filter->getOrderBy();
+if (!is_null($orderBy)) reset($orderBy);
+
+for ($i=0; $i<5; $i++) {
+    $formOrderBy[] = $f = $form->addElement('orderby'.$i,
+        StraightForm_BasicElements::select($i?null:'Sortér efter:', $fields, $default));
+    if (!is_null($orderBy) && current($orderBy)) {
+        $f->setValue(current($orderBy));
+        next($orderBy);
+    }
+}
+
+
+if ($form->handleForm()) {
+    // post submit we will convert filters to a serialized field
+    $rules = array();
+    foreach ($formFilters as $selection) {
+        $rule = $selection->getValue();
+        if (!is_null($rule)) {
+            // if used add to list
+            $rules[] = $rule;
+        }
+    }
+    // group by
+    $groupby = array();
+    foreach ($formGroupBy as $by) {
+        $value = $by->getValue();
+        var_dump($value);
+        if ((string)$value != (string)$default) {
+            $groupby[] = $value;
+        }
+    }
+
+    // order by
+    $orderby = array();
+    foreach ($formOrderBy as $by) {
+        $value = $by->getValue();
+        if ((string)$value != (string)$default) {
+            $orderby[] = $value;
+        }
+    }
+
+    $filter->setFilterBy($rules);
+    $filter->setGroupBy($groupby);
+    $filter->setOrderBy($orderby);
+    $filter->save();
+    // completed - return to list page
+    header('Location: addfilter.php?group_id=' . $filter->getGroupId());
+    exit;
+}
+
+Seth_Controller_Page::printTop($form->getHead());
+
+echo $form->getHTML();
+
+Seth_Controller_Page::printBottom();
+
+?>
diff --git a/opdatering/index.php b/opdatering/index.php
new file mode 100644 (file)
index 0000000..61c0e87
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+require_once 'Seth/Model/Group.php';
+require_once 'Seth/Controller/Group.php';
+I2_GUI_List::process();
+Seth_Controller_Group::process();
+$vendor = getSite('vendor');
+
+$head = <<<EOH
+<script type="text/javascript" src="http://$vendor/js/jquery.js"></script>
+<script type="text/javascript" src="js/group.js"></script>
+EOH;
+
+$page = new I2_GUI_Informational('Seth Projekt Organizer', 'TV 2 Net'."\n".'Seth is design and implemented by Kristian Kræmmer Nielsen <jkkn@tv2.dk>, Copyright (c) 2010');
+$page->printTop($head);
+$page->printSubtitle('Beskrivelse:');
+
+echo 'Seth, opkaldt efter den egytiske gud for kaos.<br/>I den filosofi at er du venner med Seth har du bedre chance for at få styr på dit projekt og ikke ende ud i kaos.<br/><br/>';
+
+$page->printSubtitle('Grupper:');
+
+$list = new I2_GUI_List();
+$list->enableFeatures(I2_GUI_LIST_TOTALCOUNT);
+$list->setHeaders(array(
+            'id' => 'Id',
+            'title' => 'Titel',
+            'actions' => 'Muligheder'));
+$list->setLimit(25);
+$list->setDataORM(Seth_Model_Group::TYPE);
+$list->setOrderBy('title');
+$list->setRowCallback('insertActions', 'Seth_Controller_Group');
+$list->setRowLink('views.php?group_id=%d', 'id');
+echo $list->getHTML();
+
+?>
+<br/>
+<button onclick="SethGroup.createNew()">Opret nyt gruppe</button>
+
+<?php
+
+$page->printBottom();
+
+?>
diff --git a/opdatering/js/group.js b/opdatering/js/group.js
new file mode 100644 (file)
index 0000000..7a9a4b3
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Group list
+ */
+
+SethGroup = {
+
+    createNew: function() {
+        var name = prompt('Indtast navn:');
+        if (name) {
+            name = name.replace(/\s*/, "")
+            if (name != '') {
+                // do callback
+                $.ajax({
+                    type: 'POST',
+                    data: {
+                        action: 'new',
+                        title: name
+                    },
+                    dataType: 'json',
+                    error: function() {
+                        alert('Server fejl');
+                    },
+                    success: function(data) {
+                        if (data && data.ok) {
+                            window.location.reload();
+                        }
+                    }
+                });
+            }
+        }
+    },
+
+    rename: function(id, title) {
+        var name = prompt('Indtast navn:', unescape(title));
+        if (name) {
+            name = name.replace(/\s*/, "")
+            if (name != '') {
+                // do callback
+                $.ajax({
+                    type: 'POST',
+                    data: {
+                        action: 'rename',
+                        id: id,
+                        title: name
+                    },
+                    dataType: 'json',
+                    error: function() {
+                        alert('Server fejl');
+                    },
+                    success: function(data) {
+                        if (data && data.ok) {
+                            window.location.reload();
+                        }
+                    }
+                });
+            } else {
+                alert('Ugyldigt navn');
+            }
+        }
+    },
+
+    remove: function(id, title) {
+        if (confirm('Er du sikker på at slettet "'+unescape(title)+'" ?')) {
+            // do callback
+            $.ajax({
+                type: 'POST',
+                data: {
+                    action: 'remove',
+                    id: id,
+                },
+                dataType: 'json',
+                error: function() {
+                    alert('Server fejl');
+                },
+                success: function(data) {
+                    if (data && data.ok) {
+                        window.location.reload();
+                    }
+                }
+            });
+        }
+    }
+
+}
diff --git a/opdatering/js/seth.js b/opdatering/js/seth.js
new file mode 100644 (file)
index 0000000..0656df1
--- /dev/null
@@ -0,0 +1,607 @@
+
+var SethManager = {
+
+    view_id: null,  // ID of current view
+    group_id: null, // ID of group shown
+
+    next_tab_no: 0,
+
+    disableEditLinks: false, // manual disable all links when dragging
+    cancelMove: true, // cancels moves (used when moving into tabs)
+    currentTabNo: -1, // tab_no of current tab
+
+    init: function(view_id, group_id, list_of_tabs) {
+        this.view_id = view_id;
+        this.group_id = group_id;
+
+        // create addList dialog
+        $("#addlist").dialog({
+            modal: true,
+            autoOpen: false,
+            height: 600,
+            width: 800,
+            buttons: {
+                "Fortryd": function() {
+                    SethManager.closeAddListDialog();
+                }
+            },
+            close: function() {
+                SethManager.emptyListsAndUpdateCurrent();
+                $('#addlistiframe').attr('src', 'about:blank'); // make F5 work
+            }
+        });
+
+        // create edit element dialog
+        $("#editelement").dialog({
+            modal: true,
+            autoOpen: false,
+            height: 600,
+            width: 800,
+            buttons: {
+                "Fortryd": function() {
+                    SethManager.closeEditDialog();
+                },
+                "Gem": function() {
+                    SethManager.saveAndCloseEditDialog();
+                },
+                "Slet": function() {
+                    SethManager.deleteAndCloseEditDialog();
+                }
+            },
+            close: function() {
+                $("#editiframe").attr('src', 'about:blank'); // make F5 work
+            }
+        });
+
+        // turn add/edit list links into open dialog
+        $("#addlist-link").click(function() {
+            SethManager.openAddListDialog();
+            return false;
+        });
+        $("#editlist-link").click(function() {
+            SethManager.openEditListDialog();
+            return false;
+        });
+
+        // remove list link
+        $("#removelist-link").click(function() {
+            SethManager.removeCurrentList();
+            return false;
+        });
+
+        // create filter tabs
+        $("#tabs").tabs({
+            show: function(event, ui) {
+                SethManager.showTab(event, ui);
+                SethManager.currentTabNo = $(ui.panel).data('tab_no');
+            }
+        });
+        var tabsul = $("#tabs-list");
+
+        // Allow to sort tabs
+        tabsul.sortable();
+
+        tabsul.bind('sortupdate', function(event, ui) {
+            SethManager.storeFilterOrder();
+        });
+
+        // create tabs
+        $.each(list_of_tabs, function(index, value) {
+            SethManager.addList(value.id, value.title);
+        });
+
+    },
+
+    // Callback and get a fresh list
+    updateList: function(tabno) {
+        var listDom = $("#list-" + tabno);
+        var list_id = listDom.data('list_id');
+        listDom.data('list_status', 'loading');
+
+        // do callback
+        $.ajax({
+            type: 'POST',
+            data: {
+                action: 'get',
+                listid: list_id
+            },
+            dataType: 'json',
+            error: function() {
+                SethManager.commError(this);
+            },
+            success: function(data) {
+                if (data && data.list) {
+                    SethManager.fillInList(tabno, data.list);
+                } else {
+                    SethManager.commError(this, data);
+                }
+            }
+        });
+    },
+
+    // Fill in a list using return data
+    fillInList: function(tabno, listdata) {
+        // check for error (list returned as nested result)
+        if (listdata.err) {
+            this.commError(null, listdata);
+            return;
+        }
+
+        var listDom = $("#list-" + tabno);
+        var first = true;
+
+        listDom.empty();
+
+        listDom.data('list_status', 'loaded');
+
+        // We need to create a ul for each group
+        $.each(listdata.groups, function(index, value) {
+            var group = $('<div class="group-seperator">'+value.title+'</div>');
+            group.data('group_defined_as', value.defined_as);
+            group.data('group_when_placing_here', value.when_placing_here);
+            listDom.append(group);
+
+            var table = $('<ul class="ul-helper-reset table"/>');
+            // and for each element in the group create it
+            $.each(value.elements, function(index, value) {
+                var elmLi = $('<li class="ui-state-default element" />');
+                var elmDiv = $('<div class="draggable" />');
+                var elmA = $('<a class="element-'+value.id+'" href="#"/>');
+                // Add mouseover
+                elmDiv.attr('title', value.description);
+                // store meta data
+                elmLi.data('element_id', value.id);
+                // set onclick event
+                elmA.click(function() {
+                    if (!SethManager.disableEditLinks) {
+                        SethManager.editElement(value.id);
+                    }
+                    return false;
+                });
+                // set link title
+                elmA.text(value.title);
+                // append element
+                elmDiv.append(elmA);
+                elmLi.append(elmDiv);
+                table.append(elmLi);
+            });
+
+            listDom.append(group).append(table);
+
+            // create a new element button for each group
+            if (value.defined_as) {
+                var button = $('<input type="submit" value="Tilføj ny"/>').button();
+                var input = $('<input id="list-new-'+index+'" type="text"/>');
+                var form = $('<form class="ui-helper-reset"/>').submit(function() {
+                    SethManager.createNew(this);
+                    return false;
+                });
+
+                var new_li = $('<div class="ui-state-default new"></div>');
+                form.append(new_li.append(input).append(button));
+                listDom.append(form);
+            }
+        });
+        
+        var tables = listDom.find("ul.table");
+        // Make drag'n'drop able
+        tables.sortable({
+            start: function() {
+                // disable links while dragging
+                SethManager.disableEditLinks = true;
+                SethManager.cancelMove = false;
+            },
+            stop: function() {
+                SethManager.cancelMove = true;
+                // Hack, but cannot seem to find any event that triggers at a proper time
+                setTimeout(function() {
+                    SethManager.disableEditLinks = false;
+                }, 10);
+            },
+            connectWith: '#list-' + tabno + ' ul',
+            update: function(event, ui) {
+                SethManager.sortElement(event, ui);
+            }
+        });
+
+    },
+
+    // Create new element
+    createNew: function(form) {
+        form = $(form);
+        var text = form.find('input').get(0);
+        var input_id = text.id; // going back to this one afterwards
+        var list_id = form.parent().data('list_id');
+        var tab_no = form.parent().data('tab_no');
+        var list = form.prev();
+        var group_defined_as = list.prev().data('group_defined_as');
+        var title = text.value; 
+
+        if ($.trim(title) == '') return; // have to enter something
+
+        text.disabled = true;
+
+        // create new element and add it to the list (before server update)
+        this.addNewElement(list, title);
+
+        // do callback
+        $.ajax({
+            type: 'POST',
+            data: {
+                action: 'new',
+                obj: {
+                    title: title,
+                    filter_id: list_id,
+                    group_defined_as: group_defined_as 
+                }
+            },
+            dataType: 'json',
+            error: function() {
+                SethManager.commError(this);
+            },
+            success: function(data) {
+                if (data && data['new']) {
+                    // success
+                    // update list with result
+                    SethManager.fillInList(tab_no, data.list);
+                    // flush other tabs
+                    SethManager.emptyAllListsButCurrent();
+                    // Set focus back
+                    $("#"+input_id).focus();
+                } else {
+                    SethManager.commError(this, data);
+                }
+            }
+        });
+       
+    },
+
+    commError: function(obj, data) {
+        if (data && data.err) {
+            alert(data.err);
+        } else {
+            /* silent ignore */
+//            alert('Fejl ved kommunikation med server - genindlæs siden!');
+        }
+    },
+
+    // Add one element to the list
+    addNewElement: function(list, text) {
+        var li = $('<li class="ui-state-default">test</li>');
+        li.text(text);
+
+        list.append(li);
+    },
+
+    // Add a list to the tabs
+    addList: function(id, title, addedByUser) {
+        var tab_no = (this.next_tab_no++);
+        var tabs = $("#tabs");
+        var content = $('<div id="list-'+tab_no+'" class="filtered_list"></div>');
+
+        // store list id as meta data
+        content.data('tab_no', tab_no);
+        content.data('list_id', id);
+        content.data('list_status', 'not-loaded');
+
+        tabs.append(content);
+
+        // new tab
+        tabs.tabs("add", "#list-"+tab_no, title);
+
+        // allow to drop element on the tab
+        var tab = tabs.find("> ul:first > li > a[href='#list-"+tab_no+"']").parent();
+        var list_id = id;
+        tab.droppable({
+            accept: "ul.table li",
+            hoverClass: "ui-state-hover",
+            tolerance: 'pointer',
+            drop: function(ev, ui) {
+                var item = $(ui.draggable);
+                var element_id = item.data('element_id');
+                // Lets check if its the current list so we can ignore this
+                var cur_list_id = item.parents('div.filtered_list').data('list_id');
+                if (cur_list_id == list_id) {
+                    alert('Allerede i den liste.');
+                } else {
+                    // we have to mark the element dirty to avoid moving it
+                    // back again, since the jquery sortable api will also generate an
+                    // event
+                    SethManager.cancelMove = true;
+                    item.remove();
+                    SethManager.moveElementToOtherList(element_id, list_id);
+                }
+            }
+        });
+
+        if (addedByUser) {
+            // select the tab and load it
+            tabs.tabs("select", "list-"+tab_no);
+            // store order
+            this.storeFilterOrder();
+        }
+
+    },
+
+    // Remove a list from tabs
+    removeCurrentList: function() {
+        var tabs = $("#tabs");
+        var selected = tabs.tabs('option', 'selected');
+        if (selected != -1) {
+            tabs.tabs('remove', selected);
+            SethManager.storeFilterOrder();
+        }
+    },
+
+    // Store new filter order in view
+    storeFilterOrder: function() {
+        var tabs = $("#tabs ul:first > li > a");
+        var order = [];
+        $.each(tabs, function() {
+            order[order.length] = $($(this).attr("href")).data('list_id');
+        });
+        
+        // do callback
+        $.ajax({
+            type: 'POST',
+            data: {
+                action: 'setfilterorder',
+                view_id: this.view_id,
+                order: order
+            },
+            dataType: 'json',
+            error: function() {
+                SethManager.commError(this);
+            },
+            success: function(data) {
+                if (data && data.save) {
+                    // success, be quietly happy :-)
+                } else {
+                    SethManager.commError(this, data);
+                }
+            }
+        });
+    },
+
+    // Drag element from one list to another
+    moveElementToOtherList: function(id, filter_id) {
+        // do callback
+        $.ajax({
+            type: 'POST',
+            data: {
+                action: 'move_to_list',
+                element_id: id,
+                filter_id: filter_id
+            },
+            dataType: 'json',
+            error: function() {
+                SethManager.commError(this);
+            },
+            success: function(data) {
+                if (data && data.save) {
+                    // success
+                    SethManager.emptyListsAndUpdateCurrent();
+                } else {
+                    SethManager.commError(this, data);
+                }
+            }
+        });
+    },
+
+    // Sort element between groups or in same list
+    sortElement: function(event, ui) {
+        // ignore events send from other list (group by)
+        // (the other group will handle it)
+        if (ui.sender) return;
+
+        // ignore if moving into a tab
+        if (this.cancelMove) return;
+
+        // we need to find where we were placed
+        // element before, after, and which group
+        var item    = $(ui.item)
+        var elmId   = item.data('element_id');
+        var above   = item.prev('li');
+        var aboveId = above ? $(above).data('element_id') : null;
+        var below   = item.next('li');
+        var belowId = below ? $(below).data('element_id') : null;
+
+        // find our filter id
+        var filterList = item.parents('div.filtered_list');
+        var filterId = filterList.data('list_id');
+        var tabno = filterList.data('tab_no');
+        
+        // expect to have a group header just above the <ul />
+        var group_defined_as = item.parent().prev().data('group_defined_as');
+
+        if (!group_defined_as) {
+            alert('Fix me, cannot find my group');
+        }
+
+        // do callback
+        $.ajax({
+            type: 'POST',
+            data: {
+                action: 'move_element',
+                obj: {
+                    element_id: elmId,
+                    above_id: aboveId,
+                    below_id: belowId,
+                    group_defined_as: group_defined_as,
+                    filter_id: filterId
+                }
+            },
+            dataType: 'json',
+            error: function() {
+                SethManager.commError(this);
+            },
+            success: function(data) {
+                if (data && data.moved && data.list) {
+                    SethManager.fillInList(tabno, data.list);
+                    // flush other tabs
+                    SethManager.emptyAllListsButCurrent();
+                } else {
+                    SethManager.commError(this, data);
+                }
+            }
+        });
+    },
+
+    // Flushes all lists after update and updates the current one
+    // excludeCurrent - true to not update current (done otherwise)
+    emptyListsAndUpdateCurrent: function(excludeCurrent) {
+        var tabs = $("#tabs");
+        var selected = tabs.tabs('option', 'selected');
+        var curTabNo = null;
+        var curListId = null;
+        if (selected != -1) {
+            curTabNo = this.currentTabNo;
+            curListId = "#list-" + curTabNo;
+        }
+        // wipe all lists except current
+        $.each(tabs.find("> div"), function() {
+            if (("#"+this.id) != curListId) {
+                $(this).empty();
+                $(this).data('list_status', 'not-loaded');
+            }
+        });
+
+        // update current list
+        if (curListId && !excludeCurrent) {
+            this.updateList(curTabNo);
+        }
+    },
+
+    // Flush all lists except current
+    emptyAllListsButCurrent: function() {
+        this.emptyListsAndUpdateCurrent(true);
+    },
+
+    // Display tab, load content is not already done
+    showTab: function(event, ui) {
+        var panel = $(ui.panel);
+        if (panel.data('list_status') == 'not-loaded') {
+            this.updateList(panel.data('tab_no'));
+        }
+    },
+
+    // Delete specific element
+    deleteElement: function(id) {
+        // do callback
+        $.ajax({
+            type: 'POST',
+            data: {
+                action: 'delete_element',
+                element_id: id
+            },
+            dataType: 'json',
+            error: function() {
+                SethManager.commError(this);
+            },
+            success: function(data) {
+                if (data && data['delete']) {
+                    SethManager.emptyListsAndUpdateCurrent();
+                } else {
+                    SethManager.commError(this, data);
+                }
+            }
+        });
+    },
+
+// ---------------------- Edit Element dialog ----------------------------------
+//
+    // Open dialog to edit element
+    editElement: function(id) {
+        $("#editelement").data('element_id', id);
+        $("#editiframe").attr('src', 'edit.php?id=' + id);
+        $("#editelement").dialog("open");
+    },
+
+    closeEditDialog: function() {
+        $("#editelement").dialog("close");
+    },
+
+    updateThenCloseEditDialog: function(id, title) {
+        $(".element-"+id).text(title);
+        this.emptyListsAndUpdateCurrent();
+        this.closeEditDialog();
+    },
+
+    saveAndCloseEditDialog: function () {
+        $("#editiframe").contents().find("form").submit();
+    },
+
+    deleteAndCloseEditDialog: function () {
+        if (confirm('Er du sikker på at slette dette element?')) {
+            var elementId = $("#editelement").data('element_id');
+            $.each($(".element-"+elementId), function() {
+                $(this).parent().parent().remove();
+            });
+            this.deleteElement(elementId);
+            this.closeEditDialog();
+        }
+    },
+
+// ---------------------- Add Filter dialog -------------------------------------
+
+    // Open dialog to add list
+    openAddListDialog: function(id) {
+        $("#addlistiframe").attr('src', 'addfilter.php?group_id=' + this.group_id);
+        $("#addlist").dialog("open");
+    },
+
+    // Open dialog to edit list
+    openEditListDialog: function(id) {
+        var tabs = $("#tabs");
+        var selected = tabs.tabs('option', 'selected');
+        if (selected != -1) {
+            var id = $('#list-' + this.currentTabNo).data('list_id');
+
+            $("#addlistiframe").attr('src', 'editfilter.php?id=' + id);
+            $("#addlist").dialog("open");
+        }
+    },
+
+    closeAddListDialog: function() {
+        $("#addlist").dialog("close");
+    },
+
+    addListAndCloseDialog: function(id, title) {
+        // Notice that the dialog uses will escape the title parameter
+        // as an URL-encoded string so we must decode:
+        title = unescape(title);
+        this.closeAddListDialog();
+        this.addList(id, title, true);
+    },
+
+// ------------------------ addFilter.php ---------------------------------------
+    initAddFilter: function() {
+        // auto submit form when changing group
+        $("#group_id").bind('change', function() {
+            $(this).parents('form').get(0).submit();
+        });
+    },
+
+    removeFilter: function(id, title) {
+        if (confirm('Er du sikker på at slettet "'+unescape(title)+'" ?')) {
+            // do callback
+            $.ajax({
+                type: 'POST',
+                data: {
+                    action: 'remove',
+                    id: id,
+                },
+                dataType: 'json',
+                error: function() {
+                    alert('Server fejl');
+                },
+                success: function(data) {
+                    if (data && data.ok) {
+                        window.location.reload();
+                    }
+                }
+            });
+        }
+    }
+
+}
diff --git a/opdatering/js/view.js b/opdatering/js/view.js
new file mode 100644 (file)
index 0000000..dcacb4b
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * View list
+ */
+
+SethView = {
+
+    createNew: function(group_id) {
+        var name = prompt('Indtast navn:');
+        if (name) {
+            name = name.replace(/\s*/, "")
+            if (name != '') {
+                // do callback
+                $.ajax({
+                    type: 'POST',
+                    data: {
+                        action: 'new',
+                        title: name,
+                        group_id: group_id
+                    },
+                    dataType: 'json',
+                    error: function() {
+                        alert('Server fejl');
+                    },
+                    success: function(data) {
+                        if (data && data.ok) {
+                            window.location.reload();
+                        }
+                    }
+                });
+            }
+        }
+    },
+
+    rename: function(id, title) {
+        var name = prompt('Indtast navn:', unescape(title));
+        if (name) {
+            name = name.replace(/\s*/, "")
+            if (name != '') {
+                // do callback
+                $.ajax({
+                    type: 'POST',
+                    data: {
+                        action: 'rename',
+                        id: id,
+                        title: name
+                    },
+                    dataType: 'json',
+                    error: function() {
+                        alert('Server fejl');
+                    },
+                    success: function(data) {
+                        if (data && data.ok) {
+                            window.location.reload();
+                        }
+                    }
+                });
+            } else {
+                alert('Ugyldigt navn');
+            }
+        }
+    },
+
+    remove: function(id, title) {
+        if (confirm('Er du sikker på at slettet "'+unescape(title)+'" ?')) {
+            // do callback
+            $.ajax({
+                type: 'POST',
+                data: {
+                    action: 'remove',
+                    id: id,
+                },
+                dataType: 'json',
+                error: function() {
+                    alert('Server fejl');
+                },
+                success: function(data) {
+                    if (data && data.ok) {
+                        window.location.reload();
+                    }
+                }
+            });
+        }
+    }
+
+}
diff --git a/opdatering/list.php b/opdatering/list.php
new file mode 100644 (file)
index 0000000..67489c7
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+    require_once 'Seth/Controller/Page.php';
+    require_once 'Seth/Controller/List.php';
+
+    // Check view
+    $view_id = isset($_GET['id']) ? intval($_GET['id']) : null;
+
+    // Process callbacks
+    $list = new Seth_Controller_List($view_id);
+    $list->process();
+
+    $view = new Seth_Model_View($view_id);
+    if ($view->isNew()) {
+        echo 'View has been deleted/does not exist: ' . $view_id;
+        die();
+    }
+
+    Seth_Controller_Page::printTop();
+    $order = $list->getFilterOrder($view);
+
+    $title = $view->getTitle();
+    $group_id = $view->getGroupId();
+
+?>
+
+<script type="text/javascript">
+$(function() {
+    SethManager.init(<?=$view_id?>, <?=$group_id?>, <?=$order?>);
+});
+</script>
+
+<div id="page">
+
+<h1>Seth: <?=htmlentities($title)?></h1>
+
+<!-- Main page, tabs and lists -->
+
+<div id="actions">
+<a id="removelist-link" href="#">Fjern liste</a> |
+<a id="editlist-link" href="#">Redigér liste</a> |
+<a id="addlist-link" href="#">Tilføj ny liste</a>
+</div>
+
+<div id="tabs"><ul id="tabs-list"></ul></div>
+
+</div>
+
+<!-- Dialog to create a new list (iframe) -->
+<div id="addlist">
+    <iframe id="addlistiframe"></iframe>
+</div>
+
+<!-- Dialog to edit an element (iframe) -->
+<div id="editelement">
+    <iframe id="editiframe"></iframe>
+</div>
+
+
+<?php
+    Seth_Controller_Page::printBottom();
+?>
diff --git a/opdatering/views.php b/opdatering/views.php
new file mode 100644 (file)
index 0000000..c36faf7
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+require_once 'Seth/Model/View.php';
+require_once 'Seth/Model/Group.php';
+require_once 'Seth/Controller/View.php';
+I2_GUI_List::process();
+Seth_Controller_View::process();
+
+$group_id = isset($_GET['group_id']) ? intval($_GET['group_id']) : null;
+$group = new Seth_Model_Group($group_id);
+if ($group->isNew()) {
+    echo 'Forkert gruppe';
+    die();
+}
+
+$vendor = getSite('vendor');
+
+$head = <<<EOH
+<script type="text/javascript" src="http://$vendor/js/jquery.js"></script>
+<script type="text/javascript" src="js/view.js"></script>
+EOH;
+
+$page = new I2_GUI_Informational('Seth Projekt Organizer', 'TV 2 Net'."\n".'Seth is design and implemented by Kristian Kræmmer Nielsen <jkkn@tv2.dk>, Copyright (c) 2010');
+$page->printTop($head);
+$page->printSubtitle('Gruppe:');
+echo htmlentities($group->getTitle()) . '<br/><br/>';
+
+$page->printSubtitle('Views for '.$group->getTitle().':');
+$list = new I2_GUI_List();
+$list->enableFeatures(I2_GUI_LIST_TOTALCOUNT);
+$list->setHeaders(array(
+            'id' => 'Id',
+            'title' => 'Titel',
+            'actions' => 'Muligheder'));
+$list->setLimit(25);
+$list->setOrderBy('title');
+$list->setDataORM(Seth_Model_View::TYPE, 'group_id=?', $group_id);
+$list->setRowCallback('insertActions', 'Seth_Controller_View');
+$list->setRowLink('list.php?id=%d', 'id');
+echo $list->getHTML();
+
+?>
+<br/>
+<button onclick="SethView.createNew(<?=$group_id?>)">Opret nyt view</button>
+
+<?php
+
+$page->printBottom();
+
+?>
diff --git a/scripts/importer/access.php b/scripts/importer/access.php
new file mode 100644 (file)
index 0000000..b9f68a5
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+require_once 'Seth/Model/Element.php';
+require_once 'Seth/Model/Element/Descriptions.php';
+
+/** Convert Uploaded Access-tables */
+
+define('NL', "\n");
+
+// Table to group
+$tables =
+    array('Gazelle' => 3,
+          'Entreprenør' => 2);
+
+$mapToElement =
+    array(
+        'Prioritet' => 'priority',
+        'Område' => 'area',
+        'Overskrift' => 'title', 
+        'Tidsestimat' => 'estimate_development', 
+        'Estimat point' => 'estimate_development', 
+        'Faktisk tid' => 'actual_development', 
+        'Godkendt' => 'approved', 
+        'Design' => 'design', 
+        'Kontaktperson' => 'contact_person', 
+        'Kontaktperson - mail' => 'contact_person', 
+        'Færdig' => 'completed', 
+        'Slet' => 'deleted', 
+        'Bug' => 'bug', 
+        'Deadline' => 'deadline', 
+        'Designestimat' => 'estimate_design', 
+        'Grafiker faktisk tid' => 'actual_design', 
+        'Sprint' => 'sprint', 
+        'Færdiggørelsesdato' => 'completed_date', 
+        'PO tid' => 'po_time', 
+        'site' => 'site', 
+        'Ressource' => 'ressource', 
+        'Product Owner' => 'product_owner');
+$mapToDesc = array(
+        'User story' => 'user_story',
+        'Specifikationer' => 'specifications',
+        'Beskrivelse i gazellemappe' => 'material_in_shared_folder', 
+        'URL' => 'url');
+
+$bitFields = array('Godkendt', 'Design', 'Færdig', 'Slet', 'Bug', 'Beskrivelse i gazellemappe');
+
+$db = DB_PDO::get('seth');
+
+foreach ($tables as $table => $group) {
+
+ORM_Multiple::delete(ORM::find(Seth_Model_Element::TYPE, 'group_id=?', $group));
+
+echo 'Importing '.$table.'...' .NL;
+
+    $rows = $db->getAll('SELECT * FROM ' . $table);
+
+    foreach ($rows as $row) {
+
+        $element = new Seth_Model_Element();
+        
+        // convert bits
+        foreach ($bitFields as $field) {
+            if (isset($row[$field])) {
+                $row[$field] = ord($row[$field][0]) == 1; // Fix converting boolean from mysql
+            }
+        }
+
+        if ($table == 'Entreprenør') {
+            // Entreprenør
+            if (isset($row['Product Owner'])) {
+                $row['Product Owner'] = $db->getOne('SELECT Initialer FROM `Entreprenør_Product Owners` WHERE ID=?', $row['Product Owner']);
+            }
+            if (isset($row['Område'])) {
+                $row['Område'] = $db->getOne('SELECT Område FROM Entreprenør_Område WHERE Id=?', $row['Område']);
+            }
+        }
+
+        // Gazelle: Slå sites op og ressource
+        if ($table == 'Gazelle') {
+            if (isset($row['Ressource'])) {
+                $row['Ressource'] = $db->getOne('SELECT Felt1 FROM Gazelle_Ressource WHERE Id=?', $row['Ressource']);
+            }
+            if (isset($row['Område'])) {
+                $row['Område'] = $db->getOne('SELECT Område FROM Gazelle_Område WHERE Id=?', $row['Område']);
+            }
+            if (isset($row['site'])) {
+                $row['site'] = $db->getOne('SELECT Site FROM Gazelle_Sites WHERE Id=?', $row['site']);
+            }
+            $row['Product Owner'] = 'JEPH';
+        }
+
+        foreach ($mapToElement as $from => $to) {
+            if (isset($row[$from])) {
+                $element->set($to, $row[$from]);
+            }
+        }
+        $element->setGroupId($group);
+        $element->save();
+        $desc = new Seth_Model_Element_Descriptions($element->getId());
+        foreach ($mapToDesc as $from => $to) {
+            if (isset($row[$from])) {
+                $desc->_fields[$to] = $row[$from]; // hack
+            }
+        }
+        $desc->save();
+    }
+
+}
+
+echo "Done.".NL.NL;
+