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 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* *Bug tracking*
Like Launchpad Bugs, StoryBoard implements bugs as stories, with tasks that Like Launchpad Bugs, StoryBoard implements bugs as stories, with tasks that
may affect various project/branch combinations. You can currently create 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 and is missing search features, pagination, results ordering. This should
definitely be improved if we go forward with this. 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 *Feature tracking*
project, as well as an example of a workflow-oriented view (the 'Triage The equivalent of Launchpad Blueprints, they inherit the same 'story'
bugs' view). The current POC is is also just a minimal stub of the project framework as bugs. That means they don't have most of the limitations of
view feature set. 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* *Markdown descriptions and comments*
Story descriptions and comments can use markdown for richer interaction. Story descriptions and comments can use markdown for richer interaction.
@ -52,18 +65,6 @@ No invalid/wontfix/opinion status
Future features 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* *Subscription*
Users should be able to subscribe to tasks (and get them in a specific view) 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). 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 Branch
from storyboard.projects.models import Milestone from storyboard.projects.models import Milestone
from storyboard.projects.models import Project from storyboard.projects.models import Project
from storyboard.projects.models import ProjectGroup
admin.site.register(Branch) admin.site.register(Branch)
admin.site.register(Project) admin.site.register(Project)
admin.site.register(ProjectGroup)
admin.site.register(Milestone) admin.site.register(Milestone)

View File

