diff --git a/INSTALL.mdtext b/INSTALL.mdtext index 7827f25..f5aeba2 100644 --- a/INSTALL.mdtext +++ b/INSTALL.mdtext @@ -6,9 +6,15 @@ the installation of InDefero by itself. ## PHP modules for indefero -Indefero need the GD module for PHP. It's named "php5-gd" in debian. +Indefero needs additional PHP modules to function correctly, namely - $ apt-get install php5-gd + - gd (for graphic operations) + - zip (for upload archive processing) + +The package names of these modules might vary between distributions, +for Debian they are + + $ apt-get install php5-gd php5-zip ## Recommended Layout of the Files diff --git a/NEWS.mdtext b/NEWS.mdtext index c82a54b..bf489b0 100644 --- a/NEWS.mdtext +++ b/NEWS.mdtext @@ -1,15 +1,62 @@ -# InDefero 1.3 - xxx xxx xx xx:xx:xx UTC 201X +# InDefero 1.3 - xxx xxx xx xx:xx 2011 UTC + +The development of this version of Indefero has been sponsored +by the friendly folks from Scilab ! + +ATTENTION: You need Pluf [4121ca4](http://projects.ceondo.com/p/pluf/source/commit/4121ca4) +or newer to properly run this version of Indefero! + +## Changes + +- Indefero's post-commit web hook now by default issues HTTP PUT instead of + HTTP POST requests and carries the authentication digest in the new + `Web-Hook-Hmac` header. The old behaviour can be re-enabled by setting the + `$cfg['webhook_processing']` flag to "compat", we urge you to change the + implementations of this web hook as this setting is likely to be removed + in future versions of Indefero. +- Indefero now needs PHP's zip module which is not enabled by default. +- Existing email notifications now have to be explicitely activated in the + project's administrative area. ## New Features +- It is now possible to upload and embed resources like images or text + files into wiki pages. If no preview for a resource's mime type is + available, than a download link is provided for it instead. +- The notification system has been overhauled; it is now possible to configure + what kind of user group, project administrators, members and / or additional + mail addresses are notified about updates in a certain section, such as + issues, downloads, reviews, and so on. We now also ensure that notification + emails for one object are uniquely identifyable to support a grouped view + in email clients that support that. (fixes issues 334, 452, and 480) +- Indefero can now be configured to record activity metrics for all projects + in a forge. This needs a special cron job named 'activitycron.php` + (under `scripts`) that is run on a regular basis. The metrics can be + fine-tuned via `activity_section_weights` and `activity_lookback` in + `idf.php` and the result is visible as green bar in the project list view. +- The forge's project list has been overhauled - its now possible to attach + labels on projects and to filter and order the project list by various + criteria. Additionally, projects can now get an external project URL + configured that is displayed as linkable icon right beside the project name + (if available) +- Forge administrators can furthermore configure an alternative entry page + for the forge that is displayed instead of the plain project list. This + page accepts standard Markdown syntax and has support for the new + `projectlist` macro that allows the (partial) inline rendering of the + known global project list. +- It is now also possible to configure a web hook that informs an external + URL about new and updated downloads for a specific project, similar to the + available post-commit web hook. +- One can now upload multiple files at once by using a special archive format + which Indefero processes in the background and for which individual upload + records are created. + ## Bugfixes - Ensure that IDF does not break UTF-8 encoded strings when shortening them for view rendering (issue 785) - -## Documentation - -## Translations +- Indefero no longer confuses a non-owner of an issue with a notification that + a particular ticket has been opened and assigned to him (fixes issue 562) # InDefero 1.2.1 - XXX XXX XX XX:XX:XX UTC 201X diff --git a/logo/external_link.svg b/logo/external_link.svg new file mode 100644 index 0000000..e448789 --- /dev/null +++ b/logo/external_link.svg @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/scripts/activitycron.php b/scripts/activitycron.php new file mode 100644 index 0000000..43f91d9 --- /dev/null +++ b/scripts/activitycron.php @@ -0,0 +1,48 @@ + 1) { + $date = new DateTime($_SERVER['argv'][1]); +} + +echo 'recalculating project activity for '.$date->format('Y-m-d')."\n"; +IDF_ActivityTaxonomy::recalculateTaxnomies($date); + diff --git a/src/IDF/ActivityTaxonomy.php b/src/IDF/ActivityTaxonomy.php new file mode 100644 index 0000000..9991e51 --- /dev/null +++ b/src/IDF/ActivityTaxonomy.php @@ -0,0 +1,156 @@ + $weight) { + $sectionWeights[$section] = $weight / (float) $allWeights; + } + + // + // determine the date boundaries + // + $lookback = Pluf::f('activity_lookback', 0); + if ($lookback < 1) { + throw new LogicException('lookback must be greater or equal to 1'); + } + $dateCopy = new DateTime(); + $dateCopy->setTimestamp($date->getTimestamp()); + $dateBoundaries = array( + $dateCopy->format('Y-m-d 23:59:59'), + $dateCopy->sub(new DateInterval('P'.$lookback.'D'))->format('Y-m-d 00:00:00') + ); + + // + // now recalculate the values for all projects + // + $projects = Pluf::factory('IDF_Project')->getList(); + foreach ($projects as $project) { + self::recalculateTaxonomy($date, $project, $dateBoundaries, $sectionWeights); + } + } + + private static function recalculateTaxonomy(DateTime $date, IDF_Project $project, array $dateBoundaries, array $sectionWeights) + { + $conf = new IDF_Conf(); + $conf->setProject($project); + + $sectionClasses = array( + 'source' => array('IDF_Commit'), + 'issues' => array('IDF_Issue'), + 'wiki' => array('IDF_Wiki_Page', 'IDF_Wiki_Resource'), + 'review' => array('IDF_Review'), + 'downloads' => array('IDF_Upload') + ); + + $value = 0; + foreach ($sectionWeights as $section => $weight) { + // skip closed / non-existant sections + if ($conf->getVal($section.'_access_rights') === 'none') + continue; + + if (!array_key_exists($section, $sectionClasses)) + continue; + + $sectionValue = self::calculateActivityValue( + $dateBoundaries, $sectionClasses[$section], $project->id); + $value = ((1 - $weight) * $value) + ($weight * $sectionValue); + } + + echo "project {$project->name} has an activity value of $value\n"; + + $sql = new Pluf_SQL('project=%s AND date=%s', array($project->id, $date->format('Y-m-d'))); + $activity = Pluf::factory('IDF_ProjectActivity')->getOne(array('filter' => $sql->gen())); + + if ($activity == null) { + $activity = new IDF_ProjectActivity(); + $activity->project = $project; + $activity->date = $date->format('Y-m-d'); + $activity->value = $value; + $activity->create(); + } else { + $activity->value = $value; + $activity->update(); + } + } + + private static function calculateActivityValue(array $dateBoundaries, array $classes, $projectId) + { + $allCount = self::countActivityFor($dateBoundaries, $classes); + if ($allCount == 0) return 0; + $prjCount = self::countActivityFor($dateBoundaries, $classes, $projectId); + return $prjCount / (float) $allCount; + } + + private static function countActivityFor(array $dateBoundaries, array $classes, $projectId = null) + { + static $cache = array(); + $argIdent = md5(serialize(func_get_args())); + if (array_key_exists($argIdent, $cache)) { + return $cache[$argIdent]; + } + + $cache[$argIdent] = 0; + list($higher, $lower) = $dateBoundaries; + $sql = new Pluf_SQL('model_class IN ("'.implode('","', $classes).'") '. + 'AND creation_dtime >= %s AND creation_dtime <= %s', + array($lower, $higher)); + + if ($projectId !== null) { + $sql->SAnd(new Pluf_SQL('project=%s', array($projectId))); + } + + $cache[$argIdent] = Pluf::factory('IDF_Timeline')->getCount(array('filter' => $sql->gen())); + + return $cache[$argIdent]; + } +} \ No newline at end of file diff --git a/src/IDF/Commit.php b/src/IDF/Commit.php index b8981b6..0060ec7 100644 --- a/src/IDF/Commit.php +++ b/src/IDF/Commit.php @@ -287,6 +287,12 @@ class IDF_Commit extends Pluf_Model $url = str_replace(array('%p', '%r'), array($project->shortname, $this->scm_id), $conf->getVal('webhook_url', '')); + + // trigger a POST instead of the standard PUT if we're asked for + $method = 'PUT'; + if (Pluf::f('webhook_processing', '') === 'compat') { + $method = 'POST'; + } $payload = array('to_send' => array( 'project' => $project->shortname, 'rev' => $this->scm_id, @@ -297,41 +303,51 @@ class IDF_Commit extends Pluf_Model 'creation_date' => $this->creation_dtime, ), 'project_id' => $project->id, - 'authkey' => $project->getPostCommitHookKey(), + 'authkey' => $project->getWebHookKey(), 'url' => $url, + 'method' => $method, ); $item = new IDF_Queue(); $item->type = 'new_commit'; $item->payload = $payload; $item->create(); - if ('' == $conf->getVal('source_notification_email', '')) { - return; - } - $current_locale = Pluf_Translation::getLocale(); - $langs = Pluf::f('languages', array('en')); - Pluf_Translation::loadSetLocale($langs[0]); - $context = new Pluf_Template_Context( - array( - 'c' => $this, - 'project' => $this->get_project(), - 'url_base' => Pluf::f('url_base'), - ) - ); - $tmpl = new Pluf_Template('idf/source/commit-created-email.txt'); - $text_email = $tmpl->render($context); - $addresses = explode(',', $conf->getVal('source_notification_email')); - foreach ($addresses as $address) { - $email = new Pluf_Mail(Pluf::f('from_email'), + $from_email = Pluf::f('from_email'); + $recipients = $project->getNotificationRecipientsForTab('source'); + + foreach ($recipients as $address => $language) { + + if (!empty($this->author) && $this->author->email === $address) { + continue; + } + + Pluf_Translation::loadSetLocale($language); + + $context = new Pluf_Template_Context(array( + 'commit' => $this, + 'project' => $project, + 'url_base' => Pluf::f('url_base'), + )); + + // commits are usually not updated, therefor we do not + // distinguish between create and update here + $tplfile = 'idf/source/commit-created-email.txt'; + $subject = __('New Commit %1$s - %2$s (%3$s)'); + + $tmpl = new Pluf_Template($tplfile); + $text_email = $tmpl->render($context); + + $email = new Pluf_Mail($from_email, $address, - sprintf(__('New Commit %1$s - %2$s (%3$s)'), + sprintf($subject, $this->scm_id, $this->summary, - $this->get_project()->shortname)); + $project->shortname)); $email->addTextMessage($text_email); $email->sendMail(); } + Pluf_Translation::loadSetLocale($current_locale); } } diff --git a/src/IDF/Forge.php b/src/IDF/Forge.php new file mode 100644 index 0000000..df199a0 --- /dev/null +++ b/src/IDF/Forge.php @@ -0,0 +1,95 @@ +conf = new IDF_Gconf(); + $this->conf->setModel($this); + } + + public static function instance() { + return new IDF_Forge(); + } + + public function getProjectLabels($default = '') { + return $this->conf->getVal('project_labels', $default); + } + + public function setProjectLabels($labels) { + $this->conf->setVal('project_labels', $labels); + } + + public function getProjectLabelsWithCounts() { + $sql = new Pluf_SQL('project=0'); + $tagList = Pluf::factory('IDF_Tag')->getList(array( + 'filter' => $sql->gen(), + 'view' => 'join_projects', + 'order' => 'class ASC, lcname ASC' + )); + + $maxProjectCount = 0; + foreach ($tagList as $tag) { + $maxProjectCount = max($maxProjectCount, $tag->project_count); + } + + $tags = array(); + foreach ($tagList as $tag) { + // group by class + if (!array_key_exists($tag->class, $tags)) { + $tags[$tag->class] = array(); + } + $tag->rel_project_count = $tag->project_count / (double) $maxProjectCount; + $tags[$tag->class][] = $tag; + } + return $tags; + } + + public function setCustomForgePageEnabled($enabled) { + $this->conf->setVal('custom_forge_page_enabled', $enabled); + } + + public function isCustomForgePageEnabled($default = false) { + return $this->conf->getVal('custom_forge_page_enabled', $default); + } + + public function getCustomForgePageContent($default = '') { + return $this->conf->getVal('custom_forge_page_content', $default); + } + + public function setCustomForgePageContent($content) { + $this->conf->setVal('custom_forge_page_content', $content); + } +} \ No newline at end of file diff --git a/src/IDF/Form/Admin/ForgeConf.php b/src/IDF/Form/Admin/ForgeConf.php new file mode 100644 index 0000000..669ac67 --- /dev/null +++ b/src/IDF/Form/Admin/ForgeConf.php @@ -0,0 +1,47 @@ +fields['enabled'] = new Pluf_Form_Field_Boolean( + array('required' => false, + 'label' => __('Custom forge page enabled'), + 'widget' => 'Pluf_Form_Widget_CheckboxInput', + )); + $this->fields['content'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Content'), + 'widget' => 'Pluf_Form_Widget_TextareaInput', + 'widget_attrs' => array( + 'cols' => 68, + 'rows' => 26, + ), + )); + } +} diff --git a/src/IDF/Form/Admin/LabelConf.php b/src/IDF/Form/Admin/LabelConf.php new file mode 100644 index 0000000..3230ed7 --- /dev/null +++ b/src/IDF/Form/Admin/LabelConf.php @@ -0,0 +1,62 @@ +fields['project_labels'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Predefined project labels'), + 'initial' => self::init_project_labels, + 'widget_attrs' => array('rows' => 13, + 'cols' => 75), + 'widget' => 'Pluf_Form_Widget_TextareaInput', + )); + } + + public function clean_project_labels() + { + $labels = preg_split("/\s*\n\s*/", $this->cleaned_data['project_labels'], -1, PREG_SPLIT_NO_EMPTY); + for ($i=0; $i array('size' => '35'), )); + $this->fields['external_project_url'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('External URL'), + 'widget_attrs' => array('size' => '35'), + 'initial' => '', + )); + $this->fields['scm'] = new Pluf_Form_Field_Varchar( array('required' => true, 'label' => __('Repository type'), @@ -127,6 +134,18 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form 'widget' => 'Pluf_Form_Widget_TextareaInput', )); + for ($i=1;$i<7;$i++) { + $this->fields['label'.$i] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('Labels'), + 'initial' => '', + 'widget_attrs' => array( + 'maxlength' => 50, + 'size' => 20, + ), + )); + } + $projects = array('--' => '--'); foreach (Pluf::factory('IDF_Project')->getList(array('order' => 'name ASC')) as $proj) { $projects[$proj->name] = $proj->shortname; @@ -235,6 +254,11 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form return $shortname; } + public function clean_external_project_url() + { + return IDF_Form_ProjectConf::checkWebURL($this->cleaned_data['external_project_url']); + } + public function clean() { if ($this->cleaned_data['scm'] != 'svn') { @@ -278,11 +302,29 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form if (!$this->isValid()) { throw new Exception(__('Cannot save the model from an invalid form.')); } + + // Add a tag for each label + $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::addGlobal($name, $class); + $tagids[] = $tag->id; + } + } + $project = new IDF_Project(); $project->name = $this->cleaned_data['name']; $project->shortname = $this->cleaned_data['shortname']; $project->shortdesc = $this->cleaned_data['shortdesc']; + $tagids = array(); if ($this->cleaned_data['template'] != '--') { // Find the template project $sql = new Pluf_SQL('shortname=%s', @@ -290,15 +332,36 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form $tmpl = Pluf::factory('IDF_Project')->getOne(array('filter' => $sql->gen())); $project->private = $tmpl->private; $project->description = $tmpl->description; + + foreach ($tmpl->get_tags_list() as $tag) { + $tagids[] = $tag->id; + } } else { $project->private = $this->cleaned_data['private_project']; $project->description = __('Click on the Project Management tab to set the description of your project.'); + + // Add a tag for each label + 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::addGlobal($name, $class); + $tagids[] = $tag->id; + } + } } $project->create(); + $project->batchAssoc('IDF_Tag', $tagids); + $conf = new IDF_Conf(); $conf->setProject($project); $keys = array('scm', 'svn_remote_url', 'svn_username', - 'svn_password', 'mtn_master_branch'); + 'svn_password', 'mtn_master_branch', 'external_project_url'); foreach ($keys as $key) { $this->cleaned_data[$key] = (!empty($this->cleaned_data[$key])) ? $this->cleaned_data[$key] : ''; diff --git a/src/IDF/Form/Admin/ProjectUpdate.php b/src/IDF/Form/Admin/ProjectUpdate.php index e600e08..f579381 100644 --- a/src/IDF/Form/Admin/ProjectUpdate.php +++ b/src/IDF/Form/Admin/ProjectUpdate.php @@ -53,6 +53,13 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form 'widget_attrs' => array('size' => '35'), )); + $this->fields['external_project_url'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('External URL'), + 'widget_attrs' => array('size' => '35'), + 'initial' => $conf->getVal('external_project_url'), + )); + if ($this->project->getConf()->getVal('scm') == 'mtn') { $this->fields['mtn_master_branch'] = new Pluf_Form_Field_Varchar( array('required' => false, @@ -63,6 +70,26 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form )); } + $tags = $this->project->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, + ), + )); + } $this->fields['owners'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('Project owners'), @@ -115,22 +142,52 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form return IDF_Form_MembersConf::checkBadLogins($this->cleaned_data['members']); } + public function clean_external_project_url() + { + return IDF_Form_ProjectConf::checkWebURL($this->cleaned_data['external_project_url']); + } + public function save($commit=true) { if (!$this->isValid()) { throw new Exception(__('Cannot save the model from an invalid form.')); } + + // Add a tag for each label + $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::addGlobal($name, $class); + $tagids[] = $tag->id; + } + } + $this->project->batchAssoc('IDF_Tag', $tagids); + IDF_Form_MembersConf::updateMemberships($this->project, $this->cleaned_data); $this->project->membershipsUpdated(); + $this->project->name = $this->cleaned_data['name']; $this->project->shortdesc = $this->cleaned_data['shortdesc']; $this->project->update(); - $keys = array('mtn_master_branch'); + $conf = $this->project->getConf(); + $keys = array('mtn_master_branch', 'external_project_url'); foreach ($keys as $key) { - if (!empty($this->cleaned_data[$key])) { - $this->project->getConf()->setVal($key, $this->cleaned_data[$key]); + if (array_key_exists($key, $this->cleaned_data)) { + if (!empty($this->cleaned_data[$key])) { + $conf->setVal($key, $this->cleaned_data[$key]); + } + else { + $conf->delVal($key); + } } } } diff --git a/src/IDF/Form/ProjectConf.php b/src/IDF/Form/ProjectConf.php index da41662..740aae7 100644 --- a/src/IDF/Form/ProjectConf.php +++ b/src/IDF/Form/ProjectConf.php @@ -32,6 +32,7 @@ class IDF_Form_ProjectConf extends Pluf_Form public function initFields($extra=array()) { $this->project = $extra['project']; + $conf = $this->project->getConf(); // Basic part $this->fields['name'] = new Pluf_Form_Field_Varchar(array('required' => true, @@ -51,6 +52,32 @@ class IDF_Form_ProjectConf extends Pluf_Form ), 'widget' => 'Pluf_Form_Widget_TextareaInput', )); + $this->fields['external_project_url'] = new Pluf_Form_Field_Varchar(array('required' => false, + 'label' => __('External URL'), + 'widget_attrs' => array('size' => '68'), + 'initial' => $conf->getVal('external_project_url'), + )); + + $tags = $this->project->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, + ), + )); + } // Logo part $upload_path = Pluf::f('upload_path', false); @@ -119,20 +146,63 @@ class IDF_Form_ProjectConf extends Pluf_Form return $this->cleaned_data['logo']; } + public function clean_external_project_url() + { + return self::checkWebURL($this->cleaned_data['external_project_url']); + } + + public static function checkWebURL($url) + { + $url = trim($url); + if (empty($url)) { + return ''; + } + + $parsed = parse_url($url); + if ($parsed === false || !array_key_exists('scheme', $parsed) || + ($parsed['scheme'] != 'http' && $parsed['scheme'] != 'https')) { + throw new Pluf_Form_Invalid(__('The entered URL is invalid. Only http and https URLs are allowed.')); + } + + return $url; + } + public function save($commit=true) { - $conf = $this->project->getConf(); + // Add a tag for each label + $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::addGlobal($name, $class); + $tagids[] = $tag->id; + } + } // Basic part $this->project->name = $this->cleaned_data['name']; $this->project->shortdesc = $this->cleaned_data['shortdesc']; $this->project->description = $this->cleaned_data['description']; + $this->project->batchAssoc('IDF_Tag', $tagids); $this->project->update(); - // Logo part - if ($this->cleaned_data['logo'] !== "") { + $conf = $this->project->getConf(); + if (!empty($this->cleaned_data['logo'])) { $conf->setVal('logo', $this->cleaned_data['logo']); } + if (!empty($this->cleaned_data['external_project_url'])) { + $conf->setVal('external_project_url', $this->cleaned_data['external_project_url']); + } + else { + $conf->delVal('external_project_url'); + } + if ($this->cleaned_data['logo_remove'] === true) { @unlink(Pluf::f('upload_path') . '/' . $this->project->shortname . $conf->getVal('logo')); $conf->delVal('logo'); diff --git a/src/IDF/Form/SourceConf.php b/src/IDF/Form/SourceConf.php index 7cae6f3..5a9a183 100644 --- a/src/IDF/Form/SourceConf.php +++ b/src/IDF/Form/SourceConf.php @@ -49,13 +49,10 @@ class IDF_Form_SourceConf extends Pluf_Form 'widget' => 'Pluf_Form_Widget_PasswordInput', )); } - Pluf::loadFunction('Pluf_HTTP_URL_urlForView'); - $url = Pluf_HTTP_URL_urlForView('idf_faq').'#webhooks'; $this->fields['webhook_url'] = new Pluf_Form_Field_Url( array('required' => false, 'label' => __('Webhook URL'), 'initial' => $this->conf->getVal('webhook_url', ''), - 'help_text' => sprintf(__('Learn more about the post-commit webhooks.'), $url), 'widget_attrs' => array('size' => 35), )); diff --git a/src/IDF/Form/TabsConf.php b/src/IDF/Form/TabsConf.php index 8ba3c2a..d41863b 100644 --- a/src/IDF/Form/TabsConf.php +++ b/src/IDF/Form/TabsConf.php @@ -57,20 +57,44 @@ class IDF_Form_TabsConf extends Pluf_Form 'widget' => 'Pluf_Form_Widget_SelectInput', )); } - $ak = array('downloads_notification_email', - 'review_notification_email', - 'wiki_notification_email', - 'source_notification_email', - 'issues_notification_email',); - foreach ($ak as $key) { - $this->fields[$key] = new IDF_Form_Field_EmailList( - array('required' => false, - 'label' => $key, - 'initial' => $this->conf->getVal($key, ''), - 'widget_attrs' => array('size' => 40), - )); - } + $sections = array( + 'downloads_notification', + 'review_notification', + 'wiki_notification', + 'source_notification', + 'issues_notification', + ); + + foreach ($sections as $section) { + $this->fields[$section.'_owners_enabled'] = new Pluf_Form_Field_Boolean( + array('required' => false, + 'label' => __('Project owners'), + 'initial' => $this->conf->getVal($section.'_owners_enabled', false), + 'widget' => 'Pluf_Form_Widget_CheckboxInput', + )); + $this->fields[$section.'_members_enabled'] = new Pluf_Form_Field_Boolean( + array('required' => false, + 'label' => __('Project members'), + 'initial' => $this->conf->getVal($section.'_members_enabled', false), + 'widget' => 'Pluf_Form_Widget_CheckboxInput', + )); + $this->fields[$section.'_email_enabled'] = new Pluf_Form_Field_Boolean( + array('required' => false, + 'label' => __('Others'), + 'initial' => $this->conf->getVal($section.'_email_enabled', false), + 'widget' => 'Pluf_Form_Widget_CheckboxInput', + )); + if ($this->conf->getVal($section.'_email_enabled', false)) { + $attrs['readonly'] = 'readonly'; + } + $this->fields[$section.'_email'] = new IDF_Form_Field_EmailList( + array('required' => false, + 'label' => null, + 'initial' => $this->conf->getVal($section.'_email', ''), + 'widget_attrs' => array('size' => 20), + )); + } $this->fields['private_project'] = new Pluf_Form_Field_Boolean( array('required' => false, diff --git a/src/IDF/Form/UpdateUpload.php b/src/IDF/Form/UpdateUpload.php index 4cfe44c..cf653fe 100644 --- a/src/IDF/Form/UpdateUpload.php +++ b/src/IDF/Form/UpdateUpload.php @@ -96,7 +96,7 @@ class IDF_Form_UpdateUpload extends Pluf_Form $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)), + list($class, $name) = array(mb_strtolower(trim($class)), trim($name)); } else { $class = 'other'; @@ -146,6 +146,9 @@ class IDF_Form_UpdateUpload extends Pluf_Form $this->upload->modif_dtime = gmdate('Y-m-d H:i:s'); $this->upload->update(); $this->upload->batchAssoc('IDF_Tag', $tags); + + // Send the notification + $this->upload->notify($this->project->getConf(), false); /** * [signal] * @@ -166,7 +169,7 @@ class IDF_Form_UpdateUpload extends Pluf_Form * */ $params = array('upload' => $this->upload); - Pluf_Signal::send('IDF_Upload::update', + Pluf_Signal::send('IDF_Upload::update', 'IDF_Form_UpdateUpload', $params); return $this->upload; } diff --git a/src/IDF/Form/Upload.php b/src/IDF/Form/Upload.php index e67d08c..c973581 100644 --- a/src/IDF/Form/Upload.php +++ b/src/IDF/Form/Upload.php @@ -79,6 +79,7 @@ class IDF_Form_Upload extends Pluf_Form public function clean_file() { + // FIXME: we do the same in IDF_Form_WikiResourceCreate and a couple of other places as well $extra = strtolower(implode('|', explode(' ', Pluf::f('idf_extra_upload_ext')))); if (strlen($extra)) $extra .= '|'; if (!preg_match('/\.('.$extra.'png|jpg|jpeg|gif|bmp|psd|tif|aiff|asf|avi|bz2|css|doc|eps|gz|jar|mdtext|mid|mov|mp3|mpg|ogg|pdf|ppt|ps|qt|ra|ram|rm|rtf|sdd|sdw|sit|sxi|sxw|swf|tgz|txt|wav|xls|xml|war|wmv|zip)$/i', $this->cleaned_data['file'])) { @@ -106,7 +107,7 @@ class IDF_Form_Upload extends Pluf_Form $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)), + list($class, $name) = array(mb_strtolower(trim($class)), trim($name)); } else { $class = 'other'; @@ -116,7 +117,7 @@ class IDF_Form_Upload extends Pluf_Form 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 one label from the %s class to an issue.'), $class); + $this->errors['label'.$i][] = sprintf(__('You cannot provide more than one label from the %s class to a download.'), $class); throw new Pluf_Form_Invalid(__('You provided an invalid label.')); } } @@ -129,7 +130,7 @@ class IDF_Form_Upload extends Pluf_Form */ function failed() { - if (!empty($this->cleaned_data['file']) + if (!empty($this->cleaned_data['file']) and file_exists(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['file'])) { @unlink(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['file']); } diff --git a/src/IDF/Form/UploadArchive.php b/src/IDF/Form/UploadArchive.php new file mode 100644 index 0000000..2ac3d3c --- /dev/null +++ b/src/IDF/Form/UploadArchive.php @@ -0,0 +1,227 @@ +user = $extra['user']; + $this->project = $extra['project']; + + $this->fields['archive'] = new Pluf_Form_Field_File( + array('required' => true, + 'label' => __('Archive file'), + 'initial' => '', + 'max_size' => Pluf::f('max_upload_archive_size', 20971520), + 'move_function_params' => array( + 'upload_path' => Pluf::f('upload_path').'/'.$this->project->shortname.'/archives', + 'upload_path_create' => true, + 'upload_overwrite' => true, + ))); + } + + + public function clean_archive() + { + $this->archiveHelper = new IDF_Form_UploadArchiveHelper( + Pluf::f('upload_path').'/'.$this->project->shortname.'/archives/'.$this->cleaned_data['archive']); + + // basic archive validation + $this->archiveHelper->validate(); + + // extension validation + $fileNames = $this->archiveHelper->getEntryNames(); + foreach ($fileNames as $fileName) { + $extra = strtolower(implode('|', explode(' ', Pluf::f('idf_extra_upload_ext')))); + if (strlen($extra)) $extra .= '|'; + if (!preg_match('/\.('.$extra.'png|jpg|jpeg|gif|bmp|psd|tif|aiff|asf|avi|bz2|css|doc|eps|gz|jar|mdtext|mid|mov|mp3|mpg|ogg|pdf|ppt|ps|qt|ra|ram|rm|rtf|sdd|sdw|sit|sxi|sxw|swf|tgz|txt|wav|xls|xml|war|wmv|zip)$/i', $fileName)) { + @unlink(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['archive']); + throw new Pluf_Form_Invalid(sprintf(__('For security reasons, you cannot upload a file (%s) with this extension.'), $fileName)); + } + } + + // label and file name validation + $conf = new IDF_Conf(); + $conf->setProject($this->project); + $onemax = array(); + foreach (explode(',', $conf->getVal('labels_download_one_max', IDF_Form_UploadConf::init_one_max)) as $class) { + if (trim($class) != '') { + $onemax[] = mb_strtolower(trim($class)); + } + } + + foreach ($fileNames as $fileName) { + $meta = $this->archiveHelper->getMetaData($fileName); + $count = array(); + foreach ($meta['labels'] as $label) { + $label = trim($label); + if (strpos($label, ':') !== false) { + list($class, $name) = explode(':', $label, 2); + list($class, $name) = array(mb_strtolower(trim($class)), + trim($name)); + } else { + $class = 'other'; + $name = $label; + } + if (!isset($count[$class])) $count[$class] = 1; + else $count[$class] += 1; + if (in_array($class, $onemax) and $count[$class] > 1) { + throw new Pluf_Form_Invalid( + sprintf(__('You cannot provide more than label from the %1$s class to a download (%2$s).'), $class, $name) + ); + } + } + + $sql = new Pluf_SQL('file=%s AND project=%s', array($fileName, $this->project->id)); + $upload = Pluf::factory('IDF_Upload')->getOne(array('filter' => $sql->gen())); + + $meta = $this->archiveHelper->getMetaData($fileName); + if ($upload != null && $meta['replaces'] !== $fileName) { + throw new Pluf_Form_Invalid( + sprintf(__('A file with the name "%s" has already been uploaded and is not marked to be replaced.'), $fileName)); + } + } + + return $this->cleaned_data['archive']; + } + + /** + * If we have uploaded a file, but the form failed remove it. + * + */ + function failed() + { + if (!empty($this->cleaned_data['archive']) + and file_exists(Pluf::f('upload_path').'/'.$this->project->shortname.'/archives/'.$this->cleaned_data['archive'])) { + @unlink(Pluf::f('upload_path').'/'.$this->project->shortname.'/archives/'.$this->cleaned_data['archive']); + } + } + + /** + * 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. + */ + function save($commit=true) + { + if (!$this->isValid()) { + throw new Exception(__('Cannot save the model from an invalid form.')); + } + + $uploadDir = Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'; + $fileNames = $this->archiveHelper->getEntryNames(); + + foreach ($fileNames as $fileName) { + $meta = $this->archiveHelper->getMetaData($fileName); + + // add a tag for each label + $tags = array(); + foreach ($meta['labels'] as $label) { + $label = trim($label); + if (strlen($label) > 0) { + if (strpos($label, ':') !== false) { + list($class, $name) = explode(':', $label, 2); + list($class, $name) = array(trim($class), trim($name)); + } else { + $class = 'Other'; + $name = $label; + } + $tags[] = IDF_Tag::add($name, $this->project, $class); + } + } + + // process a possible replacement + if (!empty($meta['replaces'])) { + $sql = new Pluf_SQL('file=%s AND project=%s', array($meta['replaces'], $this->project->id)); + $oldUpload = Pluf::factory('IDF_Upload')->getOne(array('filter' => $sql->gen())); + + if ($oldUpload) { + if ($meta['replaces'] === $fileName) { + $oldUpload->delete(); + } else { + $tags = $this->project->getTagsFromConfig('labels_download_predefined', + IDF_Form_UploadConf::init_predefined); + // the deprecate tag is - by definition - always the last one + $deprecatedTag = array_pop($tags); + $oldUpload->setAssoc($deprecatedTag); + } + } + } + + // extract the file + $this->archiveHelper->extract($fileName, $uploadDir); + + // create the upload + $upload = new IDF_Upload(); + $upload->project = $this->project; + $upload->submitter = $this->user; + $upload->summary = trim($meta['summary']); + $upload->changelog = trim($meta['description']); + $upload->file = $fileName; + $upload->filesize = filesize($uploadDir.$fileName); + $upload->downloads = 0; + $upload->create(); + foreach ($tags as $tag) { + $upload->setAssoc($tag); + } + + // send the notification + $upload->notify($this->project->getConf()); + /** + * [signal] + * + * IDF_Upload::create + * + * [sender] + * + * IDF_Form_Upload + * + * [description] + * + * This signal allows an application to perform a set of tasks + * just after the upload of a file and after the notification run. + * + * [parameters] + * + * array('upload' => $upload); + * + */ + $params = array('upload' => $upload); + Pluf_Signal::send('IDF_Upload::create', 'IDF_Form_Upload', + $params); + } + + // finally unlink the uploaded archive + @unlink(Pluf::f('upload_path').'/'.$this->project->shortname.'/archives/'.$this->cleaned_data['archive']); + } +} + diff --git a/src/IDF/Form/UploadArchiveHelper.php b/src/IDF/Form/UploadArchiveHelper.php new file mode 100644 index 0000000..50f8aad --- /dev/null +++ b/src/IDF/Form/UploadArchiveHelper.php @@ -0,0 +1,158 @@ +file = $file; + } + + /** + * Validates the archive; throws a invalid form exception in case the + * archive contains invalid data or cannot be read. + */ + public function validate() + { + if (!file_exists($this->file)) { + throw new Pluf_Form_Invalid(__('The archive does not exist.')); + } + + $za = new ZipArchive(); + $res = $za->open($this->file); + if ($res !== true) { + throw new Pluf_Form_Invalid( + sprintf(__('The archive could not be read (code %d).'), $res)); + } + + $manifest = $za->getFromName('manifest.xml'); + if ($manifest === false) { + throw new Pluf_Form_Invalid(__('The archive does not contain a manifest.xml.')); + } + + libxml_use_internal_errors(true); + $xml = @simplexml_load_string($manifest); + if ($xml === false) { + $error = libxml_get_last_error(); + throw new Pluf_Form_Invalid( + sprintf(__('The archive\'s manifest is invalid: %s'), $error->message)); + } + + foreach (@$xml->file as $idx => $file) + { + $entry = array( + 'name' => (string)@$file->name, + 'summary' => (string)@$file->summary, + 'description' => (string)@$file->description, + 'replaces' => (string)@$file->replaces, + 'labels' => array(), + 'stream' => null + ); + + if (empty($entry['name'])) { + throw new Pluf_Form_Invalid( + sprintf(__('The entry %d in the manifest is missing a file name.'), $idx)); + } + + if (empty($entry['summary'])) { + throw new Pluf_Form_Invalid( + sprintf(__('The entry %d in the manifest is missing a summary.'), $idx)); + } + + if ($entry['name'] === 'manifest.xml') { + throw new Pluf_Form_Invalid(__('The manifest must not reference itself.')); + } + + if ($za->locateName($entry['name']) === false) { + throw new Pluf_Form_Invalid( + sprintf(__('The entry %s in the manifest does not exist in the archive.'), $entry['name'])); + } + + if (in_array($entry['name'], $this->entries)) { + throw new Pluf_Form_Invalid( + sprintf(__('The entry %s in the manifest is referenced more than once.'), $entry['name'])); + } + + if ($file->labels) { + foreach (@$file->labels->label as $label) { + $entry['labels'][] = (string)$label; + } + } + + // FIXME: remove this once we allow more than six labels everywhere + if (count($entry['labels']) > 6) { + throw new Pluf_Form_Invalid( + sprintf(__('The entry %s in the manifest has more than the six allowed labels set.'), $entry['name'])); + } + + $this->entries[$entry['name']] = $entry; + } + + $za->close(); + } + + /** + * Returns all entry names + * + * @return array of string + */ + public function getEntryNames() + { + return array_keys($this->entries); + } + + /** + * Returns meta data for the given entry + * + * @param string $name + * @throws Exception + */ + public function getMetaData($name) + { + if (!array_key_exists($name, $this->entries)) { + throw new Exception('unknown file ' . $name); + } + return $this->entries[$name]; + } + + /** + * Extracts the file entry $name at $path + * + * @param string $name + * @param string $path + * @throws Exception + */ + public function extract($name, $path) + { + if (!array_key_exists($name, $this->entries)) { + throw new Exception('unknown file ' . $name); + } + $za = new ZipArchive(); + $za->open($this->file); + $za->extractTo($path, $name); + $za->close(); + } +} diff --git a/src/IDF/Form/UploadConf.php b/src/IDF/Form/UploadConf.php index f6b4c7d..eec6788 100644 --- a/src/IDF/Form/UploadConf.php +++ b/src/IDF/Form/UploadConf.php @@ -60,10 +60,18 @@ Deprecated = Most users should NOT download this'; $this->fields['labels_download_one_max'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('Each download may have at most one label with each of these classes'), - 'initial' => self::init_one_max, + 'initial' => self::init_one_max, 'widget_attrs' => array('size' => 60), )); + $this->conf = $extra['conf']; + $this->fields['upload_webhook_url'] = new Pluf_Form_Field_Url( + array('required' => false, + 'label' => __('Webhook URL'), + 'initial' => $this->conf->getVal('upload_webhook_url', ''), + 'widget_attrs' => array('size' => 60), + )); + } } diff --git a/src/IDF/Form/WikiCreate.php b/src/IDF/Form/WikiPageCreate.php similarity index 96% rename from src/IDF/Form/WikiCreate.php rename to src/IDF/Form/WikiPageCreate.php index 15dd17d..6f1335b 100644 --- a/src/IDF/Form/WikiCreate.php +++ b/src/IDF/Form/WikiPageCreate.php @@ -27,7 +27,7 @@ * This create a new page and the corresponding revision. * */ -class IDF_Form_WikiCreate extends Pluf_Form +class IDF_Form_WikiPageCreate extends Pluf_Form { public $user = null; public $project = null; @@ -107,9 +107,9 @@ Add your content here. Format your content with: if (preg_match('/[^a-zA-Z0-9\-]/', $title)) { throw new Pluf_Form_Invalid(__('The title contains invalid characters.')); } - $sql = new Pluf_SQL('project=%s AND title=%s', + $sql = new Pluf_SQL('project=%s AND title=%s', array($this->project->id, $title)); - $pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen())); + $pages = Pluf::factory('IDF_Wiki_Page')->getList(array('filter'=>$sql->gen())); if ($pages->count() > 0) { throw new Pluf_Form_Invalid(__('A page with this title already exists.')); } @@ -137,7 +137,7 @@ Add your content here. Format your content with: $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)), + list($class, $name) = array(mb_strtolower(trim($class)), trim($name)); } else { $class = 'other'; @@ -181,9 +181,9 @@ Add your content here. Format your content with: $tags[] = IDF_Tag::add($name, $this->project, $class); } } - } + } // Create the page - $page = new IDF_WikiPage(); + $page = new IDF_Wiki_Page(); $page->project = $this->project; $page->submitter = $this->user; $page->summary = trim($this->cleaned_data['summary']); @@ -193,7 +193,7 @@ Add your content here. Format your content with: $page->setAssoc($tag); } // add the first revision - $rev = new IDF_WikiRevision(); + $rev = new IDF_Wiki_PageRevision(); $rev->wikipage = $page; $rev->content = $this->cleaned_data['content']; $rev->submitter = $this->user; diff --git a/src/IDF/Form/WikiDelete.php b/src/IDF/Form/WikiPageDelete.php similarity index 97% rename from src/IDF/Form/WikiDelete.php rename to src/IDF/Form/WikiPageDelete.php index 1595ecb..d7b952a 100644 --- a/src/IDF/Form/WikiDelete.php +++ b/src/IDF/Form/WikiPageDelete.php @@ -27,7 +27,7 @@ * This is a hard delete of the page and the revisions. * */ -class IDF_Form_WikiDelete extends Pluf_Form +class IDF_Form_WikiPageDelete extends Pluf_Form { protected $page = null; diff --git a/src/IDF/Form/WikiUpdate.php b/src/IDF/Form/WikiPageUpdate.php similarity index 97% rename from src/IDF/Form/WikiUpdate.php rename to src/IDF/Form/WikiPageUpdate.php index b8cf504..0910775 100644 --- a/src/IDF/Form/WikiUpdate.php +++ b/src/IDF/Form/WikiPageUpdate.php @@ -27,13 +27,13 @@ * This add a corresponding revision. * */ -class IDF_Form_WikiUpdate extends Pluf_Form +class IDF_Form_WikiPageUpdate extends Pluf_Form { public $user = null; public $project = null; public $page = null; public $show_full = false; - + public function initFields($extra=array()) { @@ -118,9 +118,9 @@ class IDF_Form_WikiUpdate extends Pluf_Form if (preg_match('/[^a-zA-Z0-9\-]/', $title)) { throw new Pluf_Form_Invalid(__('The title contains invalid characters.')); } - $sql = new Pluf_SQL('project=%s AND title=%s', + $sql = new Pluf_SQL('project=%s AND title=%s', array($this->project->id, $title)); - $pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen())); + $pages = Pluf::factory('IDF_Wiki_Page')->getList(array('filter'=>$sql->gen())); if ($pages->count() > 0 and $pages[0]->id != $this->page->id) { throw new Pluf_Form_Invalid(__('A page with this title already exists.')); } @@ -148,7 +148,7 @@ class IDF_Form_WikiUpdate extends Pluf_Form $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)), + list($class, $name) = array(mb_strtolower(trim($class)), trim($name)); } else { $class = 'other'; @@ -229,7 +229,7 @@ class IDF_Form_WikiUpdate extends Pluf_Form } $this->page->update(); // add the new revision - $rev = new IDF_WikiRevision(); + $rev = new IDF_Wiki_PageRevision(); $rev->wikipage = $this->page; $rev->content = $this->cleaned_data['content']; $rev->submitter = $this->user; diff --git a/src/IDF/Form/WikiResourceCreate.php b/src/IDF/Form/WikiResourceCreate.php new file mode 100644 index 0000000..8ef1e83 --- /dev/null +++ b/src/IDF/Form/WikiResourceCreate.php @@ -0,0 +1,169 @@ +project = $extra['project']; + $this->user = $extra['user']; + $initname = (!empty($extra['name'])) ? $extra['name'] : __('ResourceName'); + + $this->fields['title'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Resource title'), + 'initial' => $initname, + 'widget_attrs' => array( + 'maxlength' => 200, + 'size' => 67, + ), + 'help_text' => __('The resource name must contains only letters, digits and the dash (-) character.'), + )); + $this->fields['summary'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Description'), + 'help_text' => __('This one line description is displayed in the list of resources.'), + 'initial' => '', + 'widget_attrs' => array( + 'maxlength' => 200, + 'size' => 67, + ), + )); + + $this->fields['file'] = new Pluf_Form_Field_File( + array('required' => true, + 'label' => __('File'), + 'initial' => '', + 'max_size' => Pluf::f('max_upload_size', 2097152), + 'move_function_params' => array('upload_path' => $this->getTempUploadPath(), + 'upload_path_create' => true, + 'upload_overwrite' => true), + )); + } + + public function clean_title() + { + $title = $this->cleaned_data['title']; + if (preg_match('/[^a-zA-Z0-9\-]/', $title)) { + throw new Pluf_Form_Invalid(__('The title contains invalid characters.')); + } + $sql = new Pluf_SQL('project=%s AND title=%s', + array($this->project->id, $title)); + $resources = Pluf::factory('IDF_Wiki_Resource')->getList(array('filter'=>$sql->gen())); + if ($resources->count() > 0) { + throw new Pluf_Form_Invalid(__('A resource with this title already exists.')); + } + return $title; + } + + public function clean_file() + { + // FIXME: we do the same in IDF_Form_Upload and a couple of other places as well + $extra = strtolower(implode('|', explode(' ', Pluf::f('idf_extra_upload_ext')))); + if (strlen($extra)) $extra .= '|'; + if (!preg_match('/\.('.$extra.'png|jpg|jpeg|gif|bmp|psd|tif|aiff|asf|avi|bz2|css|doc|eps|gz|jar|mdtext|mid|mov|mp3|mpg|ogg|pdf|ppt|ps|qt|ra|ram|rm|rtf|sdd|sdw|sit|sxi|sxw|swf|tgz|txt|wav|xls|xml|war|wmv|zip)$/i', $this->cleaned_data['file'])) { + @unlink($this->getTempUploadPath().$this->cleaned_data['file']); + throw new Pluf_Form_Invalid(__('For security reasons, you cannot upload a file with this extension.')); + } + return $this->cleaned_data['file']; + } + + /** + * If we have uploaded a file, but the form failed remove it. + * + */ + function failed() + { + if (!empty($this->cleaned_data['file']) + and file_exists($this->getTempUploadPath().$this->cleaned_data['file'])) { + @unlink($this->getTempUploadPath().$this->cleaned_data['file']); + } + } + + /** + * 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()) { + throw new Exception(__('Cannot save the model from an invalid form.')); + } + + $tempFile = $this->getTempUploadPath().$this->cleaned_data['file']; + list($mimeType, , $extension) = IDF_FileUtil::getMimeType($tempFile); + + // create the resource + $resource = new IDF_Wiki_Resource(); + $resource->project = $this->project; + $resource->submitter = $this->user; + $resource->summary = trim($this->cleaned_data['summary']); + $resource->title = trim($this->cleaned_data['title']); + $resource->mime_type = $mimeType; + $resource->create(); + + // add the first revision + $rev = new IDF_Wiki_ResourceRevision(); + $rev->wikiresource = $resource; + $rev->submitter = $this->user; + $rev->summary = __('Initial resource creation'); + $rev->filesize = filesize($tempFile); + $rev->fileext = $extension; + $rev->create(); + + $finalFile = $rev->getFilePath(); + if (!@mkdir(dirname($finalFile), 0755, true)) { + @unlink($tempFile); + $rev->delete(); + $resource->delete(); + throw new Exception('could not create final resource path'); + } + + if (!@rename($tempFile, $finalFile)) { + @unlink($tempFile); + $rev->delete(); + $resource->delete(); + throw new Exception('could not move resource to final location'); + } + + return $resource; + } + + private function getTempUploadPath() + { + return Pluf::f('upload_path').'/'.$this->project->shortname.'/wiki/temp/'; + } +} diff --git a/src/IDF/Form/WikiResourceDelete.php b/src/IDF/Form/WikiResourceDelete.php new file mode 100644 index 0000000..2eef1dd --- /dev/null +++ b/src/IDF/Form/WikiResourceDelete.php @@ -0,0 +1,64 @@ +resource = $extra['resource']; + $this->fields['confirm'] = new Pluf_Form_Field_Boolean( + array('required' => true, + 'label' => __('Yes, I understand that the resource and all its revisions will be deleted.'), + 'initial' => '', + )); + } + + /** + * Check the confirmation. + */ + public function clean_confirm() + { + if (!$this->cleaned_data['confirm']) { + throw new Pluf_Form_Invalid(__('You need to confirm the deletion.')); + } + return $this->cleaned_data['confirm']; + } + + + function save($commit=true) + { + if (!$this->isValid()) { + throw new Exception(__('Cannot save the model from an invalid form.')); + } + $this->resource->delete(); + return true; + } +} diff --git a/src/IDF/Form/WikiResourceUpdate.php b/src/IDF/Form/WikiResourceUpdate.php new file mode 100644 index 0000000..2ea448d --- /dev/null +++ b/src/IDF/Form/WikiResourceUpdate.php @@ -0,0 +1,161 @@ +resource = $extra['resource']; + $this->user = $extra['user']; + $this->project = $extra['project']; + + $this->fields['summary'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Description'), + 'help_text' => __('This one line description is displayed in the list of resources.'), + 'initial' => $this->resource->summary, + 'widget_attrs' => array( + 'maxlength' => 200, + 'size' => 67, + ), + )); + $this->fields['file'] = new Pluf_Form_Field_File( + array('required' => true, + 'label' => __('File'), + 'initial' => '', + 'max_size' => Pluf::f('max_upload_size', 2097152), + 'move_function_params' => array('upload_path' => $this->getTempUploadPath(), + 'upload_path_create' => true, + 'upload_overwrite' => true), + )); + + $this->fields['comment'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Comment'), + 'help_text' => __('One line to describe the changes you made.'), + 'initial' => '', + 'widget_attrs' => array( + 'maxlength' => 200, + 'size' => 67, + ), + )); + } + + public function clean_file() + { + // FIXME: we do the same in IDF_Form_Upload and a couple of other places as well + $extra = strtolower(implode('|', explode(' ', Pluf::f('idf_extra_upload_ext')))); + if (strlen($extra)) $extra .= '|'; + if (!preg_match('/\.('.$extra.'png|jpg|jpeg|gif|bmp|psd|tif|aiff|asf|avi|bz2|css|doc|eps|gz|jar|mdtext|mid|mov|mp3|mpg|ogg|pdf|ppt|ps|qt|ra|ram|rm|rtf|sdd|sdw|sit|sxi|sxw|swf|tgz|txt|wav|xls|xml|war|wmv|zip)$/i', $this->cleaned_data['file'])) { + @unlink($this->getTempUploadPath().$this->cleaned_data['file']); + throw new Pluf_Form_Invalid(__('For security reasons, you cannot upload a file with this extension.')); + } + + list($mimeType, , $extension) = IDF_FileUtil::getMimeType($this->getTempUploadPath().$this->cleaned_data['file']); + if ($this->resource->mime_type != $mimeType) { + throw new Pluf_Form_Invalid(sprintf( + __('The mime type of the uploaded file "%1$s" does not match the mime type of this resource "%2$s"'), + $mimeType, $this->resource->mime_type + )); + } + $this->cleaned_data['fileext'] = $extension; + + if (md5_file($this->getTempUploadPath().$this->cleaned_data['file']) === + md5_file($this->resource->get_current_revision()->getFilePath())) { + throw new Pluf_Form_Invalid(__('The current version of the resource and the uploaded file are equal.')); + } + return $this->cleaned_data['file']; + } + + /** + * If we have uploaded a file, but the form failed remove it. + * + */ + function failed() + { + if (!empty($this->cleaned_data['file']) + and file_exists($this->getTempUploadPath().$this->cleaned_data['file'])) { + @unlink($this->getTempUploadPath().$this->cleaned_data['file']); + } + } + + /** + * 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()) { + throw new Exception(__('Cannot save the model from an invalid form.')); + } + + $tempFile = $this->getTempUploadPath().$this->cleaned_data['file']; + + $this->resource->summary = trim($this->cleaned_data['summary']); + $this->resource->update(); + + // add the new revision + $rev = new IDF_Wiki_ResourceRevision(); + $rev->wikiresource = $this->resource; + $rev->submitter = $this->user; + $rev->summary = $this->cleaned_data['comment']; + $rev->filesize = filesize($tempFile); + $rev->fileext = $this->cleaned_data['fileext']; + $rev->create(); + + $finalFile = $rev->getFilePath(); + if (!is_dir(dirname($finalFile))) { + @unlink($tempFile); + $rev->delete(); + throw new Exception('resource path does not exist'); + } + + if (!@rename($tempFile, $finalFile)) { + @unlink($tempFile); + $rev->delete(); + throw new Exception('could not move resource to final location'); + } + + return $this->resource; + } + + private function getTempUploadPath() + { + return Pluf::f('upload_path').'/'.$this->project->shortname.'/wiki/temp/'; + } +} diff --git a/src/IDF/Issue.php b/src/IDF/Issue.php index 3fb94f3..0bef730 100644 --- a/src/IDF/Issue.php +++ b/src/IDF/Issue.php @@ -263,91 +263,72 @@ class IDF_Issue extends Pluf_Model */ public function notify($conf, $create=true) { - $prj = $this->get_project(); - $to_email = array(); - if ('' != $conf->getVal('issues_notification_email', '')) { - $langs = Pluf::f('languages', array('en')); - $addresses = explode(',', $conf->getVal('issues_notification_email')); - foreach ($addresses as $address) { - $to_email[] = array($address, $langs[0]); - } - } + $project = $this->get_project(); $current_locale = Pluf_Translation::getLocale(); - $id = '<'.md5($this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>'; - if ($create) { - if (null != $this->get_owner() and $this->owner != $this->submitter) { - $email_lang = array($this->get_owner()->email, - $this->get_owner()->language); - if (!in_array($email_lang, $to_email)) { - $to_email[] = $email_lang; - } - } - $comments = $this->get_comments_list(array('order' => 'id ASC')); - $context = new Pluf_Template_Context( - array( - 'issue' => $this, - 'comment' => $comments[0], - 'project' => $prj, - 'url_base' => Pluf::f('url_base'), - ) - ); - foreach ($to_email as $email_lang) { - Pluf_Translation::loadSetLocale($email_lang[1]); - $email = new Pluf_Mail(Pluf::f('from_email'), $email_lang[0], - sprintf(__('Issue %1$s - %2$s (%3$s)'), - $this->id, $this->summary, $prj->shortname)); - $tmpl = new Pluf_Template('idf/issues/issue-created-email.txt'); - $email->addTextMessage($tmpl->render($context)); - $email->addHeaders(array('Message-ID'=>$id)); - $email->sendMail(); - } - } else { - $comments = $this->get_comments_list(array('order' => 'id DESC')); - $email_sender = ''; - if (isset($comments[0])) { - $email_sender = $comments[0]->get_submitter()->email; - } - foreach ($this->get_interested_list() as $interested) { - $email_lang = array($interested->email, - $interested->language); - if (!in_array($email_lang, $to_email)) { - $to_email[] = $email_lang; - } - } - $email_lang = array($this->get_submitter()->email, - $this->get_submitter()->language); - if (!in_array($email_lang, $to_email)) { - $to_email[] = $email_lang; - } - if (null != $this->get_owner()) { - $email_lang = array($this->get_owner()->email, - $this->get_owner()->language); - if (!in_array($email_lang, $to_email)) { - $to_email[] = $email_lang; - } - } - $context = new Pluf_Template_Context( - array( - 'issue' => $this, - 'comments' => $comments, - 'project' => $prj, - 'url_base' => Pluf::f('url_base'), - )); - foreach ($to_email as $email_lang) { - if ($email_lang[0] == $email_sender) { - continue; // Do not notify the one having created - // the comment - } - Pluf_Translation::loadSetLocale($email_lang[1]); - $email = new Pluf_Mail(Pluf::f('from_email'), $email_lang[0], - sprintf(__('Updated Issue %1$s - %2$s (%3$s)'), - $this->id, $this->summary, $prj->shortname)); - $tmpl = new Pluf_Template('idf/issues/issue-updated-email.txt'); - $email->addTextMessage($tmpl->render($context)); - $email->addHeaders(array('References'=>$id)); - $email->sendMail(); - } + + $from_email = Pluf::f('from_email'); + $comments = $this->get_comments_list(array('order' => 'id DESC')); + $messageId = '<'.md5('issue'.$this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>'; + $recipients = $project->getNotificationRecipientsForTab('issues'); + + // the submitter (might be skipped later on if he is the one who also + // submitted the last comment) + if (!array_key_exists($this->get_submitter()->email, $recipients)) { + $recipients[$this->get_submitter()->email] = $this->get_submitter()->language; } + + // the owner of the issue, if we have one + $owner = $this->get_owner(); + if (null != $owner && !array_key_exists($owner->email, $recipients)) { + $recipients[$owner->email] = $owner->language; + } + + // additional users who starred the issue + foreach ($this->get_interested_list() as $interested) { + if (array_key_exists($interested->email, $recipients)) + continue; + $recipients[$interested->email] = $interested->language; + } + + foreach ($recipients as $address => $language) { + + // do not notify the creator of the last comment, + // i.e. the user who triggered this notification + if ($comments[0]->get_submitter()->email === $address) { + continue; + } + + Pluf_Translation::loadSetLocale($language); + + $context = new Pluf_Template_Context(array( + 'issue' => $this, + 'owns_issue' => $owner !== null && $owner->email === $address, + // the initial comment for create, the last for update + 'comment' => $comments[0], + 'comments' => $comments, + 'project' => $project, + 'url_base' => Pluf::f('url_base'), + )); + + $tplfile = 'idf/issues/issue-created-email.txt'; + $subject = __('Issue %1$s - %2$s (%3$s)'); + $headers = array('Message-ID' => $messageId); + if (!$create) { + $tplfile = 'idf/issues/issue-updated-email.txt'; + $subject = __('Updated Issue %1$s - %2$s (%3$s)'); + $headers = array('References' => $messageId); + } + + $tmpl = new Pluf_Template($tplfile); + $text_email = $tmpl->render($context); + + $email = new Pluf_Mail($from_email, $address, + sprintf($subject, $this->id, $this->summary, $project->shortname)); + $email->addTextMessage($text_email); + $email->addHeaders($headers); + $email->sendMail(); + } + Pluf_Translation::loadSetLocale($current_locale); } } diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php index 5e44abe..c59217b 100644 --- a/src/IDF/Middleware.php +++ b/src/IDF/Middleware.php @@ -85,6 +85,7 @@ class IDF_Middleware 'issuetext' => 'IDF_Template_IssueComment', 'timeline' => 'IDF_Template_TimelineFragment', 'markdown' => 'IDF_Template_Markdown', + 'markdown_forge' => 'IDF_Template_MarkdownForge', 'showuser' => 'IDF_Template_ShowUser', 'ashowuser' => 'IDF_Template_AssignShowUser', 'appversion' => 'IDF_Template_AppVersion', @@ -102,6 +103,7 @@ class IDF_Middleware function IDF_Middleware_ContextPreProcessor($request) { + $forge = IDF_Forge::instance(); $c = array(); $c['request'] = $request; $c['isAdmin'] = ($request->user->administrator or $request->user->staff); @@ -115,6 +117,7 @@ function IDF_Middleware_ContextPreProcessor($request) } $c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null; $c['allProjects'] = IDF_Views::getProjects($request->user); + $c['customForgePageEnabled'] = $forge->isCustomForgePageEnabled(); return $c; } diff --git a/src/IDF/Migrations/19WikiPageAssocs.php b/src/IDF/Migrations/19WikiPageAssocs.php new file mode 100644 index 0000000..8371f17 --- /dev/null +++ b/src/IDF/Migrations/19WikiPageAssocs.php @@ -0,0 +1,74 @@ +pfx.'idf_tag_idf_wiki_page_assoc', $intro->listTables())) { + echo '19 skipping up migration - relation table has correct name already'."\n"; + return; + } + + $db->execute('ALTER TABLE '.$db->pfx.'idf_tag_idf_wikipage_assoc RENAME TO '.$db->pfx.'idf_tag_idf_wiki_page_assoc'); + $db->execute('ALTER TABLE '.$db->pfx.'idf_wikipage_pluf_user_assoc RENAME TO '.$db->pfx.'idf_wiki_page_pluf_user_assoc'); + + if ($engine === 'PostgreSQL') { + $db->execute('ALTER TABLE '.$db->pfx.'idf_tag_idf_wiki_page_assoc RENAME COLUMN idf_wikipage_id TO idf_wiki_page_id'); + $db->execute('ALTER TABLE '.$db->pfx.'idf_wiki_page_pluf_user_assoc RENAME COLUMN idf_wikipage_id TO idf_wiki_page_id'); + } else if ($engine === 'MySQL') { + $db->execute('ALTER TABLE '.$db->pfx.'idf_tag_idf_wiki_page_assoc CHANGE idf_wikipage_id idf_wiki_page_id MEDIUMINT NOT NULL'); + $db->execute('ALTER TABLE '.$db->pfx.'idf_wiki_page_pluf_user_assoc CHANGE idf_wikipage_id idf_wiki_page_id MEDIUMINT NOT NULL'); + } +} + +function IDF_Migrations_19WikiPageAssocs_down($params=null) +{ + $engine = Pluf::f('db_engine'); + if (!in_array($engine, array('MySQL', 'PostgreSQL'))) { + throw new Exception('unsupported engine '.$engine); + } + + $db = Pluf::db(); + $intro = new Pluf_DB_Introspect($db); + if (in_array($db->pfx.'idf_tag_idf_wikipage_assoc', $intro->listTables())) { + echo '19 skipping down migration - relation table has correct name already'."\n"; + return; + } + + $db->execute('ALTER TABLE '.$db->pfx.'idf_tag_idf_wiki_page_assoc RENAME TO '.$db->pfx.'idf_tag_idf_wikipage_assoc'); + $db->execute('ALTER TABLE '.$db->pfx.'idf_wiki_page_pluf_user_assoc RENAME TO '.$db->pfx.'idf_wikipage_pluf_user_assoc'); + + if ($engine === 'PostgreSQL') { + $db->execute('ALTER TABLE '.$db->pfx.'idf_tag_idf_wikipage_assoc RENAME COLUMN idf_wiki_page_id TO idf_wikipage_id'); + $db->execute('ALTER TABLE '.$db->pfx.'idf_wikipage_pluf_user_assoc RENAME COLUMN idf_wiki_page_id TO idf_wikipage_id'); + } else if ($engine === 'MySQL') { + $db->execute('ALTER TABLE '.$db->pfx.'idf_tag_idf_wikipage_assoc CHANGE idf_wiki_page_id idf_wikipage_id MEDIUMINT NOT NULL'); + $db->execute('ALTER TABLE '.$db->pfx.'idf_wikipage_pluf_user_assoc CHANGE idf_wiki_page_id idf_wikipage_id MEDIUMINT NOT NULL'); + } +} diff --git a/src/IDF/Migrations/20AddWikiResources.php b/src/IDF/Migrations/20AddWikiResources.php new file mode 100644 index 0000000..20d82eb --- /dev/null +++ b/src/IDF/Migrations/20AddWikiResources.php @@ -0,0 +1,51 @@ +model = new IDF_Wiki_Resource(); + $schema->createTables(); + + $schema->model = new IDF_Wiki_ResourceRevision(); + $schema->createTables(); +} + +function IDF_Migrations_20AddWikiResources_down($params=null) +{ + $db = Pluf::db(); + $schema = new Pluf_DB_Schema($db); + + $schema->model = new IDF_Wiki_ResourceRevision(); + $schema->dropTables(); + + $schema->model = new IDF_Wiki_Resource(); + $schema->dropTables(); +} diff --git a/src/IDF/Migrations/21WikiPageRevisionName.php b/src/IDF/Migrations/21WikiPageRevisionName.php new file mode 100644 index 0000000..a9ab466 --- /dev/null +++ b/src/IDF/Migrations/21WikiPageRevisionName.php @@ -0,0 +1,60 @@ +pfx.'idf_wikipagerevs', $intro->listTables())) { + echo '21 skipping up migration - table has correct name already'."\n"; + return; + } + + $db->execute('ALTER TABLE '.$db->pfx.'idf_wikirevisions RENAME TO '.$db->pfx.'idf_wikipagerevs'); + $db->execute("UPDATE ".$db->pfx."idf_timeline SET model_class='IDF_Wiki_Page' WHERE model_class LIKE 'IDF_WikiPage'"); + $db->execute("UPDATE ".$db->pfx."idf_timeline SET model_class='IDF_Wiki_PageRevision' WHERE model_class LIKE 'IDF_WikiRevision'"); +} + +function IDF_Migrations_21WikiPageRevisionName_down($params=null) +{ + $engine = Pluf::f('db_engine'); + if (!in_array($engine, array('MySQL', 'PostgreSQL'))) { + throw new Exception('unsupported engine '.$engine); + } + + $db = Pluf::db(); + $intro = new Pluf_DB_Introspect($db); + if (in_array($db->pfx.'idf_wikirevisions', $intro->listTables())) { + echo '21 skipping down migration - table has correct name already'."\n"; + return; + } + + $db->execute('ALTER TABLE '.$db->pfx.'idf_wikipagerevs RENAME TO '.$db->pfx.'idf_wikirevisions'); + $db->execute("UPDATE ".$db->pfx."idf_timeline SET model_class='IDF_WikiPage' WHERE model_class LIKE 'IDF_Wiki_Page'"); + $db->execute("UPDATE ".$db->pfx."idf_timeline SET model_class='IDF_WikiRevision' WHERE model_class LIKE 'IDF_Wiki_PageRevision'"); +} diff --git a/src/IDF/Migrations/22ProjectTagRelationTable.php b/src/IDF/Migrations/22ProjectTagRelationTable.php new file mode 100644 index 0000000..5113af1 --- /dev/null +++ b/src/IDF/Migrations/22ProjectTagRelationTable.php @@ -0,0 +1,60 @@ +pfx.'idf_project_idf_tag_assoc'; + if (!in_array($db->engine, array('MySQL', 'PostgreSQL'))) { + throw new Exception('unsupported engine '.$engine); + } + + $intro = new Pluf_DB_Introspect($db); + if (in_array($table, $intro->listTables())) { + echo '21 skipping up migration - table already exists'."\n"; + return; + } + + $schema = Pluf::factory('Pluf_DB_Schema_'.$db->engine, $db); + $sql = $schema->getSqlCreate(new IDF_Project()); + $db->execute($sql[$table]); +} + +function IDF_Migrations_22ProjectTagRelationTable_down($params=null) +{ + $db = Pluf::db(); + $table = $db->pfx.'idf_project_idf_tag_assoc'; + if (!in_array($db->engine, array('MySQL', 'PostgreSQL'))) { + throw new Exception('unsupported engine '.$engine); + } + + $intro = new Pluf_DB_Introspect($db); + if (!in_array($table, $intro->listTables())) { + echo '22 skipping down migration - table does not exist'."\n"; + return; + } + + $schema = Pluf::factory('Pluf_DB_Schema_'.$db->engine, $db); + $sql = $schema->getSqlDelete(new IDF_Project()); + $db->execute($sql[$table]); +} diff --git a/src/IDF/Migrations/23ProjectActivity.php b/src/IDF/Migrations/23ProjectActivity.php new file mode 100644 index 0000000..bf4b2a7 --- /dev/null +++ b/src/IDF/Migrations/23ProjectActivity.php @@ -0,0 +1,42 @@ +model = new IDF_ProjectActivity(); + $schema->createTables(); +} + +function IDF_Migrations_23ProjectActivity_down($params=null) +{ + $db = Pluf::db(); + $schema = new Pluf_DB_Schema($db); + $schema->model = new IDF_ProjectActivity(); + $schema->dropTables(); +} diff --git a/src/IDF/Migrations/24CurrentProjectActivity.php b/src/IDF/Migrations/24CurrentProjectActivity.php new file mode 100644 index 0000000..78483b1 --- /dev/null +++ b/src/IDF/Migrations/24CurrentProjectActivity.php @@ -0,0 +1,40 @@ +execute('ALTER TABLE '.$db->pfx.'idf_projects ADD COLUMN current_activity INTEGER'); + } else if ($engine === 'MySQL') { + $db->execute('ALTER TABLE '.$db->pfx.'idf_projects ADD COLUMN current_activity MEDIUMINT'); + } +} + +function IDF_Migrations_24CurrentProjectActivity_down($params=null) +{ + $db = Pluf::db(); + $db->execute('ALTER TABLE '.$db->pfx.'idf_projects DROP COLUMN current_activity'); +} diff --git a/src/IDF/Migrations/7Wiki.php b/src/IDF/Migrations/7Wiki.php index 17e487a..5330d63 100644 --- a/src/IDF/Migrations/7Wiki.php +++ b/src/IDF/Migrations/7Wiki.php @@ -22,14 +22,14 @@ # ***** END LICENSE BLOCK ***** */ /** - * Add the download of files. + * Add wiki functionality. */ function IDF_Migrations_7Wiki_up($params=null) { $models = array( - 'IDF_WikiPage', - 'IDF_WikiRevision', + 'IDF_Wiki_Page', + 'IDF_Wiki_PageRevision', ); $db = Pluf::db(); $schema = new Pluf_DB_Schema($db); @@ -42,8 +42,8 @@ function IDF_Migrations_7Wiki_up($params=null) function IDF_Migrations_7Wiki_down($params=null) { $models = array( - 'IDF_WikiRevision', - 'IDF_WikiPage', + 'IDF_Wiki_PageRevision', + 'IDF_Wiki_Page', ); $db = Pluf::db(); $schema = new Pluf_DB_Schema($db); diff --git a/src/IDF/Migrations/Backup.php b/src/IDF/Migrations/Backup.php index 32fcb9e..7d4083a 100644 --- a/src/IDF/Migrations/Backup.php +++ b/src/IDF/Migrations/Backup.php @@ -34,6 +34,7 @@ function IDF_Migrations_Backup_run($folder, $name=null) { $models = array( 'IDF_Project', + 'IDF_ProjectActivity', 'IDF_Tag', 'IDF_Issue', 'IDF_IssueComment', @@ -43,8 +44,10 @@ function IDF_Migrations_Backup_run($folder, $name=null) 'IDF_IssueFile', 'IDF_Commit', 'IDF_Timeline', - 'IDF_WikiPage', - 'IDF_WikiRevision', + 'IDF_Wiki_Page', + 'IDF_Wiki_PageRevision', + 'IDF_Wiki_Resource', + 'IDF_Wiki_ResourceRevision', 'IDF_Review', 'IDF_Review_Patch', 'IDF_Review_Comment', @@ -81,6 +84,7 @@ function IDF_Migrations_Backup_restore($folder, $name) { $models = array( 'IDF_Project', + 'IDF_ProjectActivity', 'IDF_Tag', 'IDF_Issue', 'IDF_IssueComment', @@ -90,8 +94,10 @@ function IDF_Migrations_Backup_restore($folder, $name) 'IDF_IssueFile', 'IDF_Commit', 'IDF_Timeline', - 'IDF_WikiPage', - 'IDF_WikiRevision', + 'IDF_Wiki_Resource', + 'IDF_Wiki_ResourceRevision', + 'IDF_Wiki_Page', + 'IDF_Wiki_PageRevision', 'IDF_Review', 'IDF_Review_Patch', 'IDF_Review_Comment', diff --git a/src/IDF/Migrations/Install.php b/src/IDF/Migrations/Install.php index aea5f00..74709b3 100644 --- a/src/IDF/Migrations/Install.php +++ b/src/IDF/Migrations/Install.php @@ -31,6 +31,7 @@ function IDF_Migrations_Install_setup($params=null) { $models = array( 'IDF_Project', + 'IDF_ProjectActivity', 'IDF_Tag', 'IDF_Issue', 'IDF_IssueComment', @@ -40,8 +41,10 @@ function IDF_Migrations_Install_setup($params=null) 'IDF_IssueFile', 'IDF_Commit', 'IDF_Timeline', - 'IDF_WikiPage', - 'IDF_WikiRevision', + 'IDF_Wiki_Resource', + 'IDF_Wiki_ResourceRevision', + 'IDF_Wiki_Page', + 'IDF_Wiki_PageRevision', 'IDF_Review', 'IDF_Review_Patch', 'IDF_Review_Comment', @@ -97,8 +100,10 @@ function IDF_Migrations_Install_teardown($params=null) 'IDF_Review_Comment', 'IDF_Review_Patch', 'IDF_Review', - 'IDF_WikiRevision', - 'IDF_WikiPage', + 'IDF_Wiki_PageRevision', + 'IDF_Wiki_Page', + 'IDF_Wiki_ResourceRevision', + 'IDF_Wiki_Resource', 'IDF_Timeline', 'IDF_IssueFile', 'IDF_Search_Occ', @@ -108,6 +113,7 @@ function IDF_Migrations_Install_teardown($params=null) 'IDF_Issue', 'IDF_Tag', 'IDF_Commit', + 'IDF_ProjectActivity', 'IDF_Project', 'IDF_EmailAddress', 'IDF_IssueRelation', diff --git a/src/IDF/Project.php b/src/IDF/Project.php index 4199c6c..8cafd2c 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -86,6 +86,13 @@ class IDF_Project extends Pluf_Model 'verbose' => __('description'), 'help_text' => __('The description can be extended using the Markdown syntax.'), ), + 'tags' => + array( + 'type' => 'Pluf_DB_Field_Manytomany', + 'blank' => true, + 'model' => 'IDF_Tag', + 'verbose' => __('labels'), + ), 'private' => array( 'type' => 'Pluf_DB_Field_Integer', @@ -93,7 +100,29 @@ class IDF_Project extends Pluf_Model 'verbose' => __('private'), 'default' => 0, ), - ); + 'current_activity' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'IDF_ProjectActivity', + 'blank' => true, + 'verbose' => __('current project activity'), + ), + ); + $activityTable = $this->_con->pfx.'idf_projectactivities'; + $tagTable = $this->_con->pfx.'idf_project_idf_tag_assoc'; + $this->_a['views'] = array( + 'join_activities_and_tags' => + array( + 'join' => 'LEFT JOIN '.$activityTable.' ON current_activity='.$activityTable.'.id ' + .'LEFT JOIN '.$tagTable.' ON idf_project_id='.$this->getSqlTable().'.id', + 'select' => $this->getSelect().', date, value', + 'group' => $this->getSqlTable().'.id', + 'props' => array( + 'date' => 'current_activity_date', + 'value' => 'current_activity_value' + ), + ), + ); } @@ -427,13 +456,13 @@ GROUP BY uid"; $dep_ids = IDF_Views_Wiki::getDeprecatedPagesIds($this); $extra = ''; if (count($dep_ids)) { - $extra = ' AND idf_wikipage_id NOT IN ('.implode(', ', $dep_ids).') '; + $extra = ' AND idf_wiki_page_id NOT IN ('.implode(', ', $dep_ids).') '; } - $what_t = Pluf::factory('IDF_WikiPage')->getSqlTable(); - $asso_t = $this->_con->pfx.'idf_tag_idf_wikipage_assoc'; + $what_t = Pluf::factory('IDF_Wiki_Page')->getSqlTable(); + $asso_t = $this->_con->pfx.'idf_tag_idf_wiki_page_assoc'; $sql = '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 '.$what_t.' ON idf_wikipage_id='.$what_t.'.id '."\n". + 'LEFT JOIN '.$what_t.' ON idf_wiki_page_id='.$what_t.'.id '."\n". 'WHERE idf_tag_id IS NOT NULL '.$extra.' AND '.$what_t.'.project='.$this->id.' GROUP BY '.$tag_t.'.id, '.$tag_t.'.class, '.$tag_t.'.name ORDER BY '.$tag_t.'.class ASC, '.$tag_t.'.name ASC'; } elseif ($what == 'downloads') { $dep_ids = IDF_Views_Download::getDeprecatedFilesIds($this); @@ -535,12 +564,12 @@ GROUP BY uid"; } /** - * Get the post commit hook key. + * Get the web hook key. * * The goal is to get something predictable but from which one * cannot reverse find the secret key. */ - public function getPostCommitHookKey() + public function getWebHookKey() { return md5($this->id.sha1(Pluf::f('secret_key')).$this->shortname); } @@ -593,6 +622,22 @@ GROUP BY uid"; return $this->_pconf; } + /** + * Magic overload that falls back to the values of the internal configuration + * if no getter / caller matched + * + * @param string $key + */ + public function __get($key) + { + try { + return parent::__get($key); + } + catch (Exception $e) { + return $this->getConf()->getVal($key); + } + } + /** * Get simple statistics about the project. * @@ -606,10 +651,10 @@ GROUP BY uid"; $stats = array(); $stats['total'] = 0; $what = array('downloads' => 'IDF_Upload', - 'reviews' => 'IDF_Review', - 'issues' => 'IDF_Issue', - 'docpages' => 'IDF_WikiPage', - 'commits' => 'IDF_Commit', + 'reviews' => 'IDF_Review', + 'issues' => 'IDF_Issue', + 'docpages' => 'IDF_Wiki_Page', + 'commits' => 'IDF_Commit', ); foreach ($what as $key=>$m) { $i = Pluf::factory($m)->getCount(array('filter' => 'project='.(int)$this->id)); @@ -739,7 +784,8 @@ GROUP BY uid"; Pluf_Signal::send('IDF_Project::preDelete', 'IDF_Project', $params); $what = array('IDF_Upload', 'IDF_Review', 'IDF_Issue', - 'IDF_WikiPage', 'IDF_Commit', 'IDF_Tag', + 'IDF_Wiki_Page', 'IDF_Wiki_Resource', + 'IDF_Commit', 'IDF_Tag', ); foreach ($what as $m) { foreach (Pluf::factory($m)->getList(array('filter' => 'project='.(int)$this->id)) as $item) { @@ -780,4 +826,52 @@ GROUP BY uid"; $this->_isRestricted = false; return false; } + + /** + * Returns an associative array of email addresses to notify about changes + * in a certain tab like 'issues', 'source', and so on. + * + * @param string $tab + * @return array Key is the email address, value is the preferred language setting + */ + public function getNotificationRecipientsForTab($tab) + { + if (!in_array($tab, array('source', 'issues', 'downloads', 'wiki', 'review'))) { + throw new Exception(sprintf('unknown tab %s', $tab)); + } + + $conf = $this->getConf(); + $recipients = array(); + $membership_data = $this->getMembershipData(); + + if ($conf->getVal($tab.'_notification_owners_enabled', false)) { + foreach ($membership_data['owners'] as $owner) { + $recipients[$owner->email] = $owner->language; + } + } + + if ($conf->getVal($tab.'_notification_members_enabled', false)) { + foreach ($membership_data['members'] as $member) { + $recipients[$member->email] = $member->language; + } + } + + if ($conf->getVal($tab.'_notification_email_enabled', false)) { + $addresses = preg_split('/\s*,\s*/', + $conf->getVal($tab.'_notification_email', ''), + -1, PREG_SPLIT_NO_EMPTY); + + // we use a default language setting for this plain list of + // addresses, but we ensure that we do not overwrite an existing + // address which might come with a proper setting already + $languages = Pluf::f('languages', array('en')); + foreach ($addresses as $address) { + if (array_key_exists($address, $recipients)) + continue; + $recipients[$address] = $languages[0]; + } + } + + return $recipients; + } } diff --git a/src/IDF/ProjectActivity.php b/src/IDF/ProjectActivity.php new file mode 100644 index 0000000..0b0f65d --- /dev/null +++ b/src/IDF/ProjectActivity.php @@ -0,0 +1,78 @@ +_a['table'] = 'idf_projectactivities'; + $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' => 'activities', + ), + 'date' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => false, + 'verbose' => __('date'), + ), + 'value' => + array( + 'type' => 'Pluf_DB_Field_Float', + 'blank' => false, + 'verbose' => __('value'), + 'default' => 0, + ), + ); + } + + function postSave($create=false) + { + $prj = $this->get_project(); + $sql = new Pluf_SQL('project=%s', array($prj->id)); + $list = Pluf::factory('IDF_ProjectActivity')->getList(array('filter' => $sql->gen(), 'order' => 'date desc')); + if (count($list) > 0 && $prj->current_activity != $list[0]->id) { + $prj->current_activity = $list[0]; + $prj->update(); + } + } +} diff --git a/src/IDF/Review/Comment.php b/src/IDF/Review/Comment.php index 37157bd..3b6a0fd 100644 --- a/src/IDF/Review/Comment.php +++ b/src/IDF/Review/Comment.php @@ -175,50 +175,63 @@ class IDF_Review_Comment extends Pluf_Model */ public function notify($conf, $create=true) { - $patch = $this->get_patch(); - $review = $patch->get_review(); - $prj = $review->get_project(); - $to_email = array(); - if ('' != $conf->getVal('review_notification_email', '')) { - $langs = Pluf::f('languages', array('en')); - $to_email[] = array($conf->getVal('issues_notification_email'), - $langs[0]); - } - $current_locale = Pluf_Translation::getLocale(); - $reviewers = $review->getReviewers(); + $patch = $this->get_patch(); + $review = $patch->get_review(); + $prj = $review->get_project(); + $reviewers = $review->getReviewers(); + if (!Pluf_Model_InArray($review->get_submitter(), $reviewers)) { $reviewers[] = $review->get_submitter(); } + $comments = $patch->getFileComments(array('order' => 'id DESC')); $gcomments = $patch->get_comments_list(array('order' => 'id DESC')); - $context = new Pluf_Template_Context( - array( - 'review' => $review, - 'patch' => $patch, - 'comments' => $comments, - 'gcomments' => $gcomments, - 'project' => $prj, - 'url_base' => Pluf::f('url_base'), - ) - ); - // build the list of emails and lang - foreach ($reviewers as $user) { - $email_lang = array($user->email, - $user->language); - if (!in_array($email_lang, $to_email)) { - $to_email[] = $email_lang; - } - } - $tmpl = new Pluf_Template('idf/review/review-updated-email.txt'); - foreach ($to_email as $email_lang) { - Pluf_Translation::loadSetLocale($email_lang[1]); - $email = new Pluf_Mail(Pluf::f('from_email'), $email_lang[0], - sprintf(__('Updated Code Review %1$s - %2$s (%3$s)'), - $review->id, $review->summary, $prj->shortname)); - $email->addTextMessage($tmpl->render($context)); + $recipients = $prj->getNotificationRecipientsForTab('review'); + + foreach ($reviewers as $user) { + if (array_key_exists($user->email, $recipients)) + continue; + $recipients[$user->email] = $user->language; + } + + $current_locale = Pluf_Translation::getLocale(); + + $from_email = Pluf::f('from_email'); + $messageId = '<'.md5('review'.$review->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>'; + + foreach ($recipients as $address => $language) { + + if ($this->get_submitter()->email === $address) { + continue; + } + + Pluf_Translation::loadSetLocale($language); + + $context = new Pluf_Template_Context(array( + 'review' => $review, + 'patch' => $patch, + 'comments' => $comments, + 'gcomments' => $gcomments, + 'project' => $prj, + 'url_base' => Pluf::f('url_base'), + )); + + // reviews only updated through comments, see IDF_Review_Patch::notify() + $tplfile = 'idf/review/review-updated-email.txt'; + $subject = __('Updated Code Review %1$s - %2$s (%3$s)'); + $headers = array('References' => $messageId); + + $tmpl = new Pluf_Template($tplfile); + $text_email = $tmpl->render($context); + + $email = new Pluf_Mail($from_email, $address, + sprintf($subject, $review->id, $review->summary, $prj->shortname)); + $email->addTextMessage($text_email); + $email->addHeaders($headers); $email->sendMail(); } + Pluf_Translation::loadSetLocale($current_locale); } diff --git a/src/IDF/Review/Patch.php b/src/IDF/Review/Patch.php index 07ee8e3..c24e77a 100644 --- a/src/IDF/Review/Patch.php +++ b/src/IDF/Review/Patch.php @@ -42,9 +42,9 @@ class IDF_Review_Patch extends Pluf_Model 'id' => array( 'type' => 'Pluf_DB_Field_Sequence', - 'blank' => true, + 'blank' => true, ), - 'review' => + 'review' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'IDF_Review', @@ -59,7 +59,7 @@ class IDF_Review_Patch extends Pluf_Model 'size' => 250, 'verbose' => __('summary'), ), - 'commit' => + 'commit' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'IDF_Commit', @@ -129,8 +129,8 @@ class IDF_Review_Patch extends Pluf_Model function postSave($create=false) { if ($create) { - IDF_Timeline::insert($this, - $this->get_review()->get_project(), + IDF_Timeline::insert($this, + $this->get_review()->get_project(), $this->get_review()->get_submitter()); IDF_Search::index($this->get_review()); } @@ -139,7 +139,7 @@ class IDF_Review_Patch extends Pluf_Model public function timelineFragment($request) { $review = $this->get_review(); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view', + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view', array($request->project->shortname, $review->id)); $out = ''. @@ -150,14 +150,14 @@ class IDF_Review_Patch extends Pluf_Model $ic = (in_array($review->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o'; $out .= sprintf(__('Review %3$d, %4$s'), $url, $ic, $review->id, Pluf_esc($review->summary)).''; $out .= "\n".' -
'.sprintf(__('Creation of review %3$d, by %4$s'), $url, $ic, $review->id, $user).'
'; +
'.sprintf(__('Creation of review %3$d, by %4$s'), $url, $ic, $review->id, $user).'
'; return Pluf_Template::markSafe($out); } public function feedFragment($request) { $review = $this->get_review(); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view', + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view', array($request->project->shortname, $review->id)); $title = sprintf(__('%1$s: Creation of Review %2$d - %3$s'), @@ -179,35 +179,49 @@ class IDF_Review_Patch extends Pluf_Model public function notify($conf, $create=true) { - if ('' == $conf->getVal('review_notification_email', '')) { - return; - } + $review = $this->get_review(); + $project = $review->get_project(); $current_locale = Pluf_Translation::getLocale(); - $langs = Pluf::f('languages', array('en')); - Pluf_Translation::loadSetLocale($langs[0]); - $context = new Pluf_Template_Context( - array( - 'review' => $this->get_review(), - 'patch' => $this, - 'comments' => array(), - 'project' => $this->get_review()->get_project(), - 'url_base' => Pluf::f('url_base'), - ) - ); - $tmpl = new Pluf_Template('idf/review/review-created-email.txt'); - $text_email = $tmpl->render($context); - $addresses = explode(';',$conf->getVal('review_notification_email')); - foreach ($addresses as $address) { - $email = new Pluf_Mail(Pluf::f('from_email'), + $from_email = Pluf::f('from_email'); + $messageId = '<'.md5('review'.$review->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>'; + $recipients = $project->getNotificationRecipientsForTab('review'); + + foreach ($recipients as $address => $language) { + + if ($review->get_submitter()->email === $address) { + continue; + } + + Pluf_Translation::loadSetLocale($language); + + $context = new Pluf_Template_Context(array( + 'review' => $review, + 'patch' => $this, + 'comments' => array(), + 'project' => $project, + 'url_base' => Pluf::f('url_base'), + )); + + // reviews are updated through comments, see IDF_Review_Comment::notify() + $tplfile = 'idf/review/review-created-email.txt'; + $subject = __('New Code Review %1$s - %2$s (%3$s)'); + $headers = array('Message-ID' => $messageId); + + $tmpl = new Pluf_Template($tplfile); + $text_email = $tmpl->render($context); + + $email = new Pluf_Mail($from_email, $address, - sprintf(__('New Code Review %1$s - %2$s (%3$s)'), - $this->get_review()->id, - $this->get_review()->summary, - $this->get_review()->get_project()->shortname)); + sprintf($subject, + $review->id, + $review->summary, + $project->shortname)); $email->addTextMessage($text_email); + $email->addHeaders($headers); $email->sendMail(); } + Pluf_Translation::loadSetLocale($current_locale); } } diff --git a/src/IDF/Scm.php b/src/IDF/Scm.php index 5ebb8d9..350b073 100644 --- a/src/IDF/Scm.php +++ b/src/IDF/Scm.php @@ -497,5 +497,10 @@ class IDF_Scm { return 0; } + + public function repository($request, $match) + { + throw new Exception('This repository does not support web based repository access'); + } } diff --git a/src/IDF/Scm/Git.php b/src/IDF/Scm/Git.php index d84ba84..86b9055 100644 --- a/src/IDF/Scm/Git.php +++ b/src/IDF/Scm/Git.php @@ -918,4 +918,82 @@ class IDF_Scm_Git extends IDF_Scm } return false; } + + public function repository($request, $match) + { + // authenticate: authenticate connection through "extra" password + if (isset($_SERVER['HTTP_AUTHORIZATION']) && $_SERVER['HTTP_AUTHORIZATION'] != '') + list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); + + if (isset($_SERVER['PHP_AUTH_USER'])) { + $sql = new Pluf_SQL('login=%s', array($_SERVER['PHP_AUTH_USER'])); + $users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen())); + if ((count($users) == 1) && ($users[0]->active)) { + $user = $users[0]; + $realkey = substr(sha1($user->password.Pluf::f('secret_key')), 0, 8); + if ($_SERVER['PHP_AUTH_PW'] == $realkey) { + $request->user = $user; + } + } + } + + if (IDF_Precondition::accessSource($request) !== true) { + $response = new Pluf_HTTP_Response(""); + $response->status_code = 401; + $response->headers['WWW-Authenticate']='Basic realm="git for '.$this->project.'"'; + return $response; + } + + $path = $match[2]; + + // update files before delivering them + if (($path == 'objects/info/pack') || ($path == 'info/refs')) { + $cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', ''). + 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' update-server-info -f', + escapeshellarg($this->repo)); + self::shell_exec('IDF_Scm_Git::repository', $cmd); + } + + // smart HTTP discovery + if (($path == 'info/refs') && + (array_key_exists('service', $request->GET))){ + $service = $request->GET["service"]; + switch ($service) { + case 'git-upload-pack': + case 'git-receive-pack': + $content = sprintf('%04x',strlen($service)+15). + '# service='.$service."\n0000"; + $content .= self::shell_exec('IDF_Scm_Git::repository', + $service.' --stateless-rpc --advertise-refs '. + $this->repo); + $response = new Pluf_HTTP_Response($content, + 'application/x-'.$service.'-advertisement'); + return $response; + default: + throw new Exception('unknown service: '.$service); + } + } + + switch($path) { + // smart HTTP RPC + case 'git-upload-pack': + case 'git-receive-pack': + $response = new Pluf_HTTP_Response_CommandPassThru($path. + ' --stateless-rpc '.$this->repo, + 'application/x-'.$path.'-result'); + $response->addStdin('php://input'); + return $response; + + // regular file + default: + // make sure we're inside the repo hierarchy (ie. no break-out) + if (is_file($this->repo.'/'.$path) && + strpos(realpath($this->repo.'/'.$path), $this->repo.'/') == 0) { + return new Pluf_HTTP_Response_File($this->repo.'/'.$path, + 'application/octet-stream'); + } else { + return new Pluf_HTTP_Response_NotFound($request); + } + } + } } diff --git a/src/IDF/Tag.php b/src/IDF/Tag.php index 59026c2..b9fa25d 100644 --- a/src/IDF/Tag.php +++ b/src/IDF/Tag.php @@ -42,16 +42,16 @@ class IDF_Tag extends Pluf_Model array( 'type' => 'Pluf_DB_Field_Sequence', //It is automatically added. - 'blank' => true, + 'blank' => true, ), - 'project' => + 'project' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'IDF_Project', 'blank' => false, 'verbose' => __('project'), ), - 'class' => + 'class' => array( 'type' => 'Pluf_DB_Field_Varchar', 'blank' => false, @@ -59,13 +59,13 @@ class IDF_Tag extends Pluf_Model 'verbose' => __('tag class'), 'help_text' => __('The class of the tag.'), ), - 'name' => + 'name' => array( 'type' => 'Pluf_DB_Field_Varchar', 'blank' => false, 'verbose' => __('name'), ), - 'lcname' => + 'lcname' => array( 'type' => 'Pluf_DB_Field_Varchar', 'blank' => false, @@ -75,6 +75,18 @@ class IDF_Tag extends Pluf_Model ), ); + $table = $this->_con->pfx.'idf_project_idf_tag_assoc'; + $this->_a['views'] = array( + 'join_projects' => + array( + 'join' => 'LEFT JOIN '.$table + .' ON idf_tag_id=id', + 'select' => $this->getSelect().',COUNT(idf_project_id) as project_count', + 'group' => 'idf_tag_id', + 'props' => array('project_count' => 'project_count'), + ), + ); + $this->_a['idx'] = array( 'lcname_idx' => array( @@ -95,7 +107,7 @@ class IDF_Tag extends Pluf_Model } /** - * Add a tag if not already existing. + * Add a project-specific tag if not already existing. * * @param string Name of the tag. * @param IDF_Project Project of the tag. @@ -107,7 +119,7 @@ class IDF_Tag extends Pluf_Model $class = trim($class); $name = trim($name); $gtag = new IDF_Tag(); - $sql = new Pluf_SQL('class=%s AND lcname=%s AND project=%s', + $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) { @@ -122,6 +134,32 @@ class IDF_Tag extends Pluf_Model return $tags[0]; } + /** + * Add a global tag if not already existing + * + * @param string Name of the tag. + * @param string Class of the tag (IDF_TAG_DEFAULT_CLASS) + * @return IDF_Tag The tag. + */ + public static function addGlobal($name, $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=0', + array($class, mb_strtolower($name))); + $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->create(); + return $tag; + } + return $tags[0]; + } + function __toString() { if ($this->class != IDF_TAG_DEFAULT_CLASS) { diff --git a/src/IDF/Template/Markdown.php b/src/IDF/Template/Markdown.php index 7ce47b1..4c05704 100644 --- a/src/IDF/Template/Markdown.php +++ b/src/IDF/Template/Markdown.php @@ -55,8 +55,17 @@ class IDF_Template_Markdown extends Pluf_Template_Tag $text = IDF_Template_safePregReplace('#\[\[([A-Za-z0-9\-]+)\]\]#im', array($this, 'callbackWikiPageNoName'), $text); + $filter = new IDF_Template_MarkdownPrefilter(); - echo $filter->go(Pluf_Text_MarkDown_parse($text)); + $text = $filter->go(Pluf_Text_MarkDown_parse($text)); + + // Replace [[!ResourceName]] with corresponding HTML for the resource; + // we need to do that after the HTML filtering as we'd otherwise be unable to use + // certain HTML elements, such as iframes, that are used to display text content + // FIXME: no support for escaping yet in place + echo IDF_Template_safePregReplace('#\[\[!([A-Za-z0-9\-]+)(?:,\s*([^\]]+))?\]\]#im', + array($this, 'callbackWikiResource'), + $text); } function callbackWikiPageNoName($m) @@ -69,15 +78,99 @@ class IDF_Template_Markdown extends Pluf_Template_Tag { $sql = new Pluf_SQL('project=%s AND title=%s', array($this->project->id, $m[2])); - $pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen())); + $pages = Pluf::factory('IDF_Wiki_Page')->getList(array('filter'=>$sql->gen())); if ($pages->count() != 1 and $this->request->rights['hasWikiAccess'] and !$this->request->user->isAnonymous()) { - return ' '.$m[1].''; + return ' '.$m[1].''; } if (!$this->request->rights['hasWikiAccess'] or $pages->count() == 0) { return $m[1]; } - return ''.$m[1].''; + return ''.$m[1].''; + } + + function callbackWikiResource($m) + { + @list($match, $resourceName, $opts) = $m; + + if (!$this->request->rights['hasWikiAccess']) { + return ''.$match.''; + } + + $sql = new Pluf_SQL('project=%s AND title=%s', + array($this->project->id, $resourceName)); + $resources = Pluf::factory('IDF_Wiki_Resource')->getList(array('filter'=>$sql->gen())); + + if ($resources->count() == 0) { + if ($this->request->user->isAnonymous()) { + return ''.$match.''; + } + + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::createResource', + array($this->project->shortname), + array('name' => $resourceName)); + return ' '. + ''.$match.''; + } + + // by default, render the most recent revision + $resourceRevision = $resources[0]->get_current_revision(); + + list($urlConf, $urlMatches) = $this->request->view; + + // if we currently look at an existing wiki page, look up its name and find the proper resource (if any) + if ($urlConf['model'] == 'IDF_Views_Wiki' && $urlConf['method'] == 'viewPage') { + $sql = new Pluf_SQL('project=%s AND title=%s', + array($this->project->id, $urlMatches[2])); + $pages = Pluf::factory('IDF_Wiki_Page')->getList(array('filter'=>$sql->gen())); + if ($pages->count() == 0) throw new Exception('page not found'); + $pageRevision = $pages[0]->get_current_revision(); + + // if we look at an old version of the page, figure out the resource version back then + if (isset($this->request->GET['rev']) and preg_match('/^[0-9]+$/', $this->request->GET['rev'])) { + $pageRevision = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_PageRevision', + $this->request->GET['rev']); + // this is actually an invariant since we came so far looking at + // and rendering the old revision already + if ($pageRevision == null) { + throw new Exception('page revision with id '.$this->request->GET['rev'].' not found'); + } + } + + $sql = new Pluf_SQL('wikiresource=%s AND idf_wiki_pagerevision_id=%s', + array($resources[0]->id, $pageRevision->id)); + $resourceRevision = Pluf::factory('IDF_Wiki_ResourceRevision')->getOne( + array('filter' => $sql->gen(), 'view' => 'join_pagerevision')); + + if ($resourceRevision == null) { + return ''.$match.''; + } + } + + $validOpts = array( + 'align' => '/^(left|right|center)$/', + 'width' => '/^\d+(%|px|em)?$/', + 'height' => '/^\d+(%|px|em)?$/', + 'preview' => '/^yes|no$/', + 'title' => '/.+/', + ); + + $parsedOpts = array(); + // FIXME: no support for escaping yet in place + $opts = preg_split('/\s*,\s*/', $opts, -1, PREG_SPLIT_NO_EMPTY); + foreach ((array)@$opts as $opt) + { + list($key, $value) = preg_split('/\s*=\s*/', $opt, 2); + if (!array_key_exists($key, $validOpts)) { + continue; + } + if (!preg_match($validOpts[$key], $value)) { + continue; + } + $parsedOpts[$key] = $value; + } + + return $resourceRevision->render($parsedOpts); } function callbackEmbeddedDoc($m) diff --git a/src/IDF/Template/MarkdownForge.php b/src/IDF/Template/MarkdownForge.php new file mode 100644 index 0000000..1169095 --- /dev/null +++ b/src/IDF/Template/MarkdownForge.php @@ -0,0 +1,118 @@ +request = $request; + $filter = new IDF_Template_MarkdownPrefilter(); + $text = $filter->go(Pluf_Text_MarkDown_parse($text)); + + // replace {}-macros with the corresponding template rendering + echo IDF_Template_safePregReplace('#\{(\w+)(?:,\s*([^\}]+))?\}#im', + array($this, 'callbackMacros'), + $text); + } + + public function callbackMacros($matches) + { + @list(, $macro, $opts) = $matches; + $known_macros = array('projectlist'); + if (!in_array($macro, $known_macros)) { + return $matches[0]; + } + $callbackName = 'callback'.ucfirst(strtolower($macro)).'Macro'; + return $this->callbackProjectlistMacro($opts); + } + + public function callbackProjectlistMacro($opts) + { + $validOpts = array( + 'label' => '/^\d+|(\w+:)?\w+$/', + 'order' => '/^name|activity$/', + 'limit' => '/^\d+$/', + ); + + $parsedOpts = array(); + // FIXME: no support for escaping yet in place + $opts = preg_split('/\s*,\s*/', $opts, -1, PREG_SPLIT_NO_EMPTY); + foreach ((array)@$opts as $opt) + { + list($key, $value) = preg_split('/\s*=\s*/', $opt, 2); + if (!array_key_exists($key, $validOpts)) { + continue; + } + if (!preg_match($validOpts[$key], $value)) { + continue; + } + $parsedOpts[$key] = $value; + } + + $tag = false; + if (!empty($parsedOpts['label'])) { + if (is_numeric($parsedOpts['label'])) { + $tag = Pluf::factory('IDF_Tag')->get($parsedOpts['label']); + } else { + @list($class, $name) = preg_split('/:/', $parsedOpts['label'], 2); + if (empty($name)) { + $name = $class; + $class = IDF_TAG_DEFAULT_CLASS; + } + $sql = new Pluf_SQL('class=%s AND lcname=%s AND project=0', + array(strtolower($class), mb_strtolower($name))); + $tag = Pluf::factory('IDF_Tag')->getOne(array('filter' => $sql->gen())); + } + // ignore non-global tags + if ($tag !== false && $tag->project > 0) { + $tag = false; + } + } + + $order = 'name'; + if (!empty($parsedOpts['order'])) { + $order = $parsedOpts['order']; + } + + $projects = IDF_Views::getProjects($this->request->user, $tag, $order); + if (!empty($parsedOpts['limit']) && $parsedOpts['limit'] < count($projects)) { + // there is no array_slice on ArrayObject, do'h! + $projectsCopy = array(); + for ($i=0; $i<$parsedOpts['limit']; ++$i) + $projectsCopy[] = $projects[$i]; + $projects = $projectsCopy; + } + + $tmpl = new Pluf_Template('idf/project-list.html'); + $context = new Pluf_Template_Context(array( + 'projects' => $projects, + 'order' => 'name', + )); + return $tmpl->render($context); + } +} + diff --git a/src/IDF/Upload.php b/src/IDF/Upload.php index b683156..2fa7aec 100644 --- a/src/IDF/Upload.php +++ b/src/IDF/Upload.php @@ -234,31 +234,91 @@ class IDF_Upload extends Pluf_Model */ public function notify($conf, $create=true) { - if ('' == $conf->getVal('downloads_notification_email', '')) { - return; - } - $current_locale = Pluf_Translation::getLocale(); - $langs = Pluf::f('languages', array('en')); - Pluf_Translation::loadSetLocale($langs[0]); + $project = $this->get_project(); + $url = str_replace(array('%p', '%d'), + array($project->shortname, $this->id), + $conf->getVal('upload_webhook_url', '')); - $context = new Pluf_Template_Context( - array('file' => $this, - 'urlfile' => $this->getAbsoluteUrl($this->get_project()), - 'project' => $this->get_project(), - 'tags' => $this->get_tags_list(), - )); - $tmpl = new Pluf_Template('idf/downloads/download-created-email.txt'); - $text_email = $tmpl->render($context); - $addresses = explode(',', $conf->getVal('downloads_notification_email')); - foreach ($addresses as $address) { - $email = new Pluf_Mail(Pluf::f('from_email'), + $tags = array(); + foreach ($this->get_tags_list() as $tag) { + $tags[] = $tag->class.':'.$tag->name; + } + + $submitter = $this->get_submitter(); + $payload = array( + 'to_send' => array( + 'project' => $project->shortname, + 'id' => $this->id, + 'summary' => $this->summary, + 'changelog' => $this->changelog, + 'filename' => $this->file, + 'filesize' => $this->filesize, + 'md5sum' => $this->md5, + 'submitter_login' => $submitter->login, + 'submitter_email' => $submitter->email, + 'tags' => $tags, + ), + 'project_id' => $project->id, + 'authkey' => $project->getWebHookKey(), + 'url' => $url, + ); + + if ($create === true) { + $payload['method'] = 'PUT'; + $payload['to_send']['creation_date'] = $this->creation_dtime; + } else { + $payload['method'] = 'POST'; + $payload['to_send']['update_date'] = $this->modif_dtime; + } + + $item = new IDF_Queue(); + $item->type = 'upload'; + $item->payload = $payload; + $item->create(); + + $current_locale = Pluf_Translation::getLocale(); + + $from_email = Pluf::f('from_email'); + $messageId = '<'.md5('upload'.$this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>'; + $recipients = $project->getNotificationRecipientsForTab('downloads'); + + foreach ($recipients as $address => $language) { + + if ($this->get_submitter()->email === $address) { + continue; + } + + Pluf_Translation::loadSetLocale($language); + + $context = new Pluf_Template_Context(array( + 'file' => $this, + 'urlfile' => $this->getAbsoluteUrl($project), + 'project' => $project, + 'tags' => $this->get_tags_list(), + )); + + $tplfile = 'idf/downloads/download-created-email.txt'; + $subject = __('New download - %1$s (%2$s)'); + $headers = array('Message-ID' => $messageId); + if (!$create) { + $tplfile = 'idf/downloads/download-updated-email.txt'; + $subject = __('Updated download - %1$s (%2$s)'); + $headers = array('References' => $messageId); + } + + $tmpl = new Pluf_Template($tplfile); + $text_email = $tmpl->render($context); + + $email = new Pluf_Mail($from_email, $address, - sprintf(__('New download - %1$s (%2$s)'), + sprintf($subject, $this->summary, - $this->get_project()->shortname)); + $project->shortname)); $email->addTextMessage($text_email); + $email->addHeaders($headers); $email->sendMail(); } + Pluf_Translation::loadSetLocale($current_locale); } } diff --git a/src/IDF/Views.php b/src/IDF/Views.php index beca428..85dfa9b 100644 --- a/src/IDF/Views.php +++ b/src/IDF/Views.php @@ -32,20 +32,67 @@ Pluf::loadFunction('Pluf_Shortcuts_GetFormForModel'); class IDF_Views { /** - * List all the projects managed by InDefero. - * - * Only the public projects are listed or the private with correct - * rights. + * The index view. */ - public function index($request, $match, $api=false) + public function index($request, $match) { - $projects = self::getProjects($request->user); - $stats = self::getProjectsStatistics ($projects); - - if ($api == true) return $projects; - return Pluf_Shortcuts_RenderToResponse('idf/index.html', + $forge = IDF_Forge::instance(); + if (!$forge->isCustomForgePageEnabled()) { + $url = Pluf_HTTP_URL_urlForView('IDF_Views::listProjects'); + return new Pluf_HTTP_Response_Redirect($url); + } + + return Pluf_Shortcuts_RenderToResponse('idf/index.html', + array('page_title' => __('Welcome'), + 'content' => $forge->getCustomForgePageContent(), + ), + $request); + } + + /** + * List all projects unfiltered + * + * @param unknown_type $request + * @param unknown_type $match + * @return Pluf_HTTP_Response + */ + public function listProjects($request, $match) + { + $match = array('', 'all', 'name'); + return $this->listProjectsByLabel($request, $match); + } + + /** + * List projects, optionally filtered by label + * + * @param unknown_type $request + * @param unknown_type $match + * @return Pluf_HTTP_Response + */ + public function listProjectsByLabel($request, $match) + { + list(, $tagId, $order) = $match; + + $tag = false; + if ($tagId !== 'all') { + $tag = Pluf::factory('IDF_Tag')->get($match[1]); + // ignore non-global tags + if ($tag !== false && $tag->project > 0) { + $tag = false; + } + } + $order = in_array($order, array('name', 'activity')) ? $order : 'name'; + + $projects = self::getProjects($request->user, $tag, $order); + $stats = self::getProjectsStatistics($projects); + $projectLabels = IDF_Forge::instance()->getProjectLabelsWithCounts(); + + return Pluf_Shortcuts_RenderToResponse('idf/listProjects.html', array('page_title' => __('Projects'), 'projects' => $projects, + 'projectLabels' => $projectLabels, + 'tag' => $tag, + 'order' => $order, 'stats' => new Pluf_Template_ContextVars($stats)), $request); } @@ -55,7 +102,7 @@ class IDF_Views */ public function login($request, $match) { - if (isset($request->POST['action']) + if (isset($request->POST['action']) and $request->POST['action'] == 'new-user') { $login = (isset($request->POST['login'])) ? $request->POST['login'] : ''; $url = Pluf_HTTP_URL_urlForView('IDF_Views::register', array(), @@ -91,7 +138,7 @@ class IDF_Views $params = array('request'=>$request); if ($request->method == 'POST') { $form = new IDF_Form_Register(array_merge( - (array)$request->POST, + (array)$request->POST, (array)$request->FILES ), $params); if ($form->isValid()) { @@ -108,7 +155,7 @@ class IDF_Views $context = new Pluf_Template_Context(array()); $tmpl = new Pluf_Template('idf/terms.html'); $terms = Pluf_Template::markSafe($tmpl->render($context)); - return Pluf_Shortcuts_RenderToResponse('idf/register/index.html', + return Pluf_Shortcuts_RenderToResponse('idf/register/index.html', array('page_title' => $title, 'form' => $form, 'terms' => $terms), @@ -133,7 +180,7 @@ class IDF_Views } else { $form = new IDF_Form_RegisterInputKey(); } - return Pluf_Shortcuts_RenderToResponse('idf/register/inputkey.html', + return Pluf_Shortcuts_RenderToResponse('idf/register/inputkey.html', array('page_title' => $title, 'form' => $form), $request); @@ -168,7 +215,7 @@ class IDF_Views $request->session->clear(); $request->session->setData('login_time', gmdate('Y-m-d H:i:s')); $user->last_login = gmdate('Y-m-d H:i:s'); - $user->update(); + $user->update(); $request->user->setMessage(__('Welcome! You can now participate in the life of your project of choice.')); $url = Pluf_HTTP_URL_urlForView('IDF_Views::index'); return new Pluf_HTTP_Response_Redirect($url); @@ -176,7 +223,7 @@ class IDF_Views } else { $form = new IDF_Form_RegisterConfirmation(null, $extra); } - return Pluf_Shortcuts_RenderToResponse('idf/register/confirmation.html', + return Pluf_Shortcuts_RenderToResponse('idf/register/confirmation.html', array('page_title' => $title, 'new_user' => $user, 'form' => $form), @@ -213,7 +260,7 @@ class IDF_Views /** * If the key is valid, provide a nice form to reset the password - * and automatically login the user. + * and automatically login the user. * * This is also firing the password change event for the plugins. */ @@ -238,7 +285,7 @@ class IDF_Views $request->session->clear(); $request->session->setData('login_time', gmdate('Y-m-d H:i:s')); $user->last_login = gmdate('Y-m-d H:i:s'); - $user->update(); + $user->update(); $request->user->setMessage(__('Welcome back! Next time, you can use your broswer options to remember the password.')); $url = Pluf_HTTP_URL_urlForView('IDF_Views::index'); return new Pluf_HTTP_Response_Redirect($url); @@ -246,12 +293,12 @@ class IDF_Views } else { $form = new IDF_Form_PasswordReset(null, $extra); } - return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery.html', + return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery.html', array('page_title' => $title, 'new_user' => $user, 'form' => $form), $request); - + } /** @@ -270,7 +317,7 @@ class IDF_Views } else { $form = new IDF_Form_PasswordInputKey(); } - return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery-inputkey.html', + return Pluf_Shortcuts_RenderToResponse('idf/user/passrecovery-inputkey.html', array('page_title' => $title, 'form' => $form), $request); @@ -283,7 +330,23 @@ class IDF_Views { $title = __('Here to Help You!'); $projects = self::getProjects($request->user); - return Pluf_Shortcuts_RenderToResponse('idf/faq.html', + return Pluf_Shortcuts_RenderToResponse('idf/faq.html', + array( + 'page_title' => $title, + 'projects' => $projects, + ), + $request); + + } + + /** + * Download archive FAQ. + */ + public function faqArchiveFormat($request, $match) + { + $title = __('InDefero Upload Archive Format'); + $projects = self::getProjects($request->user); + return Pluf_Shortcuts_RenderToResponse('idf/faq-archive-format.html', array( 'page_title' => $title, 'projects' => $projects, @@ -299,7 +362,7 @@ class IDF_Views { $title = __('InDefero API (Application Programming Interface)'); $projects = self::getProjects($request->user); - return Pluf_Shortcuts_RenderToResponse('idf/faq-api.html', + return Pluf_Shortcuts_RenderToResponse('idf/faq-api.html', array( 'page_title' => $title, 'projects' => $projects, @@ -309,45 +372,59 @@ class IDF_Views } /** - * Returns a list of projects accessible for the user. + * Returns a list of projects accessible for the user and optionally filtered by tag. * * @param Pluf_User + * @param IDF_Tag * @return ArrayObject IDF_Project */ - public static function getProjects($user) + public static function getProjects($user, $tag = false, $order = 'name') { $db =& Pluf::db(); $false = Pluf_DB_BooleanToDb(false, $db); - if ($user->isAnonymous()) { - $sql = sprintf('%s=%s', $db->qn('private'), $false); - return Pluf::factory('IDF_Project')->getList(array('filter'=> $sql, - 'order' => 'name ASC')); + $sql = new Pluf_SQL(1); + if ($tag !== false) { + $sql->SAnd(new Pluf_SQL('idf_tag_id=%s', $tag->id)); } - if ($user->administrator) { - return Pluf::factory('IDF_Project')->getList(array('order' => 'name ASC')); - } - // grab the list of projects where the user is admin, member - // or authorized - $perms = array( - Pluf_Permission::getFromString('IDF.project-member'), - Pluf_Permission::getFromString('IDF.project-owner'), - Pluf_Permission::getFromString('IDF.project-authorized-user') - ); - $sql = new Pluf_SQL("model_class='IDF_Project' AND owner_class='Pluf_User' AND owner_id=%s AND negative=".$false, $user->id); - $rows = Pluf::factory('Pluf_RowPermission')->getList(array('filter' => $sql->gen())); - - $sql = sprintf('%s=%s', $db->qn('private'), $false); - if ($rows->count() > 0) { - $ids = array(); - foreach ($rows as $row) { - $ids[] = $row->model_id; + + if ($user->isAnonymous()) + { + $authSql = new Pluf_SQL('private=%s', $false); + $sql->SAnd($authSql); + } else + if (!$user->administrator) { + // grab the list of projects where the user is admin, + // member or authorized + $perms = array( + Pluf_Permission::getFromString('IDF.project-member'), + Pluf_Permission::getFromString('IDF.project-owner'), + Pluf_Permission::getFromString('IDF.project-authorized-user') + ); + $permSql = new Pluf_SQL("model_class='IDF_Project' AND owner_class='Pluf_User' AND owner_id=%s AND negative=".$false, $user->id); + $rows = Pluf::factory('Pluf_RowPermission')->getList(array('filter' => $permSql->gen())); + + $authSql = new Pluf_SQL('private=%s', $false); + if ($rows->count() > 0) { + $ids = array(); + foreach ($rows as $row) { + $ids[] = $row->model_id; + } + $authSql->SOr(new Pluf_SQL(sprintf($db->pfx.'idf_projects.id IN (%s)', implode(', ', $ids)))); } - $sql .= sprintf(' OR id IN (%s)', implode(', ', $ids)); + $sql->SAnd($authSql); } - return Pluf::factory('IDF_Project')->getList(array('filter' => $sql, - 'order' => 'name ASC')); + + $orderTypes = array( + 'name' => 'name ASC', + 'activity' => 'value DESC, name ASC', + ); + return Pluf::factory('IDF_Project')->getList(array( + 'filter'=> $sql->gen(), + 'view' => 'join_activities_and_tags', + 'order' => $orderTypes[$order], + )); } - + /** * Returns statistics on a list of projects. * @@ -362,25 +439,22 @@ class IDF_Views 'issues' => 0, 'docpages' => 0, 'commits' => 0); - + // Count for each projects foreach ($projects as $p) { - $pstats = $p->getStats (); + $pstats = $p->getStats(); $forgestats['downloads'] += $pstats['downloads']; $forgestats['reviews'] += $pstats['reviews']; $forgestats['issues'] += $pstats['issues']; $forgestats['docpages'] += $pstats['docpages']; $forgestats['commits'] += $pstats['commits']; } - - // Count projects - $forgestats['projects'] = count($projects); // Count members $sql = new Pluf_SQL('first_name != %s', array('---')); $forgestats['members'] = Pluf::factory('Pluf_User') ->getCount(array('filter' => $sql->gen())); - + return $forgestats; } } diff --git a/src/IDF/Views/Admin.php b/src/IDF/Views/Admin.php index 60b160e..7e78698 100644 --- a/src/IDF/Views/Admin.php +++ b/src/IDF/Views/Admin.php @@ -32,20 +32,40 @@ Pluf::loadFunction('Pluf_Shortcuts_GetFormForModel'); class IDF_Views_Admin { /** - * Home page of the administration. - * - * It should provide an overview of the forge status. + * Start page of the administration. */ - public $home_precond = array('Pluf_Precondition::staffRequired'); - public function home($request, $match) + public $forge_precond = array('Pluf_Precondition::staffRequired'); + public function forge($request, $match) { $title = __('Forge Management'); - return Pluf_Shortcuts_RenderToResponse('idf/gadmin/home.html', + $forge = IDF_Forge::instance(); + if ($request->method == 'POST') { + $form = new IDF_Form_Admin_ForgeConf($request->POST); + if ($form->isValid()) { + $forge->setCustomForgePageEnabled($form->cleaned_data['enabled']); + $forge->setCustomForgePageContent($form->cleaned_data['content']); + $request->user->setMessage(__('The forge configuration has been saved.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::forge'); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $params = array(); + $params['enabled'] = $forge->isCustomForgePageEnabled(); + if (($content = $forge->getCustomForgePageContent(false)) !== false) { + $params['content'] = $content; + } + if (count($params) == 0) { + $params = null; //Nothing in the db, so new form. + } + $form = new IDF_Form_Admin_ForgeConf($params); + } + return Pluf_Shortcuts_RenderToResponse('idf/gadmin/forge/index.html', array( - 'page_title' => $title, - ), + 'page_title' => $title, + 'form' => $form, + ), $request); - } + } /** * Projects overview. @@ -81,6 +101,40 @@ class IDF_Views_Admin $request); } + /** + * Administrate the labels of a project. + */ + public $projectLabels_precond = array('Pluf_Precondition::staffRequired'); + public function projectLabels($request, $match) + { + $title = __('Project Labels'); + $forge = IDF_Forge::instance(); + if ($request->method == 'POST') { + $form = new IDF_Form_Admin_LabelConf($request->POST); + if ($form->isValid()) { + $forge->setProjectLabels($form->cleaned_data['project_labels']); + $request->user->setMessage(__('The label configuration has been saved.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::projectLabels'); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $params = array(); + if (($labels = $forge->getProjectLabels(false)) !== false) { + $params['project_labels'] = $labels; + } + if (count($params) == 0) { + $params = null; //Nothing in the db, so new form. + } + $form = new IDF_Form_Admin_LabelConf($params); + } + return Pluf_Shortcuts_RenderToResponse('idf/gadmin/projects/labels.html', + array( + 'page_title' => $title, + 'form' => $form, + ), + $request); + } + /** * Edition of a project. * @@ -106,12 +160,16 @@ class IDF_Views_Admin } else { $form = new IDF_Form_Admin_ProjectUpdate(null, $params); } + $arrays = IDF_Views_Project::autoCompleteArrays(); return Pluf_Shortcuts_RenderToResponse('idf/gadmin/projects/update.html', - array( - 'page_title' => $title, - 'project' => $project, - 'form' => $form, - ), + array_merge( + array( + 'page_title' => $title, + 'project' => $project, + 'form' => $form, + ), + $arrays + ), $request); } @@ -139,12 +197,17 @@ class IDF_Views_Admin $form = new IDF_Form_Admin_ProjectCreate(null, $extra); } $base = Pluf::f('url_base').Pluf::f('idf_base').'/p/'; + + $arrays = IDF_Views_Project::autoCompleteArrays(); return Pluf_Shortcuts_RenderToResponse('idf/gadmin/projects/create.html', - array( - 'page_title' => $title, - 'form' => $form, - 'base_url' => $base, - ), + array_merge( + array( + 'page_title' => $title, + 'form' => $form, + 'base_url' => $base, + ), + $arrays + ), $request); } diff --git a/src/IDF/Views/Api.php b/src/IDF/Views/Api.php index ba78dec..c0e488d 100644 --- a/src/IDF/Views/Api.php +++ b/src/IDF/Views/Api.php @@ -29,7 +29,7 @@ * JSON instead of HTML. * * A special precondition is used to set the $request->user from the - * _login, _hash and _salt parameters. + * _login, _hash and _salt parameters. */ class IDF_Views_Api { @@ -90,17 +90,16 @@ class IDF_Views_Api * List all the projects */ public $projectIndex_precond = array('IDF_Precondition::apiSetUser'); - + public function projectIndex($request, $match) { - $view = new IDF_Views(); - $projects = $view->index($request, $match, true); - + $projects = IDF_Views::getProjects($request->user); + $data = array(); foreach ($projects as $p) { $data[] = array("shortname" => $p->shortname, "name" => $p->name, "shortdesc" => $p->shortdesc, "private" => $p->private); } - + $out = array(); $out['message'] = 'success'; $out['projects'] = $data; diff --git a/src/IDF/Views/Download.php b/src/IDF/Views/Download.php index 5fb49e8..637e25e 100644 --- a/src/IDF/Views/Download.php +++ b/src/IDF/Views/Download.php @@ -205,7 +205,8 @@ class IDF_Views_Download $path = $upload->getFullPath(); $mime = IDF_FileUtil::getMimeType($path); $render = new Pluf_HTTP_Response_File($path, $mime[0]); - $render->headers["Content-MD5"] = $upload->md5; + $render->headers['Content-MD5'] = $upload->md5; + $render->headers['Content-Disposition'] = 'attachment; filename="'.$upload->file.'"'; return $render; } @@ -224,11 +225,11 @@ class IDF_Views_Download } /** - * Submit a new file for download. + * Create a new file for download. */ - public $submit_precond = array('IDF_Precondition::accessDownloads', + public $create_precond = array('IDF_Precondition::accessDownloads', 'IDF_Precondition::projectMemberOrOwner'); - public function submit($request, $match) + public function create($request, $match) { $prj = $request->project; $title = __('New Download'); @@ -250,7 +251,7 @@ class IDF_Views_Download array('project' => $prj, 'user' => $request->user)); } - return Pluf_Shortcuts_RenderToResponse('idf/downloads/submit.html', + return Pluf_Shortcuts_RenderToResponse('idf/downloads/create.html', array( 'auto_labels' => self::autoCompleteArrays($prj), 'page_title' => $title, @@ -259,6 +260,39 @@ class IDF_Views_Download $request); } + /** + * Create new downloads from an uploaded archive. + */ + public $createFromArchive_precond = array('IDF_Precondition::accessDownloads', + 'IDF_Precondition::projectMemberOrOwner'); + public function createFromArchive($request, $match) + { + $prj = $request->project; + $title = __('New Downloads from Archive'); + if ($request->method == 'POST') { + $form = new IDF_Form_UploadArchive(array_merge($request->POST, $request->FILES), + array('project' => $prj, + 'user' => $request->user)); + if ($form->isValid()) { + $upload = $form->save(); + $request->user->setMessage(__('The archive has been uploaded and processed.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Download::index', + array($prj->shortname)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_UploadArchive(null, + array('project' => $prj, + 'user' => $request->user)); + } + return Pluf_Shortcuts_RenderToResponse('idf/downloads/createFromArchive.html', + array( + 'page_title' => $title, + 'form' => $form, + ), + $request); + } + /** * Create the autocomplete arrays for the little AJAX stuff. */ diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php index 11c08f4..e83fc93 100644 --- a/src/IDF/Views/Project.php +++ b/src/IDF/Views/Project.php @@ -68,7 +68,7 @@ class IDF_Views_Project $pages = array(); if ($request->rights['hasWikiAccess']) { $tags = IDF_Views_Wiki::getWikiTags($prj); - $pages = $tags[0]->get_idf_wikipage_list(); + $pages = $tags[0]->get_idf_wiki_page_list(); } return Pluf_Shortcuts_RenderToResponse('idf/project/home.html', array( @@ -131,8 +131,10 @@ class IDF_Views_Project } if (true === IDF_Precondition::accessWiki($request) && ($model_filter == 'all' || $model_filter == 'documents')) { - $classes[] = '\'IDF_WikiPage\''; - $classes[] = '\'IDF_WikiRevision\''; + $classes[] = '\'IDF_Wiki_Page\''; + $classes[] = '\'IDF_Wiki_PageRevision\''; + $classes[] = '\'IDF_Wiki_Resource\''; + $classes[] = '\'IDF_Wiki_ResourceRevision\''; } if (true === IDF_Precondition::accessReview($request) && ($model_filter == 'all' || $model_filter == 'reviews')) { @@ -305,17 +307,21 @@ class IDF_Views_Project return new Pluf_HTTP_Response_Redirect($url); } } else { - $form = new IDF_Form_ProjectConf($prj->getData(), $extra); + $form = new IDF_Form_ProjectConf(null, $extra); } $logo = $prj->getConf()->getVal('logo'); + $arrays = self::autoCompleteArrays(); return Pluf_Shortcuts_RenderToResponse('idf/admin/summary.html', - array( - 'page_title' => $title, - 'form' => $form, - 'project' => $prj, - 'logo' => $logo, - ), + array_merge( + array( + 'page_title' => $title, + 'form' => $form, + 'project' => $prj, + 'logo' => $logo, + ), + $arrays + ), $request); } @@ -375,8 +381,11 @@ class IDF_Views_Project $title = sprintf(__('%s Downloads Configuration'), (string) $prj); $conf = new IDF_Conf(); $conf->setProject($prj); + $extra = array( + 'conf' => $conf, + ); if ($request->method == 'POST') { - $form = new IDF_Form_UploadConf($request->POST); + $form = new IDF_Form_UploadConf($request->POST, $extra); if ($form->isValid()) { foreach ($form->cleaned_data as $key=>$val) { $conf->setVal($key, $val); @@ -388,7 +397,7 @@ class IDF_Views_Project } } else { $params = array(); - $keys = array('labels_download_predefined', 'labels_download_one_max'); + $keys = array('labels_download_predefined', 'labels_download_one_max', 'upload_webhook_url'); foreach ($keys as $key) { $_val = $conf->getVal($key, false); if ($_val !== false) { @@ -398,12 +407,13 @@ class IDF_Views_Project if (count($params) == 0) { $params = null; //Nothing in the db, so new form. } - $form = new IDF_Form_UploadConf($params); + $form = new IDF_Form_UploadConf($params, $extra); } return Pluf_Shortcuts_RenderToResponse('idf/admin/downloads.html', array( 'page_title' => $title, 'form' => $form, + 'hookkey' => $prj->getWebHookKey(), ), $request); } @@ -504,21 +514,24 @@ class IDF_Views_Project } } $form->save(); // Save the authorized users. - $request->user->setMessage(__('The project tabs access rights have been saved.')); + $request->user->setMessage(__('The project tabs access rights and notification settings have been saved.')); $url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::adminTabs', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } } else { $params = array(); - $keys = array('downloads_access_rights', 'source_access_rights', - 'issues_access_rights', 'review_access_rights', - 'wiki_access_rights', - 'downloads_notification_email', - 'review_notification_email', - 'wiki_notification_email', - 'source_notification_email', - 'issues_notification_email'); + $sections = array('downloads', 'wiki', 'source', 'issues', 'review'); + $keys = array(); + + foreach ($sections as $section) { + $keys[] = $section.'_access_rights'; + $keys[] = $section.'_notification_owners_enabled'; + $keys[] = $section.'_notification_members_enabled'; + $keys[] = $section.'_notification_email_enabled'; + $keys[] = $section.'_notification_email'; + } + foreach ($keys as $key) { $_val = $request->conf->getVal($key, false); if ($_val !== false) { @@ -590,6 +603,10 @@ class IDF_Views_Project 'mtn' => __('monotone'), ); $repository_type = $options[$scm]; + $hook_request_method = 'PUT'; + if (Pluf::f('webhook_processing','') === 'compat') { + $hook_request_method = 'POST'; + } return Pluf_Shortcuts_RenderToResponse('idf/admin/source.html', array( 'remote_svn' => $remote_svn, @@ -598,8 +615,41 @@ class IDF_Views_Project 'repository_size' => $prj->getRepositorySize(), 'page_title' => $title, 'form' => $form, - 'hookkey' => $prj->getPostCommitHookKey(), + 'hookkey' => $prj->getWebHookKey(), + 'hook_request_method' => $hook_request_method, ), $request); } + + /** + * Create the autocomplete arrays for the little AJAX stuff. + */ + public static function autoCompleteArrays() + { + $forge = IDF_Forge::instance(); + $labels = $forge->getProjectLabels(IDF_Form_Admin_LabelConf::init_project_labels); + + $auto = array('auto_labels' => ''); + $auto_raw = array('auto_labels' => $labels); + 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 = explode('=', $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, -2); + } + + return $auto; + } } diff --git a/src/IDF/Views/Source.php b/src/IDF/Views/Source.php index 38913f0..225d74a 100644 --- a/src/IDF/Views/Source.php +++ b/src/IDF/Views/Source.php @@ -132,6 +132,12 @@ class IDF_Views_Source $request); } + public function repository($request, $match) + { + $scm = IDF_Scm::get($request->project); + return $scm->repository($request, $match); + } + public $treeBase_precond = array('IDF_Precondition::accessSource', 'IDF_Views_Source_Precondition::scmAvailable', 'IDF_Views_Source_Precondition::revisionValid'); diff --git a/src/IDF/Views/Wiki.php b/src/IDF/Views/Wiki.php index 972d2c2..8b6d7df 100644 --- a/src/IDF/Views/Wiki.php +++ b/src/IDF/Views/Wiki.php @@ -32,22 +32,22 @@ Pluf::loadFunction('Pluf_Shortcuts_GetFormForModel'); class IDF_Views_Wiki { /** - * View list of issues for a given project. + * View list of pages for a given project. */ - public $index_precond = array('IDF_Precondition::accessWiki'); - public function index($request, $match, $api=false) + public $listPages_precond = array('IDF_Precondition::accessWiki'); + public function listPages($request, $match, $api=false) { $prj = $request->project; $title = sprintf(__('%s Documentation'), (string) $prj); // Paginator to paginate the pages - $pag = new Pluf_Paginator(new IDF_WikiPage()); + $pag = new Pluf_Paginator(new IDF_Wiki_Page()); $pag->class = 'recent-issues'; $pag->item_extra_props = array('project_m' => $prj, 'shortname' => $prj->shortname, 'current_user' => $request->user); $pag->summary = __('This table shows the documentation pages.'); - $pag->action = array('IDF_Views_Wiki::index', array($prj->shortname)); - $pag->edit_action = array('IDF_Views_Wiki::view', 'shortname', 'title'); + $pag->action = array('IDF_Views_Wiki::listPages', array($prj->shortname)); + $pag->edit_action = array('IDF_Views_Wiki::viewPage', 'shortname', 'title'); $sql = 'project=%s'; $ptags = self::getWikiTags($prj); $dtag = array_pop($ptags); // The last tag is the deprecated tag. @@ -67,7 +67,7 @@ class IDF_Views_Wiki $pag->no_results_text = __('No documentation pages were found.'); $pag->sort_order = array('title', 'ASC'); $pag->setFromRequest($request); - return Pluf_Shortcuts_RenderToResponse('idf/wiki/index.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/listPages.html', array( 'page_title' => $title, 'pages' => $pag, @@ -77,18 +77,56 @@ class IDF_Views_Wiki $request); } + /** + * View list of resources for a given project. + */ + public $listResources_precond = array('IDF_Precondition::accessWiki', + 'Pluf_Precondition::loginRequired'); + public function listResources($request, $match) + { + $prj = $request->project; + $title = sprintf(__('%s Documentation Resources'), (string) $prj); + $pag = new Pluf_Paginator(new IDF_Wiki_Resource()); + $pag->class = 'recent-issues'; + $pag->item_extra_props = array('project_m' => $prj, + 'shortname' => $prj->shortname, + 'current_user' => $request->user); + $pag->summary = __('This table shows the resources that can be used on documentation pages.'); + $pag->action = array('IDF_Views_Wiki::listResources', array($prj->shortname)); + $pag->edit_action = array('IDF_Views_Wiki::viewResource', 'shortname', 'title'); + $pag->forced_where = new Pluf_SQL('project=%s', array($prj->id)); + $pag->extra_classes = array('right', 'a-c', 'left', 'a-c'); + $list_display = array( + 'title' => __('Resource Title'), + 'mime_type' => __('MIME type'), + 'summary' => __('Description'), + array('modif_dtime', 'Pluf_Paginator_DateYMD', __('Updated')), + ); + $pag->configure($list_display, array(), array('title', 'modif_dtime')); + $pag->items_per_page = 25; + $pag->no_results_text = __('No resources were found.'); + $pag->sort_order = array('title', 'ASC'); + $pag->setFromRequest($request); + return Pluf_Shortcuts_RenderToResponse('idf/wiki/listResources.html', + array( + 'page_title' => $title, + 'resources' => $pag, + ), + $request); + } + public $search_precond = array('IDF_Precondition::accessWiki',); public function search($request, $match) { $prj = $request->project; if (!isset($request->REQUEST['q']) or trim($request->REQUEST['q']) == '') { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::index', + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listPages', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } $q = $request->REQUEST['q']; $title = sprintf(__('Documentation Search - %s'), $q); - $pages = new Pluf_Search_ResultSet(IDF_Search::mySearch($q, $prj, 'IDF_WikiPage')); + $pages = new Pluf_Search_ResultSet(IDF_Search::mySearch($q, $prj, 'IDF_Wiki_Page')); if (count($pages) > 100) { $pages->results = array_slice($pages->results, 0, 100); } @@ -100,7 +138,7 @@ class IDF_Views_Wiki 'current_user' => $request->user); $pag->summary = __('This table shows the pages found.'); $pag->action = array('IDF_Views_Wiki::search', array($prj->shortname), array('q'=> $q)); - $pag->edit_action = array('IDF_Views_Wiki::view', 'shortname', 'title'); + $pag->edit_action = array('IDF_Views_Wiki::viewPage', 'shortname', 'title'); $pag->extra_classes = array('right', '', 'a-c'); $list_display = array( 'title' => __('Page Title'), @@ -122,8 +160,8 @@ class IDF_Views_Wiki /** * View list of pages with a given label. */ - public $listLabel_precond = array('IDF_Precondition::accessWiki'); - public function listLabel($request, $match) + public $listPagesWithLabel_precond = array('IDF_Precondition::accessWiki'); + public function listPagesWithLabel($request, $match) { $prj = $request->project; $tag = Pluf_Shortcuts_GetObjectOr404('IDF_Tag', $match[2]); @@ -133,15 +171,15 @@ class IDF_Views_Wiki // Paginator to paginate the pages $ptags = self::getWikiTags($prj); $dtag = array_pop($ptags); // The last tag is the deprecated tag. - $pag = new Pluf_Paginator(new IDF_WikiPage()); + $pag = new Pluf_Paginator(new IDF_Wiki_Page()); $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 documentation pages with label %s.'), (string) $tag); $pag->forced_where = new Pluf_SQL('project=%s AND idf_tag_id=%s', array($prj->id, $tag->id)); - $pag->action = array('IDF_Views_Wiki::listLabel', array($prj->shortname, $tag->id)); - $pag->edit_action = array('IDF_Views_Wiki::view', 'shortname', 'title'); + $pag->action = array('IDF_Views_Wiki::listPagesWithLabel', array($prj->shortname, $tag->id)); + $pag->edit_action = array('IDF_Views_Wiki::viewPage', 'shortname', 'title'); $pag->extra_classes = array('right', '', 'a-c'); $list_display = array( 'title' => __('Page Title'), @@ -152,7 +190,7 @@ class IDF_Views_Wiki $pag->items_per_page = 25; $pag->no_results_text = __('No documentation pages were found.'); $pag->setFromRequest($request); - return Pluf_Shortcuts_RenderToResponse('idf/wiki/index.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/listPages.html', array( 'page_title' => $title, 'label' => $tag, @@ -165,24 +203,25 @@ class IDF_Views_Wiki /** * Create a new documentation page. */ - public $create_precond = array('IDF_Precondition::accessWiki', - 'Pluf_Precondition::loginRequired'); - public function create($request, $match) + public $createPage_precond = array('IDF_Precondition::accessWiki', + 'Pluf_Precondition::loginRequired'); + public function createPage($request, $match) { $prj = $request->project; $title = __('New Page'); $preview = false; if ($request->method == 'POST') { - $form = new IDF_Form_WikiCreate($request->POST, + $form = new IDF_Form_WikiPageCreate($request->POST, array('project' => $prj, 'user' => $request->user )); if ($form->isValid() and !isset($request->POST['preview'])) { $page = $form->save(); - $urlpage = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', + $urlpage = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', array($prj->shortname, $page->title)); - $request->user->setMessage(sprintf(__('The page %2$s has been created.'), $urlpage, Pluf_esc($page->title))); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::index', + $request->user->setMessage(sprintf(__('The page %2$s has been created.'), + $urlpage, Pluf_esc($page->title))); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listPages', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } elseif (isset($request->POST['preview'])) { @@ -191,12 +230,12 @@ class IDF_Views_Wiki } else { $pagename = (isset($request->GET['name'])) ? $request->GET['name'] : ''; - $form = new IDF_Form_WikiCreate(null, + $form = new IDF_Form_WikiPageCreate(null, array('name' => $pagename, 'project' => $prj, 'user' => $request->user)); } - return Pluf_Shortcuts_RenderToResponse('idf/wiki/create.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/createPage.html', array( 'auto_labels' => self::autoCompleteArrays($prj), 'page_title' => $title, @@ -206,27 +245,65 @@ class IDF_Views_Wiki $request); } + /** + * Create a new resource. + */ + public $createResource_precond = array('IDF_Precondition::accessWiki', + 'Pluf_Precondition::loginRequired'); + public function createResource($request, $match) + { + $prj = $request->project; + $title = __('New Resource'); + $preview = false; + if ($request->method == 'POST') { + $form = new IDF_Form_WikiResourceCreate(array_merge($request->POST, $request->FILES), + array('project' => $prj, 'user' => $request->user)); + if ($form->isValid()) { + $resource = $form->save(); + $urlresource = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($prj->shortname, $resource->title)); + $request->user->setMessage(sprintf(__('The resource %2$s has been created.'), $urlresource, Pluf_esc($resource->title))); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listResources', + array($prj->shortname)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $resourcename = (isset($request->GET['name'])) ? + $request->GET['name'] : ''; + $form = new IDF_Form_WikiResourceCreate(null, + array('name' => $resourcename, + 'project' => $prj, 'user' => $request->user)); + } + return Pluf_Shortcuts_RenderToResponse('idf/wiki/createResource.html', + array( + 'page_title' => $title, + 'form' => $form, + ), + $request); + } + /** * View a documentation page. */ - public $view_precond = array('IDF_Precondition::accessWiki'); - public function view($request, $match) + public $viewPage_precond = array('IDF_Precondition::accessWiki'); + public function viewPage($request, $match) { $prj = $request->project; // Find the page - $sql = new Pluf_SQL('project=%s AND title=%s', + $sql = new Pluf_SQL('project=%s AND title=%s', array($prj->id, $match[2])); - $pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen())); + $pages = Pluf::factory('IDF_Wiki_Page')->getList(array('filter'=>$sql->gen())); if ($pages->count() != 1) { return new Pluf_HTTP_Response_NotFound($request); } $page = $pages[0]; - $oldrev = false; + $revision = $page->get_current_revision(); + // We grab the old revision if requested. if (isset($request->GET['rev']) and preg_match('/^[0-9]+$/', $request->GET['rev'])) { - $oldrev = Pluf_Shortcuts_GetObjectOr404('IDF_WikiRevision', + $revision = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_PageRevision', $request->GET['rev']); - if ($oldrev->wikipage != $page->id or $oldrev->is_head == true) { + if ($revision->wikipage != $page->id) { return new Pluf_HTTP_Response_NotFound($request); } } @@ -235,15 +312,13 @@ class IDF_Views_Wiki $tags = $page->get_tags_list(); $dep = Pluf_Model_InArray($dtag, $tags); $title = $page->title; - $revision = $page->get_current_revision(); $false = Pluf_DB_BooleanToDb(false, $page->getDbConnection()); $revs = $page->get_revisions_list(array('order' => 'creation_dtime DESC', 'filter' => 'is_head='.$false)); - return Pluf_Shortcuts_RenderToResponse('idf/wiki/view.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/viewPage.html', array( 'page_title' => $title, 'page' => $page, - 'oldrev' => $oldrev, 'rev' => $revision, 'revs' => $revs, 'tags' => $tags, @@ -253,14 +328,77 @@ class IDF_Views_Wiki } /** - * Remove a revision of a page. + * View a documentation resource. */ - public $deleteRev_precond = array('IDF_Precondition::accessWiki', - 'IDF_Precondition::projectMemberOrOwner'); - public function deleteRev($request, $match) + public $viewResource_precond = array('IDF_Precondition::accessWiki'); + public function viewResource($request, $match) { $prj = $request->project; - $oldrev = Pluf_Shortcuts_GetObjectOr404('IDF_WikiRevision', $match[2]); + $sql = new Pluf_SQL('project=%s AND title=%s', + array($prj->id, $match[2])); + $resources = Pluf::factory('IDF_Wiki_Resource')->getList(array('filter'=>$sql->gen())); + if ($resources->count() != 1) { + return new Pluf_HTTP_Response_NotFound($request); + } + $resource = $resources[0]; + $revision = $resource->get_current_revision(); + + // grab the old revision if requested. + if (isset($request->GET['rev']) and preg_match('/^[0-9]+$/', $request->GET['rev'])) { + $revision = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_ResourceRevision', + $request->GET['rev']); + if ($revision->wikiresource != $resource->id) { + return new Pluf_HTTP_Response_NotFound($request); + } + } + $pagerevs = $revision->getPageRevisions(); + $title = $resource->title; + $false = Pluf_DB_BooleanToDb(false, $resource->getDbConnection()); + $revs = $resource->get_revisions_list(array('order' => 'creation_dtime DESC', + 'filter' => 'is_head='.$false)); + return Pluf_Shortcuts_RenderToResponse('idf/wiki/viewResource.html', + array( + 'page_title' => $title, + 'resource' => $resource, + 'rev' => $revision, + 'revs' => $revs, + 'pagerevs' => $pagerevs, + ), + $request); + } + + + /** + * Returns a bytestream to the given raw resource revision + */ + public $rawResource_precond = array('IDF_Precondition::accessWiki'); + public function rawResource($request, $match) + { + $prj = $request->project; + $rev = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_ResourceRevision', + $match[2]); + $res = $rev->get_wikiresource(); + if ($res->get_project()->id != $prj->id) { + return new Pluf_HTTP_Response_NotFound($request); + } + + $response = new Pluf_HTTP_Response_File($rev->getFilePath(), $res->mime_type); + if (isset($request->GET['attachment']) && $request->GET['attachment']) { + $response->headers['Content-Disposition'] = + 'attachment; filename="'.$res->title.'.'.$rev->fileext.'"'; + } + return $response; + } + + /** + * Remove a revision of a page. + */ + public $deletePageRev_precond = array('IDF_Precondition::accessWiki', + 'IDF_Precondition::projectMemberOrOwner'); + public function deletePageRev($request, $match) + { + $prj = $request->project; + $oldrev = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_PageRevision', $match[2]); $page = $oldrev->get_wikipage(); $prj->inOr404($page); if ($oldrev->is_head == true) { @@ -269,7 +407,7 @@ class IDF_Views_Wiki if ($request->method == 'POST') { $oldrev->delete(); $request->user->setMessage(__('The old revision has been deleted.')); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', array($prj->shortname, $page->title)); return new Pluf_HTTP_Response_Redirect($url); } @@ -279,7 +417,7 @@ class IDF_Views_Wiki $false = Pluf_DB_BooleanToDb(false, $page->getDbConnection()); $revs = $page->get_revisions_list(array('order' => 'creation_dtime DESC', 'filter' => 'is_head='.$false)); - return Pluf_Shortcuts_RenderToResponse('idf/wiki/delete.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/deletePageRev.html', array( 'page_title' => $title, 'page' => $page, @@ -292,17 +430,53 @@ class IDF_Views_Wiki } /** - * View a documentation page. + * Remove a revision of a resource. */ - public $update_precond = array('IDF_Precondition::accessWiki', - 'Pluf_Precondition::loginRequired'); - public function update($request, $match) + public $deleteResourceRev_precond = array('IDF_Precondition::accessWiki', + 'IDF_Precondition::projectMemberOrOwner'); + public function deleteResourceRev($request, $match) + { + $prj = $request->project; + $oldrev = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_ResourceRevision', $match[2]); + $resource = $oldrev->get_wikiresource(); + $prj->inOr404($resource); + if ($oldrev->is_head == true) { + return new Pluf_HTTP_Response_NotFound($request); + } + if ($request->method == 'POST') { + $oldrev->delete(); + $request->user->setMessage(__('The old revision has been deleted.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($prj->shortname, $resource->title)); + return new Pluf_HTTP_Response_Redirect($url); + } + + $title = sprintf(__('Delete Old Revision of %s'), $resource->title); + $false = Pluf_DB_BooleanToDb(false, $resource->getDbConnection()); + $revs = $resource->get_revisions_list(array('order' => 'creation_dtime DESC', + 'filter' => 'is_head='.$false)); + return Pluf_Shortcuts_RenderToResponse('idf/wiki/deleteResourceRev.html', + array( + 'page_title' => $title, + 'resource' => $resource, + 'oldrev' => $oldrev, + 'revs' => $revs, + ), + $request); + } + + /** + * Update a documentation page. + */ + public $updatePage_precond = array('IDF_Precondition::accessWiki', + 'Pluf_Precondition::loginRequired'); + public function updatePage($request, $match) { $prj = $request->project; // Find the page - $sql = new Pluf_SQL('project=%s AND title=%s', + $sql = new Pluf_SQL('project=%s AND title=%s', array($prj->id, $match[2])); - $pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen())); + $pages = Pluf::factory('IDF_Wiki_Page')->getList(array('filter'=>$sql->gen())); if ($pages->count() != 1) { return new Pluf_HTTP_Response_NotFound($request); } @@ -314,23 +488,24 @@ class IDF_Views_Wiki 'user' => $request->user, 'page' => $page); if ($request->method == 'POST') { - $form = new IDF_Form_WikiUpdate($request->POST, $params); + $form = new IDF_Form_WikiPageUpdate($request->POST, $params); if ($form->isValid() and !isset($request->POST['preview'])) { $page = $form->save(); - $urlpage = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', + $urlpage = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', array($prj->shortname, $page->title)); - $request->user->setMessage(sprintf(__('The page %2$s has been updated.'), $urlpage, Pluf_esc($page->title))); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::index', + $request->user->setMessage(sprintf(__('The page %2$s has been updated.'), + $urlpage, Pluf_esc($page->title))); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listPages', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } elseif (isset($request->POST['preview'])) { $preview = $request->POST['content']; } } else { - - $form = new IDF_Form_WikiUpdate(null, $params); + + $form = new IDF_Form_WikiPageUpdate(null, $params); } - return Pluf_Shortcuts_RenderToResponse('idf/wiki/update.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/updatePage.html', array( 'auto_labels' => self::autoCompleteArrays($prj), 'page_title' => $title, @@ -343,34 +518,82 @@ class IDF_Views_Wiki } /** - * Delete a Wiki page. + * Update a documentation resource. */ - public $delete_precond = array('IDF_Precondition::accessWiki', - 'IDF_Precondition::projectMemberOrOwner'); - public function delete($request, $match) + public $updateResource_precond = array('IDF_Precondition::accessWiki', + 'Pluf_Precondition::loginRequired'); + public function updateResource($request, $match) { $prj = $request->project; - $page = Pluf_Shortcuts_GetObjectOr404('IDF_WikiPage', $match[2]); - $prj->inOr404($page); - $params = array('page' => $page); + // Find the page + $sql = new Pluf_SQL('project=%s AND title=%s', + array($prj->id, $match[2])); + $resources = Pluf::factory('IDF_Wiki_Resource')->getList(array('filter'=>$sql->gen())); + if ($resources->count() != 1) { + return new Pluf_HTTP_Response_NotFound($request); + } + $resource = $resources[0]; + $title = sprintf(__('Update %s'), $resource->title); + $revision = $resource->get_current_revision(); + $params = array('project' => $prj, + 'user' => $request->user, + 'resource' => $resource); if ($request->method == 'POST') { - $form = new IDF_Form_WikiDelete($request->POST, $params); + $form = new IDF_Form_WikiResourceUpdate(array_merge($request->POST, $request->FILES), + $params); if ($form->isValid()) { - $form->save(); - $request->user->setMessage(__('The documentation page has been deleted.')); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::index', + $page = $form->save(); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($prj->shortname, $resource->title)); + $request->user->setMessage(sprintf(__('The resource %2$s has been updated.'), + $url, Pluf_esc($resource->title))); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listResources', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } } else { - $form = new IDF_Form_WikiDelete(null, $params); + $form = new IDF_Form_WikiResourceUpdate(null, $params); + } + + return Pluf_Shortcuts_RenderToResponse('idf/wiki/updateResource.html', + array( + 'page_title' => $title, + 'resource' => $resource, + 'rev' => $revision, + 'form' => $form, + ), + $request); + } + + /** + * Delete a Wiki page. + */ + public $deletePage_precond = array('IDF_Precondition::accessWiki', + 'IDF_Precondition::projectMemberOrOwner'); + public function deletePage($request, $match) + { + $prj = $request->project; + $page = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_Page', $match[2]); + $prj->inOr404($page); + $params = array('page' => $page); + if ($request->method == 'POST') { + $form = new IDF_Form_WikiPageDelete($request->POST, $params); + if ($form->isValid()) { + $form->save(); + $request->user->setMessage(__('The documentation page has been deleted.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listPages', + array($prj->shortname)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_WikiPageDelete(null, $params); } $title = sprintf(__('Delete Page %s'), $page->title); $revision = $page->get_current_revision(); $false = Pluf_DB_BooleanToDb(false, $page->getDbConnection()); $revs = $page->get_revisions_list(array('order' => 'creation_dtime DESC', 'filter' => 'is_head='.$false)); - return Pluf_Shortcuts_RenderToResponse('idf/wiki/deletepage.html', + return Pluf_Shortcuts_RenderToResponse('idf/wiki/deletePage.html', array( 'page_title' => $title, 'page' => $page, @@ -382,6 +605,45 @@ class IDF_Views_Wiki $request); } + /** + * Delete a Wiki resource. + */ + public $deleteResource_precond = array('IDF_Precondition::accessWiki', + 'IDF_Precondition::projectMemberOrOwner'); + public function deleteResource($request, $match) + { + $prj = $request->project; + $resource = Pluf_Shortcuts_GetObjectOr404('IDF_Wiki_Resource', $match[2]); + $prj->inOr404($resource); + $params = array('resource' => $resource); + if ($request->method == 'POST') { + $form = new IDF_Form_WikiResourceDelete($request->POST, $params); + if ($form->isValid()) { + $form->save(); + $request->user->setMessage(__('The documentation resource has been deleted.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::listResources', + array($prj->shortname)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_WikiResourceDelete(null, $params); + } + $title = sprintf(__('Delete Resource %s'), $resource->title); + $revision = $resource->get_current_revision(); + $false = Pluf_DB_BooleanToDb(false, $resource->getDbConnection()); + $revs = $resource->get_revisions_list(array('order' => 'creation_dtime DESC', + 'filter' => 'is_head='.$false)); + return Pluf_Shortcuts_RenderToResponse('idf/wiki/deleteResource.html', + array( + 'page_title' => $title, + 'resource' => $resource, + 'form' => $form, + 'rev' => $revision, + 'revs' => $revs, + ), + $request); + } + /** * Get the wiki tags. * @@ -411,7 +673,7 @@ class IDF_Views_Wiki $sql = new Pluf_SQL('project=%s AND idf_tag_id=%s', array($project->id, $dtag->id)); $ids = array(); - foreach (Pluf::factory('IDF_WikiPage')->getList(array('filter' => $sql->gen(), 'view' => 'join_tags')) + foreach (Pluf::factory('IDF_Wiki_Page')->getList(array('filter' => $sql->gen(), 'view' => 'join_tags')) as $file) { $ids[] = (int) $file->id; } @@ -425,7 +687,7 @@ class IDF_Views_Wiki { $conf = new IDF_Conf(); $conf->setProject($project); - $st = preg_split("/\015\012|\015|\012/", + $st = preg_split("/\015\012|\015|\012/", $conf->getVal('labels_wiki_predefined', IDF_Form_WikiConf::init_predefined), -1, PREG_SPLIT_NO_EMPTY); $auto = ''; foreach ($st as $s) { diff --git a/src/IDF/Webhook.php b/src/IDF/Webhook.php index 34a5cda..9d33d09 100644 --- a/src/IDF/Webhook.php +++ b/src/IDF/Webhook.php @@ -31,22 +31,28 @@ class IDF_Webhook { /** - * Perform the POST request given the webhook payload. + * Perform the request given the webhook payload. * * @param array Payload * @return bool Success or error */ - public static function postNotification($payload) + public static function processNotification($payload) { $data = json_encode($payload['to_send']); + $sign_header = 'Web-Hook-Hmac'; + // use the old signature header if we're asked for + if (Pluf::f('webhook_processing', '') === 'compat') { + $sign_header = 'Post-Commit-Hook-Hmac'; + } $sign = hash_hmac('md5', $data, $payload['authkey']); $params = array('http' => array( - 'method' => 'POST', + // fall-back to POST for old queue items + 'method' => empty($payload['method']) ? 'POST' : $payload['method'], 'content' => $data, 'user_agent' => 'Indefero Hook Sender (http://www.indefero.net)', - 'max_redirects' => 0, + 'max_redirects' => 0, 'timeout' => 15, - 'header'=> 'Post-Commit-Hook-Hmac: '.$sign."\r\n" + 'header'=> $sign_header.': '.$sign."\r\n" .'Content-Type: application/json'."\r\n", ) ); @@ -61,7 +67,7 @@ class IDF_Webhook if (!isset($meta['wrapper_data'][0]) or $meta['timed_out']) { return false; } - if (0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 2') or + if (0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 2') or 0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 3')) { return true; } @@ -76,11 +82,11 @@ class IDF_Webhook public static function process($sender, &$params) { $item = $params['item']; - if ($item->type != 'new_commit') { + if (!in_array($item->type, array('new_commit', 'upload'))) { // We do nothing. return; } - if (isset($params['res']['IDF_Webhook::process']) and + if (isset($params['res']['IDF_Webhook::process']) and $params['res']['IDF_Webhook::process'] == true) { // Already processed. return; @@ -90,7 +96,7 @@ class IDF_Webhook return; } // We have either to retry or to push for the first time. - $res = self::postNotification($item->payload); + $res = self::processNotification($item->payload); if ($res) { $params['res']['IDF_Webhook::process'] = true; } elseif ($item->trials >= 9) { diff --git a/src/IDF/WikiPage.php b/src/IDF/Wiki/Page.php similarity index 91% rename from src/IDF/WikiPage.php rename to src/IDF/Wiki/Page.php index de1a318..0c4a35c 100644 --- a/src/IDF/WikiPage.php +++ b/src/IDF/Wiki/Page.php @@ -28,10 +28,10 @@ Pluf::loadFunction('Pluf_Template_dateAgo'); * Base definition of a wiki page. * * A wiki page can have tags and be starred by the users. The real - * content of the page is stored in the IDF_WikiRevision + * content of the page is stored in the IDF_Wiki_PageRevision * object. Several revisions are associated to a given page. */ -class IDF_WikiPage extends Pluf_Model +class IDF_Wiki_Page extends Pluf_Model { public $_model = __CLASS__; @@ -44,9 +44,9 @@ class IDF_WikiPage extends Pluf_Model 'id' => array( 'type' => 'Pluf_DB_Field_Sequence', - 'blank' => true, + 'blank' => true, ), - 'project' => + 'project' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'IDF_Project', @@ -70,7 +70,7 @@ class IDF_WikiPage extends Pluf_Model 'verbose' => __('summary'), 'help_text' => __('A one line description of the page content.'), ), - 'submitter' => + 'submitter' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'Pluf_User', @@ -78,7 +78,7 @@ class IDF_WikiPage extends Pluf_Model 'verbose' => __('submitter'), 'relate_name' => 'submitted_wikipages', ), - 'interested' => + 'interested' => array( 'type' => 'Pluf_DB_Field_Manytomany', 'model' => 'Pluf_User', @@ -88,7 +88,7 @@ class IDF_WikiPage extends Pluf_Model ), 'tags' => array( - 'type' => 'Pluf_DB_Field_Manytomany', + 'type' => 'Pluf_DB_Field_Manytomany', 'blank' => true, 'model' => 'IDF_Tag', 'verbose' => __('labels'), @@ -106,19 +106,19 @@ class IDF_WikiPage extends Pluf_Model 'verbose' => __('modification date'), ), ); - $this->_a['idx'] = array( + $this->_a['idx'] = array( 'modif_dtime_idx' => array( 'col' => 'modif_dtime', 'type' => 'normal', ), ); - $table = $this->_con->pfx.'idf_tag_idf_wikipage_assoc'; + $table = $this->_con->pfx.'idf_tag_idf_wiki_page_assoc'; $this->_a['views'] = array( - 'join_tags' => + 'join_tags' => array( 'join' => 'LEFT JOIN '.$table - .' ON idf_wikipage_id=id', + .' ON idf_wiki_page_id=id', ), ); } @@ -144,7 +144,7 @@ class IDF_WikiPage extends Pluf_Model IDF_Search::remove($this); } - function get_current_revision() + function get_current_revision() { $true = Pluf_DB_BooleanToDb(true, $this->getDbConnection()); $rev = $this->get_revisions_list(array('filter' => 'is_head='.$true, @@ -167,7 +167,7 @@ class IDF_WikiPage extends Pluf_Model // that the page as a given revision in the database when // doing the indexing. if ($create) { - IDF_Timeline::insert($this, $this->get_project(), + IDF_Timeline::insert($this, $this->get_project(), $this->get_submitter()); } } @@ -180,12 +180,12 @@ class IDF_WikiPage extends Pluf_Model * as such create links to other items etc. You can consider that * if displayed, you can create a link to it. * - * @param Pluf_HTTP_Request + * @param Pluf_HTTP_Request * @return Pluf_Template_SafeString */ public function timelineFragment($request) { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', array($request->project->shortname, $this->title)); $out = ''. @@ -195,14 +195,14 @@ class IDF_WikiPage extends Pluf_Model $user = $stag->start($this->get_submitter(), $request, '', false); $out .= sprintf(__('%2$s, %3$s'), $url, Pluf_esc($this->title), Pluf_esc($this->summary)).''; $out .= "\n".' -
'.sprintf(__('Creation of page %2$s, by %3$s'), $url, Pluf_esc($this->title), $user).'
'; +
'.sprintf(__('Creation of page %2$s, by %3$s'), $url, Pluf_esc($this->title), $user).'
'; return Pluf_Template::markSafe($out); } public function feedFragment($request) { $url = Pluf::f('url_base') - .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', + .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', array($request->project->shortname, $this->title)); $title = sprintf(__('%1$s: Documentation page %2$s added - %3$s'), @@ -218,7 +218,7 @@ class IDF_WikiPage extends Pluf_Model 'create' => true, 'date' => $date) ); - $tmpl = new Pluf_Template('idf/wiki/feedfragment.xml'); + $tmpl = new Pluf_Template('idf/wiki/feedfragment-page.xml'); return $tmpl->render($context); } } diff --git a/src/IDF/WikiRevision.php b/src/IDF/Wiki/PageRevision.php similarity index 68% rename from src/IDF/WikiRevision.php rename to src/IDF/Wiki/PageRevision.php index d9140c5..12e9695 100644 --- a/src/IDF/WikiRevision.php +++ b/src/IDF/Wiki/PageRevision.php @@ -25,13 +25,13 @@ * A revision of a wiki page. * */ -class IDF_WikiRevision extends Pluf_Model +class IDF_Wiki_PageRevision extends Pluf_Model { public $_model = __CLASS__; function init() { - $this->_a['table'] = 'idf_wikirevisions'; + $this->_a['table'] = 'idf_wikipagerevs'; $this->_a['model'] = __CLASS__; $this->_a['cols'] = array( // It is mandatory to have an "id" column. @@ -43,7 +43,7 @@ class IDF_WikiRevision extends Pluf_Model 'wikipage' => array( 'type' => 'Pluf_DB_Field_Foreignkey', - 'model' => 'IDF_WikiPage', + 'model' => 'IDF_Wiki_Page', 'blank' => false, 'verbose' => __('page'), 'relate_name' => 'revisions', @@ -83,7 +83,7 @@ class IDF_WikiRevision extends Pluf_Model 'type' => 'Pluf_DB_Field_Serialized', 'blank' => true, 'verbose' => __('changes'), - 'help_text' => 'Serialized array of the changes in the issue.', + 'help_text' => 'Serialized array of the changes in the page.', ), 'creation_dtime' => array( @@ -99,6 +99,14 @@ class IDF_WikiRevision extends Pluf_Model 'type' => 'normal', ), ); + $table = $this->_con->pfx.'idf_wiki_pagerevision_idf_wiki_resourcerevision_assoc'; + $this->_a['views'] = array( + 'join_pagerevision' => + array( + 'join' => 'LEFT JOIN '.$table + .' ON idf_wiki_pagerevision_id=id', + ), + ); } function changedRevision() @@ -129,6 +137,8 @@ class IDF_WikiRevision extends Pluf_Model function postSave($create=false) { + $page = $this->get_wikipage(); + if ($create) { // Check if more than one revision for this page. We do // not want to insert the first revision in the timeline @@ -136,10 +146,9 @@ class IDF_WikiRevision extends Pluf_Model // update as update is performed to change the is_head // flag. $sql = new Pluf_SQL('wikipage=%s', array($this->wikipage)); - $rev = Pluf::factory('IDF_WikiRevision')->getList(array('filter'=>$sql->gen())); + $rev = Pluf::factory('IDF_Wiki_PageRevision')->getList(array('filter'=>$sql->gen())); if ($rev->count() > 1) { - IDF_Timeline::insert($this, $this->get_wikipage()->get_project(), - $this->get_submitter()); + IDF_Timeline::insert($this, $page->get_project(), $this->get_submitter()); foreach ($rev as $r) { if ($r->id != $this->id and $r->is_head) { $r->is_head = false; @@ -147,18 +156,38 @@ class IDF_WikiRevision extends Pluf_Model } } } - $page = $this->get_wikipage(); - $page->update(); // Will update the modification timestamp. - IDF_Search::index($page); + } + + IDF_Search::index($page); + $page->update(); // Will update the modification timestamp. + + // remember the resource revisions used in this page revision + if ($this->is_head) { + preg_match_all('#\[\[!([A-Za-z0-9\-]+)[^\]]*\]\]#im', $this->content, $matches, PREG_PATTERN_ORDER); + if (count($matches) > 1 && count($matches[1]) > 0) { + foreach ($matches[1] as $resourceName) { + $sql = new Pluf_SQL('project=%s AND title=%s', + array($prj->id, $resourceName)); + $resources = Pluf::factory('IDF_Wiki_Resource')->getList(array('filter'=>$sql->gen())); + if ($resources->count() == 0) + continue; + + $current_revision = $resources[0]->get_current_revision(); + $current_revision->setAssoc($this); + $this->setAssoc($current_revision); + } + } } } public function timelineFragment($request) { $page = $this->get_wikipage(); - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', - array($request->project->shortname, - $page->title)); + $url = Pluf::f('url_base') + .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', + array($request->project->shortname, + $page->title), + array('rev' => $this->id)); $out = "\n".''. Pluf_esc(Pluf_Template_dateAgo($this->creation_dtime, 'without')). ''; @@ -193,18 +222,12 @@ class IDF_WikiRevision extends Pluf_Model public function feedFragment($request) { $page = $this->get_wikipage(); - if (!$this->is_head) { - $url = Pluf::f('url_base') - .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', - array($request->project->shortname, - $page->title), - array('rev' => $this->id)); - } else { - $url = Pluf::f('url_base') - .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view', - array($request->project->shortname, - $page->title)); - } + $url = Pluf::f('url_base') + .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewPage', + array($request->project->shortname, + $page->title), + array('rev' => $this->id)); + $title = sprintf(__('%1$s: Documentation page %2$s updated - %3$s'), $request->project->name, $page->title, $page->summary); @@ -218,14 +241,14 @@ class IDF_WikiRevision extends Pluf_Model 'create' => false, 'date' => $date) ); - $tmpl = new Pluf_Template('idf/wiki/feedfragment.xml'); + $tmpl = new Pluf_Template('idf/wiki/feedfragment-page.xml'); return $tmpl->render($context); } /** - * Notification of change of a WikiPage. + * Notification of change of a Wiki Page. * * The content of a WikiPage is in the IDF_WikiRevision object, * this is why we send the notificatin from there. This means that @@ -243,42 +266,49 @@ class IDF_WikiRevision extends Pluf_Model */ public function notify($conf, $create=true) { - if ('' == $conf->getVal('wiki_notification_email', '')) { - return; - } + $wikipage = $this->get_wikipage(); + $project = $wikipage->get_project(); $current_locale = Pluf_Translation::getLocale(); - $langs = Pluf::f('languages', array('en')); - Pluf_Translation::loadSetLocale($langs[0]); - $context = new Pluf_Template_Context( - array( - 'page' => $this->get_wikipage(), - 'rev' => $this, - 'project' => $this->get_wikipage()->get_project(), - 'url_base' => Pluf::f('url_base'), - ) - ); - if ($create) { - $template = 'idf/wiki/wiki-created-email.txt'; - $title = sprintf(__('New Documentation Page %1$s - %2$s (%3$s)'), - $this->get_wikipage()->title, - $this->get_wikipage()->summary, - $this->get_wikipage()->get_project()->shortname); - } else { - $template = 'idf/wiki/wiki-updated-email.txt'; - $title = sprintf(__('Documentation Page Changed %1$s - %2$s (%3$s)'), - $this->get_wikipage()->title, - $this->get_wikipage()->summary, - $this->get_wikipage()->get_project()->shortname); - } - $tmpl = new Pluf_Template($template); - $text_email = $tmpl->render($context); - $addresses = explode(',', $conf->getVal('wiki_notification_email')); - foreach ($addresses as $address) { - $email = new Pluf_Mail(Pluf::f('from_email'), + $from_email = Pluf::f('from_email'); + $messageId = '<'.md5('wiki'.$wikipage->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>'; + $recipients = $project->getNotificationRecipientsForTab('wiki'); + + foreach ($recipients as $address => $language) { + + if ($this->get_submitter()->email === $address) { + continue; + } + + Pluf_Translation::loadSetLocale($language); + + $context = new Pluf_Template_Context(array( + 'page' => $wikipage, + 'rev' => $this, + 'project' => $project, + 'url_base' => Pluf::f('url_base'), + )); + + $tplfile = 'idf/wiki/wiki-created-email.txt'; + $subject = __('New Documentation Page %1$s - %2$s (%3$s)'); + $headers = array('Message-ID' => $messageId); + if (!$create) { + $tplfile = 'idf/wiki/wiki-updated-email.txt'; + $subject = __('Documentation Page Changed %1$s - %2$s (%3$s)'); + $headers = array('References' => $messageId); + } + + $tmpl = new Pluf_Template($tplfile); + $text_email = $tmpl->render($context); + + $email = new Pluf_Mail($from_email, $address, - $title); + sprintf($subject, + $wikipage->title, + $wikipage->summary, + $project->shortname)); $email->addTextMessage($text_email); + $email->addHeaders($headers); $email->sendMail(); } diff --git a/src/IDF/Wiki/Resource.php b/src/IDF/Wiki/Resource.php new file mode 100644 index 0000000..487cf1a --- /dev/null +++ b/src/IDF/Wiki/Resource.php @@ -0,0 +1,204 @@ +_a['table'] = 'idf_wikiresources'; + $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' => 'wikipages', + ), + 'title' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 250, + 'verbose' => __('title'), + 'help_text' => __('The title of the resource must only contain letters, digits, dots or the dash character. For example: my-resource.png.'), + ), + 'mime_type' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 100, + 'verbose' => __('MIME media type'), + 'help_text' => __('The MIME media type of the resource.'), + ), + 'summary' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 250, + 'verbose' => __('summary'), + 'help_text' => __('A one line description of the resource.'), + ), + 'submitter' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'Pluf_User', + 'blank' => false, + 'verbose' => __('submitter'), + 'relate_name' => 'submitted_wikipages', + ), + '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', + ), + ); + } + + function __toString() + { + return $this->title.' - '.$this->summary; + } + + /** + * We drop the information from the timeline. + */ + function preDelete() + { + IDF_Timeline::remove($this); + IDF_Search::remove($this); + } + + function get_current_revision() + { + $true = Pluf_DB_BooleanToDb(true, $this->getDbConnection()); + $rev = $this->get_revisions_list(array('filter' => 'is_head='.$true, + 'nb' => 1)); + return ($rev->count() == 1) ? $rev[0] : null; + } + + function preSave($create=false) + { + 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($create=false) + { + // Note: No indexing is performed here. The indexing is + // triggered in the postSave step of the revision to ensure + // that the page as a given revision in the database when + // doing the indexing. + if ($create) { + IDF_Timeline::insert($this, $this->get_project(), + $this->get_submitter()); + } + } + + /** + * Returns an HTML fragment used to display this resource in the + * timeline. + * + * The request object is given to be able to check the rights and + * as such create links to other items etc. You can consider that + * if displayed, you can create a link to it. + * + * @param Pluf_HTTP_Request + * @return Pluf_Template_SafeString + */ + public function timelineFragment($request) + { + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($request->project->shortname, + $this->title)); + $out = ''. + Pluf_esc(Pluf_Template_dateAgo($this->creation_dtime, 'without')). + ''; + $stag = new IDF_Template_ShowUser(); + $user = $stag->start($this->get_submitter(), $request, '', false); + $out .= sprintf(__('%2$s, %3$s'), $url, Pluf_esc($this->title), Pluf_esc($this->summary)).''; + $out .= "\n".' +
'.sprintf(__('Creation of resource %2$s, by %3$s'), $url, Pluf_esc($this->title), $user).'
'; + return Pluf_Template::markSafe($out); + } + + public function feedFragment($request) + { + $url = Pluf::f('url_base') + .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($request->project->shortname, + $this->title)); + $title = sprintf(__('%1$s: Documentation resource %2$s added - %3$s'), + $request->project->name, + $this->title, $this->summary); + $date = Pluf_Date::gmDateToGmString($this->creation_dtime); + $context = new Pluf_Template_Context_Request( + $request, + array('url' => $url, + 'title' => $title, + 'resource' => $this, + 'rev' => $this->get_current_revision(), + 'create' => true, + 'date' => $date) + ); + $tmpl = new Pluf_Template('idf/wiki/feedfragment-resource.xml'); + return $tmpl->render($context); + } +} diff --git a/src/IDF/Wiki/ResourceRevision.php b/src/IDF/Wiki/ResourceRevision.php new file mode 100644 index 0000000..ee1a3ea --- /dev/null +++ b/src/IDF/Wiki/ResourceRevision.php @@ -0,0 +1,340 @@ +_a['table'] = 'idf_wikiresourcerevs'; + $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, + ), + 'wikiresource' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'IDF_Wiki_Resource', + 'blank' => false, + 'verbose' => __('resource'), + 'relate_name' => 'revisions', + ), + 'is_head' => + array( + 'type' => 'Pluf_DB_Field_Boolean', + 'blank' => false, + 'default' => false, + 'help_text' => 'If this revision is the latest, we mark it as being the head revision.', + 'index' => true, + ), + 'summary' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 250, + 'verbose' => __('summary'), + 'help_text' => __('A one line description of the changes.'), + ), + 'filesize' => + array( + 'type' => 'Pluf_DB_Field_Integer', + 'blank' => false, + 'default' => 0, + 'verbose' => __('file size in bytes'), + ), + 'fileext' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 10, + 'verbose' => __('File extension'), + 'help_text' => __('The file extension of the uploaded resource.'), + ), + 'submitter' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'Pluf_User', + 'blank' => false, + 'verbose' => __('submitter'), + 'relate_name' => 'submitted_downloads', + ), + 'pageusage' => + array( + 'type' => 'Pluf_DB_Field_Manytomany', + 'model' => 'IDF_Wiki_PageRevision', + 'blank' => true, + 'verbose' => __('page usage'), + 'help_text' => 'Records on which pages this resource revision is used.', + ), + 'creation_dtime' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => true, + 'verbose' => __('creation date'), + ), + ); + $table = $this->_con->pfx.'idf_wiki_pagerevision_idf_wiki_resourcerevision_assoc'; + $this->_a['views'] = array( + 'join_pagerevision' => + array( + 'join' => 'LEFT JOIN '.$table + .' ON idf_wiki_resourcerevision_id=id', + ), + ); + } + + function __toString() + { + return sprintf(__('id %d: %s'), $this->id, $this->summary); + } + + function _toIndex() + { + return ''; + } + + function preDelete() + { + // if we kill off a head revision, ensure that we either mark a previous revision as head + if ($this->is_head) { + $sql = new Pluf_SQL('wikiresource=%s and id!=%s', array($this->wikiresource, $this->id)); + $revs = Pluf::factory('IDF_Wiki_ResourceRevision')->getList(array('filter'=>$sql->gen(), 'order'=>'id DESC')); + if ($revs->count() > 0) { + $previous = $revs[0]; + $previous->is_head = true; + $previous->update(); + } + } + + @unlink($this->getFilePath()); + IDF_Timeline::remove($this); + } + + function preSave($create=false) + { + if ($this->id == '') { + $this->creation_dtime = gmdate('Y-m-d H:i:s'); + $this->is_head = true; + } + } + + function postSave($create=false) + { + $resource = $this->get_wikiresource(); + + if ($create) { + $sql = new Pluf_SQL('wikiresource=%s', array($this->wikiresource)); + $rev = Pluf::factory('IDF_Wiki_ResourceRevision')->getList(array('filter'=>$sql->gen())); + if ($rev->count() > 1) { + IDF_Timeline::insert($this, $resource->get_project(), $this->get_submitter()); + foreach ($rev as $r) { + if ($r->id != $this->id and $r->is_head) { + $r->is_head = false; + $r->update(); + } + } + } + } + + // update the modification timestamp + $resource->update(); + } + + function getFilePath() + { + return sprintf(Pluf::f('upload_path').'/'.$this->get_wikiresource()->get_project()->shortname.'/wiki/res/%d/%d.%s', + $this->get_wikiresource()->id, $this->id, $this->fileext); + } + + function getViewURL() + { + $prj = $this->get_wikiresource()->get_project(); + $resource = $this->get_wikiresource(); + return Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($prj->shortname, $resource->title), + array('rev' => $this->id)); + } + + function getRawURL($attachment = false) + { + $query = $attachment ? array('attachment' => 1) : array(); + $prj = $this->get_wikiresource()->get_project(); + return Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::rawResource', + array($prj->shortname, $this->id), + $query); + } + + /** + * Returns the page revisions which contain references to this resource revision + */ + function getPageRevisions() + { + $db =& Pluf::db(); + $sql_results = $db->select( + 'SELECT idf_wiki_pagerevision_id as id '. + 'FROM '.Pluf::f('db_table_prefix', '').'idf_wiki_pagerevision_idf_wiki_resourcerevision_assoc '. + 'WHERE idf_wiki_resourcerevision_id='.$this->id + ); + $ids = array(0); + foreach ($sql_results as $id) { + $ids[] = $id['id']; + } + $ids = implode (',', $ids); + + $sql = new Pluf_SQL('id IN ('.$ids.')'); + return Pluf::factory('IDF_Wiki_PageRevision') + ->getList(array('filter' => $sql->gen())); + } + + /** + * Renders the resource with the given view options, including a link to the resource' detail page + */ + function render($opts = array()) + { + // give some reasonable defaults + $opts = array_merge(array( + 'align' => 'left', + 'width' => '', + 'height' => '', + 'preview' => 'yes', // if possible + 'title' => '', + ), $opts); + + $attrs = array('class="resource-container"'); + $styles = array(); + if (!empty($opts['align'])) { + switch ($opts['align']) { + case 'left': + $styles[] = 'float: left'; + $styles[] = 'margin-right: 10px'; + break; + case 'center': + $styles[] = 'margin: 0 auto 0 auto'; + break; + case 'right': + $styles[] = 'float: right'; + $styles[] = 'margin-left: 10px'; + break; + } + } + if (!empty($opts['width'])) { + $styles[] = 'width:'.$opts['width']; + } + if (!empty($opts['height'])) { + $styles[] = 'height:'.$opts['height']; + } + + $raw = $this->renderRaw(); + $viewUrl = $this->getViewURL(); + $download = ''; + $html = '
'; + if ($opts['preview'] == 'yes' && !empty($raw)) { + $html .= '
'.$raw.'
'."\n"; + } else { + $rawUrl = $this->getRawURL(true); + $download = ''; + } + $resource = $this->get_wikiresource(); + $title = $opts['title']; + if (empty($title)) { + $title = $resource->title.' - '.$resource->mime_type.' - '.Pluf_Utils::prettySize($this->filesize); + } + $html .= '
'.$download.''.$title.'
'."\n"; + $html .= '
'; + return $html; + } + + /** + * Renders a raw version of the resource, without any possibilities of formatting or the like + */ + function renderRaw() + { + $resource = $this->get_wikiresource(); + $url = $this->getRawURL(); + if (preg_match('#^image/(gif|jpeg|png|tiff)$#', $resource->mime_type)) { + return sprintf('%s', $url, $resource->title); + } + + if (preg_match('#^text/(plain|xml|html|sgml|javascript|ecmascript|css)$#', $resource->mime_type)) { + return sprintf('', $url, $resource->title); + } + + return ''; + } + + + public function timelineFragment($request) + { + $resource = $this->get_wikiresource(); + $url = Pluf::f('url_base') + .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($request->project->shortname, + $resource->title), + array('rev' => $this->id)); + + $out = "\n".''. + Pluf_esc(Pluf_Template_dateAgo($this->creation_dtime, 'without')). + ''; + $stag = new IDF_Template_ShowUser(); + $user = $stag->start($this->get_submitter(), $request, '', false); + $out .= sprintf(__('%2$s, %3$s'), $url, Pluf_esc($resource->title), Pluf_esc($this->summary)); + $out .= ''; + $out .= "\n".' +
'.sprintf(__('Change of %2$s, by %3$s'), $url, Pluf_esc($resource->title), $user).'
'; + return Pluf_Template::markSafe($out); + } + + public function feedFragment($request) + { + $resource = $this->get_wikiresource(); + $url = Pluf::f('url_base') + .Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::viewResource', + array($request->project->shortname, + $resource->title), + array('rev' => $this->id)); + + $title = sprintf(__('%1$s: Documentation resource %2$s updated - %3$s'), + $request->project->name, + $resource->title, $resource->summary); + $date = Pluf_Date::gmDateToGmString($this->creation_dtime); + $context = new Pluf_Template_Context_Request( + $request, + array('url' => $url, + 'title' => $title, + 'resource' => $resource, + 'rev' => $this, + 'create' => false, + 'date' => $date) + ); + $tmpl = new Pluf_Template('idf/wiki/feedfragment-resource.xml'); + return $tmpl->render($context); + } +} diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index c4424b5..97bd441 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -495,5 +495,54 @@ $cfg['idf_strong_key_check'] = false; # always have precedence. # $cfg['max_upload_size'] = 2097152; // Size in bytes +# If a download archive is uploaded, the size of the archive is limited to 20MB. +# The php.ini upload_max_filesize and post_max_size configuration setting will +# always have precedence. +# $cfg['max_upload_archive_size'] = 20971520; // Size in bytes + +# Older versions of Indefero submitted a POST request to a configured +# post-commit web hook when new revisions arrived, whereas a PUT request +# would have been more appropriate. Also, the payload's HMAC digest was +# submitted as value of the HTTP header 'Post-Commit-Hook-Hmac' during +# such a request. Since newer versions of Indefero use the same authentication +# mechanism (based on the same secret key) for other web hooks of the same +# project as well, the name of this HTTP header was no longer appropriate +# and as such changed to simply 'Web-Hook-Hmac'. +# +# Setting the following configuration option to 'compat' now restores the +# old behaviour in both cases. Please notice however that this compatibility +# option is likely to go away in the next major version of Indefero, so you +# should really change the other end of your web hooks! +$cfg['webhook_processing'] = 'compat'; + +# If IDF recalculates the activity index of the forge's projects, it does so +# by looking at the created and updated items in a particular tab / section +# for each project. +# +# You can now edit the weights that are applied to the calculation for each +# section in order to give other things more precendence. For example, if you +# do not use the documentation part to a great extent in most of your projects, +# you can weight this section lower and get an overall better activity value. +# +# If a section is removed, then activity in this section is neglected during +# the calculation. The same is true in case a section is disabled in the +# project administration. +$cfg['activity_section_weights'] = array( + 'source' => 4, + 'issues' => 2, + 'wiki' => 2, + 'downloads' => 1, + 'review' => 1, +); + +# Here you can define the timespan in days how long the activity calculation +# process should look into the history to get meaningful activity values for +# each project. +# +# If you have many low-profile projects in your forge, i.e. projects that only +# record very little activity, then it might be a good idea to bump this value +# high enough to show a proper activity index for those projects as well. +$cfg['activity_lookback'] = 7; + return $cfg; diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 74bacf9..eac28ae 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -29,6 +29,16 @@ $ctl[] = array('regex' => '#^/$#', 'model' => 'IDF_Views', 'method' => 'index'); +$ctl[] = array('regex' => '#^/projects/$#', + 'base' => $base, + 'model' => 'IDF_Views', + 'method' => 'listProjects'); + +$ctl[] = array('regex' => '#^/projects/label/(\w+)/(\w+)/$#', + 'base' => $base, + 'model' => 'IDF_Views', + 'method' => 'listProjectsByLabel'); + $ctl[] = array('regex' => '#^/login/$#', 'base' => $base, 'model' => 'IDF_Views', @@ -255,17 +265,32 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/changesrev/$#', 'model' => 'IDF_Views_Source_Svn', 'method' => 'changelogRev'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/repo/(.*)$#', + 'base' => $base, + 'model' => 'IDF_Views_Source', + 'method' => 'repository'); + // ---------- WIKI ----------------------------------------- $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'index'); + 'method' => 'listPages'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/res/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'listResources'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/create/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'create'); + 'method' => 'createPage'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/res/create/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'createResource'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/search/$#', 'base' => $base, @@ -275,30 +300,60 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/search/$#', $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/label/(\d+)/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'listLabel'); + 'method' => 'listPagesWithLabel'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/update/(.*)/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'update'); + 'method' => 'updatePage'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/res/update/(.*)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'updateResource'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/delrev/(\d+)/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'deleteRev'); + 'method' => 'deletePageRev'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/res/delrev/(\d+)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'deleteResourceRev'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/doc/delete/(\d+)/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'delete'); + 'method' => 'deletePage'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/res/delete/(\d+)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'deleteResource'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/res/raw/(.*)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'rawResource'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/page/(.*)/$#', 'base' => $base, 'model' => 'IDF_Views_Wiki', - 'method' => 'view'); + 'method' => 'viewPage'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/resource/(.*)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Wiki', + 'method' => 'viewResource'); // ---------- Downloads ------------------------------------ +$ctl[] = array('regex' => '#^/help/archive-format/$#', + 'base' => $base, + 'model' => 'IDF_Views', + 'method' => 'faqArchiveFormat'); + $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/$#', 'base' => $base, 'model' => 'IDF_Views_Download', @@ -327,7 +382,12 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/(\d+)/get/$#', $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/create/$#', 'base' => $base, 'model' => 'IDF_Views_Download', - 'method' => 'submit'); + 'method' => 'create'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/create/archive/$#', + 'base' => $base, + 'model' => 'IDF_Views_Download', + 'method' => 'createFromArchive'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/(\d+)/delete/$#', 'base' => $base, @@ -418,6 +478,11 @@ $ctl[] = array('regex' => '#^/api/$#', // ---------- FORGE ADMIN -------------------------------- +$ctl[] = array('regex' => '#^/admin/forge/$#', + 'base' => $base, + 'model' => 'IDF_Views_Admin', + 'method' => 'forge'); + $ctl[] = array('regex' => '#^/admin/projects/$#', 'base' => $base, 'model' => 'IDF_Views_Admin', @@ -428,6 +493,11 @@ $ctl[] = array('regex' => '#^/admin/projects/(\d+)/$#', 'model' => 'IDF_Views_Admin', 'method' => 'projectUpdate'); +$ctl[] = array('regex' => '#^/admin/projects/labels/$#', + 'base' => $base, + 'model' => 'IDF_Views_Admin', + 'method' => 'projectLabels'); + $ctl[] = array('regex' => '#^/admin/projects/create/$#', 'base' => $base, 'model' => 'IDF_Views_Admin', diff --git a/src/IDF/locale/idf.pot b/src/IDF/locale/idf.pot index d1ff569..a1ce29b 100644 --- a/src/IDF/locale/idf.pot +++ b/src/IDF/locale/idf.pot @@ -8,31 +8,35 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-10-31 01:11+0100\n" +"POT-Creation-Date: 2012-03-22 01:16+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: IDF/Commit.php:55 IDF/Conf.php:54 IDF/Issue.php:52 IDF/Review.php:65 -#: IDF/Search/Occ.php:69 IDF/Tag.php:52 IDF/Upload.php:49 IDF/WikiPage.php:54 +#: IDF/Commit.php:55 IDF/Conf.php:54 IDF/Issue.php:52 +#: IDF/ProjectActivity.php:49 IDF/Review.php:65 IDF/Search/Occ.php:69 +#: IDF/Tag.php:52 IDF/Upload.php:49 IDF/Wiki/Page.php:54 +#: IDF/Wiki/Resource.php:56 msgid "project" msgstr "" #: IDF/Commit.php:63 IDF/Issue.php:67 IDF/IssueComment.php:65 #: IDF/IssueFile.php:57 IDF/IssueRelation.php:69 IDF/Review/Comment.php:69 -#: IDF/Review.php:80 IDF/Upload.php:91 IDF/WikiPage.php:78 -#: IDF/WikiRevision.php:79 +#: IDF/Review.php:80 IDF/Upload.php:91 IDF/Wiki/Page.php:78 +#: IDF/Wiki/PageRevision.php:79 IDF/Wiki/Resource.php:88 +#: IDF/Wiki/ResourceRevision.php:86 msgid "submitter" msgstr "" #: IDF/Commit.php:87 IDF/Issue.php:60 IDF/Review/Patch.php:60 -#: IDF/Review.php:73 IDF/Upload.php:57 IDF/WikiPage.php:70 -#: IDF/WikiRevision.php:65 +#: IDF/Review.php:73 IDF/Upload.php:57 IDF/Wiki/Page.php:70 +#: IDF/Wiki/PageRevision.php:65 IDF/Wiki/Resource.php:80 +#: IDF/Wiki/ResourceRevision.php:63 msgid "summary" msgstr "" @@ -43,25 +47,26 @@ msgstr "" #: IDF/Commit.php:100 IDF/Issue.php:105 IDF/IssueComment.php:79 #: IDF/IssueFile.php:96 IDF/IssueRelation.php:75 IDF/Review/Comment.php:90 #: IDF/Review/FileComment.php:75 IDF/Review/Patch.php:87 IDF/Review.php:108 -#: IDF/Upload.php:112 IDF/WikiPage.php:100 IDF/WikiRevision.php:92 +#: IDF/Upload.php:112 IDF/Wiki/Page.php:100 IDF/Wiki/PageRevision.php:92 +#: IDF/Wiki/Resource.php:95 IDF/Wiki/ResourceRevision.php:101 msgid "creation date" msgstr "" #: IDF/Commit.php:238 #, php-format -msgid "Commit %s, by %s" +msgid "Commit %1$s, by %2$s" msgstr "" -#: IDF/Commit.php:329 +#: IDF/Commit.php:337 #, php-format -msgid "New Commit %s - %s (%s)" +msgid "New Commit %1$s - %2$s (%3$s)" msgstr "" #: IDF/Conf.php:61 IDF/Gconf.php:73 msgid "key" msgstr "" -#: IDF/Conf.php:67 IDF/Gconf.php:79 +#: IDF/Conf.php:67 IDF/Gconf.php:79 IDF/ProjectActivity.php:62 msgid "value" msgstr "" @@ -81,33 +86,56 @@ msgstr "" msgid "email" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:38 IDF/Views/Project.php:587 +#: IDF/Form/Admin/ForgeConf.php:34 +msgid "Custom forge page enabled" +msgstr "" + +#: IDF/Form/Admin/ForgeConf.php:39 IDF/Form/WikiPageCreate.php:80 +#: IDF/Form/WikiPageUpdate.php:72 +msgid "Content" +msgstr "" + +#: IDF/Form/Admin/LabelConf.php:40 +msgid "Predefined project labels" +msgstr "" + +#: IDF/Form/Admin/LabelConf.php:55 +#, php-format +msgid "" +"The label \"%s\" is invalid: A label must only consist of alphanumeric " +"characters and dashes, and can optionally contain a \":\" with a group " +"prefix." +msgstr "" + +#: IDF/Form/Admin/ProjectCreate.php:38 IDF/Views/Project.php:600 msgid "git" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:39 IDF/Views/Project.php:588 +#: IDF/Form/Admin/ProjectCreate.php:39 IDF/Views/Project.php:601 msgid "Subversion" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:40 IDF/Views/Project.php:589 +#: IDF/Form/Admin/ProjectCreate.php:40 IDF/Views/Project.php:602 msgid "mercurial" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:41 IDF/Views/Project.php:590 +#: IDF/Form/Admin/ProjectCreate.php:41 IDF/Views/Project.php:603 msgid "monotone" msgstr "" #: IDF/Form/Admin/ProjectCreate.php:49 IDF/Form/Admin/ProjectUpdate.php:44 -#: IDF/Form/ProjectConf.php:38 IDF/Views/Admin.php:66 IDF/Views/Admin.php:210 +#: IDF/Form/ProjectConf.php:39 IDF/Views/Admin.php:86 IDF/Views/Admin.php:273 msgid "Name" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:55 IDF/Form/TabsConf.php:77 +#: IDF/Form/Admin/ProjectCreate.php:55 IDF/Form/TabsConf.php:101 #: IDF/gettexttemplates/idf/base-full.html.php:5 +#: IDF/gettexttemplates/idf/base-full.html~.php:5 #: IDF/gettexttemplates/idf/base.html.php:5 -#: IDF/gettexttemplates/idf/index.html.php:7 -#: IDF/gettexttemplates/idf/index.html.php:8 -#: IDF/gettexttemplates/idf/main-menu.html.php:8 +#: IDF/gettexttemplates/idf/base.html~.php:5 +#: IDF/gettexttemplates/idf/main-menu.html.php:9 +#: IDF/gettexttemplates/idf/project-list.html.php:5 +#: IDF/gettexttemplates/idf/project-list.html.php:7 msgid "Private project" msgstr "" @@ -130,105 +158,122 @@ msgstr "" msgid "A one line description of the project." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:77 +#: IDF/Form/Admin/ProjectCreate.php:77 IDF/Form/Admin/ProjectUpdate.php:58 +#: IDF/Form/ProjectConf.php:56 +msgid "External URL" +msgstr "" + +#: IDF/Form/Admin/ProjectCreate.php:84 msgid "Repository type" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:85 +#: IDF/Form/Admin/ProjectCreate.php:92 msgid "Remote Subversion repository" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:92 IDF/Form/SourceConf.php:40 +#: IDF/Form/Admin/ProjectCreate.php:99 IDF/Form/SourceConf.php:40 msgid "Repository username" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:99 IDF/Form/SourceConf.php:47 +#: IDF/Form/Admin/ProjectCreate.php:106 IDF/Form/SourceConf.php:47 msgid "Repository password" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:106 IDF/Form/Admin/ProjectUpdate.php:59 +#: IDF/Form/Admin/ProjectCreate.php:113 IDF/Form/Admin/ProjectUpdate.php:66 msgid "Master branch" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:109 IDF/Form/Admin/ProjectUpdate.php:62 +#: IDF/Form/Admin/ProjectCreate.php:116 IDF/Form/Admin/ProjectUpdate.php:69 msgid "" "This should be a world-wide unique identifier for your project. A reverse " "DNS notation like \"com.my-domain.my-project\" is a good idea." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:114 IDF/Form/Admin/ProjectUpdate.php:68 +#: IDF/Form/Admin/ProjectCreate.php:121 IDF/Form/Admin/ProjectUpdate.php:95 #: IDF/Form/MembersConf.php:46 IDF/Form/TabsConf.php:53 +#: IDF/Form/TabsConf.php:72 msgid "Project owners" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:123 IDF/Form/Admin/ProjectUpdate.php:76 +#: IDF/Form/Admin/ProjectCreate.php:130 IDF/Form/Admin/ProjectUpdate.php:103 #: IDF/Form/MembersConf.php:54 IDF/Form/TabsConf.php:52 +#: IDF/Form/TabsConf.php:78 msgid "Project members" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:136 +#: IDF/Form/Admin/ProjectCreate.php:140 IDF/Form/Admin/ProjectUpdate.php:85 +#: IDF/Form/IssueCreate.php:168 IDF/Form/IssueUpdate.php:164 +#: IDF/Form/ProjectConf.php:73 IDF/Form/UpdateUpload.php:71 +#: IDF/Form/Upload.php:70 IDF/Form/WikiPageCreate.php:93 +#: IDF/Form/WikiPageUpdate.php:104 +msgid "Labels" +msgstr "" + +#: IDF/Form/Admin/ProjectCreate.php:155 msgid "Project template" msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:138 +#: IDF/Form/Admin/ProjectCreate.php:157 msgid "" "Use the given project to initialize the new project. Access rights and " "general configuration will be taken from the template project." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:185 +#: IDF/Form/Admin/ProjectCreate.php:204 msgid "" "Only a remote repository available through HTTP or HTTPS is allowed. For " "example \"http://somewhere.com/svn/trunk\"." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:201 +#: IDF/Form/Admin/ProjectCreate.php:220 IDF/Form/Admin/ProjectUpdate.php:117 msgid "" "The master branch is empty or contains illegal characters, please use only " -"letters, digits, dashs and dots as separators." +"letters, digits, dashes and dots as separators." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:211 IDF/Form/Admin/ProjectUpdate.php:101 +#: IDF/Form/Admin/ProjectCreate.php:230 IDF/Form/Admin/ProjectUpdate.php:128 msgid "This master branch is already used. Please select another one." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:222 +#: IDF/Form/Admin/ProjectCreate.php:241 msgid "" "This shortname contains illegal characters, please use only letters, digits " "and dash (-)." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:225 +#: IDF/Form/Admin/ProjectCreate.php:244 msgid "The shortname cannot start with the dash (-) character." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:228 +#: IDF/Form/Admin/ProjectCreate.php:247 msgid "The shortname cannot end with the dash (-) character." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:233 +#: IDF/Form/Admin/ProjectCreate.php:252 msgid "This shortname is already used. Please select another one." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:279 IDF/Form/Admin/ProjectDelete.php:78 -#: IDF/Form/Admin/ProjectUpdate.php:121 IDF/Form/Admin/UserCreate.php:106 +#: IDF/Form/Admin/ProjectCreate.php:303 IDF/Form/Admin/ProjectDelete.php:78 +#: IDF/Form/Admin/ProjectUpdate.php:153 IDF/Form/Admin/UserCreate.php:106 #: IDF/Form/Admin/UserUpdate.php:191 IDF/Form/IssueCreate.php:338 #: IDF/Form/IssueUpdate.php:329 IDF/Form/MembersConf.php:64 #: IDF/Form/Password.php:76 IDF/Form/Register.php:112 #: IDF/Form/ReviewCreate.php:187 IDF/Form/ReviewFileComment.php:143 -#: IDF/Form/TabsConf.php:98 IDF/Form/UpdateUpload.php:126 -#: IDF/Form/Upload.php:148 IDF/Form/UserAccount.php:216 -#: IDF/Form/UserChangeEmail.php:80 IDF/Form/WikiCreate.php:167 -#: IDF/Form/WikiDelete.php:59 IDF/Form/WikiUpdate.php:178 +#: IDF/Form/TabsConf.php:122 IDF/Form/UpdateUpload.php:126 +#: IDF/Form/Upload.php:149 IDF/Form/UploadArchive.php:137 +#: IDF/Form/UserAccount.php:216 IDF/Form/UserChangeEmail.php:80 +#: IDF/Form/WikiPageCreate.php:167 IDF/Form/WikiPageDelete.php:59 +#: IDF/Form/WikiPageUpdate.php:178 IDF/Form/WikiResourceCreate.php:123 +#: IDF/Form/WikiResourceDelete.php:59 IDF/Form/WikiResourceUpdate.php:124 msgid "Cannot save the model from an invalid form." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:295 +#: IDF/Form/Admin/ProjectCreate.php:341 msgid "" "Click on the Project Management tab to set the description of your project." msgstr "" -#: IDF/Form/Admin/ProjectCreate.php:363 +#: IDF/Form/Admin/ProjectCreate.php:426 msgid "This project is not available." msgstr "" @@ -250,12 +295,6 @@ msgstr "" msgid "Sorry, you really need to backup your data before deletion." msgstr "" -#: IDF/Form/Admin/ProjectUpdate.php:90 -msgid "" -"The master branch is empty or contains illegal characters, please use only " -"letters, digits, dashes and dots as separators." -msgstr "" - #: IDF/Form/Admin/UserCreate.php:37 IDF/Form/Admin/UserUpdate.php:38 #: IDF/Form/RegisterConfirmation.php:50 IDF/Form/UserAccount.php:40 msgid "First name" @@ -339,10 +378,11 @@ msgid "Confirm password" msgstr "" #: IDF/Form/Admin/UserUpdate.php:100 IDF/Form/IssueCreate.php:66 -#: IDF/Form/ProjectConf.php:47 IDF/Form/ReviewCreate.php:54 +#: IDF/Form/ProjectConf.php:48 IDF/Form/ReviewCreate.php:54 #: IDF/Form/UpdateUpload.php:51 IDF/Form/Upload.php:49 -#: IDF/Form/UserAccount.php:101 IDF/Form/WikiCreate.php:70 -#: IDF/Form/WikiUpdate.php:60 +#: IDF/Form/UserAccount.php:101 IDF/Form/WikiPageCreate.php:70 +#: IDF/Form/WikiPageUpdate.php:60 IDF/Form/WikiResourceCreate.php:54 +#: IDF/Form/WikiResourceUpdate.php:46 IDF/Views/Wiki.php:102 msgid "Description" msgstr "" @@ -376,15 +416,15 @@ msgstr "" msgid "Tick this to delete the custom avatar." msgstr "" -#: IDF/Form/Admin/UserUpdate.php:161 IDF/Views/Admin.php:211 +#: IDF/Form/Admin/UserUpdate.php:161 IDF/Views/Admin.php:274 msgid "Staff" msgstr "" #: IDF/Form/Admin/UserUpdate.php:164 -msgid "If you give staff rights to a user, you really need to trust them." +msgid "If you give staff rights to a user, you really need to trust him." msgstr "" -#: IDF/Form/Admin/UserUpdate.php:172 IDF/Views/Admin.php:213 +#: IDF/Form/Admin/UserUpdate.php:172 IDF/Views/Admin.php:276 msgid "Active" msgstr "" @@ -419,11 +459,11 @@ msgstr "" #: IDF/Form/ReviewCreate.php:45 IDF/Form/ReviewFileComment.php:73 #: IDF/Form/UpdateUpload.php:42 IDF/Form/Upload.php:40 #: IDF/gettexttemplates/idf/issues/base.html.php:3 IDF/Views/Download.php:65 -#: IDF/Views/Download.php:313 IDF/Views/Issue.php:62 IDF/Views/Issue.php:218 -#: IDF/Views/Issue.php:299 IDF/Views/Issue.php:388 IDF/Views/Issue.php:540 -#: IDF/Views/Issue.php:763 IDF/Views/Issue.php:822 IDF/Views/Review.php:58 -#: IDF/Views/User.php:83 IDF/Views/Wiki.php:62 IDF/Views/Wiki.php:107 -#: IDF/Views/Wiki.php:148 +#: IDF/Views/Download.php:347 IDF/Views/Issue.php:62 IDF/Views/Issue.php:221 +#: IDF/Views/Issue.php:302 IDF/Views/Issue.php:391 IDF/Views/Issue.php:543 +#: IDF/Views/Issue.php:766 IDF/Views/Issue.php:825 IDF/Views/Review.php:58 +#: IDF/Views/User.php:83 IDF/Views/Wiki.php:62 IDF/Views/Wiki.php:145 +#: IDF/Views/Wiki.php:186 msgid "Summary" msgstr "" @@ -438,9 +478,9 @@ msgstr "" #: IDF/Form/IssueCreate.php:99 IDF/Form/IssueUpdate.php:89 #: IDF/Form/ReviewCreate.php:103 IDF/Form/ReviewFileComment.php:83 -#: IDF/Views/Issue.php:63 IDF/Views/Issue.php:219 IDF/Views/Issue.php:301 -#: IDF/Views/Issue.php:389 IDF/Views/Issue.php:541 IDF/Views/Issue.php:764 -#: IDF/Views/Issue.php:823 IDF/Views/Review.php:59 IDF/Views/User.php:84 +#: IDF/Views/Issue.php:63 IDF/Views/Issue.php:222 IDF/Views/Issue.php:304 +#: IDF/Views/Issue.php:392 IDF/Views/Issue.php:544 IDF/Views/Issue.php:767 +#: IDF/Views/Issue.php:826 IDF/Views/Review.php:59 IDF/Views/User.php:84 msgid "Status" msgstr "" @@ -453,26 +493,19 @@ msgstr "" msgid "This issue" msgstr "" -#: IDF/Form/IssueCreate.php:168 IDF/Form/IssueUpdate.php:164 -#: IDF/Form/UpdateUpload.php:71 IDF/Form/Upload.php:70 -#: IDF/Form/WikiCreate.php:93 IDF/Form/WikiUpdate.php:104 -msgid "Labels" -msgstr "" - #: IDF/Form/IssueCreate.php:210 msgid "You cannot add a label with the \"Status\" prefix to an issue." msgstr "" #: IDF/Form/IssueCreate.php:211 IDF/Form/IssueCreate.php:218 -#: IDF/Form/UpdateUpload.php:110 IDF/Form/Upload.php:120 -#: IDF/Form/WikiCreate.php:151 IDF/Form/WikiUpdate.php:162 +#: IDF/Form/UpdateUpload.php:110 IDF/Form/Upload.php:121 +#: IDF/Form/WikiPageCreate.php:151 IDF/Form/WikiPageUpdate.php:162 msgid "You provided an invalid label." msgstr "" #: IDF/Form/IssueCreate.php:217 IDF/Form/UpdateUpload.php:109 -#: IDF/Form/Upload.php:119 #, php-format -msgid "You cannot provide more than label from the %s class to an issue." +msgid "You cannot provide more than one label from the %s class to an issue." msgstr "" #: IDF/Form/IssueCreate.php:228 IDF/Form/IssueUpdate.php:194 @@ -541,7 +574,7 @@ msgid "" msgstr "" #: IDF/Form/IssueTrackingConf.php:133 -msgid "Each issue may have at most one label with each of these classes" +msgid "Each issue may have at most one label with each of these classes." msgstr "" #: IDF/Form/IssueTrackingConf.php:140 @@ -551,11 +584,13 @@ msgstr "" #: IDF/Form/IssueTrackingConf.php:142 msgid "" "You can define bidirectional relations like \"is related to\" or \"blocks, " -"is blocked by\"." +"is blocked by\". For standard relations pre-configured translations exist, " +"new relations should however be defined in a language that is understood by " +"all project members." msgstr "" #: IDF/Form/IssueUpdate.php:56 IDF/Form/ReviewFileComment.php:45 -#: IDF/Form/WikiUpdate.php:82 +#: IDF/Form/WikiPageUpdate.php:82 IDF/Form/WikiResourceUpdate.php:66 msgid "Comment" msgstr "" @@ -637,34 +672,38 @@ msgid "" "activate it." msgstr "" -#: IDF/Form/ProjectConf.php:42 +#: IDF/Form/ProjectConf.php:43 msgid "Short Description" msgstr "" -#: IDF/Form/ProjectConf.php:58 +#: IDF/Form/ProjectConf.php:85 msgid "The \"upload_path\" configuration variable was not set." msgstr "" -#: IDF/Form/ProjectConf.php:63 +#: IDF/Form/ProjectConf.php:90 msgid "Update the logo" msgstr "" -#: IDF/Form/ProjectConf.php:65 +#: IDF/Form/ProjectConf.php:92 msgid "The logo must be a picture with a size of 32 by 32." msgstr "" -#: IDF/Form/ProjectConf.php:75 +#: IDF/Form/ProjectConf.php:103 msgid "Remove the current logo" msgstr "" -#: IDF/Form/ProjectConf.php:111 +#: IDF/Form/ProjectConf.php:139 msgid "Could not determine the size of the uploaded picture." msgstr "" -#: IDF/Form/ProjectConf.php:115 +#: IDF/Form/ProjectConf.php:143 msgid "The picture must have a size of 32 by 32." msgstr "" +#: IDF/Form/ProjectConf.php:164 +msgid "The entered URL is invalid. Only http and https URLs are allowed." +msgstr "" + #: IDF/Form/Register.php:41 msgid "Your login" msgstr "" @@ -675,7 +714,7 @@ msgid "" "and digits." msgstr "" -#: IDF/Form/Register.php:53 +#: IDF/Form/Register.php:53 IDF/Form/UserAccount.php:59 msgid "Your email" msgstr "" @@ -695,8 +734,8 @@ msgstr "" #: IDF/Form/Register.php:97 #, php-format msgid "" -"The email \"%s\" is already used. If you need to, click on the help link to " -"recover your password." +"The email \"%1$s\" is already used. If you need to, you can recover your password." msgstr "" #: IDF/Form/Register.php:148 @@ -720,9 +759,10 @@ msgid "" msgstr "" #: IDF/Form/RegisterConfirmation.php:110 +#, php-format msgid "" -"This account has already been confirmed. Maybe should you try to recover " -"your password using the help link." +"This account has already been confirmed. Maybe should you try to recover your password." msgstr "" #: IDF/Form/ReviewCreate.php:74 @@ -765,43 +805,48 @@ msgstr "" msgid "This field is required." msgstr "" -#: IDF/Form/SourceConf.php:56 +#: IDF/Form/SourceConf.php:54 IDF/Form/UploadConf.php:70 msgid "Webhook URL" msgstr "" -#: IDF/Form/SourceConf.php:58 -#, php-format -msgid "Learn more about the post-commit web hooks." -msgstr "" - #: IDF/Form/TabsConf.php:38 IDF/gettexttemplates/idf/admin/base.html.php:4 -#: IDF/gettexttemplates/idf/base-full.html.php:7 -#: IDF/gettexttemplates/idf/base.html.php:7 +#: IDF/gettexttemplates/idf/base-full.html.php:8 +#: IDF/gettexttemplates/idf/base-full.html~.php:7 +#: IDF/gettexttemplates/idf/base.html.php:8 +#: IDF/gettexttemplates/idf/base.html~.php:7 #: IDF/gettexttemplates/idf/downloads/base.html.php:3 #: IDF/gettexttemplates/idf/gadmin/projects/delete.html.php:14 #: IDF/Views/Project.php:97 msgid "Downloads" msgstr "" -#: IDF/Form/TabsConf.php:39 IDF/gettexttemplates/idf/base-full.html.php:11 -#: IDF/gettexttemplates/idf/base.html.php:11 +#: IDF/Form/TabsConf.php:39 IDF/gettexttemplates/idf/base-full.html.php:12 +#: IDF/gettexttemplates/idf/base-full.html~.php:11 +#: IDF/gettexttemplates/idf/base.html.php:12 +#: IDF/gettexttemplates/idf/base.html~.php:11 msgid "Code Review" msgstr "" #: IDF/Form/TabsConf.php:40 IDF/gettexttemplates/idf/admin/base.html.php:5 -#: IDF/gettexttemplates/idf/base-full.html.php:8 -#: IDF/gettexttemplates/idf/base.html.php:8 +#: IDF/gettexttemplates/idf/base-full.html.php:9 +#: IDF/gettexttemplates/idf/base-full.html~.php:8 +#: IDF/gettexttemplates/idf/base.html.php:9 +#: IDF/gettexttemplates/idf/base.html~.php:8 msgid "Documentation" msgstr "" #: IDF/Form/TabsConf.php:41 IDF/gettexttemplates/idf/admin/base.html.php:7 -#: IDF/gettexttemplates/idf/base-full.html.php:10 -#: IDF/gettexttemplates/idf/base.html.php:10 +#: IDF/gettexttemplates/idf/base-full.html.php:11 +#: IDF/gettexttemplates/idf/base-full.html~.php:10 +#: IDF/gettexttemplates/idf/base.html.php:11 +#: IDF/gettexttemplates/idf/base.html~.php:10 msgid "Source" msgstr "" -#: IDF/Form/TabsConf.php:42 IDF/gettexttemplates/idf/base-full.html.php:9 -#: IDF/gettexttemplates/idf/base.html.php:9 +#: IDF/Form/TabsConf.php:42 IDF/gettexttemplates/idf/base-full.html.php:10 +#: IDF/gettexttemplates/idf/base-full.html~.php:9 +#: IDF/gettexttemplates/idf/base.html.php:10 +#: IDF/gettexttemplates/idf/base.html~.php:9 #: IDF/gettexttemplates/idf/gadmin/projects/delete.html.php:17 msgid "Issues" msgstr "" @@ -818,22 +863,104 @@ msgstr "" msgid "Closed" msgstr "" -#: IDF/Form/TabsConf.php:83 +#: IDF/Form/TabsConf.php:84 +msgid "Others" +msgstr "" + +#: IDF/Form/TabsConf.php:107 msgid "Extra authorized users" msgstr "" -#: IDF/Form/Upload.php:59 IDF/gettexttemplates/idf/source/git/tree.html.php:6 +#: IDF/Form/Upload.php:59 IDF/Form/WikiResourceCreate.php:65 +#: IDF/Form/WikiResourceUpdate.php:56 +#: IDF/gettexttemplates/idf/source/git/tree.html.php:6 #: IDF/gettexttemplates/idf/source/mercurial/tree.html.php:6 #: IDF/gettexttemplates/idf/source/mtn/tree.html.php:7 #: IDF/gettexttemplates/idf/source/svn/tree.html.php:7 -#: IDF/Views/Download.php:64 IDF/Views/Download.php:312 +#: IDF/Views/Download.php:64 IDF/Views/Download.php:346 msgid "File" msgstr "" -#: IDF/Form/Upload.php:86 +#: IDF/Form/Upload.php:87 IDF/Form/WikiResourceCreate.php:96 +#: IDF/Form/WikiResourceUpdate.php:83 msgid "For security reasons, you cannot upload a file with this extension." msgstr "" +#: IDF/Form/Upload.php:120 +#, php-format +msgid "You cannot provide more than one label from the %s class to a download." +msgstr "" + +#: IDF/Form/UploadArchive.php:41 +msgid "Archive file" +msgstr "" + +#: IDF/Form/UploadArchive.php:67 +#, php-format +msgid "" +"For security reasons, you cannot upload a file (%s) with this extension." +msgstr "" + +#: IDF/Form/UploadArchive.php:98 +#, php-format +msgid "" +"You cannot provide more than label from the %1$s class to a download (%2$s)." +msgstr "" + +#: IDF/Form/UploadArchive.php:109 +#, php-format +msgid "" +"A file with the name \"%s\" has already been uploaded and is not marked to " +"be replaced." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:41 +msgid "The archive does not exist." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:48 +#, php-format +msgid "The archive could not be read (code %d)." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:53 +msgid "The archive does not contain a manifest.xml." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:61 +#, php-format +msgid "The archive's manifest is invalid: %s" +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:77 +#, php-format +msgid "The entry %d in the manifest is missing a file name." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:82 +#, php-format +msgid "The entry %d in the manifest is missing a summary." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:86 +msgid "The manifest must not reference itself." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:91 +#, php-format +msgid "The entry %s in the manifest does not exist in the archive." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:96 +#, php-format +msgid "The entry %s in the manifest is referenced more than once." +msgstr "" + +#: IDF/Form/UploadArchiveHelper.php:108 +#, php-format +msgid "The entry %s in the manifest has more than the six allowed labels set." +msgstr "" + #: IDF/Form/UploadConf.php:53 msgid "Predefined download labels" msgstr "" @@ -842,10 +969,6 @@ msgstr "" msgid "Each download may have at most one label with each of these classes" msgstr "" -#: IDF/Form/UserAccount.php:59 -msgid "Your mail" -msgstr "" - #: IDF/Form/UserAccount.php:61 msgid "" "If you change your email address, an email will be sent to the new address " @@ -863,11 +986,11 @@ msgid "" msgstr "" #: IDF/Form/UserAccount.php:171 -msgid "Add a secondary mail address" +msgid "Add a secondary email address" msgstr "" #: IDF/Form/UserAccount.php:173 -msgid "You will get a mail to confirm that you own the address you specify." +msgid "You will get an email to confirm that you own the address you specify." msgstr "" #: IDF/Form/UserAccount.php:200 @@ -914,7 +1037,7 @@ msgid "" "Each documentation page may have at most one label with each of these classes" msgstr "" -#: IDF/Form/WikiCreate.php:38 +#: IDF/Form/WikiPageCreate.php:38 msgid "" "# Introduction\n" "\n" @@ -930,56 +1053,95 @@ msgid "" "* Links to other [[WikiPage]].\n" msgstr "" -#: IDF/Form/WikiCreate.php:57 +#: IDF/Form/WikiPageCreate.php:57 msgid "PageName" msgstr "" -#: IDF/Form/WikiCreate.php:60 IDF/Form/WikiUpdate.php:50 +#: IDF/Form/WikiPageCreate.php:60 IDF/Form/WikiPageUpdate.php:50 msgid "Page title" msgstr "" -#: IDF/Form/WikiCreate.php:66 IDF/Form/WikiUpdate.php:56 +#: IDF/Form/WikiPageCreate.php:66 IDF/Form/WikiPageUpdate.php:56 msgid "" "The page name must contains only letters, digits and the dash (-) character." msgstr "" -#: IDF/Form/WikiCreate.php:71 IDF/Form/WikiUpdate.php:61 +#: IDF/Form/WikiPageCreate.php:71 IDF/Form/WikiPageUpdate.php:61 msgid "This one line description is displayed in the list of pages." msgstr "" -#: IDF/Form/WikiCreate.php:80 IDF/Form/WikiUpdate.php:72 -msgid "Content" -msgstr "" - -#: IDF/Form/WikiCreate.php:108 IDF/Form/WikiUpdate.php:119 +#: IDF/Form/WikiPageCreate.php:108 IDF/Form/WikiPageUpdate.php:119 +#: IDF/Form/WikiResourceCreate.php:78 msgid "The title contains invalid characters." msgstr "" -#: IDF/Form/WikiCreate.php:114 IDF/Form/WikiUpdate.php:125 +#: IDF/Form/WikiPageCreate.php:114 IDF/Form/WikiPageUpdate.php:125 msgid "A page with this title already exists." msgstr "" -#: IDF/Form/WikiCreate.php:150 IDF/Form/WikiUpdate.php:161 +#: IDF/Form/WikiPageCreate.php:150 IDF/Form/WikiPageUpdate.php:161 #, php-format -msgid "You cannot provide more than label from the %s class to a page." +msgid "You cannot provide more than one label from the %s class to a page." msgstr "" -#: IDF/Form/WikiCreate.php:200 +#: IDF/Form/WikiPageCreate.php:200 msgid "Initial page creation" msgstr "" -#: IDF/Form/WikiDelete.php:39 +#: IDF/Form/WikiPageDelete.php:39 msgid "Yes, I understand that the page and all its revisions will be deleted." msgstr "" -#: IDF/Form/WikiDelete.php:50 +#: IDF/Form/WikiPageDelete.php:50 IDF/Form/WikiResourceDelete.php:50 msgid "You need to confirm the deletion." msgstr "" -#: IDF/Form/WikiUpdate.php:83 +#: IDF/Form/WikiPageUpdate.php:83 IDF/Form/WikiResourceUpdate.php:67 msgid "One line to describe the changes you made." msgstr "" +#: IDF/Form/WikiResourceCreate.php:40 +msgid "ResourceName" +msgstr "" + +#: IDF/Form/WikiResourceCreate.php:44 +msgid "Resource title" +msgstr "" + +#: IDF/Form/WikiResourceCreate.php:50 +msgid "" +"The resource name must contains only letters, digits and the dash (-) " +"character." +msgstr "" + +#: IDF/Form/WikiResourceCreate.php:55 IDF/Form/WikiResourceUpdate.php:47 +msgid "This one line description is displayed in the list of resources." +msgstr "" + +#: IDF/Form/WikiResourceCreate.php:84 +msgid "A resource with this title already exists." +msgstr "" + +#: IDF/Form/WikiResourceCreate.php:142 +msgid "Initial resource creation" +msgstr "" + +#: IDF/Form/WikiResourceDelete.php:39 +msgid "" +"Yes, I understand that the resource and all its revisions will be deleted." +msgstr "" + +#: IDF/Form/WikiResourceUpdate.php:89 +#, php-format +msgid "" +"The mime type of the uploaded file \"%1$s\" does not match the mime type of " +"this resource \"%2$s\"" +msgstr "" + +#: IDF/Form/WikiResourceUpdate.php:97 +msgid "The current version of the resource and the uploaded file are equal." +msgstr "" + #: IDF/Gconf.php:60 IDF/Search/Occ.php:56 msgid "model class" msgstr "" @@ -1007,6 +1169,7 @@ msgstr "" #: IDF/gettexttemplates/idf/admin/downloads.html.php:3 #: IDF/gettexttemplates/idf/admin/issue-tracking.html.php:3 #: IDF/gettexttemplates/idf/admin/wiki.html.php:3 +#: IDF/gettexttemplates/idf/gadmin/projects/labels.html.php:3 msgid "" "\n" "

Instructions:

\n" @@ -1016,12 +1179,50 @@ msgid "" msgstr "" #: IDF/gettexttemplates/idf/admin/downloads.html.php:8 +msgid "" +"

The webhook URL setting specifies an URL to which a HTTP PUT\n" +"request is sent after a new download has been added or to which a HTTP " +"POST\n" +"request is sent after an existing download has been updated.\n" +"If this field is empty, notifications are disabled.

\n" +"\n" +"

Only properly-escaped HTTP URLs are supported, for " +"example:

\n" +"\n" +"
    \n" +"
  • http://domain.com/upload
  • \n" +"
  • http://domain.com/upload?my%20param
  • \n" +"
\n" +"\n" +"

In addition, the URL may contain the following \"%\" notation, which\n" +"will be replaced with specific project values for each download:

\n" +"\n" +"
    \n" +"
  • %p - project name
  • \n" +"
  • %d - download id
  • \n" +"
\n" +"\n" +"

For example, updating download 123 of project 'my-project' with\n" +"web hook URL http://mydomain.com/%p/%d would send a POST " +"request to\n" +"http://mydomain.com/my-project/123.

" +msgstr "" + +#: IDF/gettexttemplates/idf/admin/downloads.html.php:31 +#: IDF/gettexttemplates/idf/admin/source.html.php:30 +msgid "Web-Hook authentication key:" +msgstr "" + +#: IDF/gettexttemplates/idf/admin/downloads.html.php:32 #: IDF/gettexttemplates/idf/admin/issue-tracking.html.php:8 #: IDF/gettexttemplates/idf/admin/members.html.php:13 #: IDF/gettexttemplates/idf/admin/source.html.php:31 #: IDF/gettexttemplates/idf/admin/summary.html.php:11 -#: IDF/gettexttemplates/idf/admin/tabs.html.php:15 +#: IDF/gettexttemplates/idf/admin/tabs.html.php:12 #: IDF/gettexttemplates/idf/admin/wiki.html.php:8 +#: IDF/gettexttemplates/idf/gadmin/forge/index.html.php:13 +#: IDF/gettexttemplates/idf/gadmin/projects/labels.html.php:8 msgid "Save Changes" msgstr "" @@ -1054,29 +1255,31 @@ msgstr "" #: IDF/gettexttemplates/idf/admin/source.html.php:4 msgid "" -"

The webhook URL setting specifies a URL to which a HTTP POST\n" -"request is sent after each repository commit. If this field is empty,\n" -"notifications are disabled.

\n" +"

The webhook URL setting specifies an URL to which a HTTP \n" +"%%hook_request_method%% request is sent after each " +"repository\n" +"commit. If this field is empty, notifications are disabled.

\n" "\n" "

Only properly-escaped HTTP URLs are supported, for " "example:

\n" "\n" "
    \n" -"
  • http://domain.com/commit
  • \n" -"
  • http://domain.com/commit?my%20param
  • \n" +"
  • http://domain.com/commit
  • \n" +"
  • http://domain.com/commit?my%20param
  • \n" "
\n" "\n" "

In addition, the URL may contain the following \"%\" notation, which\n" "will be replaced with specific project values for each commit:

\n" "\n" "
    \n" -"
  • %p - project name
  • \n" -"
  • %r - revision number
  • \n" +"
  • %p - project name
  • \n" +"
  • %r - revision number
  • \n" "
\n" "\n" "

For example, committing revision 123 to project 'my-project' with\n" -"post-commit URL http://mydomain.com/%p/%r would send a request to\n" -"http://mydomain.com/my-project/123.

" +"post-commit URL http://mydomain.com/%p/%r would send a request " +"to\n" +"http://mydomain.com/my-project/123.

" msgstr "" #: IDF/gettexttemplates/idf/admin/source.html.php:26 @@ -1097,10 +1300,6 @@ msgstr "" msgid "Repository size:" msgstr "" -#: IDF/gettexttemplates/idf/admin/source.html.php:30 -msgid "Post-commit authentication key:" -msgstr "" - #: IDF/gettexttemplates/idf/admin/summary.html.php:3 #, php-format msgid "" @@ -1121,9 +1320,11 @@ msgstr "" #: IDF/gettexttemplates/idf/admin/summary.html.php:9 #: IDF/gettexttemplates/idf/base-full.html.php:4 +#: IDF/gettexttemplates/idf/base-full.html~.php:4 #: IDF/gettexttemplates/idf/base.html.php:4 -#: IDF/gettexttemplates/idf/index.html.php:6 -#: IDF/gettexttemplates/idf/main-menu.html.php:7 +#: IDF/gettexttemplates/idf/base.html~.php:4 +#: IDF/gettexttemplates/idf/main-menu.html.php:8 +#: IDF/gettexttemplates/idf/project-list.html.php:4 msgid "Project logo" msgstr "" @@ -1133,66 +1334,71 @@ msgstr "" #: IDF/gettexttemplates/idf/admin/tabs.html.php:3 msgid "" -"\n" -"Only project members and admins have write access to the source.
\n" -"If you restrict the access to the source, anonymous access is
\n" -"not provided and the users must authenticate themselves with their
\n" -"password or SSH key." +"This section allows you to configure project tabs access rights and " +"notifications." msgstr "" -#: IDF/gettexttemplates/idf/admin/tabs.html.php:8 +#: IDF/gettexttemplates/idf/admin/tabs.html.php:4 msgid "" -"You can configure here the project tabs access rights and notification " -"emails." +"Tab access controls whether a single user can navigate into a particular " +"section of your project via the main menu or automatically generated object " +"links." msgstr "" -#: IDF/gettexttemplates/idf/admin/tabs.html.php:9 -#, php-format -msgid "" -"Notification emails will be sent from the %%from_email%% " -"address, if you send the email to a mailing list, you may need to register " -"this email address. Multiple email addresses must be separated through " -"commas (','). If you do not want to send emails for a given type of changes, " -"simply leave the corresponding field empty." -msgstr "" - -#: IDF/gettexttemplates/idf/admin/tabs.html.php:10 +#: IDF/gettexttemplates/idf/admin/tabs.html.php:5 msgid "" "If you mark a project as private, only the project members and " "administrators, together with the extra authorized users you provide will " -"have access to the project. You will still be able to define further access " -"rights for the different tabs but the \"Open to all\" and \"Signed in users" -"\" will default to authorized users only." +"have access to the project as a whole. You will still be able to define " +"further access rights for the different tabs but the \"Open to all\" and " +"\"Signed in users\" will default to authorized users only." msgstr "" -#: IDF/gettexttemplates/idf/admin/tabs.html.php:11 +#: IDF/gettexttemplates/idf/admin/tabs.html.php:6 msgid "" -"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." +"For the extra authorized user list, 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." msgstr "" -#: IDF/gettexttemplates/idf/admin/tabs.html.php:12 +#: IDF/gettexttemplates/idf/admin/tabs.html.php:7 +msgid "" +"Only project members and admins have write access to the source. If you " +"restrict the access to the source, anonymous access is not provided and the " +"users must authenticate themselves with their password or SSH key." +msgstr "" + +#: IDF/gettexttemplates/idf/admin/tabs.html.php:8 +#, php-format +msgid "" +"Here you can configure who should be notified about changes in a particular " +"section. You can also configure additional addresses, like the one of a " +"mailing list, that should be notified. (Keep in mind that you might have to " +"register the sender address %%from_email%% to let the " +"mailing list actually accept notification emails.) Multiple email addresses " +"must be separated through commas (',')." +msgstr "" + +#: IDF/gettexttemplates/idf/admin/tabs.html.php:9 msgid "" "The form contains some errors. Please correct them to update the access " "rights." msgstr "" +#: IDF/gettexttemplates/idf/admin/tabs.html.php:10 #: IDF/gettexttemplates/idf/admin/tabs.html.php:13 msgid "Access Rights" msgstr "" +#: IDF/gettexttemplates/idf/admin/tabs.html.php:11 #: IDF/gettexttemplates/idf/admin/tabs.html.php:14 -msgid "Notification Email" -msgstr "" - -#: IDF/gettexttemplates/idf/admin/tabs.html.php:16 -#: IDF/gettexttemplates/idf/gadmin/projects/create.html.php:17 -#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:15 -msgid "Instructions:" +msgid "Notifications" msgstr "" #: IDF/gettexttemplates/idf/base-full.html.php:3 +#: IDF/gettexttemplates/idf/base-full.html~.php:3 #: IDF/gettexttemplates/idf/base.html.php:3 +#: IDF/gettexttemplates/idf/base.html~.php:3 #, php-format msgid "" "Sign in or create your account to create issues or " @@ -1201,20 +1407,117 @@ msgstr "" #: IDF/gettexttemplates/idf/base-full.html.php:6 #: IDF/gettexttemplates/idf/base.html.php:6 +#: IDF/gettexttemplates/idf/project-list.html.php:6 +msgid "External link to project" +msgstr "" + +#: IDF/gettexttemplates/idf/base-full.html.php:7 +#: IDF/gettexttemplates/idf/base-full.html~.php:6 +#: IDF/gettexttemplates/idf/base.html.php:7 +#: IDF/gettexttemplates/idf/base.html~.php:6 msgid "Project Home" msgstr "" -#: IDF/gettexttemplates/idf/base-full.html.php:12 -#: IDF/gettexttemplates/idf/base.html.php:12 +#: IDF/gettexttemplates/idf/base-full.html.php:13 +#: IDF/gettexttemplates/idf/base-full.html~.php:12 +#: IDF/gettexttemplates/idf/base.html.php:13 +#: IDF/gettexttemplates/idf/base.html~.php:12 msgid "Project Management" msgstr "" #: IDF/gettexttemplates/idf/downloads/base.html.php:4 #: IDF/gettexttemplates/idf/downloads/index.html.php:4 -#: IDF/Views/Download.php:234 +#: IDF/Views/Download.php:235 msgid "New Download" msgstr "" +#: IDF/gettexttemplates/idf/downloads/base.html.php:5 +msgid "Upload Archive" +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/create.html.php:3 +msgid "" +"Each file must have a distinct name and file contents\n" +"cannot be changed, so be sure to include release numbers in each file\n" +"name." +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/create.html.php:6 +#, php-format +msgid "" +"You can use the Markdown syntax for the description." +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/create.html.php:7 +msgid "The form contains some errors. Please correct them to submit the file." +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/create.html.php:8 +msgid "Submit File" +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/create.html.php:9 +#: IDF/gettexttemplates/idf/downloads/createFromArchive.html.php:8 +#: IDF/gettexttemplates/idf/downloads/delete.html.php:7 +#: IDF/gettexttemplates/idf/downloads/view.html.php:9 +#: IDF/gettexttemplates/idf/gadmin/projects/delete.html.php:21 +#: IDF/gettexttemplates/idf/gadmin/projects/update.html.php:16 +#: IDF/gettexttemplates/idf/gadmin/users/create.html.php:5 +#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:14 +#: IDF/gettexttemplates/idf/issues/create.html.php:14 +#: IDF/gettexttemplates/idf/issues/view.html.php:27 +#: IDF/gettexttemplates/idf/register/confirmation.html.php:7 +#: IDF/gettexttemplates/idf/register/index.html.php:8 +#: IDF/gettexttemplates/idf/register/inputkey.html.php:5 +#: IDF/gettexttemplates/idf/review/create.html.php:12 +#: IDF/gettexttemplates/idf/review/view.html.php:41 +#: IDF/gettexttemplates/idf/user/changeemail.html.php:5 +#: IDF/gettexttemplates/idf/user/myaccount.html.php:13 +#: IDF/gettexttemplates/idf/user/passrecovery-ask.html.php:5 +#: IDF/gettexttemplates/idf/user/passrecovery-inputkey.html.php:5 +#: IDF/gettexttemplates/idf/user/passrecovery.html.php:7 +#: IDF/gettexttemplates/idf/wiki/createPage.html.php:7 +#: IDF/gettexttemplates/idf/wiki/createResource.html.php:9 +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:7 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:10 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:9 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:9 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:7 +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:5 +msgid "Cancel" +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/create.html.php:10 +#: IDF/gettexttemplates/idf/downloads/createFromArchive.html.php:9 +#: IDF/gettexttemplates/idf/register/inputkey.html.php:6 +#: IDF/gettexttemplates/idf/user/changeemail.html.php:6 +#: IDF/gettexttemplates/idf/user/passrecovery-inputkey.html.php:6 +msgid "Instructions" +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/createFromArchive.html.php:3 +msgid "" +"The archive must include a manifest.xml file with meta " +"information about the\n" +"files to process inside the archive. All processed files must be unique or " +"replace existing files explicitely." +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/createFromArchive.html.php:5 +#, php-format +msgid "" +"You can learn more about the archive format here." +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/createFromArchive.html.php:6 +msgid "" +"The form contains some errors. Please correct them to submit the archive." +msgstr "" + +#: IDF/gettexttemplates/idf/downloads/createFromArchive.html.php:7 +msgid "Submit Archive" +msgstr "" + #: IDF/gettexttemplates/idf/downloads/delete.html.php:3 msgid "" "Attention! If you want to delete a specific version of your " @@ -1233,12 +1536,18 @@ msgstr "" #: IDF/gettexttemplates/idf/downloads/view.html.php:4 #: IDF/gettexttemplates/idf/issues/attachment.html.php:4 #: IDF/gettexttemplates/idf/issues/view.html.php:7 -#: IDF/gettexttemplates/idf/wiki/delete.html.php:7 -#: IDF/gettexttemplates/idf/wiki/delete.html.php:8 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:4 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:5 -#: IDF/gettexttemplates/idf/wiki/view.html.php:8 -#: IDF/gettexttemplates/idf/wiki/view.html.php:9 +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:4 +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:5 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:7 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:8 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:6 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:7 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:6 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:7 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:8 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:9 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:6 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:7 #, php-format msgid "by %%submitter%%" msgstr "" @@ -1247,32 +1556,6 @@ msgstr "" msgid "Delete File" msgstr "" -#: IDF/gettexttemplates/idf/downloads/delete.html.php:7 -#: IDF/gettexttemplates/idf/downloads/submit.html.php:9 -#: IDF/gettexttemplates/idf/downloads/view.html.php:9 -#: IDF/gettexttemplates/idf/gadmin/projects/delete.html.php:21 -#: IDF/gettexttemplates/idf/gadmin/projects/update.html.php:16 -#: IDF/gettexttemplates/idf/gadmin/users/create.html.php:5 -#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:14 -#: IDF/gettexttemplates/idf/issues/create.html.php:14 -#: IDF/gettexttemplates/idf/issues/view.html.php:27 -#: IDF/gettexttemplates/idf/register/confirmation.html.php:7 -#: IDF/gettexttemplates/idf/register/index.html.php:8 -#: IDF/gettexttemplates/idf/register/inputkey.html.php:5 -#: IDF/gettexttemplates/idf/review/create.html.php:12 -#: IDF/gettexttemplates/idf/review/view.html.php:41 -#: IDF/gettexttemplates/idf/user/changeemail.html.php:5 -#: IDF/gettexttemplates/idf/user/myaccount.html.php:13 -#: IDF/gettexttemplates/idf/user/passrecovery-ask.html.php:5 -#: IDF/gettexttemplates/idf/user/passrecovery-inputkey.html.php:5 -#: IDF/gettexttemplates/idf/user/passrecovery.html.php:7 -#: IDF/gettexttemplates/idf/wiki/create.html.php:7 -#: IDF/gettexttemplates/idf/wiki/delete.html.php:10 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:7 -#: IDF/gettexttemplates/idf/wiki/update.html.php:7 -msgid "Cancel" -msgstr "" - #: IDF/gettexttemplates/idf/downloads/delete.html.php:8 #: IDF/gettexttemplates/idf/downloads/view.html.php:14 msgid "Uploaded:" @@ -1282,38 +1565,46 @@ msgstr "" #: IDF/gettexttemplates/idf/downloads/view.html.php:15 #: IDF/gettexttemplates/idf/issues/view.html.php:29 #: IDF/gettexttemplates/idf/review/view.html.php:27 -#: IDF/gettexttemplates/idf/wiki/delete.html.php:12 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:9 -#: IDF/gettexttemplates/idf/wiki/view.html.php:15 +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:9 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:12 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:14 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:14 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:15 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:17 msgid "Updated:" msgstr "" #: IDF/gettexttemplates/idf/downloads/delete.html.php:10 #: IDF/gettexttemplates/idf/downloads/view.html.php:16 #: IDF/gettexttemplates/idf/gadmin/projects/index.html.php:6 -#: IDF/gettexttemplates/idf/index.html.php:15 +#: IDF/gettexttemplates/idf/listProjects.html.php:17 msgid "Downloads:" msgstr "" #: IDF/gettexttemplates/idf/downloads/delete.html.php:11 #: IDF/gettexttemplates/idf/downloads/download-created-email.txt.php:7 +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:7 #: IDF/gettexttemplates/idf/downloads/view.html.php:17 #: IDF/gettexttemplates/idf/issues/feedfragment.xml.php:6 +#: IDF/gettexttemplates/idf/issues/feedfragment.xml~.php:6 #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:9 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:10 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:15 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:11 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:16 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:10 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:15 #: IDF/gettexttemplates/idf/issues/view.html.php:21 #: IDF/gettexttemplates/idf/issues/view.html.php:33 +#: IDF/gettexttemplates/idf/project-list.html.php:8 #: IDF/gettexttemplates/idf/review/feedfragment.xml.php:6 #: IDF/gettexttemplates/idf/review/review-created-email.txt.php:9 #: IDF/gettexttemplates/idf/review/review-updated-email.txt.php:13 -#: IDF/gettexttemplates/idf/wiki/delete.html.php:13 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:10 -#: IDF/gettexttemplates/idf/wiki/view.html.php:16 +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:10 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:13 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:16 #: IDF/gettexttemplates/idf/wiki/wiki-created-email.txt.php:7 #: IDF/gettexttemplates/idf/wiki/wiki-updated-email.txt.php:9 #: IDF/gettexttemplates/idf/wiki/wiki-updated-email.txt.php:12 -#: IDF/IssueComment.php:157 IDF/WikiRevision.php:175 +#: IDF/IssueComment.php:157 IDF/Wiki/PageRevision.php:204 msgid "Labels:" msgstr "" @@ -1322,8 +1613,10 @@ msgid "A new file is available for download:" msgstr "" #: IDF/gettexttemplates/idf/downloads/download-created-email.txt.php:4 +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:4 #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:5 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:5 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:6 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:5 #: IDF/gettexttemplates/idf/review/review-created-email.txt.php:4 #: IDF/gettexttemplates/idf/review/review-updated-email.txt.php:8 #: IDF/gettexttemplates/idf/source/commit-created-email.txt.php:4 @@ -1333,8 +1626,10 @@ msgid "Hello," msgstr "" #: IDF/gettexttemplates/idf/downloads/download-created-email.txt.php:5 +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:5 #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:6 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:6 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:7 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:6 #: IDF/gettexttemplates/idf/review/review-created-email.txt.php:5 #: IDF/gettexttemplates/idf/review/review-updated-email.txt.php:9 #: IDF/gettexttemplates/idf/source/commit-created-email.txt.php:6 @@ -1344,20 +1639,27 @@ msgid "Project:" msgstr "" #: IDF/gettexttemplates/idf/downloads/download-created-email.txt.php:6 +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:6 msgid "Submitted by:" msgstr "" #: IDF/gettexttemplates/idf/downloads/download-created-email.txt.php:8 +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:8 msgid "Download:" msgstr "" #: IDF/gettexttemplates/idf/downloads/download-created-email.txt.php:9 +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:9 #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:10 #: IDF/gettexttemplates/idf/review/view.html.php:31 #: IDF/gettexttemplates/idf/user/public.html.php:4 msgid "Description:" msgstr "" +#: IDF/gettexttemplates/idf/downloads/download-updated-email.txt.php:3 +msgid "A file download was updated:" +msgstr "" + #: IDF/gettexttemplates/idf/downloads/feedfragment.xml.php:3 msgid "Details" msgstr "" @@ -1371,34 +1673,6 @@ msgstr "" msgid "Number of files:" msgstr "" -#: IDF/gettexttemplates/idf/downloads/submit.html.php:3 -msgid "" -"Each file must have a distinct name and file contents\n" -"cannot be changed, so be sure to include release numbers in each file\n" -"name." -msgstr "" - -#: IDF/gettexttemplates/idf/downloads/submit.html.php:6 -#, php-format -msgid "" -"You can use the Markdown syntax for the description." -msgstr "" - -#: IDF/gettexttemplates/idf/downloads/submit.html.php:7 -msgid "The form contains some errors. Please correct them to submit the file." -msgstr "" - -#: IDF/gettexttemplates/idf/downloads/submit.html.php:8 -msgid "Submit File" -msgstr "" - -#: IDF/gettexttemplates/idf/downloads/submit.html.php:10 -#: IDF/gettexttemplates/idf/register/inputkey.html.php:6 -#: IDF/gettexttemplates/idf/user/changeemail.html.php:6 -#: IDF/gettexttemplates/idf/user/passrecovery-inputkey.html.php:6 -msgid "Instructions" -msgstr "" - #: IDF/gettexttemplates/idf/downloads/view.html.php:3 msgid "" "Attention! This file is marked as deprecated, download it " @@ -1428,8 +1702,10 @@ msgstr "" #: IDF/gettexttemplates/idf/downloads/view.html.php:11 #: IDF/gettexttemplates/idf/gadmin/projects/update.html.php:18 -#: IDF/gettexttemplates/idf/wiki/update.html.php:9 -#: IDF/gettexttemplates/idf/wiki/view.html.php:12 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:9 +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:7 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:12 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:12 msgid "Trash" msgstr "" @@ -1438,15 +1714,17 @@ msgid "Delete this file" msgstr "" #: IDF/gettexttemplates/idf/faq-api.html.php:3 -#: IDF/gettexttemplates/idf/faq.html.php:34 +#: IDF/gettexttemplates/idf/faq-archive-format.html.php:3 +#: IDF/gettexttemplates/idf/faq.html.php:65 msgid "Here we are, just to help you." msgstr "" #: IDF/gettexttemplates/idf/faq-api.html.php:4 -#: IDF/gettexttemplates/idf/faq.html.php:35 -#: IDF/gettexttemplates/idf/gadmin/base.html.php:3 -#: IDF/gettexttemplates/idf/index.html.php:3 IDF/Views/Admin.php:57 -#: IDF/Views.php:47 +#: IDF/gettexttemplates/idf/faq-archive-format.html.php:4 +#: IDF/gettexttemplates/idf/faq.html.php:66 +#: IDF/gettexttemplates/idf/gadmin/base.html.php:4 +#: IDF/gettexttemplates/idf/index.html.php:4 IDF/Views/Admin.php:77 +#: IDF/Views.php:91 msgid "Projects" msgstr "" @@ -1454,137 +1732,223 @@ msgstr "" msgid "" "

This is simple:

\n" "
    \n" -"
  1. Write in the comments \"This is a duplicate of issue 123\", change 123 " +"
  2. Write in the comments \"This is a duplicate of issue 123\" or - if you " +"are a member of the crew -\n" +"directly add the \"duplicates\" relation with the value \"123\" below the " +"comment field. Change \"123\"\n" "with the corresponding issue number.
  3. \n" "
  4. Change the status of the current issue to Duplicate.
  5. \n" "
  6. Submit the changes.
  7. \n" "
" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:9 +#: IDF/gettexttemplates/idf/faq.html.php:11 msgid "" "You need to create an account on Gravatar, this takes about 5 minutes and is free." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:10 +#: IDF/gettexttemplates/idf/faq.html.php:12 +msgid "" +"\n" +"

To embed any previously uploaded resource into your wiki page, you can " +"use the [[!ResourceName]] syntax.

\n" +"\n" +"

The rendering of the resource can then be further fine-tuned:\n" +"

    \n" +"
  • [[!ImageResource, align=right, width=200]] renders " +"\"ImageResource\" right-aligned and scale its width to 200
  • \n" +"
  • [[!TextResource, align=center, width=300, height=300]] " +"renders \"TextResource\" in a centered, 300 by 300 px iframe
  • \n" +"
  • [[!AnyResource, preview=no]] does not render a preview of " +"the resource, but only provides a download link (default for binary " +"resources)
  • \n" +"
  • [[!BinaryResource, title=Download]] renders the download " +"link of \"BinaryResource\" with an alternative title
  • \n" +"
\n" +"

\n" +"\n" +"Resources are versioned, just like wiki pages. If you update a resource, old " +"wiki pages still show the state of the resource\n" +"at the time when the wiki page was edited. If you specifically want to " +"update a resource on a page, you therefor need to update\n" +"the resource at first and then also the page where it is referenced, " +"otherwise the change won't be visible until the next regular edit. \n" +msgstr "" + +#: IDF/gettexttemplates/idf/faq.html.php:28 +msgid "" +"

If you have to publish many files at once for a new release, it is a very " +"tedious task\n" +"to upload them one after another and enter meta information like a summary, " +"a description or additional\n" +"labels for each of them.

\n" +"

InDefero therefore supports a special archive format that is basically a " +"standard zip file which comes with\n" +"some meta information. These meta information are kept in a special manifest " +"file, which is distinctly kept from\n" +"the rest of the files in the archive that should be published.

\n" +"

Once this archive has been uploaded, InDefero reads in the meta " +"information, unpacks the other files from\n" +"the archive and creates new individual downloads for each of them.

" +msgstr "" + +#: IDF/gettexttemplates/idf/faq.html.php:36 +#, php-format +msgid "Learn more about the archive format." +msgstr "" + +#: IDF/gettexttemplates/idf/faq.html.php:37 msgid "" "The API (Application Programming Interface) is used to interact with " "InDefero with another program. For example, this can be used to create a " "desktop program to submit new tickets easily." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:11 +#: IDF/gettexttemplates/idf/faq.html.php:38 #, php-format msgid "Learn more about the API." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:12 -#: IDF/gettexttemplates/idf/faq.html.php:16 +#: IDF/gettexttemplates/idf/faq.html.php:39 +#: IDF/gettexttemplates/idf/faq.html.php:45 msgid "What are the keyboard shortcuts?" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:13 -#: IDF/gettexttemplates/idf/faq.html.php:31 +#: IDF/gettexttemplates/idf/faq.html.php:40 +#: IDF/gettexttemplates/idf/faq.html.php:60 msgid "How to mark an issue as duplicate?" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:14 -#: IDF/gettexttemplates/idf/faq.html.php:32 +#: IDF/gettexttemplates/idf/faq.html.php:41 +#: IDF/gettexttemplates/idf/faq.html.php:61 msgid "How can I display my head next to my comments?" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:15 -#: IDF/gettexttemplates/idf/faq.html.php:33 +#: IDF/gettexttemplates/idf/faq.html.php:42 +#: IDF/gettexttemplates/idf/faq.html.php:62 +msgid "How can I embed images and other resources in my documentation pages?" +msgstr "" + +#: IDF/gettexttemplates/idf/faq.html.php:43 +#: IDF/gettexttemplates/idf/faq.html.php:63 +msgid "What is this \"Upload Archive\" functionality about?" +msgstr "" + +#: IDF/gettexttemplates/idf/faq.html.php:44 +#: IDF/gettexttemplates/idf/faq.html.php:64 msgid "What is the API and how is it used?" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:17 +#: IDF/gettexttemplates/idf/faq.html.php:46 msgid "Shift+h: This help page." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:18 +#: IDF/gettexttemplates/idf/faq.html.php:47 msgid "If you are in a project, you have the following shortcuts:" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:19 +#: IDF/gettexttemplates/idf/faq.html.php:48 msgid "Shift+u: Project updates." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:20 +#: IDF/gettexttemplates/idf/faq.html.php:49 msgid "Shift+d: Downloads." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:21 +#: IDF/gettexttemplates/idf/faq.html.php:50 msgid "Shift+o: Documentation." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:22 +#: IDF/gettexttemplates/idf/faq.html.php:51 msgid "Shift+a: Create a new issue." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:23 +#: IDF/gettexttemplates/idf/faq.html.php:52 msgid "Shift+i: List of open issues." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:24 +#: IDF/gettexttemplates/idf/faq.html.php:53 msgid "Shift+m: The issues you submitted." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:25 +#: IDF/gettexttemplates/idf/faq.html.php:54 msgid "Shift+w: The issues assigned to you." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:26 +#: IDF/gettexttemplates/idf/faq.html.php:55 msgid "Shift+s: Source." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:27 +#: IDF/gettexttemplates/idf/faq.html.php:56 msgid "You also have the standard access keys:" msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:28 +#: IDF/gettexttemplates/idf/faq.html.php:57 msgid "Alt+1: Home." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:29 +#: IDF/gettexttemplates/idf/faq.html.php:58 msgid "Alt+2: Skip the menus." msgstr "" -#: IDF/gettexttemplates/idf/faq.html.php:30 +#: IDF/gettexttemplates/idf/faq.html.php:59 msgid "Alt+4: Search (when available)." msgstr "" -#: IDF/gettexttemplates/idf/gadmin/base.html.php:4 -msgid "People" +#: IDF/gettexttemplates/idf/gadmin/base.html.php:3 +msgid "Forge" msgstr "" #: IDF/gettexttemplates/idf/gadmin/base.html.php:5 +msgid "People" +msgstr "" + +#: IDF/gettexttemplates/idf/gadmin/base.html.php:6 msgid "Usher" msgstr "" -#: IDF/gettexttemplates/idf/gadmin/home.html.php:3 -msgid "You have here access to the administration of the forge." +#: IDF/gettexttemplates/idf/gadmin/forge/base.html.php:3 +msgid "Frontpage" msgstr "" -#: IDF/gettexttemplates/idf/gadmin/home.html.php:4 -#: IDF/gettexttemplates/idf/project/home.html.php:3 -#: IDF/gettexttemplates/idf/project/timeline.html.php:4 -msgid "Welcome" +#: IDF/gettexttemplates/idf/gadmin/forge/index.html.php:3 +#, php-format +msgid "" +"\n" +"

Instructions:

\n" +"

You can set up a custom forge page that is used as entry page for the " +"forge instead of the plain project listing. This page is then also " +"accessible via the 'Home' link in main menu bar.

\n" +"

The content of the page can use the Markdown syntax with the Extra extension.

\n" +"

Additionally, the following macros are available:
\n" +"

    \n" +"
  • {projectlist, label=..., order=(name|activity), limit=...} - Renders a project list that can optionally be filtered by label, " +"ordered by 'name' or 'activity' and / or limited to a specific number of " +"projects.
  • \n" +"
\n" +"

\n" msgstr "" #: IDF/gettexttemplates/idf/gadmin/projects/base.html.php:3 -#: IDF/gettexttemplates/idf/main-menu.html.php:6 +#: IDF/gettexttemplates/idf/main-menu.html.php:7 msgid "Project List" msgstr "" #: IDF/gettexttemplates/idf/gadmin/projects/base.html.php:4 -#: IDF/gettexttemplates/idf/gadmin/projects/create.html.php:16 -#: IDF/gettexttemplates/idf/index.html.php:5 IDF/Views/Admin.php:128 -msgid "Create Project" +#: IDF/Views/Admin.php:110 +msgid "Project Labels" msgstr "" #: IDF/gettexttemplates/idf/gadmin/projects/base.html.php:5 +#: IDF/gettexttemplates/idf/gadmin/projects/create.html.php:16 +#: IDF/gettexttemplates/idf/listProjects.html.php:6 IDF/Views/Admin.php:186 +msgid "Create Project" +msgstr "" + +#: IDF/gettexttemplates/idf/gadmin/projects/base.html.php:6 msgid "Change Project Details" msgstr "" @@ -1617,6 +1981,11 @@ msgstr "" msgid "Provide at least one owner for the project or use a template." msgstr "" +#: IDF/gettexttemplates/idf/gadmin/projects/create.html.php:17 +#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:15 +msgid "Instructions:" +msgstr "" + #: IDF/gettexttemplates/idf/gadmin/projects/delete.html.php:3 #, php-format msgid "" @@ -1682,7 +2051,8 @@ msgstr "" #: IDF/gettexttemplates/idf/gadmin/projects/index.html.php:5 #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:11 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:17 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:18 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:16 msgid "Attachments:" msgstr "" @@ -1718,7 +2088,7 @@ msgid "You will be asked to confirm." msgstr "" #: IDF/gettexttemplates/idf/gadmin/users/base.html.php:3 -#: IDF/Views/Admin.php:201 +#: IDF/Views/Admin.php:264 msgid "User List" msgstr "" @@ -1809,15 +2179,17 @@ msgid "Configured servers" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/base.html.php:4 -#: IDF/Views/Admin.php:358 +#: IDF/Views/Admin.php:421 msgid "Usher control" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/connections.html.php:3 +#: IDF/gettexttemplates/idf/gadmin/usher/connections.html~.php:3 msgid "address" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/connections.html.php:4 +#: IDF/gettexttemplates/idf/gadmin/usher/connections.html~.php:4 msgid "port" msgstr "" @@ -1847,6 +2219,7 @@ msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/control.html.php:8 #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:11 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:11 msgid "Status explanation" msgstr "" @@ -1869,63 +2242,78 @@ msgid "" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:3 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:3 msgid "server name" msgstr "" -#: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:4 IDF/Issue.php:99 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:4 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:4 IDF/Issue.php:99 #: IDF/Review.php:102 msgid "status" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:5 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:5 msgid "action" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:6 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:6 msgid "No monotone servers configured." msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:7 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:7 msgid "stop" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:8 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:8 msgid "start" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:9 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:9 msgid "kill" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:10 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:10 msgid "active connections" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:12 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:12 msgid "remote server without open connections" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:13 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:13 msgid "server with n open connections" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:14 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:14 msgid "local server running, without open connections" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:15 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:15 msgid "local server not running, waiting for connections" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:16 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:16 msgid "local server is about to stop, n connections still open" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:17 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:17 msgid "local server not running, not accepting connections" msgstr "" #: IDF/gettexttemplates/idf/gadmin/usher/index.html.php:18 +#: IDF/gettexttemplates/idf/gadmin/usher/index.html~.php:18 msgid "usher is shut down, not running and not accepting connections" msgstr "" @@ -1934,36 +2322,9 @@ msgstr "" msgid "Personal project feed for %%user%%." msgstr "" -#: IDF/gettexttemplates/idf/index.html.php:4 -msgid "No projects managed with InDefero were found." -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:9 -msgid "Forge statistics" -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:10 -msgid "Projects:" -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:11 -msgid "Members:" -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:12 -msgid "Issues:" -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:13 -msgid "Commits:" -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:14 -msgid "Documentations:" -msgstr "" - -#: IDF/gettexttemplates/idf/index.html.php:16 -msgid "Code reviews:" +#: IDF/gettexttemplates/idf/index.html.php:3 +#: IDF/gettexttemplates/idf/main-menu.html.php:6 +msgid "Home" msgstr "" #: IDF/gettexttemplates/idf/issues/attachment.html.php:3 @@ -1974,6 +2335,7 @@ msgstr "" #: IDF/gettexttemplates/idf/issues/attachment.html.php:5 #: IDF/gettexttemplates/idf/review/view.html.php:35 #: IDF/gettexttemplates/idf/source/commit.html.php:23 +#: IDF/gettexttemplates/idf/source/commit.html~.php:22 #: IDF/gettexttemplates/idf/source/git/file.html.php:6 #: IDF/gettexttemplates/idf/source/git/tree.html.php:11 #: IDF/gettexttemplates/idf/source/mercurial/file.html.php:6 @@ -1989,15 +2351,21 @@ msgstr "" #: IDF/gettexttemplates/idf/source/mercurial/file.html.php:7 #: IDF/gettexttemplates/idf/source/mtn/file.html.php:8 #: IDF/gettexttemplates/idf/source/svn/file.html.php:8 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:12 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:12 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:10 msgid "Download this file" msgstr "" #: IDF/gettexttemplates/idf/issues/attachment.html.php:7 #: IDF/gettexttemplates/idf/issues/view.html.php:28 #: IDF/gettexttemplates/idf/review/view.html.php:26 -#: IDF/gettexttemplates/idf/wiki/delete.html.php:11 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:8 -#: IDF/gettexttemplates/idf/wiki/view.html.php:14 +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:8 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:11 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:13 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:13 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:14 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:16 msgid "Created:" msgstr "" @@ -2023,7 +2391,7 @@ msgid "New Issue" msgstr "" #: IDF/gettexttemplates/idf/issues/base.html.php:8 -#: IDF/gettexttemplates/idf/wiki/base.html.php:6 +#: IDF/gettexttemplates/idf/wiki/base.html.php:9 msgid "Search" msgstr "" @@ -2068,8 +2436,8 @@ msgstr "" #: IDF/gettexttemplates/idf/issues/create.html.php:13 #: IDF/gettexttemplates/idf/issues/view.html.php:24 #: IDF/gettexttemplates/idf/issues/view.html.php:26 -#: IDF/gettexttemplates/idf/wiki/create.html.php:5 -#: IDF/gettexttemplates/idf/wiki/update.html.php:5 +#: IDF/gettexttemplates/idf/wiki/createPage.html.php:5 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:5 msgid "Preview" msgstr "" @@ -2090,7 +2458,9 @@ msgid "Attach another file" msgstr "" #: IDF/gettexttemplates/idf/issues/feedfragment.xml.php:3 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:12 +#: IDF/gettexttemplates/idf/issues/feedfragment.xml~.php:3 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:13 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:12 #: IDF/gettexttemplates/idf/issues/view.html.php:18 #: IDF/gettexttemplates/idf/review/feedfragment.xml.php:3 #: IDF/gettexttemplates/idf/review/view.html.php:38 @@ -2101,9 +2471,12 @@ msgid "Summary:" msgstr "" #: IDF/gettexttemplates/idf/issues/feedfragment.xml.php:4 +#: IDF/gettexttemplates/idf/issues/feedfragment.xml~.php:4 #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:7 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:7 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:13 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:8 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:14 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:7 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:13 #: IDF/gettexttemplates/idf/issues/view.html.php:19 #: IDF/gettexttemplates/idf/issues/view.html.php:30 #: IDF/gettexttemplates/idf/review/feedfragment.xml.php:4 @@ -2114,7 +2487,9 @@ msgid "Status:" msgstr "" #: IDF/gettexttemplates/idf/issues/feedfragment.xml.php:5 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:14 +#: IDF/gettexttemplates/idf/issues/feedfragment.xml~.php:5 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:15 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:14 #: IDF/gettexttemplates/idf/issues/view.html.php:20 #: IDF/gettexttemplates/idf/issues/view.html.php:31 #: IDF/gettexttemplates/idf/review/feedfragment.xml.php:5 @@ -2123,7 +2498,7 @@ msgid "Owner:" msgstr "" #: IDF/gettexttemplates/idf/issues/feedfragment.xml.php:7 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:16 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:17 #: IDF/gettexttemplates/idf/issues/view.html.php:22 IDF/IssueComment.php:159 msgid "Relations:" msgstr "" @@ -2139,40 +2514,52 @@ msgid "" msgstr "" #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:3 -msgid "" -"A new issue has been created and assigned\n" -"to you:" +msgid "A new issue has been created and assigned to you:" +msgstr "" + +#: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:4 +msgid "A new issue has been created:" msgstr "" #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:8 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:8 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:9 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:8 #: IDF/gettexttemplates/idf/review/review-created-email.txt.php:7 #: IDF/gettexttemplates/idf/review/review-updated-email.txt.php:11 msgid "Reported by:" msgstr "" #: IDF/gettexttemplates/idf/issues/issue-created-email.txt.php:12 -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:18 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:19 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:17 msgid "Issue:" msgstr "" #: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:3 -msgid "The following issue has been updated:" +msgid "The following issue you are owning has been updated:" msgstr "" #: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:4 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:3 +msgid "The following issue has been updated:" +msgstr "" + +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:5 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:4 #: IDF/gettexttemplates/idf/review/review-updated-email.txt.php:4 #, php-format msgid "By %%who%%, %%c.creation_dtime%%:" msgstr "" -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:9 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:10 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:9 #: IDF/gettexttemplates/idf/review/review-created-email.txt.php:8 #: IDF/gettexttemplates/idf/review/review-updated-email.txt.php:12 msgid "URL:" msgstr "" -#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:11 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt.php:12 +#: IDF/gettexttemplates/idf/issues/issue-updated-email.txt~.php:11 msgid "Comments (last first):" msgstr "" @@ -2195,25 +2582,43 @@ msgid "" msgstr "" #: IDF/gettexttemplates/idf/issues/summary.html.php:3 -#, php-format -msgid "" -"The issue tracker is empty.
You can create your first issue here." +msgid "View all open issues." msgstr "" #: IDF/gettexttemplates/idf/issues/summary.html.php:4 +msgid "Create an issue." +msgstr "" + +#: IDF/gettexttemplates/idf/issues/summary.html.php:5 +#, php-format +msgid "" +"The issue tracker is empty.
Create your " +"first issue." +msgstr "" + +#: IDF/gettexttemplates/idf/issues/summary.html.php:6 +#: IDF/gettexttemplates/idf/issues/summary.html~.php:4 #, php-format msgid "Unresolved: By %%key%%" msgstr "" -#: IDF/gettexttemplates/idf/issues/summary.html.php:5 +#: IDF/gettexttemplates/idf/issues/summary.html.php:7 +#: IDF/gettexttemplates/idf/issues/summary.html~.php:5 msgid "Status Summary" msgstr "" -#: IDF/gettexttemplates/idf/issues/summary.html.php:6 +#: IDF/gettexttemplates/idf/issues/summary.html.php:8 +#: IDF/gettexttemplates/idf/issues/summary.html~.php:6 msgid "Unresolved: By Assignee" msgstr "" +#: IDF/gettexttemplates/idf/issues/summary.html~.php:3 +#, php-format +msgid "" +"The issue tracker is empty.
You can create your first issue here." +msgstr "" + #: IDF/gettexttemplates/idf/issues/userIssues.html.php:3 #, php-format msgid "" @@ -2286,19 +2691,19 @@ msgid "Add this issue to your watch list" msgstr "" #: IDF/gettexttemplates/idf/issues/view.html.php:12 -msgid "Click here to view the previous closed issue" +msgid "View the previous closed issue" msgstr "" #: IDF/gettexttemplates/idf/issues/view.html.php:13 -msgid "Click here to view the previous open issue" +msgid "View the previous open issue" msgstr "" #: IDF/gettexttemplates/idf/issues/view.html.php:14 -msgid "Click here to view the next closed issue" +msgid "View the next closed issue" msgstr "" #: IDF/gettexttemplates/idf/issues/view.html.php:15 -msgid "Click here to view the next open issue" +msgid "View the next open issue" msgstr "" #: IDF/gettexttemplates/idf/issues/view.html.php:16 @@ -2321,11 +2726,70 @@ msgstr "" msgid "Followed by:" msgstr "" +#: IDF/gettexttemplates/idf/listProjects.html.php:3 +#, php-format +msgid "1 project" +msgid_plural "%%label.project_count%% projects" +msgstr[0] "" +msgstr[1] "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:4 +#, php-format +msgid "Remove filter for %%tag%%" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:5 +msgid "No projects managed with InDefero were found." +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:7 +msgid "Filter projects by label" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:8 +msgid "No projects with labels found." +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:9 +msgid "Order" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:10 +msgid "By name" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:11 +msgid "By activity" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:12 +msgid "Filtered project stats" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:13 +msgid "Members:" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:14 +msgid "Issues:" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:15 +msgid "Commits:" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:16 +msgid "Documentations:" +msgstr "" + +#: IDF/gettexttemplates/idf/listProjects.html.php:18 +msgid "Code reviews:" +msgstr "" + #: IDF/gettexttemplates/idf/login_form.html.php:3 #, php-format msgid "" -"If you don't have an account yet, you can create one here." +"You can create an account if you don't have one yet." msgstr "" #: IDF/gettexttemplates/idf/login_form.html.php:4 @@ -2370,15 +2834,15 @@ msgstr "" msgid "Sign in or create your account" msgstr "" -#: IDF/gettexttemplates/idf/main-menu.html.php:9 IDF/Views/Admin.php:42 +#: IDF/gettexttemplates/idf/main-menu.html.php:10 IDF/Views/Admin.php:40 msgid "Forge Management" msgstr "" -#: IDF/gettexttemplates/idf/main-menu.html.php:10 +#: IDF/gettexttemplates/idf/main-menu.html.php:11 msgid "Help and accessibility features" msgstr "" -#: IDF/gettexttemplates/idf/main-menu.html.php:11 +#: IDF/gettexttemplates/idf/main-menu.html.php:12 #: IDF/gettexttemplates/idf/source/git/tree.html.php:14 #: IDF/gettexttemplates/idf/source/mercurial/tree.html.php:14 #: IDF/gettexttemplates/idf/source/mtn/tree.html.php:15 @@ -2386,6 +2850,11 @@ msgstr "" msgid "Help" msgstr "" +#: IDF/gettexttemplates/idf/project/home.html.php:3 +#: IDF/gettexttemplates/idf/project/timeline.html.php:4 IDF/Views.php:46 +msgid "Welcome" +msgstr "" + #: IDF/gettexttemplates/idf/project/home.html.php:4 #: IDF/gettexttemplates/idf/project/timeline.html.php:5 msgid "Latest Updates" @@ -2396,23 +2865,31 @@ msgid "Featured Downloads" msgstr "" #: IDF/gettexttemplates/idf/project/home.html.php:6 -#: IDF/gettexttemplates/idf/project/home.html.php:8 -msgid "show more..." +msgid "Show more featured downloads" msgstr "" #: IDF/gettexttemplates/idf/project/home.html.php:7 +#: IDF/gettexttemplates/idf/project/home.html.php:10 +msgid "show more..." +msgstr "" + +#: IDF/gettexttemplates/idf/project/home.html.php:8 msgid "Featured Documentation" msgstr "" #: IDF/gettexttemplates/idf/project/home.html.php:9 -msgid "Development Team" -msgstr "" - -#: IDF/gettexttemplates/idf/project/home.html.php:10 -msgid "Admins" +msgid "Show more featured documentation" msgstr "" #: IDF/gettexttemplates/idf/project/home.html.php:11 +msgid "Development Team" +msgstr "" + +#: IDF/gettexttemplates/idf/project/home.html.php:12 +msgid "Admins" +msgstr "" + +#: IDF/gettexttemplates/idf/project/home.html.php:13 msgid "Happy Crew" msgstr "" @@ -2441,6 +2918,14 @@ msgstr "" msgid "Atom feed" msgstr "" +#: IDF/gettexttemplates/idf/project-list.html.php:3 +msgid "Project activity: %%activity%%%" +msgstr "" + +#: IDF/gettexttemplates/idf/project-list.html.php:9 +msgid "n/a" +msgstr "" + #: IDF/gettexttemplates/idf/register/confirmation-email.txt.php:3 #, php-format msgid "" @@ -2495,16 +2980,16 @@ msgstr "" #: IDF/gettexttemplates/idf/register/index.html.php:3 msgid "" -"Read the terms and conditions " -"– basically \"Please be nice, we respect you\"." +"Read the terms and conditions – " +"basically \"Please be nice, we respect you\"." msgstr "" #: IDF/gettexttemplates/idf/register/index.html.php:4 #, php-format msgid "" "If you have just forgotten your login information, then there is no need to " -"create a new account. Just go here to recover your " -"login name and password." +"create a new account. You can just recover your login " +"name and password." msgstr "" #: IDF/gettexttemplates/idf/register/index.html.php:5 @@ -2520,7 +3005,7 @@ msgstr "" msgid "Oops, please check the provided login and email address to register." msgstr "" -#: IDF/gettexttemplates/idf/register/index.html.php:7 IDF/Views.php:90 +#: IDF/gettexttemplates/idf/register/index.html.php:7 IDF/Views.php:137 msgid "Create Your Account" msgstr "" @@ -2643,6 +3128,7 @@ msgstr "" #: IDF/gettexttemplates/idf/review/view.html.php:18 #: IDF/gettexttemplates/idf/source/commit.html.php:3 +#: IDF/gettexttemplates/idf/source/commit.html~.php:3 #, php-format msgid "%%ndiff%% diff" msgid_plural "%%ndiff%% diffs" @@ -2684,17 +3170,20 @@ msgstr "" #: IDF/gettexttemplates/idf/review/view.html.php:28 #: IDF/gettexttemplates/idf/source/commit.html.php:5 +#: IDF/gettexttemplates/idf/source/commit.html~.php:5 msgid "Author:" msgstr "" #: IDF/gettexttemplates/idf/review/view.html.php:29 #: IDF/gettexttemplates/idf/source/commit-created-email.txt.php:5 #: IDF/gettexttemplates/idf/source/commit.html.php:7 +#: IDF/gettexttemplates/idf/source/commit.html~.php:7 msgid "Commit:" msgstr "" #: IDF/gettexttemplates/idf/review/view.html.php:30 #: IDF/gettexttemplates/idf/source/commit.html.php:8 +#: IDF/gettexttemplates/idf/source/commit.html~.php:8 msgid "View corresponding source tree" msgstr "" @@ -2712,6 +3201,7 @@ msgstr "" #: IDF/gettexttemplates/idf/review/view.html.php:36 #: IDF/gettexttemplates/idf/source/commit.html.php:24 +#: IDF/gettexttemplates/idf/source/commit.html~.php:23 msgid "Download the corresponding diff file" msgstr "" @@ -2740,7 +3230,7 @@ msgstr "" #: IDF/gettexttemplates/idf/source/mercurial/tree.html.php:7 #: IDF/gettexttemplates/idf/source/mtn/tree.html.php:8 #: IDF/gettexttemplates/idf/source/svn/tree.html.php:8 -#: IDF/Views/Project.php:188 +#: IDF/Views/Project.php:190 msgid "Age" msgstr "" @@ -2758,6 +3248,7 @@ msgstr "" #: IDF/gettexttemplates/idf/source/changelog.html.php:6 #: IDF/gettexttemplates/idf/source/commit.html.php:10 +#: IDF/gettexttemplates/idf/source/commit.html~.php:10 msgid "View corresponding commit" msgstr "" @@ -2789,37 +3280,47 @@ msgid "Commit details:" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:4 +#: IDF/gettexttemplates/idf/source/commit.html~.php:4 msgid "Date:" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:6 +#: IDF/gettexttemplates/idf/source/commit.html~.php:6 msgid "Branch:" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:9 +#: IDF/gettexttemplates/idf/source/commit.html~.php:9 msgid "Parents:" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:11 +#: IDF/gettexttemplates/idf/source/commit.html~.php:11 msgid "Message:" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:12 -#: IDF/gettexttemplates/idf/wiki/feedfragment.xml.php:3 +#: IDF/gettexttemplates/idf/source/commit.html~.php:12 +#: IDF/gettexttemplates/idf/wiki/feedfragment-page.xml.php:3 +#: IDF/gettexttemplates/idf/wiki/feedfragment-resource.xml.php:3 #: IDF/gettexttemplates/idf/wiki/wiki-updated-email.txt.php:10 msgid "Changes:" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:13 +#: IDF/gettexttemplates/idf/source/commit.html~.php:13 msgid "deleted" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:14 #: IDF/gettexttemplates/idf/source/commit.html.php:18 +#: IDF/gettexttemplates/idf/source/commit.html~.php:14 +#: IDF/gettexttemplates/idf/source/commit.html~.php:17 msgid "full" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:15 +#: IDF/gettexttemplates/idf/source/commit.html~.php:15 msgid "renamed" msgstr "" @@ -2828,25 +3329,33 @@ msgid "copied" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:17 +#: IDF/gettexttemplates/idf/source/commit.html~.php:16 msgid "added" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:19 +#: IDF/gettexttemplates/idf/source/commit.html~.php:18 msgid "modified" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:20 -msgid "properies changed" +msgid "properties changed" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:21 +#: IDF/gettexttemplates/idf/source/commit.html~.php:20 msgid "removed" msgstr "" #: IDF/gettexttemplates/idf/source/commit.html.php:22 +#: IDF/gettexttemplates/idf/source/commit.html~.php:21 msgid "File differences" msgstr "" +#: IDF/gettexttemplates/idf/source/commit.html~.php:19 +msgid "properies changed" +msgstr "" + #: IDF/gettexttemplates/idf/source/disambiguate_revision.html.php:3 #, php-format msgid "" @@ -2986,7 +3495,7 @@ msgstr "" #: IDF/gettexttemplates/idf/source/mercurial/tree.html.php:9 #: IDF/gettexttemplates/idf/source/mtn/tree.html.php:10 #: IDF/gettexttemplates/idf/source/svn/tree.html.php:11 -#: IDF/Views/Download.php:66 IDF/Views/Download.php:314 +#: IDF/Views/Download.php:66 IDF/Views/Download.php:348 msgid "Size" msgstr "" @@ -3010,6 +3519,7 @@ msgid "or" msgstr "" #: IDF/gettexttemplates/idf/source/invalid_revision.html.php:3 +#: IDF/gettexttemplates/idf/source/invalid_revision.html~.php:3 #, php-format msgid "" "The branch or revision %%commit%% is not valid or does not exist\n" @@ -3017,12 +3527,16 @@ msgid "" msgstr "" #: IDF/gettexttemplates/idf/source/invalid_revision.html.php:5 +#: IDF/gettexttemplates/idf/source/invalid_revision.html~.php:5 #: IDF/gettexttemplates/idf/source/svn/invalid_revision.html.php:5 +#: IDF/gettexttemplates/idf/source/svn/invalid_revision.html~.php:5 msgid "The following list shows all available branches:" msgstr "" #: IDF/gettexttemplates/idf/source/invalid_revision.html.php:6 +#: IDF/gettexttemplates/idf/source/invalid_revision.html~.php:6 #: IDF/gettexttemplates/idf/source/svn/invalid_revision.html.php:6 +#: IDF/gettexttemplates/idf/source/svn/invalid_revision.html~.php:6 #, php-format msgid "" "If this is a new repository, the reason for this error\n" @@ -3070,6 +3584,7 @@ msgstr "" #: IDF/gettexttemplates/idf/source/svn/changelog.html.php:3 #: IDF/gettexttemplates/idf/source/svn/commit.html.php:3 +#: IDF/gettexttemplates/idf/source/svn/commit.html~.php:3 #: IDF/gettexttemplates/idf/source/svn/file.html.php:9 #: IDF/gettexttemplates/idf/source/svn/tree.html.php:14 msgid "Revision:" @@ -3077,6 +3592,7 @@ msgstr "" #: IDF/gettexttemplates/idf/source/svn/changelog.html.php:4 #: IDF/gettexttemplates/idf/source/svn/commit.html.php:4 +#: IDF/gettexttemplates/idf/source/svn/commit.html~.php:4 #: IDF/gettexttemplates/idf/source/svn/file.html.php:10 #: IDF/gettexttemplates/idf/source/svn/tree.html.php:15 msgid "Switch" @@ -3091,6 +3607,7 @@ msgid "" msgstr "" #: IDF/gettexttemplates/idf/source/svn/invalid_revision.html.php:3 +#: IDF/gettexttemplates/idf/source/svn/invalid_revision.html~.php:3 #, php-format msgid "" "The revision %%commit%% is not valid or does not exist\n" @@ -3313,29 +3830,76 @@ msgid "List Pages" msgstr "" #: IDF/gettexttemplates/idf/wiki/base.html.php:4 -#: IDF/gettexttemplates/idf/wiki/index.html.php:4 -#: IDF/gettexttemplates/idf/wiki/search.html.php:3 IDF/Views/Wiki.php:173 -msgid "New Page" +msgid "List Resources" msgstr "" #: IDF/gettexttemplates/idf/wiki/base.html.php:5 +#: IDF/gettexttemplates/idf/wiki/listPages.html.php:4 +#: IDF/gettexttemplates/idf/wiki/search.html.php:3 IDF/Views/Wiki.php:211 +msgid "New Page" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/base.html.php:6 +#: IDF/gettexttemplates/idf/wiki/listResources.html.php:3 +#: IDF/Views/Wiki.php:256 +msgid "New Resource" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/base.html.php:7 msgid "Update This Page" msgstr "" -#: IDF/gettexttemplates/idf/wiki/create.html.php:3 -#: IDF/gettexttemplates/idf/wiki/update.html.php:3 +#: IDF/gettexttemplates/idf/wiki/base.html.php:8 +msgid "Update This Resource" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/createPage.html.php:3 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:3 msgid "Preview of the Page" msgstr "" -#: IDF/gettexttemplates/idf/wiki/create.html.php:4 +#: IDF/gettexttemplates/idf/wiki/createPage.html.php:4 +#: IDF/gettexttemplates/idf/wiki/createResource.html.php:7 msgid "The form contains some errors. Please correct them to create the page." msgstr "" -#: IDF/gettexttemplates/idf/wiki/create.html.php:6 +#: IDF/gettexttemplates/idf/wiki/createPage.html.php:6 msgid "Create Page" msgstr "" -#: IDF/gettexttemplates/idf/wiki/delete.html.php:3 +#: IDF/gettexttemplates/idf/wiki/createResource.html.php:3 +msgid "" +"\n" +"Wiki resources are later addressed in wiki pages by their title, so ensure " +"that you\n" +"give your resource a unique and an easy to remember name.\n" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/createResource.html.php:8 +msgid "Create Resource" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:3 +msgid "" +"If you delete this documentation page, it will be removed from the database " +"with all the associated revisions and you will not be able to " +"recover it." +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:6 +msgid "Delete Page" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deletePage.html.php:11 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:14 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:15 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:15 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:17 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:18 +msgid "Old Revisions" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:3 #, php-format msgid "" "You are looking at an old revision (%%oldrev.summary%%) of the " @@ -3344,31 +3908,51 @@ msgid "" "by %%submitter%%." msgstr "" -#: IDF/gettexttemplates/idf/wiki/delete.html.php:6 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:6 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:5 msgid "" "If you delete this old revision, it will be removed from the database and " "you will not be able to recover it." msgstr "" -#: IDF/gettexttemplates/idf/wiki/delete.html.php:9 +#: IDF/gettexttemplates/idf/wiki/deletePageRev.html.php:9 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:8 msgid "Delete Revision" msgstr "" -#: IDF/gettexttemplates/idf/wiki/delete.html.php:14 -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:11 -#: IDF/gettexttemplates/idf/wiki/view.html.php:17 -msgid "Old Revisions" -msgstr "" - -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:3 +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:3 msgid "" -"If you delete this documentation page, it will be removed from the database " -"with all the associated revisions and you will not be able to " -"recover it." +"If you delete this documentation resource, it will be removed from the " +"database with all the associated revisions \n" +"and you will not be able to recover it. Any documentation " +"pages that reference this resource,\n" +"will no longer be able to render it, but won't be deleted." msgstr "" -#: IDF/gettexttemplates/idf/wiki/deletepage.html.php:6 -msgid "Delete Page" +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:8 +msgid "Delete Resource" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:10 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:10 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:8 +msgid "File size" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deleteResource.html.php:11 +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:11 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:9 +#: IDF/Views/Wiki.php:101 +msgid "MIME type" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/deleteResourceRev.html.php:3 +#, php-format +msgid "" +"You are looking at an old revision (%%oldrev.summary%%) of the " +"resource \n" +"%%resource.title%%. This revision was created by " +"%%submitter%%." msgstr "" #: IDF/gettexttemplates/idf/wiki/edit-info.html.php:3 @@ -3379,46 +3963,68 @@ msgid "" "

The content of the page can use the Markdown syntax with the Extra extension.

\n" "

Website addresses are automatically linked and you can link to another " -"page in the documentation using double square brackets like that " -"[[AnotherPage]].

\n" +"page in the documentation using double square brackets like that " +"[[AnotherPage]].

\n" +"

If you want to embed uploaded resources, use the [[!ResourceName]] syntax for that. This is described more in detail in the FAQ.

\n" "

To directly include a file content from the repository, embrace its path " -"with triple square brackets: [[[path/to/file.txt]]].

\n" +"with triple square brackets: [[[my/file.txt]]].

\n" msgstr "" -#: IDF/gettexttemplates/idf/wiki/index.html.php:3 +#: IDF/gettexttemplates/idf/wiki/feedfragment-resource.xml.php:4 +msgid "Initial creation" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/listPages.html.php:3 #, php-format msgid "See the deprecated pages." msgstr "" -#: IDF/gettexttemplates/idf/wiki/index.html.php:5 +#: IDF/gettexttemplates/idf/wiki/listPages.html.php:5 msgid "Number of pages:" msgstr "" +#: IDF/gettexttemplates/idf/wiki/listResources.html.php:4 +msgid "Number of resources:" +msgstr "" + #: IDF/gettexttemplates/idf/wiki/search.html.php:4 msgid "Pages found:" msgstr "" -#: IDF/gettexttemplates/idf/wiki/update.html.php:4 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:4 +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:3 msgid "The form contains some errors. Please correct them to update the page." msgstr "" -#: IDF/gettexttemplates/idf/wiki/update.html.php:6 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:6 msgid "Update Page" msgstr "" -#: IDF/gettexttemplates/idf/wiki/update.html.php:8 -#: IDF/gettexttemplates/idf/wiki/update.html.php:10 -#: IDF/gettexttemplates/idf/wiki/update.html.php:11 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:8 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:10 +#: IDF/gettexttemplates/idf/wiki/updatePage.html.php:11 msgid "Delete this page" msgstr "" -#: IDF/gettexttemplates/idf/wiki/view.html.php:3 +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:4 +msgid "Update Resource" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:6 +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:8 +#: IDF/gettexttemplates/idf/wiki/updateResource.html.php:9 +msgid "Delete this resource" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:3 msgid "" "Attention! This page is marked as deprecated, \n" "use it as reference only if you are sure you need these specific information." msgstr "" -#: IDF/gettexttemplates/idf/wiki/view.html.php:5 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:5 #, php-format msgid "" "You are looking at an old revision of the page \n" @@ -3426,15 +4032,33 @@ msgid "" "by %%submitter%%." msgstr "" -#: IDF/gettexttemplates/idf/wiki/view.html.php:10 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:10 msgid "Table of Content" msgstr "" -#: IDF/gettexttemplates/idf/wiki/view.html.php:11 -#: IDF/gettexttemplates/idf/wiki/view.html.php:13 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:11 +#: IDF/gettexttemplates/idf/wiki/viewPage.html.php:13 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:11 +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:13 msgid "Delete this revision" msgstr "" +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:3 +#, php-format +msgid "" +"You are looking at an old revision of the resource \n" +"%%resource.title%%. This revision was created\n" +"by %%submitter%%." +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:14 +msgid "Page Usage" +msgstr "" + +#: IDF/gettexttemplates/idf/wiki/viewResource.html.php:15 +msgid "This resource is not used on any pages yet." +msgstr "" + #: IDF/gettexttemplates/idf/wiki/wiki-created-email.txt.php:3 msgid "A new documentation page has been created:" msgstr "" @@ -3460,7 +4084,7 @@ msgstr "" msgid "owner" msgstr "" -#: IDF/Issue.php:84 IDF/WikiPage.php:86 +#: IDF/Issue.php:84 IDF/Wiki/Page.php:86 msgid "interested users" msgstr "" @@ -3469,12 +4093,13 @@ msgid "" "Interested users will get an email notification when the issue is changed." msgstr "" -#: IDF/Issue.php:92 IDF/Review.php:95 IDF/Upload.php:99 IDF/WikiPage.php:94 +#: IDF/Issue.php:92 IDF/Project.php:94 IDF/Review.php:95 IDF/Upload.php:99 +#: IDF/Wiki/Page.php:94 msgid "labels" msgstr "" #: IDF/Issue.php:111 IDF/IssueFile.php:102 IDF/Review.php:114 -#: IDF/Upload.php:118 IDF/WikiPage.php:106 +#: IDF/Upload.php:118 IDF/Wiki/Page.php:106 IDF/Wiki/Resource.php:101 msgid "modification date" msgstr "" @@ -3486,22 +4111,22 @@ msgstr "" #: IDF/Issue.php:214 #, php-format -msgid "Creation of issue %d, by %s" +msgid "Creation of issue %3$d, by %4$s" msgstr "" #: IDF/Issue.php:224 #, php-format -msgid "%s: Issue %d created - %s" +msgid "%1$s: Issue %2$d created - %3$s" msgstr "" -#: IDF/Issue.php:290 +#: IDF/Issue.php:307 #, php-format -msgid "Issue %s - %s (%s)" +msgid "Issue %1$s - %2$s (%3$s)" msgstr "" -#: IDF/Issue.php:336 +#: IDF/Issue.php:311 #, php-format -msgid "Updated Issue %s - %s (%s)" +msgid "Updated Issue %1$s - %2$s (%3$s)" msgstr "" #: IDF/IssueComment.php:51 IDF/IssueRelation.php:47 @@ -3514,7 +4139,7 @@ msgid "comment" msgstr "" #: IDF/IssueComment.php:72 IDF/Review/Comment.php:75 IDF/Upload.php:63 -#: IDF/WikiRevision.php:85 +#: IDF/Wiki/PageRevision.php:85 msgid "changes" msgstr "" @@ -3524,12 +4149,12 @@ msgstr "" #: IDF/IssueComment.php:180 #, php-format -msgid "Comment on issue %d, by %s" +msgid "Comment on issue %3$d, by %4$s" msgstr "" #: IDF/IssueComment.php:191 #, php-format -msgid "%s: Comment on issue %d - %s" +msgid "%1$s: Comment on issue %2$d - %3$s" msgstr "" #: IDF/IssueFile.php:64 @@ -3637,7 +4262,7 @@ msgstr "" #: IDF/Plugin/SyncMonotone.php:309 IDF/Plugin/SyncMonotone.php:525 #, php-format -msgid "Could not parse usher configuration in \"%s\": %s" +msgid "Could not parse usher configuration in \"%1$s\": %2$s" msgstr "" #: IDF/Plugin/SyncMonotone.php:320 @@ -3682,7 +4307,7 @@ msgstr "" #: IDF/Plugin/SyncMonotone.php:599 IDF/Plugin/SyncMonotone.php:718 #, php-format -msgid "Could not parse read-permissions for project \"%s\": %s" +msgid "Could not parse read-permissions for project \"%1$s\": %2$s" msgstr "" #: IDF/Plugin/SyncMonotone.php:643 IDF/Plugin/SyncMonotone.php:741 @@ -3719,7 +4344,7 @@ msgstr "" #: IDF/Project.php:70 msgid "" -"Used in the url to access the project, must be short with only letters and " +"Used in the URL to access the project, must be short with only letters and " "numbers." msgstr "" @@ -3732,18 +4357,26 @@ msgid "description" msgstr "" #: IDF/Project.php:87 -msgid "The description can be extended using the markdown syntax." +msgid "The description can be extended using the Markdown syntax." msgstr "" -#: IDF/Project.php:93 +#: IDF/Project.php:100 msgid "private" msgstr "" -#: IDF/Project.php:130 +#: IDF/Project.php:108 +msgid "current project activity" +msgstr "" + +#: IDF/Project.php:159 #, php-format msgid "Project \"%s\" not found." msgstr "" +#: IDF/ProjectActivity.php:56 +msgid "date" +msgstr "" + #: IDF/Review/Comment.php:55 IDF/Review/Patch.php:80 msgid "patch" msgstr "" @@ -3760,17 +4393,17 @@ msgstr "" #: IDF/Review/Comment.php:141 #, php-format -msgid "Update of review %d, by %s" +msgid "Update of review %3$d, by %4$s" msgstr "" #: IDF/Review/Comment.php:151 #, php-format -msgid "%s: Updated review %d - %s" +msgid "%1$s: Updated review %2$d - %3$s" msgstr "" -#: IDF/Review/Comment.php:216 +#: IDF/Review/Comment.php:222 #, php-format -msgid "Updated Code Review %s - %s (%s)" +msgid "Updated Code Review %1$s - %2$s (%3$s)" msgstr "" #: IDF/Review/Patch.php:52 @@ -3783,17 +4416,17 @@ msgstr "" #: IDF/Review/Patch.php:153 #, php-format -msgid "Creation of review %d, by %s" +msgid "Creation of review %3$d, by %4$s" msgstr "" #: IDF/Review/Patch.php:163 #, php-format -msgid "%s: Creation of Review %d - %s" +msgid "%1$s: Creation of Review %2$d - %3$s" msgstr "" -#: IDF/Review/Patch.php:204 +#: IDF/Review/Patch.php:208 #, php-format -msgid "New Code Review %s - %s (%s)" +msgid "New Code Review %1$s - %2$s (%3$s)" msgstr "" #: IDF/Scm/Git.php:309 IDF/Scm/Mercurial.php:199 @@ -3821,7 +4454,7 @@ msgid "Could not write client key \"%s\"" msgstr "" #: IDF/Search/Occ.php:33 -msgid "occurence" +msgid "occurrence" msgstr "" #: IDF/Search/Occ.php:49 @@ -3829,11 +4462,11 @@ msgid "word" msgstr "" #: IDF/Search/Occ.php:75 -msgid "occurences" +msgid "occurrences" msgstr "" #: IDF/Search/Occ.php:81 -msgid "ponderated occurence" +msgid "ponderated occurrence" msgstr "" #: IDF/Tag.php:59 @@ -3852,10 +4485,26 @@ msgstr "" msgid "Lower case version of the name for fast searching." msgstr "" -#: IDF/Template/Markdown.php:75 +#: IDF/Template/Markdown.php:84 msgid "Create this documentation page" msgstr "" +#: IDF/Template/Markdown.php:97 +msgid "You are not allowed to access the wiki." +msgstr "" + +#: IDF/Template/Markdown.php:106 +msgid "The wiki resource has not been found." +msgstr "" + +#: IDF/Template/Markdown.php:113 +msgid "The wiki resource has not been found. Create it!" +msgstr "" + +#: IDF/Template/Markdown.php:146 +msgid "This revision of the resource is no longer available." +msgstr "" + #: IDF/Template/ShowUser.php:51 msgid "Anonymous" msgstr "" @@ -3876,7 +4525,7 @@ msgstr "" msgid "The path is relative to the upload path." msgstr "" -#: IDF/Upload.php:78 +#: IDF/Upload.php:78 IDF/Wiki/ResourceRevision.php:71 msgid "file size in bytes" msgstr "" @@ -3895,144 +4544,158 @@ msgstr "" #: IDF/Upload.php:204 #, php-format -msgid "Addition of download %d, by %s" +msgid "Addition of download %2$d, by %3$s" msgstr "" #: IDF/Upload.php:214 #, php-format -msgid "%s: Download %d added - %s" +msgid "%1$s: Download %2$d added - %3$s" msgstr "" -#: IDF/Upload.php:256 +#: IDF/Upload.php:301 #, php-format -msgid "New download - %s (%s)" +msgid "New download - %1$s (%2$s)" msgstr "" -#: IDF/Views/Admin.php:60 +#: IDF/Upload.php:305 +#, php-format +msgid "Updated download - %1$s (%2$s)" +msgstr "" + +#: IDF/Views/Admin.php:47 +msgid "The forge configuration has been saved." +msgstr "" + +#: IDF/Views/Admin.php:80 msgid "This table shows the projects in the forge." msgstr "" -#: IDF/Views/Admin.php:65 +#: IDF/Views/Admin.php:85 msgid "Short Name" msgstr "" -#: IDF/Views/Admin.php:67 +#: IDF/Views/Admin.php:87 msgid "Repository Size" msgstr "" -#: IDF/Views/Admin.php:73 +#: IDF/Views/Admin.php:93 msgid "No projects were found." msgstr "" -#: IDF/Views/Admin.php:93 IDF/Views/Admin.php:251 IDF/Views/Wiki.php:310 +#: IDF/Views/Admin.php:116 +msgid "The label configuration has been saved." +msgstr "" + +#: IDF/Views/Admin.php:147 IDF/Views/Admin.php:314 IDF/Views/Wiki.php:484 +#: IDF/Views/Wiki.php:536 #, php-format msgid "Update %s" msgstr "" -#: IDF/Views/Admin.php:101 IDF/Views/Project.php:302 +#: IDF/Views/Admin.php:155 IDF/Views/Project.php:304 msgid "The project has been updated." msgstr "" -#: IDF/Views/Admin.php:134 +#: IDF/Views/Admin.php:192 msgid "The project has been created." msgstr "" -#: IDF/Views/Admin.php:160 +#: IDF/Views/Admin.php:223 #, php-format msgid "Delete %s Project" msgstr "" -#: IDF/Views/Admin.php:167 +#: IDF/Views/Admin.php:230 msgid "The project has been deleted." msgstr "" -#: IDF/Views/Admin.php:197 +#: IDF/Views/Admin.php:260 msgid "Not Validated User List" msgstr "" -#: IDF/Views/Admin.php:205 +#: IDF/Views/Admin.php:268 msgid "This table shows the users in the forge." msgstr "" -#: IDF/Views/Admin.php:209 +#: IDF/Views/Admin.php:272 msgid "login" msgstr "" -#: IDF/Views/Admin.php:212 +#: IDF/Views/Admin.php:275 msgid "Admin" msgstr "" -#: IDF/Views/Admin.php:214 +#: IDF/Views/Admin.php:277 msgid "Last Login" msgstr "" -#: IDF/Views/Admin.php:221 +#: IDF/Views/Admin.php:284 msgid "No users were found." msgstr "" -#: IDF/Views/Admin.php:258 +#: IDF/Views/Admin.php:321 msgid "You do not have the rights to update this user." msgstr "" -#: IDF/Views/Admin.php:276 +#: IDF/Views/Admin.php:339 msgid "The user has been updated." msgstr "" -#: IDF/Views/Admin.php:309 +#: IDF/Views/Admin.php:372 #, php-format msgid "The user %s has been created." msgstr "" -#: IDF/Views/Admin.php:316 +#: IDF/Views/Admin.php:379 msgid "Add User" msgstr "" -#: IDF/Views/Admin.php:332 +#: IDF/Views/Admin.php:395 msgid "Usher management" msgstr "" -#: IDF/Views/Admin.php:369 +#: IDF/Views/Admin.php:432 msgid "Usher configuration has been reloaded" msgstr "" -#: IDF/Views/Admin.php:373 +#: IDF/Views/Admin.php:436 msgid "Usher has been shut down" msgstr "" -#: IDF/Views/Admin.php:378 +#: IDF/Views/Admin.php:441 msgid "Usher has been started up" msgstr "" -#: IDF/Views/Admin.php:416 +#: IDF/Views/Admin.php:479 #, php-format msgid "The server \"%s\" has been started" msgstr "" -#: IDF/Views/Admin.php:420 +#: IDF/Views/Admin.php:483 #, php-format msgid "The server \"%s\" has been stopped" msgstr "" -#: IDF/Views/Admin.php:425 +#: IDF/Views/Admin.php:488 #, php-format msgid "The server \"%s\" has been killed" msgstr "" -#: IDF/Views/Admin.php:445 +#: IDF/Views/Admin.php:508 #, php-format msgid "Open connections for \"%s\"" msgstr "" -#: IDF/Views/Admin.php:450 +#: IDF/Views/Admin.php:513 #, php-format msgid "no connections for server \"%s\"" msgstr "" -#: IDF/Views/Admin.php:471 +#: IDF/Views/Admin.php:534 msgid "Yes" msgstr "" -#: IDF/Views/Admin.php:471 +#: IDF/Views/Admin.php:534 msgid "No" msgstr "" @@ -4045,11 +4708,11 @@ msgstr "" msgid "This table shows the files to download." msgstr "" -#: IDF/Views/Download.php:67 IDF/Views/Download.php:315 +#: IDF/Views/Download.php:67 IDF/Views/Download.php:349 msgid "Uploaded" msgstr "" -#: IDF/Views/Download.php:71 IDF/Views/Download.php:319 +#: IDF/Views/Download.php:71 IDF/Views/Download.php:353 msgid "No downloads were found." msgstr "" @@ -4072,17 +4735,25 @@ msgstr "" msgid "The file has been deleted." msgstr "" -#: IDF/Views/Download.php:243 +#: IDF/Views/Download.php:244 #, php-format msgid "The file has been uploaded." msgstr "" -#: IDF/Views/Download.php:297 +#: IDF/Views/Download.php:271 +msgid "New Downloads from Archive" +msgstr "" + +#: IDF/Views/Download.php:278 +msgid "The archive has been uploaded and processed." +msgstr "" + +#: IDF/Views/Download.php:331 #, php-format msgid "%1$s Downloads with Label %2$s" msgstr "" -#: IDF/Views/Download.php:307 +#: IDF/Views/Download.php:341 #, php-format msgid "This table shows the downloads with label %s." msgstr "" @@ -4092,25 +4763,25 @@ msgstr "" msgid "%s Open Issues" msgstr "" -#: IDF/Views/Issue.php:51 IDF/Views/Issue.php:379 IDF/Views/User.php:75 +#: IDF/Views/Issue.php:51 IDF/Views/Issue.php:382 IDF/Views/User.php:75 msgid "This table shows the open issues." msgstr "" -#: IDF/Views/Issue.php:61 IDF/Views/Issue.php:217 IDF/Views/Issue.php:298 -#: IDF/Views/Issue.php:387 IDF/Views/Issue.php:539 IDF/Views/Issue.php:762 -#: IDF/Views/Issue.php:821 IDF/Views/Review.php:57 IDF/Views/User.php:81 +#: IDF/Views/Issue.php:61 IDF/Views/Issue.php:220 IDF/Views/Issue.php:301 +#: IDF/Views/Issue.php:390 IDF/Views/Issue.php:542 IDF/Views/Issue.php:765 +#: IDF/Views/Issue.php:824 IDF/Views/Review.php:57 IDF/Views/User.php:81 msgid "Id" msgstr "" -#: IDF/Views/Issue.php:64 IDF/Views/Issue.php:220 IDF/Views/Issue.php:302 -#: IDF/Views/Issue.php:390 IDF/Views/Issue.php:542 IDF/Views/Issue.php:765 -#: IDF/Views/Issue.php:824 IDF/Views/Review.php:60 IDF/Views/User.php:85 +#: IDF/Views/Issue.php:64 IDF/Views/Issue.php:223 IDF/Views/Issue.php:305 +#: IDF/Views/Issue.php:393 IDF/Views/Issue.php:545 IDF/Views/Issue.php:768 +#: IDF/Views/Issue.php:827 IDF/Views/Review.php:60 IDF/Views/User.php:85 msgid "Last Updated" msgstr "" -#: IDF/Views/Issue.php:68 IDF/Views/Issue.php:224 IDF/Views/Issue.php:306 -#: IDF/Views/Issue.php:394 IDF/Views/Issue.php:546 IDF/Views/Issue.php:769 -#: IDF/Views/Issue.php:828 +#: IDF/Views/Issue.php:68 IDF/Views/Issue.php:227 IDF/Views/Issue.php:309 +#: IDF/Views/Issue.php:397 IDF/Views/Issue.php:549 IDF/Views/Issue.php:772 +#: IDF/Views/Issue.php:831 msgid "No issues were found." msgstr "" @@ -4118,142 +4789,142 @@ msgstr "" msgid "Not assigned" msgstr "" -#: IDF/Views/Issue.php:146 +#: IDF/Views/Issue.php:149 #, php-format msgid "Summary of tracked issues in %s." msgstr "" -#: IDF/Views/Issue.php:191 +#: IDF/Views/Issue.php:194 #, php-format msgid "Watch List: Closed Issues for %s" msgstr "" -#: IDF/Views/Issue.php:192 +#: IDF/Views/Issue.php:195 #, php-format msgid "This table shows the closed issues in your watch list for %s project." msgstr "" -#: IDF/Views/Issue.php:197 +#: IDF/Views/Issue.php:200 #, php-format msgid "Watch List: Open Issues for %s" msgstr "" -#: IDF/Views/Issue.php:198 +#: IDF/Views/Issue.php:201 #, php-format msgid "This table shows the open issues in your watch list for %s project." msgstr "" -#: IDF/Views/Issue.php:274 +#: IDF/Views/Issue.php:277 msgid "Watch List: Closed Issues" msgstr "" -#: IDF/Views/Issue.php:275 +#: IDF/Views/Issue.php:278 msgid "This table shows the closed issues in your watch list." msgstr "" -#: IDF/Views/Issue.php:280 +#: IDF/Views/Issue.php:283 msgid "Watch List: Open Issues" msgstr "" -#: IDF/Views/Issue.php:281 +#: IDF/Views/Issue.php:284 msgid "This table shows the open issues in your watch list." msgstr "" -#: IDF/Views/Issue.php:300 IDF/Views/User.php:82 +#: IDF/Views/Issue.php:303 IDF/Views/User.php:82 msgid "Project" msgstr "" -#: IDF/Views/Issue.php:341 +#: IDF/Views/Issue.php:344 #, php-format -msgid "%s %s Submitted %s Issues" +msgid "%1$s %2$s Submitted %3$s Issues" msgstr "" -#: IDF/Views/Issue.php:345 +#: IDF/Views/Issue.php:348 #, php-format -msgid "%s %s Closed Submitted %s Issues" +msgid "%1$s %2$s Closed Submitted %3$s Issues" msgstr "" -#: IDF/Views/Issue.php:349 +#: IDF/Views/Issue.php:352 #, php-format -msgid "%s %s Closed Working %s Issues" +msgid "%1$s %2$s Closed Working %3$s Issues" msgstr "" -#: IDF/Views/Issue.php:353 +#: IDF/Views/Issue.php:356 #, php-format -msgid "%s %s Working %s Issues" +msgid "%1$s %2$s Working %3$s Issues" msgstr "" -#: IDF/Views/Issue.php:414 +#: IDF/Views/Issue.php:417 msgid "Submit a new issue" msgstr "" -#: IDF/Views/Issue.php:430 +#: IDF/Views/Issue.php:433 #, php-format -msgid "Issue %d has been created." +msgid "Issue %2$d has been created." msgstr "" -#: IDF/Views/Issue.php:487 +#: IDF/Views/Issue.php:490 #, php-format msgid "Search issues - %s" msgstr "" -#: IDF/Views/Issue.php:489 +#: IDF/Views/Issue.php:492 #, php-format msgid "Search closed issues - %s" msgstr "" -#: IDF/Views/Issue.php:536 +#: IDF/Views/Issue.php:539 msgid "This table shows the found issues." msgstr "" -#: IDF/Views/Issue.php:601 +#: IDF/Views/Issue.php:604 #, php-format -msgid "Issue %d: %s" +msgid "Issue %2$d: %3$s" msgstr "" -#: IDF/Views/Issue.php:625 +#: IDF/Views/Issue.php:628 #, php-format -msgid "Issue %d has been updated." +msgid "Issue %2$d has been updated." msgstr "" -#: IDF/Views/Issue.php:715 +#: IDF/Views/Issue.php:718 #, php-format msgid "View %s" msgstr "" -#: IDF/Views/Issue.php:742 +#: IDF/Views/Issue.php:745 #, php-format msgid "%s Closed Issues" msgstr "" -#: IDF/Views/Issue.php:752 +#: IDF/Views/Issue.php:755 msgid "This table shows the closed issues." msgstr "" -#: IDF/Views/Issue.php:795 -#, php-format -msgid "%1$s Issues with Label %2$s" -msgstr "" - #: IDF/Views/Issue.php:798 #, php-format +msgid "%1$s Issues with Label %2$s" +msgstr "" + +#: IDF/Views/Issue.php:801 +#, php-format msgid "%1$s Closed Issues with Label %2$s" msgstr "" -#: IDF/Views/Issue.php:811 +#: IDF/Views/Issue.php:814 #, php-format msgid "This table shows the issues with label %s." msgstr "" -#: IDF/Views/Issue.php:934 +#: IDF/Views/Issue.php:937 msgid "The issue has been removed from your watch list." msgstr "" -#: IDF/Views/Issue.php:937 +#: IDF/Views/Issue.php:940 msgid "The issue has been added to your watch list." msgstr "" -#: IDF/Views/Issue.php:1035 +#: IDF/Views/Issue.php:1037 msgid "On your watch list." msgstr "" @@ -4269,74 +4940,75 @@ msgstr "" msgid "Reviews and Patches" msgstr "" -#: IDF/Views/Project.php:178 +#: IDF/Views/Project.php:180 msgid "This table shows the project updates." msgstr "" -#: IDF/Views/Project.php:189 +#: IDF/Views/Project.php:191 msgid "Change" msgstr "" -#: IDF/Views/Project.php:193 +#: IDF/Views/Project.php:195 msgid "No changes were found." msgstr "" -#: IDF/Views/Project.php:294 +#: IDF/Views/Project.php:296 #, php-format msgid "%s Project Summary" msgstr "" -#: IDF/Views/Project.php:329 +#: IDF/Views/Project.php:335 #, php-format msgid "%s Issue Tracking Configuration" msgstr "" -#: IDF/Views/Project.php:338 +#: IDF/Views/Project.php:344 msgid "The issue tracking configuration has been saved." msgstr "" -#: IDF/Views/Project.php:375 +#: IDF/Views/Project.php:381 #, php-format msgid "%s Downloads Configuration" msgstr "" -#: IDF/Views/Project.php:384 +#: IDF/Views/Project.php:393 msgid "The downloads configuration has been saved." msgstr "" -#: IDF/Views/Project.php:418 +#: IDF/Views/Project.php:428 #, php-format msgid "%s Documentation Configuration" msgstr "" -#: IDF/Views/Project.php:427 +#: IDF/Views/Project.php:437 msgid "The documentation configuration has been saved." msgstr "" -#: IDF/Views/Project.php:461 +#: IDF/Views/Project.php:471 #, php-format msgid "%s Project Members" msgstr "" -#: IDF/Views/Project.php:470 +#: IDF/Views/Project.php:480 msgid "The project membership has been saved." msgstr "" -#: IDF/Views/Project.php:493 +#: IDF/Views/Project.php:503 #, php-format msgid "%s Tabs Access Rights" msgstr "" -#: IDF/Views/Project.php:507 -msgid "The project tabs access rights have been saved." +#: IDF/Views/Project.php:517 +msgid "" +"The project tabs access rights and notification settings have been saved." msgstr "" -#: IDF/Views/Project.php:553 +#: IDF/Views/Project.php:566 #, php-format msgid "%s Source" msgstr "" -#: IDF/Views/Project.php:567 +#: IDF/Views/Project.php:580 msgid "The project source configuration has been saved." msgstr "" @@ -4355,17 +5027,17 @@ msgstr "" #: IDF/Views/Review.php:94 #, php-format -msgid "The code review %d has been created." +msgid "The code review %2$d has been created." msgstr "" #: IDF/Views/Review.php:140 #, php-format -msgid "Review %d: %s" +msgid "Review %2$d: %3$s" msgstr "" #: IDF/Views/Review.php:160 #, php-format -msgid "Your code review %d has been published." +msgid "Your code review %2$d has been published." msgstr "" #: IDF/Views/Source.php:40 @@ -4388,19 +5060,19 @@ msgstr "" msgid "%1$s %2$s Change Log" msgstr "" -#: IDF/Views/Source.php:147 IDF/Views/Source.php:228 IDF/Views/Source.php:356 +#: IDF/Views/Source.php:153 IDF/Views/Source.php:234 IDF/Views/Source.php:362 #, php-format msgid "%1$s %2$s Source Tree" msgstr "" -#: IDF/Views/Source.php:304 +#: IDF/Views/Source.php:310 #, php-format msgid "%s Commit Details" msgstr "" -#: IDF/Views/Source.php:305 +#: IDF/Views/Source.php:311 #, php-format -msgid "%s Commit Details - %s" +msgid "%1$s Commit Details - %2$s" msgstr "" #: IDF/Views/User.php:59 @@ -4453,152 +5125,261 @@ msgstr "" msgid "This table shows the documentation pages." msgstr "" -#: IDF/Views/Wiki.php:61 IDF/Views/Wiki.php:106 IDF/Views/Wiki.php:147 +#: IDF/Views/Wiki.php:61 IDF/Views/Wiki.php:144 IDF/Views/Wiki.php:185 msgid "Page Title" msgstr "" -#: IDF/Views/Wiki.php:63 IDF/Views/Wiki.php:108 IDF/Views/Wiki.php:149 +#: IDF/Views/Wiki.php:63 IDF/Views/Wiki.php:103 IDF/Views/Wiki.php:146 +#: IDF/Views/Wiki.php:187 msgid "Updated" msgstr "" -#: IDF/Views/Wiki.php:67 IDF/Views/Wiki.php:153 +#: IDF/Views/Wiki.php:67 IDF/Views/Wiki.php:191 msgid "No documentation pages were found." msgstr "" -#: IDF/Views/Wiki.php:90 +#: IDF/Views/Wiki.php:88 +#, php-format +msgid "%s Documentation Resources" +msgstr "" + +#: IDF/Views/Wiki.php:94 +msgid "This table shows the resources that can be used on documentation pages." +msgstr "" + +#: IDF/Views/Wiki.php:100 +msgid "Resource Title" +msgstr "" + +#: IDF/Views/Wiki.php:107 +msgid "No resources were found." +msgstr "" + +#: IDF/Views/Wiki.php:128 #, php-format msgid "Documentation Search - %s" msgstr "" -#: IDF/Views/Wiki.php:101 +#: IDF/Views/Wiki.php:139 msgid "This table shows the pages found." msgstr "" -#: IDF/Views/Wiki.php:112 +#: IDF/Views/Wiki.php:150 msgid "No pages were found." msgstr "" -#: IDF/Views/Wiki.php:131 +#: IDF/Views/Wiki.php:169 #, php-format msgid "%1$s Documentation Pages with Label %2$s" msgstr "" -#: IDF/Views/Wiki.php:141 +#: IDF/Views/Wiki.php:179 #, php-format msgid "This table shows the documentation pages with label %s." msgstr "" -#: IDF/Views/Wiki.php:184 +#: IDF/Views/Wiki.php:222 #, php-format -msgid "The page %s has been created." +msgid "The page %2$s has been created." msgstr "" -#: IDF/Views/Wiki.php:271 +#: IDF/Views/Wiki.php:265 +#, php-format +msgid "The resource %2$s has been created." +msgstr "" + +#: IDF/Views/Wiki.php:409 IDF/Views/Wiki.php:448 msgid "The old revision has been deleted." msgstr "" -#: IDF/Views/Wiki.php:277 +#: IDF/Views/Wiki.php:415 IDF/Views/Wiki.php:454 #, php-format msgid "Delete Old Revision of %s" msgstr "" -#: IDF/Views/Wiki.php:322 +#: IDF/Views/Wiki.php:496 #, php-format -msgid "The page %s has been updated." +msgid "The page %2$s has been updated." msgstr "" -#: IDF/Views/Wiki.php:360 +#: IDF/Views/Wiki.php:548 +#, php-format +msgid "The resource %2$s has been updated." +msgstr "" + +#: IDF/Views/Wiki.php:583 msgid "The documentation page has been deleted." msgstr "" -#: IDF/Views/Wiki.php:368 +#: IDF/Views/Wiki.php:591 #, php-format msgid "Delete Page %s" msgstr "" -#: IDF/Views.php:126 IDF/Views.php:152 +#: IDF/Views/Wiki.php:623 +msgid "The documentation resource has been deleted." +msgstr "" + +#: IDF/Views/Wiki.php:631 +#, php-format +msgid "Delete Resource %s" +msgstr "" + +#: IDF/Views.php:173 IDF/Views.php:199 msgid "Confirm Your Account Creation" msgstr "" -#: IDF/Views.php:172 +#: IDF/Views.php:219 msgid "Welcome! You can now participate in the life of your project of choice." msgstr "" -#: IDF/Views.php:198 IDF/Views.php:222 IDF/Views.php:263 +#: IDF/Views.php:245 IDF/Views.php:269 IDF/Views.php:310 msgid "Password Recovery" msgstr "" -#: IDF/Views.php:242 +#: IDF/Views.php:289 msgid "" "Welcome back! Next time, you can use your broswer options to remember the " "password." msgstr "" -#: IDF/Views.php:284 +#: IDF/Views.php:331 msgid "Here to Help You!" msgstr "" -#: IDF/Views.php:300 +#: IDF/Views.php:347 +msgid "InDefero Upload Archive Format" +msgstr "" + +#: IDF/Views.php:363 msgid "InDefero API (Application Programming Interface)" msgstr "" -#: IDF/WikiPage.php:62 +#: IDF/Wiki/Page.php:62 IDF/Wiki/Resource.php:64 msgid "title" msgstr "" -#: IDF/WikiPage.php:63 +#: IDF/Wiki/Page.php:63 msgid "" "The title of the page must only contain letters, digits or the dash " "character. For example: My-new-Wiki-Page." msgstr "" -#: IDF/WikiPage.php:71 +#: IDF/Wiki/Page.php:71 msgid "A one line description of the page content." msgstr "" -#: IDF/WikiPage.php:196 IDF/WikiRevision.php:167 +#: IDF/Wiki/Page.php:196 IDF/Wiki/PageRevision.php:196 #, php-format msgid "%2$s, %3$s" msgstr "" -#: IDF/WikiPage.php:198 +#: IDF/Wiki/Page.php:198 #, php-format -msgid "Creation of page %s, by %s" +msgid "Creation of page %2$s, by %3$s" msgstr "" -#: IDF/WikiPage.php:208 +#: IDF/Wiki/Page.php:208 #, php-format -msgid "%s: Documentation page %s added - %s" +msgid "%1$s: Documentation page %2$s added - %3$s" msgstr "" -#: IDF/WikiRevision.php:48 +#: IDF/Wiki/PageRevision.php:48 msgid "page" msgstr "" -#: IDF/WikiRevision.php:66 +#: IDF/Wiki/PageRevision.php:66 IDF/Wiki/ResourceRevision.php:64 msgid "A one line description of the changes." msgstr "" -#: IDF/WikiRevision.php:72 +#: IDF/Wiki/PageRevision.php:72 msgid "content" msgstr "" -#: IDF/WikiRevision.php:189 +#: IDF/Wiki/PageRevision.php:218 IDF/Wiki/ResourceRevision.php:311 #, php-format -msgid "Change of %s, by %s" +msgid "Change of %2$s, by %3$s" msgstr "" -#: IDF/WikiRevision.php:208 +#: IDF/Wiki/PageRevision.php:231 #, php-format -msgid "%s: Documentation page %s updated - %s" +msgid "%1$s: Documentation page %2$s updated - %3$s" msgstr "" -#: IDF/WikiRevision.php:262 +#: IDF/Wiki/PageRevision.php:293 #, php-format -msgid "New Documentation Page %s - %s (%s)" +msgid "New Documentation Page %1$s - %2$s (%3$s)" msgstr "" -#: IDF/WikiRevision.php:268 +#: IDF/Wiki/PageRevision.php:297 #, php-format -msgid "Documentation Page Changed %s - %s (%s)" +msgid "Documentation Page Changed %1$s - %2$s (%3$s)" +msgstr "" + +#: IDF/Wiki/Resource.php:65 +msgid "" +"The title of the resource must only contain letters, digits, dots or the " +"dash character. For example: my-resource.png." +msgstr "" + +#: IDF/Wiki/Resource.php:72 +msgid "MIME media type" +msgstr "" + +#: IDF/Wiki/Resource.php:73 +msgid "The MIME media type of the resource." +msgstr "" + +#: IDF/Wiki/Resource.php:81 +msgid "A one line description of the resource." +msgstr "" + +#: IDF/Wiki/Resource.php:176 IDF/Wiki/ResourceRevision.php:308 +#, php-format +msgid "%2$s, %3$s" +msgstr "" + +#: IDF/Wiki/Resource.php:178 +#, php-format +msgid "Creation of resource %2$s, by %3$s" +msgstr "" + +#: IDF/Wiki/Resource.php:188 +#, php-format +msgid "%1$s: Documentation resource %2$s added - %3$s" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:47 +msgid "resource" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:78 +msgid "File extension" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:79 +msgid "The file extension of the uploaded resource." +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:94 +msgid "page usage" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:116 +#, php-format +msgid "id %d: %s" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:263 +#, php-format +msgid "Download (%s)" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:270 +msgid "View resource details" +msgstr "" + +#: IDF/Wiki/ResourceRevision.php:324 +#, php-format +msgid "%1$s: Documentation resource %2$s updated - %3$s" msgstr "" diff --git a/src/IDF/relations.php b/src/IDF/relations.php index 38f0ea1..b8ae4e3 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -22,7 +22,9 @@ # ***** END LICENSE BLOCK ***** */ $m = array(); -$m['IDF_Tag'] = array('relate_to' => array('IDF_Project')); +$m['IDF_ProjectActivity'] = array('relate_to' => array('IDF_Project')); +$m['IDF_Tag'] = array('relate_to' => array('IDF_Project'), + 'relate_to_many' => 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')); @@ -30,9 +32,12 @@ $m['IDF_IssueFile'] = array('relate_to' => array('IDF_IssueComment', 'Pluf_User' $m['IDF_Upload'] = array('relate_to' => array('IDF_Project', 'Pluf_User'), 'relate_to_many' => array('IDF_Tag')); $m['IDF_Search_Occ'] = array('relate_to' => array('IDF_Project'),); -$m['IDF_WikiPage'] = array('relate_to' => array('IDF_Project', 'Pluf_User'), - 'relate_to_many' => array('IDF_Tag', 'Pluf_User')); -$m['IDF_WikiRevision'] = array('relate_to' => array('IDF_WikiPage', 'Pluf_User')); +$m['IDF_Wiki_Page'] = array('relate_to' => array('IDF_Project', 'Pluf_User'), + 'relate_to_many' => array('IDF_Tag', 'Pluf_User')); +$m['IDF_Wiki_PageRevision'] = array('relate_to' => array('IDF_Wiki_Page', 'Pluf_User')); +$m['IDF_Wiki_Resource'] = array('relate_to' => array('IDF_Project', 'Pluf_User')); +$m['IDF_Wiki_ResourceRevision'] = array('relate_to' => array('IDF_Wiki_Resource', 'Pluf_User'), + 'relate_to_many' => array('IDF_PageRevision', 'Pluf_User')); $m['IDF_Review'] = array('relate_to' => array('IDF_Project', 'Pluf_User', 'IDF_Tag'), 'relate_to_many' => array('IDF_Tag', 'Pluf_User')); $m['IDF_Review_Patch'] = array('relate_to' => array('IDF_Review', 'Pluf_User')); diff --git a/src/IDF/templates/idf/admin/downloads.html b/src/IDF/templates/idf/admin/downloads.html index c22be5e..a614484 100644 --- a/src/IDF/templates/idf/admin/downloads.html +++ b/src/IDF/templates/idf/admin/downloads.html @@ -1,5 +1,5 @@ {extends "idf/admin/base.html"} -{block docclass}yui-t1{assign $inDownloads = true}{/block} +{block docclass}yui-t3{assign $inDownloads = true}{/block} {block body}
@@ -16,6 +16,15 @@ + + + + + + @@ -31,4 +40,29 @@

Optionally, use an equals-sign to document the meaning of each status value.

{/blocktrans} +
+ +{blocktrans}

The webhook URL setting specifies an URL to which a HTTP PUT +request is sent after a new download has been added or to which a HTTP POST +request is sent after an existing download has been updated. +If this field is empty, notifications are disabled.

+ +

Only properly-escaped HTTP URLs are supported, for example:

+ +
    +
  • http://domain.com/upload
  • +
  • http://domain.com/upload?my%20param
  • +
+ +

In addition, the URL may contain the following "%" notation, which +will be replaced with specific project values for each download:

+ +
    +
  • %p - project name
  • +
  • %d - download id
  • +
+ +

For example, updating download 123 of project 'my-project' with +web hook URL http://mydomain.com/%p/%d would send a POST request to +http://mydomain.com/my-project/123.

{/blocktrans}
{/block} diff --git a/src/IDF/templates/idf/admin/source.html b/src/IDF/templates/idf/admin/source.html index 3885227..7317707 100644 --- a/src/IDF/templates/idf/admin/source.html +++ b/src/IDF/templates/idf/admin/source.html @@ -41,12 +41,11 @@ - + @@ -68,26 +67,26 @@
-{blocktrans}

The webhook URL setting specifies a URL to which a HTTP POST -request is sent after each repository commit. If this field is empty, -notifications are disabled.

+{blocktrans}

The webhook URL setting specifies an URL to which a HTTP +{$hook_request_method} request is sent after each repository +commit. If this field is empty, notifications are disabled.

Only properly-escaped HTTP URLs are supported, for example:

    -
  • http://domain.com/commit
  • -
  • http://domain.com/commit?my%20param
  • +
  • http://domain.com/commit
  • +
  • http://domain.com/commit?my%20param

In addition, the URL may contain the following "%" notation, which will be replaced with specific project values for each commit:

    -
  • %p - project name
  • -
  • %r - revision number
  • +
  • %p - project name
  • +
  • %r - revision number

For example, committing revision 123 to project 'my-project' with -post-commit URL http://mydomain.com/%p/%r would send a request to -http://mydomain.com/my-project/123.

{/blocktrans}
+post-commit URL http://mydomain.com/%p/%r would send a request to +http://mydomain.com/my-project/123.

{/blocktrans} {/block} diff --git a/src/IDF/templates/idf/admin/summary.html b/src/IDF/templates/idf/admin/summary.html index 5a44503..095b004 100644 --- a/src/IDF/templates/idf/admin/summary.html +++ b/src/IDF/templates/idf/admin/summary.html @@ -25,13 +25,30 @@ + + + + - + + + + + - + {if $logo} - + @@ -60,6 +77,7 @@
{$form.f.upload_webhook_url.labelTag}:
+{if $form.f.upload_webhook_url.errors}{$form.f.upload_webhook_url.fieldErrors}{/if} +{$form.f.upload_webhook_url|unsafe}
+
{trans 'Web-Hook authentication key:'} {$hookkey}
{$form.f.webhook_url.labelTag}: {if $form.f.webhook_url.errors}{$form.f.webhook_url.fieldErrors}{/if} -{$form.f.webhook_url|unsafe}
- +{$form.f.webhook_url|unsafe}
{trans 'Post-commit authentication key:'}{trans 'Web-Hook authentication key:'} {$hookkey}
{$form.f.external_project_url.labelTag}:{if $form.f.external_project_url.errors}{$form.f.external_project_url.fieldErrors}{/if} +{$form.f.external_project_url|unsafe} +
{$form.f.description.labelTag}: {if $form.f.description.errors}{$form.f.description.fieldErrors}{/if} {$form.f.description|unsafe}
{trans 'Current logo'}:{$form.f.label1.labelTag}: +{if $form.f.label1.errors}{$form.f.label1.fieldErrors}{/if}{$form.f.label1|unsafe} +{if $form.f.label2.errors}{$form.f.label2.fieldErrors}{/if}{$form.f.label2|unsafe} +{if $form.f.label3.errors}{$form.f.label3.fieldErrors}{/if}{$form.f.label3|unsafe}
+{if $form.f.label4.errors}{$form.f.label4.fieldErrors}{/if}{$form.f.label4|unsafe} +{if $form.f.label5.errors}{$form.f.label5.fieldErrors}{/if}{$form.f.label5|unsafe} +{if $form.f.label6.errors}{$form.f.label6.fieldErrors}{/if}{$form.f.label6|unsafe} +
{trans 'Current logo'}: {if $logo} {trans 'Project logo'} @@ -41,14 +58,14 @@
{$form.f.logo.labelTag}:{$form.f.logo.labelTag}: {if $form.f.logo.errors}{$form.f.logo.fieldErrors}{/if} {$form.f.logo|unsafe}
{$form.f.logo_remove.labelTag}:{$form.f.logo_remove.labelTag}: {if $form.f.logo_remove.errors}{$form.f.logo_remove.fieldErrors}{/if} {$form.f.logo_remove|unsafe}
+{include 'idf/project/js-autocomplete.html'}{/block} {/block} {block context} diff --git a/src/IDF/templates/idf/admin/tabs.html b/src/IDF/templates/idf/admin/tabs.html index decea32..10ae8dc 100644 --- a/src/IDF/templates/idf/admin/tabs.html +++ b/src/IDF/templates/idf/admin/tabs.html @@ -10,11 +10,11 @@ {/if}
- +
- - + + @@ -22,6 +22,12 @@ {$form.f.downloads_access_rights|unsafe} @@ -31,6 +37,12 @@ {$form.f.wiki_access_rights|unsafe} @@ -40,6 +52,12 @@ {$form.f.issues_access_rights|unsafe} @@ -49,32 +67,37 @@ {$form.f.source_access_rights|unsafe} - - - - + +{$form.f.private_project.labelTag} + @@ -94,27 +117,45 @@ password or SSH key.{/blocktrans} {/block} {block context}
-

{trans 'Instructions:'}

-

{blocktrans}You can configure here the project tabs access rights and notification emails.{/blocktrans}

-

{blocktrans}Notification emails will be sent from the {$from_email} address, if you send the email to a mailing list, you may need to register this email address. Multiple email addresses must be separated through commas (','). If you do not want to send emails for a given type of changes, simply leave the corresponding field empty.{/blocktrans}

-

{blocktrans}If you mark a project as private, only the project members and administrators, together with the extra authorized users you provide will have access to the project. You will still be able to define further access rights for the different tabs but the "Open to all" and "Signed in users" will default to authorized users only.{/blocktrans}

-

{blocktrans}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}

+

{blocktrans}This section allows you to configure project tabs access rights and notifications.{/blocktrans}

+

{trans 'Access Rights'}

+

{blocktrans}Tab access controls whether a single user can navigate into a particular section of your project via the main menu or automatically generated object links.{/blocktrans}

+

{blocktrans}If you mark a project as private, only the project members and administrators, together with the extra authorized users you provide will have access to the project as a whole. You will still be able to define further access rights for the different tabs but the "Open to all" and "Signed in users" will default to authorized users only.{/blocktrans}

+

{blocktrans}For the extra authorized user list, 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}

+

{blocktrans}Only project members and admins have write access to the source. If you restrict the access to the source, anonymous access is not provided and the users must authenticate themselves with their password or SSH key.{/blocktrans}

+

{trans 'Notifications'}

+

{blocktrans}Here you can configure who should be notified about changes in a particular section. You can also configure additional addresses, like the one of a mailing list, that should be notified. (Keep in mind that you might have to register the sender address {$from_email} to let the mailing list actually accept notification emails.) Multiple email addresses must be separated through commas (',').{/blocktrans}

{/block} {block javascript}{literal} {/literal}{/block} diff --git a/src/IDF/templates/idf/base-full.html b/src/IDF/templates/idf/base-full.html index 608a50d..fcddad9 100644 --- a/src/IDF/templates/idf/base-full.html +++ b/src/IDF/templates/idf/base-full.html @@ -39,14 +39,14 @@
- {if $project}

{$project}{if $project.private}{trans 'Private project'}{/if}{$p}

{/if} + {if $project}

{$project}{if $project.private}{trans 'Private project'}{/if}{$p}{assign $url = $project.external_project_url}{if $url != ''} {/if}

{/if} {include 'idf/main-menu.html'}
 {trans 'Access Rights'}{trans 'Notification Emails'}{trans 'Access Rights'}{trans 'Notifications'}
{$form.f.downloads_access_rights.labelTag}: {if $form.f.downloads_notification_email.errors}{$form.f.downloads_notification_email.fieldErrors}{/if} +{$form.f.downloads_notification_owners_enabled|unsafe} +{$form.f.downloads_notification_owners_enabled.labelTag} +{$form.f.downloads_notification_members_enabled|unsafe} +{$form.f.downloads_notification_members_enabled.labelTag} +{$form.f.downloads_notification_email_enabled|unsafe} +{$form.f.downloads_notification_email_enabled.labelTag} {$form.f.downloads_notification_email|unsafe}
{if $form.f.wiki_notification_email.errors}{$form.f.wiki_notification_email.fieldErrors}{/if} +{$form.f.wiki_notification_owners_enabled|unsafe} +{$form.f.wiki_notification_owners_enabled.labelTag} +{$form.f.wiki_notification_members_enabled|unsafe} +{$form.f.wiki_notification_members_enabled.labelTag} +{$form.f.wiki_notification_email_enabled|unsafe} +{$form.f.wiki_notification_email_enabled.labelTag} {$form.f.wiki_notification_email|unsafe}
{if $form.f.issues_notification_email.errors}{$form.f.issues_notification_email.fieldErrors}{/if} +{$form.f.issues_notification_owners_enabled|unsafe} +{$form.f.issues_notification_owners_enabled.labelTag} +{$form.f.issues_notification_members_enabled|unsafe} +{$form.f.issues_notification_members_enabled.labelTag} +{$form.f.issues_notification_email_enabled|unsafe} +{$form.f.issues_notification_email_enabled.labelTag} {$form.f.issues_notification_email|unsafe}
{if $form.f.source_notification_email.errors}{$form.f.source_notification_email.fieldErrors}{/if} +{$form.f.source_notification_owners_enabled|unsafe} +{$form.f.source_notification_owners_enabled.labelTag} +{$form.f.source_notification_members_enabled|unsafe} +{$form.f.source_notification_members_enabled.labelTag} +{$form.f.source_notification_email_enabled|unsafe} +{$form.f.source_notification_email_enabled.labelTag} {$form.f.source_notification_email|unsafe}
  -{blocktrans} -Only project members and admins have write access to the source.
-If you restrict the access to the source, anonymous access is
-not provided and the users must authenticate themselves with their
-password or SSH key.{/blocktrans} -
{$form.f.review_access_rights.labelTag}: {if $form.f.review_access_rights.errors}{$form.f.review_access_rights.fieldErrors}{/if} {$form.f.review_access_rights|unsafe} {if $form.f.review_notification_email.errors}{$form.f.review_notification_email.fieldErrors}{/if} +{$form.f.review_notification_owners_enabled|unsafe} +{$form.f.review_notification_owners_enabled.labelTag} +{$form.f.review_notification_members_enabled|unsafe} +{$form.f.review_notification_members_enabled.labelTag} +{$form.f.review_notification_email_enabled|unsafe} +{$form.f.review_notification_email_enabled.labelTag} {$form.f.review_notification_email|unsafe}
{if $form.f.private_project.errors}{$form.f.private_project.fieldErrors}{/if} +  +{if $form.f.private_project.errors}{$form.f.private_project.fieldErrors}{/if} {$form.f.private_project|unsafe} - -{$form.f.private_project.labelTag}
 
+ + + + + + + + +
{$form.f.archive.labelTag}:{if $form.f.archive.errors}{$form.f.archive.fieldErrors}{/if} +{$form.f.archive|unsafe} +
  | {trans 'Cancel'} +
+
+{/block} +{block context} +
+

{trans 'Instructions'}

+ +

{blocktrans}The archive must include a manifest.xml file with meta information about the +files to process inside the archive. All processed files must be unique or replace existing files explicitely.{/blocktrans}

+{aurl 'url', 'IDF_Views::faqArchiveFormat'} +

{blocktrans}You can learn more about the archive format here.{/blocktrans}

+
+{/block} diff --git a/src/IDF/templates/idf/downloads/download-updated-email.txt b/src/IDF/templates/idf/downloads/download-updated-email.txt new file mode 100644 index 0000000..1f5b1cf --- /dev/null +++ b/src/IDF/templates/idf/downloads/download-updated-email.txt @@ -0,0 +1,17 @@ +{trans 'Hello,'} + +{blocktrans}A file download was updated:{/blocktrans} + +{$file.summary|safe} +{$file} - {$file.filesize|ssize} +{trans 'Project:'} {$project.name|safe} +{trans 'Submitted by:'} {$file.get_submitter|safe} +{if $tags.count()}{trans 'Labels:'} +{foreach $tags as $tag} {$tag.class|safe}:{$tag.name|safe} +{/foreach}{/if} +{trans 'Download:'} {$urlfile} +{if $file.changelog} +{trans 'Description:'} + +{$file.changelog} +{/if} diff --git a/src/IDF/templates/idf/downloads/index.html b/src/IDF/templates/idf/downloads/index.html index 9a25555..97e76b3 100644 --- a/src/IDF/templates/idf/downloads/index.html +++ b/src/IDF/templates/idf/downloads/index.html @@ -3,7 +3,7 @@ {block body} {$downloads.render} {if $isOwner or $isMember} -{aurl 'url', 'IDF_Views_Download::submit', array($project.shortname)} +{aurl 'url', 'IDF_Views_Download::create', array($project.shortname)}

+ {trans 'New Download'}

{/if} {/block} diff --git a/src/IDF/templates/idf/faq-archive-format.html b/src/IDF/templates/idf/faq-archive-format.html new file mode 100644 index 0000000..7952bb5 --- /dev/null +++ b/src/IDF/templates/idf/faq-archive-format.html @@ -0,0 +1,125 @@ +{extends "idf/base-simple.html"} +{block docclass}yui-t3{/block} +{block body} +

At the moment, this documentation is only available in English.

+ + + +

Motivation

+ +

+Adding multiple, individual downloads to a project for a release can be a tedious task if +one has to select each file manually, and then has to fill in the summary and correct labels +for each of these downloads individually. +

+ +

+InDefero therefore supports the upload of "archives" that contain multiple downloadable +files. These archives are standard PKZIP files with only one special property - they +contain an additional manifest file which describes the files that should be published. +

+ +

+Once such an archive has been uploaded and validated by InDefero, its files are extracted +and individual downloads are created for each of them. If the archive contains files +that should replace existing downloads, then InDefero takes care of this as well - +automatically. Files that exist in the archive but are not listed in the manifest are +not extracted. +

+ +

+An archive file and its manifest file can easily be compiled, either by hand with the help +of a text editor, or through an automated build system with the help of your build tool of +choice, such as Apache Ant. +

+ +

The manifest format

+ +

+The manifest is an XML file that follows a simple syntax. As it is always easier to look +at an example, here you have one: +

+ +
+<?xml version="1.0" encoding="UTF-8" ?>
+<manifest>
+  <file>
+    <name>foo-1.2.tar.gz</name>
+    <summary>Tarball</summary>
+    <replaces>foo-1.1.tar.gz</replaces>
+    <labels>
+       <label>Type:Archive</label>
+    </labels>
+  </file>
+  <file>
+    <name>foo-1.2-installer.exe</name>
+    <summary>Windows MSI Installer</summary>
+    <description>This installer needs Windows XP SP2 or later.</description>
+    <labels>
+       <label>Type:Installer</label>
+       <label>OpSys:Windows</label>
+    </labels>
+  </file>
+</manifest>
+
+ +

+This is the DTD for the format: +

+ +
+<!DOCTYPE manifest [
+<!ELEMENT manifest (file+)>
+<!ELEMENT file (name,summary,replaces?,description?,tags?)>
+<!ELEMENT name (#PCDATA)>
+<!ELEMENT summary (#PCDATA)>
+<!ELEMENT replaces (#PCDATA)>
+<!ELEMENT description (#PCDATA)>
+<!ELEMENT labels (label+)>
+<!ELEMENT label (#PCDATA)>
+]>
+
+ +

+The format is more or less self-explaining, all fields map to properties of a single download. Note +that there is a limit of six labels that you can attach to a download, similar to +what the regular file upload functionality allows. +

+ +

One special element has been introduced and this is named replaces. If this optional element +is given, InDefero looks for a file with that name in the project and acts in one of the following +two ways: +

+ +
    +
  • If the new file's name is distinct from the file's name that is given in replaces, +then the replaced file is marked as deprecated with the Other:Deprecated label +(or any other label that is configured as the very last label in the download project configuration).
  • +
  • If the new file's name is equal to the file's name that is given in replaces, +then the replaced file is deleted before the new file is created. This happens because each file name +has to be unique per project and a deprecated and a new file with the same name cannot coexist in InDefero. +Note that while the filename-based URI of the download keeps intact, the download counter will be reset by +this procedure.
  • +
+ +If no existing file is found for the replaces tag, then this is completely ignored. +

+ +

Final notes

+ +

Any file in the archive that is not enlisted in the manifest is ignored during +the upload process, so ensure that your manifest.xml contains all the file names (case +sensitive) you want to be processed.

+{/block} + +{block context} +

{trans 'Here we are, just to help you.'}

+

{trans 'Projects'}

+
    {foreach $projects as $p} +
  • {$p}
  • +{/foreach}
+{/block} diff --git a/src/IDF/templates/idf/faq.html b/src/IDF/templates/idf/faq.html index db821da..0e744df 100644 --- a/src/IDF/templates/idf/faq.html +++ b/src/IDF/templates/idf/faq.html @@ -5,6 +5,8 @@
  • {trans 'What are the keyboard shortcuts?'}
  • {trans 'How to mark an issue as duplicate?'}
  • {trans 'How can I display my head next to my comments?'}
  • +
  • {trans 'How can I embed images and other resources in my documentation pages?'}
  • +
  • {trans 'What is this "Upload Archive" functionality about?'}
  • {trans 'What is the API and how is it used?'}
  • @@ -39,7 +41,9 @@ {blocktrans}

    This is simple:

      -
    1. Write in the comments "This is a duplicate of issue 123", change 123 with the corresponding issue number.
    2. +
    3. Write in the comments "This is a duplicate of issue 123" or - if you are a member of the crew - +directly add the "duplicates" relation with the value "123" below the comment field. Change "123" +with the corresponding issue number.
    4. Change the status of the current issue to Duplicate.
    5. Submit the changes.
    {/blocktrans} @@ -48,11 +52,45 @@

    {blocktrans}You need to create an account on Gravatar, this takes about 5 minutes and is free.{/blocktrans}

    +

    {trans 'How can I embed images and other resources in my documentation pages?'}

    + +{blocktrans} +

    To embed any previously uploaded resource into your wiki page, you can use the [[!ResourceName]] syntax.

    + +

    The rendering of the resource can then be further fine-tuned: +

      +
    • [[!ImageResource, align=right, width=200]] renders "ImageResource" right-aligned and scale its width to 200
    • +
    • [[!TextResource, align=center, width=300, height=300]] renders "TextResource" in a centered, 300 by 300 px iframe
    • +
    • [[!AnyResource, preview=no]] does not render a preview of the resource, but only provides a download link (default for binary resources)
    • +
    • [[!BinaryResource, title=Download]] renders the download link of "BinaryResource" with an alternative title
    • +
    +

    + +Resources are versioned, just like wiki pages. If you update a resource, old wiki pages still show the state of the resource +at the time when the wiki page was edited. If you specifically want to update a resource on a page, you therefor need to update +the resource at first and then also the page where it is referenced, otherwise the change won't be visible until the next regular edit. +{/blocktrans} + +

    {trans 'What is this "Upload Archive" functionality about?'}

    + +{blocktrans}

    If you have to publish many files at once for a new release, it is a very tedious task +to upload them one after another and enter meta information like a summary, a description or additional +labels for each of them.

    +

    InDefero therefore supports a special archive format that is basically a standard zip file which comes with +some meta information. These meta information are kept in a special manifest file, which is distinctly kept from +the rest of the files in the archive that should be published.

    +

    Once this archive has been uploaded, InDefero reads in the meta information, unpacks the other files from +the archive and creates new individual downloads for each of them.

    {/blocktrans} + +{aurl 'url', 'IDF_Views::faqArchiveFormat'} +

    {blocktrans}Learn more about the archive format.{/blocktrans}

    +

    {trans 'What is the API and how is it used?'}

    -

    {blocktrans}The API (Application Programming Interface) is used to interact with InDefero with another program. For example, this can be used to create a desktop program to submit new tickets easily.{/blocktrans}

    {aurl 'url', 'IDF_Views::faqApi'} +

    {blocktrans}The API (Application Programming Interface) is used to interact with InDefero with another program. For example, this can be used to create a desktop program to submit new tickets easily.{/blocktrans}

    +{aurl 'url', 'IDF_Views::faqApi'}

    {blocktrans}Learn more about the API.{/blocktrans}

    - + {/block} {block context}

    {trans 'Here we are, just to help you.'}

    diff --git a/src/IDF/templates/idf/gadmin/base.html b/src/IDF/templates/idf/gadmin/base.html index 20249ee..6a6ab92 100644 --- a/src/IDF/templates/idf/gadmin/base.html +++ b/src/IDF/templates/idf/gadmin/base.html @@ -39,6 +39,7 @@ {include 'idf/main-menu.html'}