| 1 | <?php␊ |
| 2 | /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */␊ |
| 3 | /*␊ |
| 4 | # ***** BEGIN LICENSE BLOCK *****␊ |
| 5 | # This file is part of InDefero, an open source project management application.␊ |
| 6 | # Copyright (C) 2008-2011 CĂ©ondo Ltd and contributors.␊ |
| 7 | #␊ |
| 8 | # InDefero is free software; you can redistribute it and/or modify␊ |
| 9 | # it under the terms of the GNU General Public License as published by␊ |
| 10 | # the Free Software Foundation; either version 2 of the License, or␊ |
| 11 | # (at your option) any later version.␊ |
| 12 | #␊ |
| 13 | # InDefero is distributed in the hope that it will be useful,␊ |
| 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of␊ |
| 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the␊ |
| 16 | # GNU General Public License for more details.␊ |
| 17 | #␊ |
| 18 | # You should have received a copy of the GNU General Public License␊ |
| 19 | # along with this program; if not, write to the Free Software␊ |
| 20 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA␊ |
| 21 | #␊ |
| 22 | # ***** END LICENSE BLOCK ***** */␊ |
| 23 | ␊ |
| 24 | /**␊ |
| 25 | * Base definition of a project.␊ |
| 26 | *␊ |
| 27 | * The issue management system can be used to manage several projects␊ |
| 28 | * at the same time.␊ |
| 29 | */␊ |
| 30 | class IDF_Project extends Pluf_Model␊ |
| 31 | {␊ |
| 32 | public $_model = __CLASS__;␊ |
| 33 | public $_extra_cache = array();␊ |
| 34 | protected $_pconf = null;␊ |
| 35 | /**␊ |
| 36 | * Check if the project as one restricted tab.␊ |
| 37 | *␊ |
| 38 | * This is the cached information.␊ |
| 39 | *␊ |
| 40 | * @see self::isRestricted␊ |
| 41 | */␊ |
| 42 | protected $_isRestricted = null;␊ |
| 43 | ␊ |
| 44 | function init()␊ |
| 45 | {␊ |
| 46 | $this->_pconf = null;␊ |
| 47 | $this->_extra_cache = array();␊ |
| 48 | $this->_a['table'] = 'idf_projects';␊ |
| 49 | $this->_a['model'] = __CLASS__;␊ |
| 50 | $this->_a['cols'] = array(␊ |
| 51 | // It is mandatory to have an "id" column.␊ |
| 52 | 'id' =>␊ |
| 53 | array(␊ |
| 54 | 'type' => 'Pluf_DB_Field_Sequence',␊ |
| 55 | 'blank' => true,␊ |
| 56 | ),␊ |
| 57 | 'name' =>␊ |
| 58 | array(␊ |
| 59 | 'type' => 'Pluf_DB_Field_Varchar',␊ |
| 60 | 'blank' => false,␊ |
| 61 | 'size' => 250,␊ |
| 62 | 'verbose' => __('name'),␊ |
| 63 | ),␊ |
| 64 | 'shortname' =>␊ |
| 65 | array(␊ |
| 66 | 'type' => 'Pluf_DB_Field_Varchar',␊ |
| 67 | 'blank' => false,␊ |
| 68 | 'size' => 50,␊ |
| 69 | 'verbose' => __('short name'),␊ |
| 70 | 'help_text' => __('Used in the URL to access the project, must be short with only letters and numbers.'),␊ |
| 71 | 'unique' => true,␊ |
| 72 | ),␊ |
| 73 | 'shortdesc' =>␊ |
| 74 | array(␊ |
| 75 | 'type' => 'Pluf_DB_Field_Varchar',␊ |
| 76 | 'blank' => false,␊ |
| 77 | 'size' => 255,␊ |
| 78 | 'verbose' => __('short description'),␊ |
| 79 | 'help_text' => __('A one line description of the project.'),␊ |
| 80 | ),␊ |
| 81 | 'description' =>␊ |
| 82 | array(␊ |
| 83 | 'type' => 'Pluf_DB_Field_Text',␊ |
| 84 | 'blank' => false,␊ |
| 85 | 'size' => 250,␊ |
| 86 | 'verbose' => __('description'),␊ |
| 87 | 'help_text' => __('The description can be extended using the Markdown syntax.'),␊ |
| 88 | ),␊ |
| 89 | 'tags' =>␊ |
| 90 | array(␊ |
| 91 | 'type' => 'Pluf_DB_Field_Manytomany',␊ |
| 92 | 'blank' => true,␊ |
| 93 | 'model' => 'IDF_Tag',␊ |
| 94 | 'verbose' => __('labels'),␊ |
| 95 | ),␊ |
| 96 | 'private' =>␊ |
| 97 | array(␊ |
| 98 | 'type' => 'Pluf_DB_Field_Integer',␊ |
| 99 | 'blank' => false,␊ |
| 100 | 'verbose' => __('private'),␊ |
| 101 | 'default' => 0,␊ |
| 102 | ),␊ |
| 103 | 'current_activity' =>␊ |
| 104 | array(␊ |
| 105 | 'type' => 'Pluf_DB_Field_Foreignkey',␊ |
| 106 | 'model' => 'IDF_ProjectActivity',␊ |
| 107 | 'blank' => true,␊ |
| 108 | 'verbose' => __('current project activity'),␊ |
| 109 | ),␊ |
| 110 | );␊ |
| 111 | $activityTable = $this->_con->pfx.'idf_projectactivities';␊ |
| 112 | $tagTable = $this->_con->pfx.'idf_project_idf_tag_assoc';␊ |
| 113 | $this->_a['views'] = array(␊ |
| 114 | 'join_activities_and_tags' =>␊ |
| 115 | array(␊ |
| 116 | 'join' => 'LEFT JOIN '.$activityTable.' ON current_activity='.$activityTable.'.id '␊ |
| 117 | .'LEFT JOIN '.$tagTable.' ON idf_project_id='.$this->getSqlTable().'.id',␊ |
| 118 | 'select' => $this->getSelect().', date, value',␊ |
| 119 | 'group' => $this->getSqlTable().'.id',␊ |
| 120 | 'props' => array(␊ |
| 121 | 'date' => 'current_activity_date',␊ |
| 122 | 'value' => 'current_activity_value'␊ |
| 123 | ),␊ |
| 124 | ),␊ |
| 125 | );␊ |
| 126 | }␊ |
| 127 | ␊ |
| 128 | ␊ |
| 129 | /**␊ |
| 130 | * String representation of the abstract.␊ |
| 131 | */␊ |
| 132 | function __toString()␊ |
| 133 | {␊ |
| 134 | return $this->name;␊ |
| 135 | }␊ |
| 136 | ␊ |
| 137 | /**␊ |
| 138 | * String ready for indexation.␊ |
| 139 | */␊ |
| 140 | function _toIndex()␊ |
| 141 | {␊ |
| 142 | return '';␊ |
| 143 | }␊ |
| 144 | ␊ |
| 145 | ␊ |
| 146 | function preSave($create=false)␊ |
| 147 | {␊ |
| 148 | if ($this->id == '') {␊ |
| 149 | $this->creation_dtime = gmdate('Y-m-d H:i:s');␊ |
| 150 | }␊ |
| 151 | $this->modif_dtime = gmdate('Y-m-d H:i:s');␊ |
| 152 | }␊ |
| 153 | ␊ |
| 154 | public static function getOr404($shortname)␊ |
| 155 | {␊ |
| 156 | $sql = new Pluf_SQL('shortname=%s', array(trim($shortname)));␊ |
| 157 | $projects = Pluf::factory(__CLASS__)->getList(array('filter' => $sql->gen()));␊ |
| 158 | if ($projects->count() != 1) {␊ |
| 159 | throw new Pluf_HTTP_Error404(sprintf(__('Project "%s" not found.'),␊ |
| 160 | $shortname));␊ |
| 161 | }␊ |
| 162 | return $projects[0];␊ |
| 163 | }␊ |
| 164 | ␊ |
| 165 | /**␊ |
| 166 | * Returns the number of open/closed issues.␊ |
| 167 | *␊ |
| 168 | * @param string Status ('open'), 'closed'␊ |
| 169 | * @param IDF_Tag Subfilter with a label (null)␊ |
| 170 | * @return int Count␊ |
| 171 | */␊ |
| 172 | public function getIssueCountByOwner($status='open')␊ |
| 173 | {␊ |
| 174 | switch ($status) {␊ |
| 175 | case 'open':␊ |
| 176 | $tags = implode(',', $this->getTagIdsByStatus('open'));␊ |
| 177 | break;␊ |
| 178 | case 'closed':␊ |
| 179 | default:␊ |
| 180 | $tags = implode(',', $this->getTagIdsByStatus('closed'));␊ |
| 181 | break;␊ |
| 182 | }␊ |
| 183 | $sqlIssueTable = Pluf::factory('IDF_Issue')->getSqlTable();␊ |
| 184 | $query = "SELECT uid AS id,COUNT(uid) AS nb␊ |
| 185 | FROM (␊ |
| 186 | SELECT COALESCE(owner, -1) AS uid␊ |
| 187 | FROM $sqlIssueTable␊ |
| 188 | WHERE status IN ($tags)␊ |
| 189 | ) AS ff␊ |
| 190 | GROUP BY uid";␊ |
| 191 | ␊ |
| 192 | $db = Pluf::db();␊ |
| 193 | $dbData = $db->select($query);␊ |
| 194 | $ownerStatistics = array();␊ |
| 195 | foreach ($dbData as $k => $v) {␊ |
| 196 | $key = ($v['id'] === '-1') ? null : $v['id'];␊ |
| 197 | $ownerStatistics[$key] = (int)$v['nb'];␊ |
| 198 | }␊ |
| 199 | ␊ |
| 200 | arsort($ownerStatistics);␊ |
| 201 | ␊ |
| 202 | return $ownerStatistics;␊ |
| 203 | }␊ |
| 204 | ␊ |
| 205 | /**␊ |
| 206 | * Returns the number of open/closed issues.␊ |
| 207 | *␊ |
| 208 | * @param string Status ('open'), 'closed'␊ |
| 209 | * @param IDF_Tag Subfilter with a label (null)␊ |
| 210 | * @param array Restrict further to a list of ids␊ |
| 211 | * @return int Count␊ |
| 212 | */␊ |
| 213 | public function getIssueCountByStatus($status='open', $label=null, $ids=array())␊ |
| 214 | {␊ |
| 215 | switch ($status) {␊ |
| 216 | case 'open':␊ |
| 217 | $key = 'labels_issue_open';␊ |
| 218 | $default = IDF_Form_IssueTrackingConf::init_open;␊ |
| 219 | break;␊ |
| 220 | case 'closed':␊ |
| 221 | default:␊ |
| 222 | $key = 'labels_issue_closed';␊ |
| 223 | $default = IDF_Form_IssueTrackingConf::init_closed;␊ |
| 224 | break;␊ |
| 225 | }␊ |
| 226 | $tags = array();␊ |
| 227 | foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) {␊ |
| 228 | $tags[] = (int)$tag->id;␊ |
| 229 | }␊ |
| 230 | if (count($tags) == 0) return array();␊ |
| 231 | $sql = new Pluf_SQL(sprintf('project=%%s AND status IN (%s)', implode(', ', $tags)), array($this->id));␊ |
| 232 | if (!is_null($label)) {␊ |
| 233 | $sql2 = new Pluf_SQL('idf_tag_id=%s', array($label->id));␊ |
| 234 | $sql->SAnd($sql2);␊ |
| 235 | }␊ |
| 236 | if (count($ids) > 0) {␊ |
| 237 | $sql2 = new Pluf_SQL(sprintf('id IN (%s)', implode(', ', $ids)));␊ |
| 238 | $sql->SAnd($sql2);␊ |
| 239 | }␊ |
| 240 | $params = array('filter' => $sql->gen());␊ |
| 241 | if (!is_null($label)) { $params['view'] = 'join_tags'; }␊ |
| 242 | $gissue = new IDF_Issue();␊ |
| 243 | return $gissue->getCount($params);␊ |
| 244 | }␊ |
| 245 | ␊ |
| 246 | /**␊ |
| 247 | * Get the tags for a specific list of issues.␊ |
| 248 | *␊ |
| 249 | * @param string Status ('open') or 'closed'␊ |
| 250 | * @param array A list of issue ids␊ |
| 251 | * @return array An array of tag objects␊ |
| 252 | */␊ |
| 253 | public function getTagsByIssues($issue_ids=array())␊ |
| 254 | {␊ |
| 255 | // make the below query always a valid one␊ |
| 256 | if (count($issue_ids) == 0) $issue_ids[] = 0;␊ |
| 257 | ␊ |
| 258 | $assocTable = $this->_con->pfx.'idf_issue_idf_tag_assoc';␊ |
| 259 | $query = sprintf(␊ |
| 260 | 'SELECT DISTINCT idf_tag_id FROM %s '.␊ |
| 261 | 'WHERE idf_issue_id IN (%s) '.␊ |
| 262 | 'GROUP BY idf_tag_id',␊ |
| 263 | $assocTable, implode(',', $issue_ids)␊ |
| 264 | );␊ |
| 265 | ␊ |
| 266 | $db = Pluf::db();␊ |
| 267 | $dbData = $db->select($query);␊ |
| 268 | $ids = array(0);␊ |
| 269 | foreach ($dbData as $data) {␊ |
| 270 | $ids[] = $data['idf_tag_id'];␊ |
| 271 | }␊ |
| 272 | ␊ |
| 273 | $sql = new Pluf_SQL(sprintf('id IN (%s)', implode(', ', $ids)));␊ |
| 274 | $model = new IDF_Tag();␊ |
| 275 | return $model->getList(array('filter' => $sql->gen()));␊ |
| 276 | }␊ |
| 277 | ␊ |
| 278 | /**␊ |
| 279 | * Get the open/closed tag ids as they are often used when doing␊ |
| 280 | * listings.␊ |
| 281 | *␊ |
| 282 | * As this can be often used, the info are cached.␊ |
| 283 | *␊ |
| 284 | * @param string Status ('open') or 'closed'␊ |
| 285 | * @param bool Force cache refresh (false)␊ |
| 286 | * @return array Ids of the open/closed tags␊ |
| 287 | */␊ |
| 288 | public function getTagIdsByStatus($status='open', $cache_refresh=false)␊ |
| 289 | {␊ |
| 290 | if (!$cache_refresh␊ |
| 291 | and isset($this->_extra_cache['getTagIdsByStatus-'.$status])) {␊ |
| 292 | return $this->_extra_cache['getTagIdsByStatus-'.$status];␊ |
| 293 | }␊ |
| 294 | switch ($status) {␊ |
| 295 | case 'open':␊ |
| 296 | $key = 'labels_issue_open';␊ |
| 297 | $default = IDF_Form_IssueTrackingConf::init_open;␊ |
| 298 | break;␊ |
| 299 | case 'closed':␊ |
| 300 | default:␊ |
| 301 | $key = 'labels_issue_closed';␊ |
| 302 | $default = IDF_Form_IssueTrackingConf::init_closed;␊ |
| 303 | break;␊ |
| 304 | }␊ |
| 305 | $tags = array();␊ |
| 306 | foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) {␊ |
| 307 | $tags[] = (int) $tag->id;␊ |
| 308 | }␊ |
| 309 | $this->_extra_cache['getTagIdsByStatus-'.$status] = $tags;␊ |
| 310 | return $tags;␊ |
| 311 | }␊ |
| 312 | ␊ |
| 313 | /**␊ |
| 314 | * Convert the definition of tags in the configuration into the␊ |
| 315 | * corresponding list of tags.␊ |
| 316 | *␊ |
| 317 | * @param string Configuration key where the tag is.␊ |
| 318 | * @param string Default config if nothing in the db.␊ |
| 319 | * @param string Default class.␊ |
| 320 | * @return array List of tags␊ |
| 321 | */␊ |
| 322 | public function getTagsFromConfig($cfg_key, $default, $dclass='Other')␊ |
| 323 | {␊ |
| 324 | $conf = $this->getConf();␊ |
| 325 | $tags = array();␊ |
| 326 | foreach (preg_split("/\015\012|\015|\012/", $conf->getVal($cfg_key, $default), -1, PREG_SPLIT_NO_EMPTY) as $s) {␊ |
| 327 | $_s = explode('=', $s, 2);␊ |
| 328 | $v = trim($_s[0]);␊ |
| 329 | $_v = explode(':', $v, 2);␊ |
| 330 | if (count($_v) > 1) {␊ |
| 331 | $class = trim($_v[0]);␊ |
| 332 | $name = trim($_v[1]);␊ |
| 333 | } else {␊ |
| 334 | $name = trim($_s[0]);␊ |
| 335 | $class = $dclass;␊ |
| 336 | }␊ |
| 337 | $tags[] = IDF_Tag::add($name, $this, $class);␊ |
| 338 | }␊ |
| 339 | return $tags;␊ |
| 340 | }␊ |
| 341 | ␊ |
| 342 | /**␊ |
| 343 | * Returns a list of relations which are available in this project as␊ |
| 344 | * associative array. Each key-value pair marks a set of orthogonal␊ |
| 345 | * relations. To ease processing, each of these pairs is included twice␊ |
| 346 | * in the array, once as key1 => key2 and once as key2 => key1.␊ |
| 347 | *␊ |
| 348 | * @return array List of relation names␊ |
| 349 | */␊ |
| 350 | public function getRelationsFromConfig()␊ |
| 351 | {␊ |
| 352 | $conf = $this->getConf();␊ |
| 353 | $rel = $conf->getVal('issue_relations', IDF_Form_IssueTrackingConf::init_relations);␊ |
| 354 | $relations = array();␊ |
| 355 | foreach (preg_split("/\015\012|\015|\012/", $rel, -1, PREG_SPLIT_NO_EMPTY) as $s) {␊ |
| 356 | $verbs = preg_split("/\s*,\s*/", $s, 2);␊ |
| 357 | if (count($verbs) == 1)␊ |
| 358 | $relations += array($verbs[0] => $verbs[0]);␊ |
| 359 | else␊ |
| 360 | $relations += array($verbs[0] => $verbs[1], $verbs[1] => $verbs[0]);␊ |
| 361 | }␊ |
| 362 | return $relations;␊ |
| 363 | }␊ |
| 364 | ␊ |
| 365 | /**␊ |
| 366 | * Return membership data.␊ |
| 367 | *␊ |
| 368 | * The array has 3 keys: 'members', 'owners' and 'authorized'.␊ |
| 369 | *␊ |
| 370 | * The list of users is only taken using the row level permission␊ |
| 371 | * table. That is, if you set a user as administrator, he will␊ |
| 372 | * have the member and owner rights but will not appear in the␊ |
| 373 | * lists.␊ |
| 374 | *␊ |
| 375 | * @param string Format ('objects'), 'string'.␊ |
| 376 | * @return mixed Array of Pluf_User or newline separated list of logins.␊ |
| 377 | */␊ |
| 378 | public function getMembershipData($fmt='objects')␊ |
| 379 | {␊ |
| 380 | $mperm = Pluf_Permission::getFromString('IDF.project-member');␊ |
| 381 | $operm = Pluf_Permission::getFromString('IDF.project-owner');␊ |
| 382 | $aperm = Pluf_Permission::getFromString('IDF.project-authorized-user');␊ |
| 383 | $grow = new Pluf_RowPermission();␊ |
| 384 | $db =& Pluf::db();␊ |
| 385 | $false = Pluf_DB_BooleanToDb(false, $db);␊ |
| 386 | $sql = new Pluf_SQL('model_class=%s AND model_id=%s AND owner_class=%s AND permission=%s AND negative='.$false,␊ |
| 387 | array('IDF_Project', $this->id, 'Pluf_User', $operm->id));␊ |
| 388 | $owners = new Pluf_Template_ContextVars(array());␊ |
| 389 | foreach ($grow->getList(array('filter' => $sql->gen())) as $row) {␊ |
| 390 | if ($fmt == 'objects') {␊ |
| 391 | $owners[] = Pluf::factory('Pluf_User', $row->owner_id);␊ |
| 392 | } else {␊ |
| 393 | $owners[] = Pluf::factory('Pluf_User', $row->owner_id)->login;␊ |
| 394 | }␊ |
| 395 | }␊ |
| 396 | $sql = new Pluf_SQL('model_class=%s AND model_id=%s AND owner_class=%s AND permission=%s AND negative='.$false,␊ |
| 397 | array('IDF_Project', $this->id, 'Pluf_User', $mperm->id));␊ |
| 398 | $members = new Pluf_Template_ContextVars(array());␊ |
| 399 | foreach ($grow->getList(array('filter' => $sql->gen())) as $row) {␊ |
| 400 | if ($fmt == 'objects') {␊ |
| 401 | $members[] = Pluf::factory('Pluf_User', $row->owner_id);␊ |
| 402 | } else {␊ |
| 403 | $members[] = Pluf::factory('Pluf_User', $row->owner_id)->login;␊ |
| 404 | }␊ |
| 405 | }␊ |
| 406 | $authorized = new Pluf_Template_ContextVars(array());␊ |
| 407 | if ($aperm != false) {␊ |
| 408 | $sql = new Pluf_SQL('model_class=%s AND model_id=%s AND owner_class=%s AND permission=%s AND negative='.$false,␊ |
| 409 | array('IDF_Project', $this->id, 'Pluf_User', $aperm->id));␊ |
| 410 | foreach ($grow->getList(array('filter' => $sql->gen())) as $row) {␊ |
| 411 | if ($fmt == 'objects') {␊ |
| 412 | $authorized[] = Pluf::factory('Pluf_User', $row->owner_id);␊ |
| 413 | } else {␊ |
| 414 | $authorized[] = Pluf::factory('Pluf_User', $row->owner_id)->login;␊ |
| 415 | }␊ |
| 416 | }␊ |
| 417 | }␊ |
| 418 | if ($fmt == 'objects') {␊ |
| 419 | return new Pluf_Template_ContextVars(array('members' => $members, 'owners' => $owners, 'authorized' => $authorized));␊ |
| 420 | } else {␊ |
| 421 | return array('members' => implode("\n", (array) $members),␊ |
| 422 | 'owners' => implode("\n", (array) $owners),␊ |
| 423 | 'authorized' => implode("\n", (array) $authorized),␊ |
| 424 | );␊ |
| 425 | }␊ |
| 426 | }␊ |
| 427 | ␊ |
| 428 | /**␊ |
| 429 | * Generate the tag clouds.␊ |
| 430 | *␊ |
| 431 | * Return an array of tags sorted by class, then name. Each tag␊ |
| 432 | * get the extra property 'nb_use' for the number of use in the␊ |
| 433 | * project.␊ |
| 434 | *␊ |
| 435 | * @param string ('issues') 'closed_issues', 'wiki' or 'downloads'␊ |
| 436 | * @return ArrayObject of IDF_Tag␊ |
| 437 | */␊ |
| 438 | public function getTagCloud($what='issues')␊ |
| 439 | {␊ |
| 440 | $tag_t = Pluf::factory('IDF_Tag')->getSqlTable();␊ |
| 441 | if ($what == 'issues' or $what == 'closed_issues') {␊ |
| 442 | $what_t = Pluf::factory('IDF_Issue')->getSqlTable();␊ |
| 443 | $asso_t = $this->_con->pfx.'idf_issue_idf_tag_assoc';␊ |
| 444 | if ($what == 'issues') {␊ |
| 445 | $ostatus = $this->getTagIdsByStatus('open');␊ |
| 446 | } else {␊ |
| 447 | $ostatus = $this->getTagIdsByStatus('closed');␊ |
| 448 | }␊ |
| 449 | if (count($ostatus) == 0) $ostatus[] = 0;␊ |
| 450 | $sql = sprintf('SELECT '.$tag_t.'.id AS id, COUNT(*) AS nb_use FROM '.$tag_t.' '."\n".␊ |
| 451 | 'LEFT JOIN '.$asso_t.' ON idf_tag_id='.$tag_t.'.id '."\n".␊ |
| 452 | 'LEFT JOIN '.$what_t.' ON idf_issue_id='.$what_t.'.id '."\n".␊ |
| 453 | 'WHERE idf_tag_id IS NOT NULL AND '.$what_t.'.status IN (%s) 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',␊ |
| 454 | implode(', ', $ostatus));␊ |
| 455 | } elseif ($what == 'wiki') {␊ |
| 456 | $dep_ids = IDF_Views_Wiki::getDeprecatedPagesIds($this);␊ |
| 457 | $extra = '';␊ |
| 458 | if (count($dep_ids)) {␊ |
| 459 | $extra = ' AND idf_wiki_page_id NOT IN ('.implode(', ', $dep_ids).') ';␊ |
| 460 | }␊ |
| 461 | $what_t = Pluf::factory('IDF_Wiki_Page')->getSqlTable();␊ |
| 462 | $asso_t = $this->_con->pfx.'idf_tag_idf_wiki_page_assoc';␊ |
| 463 | $sql = 'SELECT '.$tag_t.'.id AS id, COUNT(*) AS nb_use FROM '.$tag_t.' '."\n".␊ |
| 464 | 'LEFT JOIN '.$asso_t.' ON idf_tag_id='.$tag_t.'.id '."\n".␊ |
| 465 | 'LEFT JOIN '.$what_t.' ON idf_wiki_page_id='.$what_t.'.id '."\n".␊ |
| 466 | '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';␊ |
| 467 | } elseif ($what == 'downloads') {␊ |
| 468 | $dep_ids = IDF_Views_Download::getDeprecatedFilesIds($this);␊ |
| 469 | $extra = '';␊ |
| 470 | if (count($dep_ids)) {␊ |
| 471 | $extra = ' AND idf_upload_id NOT IN ('.implode(', ', $dep_ids).') ';␊ |
| 472 | }␊ |
| 473 | $what_t = Pluf::factory('IDF_Upload')->getSqlTable();␊ |
| 474 | $asso_t = $this->_con->pfx.'idf_tag_idf_upload_assoc';␊ |
| 475 | $sql = 'SELECT '.$tag_t.'.id AS id, COUNT(*) AS nb_use FROM '.$tag_t.' '."\n".␊ |
| 476 | 'LEFT JOIN '.$asso_t.' ON idf_tag_id='.$tag_t.'.id '."\n".␊ |
| 477 | 'LEFT JOIN '.$what_t.' ON idf_upload_id='.$what_t.'.id '."\n".␊ |
| 478 | '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';␊ |
| 479 | }␊ |
| 480 | $tags = array();␊ |
| 481 | foreach ($this->_con->select($sql) as $idc) {␊ |
| 482 | $tag = new IDF_Tag($idc['id']);␊ |
| 483 | $tag->nb_use = $idc['nb_use'];␊ |
| 484 | // group by class␊ |
| 485 | if (!array_key_exists($tag->class, $tags)) {␊ |
| 486 | $tags[$tag->class] = array();␊ |
| 487 | }␊ |
| 488 | $tags[$tag->class][] = $tag;␊ |
| 489 | }␊ |
| 490 | return new Pluf_Template_ContextVars($tags);␊ |
| 491 | }␊ |
| 492 | ␊ |
| 493 | /**␊ |
| 494 | * Get the repository size.␊ |
| 495 | *␊ |
| 496 | * @param bool Force to skip the cache (false)␊ |
| 497 | * @return int Size in byte or -1 if not available␊ |
| 498 | */␊ |
| 499 | public function getRepositorySize($force=false)␊ |
| 500 | {␊ |
| 501 | $last_eval = $this->getConf()->getVal('repository_size_check_date', 0);␊ |
| 502 | if (Pluf::f('idf_no_size_check', false) or␊ |
| 503 | (!$force and $last_eval > time()-172800)) {␊ |
| 504 | return $this->getConf()->getVal('repository_size', -1);␊ |
| 505 | }␊ |
| 506 | $this->getConf()->setVal('repository_size_check_date', time());␊ |
| 507 | $scm = IDF_Scm::get($this);␊ |
| 508 | $this->getConf()->setVal('repository_size', $scm->getRepositorySize());␊ |
| 509 | return $this->getConf()->getVal('repository_size', -1);␊ |
| 510 | }␊ |
| 511 | ␊ |
| 512 | /**␊ |
| 513 | * Get the access url to the repository.␊ |
| 514 | *␊ |
| 515 | * This will return the right url based on the user.␊ |
| 516 | *␊ |
| 517 | * @param Pluf_User The user (null)␊ |
| 518 | * @param string A specific commit to access␊ |
| 519 | */␊ |
| 520 | public function getSourceAccessUrl($user=null, $commit=null)␊ |
| 521 | {␊ |
| 522 | $right = $this->getConf()->getVal('source_access_rights', 'all');␊ |
| 523 | if (($user == null or $user->isAnonymous())␊ |
| 524 | and $right == 'all' and !$this->private) {␊ |
| 525 | return $this->getRemoteAccessUrl($commit);␊ |
| 526 | }␊ |
| 527 | return $this->getWriteRemoteAccessUrl($user, $commit);␊ |
| 528 | }␊ |
| 529 | ␊ |
| 530 | ␊ |
| 531 | /**␊ |
| 532 | * Get the remote access url to the repository.␊ |
| 533 | *␊ |
| 534 | * This will always return the anonymous access url.␊ |
| 535 | *␊ |
| 536 | * @param string A specific commit to access␊ |
| 537 | */␊ |
| 538 | public function getRemoteAccessUrl($commit=null)␊ |
| 539 | {␊ |
| 540 | $conf = $this->getConf();␊ |
| 541 | $scm = $conf->getVal('scm', 'git');␊ |
| 542 | $scms = Pluf::f('allowed_scm');␊ |
| 543 | Pluf::loadClass($scms[$scm]);␊ |
| 544 | return call_user_func(array($scms[$scm], 'getAnonymousAccessUrl'),␊ |
| 545 | $this, $commit);␊ |
| 546 | }␊ |
| 547 | ␊ |
| 548 | /**␊ |
| 549 | * Get the remote write access url to the repository.␊ |
| 550 | *␊ |
| 551 | * Some SCM have a remote access URL to write which is not the␊ |
| 552 | * same as the one to read. For example, you do a checkout with␊ |
| 553 | * git-daemon and push with SSH.␊ |
| 554 | *␊ |
| 555 | * @param string A specific commit to access␊ |
| 556 | */␊ |
| 557 | public function getWriteRemoteAccessUrl($user,$commit=null)␊ |
| 558 | {␊ |
| 559 | $conf = $this->getConf();␊ |
| 560 | $scm = $conf->getVal('scm', 'git');␊ |
| 561 | $scms = Pluf::f('allowed_scm');␊ |
| 562 | return call_user_func(array($scms[$scm], 'getAuthAccessUrl'),␊ |
| 563 | $this, $user, $commit);␊ |
| 564 | }␊ |
| 565 | ␊ |
| 566 | /**␊ |
| 567 | * Get the web hook key.␊ |
| 568 | *␊ |
| 569 | * The goal is to get something predictable but from which one␊ |
| 570 | * cannot reverse find the secret key.␊ |
| 571 | */␊ |
| 572 | public function getWebHookKey()␊ |
| 573 | {␊ |
| 574 | return md5($this->id.sha1(Pluf::f('secret_key')).$this->shortname);␊ |
| 575 | }␊ |
| 576 | ␊ |
| 577 | /**␊ |
| 578 | * Get the root name of the project scm␊ |
| 579 | *␊ |
| 580 | * @return string SCM root␊ |
| 581 | */␊ |
| 582 | public function getScmRoot()␊ |
| 583 | {␊ |
| 584 | $conf = $this->getConf();␊ |
| 585 | $roots = array(␊ |
| 586 | 'git' => 'master',␊ |
| 587 | 'svn' => 'HEAD',␊ |
| 588 | 'mercurial' => 'tip',␊ |
| 589 | 'mtn' => 'h:'.$conf->getVal('mtn_master_branch', '*'),␊ |
| 590 | );␊ |
| 591 | $scm = $conf->getVal('scm', 'git');␊ |
| 592 | return $roots[$scm];␊ |
| 593 | }␊ |
| 594 | ␊ |
| 595 | /**␊ |
| 596 | * Check that the object belongs to the project or rise a 404␊ |
| 597 | * error.␊ |
| 598 | *␊ |
| 599 | * By convention, all the objects belonging to a project have the␊ |
| 600 | * 'project' property set, so this is easy to check.␊ |
| 601 | *␊ |
| 602 | * @param Pluf_Model␊ |
| 603 | */␊ |
| 604 | public function inOr404($obj)␊ |
| 605 | {␊ |
| 606 | if ($obj->project != $this->id) {␊ |
| 607 | throw new Pluf_HTTP_Error404();␊ |
| 608 | }␊ |
| 609 | }␊ |
| 610 | ␊ |
| 611 | /**␊ |
| 612 | * Utility function to get a configuration object.␊ |
| 613 | *␊ |
| 614 | * @return IDF_Conf␊ |
| 615 | */␊ |
| 616 | public function getConf()␊ |
| 617 | {␊ |
| 618 | if ($this->_pconf == null) {␊ |
| 619 | $this->_pconf = new IDF_Conf();␊ |
| 620 | $this->_pconf->setProject($this);␊ |
| 621 | }␊ |
| 622 | return $this->_pconf;␊ |
| 623 | }␊ |
| 624 | ␊ |
| 625 | /**␊ |
| 626 | * Magic overload that falls back to the values of the internal configuration␊ |
| 627 | * if no getter / caller matched␊ |
| 628 | *␊ |
| 629 | * @param string $key␊ |
| 630 | */␊ |
| 631 | public function __get($key)␊ |
| 632 | {␊ |
| 633 | try {␊ |
| 634 | return parent::__get($key);␊ |
| 635 | }␊ |
| 636 | catch (Exception $e) {␊ |
| 637 | return $this->getConf()->getVal($key);␊ |
| 638 | }␊ |
| 639 | }␊ |
| 640 | ␊ |
| 641 | /**␊ |
| 642 | * Get simple statistics about the project.␊ |
| 643 | *␊ |
| 644 | * This returns an associative array with number of tickets,␊ |
| 645 | * number of downloads, etc.␊ |
| 646 | *␊ |
| 647 | * @return array Stats␊ |
| 648 | */␊ |
| 649 | public function getStats()␊ |
| 650 | {␊ |
| 651 | $stats = array();␊ |
| 652 | $stats['total'] = 0;␊ |
| 653 | $what = array('downloads' => 'IDF_Upload',␊ |
| 654 | 'reviews' => 'IDF_Review',␊ |
| 655 | 'issues' => 'IDF_Issue',␊ |
| 656 | 'docpages' => 'IDF_Wiki_Page',␊ |
| 657 | 'commits' => 'IDF_Commit',␊ |
| 658 | );␊ |
| 659 | foreach ($what as $key=>$m) {␊ |
| 660 | $i = Pluf::factory($m)->getCount(array('filter' => 'project='.(int)$this->id));␊ |
| 661 | $stats[$key] = $i;␊ |
| 662 | $stats['total'] += $i;␊ |
| 663 | }␊ |
| 664 | /**␊ |
| 665 | * [signal]␊ |
| 666 | *␊ |
| 667 | * IDF_Project::getStats␊ |
| 668 | *␊ |
| 669 | * [sender]␊ |
| 670 | *␊ |
| 671 | * IDF_Project␊ |
| 672 | *␊ |
| 673 | * [description]␊ |
| 674 | *␊ |
| 675 | * This signal allows an application to update the statistics␊ |
| 676 | * array of a project. For example to add the on disk size␊ |
| 677 | * of the repository if available.␊ |
| 678 | *␊ |
| 679 | * [parameters]␊ |
| 680 | *␊ |
| 681 | * array('project' => $project,␊ |
| 682 | * 'stats' => $stats)␊ |
| 683 | *␊ |
| 684 | */␊ |
| 685 | $params = array('project' => $this,␊ |
| 686 | 'stats' => $stats);␊ |
| 687 | Pluf_Signal::send('IDF_Project::getStats',␊ |
| 688 | 'IDF_Project', $params);␊ |
| 689 | return $stats;␊ |
| 690 | }␊ |
| 691 | ␊ |
| 692 | /**␊ |
| 693 | * Needs to be called when you update the memberships of a␊ |
| 694 | * project.␊ |
| 695 | *␊ |
| 696 | * This will allow a plugin to, for example, update some access␊ |
| 697 | * rights to a repository.␊ |
| 698 | */␊ |
| 699 | public function membershipsUpdated()␊ |
| 700 | {␊ |
| 701 | /**␊ |
| 702 | * [signal]␊ |
| 703 | *␊ |
| 704 | * IDF_Project::membershipsUpdated␊ |
| 705 | *␊ |
| 706 | * [sender]␊ |
| 707 | *␊ |
| 708 | * IDF_Project␊ |
| 709 | *␊ |
| 710 | * [description]␊ |
| 711 | *␊ |
| 712 | * This signal allows an application to update the some access␊ |
| 713 | * rights to a repository when the project memberships is␊ |
| 714 | * updated.␊ |
| 715 | *␊ |
| 716 | * [parameters]␊ |
| 717 | *␊ |
| 718 | * array('project' => $project)␊ |
| 719 | *␊ |
| 720 | */␊ |
| 721 | $params = array('project' => $this);␊ |
| 722 | Pluf_Signal::send('IDF_Project::membershipsUpdated',␊ |
| 723 | 'IDF_Project', $params);␊ |
| 724 | }␊ |
| 725 | ␊ |
| 726 | /**␊ |
| 727 | * Needs to be called when you create a project.␊ |
| 728 | *␊ |
| 729 | * We cannot put it into the postSave call as the configuration of␊ |
| 730 | * the project is not defined at that time.␊ |
| 731 | */␊ |
| 732 | function created()␊ |
| 733 | {␊ |
| 734 | /**␊ |
| 735 | * [signal]␊ |
| 736 | *␊ |
| 737 | * IDF_Project::created␊ |
| 738 | *␊ |
| 739 | * [sender]␊ |
| 740 | *␊ |
| 741 | * IDF_Project␊ |
| 742 | *␊ |
| 743 | * [description]␊ |
| 744 | *␊ |
| 745 | * This signal allows an application to perform special␊ |
| 746 | * operations at the creation of a project.␊ |
| 747 | *␊ |
| 748 | * [parameters]␊ |
| 749 | *␊ |
| 750 | * array('project' => $project)␊ |
| 751 | *␊ |
| 752 | */␊ |
| 753 | $params = array('project' => $this);␊ |
| 754 | Pluf_Signal::send('IDF_Project::created',␊ |
| 755 | 'IDF_Project', $params);␊ |
| 756 | }␊ |
| 757 | ␊ |
| 758 | /**␊ |
| 759 | * The delete() call do not like circular references and the␊ |
| 760 | * IDF_Tag is creating some. We predelete to solve these issues.␊ |
| 761 | */␊ |
| 762 | public function preDelete()␊ |
| 763 | {␊ |
| 764 | /**␊ |
| 765 | * [signal]␊ |
| 766 | *␊ |
| 767 | * IDF_Project::preDelete␊ |
| 768 | *␊ |
| 769 | * [sender]␊ |
| 770 | *␊ |
| 771 | * IDF_Project␊ |
| 772 | *␊ |
| 773 | * [description]␊ |
| 774 | *␊ |
| 775 | * This signal allows an application to perform special␊ |
| 776 | * operations at the deletion of a project.␊ |
| 777 | *␊ |
| 778 | * [parameters]␊ |
| 779 | *␊ |
| 780 | * array('project' => $project)␊ |
| 781 | *␊ |
| 782 | */␊ |
| 783 | $params = array('project' => $this);␊ |
| 784 | Pluf_Signal::send('IDF_Project::preDelete',␊ |
| 785 | 'IDF_Project', $params);␊ |
| 786 | $what = array('IDF_Upload', 'IDF_Review', 'IDF_Issue',␊ |
| 787 | 'IDF_Wiki_Page', 'IDF_Wiki_Resource',␊ |
| 788 | 'IDF_Commit', 'IDF_Tag',␊ |
| 789 | );␊ |
| 790 | foreach ($what as $m) {␊ |
| 791 | foreach (Pluf::factory($m)->getList(array('filter' => 'project='.(int)$this->id)) as $item) {␊ |
| 792 | $item->delete();␊ |
| 793 | }␊ |
| 794 | }␊ |
| 795 | }␊ |
| 796 | ␊ |
| 797 | /**␊ |
| 798 | * Check if the project has one restricted tab.␊ |
| 799 | *␊ |
| 800 | * @return bool␊ |
| 801 | */␊ |
| 802 | public function isRestricted()␊ |
| 803 | {␊ |
| 804 | if ($this->_isRestricted !== null) {␊ |
| 805 | return $this->_isRestricted;␊ |
| 806 | }␊ |
| 807 | if ($this->private) {␊ |
| 808 | $this->_isRestricted = true;␊ |
| 809 | return true;␊ |
| 810 | }␊ |
| 811 | $tabs = array(␊ |
| 812 | 'source_access_rights',␊ |
| 813 | 'issues_access_rights',␊ |
| 814 | 'downloads_access_rights',␊ |
| 815 | 'wiki_access_rights',␊ |
| 816 | 'review_access_rights'␊ |
| 817 | );␊ |
| 818 | $conf = $this->getConf();␊ |
| 819 | foreach ($tabs as $tab) {␊ |
| 820 | if (!in_array($conf->getVal($tab, 'all'),␊ |
| 821 | array('all', 'none'))) {␊ |
| 822 | $this->_isRestricted = true;␊ |
| 823 | return true;␊ |
| 824 | }␊ |
| 825 | }␊ |
| 826 | $this->_isRestricted = false;␊ |
| 827 | return false;␊ |
| 828 | }␊ |
| 829 | ␊ |
| 830 | /**␊ |
| 831 | * Returns an associative array of email addresses to notify about changes␊ |
| 832 | * in a certain tab like 'issues', 'source', and so on.␊ |
| 833 | *␊ |
| 834 | * @param string $tab␊ |
| 835 | * @return array Key is the email address, value is the preferred language setting␊ |
| 836 | */␊ |
| 837 | public function getNotificationRecipientsForTab($tab)␊ |
| 838 | {␊ |
| 839 | if (!in_array($tab, array('source', 'issues', 'downloads', 'wiki', 'review'))) {␊ |
| 840 | throw new Exception(sprintf('unknown tab %s', $tab));␊ |
| 841 | }␊ |
| 842 | ␊ |
| 843 | $conf = $this->getConf();␊ |
| 844 | $recipients = array();␊ |
| 845 | $membership_data = $this->getMembershipData();␊ |
| 846 | ␊ |
| 847 | if ($conf->getVal($tab.'_notification_owners_enabled', false)) {␊ |
| 848 | foreach ($membership_data['owners'] as $owner) {␊ |
| 849 | $recipients[$owner->email] = $owner->language;␊ |
| 850 | }␊ |
| 851 | }␊ |
| 852 | ␊ |
| 853 | if ($conf->getVal($tab.'_notification_members_enabled', false)) {␊ |
| 854 | foreach ($membership_data['members'] as $member) {␊ |
| 855 | $recipients[$member->email] = $member->language;␊ |
| 856 | }␊ |
| 857 | }␊ |
| 858 | ␊ |
| 859 | if ($conf->getVal($tab.'_notification_email_enabled', false)) {␊ |
| 860 | $addresses = preg_split('/\s*,\s*/',␊ |
| 861 | $conf->getVal($tab.'_notification_email', ''),␊ |
| 862 | -1, PREG_SPLIT_NO_EMPTY);␊ |
| 863 | ␊ |
| 864 | // we use a default language setting for this plain list of␊ |
| 865 | // addresses, but we ensure that we do not overwrite an existing␊ |
| 866 | // address which might come with a proper setting already␊ |
| 867 | $languages = Pluf::f('languages', array('en'));␊ |
| 868 | foreach ($addresses as $address) {␊ |
| 869 | if (array_key_exists($address, $recipients))␊ |
| 870 | continue;␊ |
| 871 | $recipients[$address] = $languages[0];␊ |
| 872 | }␊ |
| 873 | }␊ |
| 874 | ␊ |
| 875 | return $recipients;␊ |
| 876 | }␊ |
| 877 | }␊ |
| 878 | |