diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b067092
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+tmp
+src/IDF/conf/idf.php
+src/IDF/conf/idf.test.php
+www/test.php
diff --git a/src/IDF/Conf.php b/src/IDF/Conf.php
new file mode 100644
index 0000000..94483b1
--- /dev/null
+++ b/src/IDF/Conf.php
@@ -0,0 +1,133 @@
+_a['table'] = 'idf_conf';
+ $this->_a['model'] = __CLASS__;
+ $this->_a['cols'] = array(
+ // It is mandatory to have an "id" column.
+ 'id' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Sequence',
+ //It is automatically added.
+ 'blank' => true,
+ ),
+ 'project' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'IDF_Project',
+ 'blank' => false,
+ 'verbose' => __('project'),
+ ),
+ 'vkey' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'size' => 50,
+ 'verbose' => __('key'),
+ ),
+ 'vdesc' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'size' => 250,
+ 'verbose' => __('value'),
+ ),
+ );
+ $this->_a['idx'] = array('project_vkey_idx' =>
+ array(
+ 'col' => 'project, vkey',
+ 'type' => 'unique',
+ ),
+ );
+ $this->f = new IDF_Conf_DataProxy($this);
+ }
+
+ function setProject($project)
+ {
+ $this->datacache = null;
+ $this->_project = $project;
+ }
+
+ function initCache()
+ {
+ $this->datacache = new ArrayObject();
+ $sql = new Pluf_SQL('project=%s', $this->_project);
+ foreach ($this->getList(array('filter' => $sql->gen())) as $val) {
+ $this->datacache[$val->vkey] = $val->vdesc;
+ }
+ }
+
+ /**
+ * FIXME: This is not efficient when setting a large number of
+ * values in a loop.
+ */
+ function setVal($key, $value)
+ {
+ if (!is_null($this->getVal($key, null))
+ and $value == $this->getVal($key)) {
+ return;
+ }
+ $this->delVal($key, false);
+ $conf = new IDF_Conf();
+ $conf->project = $this->_project;
+ $conf->vkey = $key;
+ $conf->vdesc = $value;
+ $conf->create();
+ $this->initCache();
+ }
+
+ function getVal($key, $default='')
+ {
+ if ($this->datacache === null) {
+ $this->initCache();
+ }
+ return (isset($this->datacache[$key])) ? $this->datacache[$key] : $default;
+ }
+
+ function delVal($key, $initcache=true)
+ {
+ $gconf = new IDF_Conf();
+ $sql = new Pluf_SQL('vkey=%s AND project=%s', array($key, $this->_project));
+ foreach ($gconf->getList(array('filter' => $sql->gen())) as $c) {
+ $c->delete();
+ }
+ if ($initcache) {
+ $this->initCache();
+ }
+ }
+}
diff --git a/src/IDF/Conf/DataProxy.php b/src/IDF/Conf/DataProxy.php
new file mode 100644
index 0000000..226b17b
--- /dev/null
+++ b/src/IDF/Conf/DataProxy.php
@@ -0,0 +1,42 @@
+obj = $obj;
+ }
+
+ public function __get($field)
+ {
+ return $this->obj->getVal($field);
+ }
+}
diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php
new file mode 100644
index 0000000..8f26c0b
--- /dev/null
+++ b/src/IDF/Form/IssueCreate.php
@@ -0,0 +1,261 @@
+user = $extra['user'];
+ $this->project = $extra['project'];
+ if ($this->user->hasPerm('IDF.project-owner', $this->project)
+ or $this->user->hasPerm('IDF.project-member', $this->project)) {
+ $this->show_full = true;
+ }
+ $this->fields['summary'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Summary'),
+ 'initial' => '',
+ 'widget_attrs' => array(
+ 'maxlength' => 200,
+ 'size' => 67,
+ ),
+ ));
+ $this->fields['content'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Description'),
+ 'initial' => '',
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ 'widget_attrs' => array(
+ 'cols' => 58,
+ 'rows' => 13,
+ ),
+ ));
+ if ($this->show_full) {
+ $this->fields['status'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Status'),
+ 'initial' => 'New',
+ 'widget_attrs' => array(
+ 'maxlength' => 20,
+ 'size' => 15,
+ ),
+ ));
+ $this->fields['owner'] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Owner'),
+ 'initial' => '',
+ 'widget_attrs' => array(
+ 'maxlength' => 20,
+ 'size' => 15,
+ ),
+ ));
+ for ($i=1;$i<7;$i++) {
+ $initial = '';
+ switch ($i) {
+ case 1:
+ $initial = 'Type:Defect';
+ break;
+ case 2:
+ $initial = 'Priority:Medium';
+ break;
+ }
+ $this->fields['label'.$i] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Labels'),
+ 'initial' => $initial,
+ 'widget_attrs' => array(
+ 'maxlength' => 50,
+ 'size' => 20,
+ ),
+ ));
+ }
+ }
+ }
+
+ /**
+ * Validate the interconnection in the form.
+ */
+ public function clean()
+ {
+ // We need to check that no label with the 'Status' class is
+ // given.
+ if (!$this->show_full) {
+ return $this->cleaned_data;
+ }
+ $conf = new IDF_Conf();
+ $conf->setProject($this->project);
+ $onemax = array();
+ foreach (split(',', $conf->getVal('labels_issue_one_max', IDF_Form_IssueTrackingConf::init_one_max)) as $class) {
+ if (trim($class) != '') {
+ $onemax[] = mb_strtolower(trim($class));
+ }
+ }
+ $count = array();
+ for ($i=1;$i<7;$i++) {
+ $this->cleaned_data['label'.$i] = trim($this->cleaned_data['label'.$i]);
+ if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
+ list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
+ list($class, $name) = array(mb_strtolower(trim($class)),
+ trim($name));
+ } else {
+ $class = 'other';
+ $name = $this->cleaned_data['label'.$i];
+ }
+ if ($class == 'status') {
+ if (!isset($this->errors['label'.$i])) $this->errors['label'.$i] = array();
+ $this->errors['label'.$i][] = __('You cannot add a label with the "Status" prefix to an issue.');
+ throw new Pluf_Form_Invalid(__('You provided an invalid label.'));
+ }
+ if (!isset($count[$class])) $count[$class] = 1;
+ else $count[$class] += 1;
+ if (in_array($class, $onemax) and $count[$class] > 1) {
+ if (!isset($this->errors['label'.$i])) $this->errors['label'.$i] = array();
+ $this->errors['label'.$i][] = sprintf(__('You cannot provide more than label from the %s class to an issue.'), $class);
+ throw new Pluf_Form_Invalid(__('You provided an invalid label.'));
+ }
+ }
+ return $this->cleaned_data;
+ }
+
+ function clean_status()
+ {
+ // Check that the status is in the list of official status
+ $tags = $this->project->getTagsFromConfig('labels_issue_open',
+ IDF_Form_IssueTrackingConf::init_open,
+ 'Status');
+ $tags = array_merge($this->project->getTagsFromConfig('labels_issue_closed',
+ IDF_Form_IssueTrackingConf::init_closed,
+ 'Status')
+ , $tags);
+ $found = false;
+ foreach ($tags as $tag) {
+ if ($tag->name == trim($this->cleaned_data['status'])) {
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ throw new Pluf_Form_Invalid(__('You provided an invalid status.'));
+ }
+ return $this->cleaned_data['status'];
+ }
+
+ /**
+ * Save the model in the database.
+ *
+ * @param bool Commit in the database or not. If not, the object
+ * is returned but not saved in the database.
+ * @return Object Model with data set from the form.
+ */
+ function save($commit=true)
+ {
+ if ($this->isValid()) {
+ // Add a tag for each label
+ $tags = array();
+ if ($this->show_full) {
+ for ($i=1;$i<7;$i++) {
+ if (strlen($this->cleaned_data['label'.$i]) > 0) {
+ if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
+ list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
+ list($class, $name) = array(trim($class), trim($name));
+ } else {
+ $class = 'Other';
+ $name = trim($this->cleaned_data['label'.$i]);
+ }
+ $tags[] = IDF_Tag::add($name, $this->project, $class);
+ }
+ }
+ } else {
+ $tags[] = IDF_Tag::add('Medium', $this->project, 'Priority');
+ $tags[] = IDF_Tag::add('Defect', $this->project, 'Type');
+ }
+ // Create the issue
+ $issue = new IDF_Issue();
+ $issue->project = $this->project;
+ $issue->submitter = $this->user;
+ if ($this->show_full) {
+ $issue->status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
+ $issue->owner = self::findUser($this->cleaned_data['owner']);
+ } else {
+ $_t = $this->project->getTagIdsByStatus('open');
+ $issue->status = new IDF_Tag($_t[0]); // first one is the default
+ $issue->owner = null;
+ }
+ $issue->summary = trim($this->cleaned_data['summary']);
+ $issue->create();
+ foreach ($tags as $tag) {
+ $issue->setAssoc($tag);
+ }
+ $issue->setAssoc($this->user); // the user is
+ // automatically
+ // interested.
+ // add the first comment
+ $comment = new IDF_IssueComment();
+ $comment->issue = $issue;
+ $comment->content = $this->cleaned_data['content'];
+ $comment->submitter = $this->user;
+ $comment->create();
+ return $issue;
+ }
+ throw new Exception(__('Cannot save the model from an invalid form.'));
+ }
+
+ /**
+ * Based on the given string, try to find the matching user.
+ *
+ * Search order is: email, login, last_name.
+ *
+ * If no user found, simply returns null.
+ *
+ * @param string User
+ * @return Pluf_User or null
+ */
+ public static function findUser($string)
+ {
+ $string = trim($string);
+ if (strlen($string) == 0) return null;
+ $guser = new Pluf_User();
+ foreach (array('email', 'login', 'last_name') as $what) {
+ $sql = new Pluf_SQL($what.'=%s', $string);
+ $users = $guser->getList(array('filter' => $sql->gen()));
+ if ($users->count() > 0) {
+ return $users[0];
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/IDF/Form/IssueTrackingConf.php b/src/IDF/Form/IssueTrackingConf.php
new file mode 100644
index 0000000..ae7a9d0
--- /dev/null
+++ b/src/IDF/Form/IssueTrackingConf.php
@@ -0,0 +1,107 @@
+fields['labels_issue_open'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Open issue status values'),
+ 'initial' => self::init_open,
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ 'widget_attrs' => array('rows' => 5,
+ 'cols' => 75),
+ ));
+ $this->fields['labels_issue_closed'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Closed issue status values'),
+ 'initial' => self::init_closed,
+ 'widget_attrs' => array('rows' => 7,
+ 'cols' => 75),
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ ));
+
+ $this->fields['labels_issue_predefined'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Predefined issue labels'),
+ 'initial' => self::init_predefined,
+ 'widget_attrs' => array('rows' => 7,
+ 'cols' => 75),
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ ));
+
+ $this->fields['labels_issue_one_max'] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Each issue may have at most one label with each of these classes'),
+ 'initial' => self::init_one_max,
+ 'widget_attrs' => array('size' => 60),
+ ));
+
+
+
+ }
+}
+
+
diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php
new file mode 100644
index 0000000..ae4b274
--- /dev/null
+++ b/src/IDF/Form/IssueUpdate.php
@@ -0,0 +1,260 @@
+user = $extra['user'];
+ $this->project = $extra['project'];
+ $this->issue = $extra['issue'];
+ if ($this->user->hasPerm('IDF.project-owner', $this->project)
+ or $this->user->hasPerm('IDF.project-member', $this->project)) {
+ $this->show_full = true;
+ }
+ if ($this->show_full) {
+ $this->fields['summary'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Summary'),
+ 'initial' => $this->issue->summary,
+ 'widget_attrs' => array(
+ 'maxlength' => 200,
+ 'size' => 67,
+ ),
+ ));
+ }
+ $this->fields['content'] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Comment'),
+ 'initial' => '',
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ 'widget_attrs' => array(
+ 'cols' => 58,
+ 'rows' => 9,
+ ),
+ ));
+ if ($this->show_full) {
+ $this->fields['status'] = new Pluf_Form_Field_Varchar(
+ array('required' => true,
+ 'label' => __('Status'),
+ 'initial' => $this->issue->get_status()->name,
+ 'widget_attrs' => array(
+ 'maxlength' => 20,
+ 'size' => 15,
+ ),
+ ));
+ $initial = ($this->issue->get_owner() == null) ? '' : $this->issue->get_owner()->login;
+ $this->fields['owner'] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Owner'),
+ 'initial' => $initial,
+ 'widget_attrs' => array(
+ 'maxlength' => 20,
+ 'size' => 15,
+ ),
+ ));
+ $tags = $this->issue->get_tags_list();
+ for ($i=1;$i<7;$i++) {
+ $initial = '';
+ if (isset($tags[$i-1])) {
+ if ($tags[$i-1]->class != 'Other') {
+ $initial = (string) $tags[$i-1];
+ } else {
+ $initial = $tags[$i-1]->name;
+ }
+ }
+ $this->fields['label'.$i] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Labels'),
+ 'initial' => $initial,
+ 'widget_attrs' => array(
+ 'maxlength' => 50,
+ 'size' => 20,
+ ),
+ ));
+ }
+ }
+ }
+
+ /**
+ * We check that something is really changed.
+ */
+ public function clean()
+ {
+ $this->cleaned_data = parent::clean();
+ // As soon as we know that at least one change was done, we
+ // return the cleaned data and do not go further.
+ if (strlen(trim($this->cleaned_data['content']))) {
+ return $this->cleaned_data;
+ }
+ if ($this->show_full) {
+ $status = $this->issue->get_status();
+ if (trim($this->cleaned_data['status']) != $status->name) {
+ return $this->cleaned_data;
+ }
+ if (trim($this->issue->summary) != trim($this->cleaned_data['summary'])) {
+ return $this->cleaned_data;
+ }
+ $owner = self::findUser($this->cleaned_data['owner']);
+ if ((is_null($owner) and !is_null($this->issue->get_owner()))
+ or (!is_null($owner) and is_null($this->issue->get_owner()))
+ or ((!is_null($owner) and !is_null($this->issue->get_owner())) and $owner->id != $this->issue->get_owner()->id)) {
+ return $this->cleaned_data;
+ }
+ $tags = array();
+ for ($i=1;$i<7;$i++) {
+ if (strlen($this->cleaned_data['label'.$i]) > 0) {
+ if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
+ list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
+ list($class, $name) = array(trim($class), trim($name));
+ } else {
+ $class = 'Other';
+ $name = trim($this->cleaned_data['label'.$i]);
+ }
+ $tags[] = array($class, $name);
+ }
+ }
+ $oldtags = $this->issue->get_tags_list();
+ foreach ($tags as $tag) {
+ $found = false;
+ foreach ($oldtags as $otag) {
+ if ($otag->class == $tag[0] and $otag->name == $tag[1]) {
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ // new tag not found in the old tags
+ return $this->cleaned_data;
+ }
+ }
+ foreach ($oldtags as $otag) {
+ $found = false;
+ foreach ($tags as $tag) {
+ if ($otag->class == $tag[0] and $otag->name == $tag[1]) {
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ // old tag not found in the new tags
+ return $this->cleaned_data;
+ }
+ }
+ }
+ // no changes!
+ throw new Pluf_Form_Invalid(__('No changes were entered.'));
+ }
+
+ /**
+ * Save the model in the database.
+ *
+ * @param bool Commit in the database or not. If not, the object
+ * is returned but not saved in the database.
+ * @return Object Model with data set from the form.
+ */
+ function save($commit=true)
+ {
+ if ($this->isValid()) {
+ if ($this->show_full) {
+ // Add a tag for each label
+ $tags = array();
+ $tagids = array();
+ for ($i=1;$i<7;$i++) {
+ if (strlen($this->cleaned_data['label'.$i]) > 0) {
+ if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
+ list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
+ list($class, $name) = array(trim($class), trim($name));
+ } else {
+ $class = 'Other';
+ $name = trim($this->cleaned_data['label'.$i]);
+ }
+ $tag = IDF_Tag::add($name, $this->project, $class);
+ $tags[] = $tag;
+ $tagids[] = $tag->id;
+ }
+ }
+ // Compare between the old and the new data
+ $changes = array();
+ $oldtags = $this->issue->get_tags_list();
+ foreach ($tags as $tag) {
+ if (!Pluf_Model_InArray($tag, $oldtags)) {
+ if (!isset($changes['lb'])) $changes['lb'] = array();
+ if ($tag->class != 'Other') {
+ $changes['lb'][] = (string) $tag; //new tag
+ } else {
+ $changes['lb'][] = (string) $tag->name;
+ }
+ }
+ }
+ foreach ($oldtags as $tag) {
+ if (!Pluf_Model_InArray($tag, $tags)) {
+ if (!isset($changes['lb'])) $changes['lb'] = array();
+ if ($tag->class != 'Other') {
+ $changes['lb'][] = '-'.(string) $tag; //new tag
+ } else {
+ $changes['lb'][] = '-'.(string) $tag->name;
+ }
+ }
+ }
+ // Status, summary and owner
+ $status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
+ if ($status->id != $this->issue->status) {
+ $changes['st'] = $status->name;
+ }
+ if (trim($this->issue->summary) != trim($this->cleaned_data['summary'])) {
+ $changes['su'] = trim($this->cleaned_data['summary']);
+ }
+ $owner = self::findUser($this->cleaned_data['owner']);
+ if ((is_null($owner) and !is_null($this->issue->get_owner()))
+ or (!is_null($owner) and is_null($this->issue->get_owner()))
+ or ((!is_null($owner) and !is_null($this->issue->get_owner())) and $owner->id != $this->issue->get_owner()->id)) {
+ $changes['ow'] = (is_null($owner)) ? '---' : $owner->login;
+ }
+ // Update the issue
+ $this->issue->batchAssoc('IDF_Tag', $tagids);
+ $this->issue->summary = trim($this->cleaned_data['summary']);
+ $this->issue->status = $status;
+ $this->issue->owner = $owner;
+ }
+ // Create the comment
+ $comment = new IDF_IssueComment();
+ $comment->issue = $this->issue;
+ $comment->content = $this->cleaned_data['content'];
+ $comment->submitter = $this->user;
+ if (!$this->show_full) $changes = array();
+ $comment->changes = $changes;
+ $comment->create();
+ $this->issue->update();
+ return $this->issue;
+ }
+ throw new Exception(__('Cannot save the model from an invalid form.'));
+ }
+}
diff --git a/src/IDF/Form/MembersConf.php b/src/IDF/Form/MembersConf.php
new file mode 100644
index 0000000..c919aa2
--- /dev/null
+++ b/src/IDF/Form/MembersConf.php
@@ -0,0 +1,86 @@
+project = $extra['project'];
+
+ $this->fields['owners'] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Project owners'),
+ 'initial' => '',
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ 'widget_attrs' => array('rows' => 5,
+ 'cols' => 40),
+ ));
+ $this->fields['members'] = new Pluf_Form_Field_Varchar(
+ array('required' => false,
+ 'label' => __('Project members'),
+ 'widget_attrs' => array('rows' => 7,
+ 'cols' => 40),
+ 'widget' => 'Pluf_Form_Widget_TextareaInput',
+ ));
+ }
+
+ public function save($commit=true)
+ {
+ if (!$this->isValid()) {
+ throw new Exception(__('Cannot save the model from an invalid form.'));
+ }
+ // remove all the permissions
+ $cm = $this->project->getMembershipData();
+ $def = array('owners' => Pluf_Permission::getFromString('IDF.project-owner'),
+ 'members' => Pluf_Permission::getFromString('IDF.project-member'));
+ $guser = new Pluf_User();
+ foreach ($def as $key=>$perm) {
+ foreach ($cm[$key] as $user) {
+ Pluf_RowPermission::remove($user, $this->project, $perm);
+ }
+ foreach (preg_split("/\015\012|\015|\012|\,/", $this->cleaned_data[$key], -1, PREG_SPLIT_NO_EMPTY) as $login) {
+ $sql = new Pluf_SQL('login=%s', array(trim($login)));
+ $users = $guser->getList(array('filter'=>$sql->gen()));
+ if ($users->count() == 1) {
+ Pluf_RowPermission::add($users[0], $this->project, $perm);
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/src/IDF/Issue.php b/src/IDF/Issue.php
new file mode 100644
index 0000000..9341947
--- /dev/null
+++ b/src/IDF/Issue.php
@@ -0,0 +1,158 @@
+_a['table'] = 'idf_issues';
+ $this->_a['model'] = __CLASS__;
+ $this->_a['cols'] = array(
+ // It is mandatory to have an "id" column.
+ 'id' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Sequence',
+ 'blank' => true,
+ ),
+ 'project' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'IDF_Project',
+ 'blank' => false,
+ 'verbose' => __('project'),
+ 'relate_name' => 'issues',
+ ),
+ 'summary' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'size' => 250,
+ 'verbose' => __('summary'),
+ ),
+ 'submitter' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'Pluf_User',
+ 'blank' => false,
+ 'verbose' => __('submitter'),
+ 'relate_name' => 'submitted_issue',
+ ),
+ 'owner' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'Pluf_User',
+ 'blank' => true, // no owner when submitted.
+ 'is_null' => true,
+ 'verbose' => __('owner'),
+ 'relate_name' => 'owned_issue',
+ ),
+ 'interested' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Manytomany',
+ 'model' => 'Pluf_User',
+ 'blank' => true,
+ 'verbose' => __('interested users'),
+ 'help_text' => __('Interested users will get an email notification when the issue is changed.'),
+ ),
+ 'tags' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Manytomany',
+ 'blank' => true,
+ 'model' => 'IDF_Tag',
+ 'verbose' => __('labels'),
+ ),
+ 'status' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'blank' => false,
+ 'model' => 'IDF_Tag',
+ 'verbose' => __('status'),
+ ),
+ 'creation_dtime' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Datetime',
+ 'blank' => true,
+ 'verbose' => __('creation date'),
+ ),
+ 'modif_dtime' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Datetime',
+ 'blank' => true,
+ 'verbose' => __('modification date'),
+ ),
+ );
+ $this->_a['idx'] = array(
+ 'modif_dtime_idx' =>
+ array(
+ 'col' => 'modif_dtime',
+ 'type' => 'normal',
+ ),
+ );
+ $table = $this->_con->pfx.'idf_issue_idf_tag_assoc';
+ $this->_a['views'] = array(
+ 'join_tags' =>
+ array(
+ 'join' => 'LEFT JOIN '.$table
+ .' ON idf_issue_id=id',
+ ),
+ );
+ }
+
+ function __toString()
+ {
+ return $this->id.' - '.$this->summary;
+ }
+
+ function _toIndex()
+ {
+ return '';
+ }
+
+
+ function preSave()
+ {
+ if ($this->id == '') {
+ $this->creation_dtime = gmdate('Y-m-d H:i:s');
+ }
+ $this->modif_dtime = gmdate('Y-m-d H:i:s');
+ }
+
+ function postSave()
+ {
+ // This will be used to fire the indexing or send a
+ // notification email to the interested people, etc.
+ $q = new Pluf_Queue();
+ $q->model_class = __CLASS__;
+ $q->model_id = $this->id;
+ $q->action = 'updated';
+ $q->lock = 0;
+ $q->create();
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/IssueComment.php b/src/IDF/IssueComment.php
new file mode 100644
index 0000000..8d82b94
--- /dev/null
+++ b/src/IDF/IssueComment.php
@@ -0,0 +1,119 @@
+_a['table'] = 'idf_issuecomments';
+ $this->_a['model'] = __CLASS__;
+ $this->_a['cols'] = array(
+ // It is mandatory to have an "id" column.
+ 'id' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Sequence',
+ 'blank' => true,
+ ),
+ 'issue' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'IDF_Issue',
+ 'blank' => false,
+ 'verbose' => __('issue'),
+ 'relate_name' => 'comments',
+ ),
+ 'content' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Text',
+ 'blank' => false,
+ 'verbose' => __('comment'),
+ ),
+ 'submitter' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'Pluf_User',
+ 'blank' => false,
+ 'verbose' => __('submitter'),
+ 'relate_name' => 'commented_issue',
+ ),
+ 'changes' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Serialized',
+ 'blank' => true,
+ 'verbose' => __('changes'),
+ 'help_text' => __('Serialized array of the changes in the issue.'),
+ ),
+ 'creation_dtime' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Datetime',
+ 'blank' => true,
+ 'verbose' => __('creation date'),
+ ),
+ );
+ $this->_a['idx'] = array(
+ 'creation_dtime_idx' =>
+ array(
+ 'col' => 'creation_dtime',
+ 'type' => 'normal',
+ ),
+ );
+ }
+
+ function changedIssue()
+ {
+ return count($this->changes) > 0;
+ }
+
+ function _toIndex()
+ {
+ return $this->content;
+ }
+
+ function preSave()
+ {
+ if ($this->id == '') {
+ $this->creation_dtime = gmdate('Y-m-d H:i:s');
+ }
+ }
+
+ function postSave()
+ {
+ // This will be used to fire the indexing or send a
+ // notification email to the interested people, etc.
+ $q = new Pluf_Queue();
+ $q->model_class = __CLASS__;
+ $q->model_id = $this->id;
+ $q->action = 'updated';
+ $q->lock = 0;
+ $q->create();
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php
new file mode 100644
index 0000000..741c462
--- /dev/null
+++ b/src/IDF/Middleware.php
@@ -0,0 +1,66 @@
+project to the project.
+ *
+ * The url to match a project is in the format
+ * /p/(\w+)/whatever. This means that it will not try to match on
+ * /login/ or /logout/.
+ *
+ * @param Pluf_HTTP_Request The request
+ * @return bool false or redirect.
+ */
+ function process_request(&$request)
+ {
+ $match = array();
+ if (preg_match('#^/p/(\w+)/#', $request->query, $match)) {
+ $request->project = IDF_Project::getOr404($match[1]);
+ }
+ return false;
+ }
+}
+
+
+function IDF_Middleware_ContextPreProcessor($request)
+{
+ $c = array();
+ if (isset($request->project)) {
+ $c['project'] = $request->project;
+ $c['isOwner'] = $request->user->hasPerm('IDF.project-owner',
+ $request->project);
+ $c['isMember'] = $request->user->hasPerm('IDF.project-member',
+ $request->project);
+ }
+ return $c;
+}
+
diff --git a/src/IDF/Migrations/Install.php b/src/IDF/Migrations/Install.php
new file mode 100644
index 0000000..4cc6d26
--- /dev/null
+++ b/src/IDF/Migrations/Install.php
@@ -0,0 +1,79 @@
+model = new $model();
+ $schema->createTables();
+ }
+ // Install the permissions
+ $perm = new Pluf_Permission();
+ $perm->name = 'Project membership';
+ $perm->code_name = 'project-member';
+ $perm->description = 'Permission given to project members.';
+ $perm->application = 'IDF';
+ $perm->create();
+ $perm = new Pluf_Permission();
+ $perm->name = 'Project ownership';
+ $perm->code_name = 'project-owner';
+ $perm->description = 'Permission given to project owners.';
+ $perm->application = 'IDF';
+ $perm->create();
+}
+
+function IDF_Migrations_Install_teardown($params=null)
+{
+ $perm = Pluf_Permission::getFromString('IDF.project-member');
+ if ($perm) $perm->delete();
+ $perm = Pluf_Permission::getFromString('IDF.project-owner');
+ if ($perm) $perm->delete();
+ $models = array(
+ 'IDF_Conf',
+ 'IDF_IssueComment',
+ 'IDF_Issue',
+ 'IDF_Tag',
+ 'IDF_Project',
+ );
+ $db = Pluf::db();
+ $schema = new Pluf_DB_Schema($db);
+ foreach ($models as $model) {
+ $schema->model = new $model();
+ $schema->dropTables();
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Precondition.php b/src/IDF/Precondition.php
new file mode 100644
index 0000000..9fc1f76
--- /dev/null
+++ b/src/IDF/Precondition.php
@@ -0,0 +1,43 @@
+user->hasPerm('IDF.project-owner', $request->project)) {
+ return true;
+ }
+ return new Pluf_HTTP_Response_Forbidden($request);
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Project.php b/src/IDF/Project.php
new file mode 100644
index 0000000..75f11a6
--- /dev/null
+++ b/src/IDF/Project.php
@@ -0,0 +1,280 @@
+_a['table'] = 'idf_projects';
+ $this->_a['model'] = __CLASS__;
+ $this->_a['cols'] = array(
+ // It is mandatory to have an "id" column.
+ 'id' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Sequence',
+ 'blank' => true,
+ ),
+ 'name' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'size' => 250,
+ 'verbose' => __('name'),
+ ),
+ 'shortname' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'size' => 50,
+ 'verbose' => __('short name'),
+ 'help_text' => __('Used in the url to access the project, must be short with only letters and numbers.'),
+ 'unique' => true,
+ ),
+ 'description' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Text',
+ 'blank' => false,
+ 'size' => 250,
+ 'verbose' => __('description'),
+ 'help_text' => __('The description can be extended using the markdown syntax.'),
+ ),
+ );
+ $this->_a['idx'] = array( );
+ }
+
+
+ /**
+ * String representation of the abstract.
+ */
+ function __toString()
+ {
+ return $this->name;
+ }
+
+ /**
+ * String ready for indexation.
+ */
+ function _toIndex()
+ {
+ return '';
+ }
+
+
+ function preSave()
+ {
+ if ($this->id == '') {
+ $this->creation_dtime = gmdate('Y-m-d H:i:s');
+ }
+ $this->modif_dtime = gmdate('Y-m-d H:i:s');
+ }
+
+ public static function getOr404($shortname)
+ {
+ $sql = new Pluf_SQL('shortname=%s', array(trim($shortname)));
+ $projects = Pluf::factory(__CLASS__)->getList(array('filter' => $sql->gen()));
+ if ($projects->count() != 1) {
+ throw new Pluf_HTTP_Error404(sprintf(__('Project "%s" not found.'),
+ $shortname));
+ }
+ return $projects[0];
+ }
+
+ /**
+ * Returns the number of open/closed issues.
+ *
+ * @param string Status ('open'), 'closed'
+ * @param IDF_Tag Subfilter with a label (null)
+ * @return int Count
+ */
+ public function getIssueCountByStatus($status='open', $label=null)
+ {
+ switch ($status) {
+ case 'open':
+ $key = 'labels_issue_open';
+ $default = IDF_Form_IssueTrackingConf::init_open;
+ break;
+ case 'closed':
+ default:
+ $key = 'labels_issue_closed';
+ $default = IDF_Form_IssueTrackingConf::init_closed;
+ break;
+ }
+ $tags = array();
+ foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) {
+ $tags[] = (int)$tag->id;
+ }
+ if (count($tags) == 0) return array();
+ $sql = new Pluf_SQL(sprintf('project=%%s AND status IN (%s)', implode(', ', $tags)), array($this->id));
+ if (!is_null($label)) {
+ $sql2 = new Pluf_SQL('idf_tag_id=%s', array($label->id));
+ $sql->SAnd($sql2);
+ }
+ $params = array('filter' => $sql->gen());
+ if (!is_null($label)) { $params['view'] = 'join_tags'; }
+ $gissue = new IDF_Issue();
+ return $gissue->getCount($params);
+ }
+
+ /**
+ * Get the open/closed tag ids as they are often used when doing
+ * listings.
+ *
+ * @param string Status ('open') or 'closed'
+ * @return array Ids of the open/closed tags
+ */
+ public function getTagIdsByStatus($status='open')
+ {
+ switch ($status) {
+ case 'open':
+ $key = 'labels_issue_open';
+ $default = IDF_Form_IssueTrackingConf::init_open;
+ break;
+ case 'closed':
+ default:
+ $key = 'labels_issue_closed';
+ $default = IDF_Form_IssueTrackingConf::init_closed;
+ break;
+ }
+ $tags = array();
+ foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) {
+ $tags[] = (int) $tag->id;
+ }
+ return $tags;
+ }
+
+ /**
+ * Convert the definition of tags in the configuration into the
+ * corresponding list of tags.
+ *
+ * @param string Configuration key where the tag is.
+ * @param string Default config if nothing in the db.
+ * @param string Default class.
+ * @return array List of tags
+ */
+ public function getTagsFromConfig($cfg_key, $default, $dclass='Other')
+ {
+ $conf = new IDF_Conf();
+ $conf->setProject($this);
+ $tags = array();
+ foreach (preg_split("/\015\012|\015|\012/", $conf->getVal($cfg_key, $default), -1, PREG_SPLIT_NO_EMPTY) as $s) {
+ $_s = split('=', $s, 2);
+ $v = trim($_s[0]);
+ $_v = split(':', $v, 2);
+ if (count($_v) > 1) {
+ $class = trim($_v[0]);
+ $name = trim($_v[1]);
+ } else {
+ $name = trim($_s[0]);
+ $class = $dclass;
+ }
+ $tags[] = IDF_Tag::add($name, $this, $class);
+ }
+ return $tags;
+ }
+
+ /**
+ * Return membership data.
+ *
+ * The array has 2 keys: 'members' and 'owners'.
+ *
+ * The list of users is only taken using the row level permission
+ * table. That is, if you set a user as administrator, he will
+ * have the member and owner rights but will not appear in the
+ * lists.
+ *
+ * @param string Format ('objects'), 'string'.
+ * @return mixed Array of Pluf_User or newline separated list of logins.
+ */
+ public function getMembershipData($fmt='objects')
+ {
+ $mperm = Pluf_Permission::getFromString('IDF.project-member');
+ $operm = Pluf_Permission::getFromString('IDF.project-owner');
+ $grow = new Pluf_RowPermission();
+ $db =& Pluf::db();
+ $false = Pluf_DB_BooleanToDb(false, $db);
+ $sql = new Pluf_SQL('model_class=%s AND model_id=%s AND owner_class=%s AND permission=%s AND negative='.$false,
+ array('IDF_Project', $this->id, 'Pluf_User', $operm->id));
+ $owners = array();
+ foreach ($grow->getList(array('filter' => $sql->gen())) as $row) {
+ if ($fmt == 'objects') {
+ $owners[] = Pluf::factory('Pluf_User', $row->owner_id);
+ } else {
+ $owners[] = Pluf::factory('Pluf_User', $row->owner_id)->login;
+ }
+ }
+ $sql = new Pluf_SQL('model_class=%s AND model_id=%s AND owner_class=%s AND permission=%s AND negative=0',
+ array('IDF_Project', $this->id, 'Pluf_User', $mperm->id));
+ $members = array();
+ foreach ($grow->getList(array('filter' => $sql->gen())) as $row) {
+ if ($fmt == 'objects') {
+ $members[] = Pluf::factory('Pluf_User', $row->owner_id);
+ } else {
+ $members[] = Pluf::factory('Pluf_User', $row->owner_id)->login;
+ }
+ }
+ if ($fmt == 'objects') {
+ return array('members' => $members, 'owners' => $owners);
+ } else {
+ return array('members' => implode("\n", $members),
+ 'owners' => implode("\n", $owners));
+ }
+ }
+
+ /**
+ * Generate the tag clouds.
+ *
+ * Return an array of tags sorted class, then name. Each tag get
+ * the extra property 'nb_use' for the number of use in the
+ * project. Only open issues are used to generate the cloud.
+ *
+ * @return ArrayObject of IDF_Tag
+ */
+ public function getTagCloud()
+ {
+ $tag_t = Pluf::factory('IDF_Tag')->getSqlTable();
+ $issue_t = Pluf::factory('IDF_Issue')->getSqlTable();
+ $asso_t = $this->_con->pfx.'idf_issue_idf_tag_assoc';
+ $ostatus = $this->getTagIdsByStatus('open');
+ if (count($ostatus) == 0) $ostatus[] = 0;
+ $sql = sprintf('SELECT '.$tag_t.'.id AS id, COUNT(*) AS nb_use FROM '.$tag_t.' '."\n".
+ 'LEFT JOIN '.$asso_t.' ON idf_tag_id='.$tag_t.'.id '."\n".
+ 'LEFT JOIN '.$issue_t.' ON idf_issue_id='.$issue_t.'.id '."\n".
+ 'WHERE idf_tag_id NOT NULL AND '.$issue_t.'.status IN (%s) AND '.$issue_t.'.project='.$this->id.' GROUP BY '.$tag_t.'.id ORDER BY '.$tag_t.'.class ASC, '.$tag_t.'.name ASC',
+ implode(', ', $ostatus));
+ $tags = array();
+ foreach ($this->_con->select($sql) as $idc) {
+ $tag = new IDF_Tag($idc['id']);
+ $tag->nb_use = $idc['nb_use'];
+ $tags[] = $tag;
+ }
+ return $tags;
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Tag.php b/src/IDF/Tag.php
new file mode 100644
index 0000000..9bf2e3b
--- /dev/null
+++ b/src/IDF/Tag.php
@@ -0,0 +1,133 @@
+_a['table'] = 'idf_tags';
+ $this->_a['model'] = __CLASS__;
+ $this->_a['cols'] = array(
+ // It is mandatory to have an "id" column.
+ 'id' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Sequence',
+ //It is automatically added.
+ 'blank' => true,
+ ),
+ 'project' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Foreignkey',
+ 'model' => 'IDF_Project',
+ 'blank' => false,
+ 'verbose' => __('project'),
+ ),
+ 'class' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'default' => IDF_TAG_DEFAULT_CLASS,
+ 'verbose' => __('tag class'),
+ 'help_text' => __('The class of the tag.'),
+ ),
+ 'name' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'verbose' => __('name'),
+ ),
+ 'lcname' =>
+ array(
+ 'type' => 'Pluf_DB_Field_Varchar',
+ 'blank' => false,
+ 'editable' => false,
+ 'verbose' => __('lcname'),
+ 'help_text' => __('Lower case version of the name for fast searching.'),
+ ),
+ );
+
+ $this->_a['idx'] = array(
+ 'lcname_idx' =>
+ array(
+ 'col' => 'lcname',
+ 'type' => 'normal',
+ ),
+ 'class_idx' =>
+ array(
+ 'col' => 'class',
+ 'type' => 'normal',
+ ),
+ );
+ }
+
+ function preSave()
+ {
+ $this->lcname = mb_strtolower($this->name);
+ }
+
+ /**
+ * Add a tag if not already existing.
+ *
+ * @param string Name of the tag.
+ * @param IDF_Project Project of the tag.
+ * @param string Class of the tag (IDF_TAG_DEFAULT_CLASS)
+ * @return IDF_Tag The tag.
+ */
+ public static function add($name, $project, $class=IDF_TAG_DEFAULT_CLASS)
+ {
+ $class = trim($class);
+ $name = trim($name);
+ $gtag = new IDF_Tag();
+ $sql = new Pluf_SQL('class=%s AND lcname=%s AND project=%s',
+ array($class, mb_strtolower($name), $project->id));
+ $tags = $gtag->getList(array('filter' => $sql->gen()));
+ if ($tags->count() < 1) {
+ // create a new tag
+ $tag = new IDF_Tag();
+ $tag->name = $name;
+ $tag->class = $class;
+ $tag->project = $project;
+ $tag->create();
+ return $tag;
+ }
+ return $tags[0];
+ }
+
+ function __toString()
+ {
+ if ($this->class != IDF_TAG_DEFAULT_CLASS) {
+ return $this->class.':'.$this->name;
+ }
+ return $this->name;
+ }
+
+}
diff --git a/src/IDF/Tests/TestIssue.php b/src/IDF/Tests/TestIssue.php
new file mode 100644
index 0000000..a6c300f
--- /dev/null
+++ b/src/IDF/Tests/TestIssue.php
@@ -0,0 +1,138 @@
+projects = array();
+ $this->users = array();
+ for ($i=1;$i<3;$i++) {
+ $project = new IDF_Project();
+ $project->name = 'Test project '.$i;
+ $project->shortname = 'test'.$i;
+ $project->description = sprintf('This is a test project %d.', $i);
+ $project->create();
+ $this->projects[] = $project;
+ $user = new Pluf_User();
+ $user->last_name = 'user'.$i;
+ $user->login = 'user'.$i;
+ $user->email = 'user'.$i.'@example.com';
+ $user->create();
+ $this->users[] = $user;
+ }
+ }
+
+
+ public function tearDown()
+ {
+ // This will drop cascading issues, comments and tags.
+ foreach ($this->projects as $proj) {
+ $proj->delete();
+ }
+ foreach ($this->users as $u) {
+ $u->delete();
+ }
+ }
+
+ public function testCreate()
+ {
+ $issue = new IDF_Issue();
+ $issue->project = $this->projects[0];
+ $issue->summary = 'This is a test issue';
+ $issue->submitter = $this->users[0];
+ $issue->create();
+ $this->assertEqual(1, $issue->id);
+ $this->assertIdentical(null, $issue->get_owner());
+ $this->assertNotIdentical(null, $issue->get_submitter());
+ }
+
+ public function testCreateMultiple()
+ {
+ for ($i=1;$i<11;$i++) {
+ $issue = new IDF_Issue();
+ $issue->project = $this->projects[0];
+ $issue->summary = 'This is a test issue '.$i;
+ $issue->submitter = $this->users[0];
+ $issue->owner = $this->users[1];
+ $issue->create();
+ }
+ for ($i=11;$i<16;$i++) {
+ $issue = new IDF_Issue();
+ $issue->project = $this->projects[1];
+ $issue->summary = 'This is a test issue '.$i;
+ $issue->submitter = $this->users[1];
+ $issue->create();
+ }
+ $this->assertEqual(10,
+ $this->projects[0]->get_issues_list()->count());
+ $this->assertEqual(5,
+ $this->projects[1]->get_issues_list()->count());
+ $this->assertEqual(5,
+ $this->users[1]->get_submitted_issue_list()->count());
+ $this->assertEqual(10,
+ $this->users[0]->get_submitted_issue_list()->count());
+ $this->assertEqual(10,
+ $this->users[1]->get_owned_issue_list()->count());
+ $this->assertEqual(0,
+ $this->users[1]->get_owned_issue_list(array('filter' => 'project='.(int)$this->projects[1]->id))->count());
+ $this->assertEqual(10,
+ $this->users[1]->get_owned_issue_list(array('filter' => 'project='.(int)$this->projects[0]->id))->count());
+ }
+
+ public function testAddIssueComment()
+ {
+ $issue = new IDF_Issue();
+ $issue->project = $this->projects[0];
+ $issue->summary = 'This is a test issue';
+ $issue->submitter = $this->users[0];
+ $issue->create();
+ $ic = new IDF_IssueComment();
+ $ic->issue = $issue;
+ $ic->submitter = $this->users[0];
+ $ic->content = 'toto';
+ $changes = array('s' => 'New summary',
+ 'st' => 'Active',
+ 't' => '-OS:Linux OS:Windows');
+ $ic->changes = $changes;
+ $ic->create();
+ $comments = $issue->get_comments_list();
+ $this->assertEqual($changes, $comments[0]->changes);
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Tests/TestProject.php b/src/IDF/Tests/TestProject.php
new file mode 100644
index 0000000..03a1665
--- /dev/null
+++ b/src/IDF/Tests/TestProject.php
@@ -0,0 +1,77 @@
+getList() as $proj) {
+ $proj->delete();
+ }
+ }
+
+ public function testCreate()
+ {
+ $gproj = Pluf::factory('IDF_Project')->getList();
+ $this->assertEqual(0, $gproj->count());
+ $project = new IDF_Project();
+ $project->name = 'Test project';
+ $project->shortname = 'test';
+ $project->description = 'This is a test project.';
+ $project->create();
+ $id = $project->id;
+ $p2 = new IDF_Project($id);
+ $this->assertEqual($p2->shortname, $project->shortname);
+ }
+
+ public function testMultipleCreate()
+ {
+ $project = new IDF_Project();
+ $project->name = 'Test project';
+ $project->shortname = 'test';
+ $project->description = 'This is a test project.';
+ $project->create();
+ try {
+ $project = new IDF_Project();
+ $project->name = 'Test project';
+ $project->shortname = 'test';
+ $project->description = 'This is a test project.';
+ $project->create();
+ // if here it as failed
+ $this->fail('It should not be possible to create 2 projects with same shortname');
+ } catch (Exception $e) {
+ $this->pass();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Views.php b/src/IDF/Views.php
new file mode 100644
index 0000000..fb90764
--- /dev/null
+++ b/src/IDF/Views.php
@@ -0,0 +1,64 @@
+getList();
+ return Pluf_Shortcuts_RenderToResponse('index.html',
+ array('page_title' => __('Projects'),
+ 'projects' => $projects),
+ $request);
+ }
+
+ /**
+ * Login view.
+ */
+ public function login($request, $match)
+ {
+ $v = new Pluf_Views();
+ return $v->login($request, $match, Pluf::f('login_success_url'));
+ }
+
+ /**
+ * Logout view.
+ */
+ function logout($request, $match)
+ {
+ $views = new Pluf_Views();
+ return $views->logout($request, $match, Pluf::f('after_logout_page'));
+ }
+
+}
\ No newline at end of file
diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php
new file mode 100644
index 0000000..53d4a0b
--- /dev/null
+++ b/src/IDF/Views/Issue.php
@@ -0,0 +1,371 @@
+project;
+ $title = sprintf(__('%s Recent Issues'), (string) $prj);
+ // Get stats about the issues
+ $open = $prj->getIssueCountByStatus('open');
+ $closed = $prj->getIssueCountByStatus('closed');
+ // Paginator to paginate the issues
+ $pag = new Pluf_Paginator(new IDF_Issue());
+ $pag->class = 'recent-issues';
+ $pag->item_extra_props = array('project_m' => $prj,
+ 'shortname' => $prj->shortname);
+ $pag->summary = __('This table shows the open recent issues.');
+ $otags = $prj->getTagIdsByStatus('open');
+ if (count($otags) == 0) $otags[] = 0;
+ $pag->forced_where = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $otags).')', array($prj->id));
+ $pag->action = array('IDF_Views_Issue::index', array($prj->shortname));
+ $pag->sort_order = array('modif_dtime', 'DESC');
+ $list_display = array(
+ 'id' => __('Id'),
+ array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')),
+ array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
+ array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
+ );
+ $pag->configure($list_display, array(), array('status', 'modif_dtime'));
+ $pag->items_per_page = 10;
+ $pag->no_results_text = __('No issues were found.');
+ $pag->setFromRequest($request);
+ return Pluf_Shortcuts_RenderToResponse('issues/index.html',
+ array('project' => $prj,
+ 'page_title' => $title,
+ 'open' => $open,
+ 'closed' => $closed,
+ 'issues' => $pag,
+ ),
+ $request);
+ }
+
+ /**
+ * View the issues of a given user.
+ *
+ * Only open issues are shown.
+ */
+ public $myIssues_precond = array('Pluf_Precondition::loginRequired');
+ public function myIssues($request, $match)
+ {
+ $prj = $request->project;
+ $otags = $prj->getTagIdsByStatus('open');
+ if (count($otags) == 0) $otags[] = 0;
+ if ($match[2] == 'submit') {
+ $title = sprintf(__('My Submitted %s Issues'), (string) $prj);
+ $f_sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
+ } else {
+ $title = sprintf(__('My Working %s Issues'), (string) $prj);
+ $f_sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
+ }
+ // Get stats about the issues
+ $sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
+ $nb_submit = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
+ $sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
+ $nb_owner = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
+ // Paginator to paginate the issues
+ $pag = new Pluf_Paginator(new IDF_Issue());
+ $pag->class = 'recent-issues';
+ $pag->item_extra_props = array('project_m' => $prj,
+ 'shortname' => $prj->shortname);
+ $pag->summary = __('This table shows the open recent issues.');
+ $pag->forced_where = $f_sql;
+ $pag->action = array('IDF_Views_Issue::myIssues', array($prj->shortname, $match[2]));
+ $pag->sort_order = array('modif_dtime', 'DESC');
+ $list_display = array(
+ 'id' => __('Id'),
+ array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')),
+ array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
+ array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
+ );
+ $pag->configure($list_display, array(), array('status', 'modif_dtime'));
+ $pag->items_per_page = 10;
+ $pag->no_results_text = __('No issues were found.');
+ $pag->setFromRequest($request);
+ return Pluf_Shortcuts_RenderToResponse('issues/my-issues.html',
+ array('project' => $prj,
+ 'page_title' => $title,
+ 'nb_submit' => $nb_submit,
+ 'nb_owner' => $nb_owner,
+ 'issues' => $pag,
+ ),
+ $request);
+ }
+
+ public $create_precond = array('Pluf_Precondition::loginRequired');
+ public function create($request, $match)
+ {
+ $prj = $request->project;
+ $title = __('Submit a new issue');
+ $params = array(
+ 'project' => $prj,
+ 'user' => $request->user);
+ if ($request->method == 'POST') {
+ $form = new IDF_Form_IssueCreate($request->POST, $params);
+ if ($form->isValid()) {
+ $issue = $form->save();
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index',
+ array($prj->shortname));
+ $urlissue = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
+ array($prj->shortname, $issue->id));
+ $request->user->setMessage(sprintf(__('Issue %d has been created.'), $urlissue, $issue->id));
+ return new Pluf_HTTP_Response_Redirect($url);
+ }
+ } else {
+ $form = new IDF_Form_IssueCreate(null, $params);
+ }
+ $arrays = self::autoCompleteArrays($prj);
+ return Pluf_Shortcuts_RenderToResponse('issues/create.html',
+ array_merge(
+ array('project' => $prj,
+ 'form' => $form,
+ 'page_title' => $title,
+ ),
+ $arrays),
+ $request);
+ }
+
+ public function view($request, $match)
+ {
+ $prj = $request->project;
+ $issue = Pluf_Shortcuts_GetObjectOr404('IDF_Issue', $match[2]);
+ if ($issue->project != $prj->id) {
+ throw new Pluf_HTTP_Error404();
+ }
+ $comments = $issue->get_comments_list(array('order' => 'id ASC'));
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
+ array($prj->shortname, $issue->id));
+ $title = Pluf_Template::markSafe(sprintf(__('Issue %d: %s'), $url, $issue->id, $issue->summary));
+ $form = false; // The form is available only if logged in.
+ if (!$request->user->isAnonymous()) {
+ $params = array(
+ 'project' => $prj,
+ 'user' => $request->user,
+ 'issue' => $issue,
+ );
+ if ($request->method == 'POST') {
+ $form = new IDF_Form_IssueUpdate($request->POST, $params);
+ if ($form->isValid()) {
+ $issue = $form->save();
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index',
+ array($prj->shortname));
+ $request->user->setMessage(sprintf(__('Issue %d has been updated.'), $issue->id));
+ return new Pluf_HTTP_Response_Redirect($url);
+ }
+ } else {
+ $form = new IDF_Form_IssueUpdate(null, $params);
+ }
+ }
+ $arrays = self::autoCompleteArrays($prj);
+ return Pluf_Shortcuts_RenderToResponse('issues/view.html',
+ array_merge(
+ array('project' => $prj,
+ 'issue' => $issue,
+ 'comments' => $comments,
+ 'form' => $form,
+ 'page_title' => $title,
+ ),
+ $arrays),
+ $request);
+ }
+
+ /**
+ * View list of issues for a given project with a given status.
+ */
+ public function listStatus($request, $match)
+ {
+ $prj = $request->project;
+ $status = $match[2];
+ $title = sprintf(__('%s Closed Issues'), (string) $prj);
+ // Get stats about the issues
+ $open = $prj->getIssueCountByStatus('open');
+ $closed = $prj->getIssueCountByStatus('closed');
+ // Paginator to paginate the issues
+ $pag = new Pluf_Paginator(new IDF_Issue());
+ $pag->class = 'recent-issues';
+ $pag->item_extra_props = array('project_m' => $prj,
+ 'shortname' => $prj->shortname);
+ $pag->summary = __('This table shows the closed issues.');
+ $otags = $prj->getTagIdsByStatus('closed');
+ if (count($otags) == 0) $otags[] = 0;
+ $pag->forced_where = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $otags).')', array($prj->id));
+ $pag->action = array('IDF_Views_Issue::index', array($prj->shortname));
+ $pag->sort_order = array('modif_dtime', 'DESC');
+ $list_display = array(
+ 'id' => __('Id'),
+ array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')),
+ array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
+ array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
+ );
+ $pag->configure($list_display, array(), array('id', 'status', 'modif_dtime'));
+ $pag->items_per_page = 10;
+ $pag->no_results_text = __('No issues were found.');
+ $pag->setFromRequest($request);
+ return Pluf_Shortcuts_RenderToResponse('issues/index.html',
+ array('project' => $prj,
+ 'page_title' => $title,
+ 'open' => $open,
+ 'closed' => $closed,
+ 'issues' => $pag,
+ ),
+ $request);
+ }
+
+ /**
+ * View list of issues for a given project with a given label.
+ */
+ public function listLabel($request, $match)
+ {
+ $prj = $request->project;
+ $tag = Pluf_Shortcuts_GetObjectOr404('IDF_Tag', $match[2]);
+ $status = $match[3];
+ if ($tag->project != $prj->id or !in_array($status, array('open', 'closed'))) {
+ throw new Pluf_HTTP_Error404();
+ }
+ if ($status == 'open') {
+ $title = sprintf(__('%1$s Issues with Label %2$s'), (string) $prj,
+ (string) $tag);
+ } else {
+ $title = sprintf(__('%1$s Closed Issues with Label %2$s'),
+ (string) $prj, (string) $tag);
+ }
+ // Get stats about the open/closed issues having this tag.
+ $open = $prj->getIssueCountByStatus('open', $tag);
+ $closed = $prj->getIssueCountByStatus('closed', $tag);
+ // Paginator to paginate the issues
+ $pag = new Pluf_Paginator(new IDF_Issue());
+ $pag->model_view = 'join_tags';
+ $pag->class = 'recent-issues';
+ $pag->item_extra_props = array('project_m' => $prj,
+ 'shortname' => $prj->shortname);
+ $pag->summary = sprintf(__('This table shows the issues with label %s.'), (string) $tag);
+ $otags = $prj->getTagIdsByStatus($status);
+ if (count($otags) == 0) $otags[] = 0;
+ $pag->forced_where = new Pluf_SQL('project=%s AND idf_tag_id=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $tag->id));
+ $pag->action = array('IDF_Views_Issue::listLabel', array($prj->shortname, $tag->id, $status));
+ $pag->sort_order = array('modif_dtime', 'DESC');
+ $list_display = array(
+ 'id' => __('Id'),
+ array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')),
+ array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
+ array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
+ );
+ $pag->configure($list_display, array(), array('status', 'modif_dtime'));
+ $pag->items_per_page = 10;
+ $pag->no_results_text = __('No issues were found.');
+ $pag->setFromRequest($request);
+ if (($open+$closed) > 0) {
+ $completion = sprintf('%01.0f%%', (100*$closed)/((float) $open+$closed));
+ } else {
+ $completion = false;
+ }
+ return Pluf_Shortcuts_RenderToResponse('issues/by-label.html',
+ array('project' => $prj,
+ 'completion' => $completion,
+ 'page_title' => $title,
+ 'open' => $open,
+ 'label' => $tag,
+ 'closed' => $closed,
+ 'issues' => $pag,
+ ),
+ $request);
+ }
+
+ /**
+ * Create the autocomplete arrays for the little AJAX stuff.
+ */
+ public static function autoCompleteArrays($project)
+ {
+ $conf = new IDF_Conf();
+ $conf->setProject($project);
+ $auto = array('auto_status' => '', 'auto_labels' => '');
+ $auto_raw = array('auto_status' => '', 'auto_labels' => '');
+ $st = $conf->getVal('labels_issue_open', IDF_Form_IssueTrackingConf::init_open);
+ $st .= "\n".$conf->getVal('labels_issue_closed', IDF_Form_IssueTrackingConf::init_closed);
+ $auto_raw['auto_status'] = $st;
+ $auto_raw['auto_labels'] = $conf->getVal('labels_issue_predefined', IDF_Form_IssueTrackingConf::init_predefined);
+ foreach ($auto_raw as $key => $st) {
+ $st = preg_split("/\015\012|\015|\012/", $st, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($st as $s) {
+ $v = '';
+ $d = '';
+ $_s = split('=', $s, 2);
+ if (count($_s) > 1) {
+ $v = trim($_s[0]);
+ $d = trim($_s[1]);
+ } else {
+ $v = trim($_s[0]);
+ }
+ $auto[$key] .= sprintf('{ name: "%s", to: "%s" }, ',
+ Pluf_esc($d),
+ Pluf_esc($v));
+ }
+ $auto[$key] = substr($auto[$key], 0, -1);
+ }
+ return $auto;
+ }
+}
+
+/**
+ * Display the summary of an issue, then on a new line, display the
+ * list of labels with a link to a view "by label only".
+ *
+ * The summary of the issue is linking to the issue.
+ */
+function IDF_Views_Issue_SummaryAndLabels($field, $issue, $extra='')
+{
+ $edit = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
+ array($issue->shortname, $issue->id));
+ $tags = array();
+ foreach ($issue->get_tags_list() as $tag) {
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::listLabel',
+ array($issue->shortname, $tag->id, 'open'));
+ $tags[] = sprintf('%s', $url, Pluf_esc((string) $tag));
+ }
+ $out = '';
+ if (count($tags)) {
+ $out = '
'.implode(', ', $tags).'';
+ }
+ return sprintf('%s', $edit, Pluf_esc($issue->summary)).$out;
+}
+
+/**
+ * Display the status in the issue listings.
+ *
+ */
+function IDF_Views_Issue_ShowStatus($field, $issue, $extra='')
+{
+ return Pluf_esc($issue->get_status()->name);
+}
\ No newline at end of file
diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php
new file mode 100644
index 0000000..48c1132
--- /dev/null
+++ b/src/IDF/Views/Project.php
@@ -0,0 +1,140 @@
+project;
+ $title = sprintf(__('%s Project Summary'), (string) $prj);
+ $form_fields = array('fields'=> array('name', 'description'));
+ if ($request->method == 'POST') {
+ $form = Pluf_Shortcuts_GetFormForModel($prj, $request->POST,
+ $form_fields);
+ if ($form->isValid()) {
+ $prj = $form->save();
+ $request->user->setMessage(__('The project has been updated.'));
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::admin',
+ array($prj->shortname));
+ return new Pluf_HTTP_Response_Redirect($url);
+ }
+ } else {
+ $form = Pluf_Shortcuts_GetFormForModel($prj, $prj->getData(),
+ $form_fields);
+ }
+ return Pluf_Shortcuts_RenderToResponse('admin/summary.html',
+ array(
+ 'page_title' => $title,
+ 'form' => $form,
+ ),
+ $request);
+ }
+
+ /**
+ * Administrate the issue tracking of a project.
+ */
+ public $adminIssueTracking_precond = array('IDF_Precondition::projectOwner');
+ public function adminIssues($request, $match)
+ {
+ $prj = $request->project;
+ $title = sprintf(__('%s Issue Tracking Configuration'), (string) $prj);
+ $conf = new IDF_Conf();
+ $conf->setProject($prj);
+ if ($request->method == 'POST') {
+ $form = new IDF_Form_IssueTrackingConf($request->POST);
+ if ($form->isValid()) {
+ foreach ($form->cleaned_data as $key=>$val) {
+ $conf->setVal($key, $val);
+ }
+ $request->user->setMessage(__('The issue tracking configuration has been saved.'));
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::adminIssues',
+ array($prj->shortname));
+ return new Pluf_HTTP_Response_Redirect($url);
+ }
+ } else {
+ $params = array();
+ $keys = array('labels_issue_open', 'labels_issue_closed',
+ 'labels_issue_predefined', 'labels_issue_one_max');
+ foreach ($keys as $key) {
+ $_val = $conf->getVal($key, false);
+ if ($_val !== false) {
+ $params[$key] = $_val;
+ }
+ }
+ if (count($params) == 0) {
+ $params = null; //Nothing in the db, so new form.
+ }
+ $form = new IDF_Form_IssueTrackingConf($params);
+ }
+ return Pluf_Shortcuts_RenderToResponse('admin/issue-tracking.html',
+ array(
+ 'page_title' => $title,
+ 'form' => $form,
+ ),
+ $request);
+ }
+
+ /**
+ * Administrate the members of a project.
+ */
+ public $adminMembers_precond = array('IDF_Precondition::projectOwner');
+ public function adminMembers($request, $match)
+ {
+ $prj = $request->project;
+ $title = sprintf(__('%s Project Members'), (string) $prj);
+ $params = array(
+ 'project' => $prj,
+ 'user' => $request->user,
+ );
+ if ($request->method == 'POST') {
+ $form = new IDF_Form_MembersConf($request->POST, $params);
+ if ($form->isValid()) {
+ $form->save();
+ $request->user->setMessage(__('The project membership has been saved.'));
+ $url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::adminMembers',
+ array($prj->shortname));
+ return new Pluf_HTTP_Response_Redirect($url);
+ }
+ } else {
+ $form = new IDF_Form_MembersConf($prj->getMembershipData('string'), $params);
+ }
+ return Pluf_Shortcuts_RenderToResponse('admin/members.html',
+ array(
+ 'page_title' => $title,
+ 'form' => $form,
+ ),
+ $request);
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/conf/views.php b/src/IDF/conf/views.php
new file mode 100644
index 0000000..472711d
--- /dev/null
+++ b/src/IDF/conf/views.php
@@ -0,0 +1,112 @@
+ '#^/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views',
+ 'method' => 'index');
+
+$ctl[] = array('regex' => '#^/login/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views',
+ 'method' => 'login');
+
+$ctl[] = array('regex' => '#^/logout/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views',
+ 'method' => 'logout');
+
+$ctl[] = array('regex' => '#^/help/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views',
+ 'method' => 'faq');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views',
+ 'method' => 'projectHome');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/issues/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Issue',
+ 'method' => 'index');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/issues/(\d+)/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Issue',
+ 'method' => 'view');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/issues/status/(\w+)/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Issue',
+ 'method' => 'listStatus');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/issues/label/(\d+)/(\w+)/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Issue',
+ 'method' => 'listLabel');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/issues/create/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Issue',
+ 'method' => 'create');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/issues/my/(\w+)/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Issue',
+ 'method' => 'myIssues');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/admin/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Project',
+ 'method' => 'admin');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/admin/issues/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Project',
+ 'method' => 'adminIssues');
+
+$ctl[] = array('regex' => '#^/p/(\w+)/admin/members/$#',
+ 'base' => $base,
+ 'priority' => 4,
+ 'model' => 'IDF_Views_Project',
+ 'method' => 'adminMembers');
+
+return $ctl;
diff --git a/src/IDF/relations.php b/src/IDF/relations.php
new file mode 100644
index 0000000..21f85c0
--- /dev/null
+++ b/src/IDF/relations.php
@@ -0,0 +1,30 @@
+ array('IDF_Project'));
+$m['IDF_Issue'] = array('relate_to' => array('IDF_Project', 'Pluf_User', 'IDF_Tag'),
+ 'relate_to_many' => array('IDF_Tag', 'Pluf_User'));
+$m['IDF_IssueComment'] = array('relate_to' => array('IDF_Issue', 'Pluf_User'));
+
+return $m;
diff --git a/src/IDF/templates/admin/base.html b/src/IDF/templates/admin/base.html
new file mode 100644
index 0000000..3670329
--- /dev/null
+++ b/src/IDF/templates/admin/base.html
@@ -0,0 +1,9 @@
+{extends "base.html"}
+{block tabadmin} class="active"{/block}
+{block subtabs}
+
Instructions:
+List one status value per line in desired sort-order.
+Optionally, use an equals-sign to document the meaning of each status value.
+{/blocktrans} +Instructions:
+Specify each person by its login. Each person must have already registered with the given login.
+Separate the logins with commas and/or new lines.
+{/blocktrans} +Notes:
+A project owner may make any change to this project, including removing other project owners. You need to be carefull when you give owner rights.
+A project member will not have access to the administration area but will have more options available in the use of the project.
+{/blocktrans} +Instructions:
+The description of the project can be improved using the Markdown syntax.
+{/blocktrans} ++{if !$user.isAnonymous()}{blocktrans}Welcome, {$user.first_name} {$user.last_name}.{/blocktrans} {trans 'Sign Out'}{else}{trans 'Sign in or create your account'}{/if} +| {trans 'Help'} +
+{*+{if !$user.isAnonymous()}{blocktrans}Welcome, {$user.first_name} {$user.last_name}.{/blocktrans} {trans 'Sign Out'}{else}{trans 'Sign in or create your account'}{/if} +| {trans 'Help'} +
+{trans 'No projects managed with InDefero were found.'}
+{if $user.administrator}{blocktrans}Create a new project.{/blocktrans}
{/if} +{else} +{trans 'Managed Projects:'} {$projects.count()}
+{/block} diff --git a/src/IDF/templates/issues/base.html b/src/IDF/templates/issues/base.html new file mode 100644 index 0000000..0ca71d9 --- /dev/null +++ b/src/IDF/templates/issues/base.html @@ -0,0 +1,9 @@ +{extends "base.html"} +{block tabissues} class="active"{/block} +{block subtabs} +Open issues: {$open}
+Closed issues: {$closed}
+{/blocktrans}{if $completion} +{trans 'Completion:'} {$completion}
+{/if} +{trans 'Label:'} +{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')} +{$label.class}:{$label.name}
+ + +{/block} diff --git a/src/IDF/templates/issues/create.html b/src/IDF/templates/issues/create.html new file mode 100644 index 0000000..6d315b2 --- /dev/null +++ b/src/IDF/templates/issues/create.html @@ -0,0 +1,72 @@ +{extends "issues/base.html"} +{block body} +{if $form.errors} + +{/if} + + +{/block} +{block context} +When you submit the issue do not forget to provide the following information:
+Open issues: {$open}
+Closed issues: {$closed}
{/blocktrans} + +{foreach $project.getTagCloud() as $label} +{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')} +{$label.class}:{$label.name}{/foreach}
+ +{/block} diff --git a/src/IDF/templates/issues/js-autocomplete.html b/src/IDF/templates/issues/js-autocomplete.html new file mode 100644 index 0000000..6ae60e4 --- /dev/null +++ b/src/IDF/templates/issues/js-autocomplete.html @@ -0,0 +1,40 @@ + + + diff --git a/src/IDF/templates/issues/my-issues.html b/src/IDF/templates/issues/my-issues.html new file mode 100644 index 0000000..992e95c --- /dev/null +++ b/src/IDF/templates/issues/my-issues.html @@ -0,0 +1,16 @@ +{extends "issues/base.html"} +{block docclass}yui-t2{/block} +{block body} +{$issues.render} +{if !$user.isAnonymous()} +{aurl 'url', 'IDF_Views_Issue::create', array($project.shortname)} +{/if} + +{/block} +{block context} +{aurl 'owner_url', 'IDF_Views_Issue::myIssues', array($project.shortname, 'owner')} +{aurl 'submit_url', 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')} +{trans 'Submitted issues:'} {$nb_submit}
+{if $nb_owner > 0} +{trans 'Working issues:'} {$nb_owner}
{/if} +{/block} diff --git a/src/IDF/templates/issues/view.html b/src/IDF/templates/issues/view.html new file mode 100644 index 0000000..2c58ab2 --- /dev/null +++ b/src/IDF/templates/issues/view.html @@ -0,0 +1,110 @@ +{extends "issues/base.html"} +{block body} +{assign $i = 0} +{assign $nc = $comments.count()} +{foreach $comments as $c} +{trans 'Created:'} {$issue.creation_dtime|dateago} {blocktrans}by {$submitter}{/blocktrans}
++{trans 'Updated:'} {$issue.modif_dtime|dateago}
++{trans 'Status:'} {$issue.get_status.name}
++{trans 'Owner:'} {if $issue.get_owner == null}{trans 'No owner'}{else}{$issue.get_owner}{/if} +
{assign $tags = $issue.get_tags_list()}{if $tags.count()} +
+{trans 'Labels:'}
+{foreach $issue.get_tags_list() as $tag}{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $tag.id, 'open')}
+{$tag.class}:{$tag.name}
+{/foreach}
+
{blocktrans}Reported by {$who}, {$c.creation_dtime|date}{/blocktrans}
+{else}{assign $who = $c.get_submitter()} +{aurl 'url', 'IDF_Views_Issue::view', array($project.shortname, $issue.id)} +{assign $id = $c.id} +{assign $url = $url~'#ic'~$c.id} +{blocktrans}Comment {$id} by {$who}, {$c.creation_dtime|date}{/blocktrans}
+{/if} + +{if strlen($c.content) > 0}{$c.content|nl2br}{else}{trans '(No comments were given for this change.)'}{/if}
+ +{if $i> 0 and $c.changedIssue()} ++{/foreach} +