From c07aee6287dab92fb29fc838a5fafb5a80c2fb04 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Fri, 23 Dec 2011 01:03:07 +0100 Subject: [PATCH] Implement a basic, configurable project activity taxonomy. --- scripts/activitycron.php | 41 ++++++ src/IDF/ActivityTaxonomy.php | 156 +++++++++++++++++++++++ src/IDF/Migrations/23ProjectActivity.php | 42 ++++++ src/IDF/Migrations/Backup.php | 2 + src/IDF/Migrations/Install.php | 2 + src/IDF/ProjectActivity.php | 67 ++++++++++ src/IDF/conf/idf.php-dist | 29 +++++ src/IDF/relations.php | 1 + 8 files changed, 340 insertions(+) create mode 100644 scripts/activitycron.php create mode 100644 src/IDF/ActivityTaxonomy.php create mode 100644 src/IDF/Migrations/23ProjectActivity.php create mode 100644 src/IDF/ProjectActivity.php diff --git a/scripts/activitycron.php b/scripts/activitycron.php new file mode 100644 index 0000000..3a7717e --- /dev/null +++ b/scripts/activitycron.php @@ -0,0 +1,41 @@ + 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..2feee34 --- /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') === '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/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/Backup.php b/src/IDF/Migrations/Backup.php index 60f6f78..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', @@ -83,6 +84,7 @@ function IDF_Migrations_Backup_restore($folder, $name) { $models = array( 'IDF_Project', + 'IDF_ProjectActivity', 'IDF_Tag', 'IDF_Issue', 'IDF_IssueComment', diff --git a/src/IDF/Migrations/Install.php b/src/IDF/Migrations/Install.php index cefdb30..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', @@ -112,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/ProjectActivity.php b/src/IDF/ProjectActivity.php new file mode 100644 index 0000000..d994a6a --- /dev/null +++ b/src/IDF/ProjectActivity.php @@ -0,0 +1,67 @@ +_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, + ), + ); + } +} diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index a03aa1a..97bd441 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -515,5 +515,34 @@ $cfg['idf_strong_key_check'] = false; # 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/relations.php b/src/IDF/relations.php index 221bd81..b8ae4e3 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -22,6 +22,7 @@ # ***** END LICENSE BLOCK ***** */ $m = array(); +$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'),