Introducing project groups

Project groups are convenient ways to display tasks that matter to you.
All views support listing per-project or per-projectgroup, so you can
create a "Nova program" group that will include nova and
python-novaclient, so that the Nova team can do all its bug triaging
in a single view.

Change-Id: Iaa32ab2c528033bcd4917fc5df5ab30839c5d3d7
This commit is contained in:
Thierry Carrez 2013-08-07 16:03:19 +02:00
parent b72dea7c38
commit f521b399df
10 changed files with 123 additions and 45 deletions

View File

@ -15,6 +15,12 @@ investing more into developing it.
Current features
----------------
*Project views*
Basic project views that let you retrieve the list of tasks for a given
project, as well as an example of a workflow-oriented view (the 'Triage
bugs' view). The current POC is is also just a minimal stub of the project
view feature set.
*Bug tracking*
Like Launchpad Bugs, StoryBoard implements bugs as stories, with tasks that
may affect various project/branch combinations. You can currently create
@ -23,11 +29,18 @@ Current features
and is missing search features, pagination, results ordering. This should
definitely be improved if we go forward with this.
*Project views*
Basic project views that let you retrieve the list of tasks for a given
project, as well as an example of a workflow-oriented view (the 'Triage
bugs' view). The current POC is is also just a minimal stub of the project
view feature set.
*Feature tracking*
The equivalent of Launchpad Blueprints, they inherit the same 'story'
framework as bugs. That means they don't have most of the limitations of
LP blueprints: you can comment in them, you can have tasks affecting multiple
projects, you can even have multiple tasks affecting the same project and
order them !
*Project groups*
Projects can be grouped together arbitrarily, and all 'project' views can
be reused by project groups. That makes it easy to triage or track all
tasks for projects within a given OpenStack program.
*Markdown descriptions and comments*
Story descriptions and comments can use markdown for richer interaction.
@ -52,18 +65,6 @@ No invalid/wontfix/opinion status
Future features
---------------
*Feature tracking*
The equivalent of Launchpad Blueprints, they inherit the same 'story'
framework as bugs. That means they don't have most of the limitations of
LP blueprints: you can comment in them, you can have tasks affecting multiple
projects, you can even have multiple tasks affecting the same project and
order them !
*Project groups*
Projects can be grouped together arbitrarily, and all 'project' views can
be reused by project groups. That makes it easy to triage or track all
tasks for projects within a given OpenStack program.
*Subscription*
Users should be able to subscribe to tasks (and get them in a specific view)
as well as subscribe to projects (have their own customized project group).

View File

@ -18,8 +18,10 @@ from django.contrib import admin
from storyboard.projects.models import Branch
from storyboard.projects.models import Milestone
from storyboard.projects.models import Project
from storyboard.projects.models import ProjectGroup
admin.site.register(Branch)
admin.site.register(Project)
admin.site.register(ProjectGroup)
admin.site.register(Milestone)

View File

@ -24,6 +24,15 @@ class Project(models.Model):
return self.name
class ProjectGroup(models.Model):
name = models.CharField(max_length=50, primary_key=True)
title = models.CharField(max_length=100)
members = models.ManyToManyField(Project)
def __unicode__(self):
return self.name
class Branch(models.Model):
BRANCH_STATUS = (
('M', 'master'),

View File

@ -2,8 +2,13 @@
{% block content %}
<div class="row-fluid">
<div class="span12">
<h3>{{ project.title }} ({{ project.name }})</h3>
Interesting graphs and information shall be placed here.
<h3>{{ ref.title }} ({{ ref.name }})</h3>
<h4>Groups</h4>
<ul>
{% for group in ref.projectgroup_set.all %}
<li><a href="/projectgroup/{{group.name}}">{{group.title}}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "projects.project.html" %}
{% block content %}
<div class="row-fluid">
<div class="span12">
<h3>Project group: {{ ref.title }} ({{ ref.name }})</h3>
<h4>Projects</h4>
<ul>
{% for project in ref.members.all %}
<li><a href="/project/{{ project.name }}">{{ project.title }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block content %}
<div class="row-fluid">
<div class="span12">
<h3>{{ title }} for {{ project.title }}</h3>
<h3>{{ title }} for {{ ref.name }}</h3>
<table class="table table-condensed table-hover">
<thead>
<tr>
@ -12,6 +12,7 @@
<th>Story</th>
<th>Task</th>
{% if is_bug %}<th>Branch</th>{% endif %}
{% if is_group %}<th>Project</th>{% endif %}
<th>Assignee</th>
<th>Milestone</th>
</tr>
@ -25,6 +26,7 @@
<td><small><a href="/story/{{task.story.id}}">{{ task.story.title }}</a></small></td>
<td>{{ task.title }}</td>
{% if is_bug %}<td>{{ task.milestone.branch.name }}</td>{% endif %}
{% if is_group %}<td>{{ task.project.name }}</td>{% endif %}
<td>{{ task.assignee.username }}</td>
<td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td>
</tr>

View File

@ -2,22 +2,28 @@
{% block extranav %}
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li class="nav-header">{{project.name}}</li>
<li><a href="/project/{{project.name}}">Dashboard</a></li>
<li class="nav-header">{{ref.name}}</li>
<li><a href="/project{% if is_group %}group{% endif %}/{{ref.name}}">Dashboard</a></li>
<li class="divider"></li>
<li><a href="/project/{{project.name}}/bugs">List bug tasks</a></li>
<li><a href="/project/{{project.name}}/bugs/triage">Triage bugs
<li><a href="/project{% if is_group %}group{% endif %}/{{ref.name}}/bugs">List bug tasks</a></li>
<li><a href="/project{% if is_group %}group{% endif %}/{{ref.name}}/bugs/triage">Triage bugs
{% if bugtriagecount > 0 %}<span class="badge
{% if bugtriagecount < 20 %}badge-success{% else %}{% if bugtriagecount < 50 %}badge-warning{% else %}badge-important{% endif %}{% endif %}">
{{ bugtriagecount }}</span>{% endif %}</a></li>
{% if not is_group %}
<li><a href="#addbug" data-toggle="modal">Report new bug</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="/project/{{project.name}}/features">List feature tasks</a></li>
<li><a href="/project{% if is_group %}group{% endif %}/{{ref.name}}/features">List feature tasks</a></li>
{% if not is_group %}
<li><a href="#addfeature" data-toggle="modal">Propose new feature</a></li>
{% endif %}
</ul>
</div><!--/.well -->
{% endblock %}
{% block modals %}
{% include "stories.modal_addstory.html" with project=project.name story_type='bug' %}
{% include "stories.modal_addstory.html" with project=project.name story_type='feature' %}
{% if not is_group %}
{% include "stories.modal_addstory.html" with project=ref.name story_type='bug' %}
{% include "stories.modal_addstory.html" with project=ref.name story_type='feature' %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,26 @@
# Copyright 2013 Thierry Carrez <thierry@openstack.org>
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from storyboard.projects.models import Project
from storyboard.projects.models import ProjectGroup
def retrieve_projects(name, group):
if group:
ref = ProjectGroup.objects.get(name=name)
return ref, ref.members.all()
else:
ref = Project.objects.get(name=name)
return ref, [ref]

View File

@ -16,6 +16,7 @@
from django.shortcuts import render
from storyboard.projects.models import Project
from storyboard.projects.utils import retrieve_projects
from storyboard.stories.models import Task
@ -25,60 +26,71 @@ def default_list(request):
})
def dashboard(request, projectname):
project = Project.objects.get(name=projectname)
bugcount = Task.objects.filter(project=project,
def dashboard(request, projectname, group=False):
ref, projects = retrieve_projects(projectname, group)
bugcount = Task.objects.filter(project__in=projects,
story__is_bug=True,
story__priority=0).count()
if group:
return render(request, "projects.group.html", {
'ref': ref,
'is_group': group,
'bugtriagecount': bugcount,
})
return render(request, "projects.dashboard.html", {
'project': project,
'ref': ref,
'is_group': group,
'bugtriagecount': bugcount,
})
def list_featuretasks(request, projectname):
project = Project.objects.get(name=projectname)
bugcount = Task.objects.filter(project=project,
def list_featuretasks(request, projectname, group=False):
ref, projects = retrieve_projects(projectname, group)
bugcount = Task.objects.filter(project__in=projects,
story__is_bug=True,
story__priority=0).count()
featuretasks = Task.objects.filter(project=project,
featuretasks = Task.objects.filter(project__in=projects,
story__is_bug=False,
status__in=['T', 'R'])
return render(request, "projects.list_tasks.html", {
'title': "Active feature tasks",
'project': project,
'ref': ref,
'is_group': group,
'name': projectname,
'bugtriagecount': bugcount,
'tasks': featuretasks,
'is_bug': False,
})
def list_bugtasks(request, projectname):
project = Project.objects.get(name=projectname)
bugcount = Task.objects.filter(project=project,
def list_bugtasks(request, projectname, group=False):
ref, projects = retrieve_projects(projectname, group)
bugcount = Task.objects.filter(project__in=projects,
story__is_bug=True,
story__priority=0).count()
bugtasks = Task.objects.filter(project=project,
bugtasks = Task.objects.filter(project__in=projects,
story__is_bug=True,
status__in=['T', 'R'])
return render(request, "projects.list_tasks.html", {
'title': "Active bug tasks",
'project': project,
'ref': ref,
'is_group': group,
'bugtriagecount': bugcount,
'tasks': bugtasks,
'is_bug': True,
})
def list_bugtriage(request, projectname):
project = Project.objects.get(name=projectname)
tasks = Task.objects.filter(project=project,
def list_bugtriage(request, projectname, group=False):
ref, projects = retrieve_projects(projectname, group)
tasks = Task.objects.filter(project__in=projects,
story__is_bug=True,
story__priority=0)
bugcount = tasks.count()
return render(request, "projects.list_tasks.html", {
'title': "Bugs needing triage",
'project': project,
'ref': ref,
'is_group': group,
'bugtriagecount': bugcount,
'tasks': tasks,
'is_bug': True,

View File

@ -26,6 +26,7 @@ urlpatterns = patterns('',
(r'^$', 'storyboard.about.views.welcome'),
(r'^about/', include('storyboard.about.urls')),
(r'^project/', include('storyboard.projects.urls')),
(r'^projectgroup/', include('storyboard.projects.urls'), {'group': True}),
(r'^story/', include('storyboard.stories.urls')),
url(r'^admin/', include(admin.site.urls)),
(r'^logout$', 'storyboard.about.views.dologout'),