Add support for features tracking

Features are a specific story type. The main difference with bugs is
that all tasks in a feature are affecting the master branch (i.e. are
set to a milestone linked to the master branch). The task title is also
given more prominence.

Change-Id: I9c40242acbb0c3d40e9c8b389e920e4dec66fc85
This commit is contained in:
Thierry Carrez 2013-07-19 16:28:07 +02:00
parent 620f29de13
commit 70a7ca6f6d
13 changed files with 148 additions and 52 deletions

@ -7,11 +7,11 @@
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>Priority</th>
<th>#</th>
<th>Story</th>
<th>Priority</th>
<th>Task</th>
<th>Branch</th>
{% if is_bug %}<th>Branch</th>{% endif %}
<th>Assignee</th>
<th>Milestone</th>
</tr>
@ -19,12 +19,12 @@
<tbody>
{% for task in tasks %}
<tr class="{{ task.status|taskcolor }}">
<td>{{ task.story.id }}</td>
<td><small><a href="/story/{{task.story.id}}">{{ task.story.title }}</a></small></td>
<td><span class="badge{{ task.story.priority|priobadge }}">
{{ task.story.get_priority_display }}</span></td>
<td>{{ task.story.id }}</td>
<td><small><a href="/story/{{task.story.id}}">{{ task.story.title }}</a></small></td>
<td>{{ task.title }}</td>
<td>{{ task.milestone.branch.name }}</td>
{% if is_bug %}<td>{{ task.milestone.branch.name }}</td>{% endif %}
<td>{{ task.assignee.username }}</td>
<td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td>
</tr>