@ -24,6 +24,15 @@ class Project(models.Model):
return self.name 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): class Branch(models.Model):
BRANCH_STATUS = ( BRANCH_STATUS = (
('M', 'master'), ('M', 'master'),

View File

@ -2,8 +2,13 @@
{% block content %} {% block content %}
<div class="row-fluid"> <div class="row-fluid">
<div class="span12"> <div class="span12">
<h3>{{ project.title }} ({{ project.name }})</h3> <h3>{{ ref.title }} ({{ ref.name }})</h3>
Interesting graphs and information shall be placed here. <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>
</div> </div>
{% endblock %} {% 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 %} {% block content %}
<div class="row-fluid"> <div class="row-fluid">
<div class="span12"> <div class="span12">
<h3>{{ title }} for {{ project.title }}</h3> <h3>{{ title }} for {{ ref.name }}</h3>
<table class="table table-condensed table-hover"> <table class="table table-condensed table-hover">
<thead> <thead>
<tr> <tr>
@ -12,6 +12,7 @@
<th>Story</th> <th>Story</th>
<th>Task</th> <th>Task</th>
{% if is_bug %}<th>Branch</th>{% endif %} {% if is_bug %}<th>Branch</th>{% endif %}
{% if is_group %}<th>Project</th>{% endif %}
<th>Assignee</th> <th>Assignee</th>
<th>Milestone</th> <th>Milestone</th>
</tr> </tr>
@ -25,6 +26,7 @@
<td><small><a href="/story/{{task.story.id}}">{{ task.story.title }}</a></small></td> <td><small><a href="/story/{{task.story.id}}">{{ task.story.title }}</a></small></td>
<td>{{ task.title }}</td> <td>{{ task.title }}</td>
{% if is_bug %}<td>{{ task.milestone.branch.name }}</td>{% endif %} {% 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>{{ task.assignee.username }}</td>
<td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td> <td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td>
</tr> </tr>

View File

@ -2,22 +2,28 @@
{% block extranav %} {% block extranav %}
<div class="well sidebar-nav"> <div class="well sidebar-nav">
<ul class="nav nav-list"> <ul class="nav nav-list">
<li class="nav-header">{{project.name}}</li> <li class="nav-header">{{ref.name}}</li>
<li><a href="/project/{{project.name}}">Dashboard</a></li> <li><a href="/project{% if is_group %}group{% endif %}/{{ref.name}}">Dashboard</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="/project/{{project.name}}/bugs">List bug tasks</a></li> <li><a href="/project{% if is_group %}group{% endif %}/{{ref.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/triage">Triage bugs
{% if bugtriagecount > 0 %}<span class="badge {% if bugtriagecount > 0 %}<span class="badge
{% if bugtriagecount < 20 %}badge-success{% else %}{% if bugtriagecount < 50 %}badge-warning{% else %}badge-important{% endif %}{% endif %}"> {% if bugtriagecount < 20 %}badge-success{% else %}{% if bugtriagecount < 50 %}badge-warning{% else %}badge-important{% endif %}{% endif %}">
{{ bugtriagecount }}</span>{% endif %}</a></li> {{ bugtriagecount }}</span>{% endif %}</a></li>
{% if not is_group %}
<li><a href="#addbug" data-toggle="modal">Report new bug</a></li> <li><a href="#addbug" data-toggle="modal">Report new bug</a></li>
{% endif %}
<li class="divider"></li> <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> <li><a href="#addfeature" data-toggle="modal">Propose new feature</a></li>
{% endif %}
</ul> </ul>
</div><!--/.well --> </div><!--/.well -->
{% endblock %} {% endblock %}
{% block modals %} {% block modals %}
{% include "stories.modal_addstory.html" with project=project.name story_type='bug' %} {% if not is_group %}
{% include "stories.modal_addstory.html" with project=project.name story_type='feature' %} {% 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 %} {% 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 django.shortcuts import render
from storyboard.projects.models import Project from storyboard.projects.models import Project
from storyboard.projects.utils import retrieve_projects
from storyboard.stories.models import Task from storyboard.stories.models import Task
@ -25,60 +26,71 @@ def default_list(request):
}) })
def dashboard(request, projectname): def dashboard(request, projectname, group=False):
project = Project.objects.get(name=projectname) ref, projects = retrieve_projects(projectname, group)
bugcount = Task.objects.filter(project=project, bugcount = Task.objects.filter(project__in=projects,
story__is_bug=True, story__is_bug=True,
story__priority=0).count() 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", { return render(request, "projects.dashboard.html", {
'project': project, 'ref': ref,
'is_group': group,
'bugtriagecount': bugcount, 'bugtriagecount': bugcount,
}) })
def list_featuretasks(request, projectname): def list_featuretasks(request, projectname, group=False):
project = Project.objects.get(name=projectname) ref, projects = retrieve_projects(projectname, group)
bugcount = Task.objects.filter(project=project, bugcount = Task.objects.filter(project__in=projects,
story__is_bug=True, story__is_bug=True,
story__priority=0).count() story__priority=0).count()
featuretasks = Task.objects.filter(project=project, featuretasks = Task.objects.filter(project__in=projects,
story__is_bug=False, story__is_bug=False,
status__in=['T', 'R']) status__in=['T', 'R'])
return render(request, "projects.list_tasks.html", { return render(request, "projects.list_tasks.html", {
'title': "Active feature tasks", 'title': "Active feature tasks",
'project': project, 'ref': ref,
'is_group': group,
'name': projectname,
'bugtriagecount': bugcount, 'bugtriagecount': bugcount,
'tasks': featuretasks, 'tasks': featuretasks,
'is_bug': False, 'is_bug': False,
}) })
def list_bugtasks(request, projectname): def list_bugtasks(request, projectname, group=False):
project = Project.objects.get(name=projectname) ref, projects = retrieve_projects(projectname, group)
bugcount = Task.objects.filter(project=project, bugcount = Task.objects.filter(project__in=projects,
story__is_bug=True, story__is_bug=True,
story__priority=0).count() story__priority=0).count()
bugtasks = Task.objects.filter(project=project, bugtasks = Task.objects.filter(project__in=projects,
story__is_bug=True, story__is_bug=True,
status__in=['T', 'R']) status__in=['T', 'R'])
return render(request, "projects.list_tasks.html", { return render(request, "projects.list_tasks.html", {
'title': "Active bug tasks", 'title': "Active bug tasks",
'project': project, 'ref': ref,
'is_group': group,
'bugtriagecount': bugcount, 'bugtriagecount': bugcount,
'tasks': bugtasks, 'tasks': bugtasks,
'is_bug': True, 'is_bug': True,
}) })
def list_bugtriage(request, projectname): def list_bugtriage(request, projectname, group=False):
project = Project.objects.get(name=projectname) ref, projects = retrieve_projects(projectname, group)
tasks = Task.objects.filter(project=project, tasks = Task.objects.filter(project__in=projects,
story__is_bug=True, story__is_bug=True,
story__priority=0) story__priority=0)
bugcount = tasks.count() bugcount = tasks.count()
return render(request, "projects.list_tasks.html", { return render(request, "projects.list_tasks.html", {
'title': "Bugs needing triage", 'title': "Bugs needing triage",
'project': project, 'ref': ref,
'is_group': group,
'bugtriagecount': bugcount, 'bugtriagecount': bugcount,
'tasks': tasks, 'tasks': tasks,
'is_bug': True, 'is_bug': True,

View File

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