From b85da85dfe8ce2f27cfc5dedb9b4b9bee327c058 Mon Sep 17 00:00:00 2001 From: Loic d'Anterroches Date: Fri, 14 Nov 2008 15:41:51 +0100 Subject: [PATCH] Added ticket 45, base implementation of a timeline. Still some cleaning of the code to have a nicer display of the timeline especially for the issue updates. --- src/IDF/Commit.php | 172 ++++++++++++++++++ src/IDF/Issue.php | 27 +++ src/IDF/IssueComment.php | 22 +++ src/IDF/Migrations/4Timeline.php | 54 ++++++ src/IDF/Template/IssueComment.php | 8 +- src/IDF/Template/TimelineFragment.php | 34 ++++ src/IDF/Timeline.php | 133 ++++++++++++++ src/IDF/Views/Project.php | 28 ++- src/IDF/Views/Source.php | 9 +- src/IDF/conf/idf.php-dist | 71 +++++--- src/IDF/conf/views.php | 6 + .../{project-home.html => project/home.html} | 2 +- src/IDF/templates/project/timeline.html | 51 ++++++ 13 files changed, 585 insertions(+), 32 deletions(-) create mode 100644 src/IDF/Commit.php create mode 100644 src/IDF/Migrations/4Timeline.php create mode 100644 src/IDF/Template/TimelineFragment.php create mode 100644 src/IDF/Timeline.php rename src/IDF/templates/{project-home.html => project/home.html} (90%) create mode 100644 src/IDF/templates/project/timeline.html diff --git a/src/IDF/Commit.php b/src/IDF/Commit.php new file mode 100644 index 0000000..13e71f6 --- /dev/null +++ b/src/IDF/Commit.php @@ -0,0 +1,172 @@ +_a['table'] = 'idf_commits'; + $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' => 'commits', + ), + 'author' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'Pluf_User', + 'is_null' => true, + 'verbose' => __('submitter'), + 'relate_name' => 'submitted_commit', + 'help_text' => 'This will allow us to list the latest commits of a user in its profile.', + ), + 'origauthor' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 150, + 'help_text' => 'As we do not necessary have the mapping between the author in the database and the scm, we store the scm author commit information here. That way we can update the author info later in the process.', + ), + 'scm_id' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 50, + 'index' => true, + 'help_text' => 'The id of the commit. For git, it will be the SHA1 hash, for subversion it will be the revision id.', + ), + 'summary' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 250, + 'verbose' => __('summary'), + ), + 'fullmessage' => + array( + 'type' => 'Pluf_DB_Field_Compressed', + 'blank' => true, + 'verbose' => __('changelog'), + 'help_text' => 'This is the full message of the commit.', + ), + 'creation_dtime' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => true, + 'verbose' => __('creation date'), + 'index' => true, + 'help_text' => 'Date of creation by the scm', + ), + ); + } + + function __toString() + { + return $this->summary.' - ('.$this->scm_id.')'; + } + + function _toIndex() + { + $str = str_repeat($this->summary.' ', 4).' '.$this->fullmessage; + return Pluf_Text::cleanString(html_entity_decode($str, ENT_QUOTES, 'UTF-8')); + } + + function postSave($create=false) + { + IDF_Search::index($this); + if ($create) { + IDF_Timeline::insert($this, $this->get_project(), + $this->get_author(), $this->creation_dtime); + } + } + + /** + * Create a commit from a simple class commit info of a changelog. + * + * @param stdClass Commit info + * @param IDF_Project Current project + * @return IDF_Commit + */ + public static function getOrAdd($change, $project) + { + $sql = new Pluf_SQL('project=%s AND scm_id=%s', + array($project->id, $change->commit)); + $r = Pluf::factory('IDF_Commit')->getList(array('filter'=>$sql->gen())); + if ($r->count()) { + return $r[0]; + } + $commit = new IDF_Commit(); + $commit->project = $project; + $commit->scm_id = $change->commit; + $commit->summary = $change->title; + $commit->fullmessage = $change->full_message; + $commit->author = null; + $commit->origauthor = $change->author; + $commit->creation_dtime = $change->date; + $commit->create(); + return $commit; + } + + /** + * Returns the timeline fragment for the commit. + * + * + * @param Pluf_HTTP_Request + * @return Pluf_Template_SafeString + */ + public function timelineFragment($request) + { + $tag = new IDF_Template_IssueComment(); + $out = $tag->start($this->summary, $request, false); + if ($this->fullmessage) { + $out .= '

'.$tag->start($this->fullmessage, $request, false); + } + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::commit', + array($request->project->shortname, + $this->scm_id)); + $out .= '

'.__('Commit:').' '.$this->scm_id.', '.__('by').' '.strip_tags($this->origauthor).'
'; + return Pluf_Template::markSafe($out); + } +} \ No newline at end of file diff --git a/src/IDF/Issue.php b/src/IDF/Issue.php index 398489a..b69e220 100644 --- a/src/IDF/Issue.php +++ b/src/IDF/Issue.php @@ -21,6 +21,8 @@ # # ***** END LICENSE BLOCK ***** */ +Pluf::loadFunction('Pluf_HTTP_URL_urlForView'); + /** * Base definition of an issue. * @@ -152,5 +154,30 @@ class IDF_Issue extends Pluf_Model function postSave($create=false) { IDF_Search::index($this); + if ($create) { + IDF_Timeline::insert($this, $this->get_project(), + $this->get_submitter()); + } + } + + /** + * Returns an HTML fragment used to display this issue 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) + { + $submitter = $this->get_submitter(); + $ic = (in_array($this->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o'; + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view', + array($request->project->shortname, + $this->id)); + return Pluf_Template::markSafe(sprintf(__('Issue %3$d %4$s created by %5$s'), $url, $ic, $this->id, Pluf_esc($this->summary), Pluf_esc($submitter))); } } \ No newline at end of file diff --git a/src/IDF/IssueComment.php b/src/IDF/IssueComment.php index 9e8bf5b..5619f1d 100644 --- a/src/IDF/IssueComment.php +++ b/src/IDF/IssueComment.php @@ -107,5 +107,27 @@ class IDF_IssueComment extends Pluf_Model function postSave($create=false) { + if ($create) { + // Check if more than one comment for this issue. We do + // not want to insert the first comment in the timeline as + // the issue itself is inserted. + $sql = new Pluf_SQL('issue=%s', array($this->issue)); + $co = Pluf::factory('IDF_IssueComment')->getList(array('filter'=>$sql->gen())); + if ($co->count() > 1) { + IDF_Timeline::insert($this, $this->get_issue()->get_project(), + $this->get_submitter()); + } + } + } + + public function timelineFragment($request) + { + $submitter = $this->get_submitter(); + $issue = $this->get_issue(); + $ic = (in_array($issue->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o'; + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view', + array($request->project->shortname, + $issue->id)); + return Pluf_Template::markSafe(sprintf(__('Issue %3$d %4$s updated by %5$s'), $url, $ic, $issue->id, Pluf_esc($issue->summary), Pluf_esc($submitter))); } } diff --git a/src/IDF/Migrations/4Timeline.php b/src/IDF/Migrations/4Timeline.php new file mode 100644 index 0000000..8708fad --- /dev/null +++ b/src/IDF/Migrations/4Timeline.php @@ -0,0 +1,54 @@ +model = new $model(); + $schema->createTables(); + } +} + +function IDF_Migrations_4Timeline_down($params=null) +{ + $models = array( + 'IDF_Timeline', + 'IDF_Commit', + ); + $db = Pluf::db(); + $schema = new Pluf_DB_Schema($db); + foreach ($models as $model) { + $schema->model = new $model(); + $schema->dropTables(); + } +} \ No newline at end of file diff --git a/src/IDF/Template/IssueComment.php b/src/IDF/Template/IssueComment.php index 1b7d957..a7a86c9 100644 --- a/src/IDF/Template/IssueComment.php +++ b/src/IDF/Template/IssueComment.php @@ -32,7 +32,7 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag private $request = null; private $scm = null; - function start($text, $request) + function start($text, $request, $echo=true) { $this->project = $request->project; $this->request = $request; @@ -50,7 +50,11 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag $text = preg_replace_callback('#(commit\s+)([0-9a-f]{1,40})#im', array($this, 'callbackCommit'), $text); } - echo $text; + if ($echo) { + echo $text; + } else { + return $text; + } } /** diff --git a/src/IDF/Template/TimelineFragment.php b/src/IDF/Template/TimelineFragment.php new file mode 100644 index 0000000..ae69861 --- /dev/null +++ b/src/IDF/Template/TimelineFragment.php @@ -0,0 +1,34 @@ +model_class, $item->model_id); + echo $m->timelineFragment($request); + } +} diff --git a/src/IDF/Timeline.php b/src/IDF/Timeline.php new file mode 100644 index 0000000..e85868d --- /dev/null +++ b/src/IDF/Timeline.php @@ -0,0 +1,133 @@ +_a['table'] = 'idf_timeline'; + $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, + 'relate_name' => 'thumbroll', + ), + 'author' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'Pluf_User', + 'is_null' => true, + 'help_text' => 'This will allow us to list the latest commits of a user in its profile.', + ), + 'model_class' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 150, + ), + 'model_id' => + array( + 'type' => 'Pluf_DB_Field_Integer', + 'blank' => false, + ), + 'creation_dtime' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => true, + 'index' => true, + ), + 'public_dtime' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => true, + 'index' => true, + ), + ); + } + + function __toString() + { + return $this->summary.' - ('.$this->scm_id.')'; + } + + function _toIndex() + { + $str = str_repeat($this->summary.' ', 4).' '.$this->fullmessage; + return Pluf_Text::cleanString(html_entity_decode($str, ENT_QUOTES, 'UTF-8')); + } + + function preSave($create=false) + { + if ($this->id == '') { + $this->public_dtime = gmdate('Y-m-d H:i:s'); + } + if ($this->creation_dtime == '') { + $this->creation_dtime = gmdate('Y-m-d H:i:s'); + } + } + + /** + * Easily insert an item in the timeline. + * + * @param mixed Item to be inserted + * @param IDF_Project Project of the item + * @param Pluf_User Author of the item (null) + * @param string GMT creation date time (null) + * @return bool Success + */ + public static function insert($item, $project, $author=null, $creation=null) + { + $t = new IDF_Timeline(); + $t->project = $project; + $t->author = $author; + $t->creation_dtime = (is_null($creation)) ? '' : $creation; + $t->model_id = $item->id; + $t->model_class = $item->_model; + $t->create(); + return true; + } +} diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php index 8ba02c9..2c9344d 100644 --- a/src/IDF/Views/Project.php +++ b/src/IDF/Views/Project.php @@ -45,7 +45,7 @@ class IDF_Views_Project // the first tag is the featured, the last is the deprecated. $downloads = $tags[0]->get_idf_upload_list(); } - return Pluf_Shortcuts_RenderToResponse('project-home.html', + return Pluf_Shortcuts_RenderToResponse('project/home.html', array( 'page_title' => $title, 'team' => $team, @@ -54,6 +54,32 @@ class IDF_Views_Project $request); } + /** + * Timeline of the project. + */ + public function timeline($request, $match) + { + $prj = $request->project; + $title = sprintf(__('%s Timeline'), (string) $prj); + $team = $prj->getMembershipData(); + $sql = new Pluf_SQL('project=%s', array($prj->id)); + $timeline = Pluf::factory('IDF_Timeline')->getList(array('filter'=>$sql->gen(), 'order' => 'creation_dtime DESC')); + $downloads = array(); + if ($request->rights['hasDownloadsAccess']) { + $tags = IDF_Views_Download::getDownloadTags($prj); + // the first tag is the featured, the last is the deprecated. + $downloads = $tags[0]->get_idf_upload_list(); + } + return Pluf_Shortcuts_RenderToResponse('project/timeline.html', + array( + 'page_title' => $title, + 'timeline' => $timeline, + 'team' => $team, + 'downloads' => $downloads, + ), + $request); + } + /** * Administrate the summary of a project. diff --git a/src/IDF/Views/Source.php b/src/IDF/Views/Source.php index 172277d..cd4fc0d 100644 --- a/src/IDF/Views/Source.php +++ b/src/IDF/Views/Source.php @@ -46,13 +46,18 @@ class IDF_Views_Source $branches[0])); return new Pluf_HTTP_Response_Redirect($url); } - $res = new Pluf_Template_ContextVars($scm->getChangeLog($commit, 25)); + $changes = $scm->getChangeLog($commit, 25); + // Sync with the database + foreach ($changes as $change) { + IDF_Commit::getOrAdd($change, $request->project); + } + $changes = new Pluf_Template_ContextVars($changes); $scmConf = $request->conf->getVal('scm', 'git'); return Pluf_Shortcuts_RenderToResponse('source/changelog.html', array( 'page_title' => $title, 'title' => $title, - 'changes' => $res, + 'changes' => $changes, 'commit' => $commit, 'branches' => $branches, 'scm' => $scmConf, diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 6e0c295..2e7db9a 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -26,18 +26,10 @@ $cfg = array(); // to start with, it can be practical. $cfg['debug'] = false; -// available languages -$cfg['languages'] = array('en', 'fr'); - -# SCM base configuration -$cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', - 'svn' => 'IDF_Scm_Svn', - ); - // if you have a single git repository, just put the full path to it // without trailing slash. -// If within a folder you have a series of git repository, just put -// the folder without a trailing slash. +// If within a folder you have a series of bare git repository, just +// put the folder without a trailing slash. // InDefero will automatically append a slash, the project shortname // and .git to create the name of the repository. $cfg['git_repositories'] = '/home/git/repositories/indefero.git'; @@ -51,11 +43,25 @@ $cfg['git_remote_url'] = 'git://projects.ceondo.com/indefero.git'; //$cfg['git_remote_url'] = 'git://projects.ceondo.com'; // Same as for git, you can have multiple repositories, one for each -// project or a single one for all the projects. +// project or a single one for all the projects. +// +// In the case of subversion, the admin of a project can also select a +// remote repository from the web interface. From the web interface +// you can define a local repository, local repositories are defined +// here. This if for security reasons. $cfg['svn_repositories'] = 'file:///home/svn/repositories/indefero'; $cfg['svn_repositories_unique'] = true; $cfg['svn_remote_url'] = 'http://projects.ceondo.com/svn/indefero'; +// Example of one *local* subversion repository for each project: + +// the path to the repository on disk will automatically created to be +// 'file:///home/svn/repositories'.'/'.$project->shortname +// the url will be generated the same way: +// 'http://projects.ceondo.com/svn'.'/'.$project->shortname +// $cfg['svn_repositories'] = 'file:///home/svn/repositories'; +// $cfg['svn_repositories_unique'] = false; +// $cfg['svn_remote_url'] = 'http://projects.ceondo.com/svn'; // admins will get an email in case of errors in the system in non // debug mode. @@ -68,7 +74,6 @@ $cfg['send_emails'] = true; $cfg['mail_backend'] = 'smtp'; $cfg['mail_host'] = 'localhost'; $cfg['mail_port'] = 25; -$cfg['pear_path'] = '/usr/share/php'; // Paths/Url configuration # @@ -94,9 +99,6 @@ $cfg['upload_path'] = '/path/to/media/upload'; # $cfg['upload_issue_path'] = '/path/to/attachments'; -$cfg['login_success_url'] = $cfg['url_base'].$cfg['idf_base']; -$cfg['after_logout_page'] = $cfg['url_base'].$cfg['idf_base']; - // write here a long random string unique for this installation. This // is critical to put a long string. $cfg['secret_key'] = ''; @@ -113,6 +115,23 @@ $cfg['bounce_email'] = 'no-reply@example.com'; // It is mandatory if you are using the template system. $cfg['tmp_folder'] = '/tmp'; +// Database configuration +// For testing we are using in memory SQLite database. +$cfg['db_login'] = 'www'; +$cfg['db_password'] = ''; +$cfg['db_server'] = ''; +$cfg['db_version'] = ''; +$cfg['db_table_prefix'] = ''; +$cfg['db_engine'] = 'PostgreSQL'; // SQLite is also well tested or MySQL +$cfg['db_database'] = 'website'; // put absolute path to the db if you + // are using SQLite + +// -- From this point you should not need to update anything. -- +$cfg['pear_path'] = '/usr/share/php'; + +$cfg['login_success_url'] = $cfg['url_base'].$cfg['idf_base']; +$cfg['after_logout_page'] = $cfg['url_base'].$cfg['idf_base']; + // Caching of the scm commands. $cfg['cache_engine'] = 'Pluf_Cache_File'; $cfg['cache_timeout'] = 300; @@ -123,17 +142,6 @@ $cfg['template_folders'] = array( dirname(__FILE__).'/../templates', ); -// Database configuration -// For testing we are using in memory SQLite database. -$cfg['db_login'] = 'www'; -$cfg['db_password'] = ''; -$cfg['db_server'] = ''; -$cfg['db_version'] = ''; -$cfg['db_table_prefix'] = ''; -$cfg['db_engine'] = 'PostgreSQL'; // SQLite is also well tested or MySQL -$cfg['db_database'] = 'website'; - -// From this point you should not need to update anything. $cfg['installed_apps'] = array('Pluf', 'IDF'); $cfg['pluf_use_rowpermission'] = true; $cfg['middleware_classes'] = array( @@ -146,9 +154,20 @@ $cfg['idf_views'] = dirname(__FILE__).'/views.php'; $cfg['template_tags'] = array( 'hotkey' => 'IDF_Template_HotKey', 'issuetext' => 'IDF_Template_IssueComment', + 'timeline' => 'IDF_Template_TimelineFragment', ); $cfg['template_modifiers'] = array( 'size' => 'IDF_Views_Source_PrettySize', 'markdown' => 'IDF_Template_Markdown_filter', ); + +// available languages +$cfg['languages'] = array('en', 'fr'); + +# SCM base configuration +$cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', + 'svn' => 'IDF_Scm_Svn', + ); + + return $cfg; diff --git a/src/IDF/conf/views.php b/src/IDF/conf/views.php index 4b89173..52fd269 100644 --- a/src/IDF/conf/views.php +++ b/src/IDF/conf/views.php @@ -85,6 +85,12 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/$#', 'model' => 'IDF_Views_Project', 'method' => 'home'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views_Project', + 'method' => 'timeline'); + $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/$#', 'base' => $base, 'priority' => 4, diff --git a/src/IDF/templates/project-home.html b/src/IDF/templates/project/home.html similarity index 90% rename from src/IDF/templates/project-home.html rename to src/IDF/templates/project/home.html index ee2b80a..4c83e81 100644 --- a/src/IDF/templates/project-home.html +++ b/src/IDF/templates/project/home.html @@ -3,7 +3,7 @@ {block tabhome} class="active"{/block} {block subtabs}
-{trans 'Welcome'} {superblock} +{trans 'Welcome'} | {trans 'Latest Changes'}{superblock}
{/block} {block body} diff --git a/src/IDF/templates/project/timeline.html b/src/IDF/templates/project/timeline.html new file mode 100644 index 0000000..afbdc6e --- /dev/null +++ b/src/IDF/templates/project/timeline.html @@ -0,0 +1,51 @@ +{extends "base.html"} +{block docclass}yui-t2{/block} +{block tabhome} class="active"{/block} +{block subtabs} +
+{trans 'Welcome'} | {trans 'Latest Changes'}{superblock} +
+{/block} +{block body} + + + + + + + + +{foreach $timeline as $item} + + + + +{/foreach} + +
{trans 'Age'}{trans 'Change'}
{$item.creation_dtime|dateago:"wihtout"}{timeline $item, $request}
+{/block} +{block context} +{if count($downloads) > 0} +

{trans 'Featured Downloads'}
+{foreach $downloads as $download} +{$download}
+{/foreach} + {trans 'show more...'} +{/if} +{assign $ko = 'owners'} +{assign $km = 'members'} +

{trans 'Development Team'}
+{trans 'Admins'}
+{foreach $team[$ko] as $owner}{aurl 'url', 'IDF_Views_User::view', array($owner.login)} +{$owner}
+{/foreach} +{if count($team[$km]) > 0} +{trans 'Happy Crew'}
+{foreach $team[$km] as $member}{aurl 'url', 'IDF_Views_User::view', array($member.login)} +{$member}
+{/foreach} +{/if} +

+{/block} + +