public function dashboard($f3, $params) { $issue = new \Model\Issue\Detail(); // Add user's group IDs to owner filter $owner_ids = array($this->_userId); $groups = new \Model\User\Group(); foreach ($groups->find(array("user_id = ?", $this->_userId)) as $r) { $owner_ids[] = $r->group_id; } $owner_ids = implode(",", $owner_ids); $order = "priority DESC, has_due_date ASC, due_date ASC"; $projects = $issue->find(array("owner_id IN ({$owner_ids}) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0", ":type" => $f3->get("issue_type.project")), array("order" => $order)); $subprojects = array(); foreach ($projects as $i => $project) { if ($project->parent_id) { $subprojects[] = $project; unset($projects[$i]); } } $f3->set("projects", $projects); $f3->set("subprojects", $subprojects); $f3->set("bugs", $issue->find(array("owner_id IN ({$owner_ids}) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0", ":type" => $f3->get("issue_type.bug")), array("order" => $order))); $f3->set("repeat_issues", $issue->find(array("owner_id IN ({$owner_ids}) AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0 AND repeat_cycle NOT IN ('none', '')", ":type" => $f3->get("issue_type.bug")), array("order" => $order))); $watchlist = new \Model\Issue\Watcher(); $f3->set("watchlist", $watchlist->findby_watcher($this->_userId, $order)); $tasks = new \Model\Issue\Detail(); $f3->set("tasks", $tasks->find(array("owner_id IN ({$owner_ids}) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0", ":type" => $f3->get("issue_type.task")), array("order" => $order))); // Get current sprint if there is one $sprint = new \Model\Sprint(); $sprint->load("NOW() BETWEEN start_date AND end_date"); $f3->set("sprint", $sprint); $f3->set("menuitem", "index"); $this->_render("user/dashboard.html"); }
/** * GET /user/dashboard User dashboard * * @param \Base $f3 * @throws \Exception */ public function dashboard($f3) { $dashboard = $f3->get("user_obj")->option("dashboard"); if (!$dashboard) { $dashboard = array("left" => array("projects", "subprojects", "bugs", "repeat_work", "watchlist"), "right" => array("tasks")); } // Load dashboard widget data $allWidgets = array("projects", "subprojects", "tasks", "bugs", "repeat_work", "watchlist", "my_comments", "recent_comments", "open_comments", "issue_tree"); $helper = \Helper\Dashboard::instance(); foreach ($dashboard as $widgets) { foreach ($widgets as $widget) { if (is_callable(array($helper, $widget))) { $f3->set($widget, $helper->{$widget}()); } else { $f3->set("error", "Widget '{$widget}' is not available."); } unset($allWidgets[array_search($widget, $allWidgets)]); } } $f3->set("unused_widgets", $allWidgets); // Get current sprint if there is one $sprint = new \Model\Sprint(); $localDate = date('Y-m-d', \Helper\View::instance()->utc2local()); $sprint->load(array("? BETWEEN start_date AND end_date", $localDate)); $f3->set("sprint", $sprint); $f3->set("dashboard", $dashboard); $f3->set("menuitem", "index"); $this->_render("user/dashboard.html"); }
public function index_old($f3) { $sprint_model = new \Model\Sprint(); $sprints = $sprint_model->find(array("end_date < ?", $this->now(false)), array("order" => "start_date ASC")); $issue = new \Model\Issue\Detail(); $sprint_details = array(); foreach ($sprints as $sprint) { $projects = $issue->find(array("deleted_date IS NULL AND sprint_id = ? AND type_id = ?", $sprint->id, $f3->get("issue_type.project"))); $sprint_details[] = $sprint->cast() + array("projects" => $projects); } $f3->set("sprints", $sprint_details); $f3->set("title", $f3->get("dict.backlog")); $f3->set("menuitem", "backlog"); $this->_render("backlog/old.html"); }
public function sprint_edit($f3, $params) { $f3->set("title", $f3->get("dict.sprints")); $sprint = new \Model\Sprint(); $sprint->load($params["id"]); if (!$sprint->id) { $f3->error(404); return; } if ($post = $f3->get("POST")) { if (empty($post["start_date"]) || empty($post["end_date"])) { $f3->set("error", "Start and end date are required"); $this->_render("admin/sprints/edit.html"); return; } $start = strtotime($post["start_date"]); $end = strtotime($post["end_date"]); if ($end <= $start) { $f3->set("error", "End date must be after start date"); $this->_render("admin/sprints/edit.html"); return; } $sprint->name = trim($post["name"]); $sprint->start_date = date("Y-m-d", $start); $sprint->end_date = date("Y-m-d", $end); $sprint->save(); $f3->reroute("/admin/sprints"); return; } $f3->set("sprint", $sprint); $this->_render("admin/sprints/edit.html"); }
/** * Load the burndown chart data */ public function burndown($f3, $params) { $sprint = new \Model\Sprint(); $sprint->load($params["id"]); if (!$sprint->id) { $f3->error(404); return; } $visible_tasks = explode(",", $params["tasks"]); // Visible tasks must have at least one key if (empty($visible_tasks)) { $visible_tasks = array(0); } // Get today's date $today = date('Y-m-d'); $today = $today . " 23:59:59"; // Check to see if the sprint is completed if ($today < strtotime($sprint->end_date . ' + 1 day')) { $burnComplete = 0; $burnDates = $this->_createDateRangeArray($sprint->start_date, $today); $remainingDays = $this->_createDateRangeArray($today, $sprint->end_date); } else { $burnComplete = 1; $burnDates = $this->_createDateRangeArray($sprint->start_date, $sprint->end_date); $remainingDays = array(); } $burnDays = array(); $burnDatesCount = count($burnDates); $db = $f3->get("db.instance"); $visible_tasks_str = implode(",", $visible_tasks); $query_initial = "SELECT SUM(IFNULL(i.hours_total, i.hours_remaining)) AS remaining\n\t\t\t\tFROM issue i\n\t\t\t\tWHERE i.created_date < :date\n\t\t\t\tAND i.id IN (" . implode(",", $visible_tasks) . ")"; $query_daily = "SELECT SUM(IF(f.id IS NULL, IFNULL(i.hours_total, i.hours_remaining), f.new_value)) AS remaining\n\t\t\t\tFROM issue_update_field f\n\t\t\t\tJOIN issue_update u ON u.id = f.issue_update_id\n\t\t\t\tJOIN (\n\t\t\t\t\tSELECT MAX(u.id) AS max_id\n\t\t\t\t\tFROM issue_update u\n\t\t\t\t\tJOIN issue_update_field f ON f.issue_update_id = u.id\n\t\t\t\t\tWHERE f.field = 'hours_remaining'\n\t\t\t\t\tAND u.created_date < :date\n\t\t\t\t\tAND u.issue_id IN ({$visible_tasks_str})\n\t\t\t\t\tGROUP BY u.issue_id\n\t\t\t\t) a ON a.max_id = u.id\n\t\t\t\tRIGHT JOIN issue i ON i.id = u.issue_id\n\t\t\t\tWHERE (f.field = 'hours_remaining' OR f.field IS NULL)\n\t\t\t\tAND i.created_date < :date\n\t\t\t\tAND i.id IN ({$visible_tasks_str})"; $i = 1; foreach ($burnDates as $date) { // Get total_hours, which is the initial amount entered on each task, and cache this query if ($i == 1) { $result = $db->exec($query_initial, array(":date" => $sprint->start_date), 2592000); $burnDays[$date] = $result[0]; } elseif ($i < $burnDatesCount - 1 || $burnComplete) { $result = $db->exec($query_daily, array(":date" => $date . " 23:59:59"), 2592000); $burnDays[$date] = $result[0]; } else { $result = $db->exec($query_daily, array(":date" => $date . " 23:59:59")); $burnDays[$date] = $result[0]; } $i++; } // Add in empty days if (!$burnComplete) { $i = 0; foreach ($remainingDays as $day) { if ($i != 0) { $burnDays[$day] = NULL; } $i++; } } // Reformat the date and remove weekends $i = 0; foreach ($burnDays as $burnKey => $burnDay) { $weekday = date("D", strtotime($burnKey)); $weekendDays = array("Sat", "Sun"); if (!in_array($weekday, $weekendDays)) { $newDate = date("M j", strtotime($burnKey)); $burnDays[$newDate] = $burnDays[$burnKey]; unset($burnDays[$burnKey]); } else { // Remove weekend days unset($burnDays[$burnKey]); } $i++; } $this->_printJson($burnDays); }
/** * GET /issues/@id * View an issue * * @param \Base $f3 * @param array $params * @throws \Exception */ public function single($f3, $params) { $issue = new \Model\Issue\Detail(); $issue->load(array("id=?", $params["id"])); $user = $f3->get("user_obj"); if (!$issue->id || $issue->deleted_date && !($user->role == 'admin' || $user->rank >= \Model\User::RANK_MANAGER || $issue->author_id == $user->id)) { $f3->error(404); return; } $type = new \Model\Issue\Type(); $type->load($issue->type_id); $f3->set("title", $type->name . " #" . $issue->id . ": " . $issue->name); $f3->set("menuitem", "browse"); $author = new \Model\User(); $author->load($issue->author_id); $owner = new \Model\User(); if ($issue->owner_id) { $owner->load($issue->owner_id); } $files = new \Model\Issue\File\Detail(); $f3->set("files", $files->find(array("issue_id = ? AND deleted_date IS NULL", $issue->id))); if ($issue->sprint_id) { $sprint = new \Model\Sprint(); $sprint->load($issue->sprint_id); $f3->set("sprint", $sprint); } $watching = new \Model\Issue\Watcher(); $watching->load(array("issue_id = ? AND user_id = ?", $issue->id, $this->_userId)); $f3->set("watching", !!$watching->id); $f3->set("issue", $issue); $f3->set("ancestors", $issue->getAncestors()); $f3->set("type", $type); $f3->set("author", $author); $f3->set("owner", $owner); $comments = new \Model\Issue\Comment\Detail(); $f3->set("comments", $comments->find(array("issue_id = ?", $issue->id), array("order" => "created_date DESC"))); // Extra data needed for inline edit form $status = new \Model\Issue\Status(); $f3->set("statuses", $status->find(null, null, $f3->get("cache_expire.db"))); $priority = new \Model\Issue\Priority(); $f3->set("priorities", $priority->find(null, array("order" => "value DESC"), $f3->get("cache_expire.db"))); $sprint = new \Model\Sprint(); $f3->set("sprints", $sprint->find(array("end_date >= ? OR id = ?", $this->now(false), $issue->sprint_id), array("order" => "start_date ASC"))); $users = new \Model\User(); $f3->set("users", $users->find("deleted_date IS NULL AND role != 'group'", array("order" => "name ASC"))); $f3->set("groups", $users->find("deleted_date IS NULL AND role = 'group'", array("order" => "name ASC"))); $this->_render("issues/single.html"); }
/** * ~SprintBreaker~ * Alanaktion's insane attempt at automated sprint management. * * Moves tasks under projects into sprints by due date * * Tested, but still a bit scary. Also unnecessary now that * sprint tasks are dynamically loaded by due date. * * Use with caution and pizza. * **/ require_once "base.php"; $issue_type_project = $f3->get("issue_type.project"); // Get all current and future sprints $sprint = new \Model\Sprint(); $sprints = $sprint->find(array("start_date >= :now OR end_date <= :now", ":now" => date("Y-m-d"))); echo "Using " . count($sprints) . " sprints.\n"; // Get all top level projects $project_model = new \Model\Issue(); $projects = $project_model->find(array("type_id = ? AND parent_id IS NULL AND sprint_id IS NULL", $issue_type_project)); if ($projects && $sprints) { foreach ($projects as $project) { echo "\nBreaking project {$project->id}:\n"; // Get all tasks with due dates directly under project $due_date_tasks = new \Model\Issue(); $tasks = $due_date_tasks->find(array("parent_id = :project AND due_date >= :now AND type_id != :type", ":project" => $project->id, ":now" => date("Y-m-d"), ":type" => $issue_type_project)); if ($tasks) { foreach ($sprints as $sprint) { echo "Using sprint {$sprint->id}\n"; $start = strtotime($sprint->start_date);
public function sprints_old($f3) { $sprint_model = new \Model\Sprint(); $sprints = $sprint_model->find(array("end_date < ?", $this->now(false)), array("order" => "start_date ASC")); $return = array(); foreach ($sprints as $sprint) { $return[] = $sprint->cast(); } $this->_printJson($return); }
/** * Convert a sprint ID to a name/date * @param int $id * @return string */ public function convertSprintId($id) { if (isset($this->cache['sprint.' . $id])) { $sprint = $this->cache['sprint.' . $id]; } else { $sprint = new \Model\Sprint(); $sprint->load($id); $this->cache['sprint.' . $id] = $sprint; } return $sprint->name . " - " . date('n/j', strtotime($sprint->start_date)) . "-" . date('n/j', strtotime($sprint->end_date)); }
/** * Log and save an issue update * @param boolean $notify * @return Issue\Update */ protected function _saveUpdate($notify = true) { $f3 = \Base::instance(); // Ensure issue is not tied to itself as a parent if ($this->get("id") == $this->get("parent_id")) { $this->set("parent_id", $this->_getPrev("parent_id")); } // Log update $update = new \Model\Issue\Update(); $update->issue_id = $this->id; $update->user_id = $f3->get("user.id"); $update->created_date = date("Y-m-d H:i:s"); if ($f3->exists('update_comment')) { $update->comment_id = $f3->get('update_comment')->id; if ($notify) { $update->notify = 1; } } else { $update->notify = 0; } $update->save(); // Set hours_total to the hours_remaining value if it's 0 or null if ($this->get("hours_remaining") && !$this->get("hours_total")) { $this->set("hours_total", $this->get("hours_remaining")); } // Set hours remaining to 0 if the issue has been closed if ($this->get("closed_date") && $this->get("hours_remaining")) { $this->set("hours_remaining", 0); } // Create a new issue if repeating if ($this->get("closed_date") && $this->get("repeat_cycle") && $this->get("repeat_cycle") != "none") { $repeat_issue = new \Model\Issue(); $repeat_issue->name = $this->get("name"); $repeat_issue->type_id = $this->get("type_id"); $repeat_issue->parent_id = $this->get("parent_id"); $repeat_issue->author_id = $this->get("author_id"); $repeat_issue->owner_id = $this->get("owner_id"); $repeat_issue->description = $this->get("description"); $repeat_issue->priority = $this->get("priority"); $repeat_issue->repeat_cycle = $this->get("repeat_cycle"); $repeat_issue->hours_total = $this->get("hours_total"); $repeat_issue->hours_remaining = $this->get("hours_total"); // Reset hours remaining to start hours $repeat_issue->created_date = date("Y-m-d H:i:s"); // Find a due date in the future switch ($repeat_issue->repeat_cycle) { case 'daily': $repeat_issue->start_date = $this->get("start_date") ? date("Y-m-d", strtotime("tomorrow")) : NULL; $repeat_issue->due_date = date("Y-m-d", strtotime("tomorrow")); break; case 'weekly': $repeat_issue->start_date = $this->get("start_date") ? date("Y-m-d", strtotime($this->get("start_date") . " +1 week")) : NULL; $repeat_issue->due_date = date("Y-m-d", strtotime($this->get("due_date") . " +1 week")); break; case 'monthly': $repeat_issue->start_date = $this->get("start_date") ? date("Y-m-d", strtotime($this->get("start_date") . " +1 month")) : NULL; $repeat_issue->due_date = date("Y-m-d", strtotime($this->get("due_date") . " +1 month")); break; case 'sprint': $sprint = new \Model\Sprint(); $sprint->load(array("start_date > NOW()"), array('order' => 'start_date')); $repeat_issue->start_date = $this->get("start_date") ? $sprint->start_date : NULL; $repeat_issue->due_date = $sprint->end_date; break; default: $repeat_issue->repeat_cycle = 'none'; } // If the issue was in a sprint before, put it in a sprint again. if ($this->get("sprint_id")) { $sprint = new \Model\Sprint(); $sprint->load(array("end_date >= ? AND start_date <= ?", $repeat_issue->due_date, $repeat_issue->due_date), array('order' => 'start_date')); $repeat_issue->sprint_id = $sprint->id; } $repeat_issue->save(); $notification = \Helper\Notification::instance(); $notification->issue_create($repeat_issue->id); $this->set("repeat_cycle", null); } // Log updated fields $updated = 0; $important_changes = 0; $important_fields = array('status', 'name', 'description', 'owner_id', 'priority', 'due_date'); foreach ($this->fields as $key => $field) { if ($field["changed"] && $field["value"] != $this->_getPrev($key)) { $update_field = new \Model\Issue\Update\Field(); $update_field->issue_update_id = $update->id; $update_field->field = $key; $update_field->old_value = $this->_getPrev($key); $update_field->new_value = $field["value"]; $update_field->save(); $updated++; if ($key == 'sprint_id') { $this->resetTaskSprints(); } if (in_array($key, $important_fields)) { $important_changes++; } } } // Delete update if no fields were changed if (!$updated) { $update->delete(); } // Set notify flag if important changes occurred if ($notify && $important_changes) { $update->notify = 1; $update->save(); } // Send back the update return $update->id ? $update : false; }
/** * @param \Base $f3 * @param array $params * @throws \Exception */ public function single($f3, $params) { $issue = new \Model\Issue\Detail(); $issue->load(array("id=?", $f3->get("PARAMS.id"))); $user = $f3->get("user_obj"); if (!$issue->id || $issue->deleted_date && !($user->role == 'admin' || $user->rank >= \Model\User::RANK_MANAGER || $issue->author_id == $user->id)) { $f3->error(404); return; } $type = new \Model\Issue\Type(); $type->load($issue->type_id); // Run actions if passed $post = $f3->get("POST"); if (!empty($post)) { switch ($post["action"]) { case "add_watcher": $watching = new \Model\Issue\Watcher(); // Loads just in case the user is already a watcher $watching->load(array("issue_id = ? AND user_id = ?", $issue->id, $post["user_id"])); if (!$watching->id) { $watching->issue_id = $issue->id; $watching->user_id = $post["user_id"]; $watching->save(); } if ($f3->get("AJAX")) { return; } break; case "remove_watcher": $watching = new \Model\Issue\Watcher(); $watching->load(array("issue_id = ? AND user_id = ?", $issue->id, $post["user_id"])); $watching->delete(); if ($f3->get("AJAX")) { return; } break; case "add_dependency": $dependencies = new \Model\Issue\Dependency(); // Loads just in case the task is already a dependency $dependencies->load(array("issue_id = ? AND dependency_id = ?", $issue->id, $post["id"])); $dependencies->issue_id = $issue->id; $dependencies->dependency_id = $post["id"]; $dependencies->dependency_type = $post["type_id"]; $dependencies->save(); if ($f3->get("AJAX")) { return; } break; case "add_dependent": $dependencies = new \Model\Issue\Dependency(); // Loads just in case the task is already a dependency $dependencies->load(array("issue_id = ? AND dependency_id = ?", $post["id"], $issue->id)); $dependencies->dependency_id = $issue->id; $dependencies->issue_id = $post["id"]; $dependencies->dependency_type = $post["type_id"]; $dependencies->save(); if ($f3->get("AJAX")) { return; } break; case "remove_dependency": $dependencies = new \Model\Issue\Dependency(); $dependencies->load($post["id"]); $dependencies->delete(); if ($f3->get("AJAX")) { return; } break; } } $f3->set("title", $type->name . " #" . $issue->id . ": " . $issue->name); $f3->set("menuitem", "browse"); $author = new \Model\User(); $author->load($issue->author_id); $owner = new \Model\User(); if ($issue->owner_id) { $owner->load($issue->owner_id); } $files = new \Model\Issue\File\Detail(); $f3->set("files", $files->find(array("issue_id = ? AND deleted_date IS NULL", $issue->id))); if ($issue->sprint_id) { $sprint = new \Model\Sprint(); $sprint->load($issue->sprint_id); $f3->set("sprint", $sprint); } $watching = new \Model\Issue\Watcher(); $watching->load(array("issue_id = ? AND user_id = ?", $issue->id, $this->_userId)); $f3->set("watching", !!$watching->id); $f3->set("issue", $issue); $f3->set("ancestors", $issue->getAncestors()); $f3->set("type", $type); $f3->set("author", $author); $f3->set("owner", $owner); $comments = new \Model\Issue\Comment\Detail(); $f3->set("comments", $comments->find(array("issue_id = ?", $issue->id), array("order" => "created_date DESC"))); // Extra data needed for inline edit form $status = new \Model\Issue\Status(); $f3->set("statuses", $status->find(null, null, $f3->get("cache_expire.db"))); $priority = new \Model\Issue\Priority(); $f3->set("priorities", $priority->find(null, array("order" => "value DESC"), $f3->get("cache_expire.db"))); $sprint = new \Model\Sprint(); $f3->set("sprints", $sprint->find(array("end_date >= ? OR id = ?", $this->now(false), $issue->sprint_id), array("order" => "start_date ASC"))); $users = new \Model\User(); $f3->set("users", $users->find("deleted_date IS NULL AND role != 'group'", array("order" => "name ASC"))); $f3->set("groups", $users->find("deleted_date IS NULL AND role = 'group'", array("order" => "name ASC"))); $this->_render("issues/single.html"); }
/** * Repeat an issue by generating a minimal copy and setting new due date * @param boolean $notify * @return Issue */ public function repeat($notify = true) { $repeat_issue = new \Model\Issue(); $repeat_issue->name = $this->name; $repeat_issue->type_id = $this->type_id; $repeat_issue->parent_id = $this->parent_id; $repeat_issue->author_id = $this->author_id; $repeat_issue->owner_id = $this->owner_id; $repeat_issue->description = $this->description; $repeat_issue->priority = $this->priority; $repeat_issue->repeat_cycle = $this->repeat_cycle; $repeat_issue->hours_total = $this->hours_total; $repeat_issue->hours_remaining = $this->hours_total; $repeat_issue->created_date = date("Y-m-d H:i:s"); // Find a due date in the future switch ($repeat_issue->repeat_cycle) { case 'daily': $repeat_issue->start_date = $this->start_date ? date("Y-m-d", strtotime("tomorrow")) : NULL; $repeat_issue->due_date = date("Y-m-d", strtotime("tomorrow")); break; case 'weekly': $repeat_issue->start_date = $this->start_date ? date("Y-m-d", strtotime($this->start_date . " +1 week")) : NULL; $repeat_issue->due_date = date("Y-m-d", strtotime($this->due_date . " +1 week")); break; case 'monthly': $repeat_issue->start_date = $this->start_date ? date("Y-m-d", strtotime($this->start_date . " +1 month")) : NULL; $repeat_issue->due_date = date("Y-m-d", strtotime($this->due_date . " +1 month")); break; case 'sprint': $sprint = new \Model\Sprint(); $sprint->load(array("start_date > NOW()"), array('order' => 'start_date')); $repeat_issue->start_date = $this->start_date ? $sprint->start_date : NULL; $repeat_issue->due_date = $sprint->end_date; break; default: $repeat_issue->repeat_cycle = 'none'; } // If the issue was in a sprint before, put it in a sprint again. if ($this->sprint_id) { $sprint = new \Model\Sprint(); $sprint->load(array("end_date >= ? AND start_date <= ?", $repeat_issue->due_date, $repeat_issue->due_date), array('order' => 'start_date')); $repeat_issue->sprint_id = $sprint->id; } $repeat_issue->save(); if ($notify) { $notification = \Helper\Notification::instance(); $notification->issue_create($repeat_issue->id); } return $repeat_issue; }