@ -7,16 +7,17 @@
<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
{% if triagecount > 0 %}<span class="badge
{% if triagecount < 20 %}badge-success{% else %}{% if triagecount < 50 %}badge-warning{% else %}badge-important{% endif %}{% endif %}">
{{ triagecount }}</span>{% endif %}</a></li>
<li><a href="#addstory" data-toggle="modal">Report new bug</a></li>
{% 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>
<li><a href="#addbug" data-toggle="modal">Report new bug</a></li>
<li class="divider"></li>
<li class="disabled"><a href="#">List feature tasks</a></li>
<li class="disabled"><a href="#">Propose new feature</a></li>
<li><a href="/project/{{project.name}}/features">List feature tasks</a></li>
<li><a href="#addfeature" data-toggle="modal">Propose new feature</a></li>
</ul>
</div><!--/.well -->
{% endblock %}
{% block modals %}
{% include "stories.modal_addstory.html" with project=project.name %}
{% include "stories.modal_addstory.html" with project=project.name story_type='bug' %}
{% include "stories.modal_addstory.html" with project=project.name story_type='feature' %}
{% endblock %}

@ -20,5 +20,6 @@ urlpatterns = patterns('storyboard.projects.views',
(r'^$', 'default_list'),
(r'^(\S+)/bugs/triage$', 'list_bugtriage'),
(r'^(\S+)/bugs$', 'list_bugtasks'),
(r'^(\S+)/features$', 'list_featuretasks'),
(r'^(\S+)$', 'dashboard'),
)

@ -27,31 +27,59 @@ def default_list(request):
def dashboard(request, projectname):
project = Project.objects.get(name=projectname)
count = Task.objects.filter(project=project, story__priority=0).count()
bugcount = Task.objects.filter(project=project,
story__is_bug=True,
story__priority=0).count()
return render(request, "projects.dashboard.html", {
'project': project,
'triagecount': count,
'bugtriagecount': bugcount,
})
def list_featuretasks(request, projectname):
project = Project.objects.get(name=projectname)
bugcount = Task.objects.filter(project=project,
story__is_bug=True,
story__priority=0).count()
featuretasks = Task.objects.filter(project=project,
story__is_bug=False,
status__in=['T', 'R'])
return render(request, "projects.list_tasks.html", {
'title': "Active feature tasks",
'project': project,
'bugtriagecount': bugcount,
'tasks': featuretasks,
'is_bug': False,
})
def list_bugtasks(request, projectname):
project = Project.objects.get(name=projectname)
count = Task.objects.filter(project=project, story__priority=0).count()
bugcount = Task.objects.filter(project=project,
story__is_bug=True,
story__priority=0).count()
bugtasks = Task.objects.filter(project=project,
story__is_bug=True,
status__in=['T', 'R'])
return render(request, "projects.list_tasks.html", {
'title': "Active bug tasks",
'project': project,
'triagecount': count,
'tasks': Task.objects.filter(project=project, status__in=['T', 'R']),
'bugtriagecount': bugcount,
'tasks': bugtasks,
'is_bug': True,
})
def list_bugtriage(request, projectname):
project = Project.objects.get(name=projectname)
tasks = Task.objects.filter(project=project, story__priority=0)
count = tasks.count()
tasks = Task.objects.filter(project=project,
story__is_bug=True,
story__priority=0)
bugcount = tasks.count()
return render(request, "projects.list_tasks.html", {
'title': "Bugs needing triage",
'project': project,
'triagecount': count,
'tasks': Task.objects.filter(project=project, story__priority=0),
'bugtriagecount': bugcount,
'tasks': tasks,
'is_bug': True,
})

@ -21,10 +21,6 @@ from storyboard.projects.models import Project
class Story(models.Model):
STORY_TYPES = (
('B', 'Bug'),
('F', 'Feature'),
)
STORY_PRIORITIES = (
(4, 'Critical'),
(3, 'High'),
@ -35,7 +31,7 @@ class Story(models.Model):
creator = models.ForeignKey(User)
title = models.CharField(max_length=100)
description = models.TextField()
story_type = models.CharField(max_length=1, choices=STORY_TYPES)
is_bug = models.BooleanField(default=True)
priority = models.IntegerField(choices=STORY_PRIORITIES)
def __unicode__(self):

@ -5,8 +5,8 @@
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li class="nav-header">Stories</li>
<li><a href="#addstory" data-toggle="modal">Add new bug</a></li>
<li class="disabled"><a href="#">Add new feature</a></li>
<li><a href="#addbug" data-toggle="modal">Add new bug</a></li>
<li><a href="#addfeature" data-toggle="modal">Add new feature</a></li>
<li class="disabled"><a href="#">Search stories</a></li>
<li class="nav-header">Reports</li>
<li class="disabled"><a href="#">A report</a></li>
@ -25,7 +25,8 @@
<script type="text/javascript">
$("#tab-stories").addClass('active');
</script>
{% include "stories.modal_addstory.html" %}
{% include "stories.modal_addstory.html" with story_type='bug' %}
{% include "stories.modal_addstory.html" with story_type='feature' %}
{% block modals %}
{% endblock %}
{% endblock %}

@ -9,9 +9,8 @@
<thead>
<tr>
<th>#</th>
<th>Story</th>
<th>Bug</th>
<th>Priority</th>
<th>Affects</th>
</tr>
</thead>
<tbody>
@ -21,9 +20,26 @@
<td><small><a href="/story/{{story.id}}">{{ story.title }}</a></small></td>
<td><span class="badge{{ story.priority|priobadge }}">
{{ story.get_priority_display }}</span></td>
<td>
{% for task in story.task_set.all %}{% if not forloop.first %}, {% endif %}{{ task.project.name }}{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h5>Recent features</h5>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>#</th>
<th>Feature</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
{% for story in recent_features %}
<tr>
<td>{{ story.id }}</td>
<td><small><a href="/story/{{story.id}}">{{ story.title }}</a></small></td>
<td><span class="badge{{ story.priority|priobadge }}">
{{ story.get_priority_display }}</span></td>
</tr>
{% endfor %}
</tbody>

@ -1,8 +1,8 @@
<form method="POST" action="/story/new">{% csrf_token %}
<div id="addstory" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="addstoryLabel" aria-hidden="true">
<div id="add{{ story_type }}" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="add{{ story_type }}Label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="addstoryLabel">Create new bug</h3>
<h3 id="add{{ story_type }}Label">Add new {{ story_type }}</h3>
</div>
<div class="modal-body">
<label>Affected projects <small>(optional)</small></label>
@ -14,10 +14,15 @@
</div>
<label>Title</label>
<input class="input-block-level" name="title"
type="text" placeholder="Short description of the bug" value="">
type="text" placeholder="Short description of the {{ story_type}}" value="">
<label>Description <small>(can use Markdown)</small></label>
<textarea class="input-block-level" name="description"
placeholder="Enter bug description here. Please include the version of the software used and detailed steps to reproduce." rows="7"></textarea>
{% if story_type == 'bug' %}
placeholder="enter bug description here. please include the version of the software used and detailed steps to reproduce."
{% else %}
placeholder="enter feature description here."
{% endif %}
rows="7"></textarea>
<label>Tags <small>(optional)</small></label>
<div class="input-prepend">
<span class="add-on"><i class="icon-tags"></i></span>
@ -27,8 +32,9 @@
</div>
</div>
<div class="modal-footer">
<input type="hidden" name="story_type" value="{% if story_type == 'bug' %}1{% endif %}">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
<input class="btn btn-primary" type="submit" value="Create bug">
<input class="btn btn-primary" type="submit" value="Create {{story_type}}">
</div>
</div>
</form>

@ -14,8 +14,9 @@
<input class="input-block-level" name="project" id="prependedInput"
type="text" value="">
</div>
{% if story.is_bug %}
<label>Branch / Milestone</label>
{% regroup milestones by branch as branch_list %}
{% regroup milestones by branch as branch_list %}
<div class="btn-toolbar" data-toggle="buttons-radio">
{% for branch in branch_list %}
<div class="btn-group">
@ -33,7 +34,23 @@
</div>
{% endfor %}
</div>
<label>Comment</label>
{% else %}
<label>Milestone</label>
<div class="btn-group" data-toggle="buttons-radio">
{% for milestone in milestones %}
{% if milestone.branch.status == 'M' %}
{% if milestone.undefined %}
<button type="button" data-value="{{ milestone.id }}"
class="addtask_milestone btn btn-small active">{{milestone.name}}</button>
{% else %}
<button type="button" data-value="{{ milestone.id }}"
class="addtask_milestone btn btn-small">{{ milestone.name }}</button>
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endif %}
<label class="after-buttongroup">Comment</label>
<textarea class="input-block-level" rows="6" name="comment"
placeholder="Add a comment"></textarea>
<input type="hidden" id="addtask_milestone" name="milestone" value="">

@ -4,8 +4,9 @@
{% csrf_token %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="edittaskLabel">Edit
{{task.project.name}}/{{task.milestone.branch.short_name}} task</h3>
<h3 id="edittaskLabel">Edit {{task.project.name}}
{% if story.is_bug %}({{task.milestone.branch.short_name}}){% endif %}
task</h3>
</div>
<div class="modal-body">
<label>Title <small>(optional)</small></label>

@ -15,7 +15,8 @@
{% block content %}
<div class="row-fluid">
<div class="span2">
<h3>Bug {{ story.id }}</h3>
<h3>{% if story.is_bug %}Bug{% else %}Feature{% endif %}
{{ story.id }}</h3>
</div>
<div class="span10">
<a href=#editprio data-toggle="modal">
@ -33,7 +34,9 @@
<tr>
<th>Task</th>
<th>Project</th>
{% if story.is_bug %}
<th>Branch</th>
{% endif %}
<th>Assignee</th>
<th>Status</th>
<th>Milestone</th>
@ -44,7 +47,9 @@
<tr class="{{ task.status|taskcolor }}">
<td>{{ task.title }}</td>
<td>{{ task.project.title }}</td>
{% if story.is_bug %}
<td>{{ task.milestone.branch.name }}</td>
{% endif %}
<td>{{ task.assignee.username }}</td>
<td>{{ task.get_status_display }}</td>
<td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td>

@ -0,0 +1,22 @@
# 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.
def format_taskname(task):
if not task.story.is_bug:
if not task.title:
return task.project.name
return "%s (%s)" % (task.project.name, task.title)
return "%s (%s)" % (task.project.name, task.milestone.branch.short_name)

@ -25,12 +25,15 @@ from storyboard.stories.models import Comment
from storyboard.stories.models import Story
from storyboard.stories.models import StoryTag
from storyboard.stories.models import Task
from storyboard.stories.utils import format_taskname
def dashboard(request):
recent_bugs = Story.objects.order_by("-id")[:5]
recent_bugs = Story.objects.filter(is_bug=True).order_by("-id")[:5]
recent_features = Story.objects.filter(is_bug=False).order_by("-id")[:5]
return render(request, "stories.dashboard.html", {
'recent_bugs': recent_bugs,
'recent_features': recent_features,
})
@ -89,6 +92,7 @@ def add_story(request):
title=request.POST['title'],
description=request.POST['description'],
creator=request.user,
is_bug=bool(request.POST['story_type']),
priority=0,
)
newstory.save()
@ -128,9 +132,10 @@ def add_task(request, storyid):
story = Story.objects.get(id=storyid)
try:
if request.POST['project']:
milestone = None
if request.POST['milestone']:
milestone = Milestone.objects.get(id=request.POST['milestone'])
else:
if not milestone or milestone.branch.status != 'M':
milestone = Milestone.objects.get(branch__status='M',
undefined=True)
newtask = Task(
@ -140,8 +145,7 @@ def add_task(request, storyid):
milestone=milestone,
)
newtask.save()
msg = "Added %s/%s task " % (
newtask.project.name, newtask.milestone.branch.short_name)
msg = "Added %s task " % format_taskname(newtask)
newcomment = Comment(story=story,
action=msg,
author=request.user,
@ -181,8 +185,7 @@ def edit_task(request, taskid):
actions.append("assignee -> %s" % assigneename)
task.assignee = assignee
if actions:
msg = "Updated %s/%s task " % (task.project.name,
task.milestone.branch.short_name)
msg = "Updated %s task " % format_taskname(task)
msg += ", ".join(actions)
task.save()
newcomment = Comment(story=task.story,
@ -201,8 +204,7 @@ def edit_task(request, taskid):
def delete_task(request, taskid):
task = Task.objects.get(id=taskid)
task.delete()
msg = "Deleted %s/%s task" % (task.project.name,
task.milestone.branch.short_name)
msg = "Deleted %s task" % format_taskname(task)
newcomment = Comment(story=task.story,
action=msg,
author=request.user,