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

View File

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

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

View File

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

View File

@ -27,31 +27,59 @@ def default_list(request):
def dashboard(request, projectname): def dashboard(request, projectname):
project = Project.objects.get(name=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", { return render(request, "projects.dashboard.html", {
'project': project, '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): def list_bugtasks(request, projectname):
project = Project.objects.get(name=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", { return render(request, "projects.list_tasks.html", {
'title': "Active bug tasks", 'title': "Active bug tasks",
'project': project, 'project': project,
'triagecount': count, 'bugtriagecount': bugcount,
'tasks': Task.objects.filter(project=project, status__in=['T', 'R']), 'tasks': bugtasks,
'is_bug': True,
}) })
def list_bugtriage(request, projectname): def list_bugtriage(request, projectname):
project = Project.objects.get(name=projectname) project = Project.objects.get(name=projectname)
tasks = Task.objects.filter(project=project, story__priority=0) tasks = Task.objects.filter(project=project,
count = tasks.count() story__is_bug=True,
story__priority=0)
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, 'project': project,
'triagecount': count, 'bugtriagecount': bugcount,
'tasks': Task.objects.filter(project=project, story__priority=0), 'tasks': tasks,
'is_bug': True,
}) })

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
<form method="POST" action="/story/new">{% csrf_token %} <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"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <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>
<div class="modal-body"> <div class="modal-body">
<label>Affected projects <small>(optional)</small></label> <label>Affected projects <small>(optional)</small></label>
@ -14,10 +14,15 @@
</div> </div>
<label>Title</label> <label>Title</label>
<input class="input-block-level" name="title" <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> <label>Description <small>(can use Markdown)</small></label>
<textarea class="input-block-level" name="description" <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> <label>Tags <small>(optional)</small></label>
<div class="input-prepend"> <div class="input-prepend">
<span class="add-on"><i class="icon-tags"></i></span> <span class="add-on"><i class="icon-tags"></i></span>
@ -27,8 +32,9 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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> <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>
</div> </div>
</form> </form>

View File

@ -14,8 +14,9 @@
<input class="input-block-level" name="project" id="prependedInput" <input class="input-block-level" name="project" id="prependedInput"
type="text" value=""> type="text" value="">
</div> </div>
{% if story.is_bug %}
<label>Branch / Milestone</label> <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"> <div class="btn-toolbar" data-toggle="buttons-radio">
{% for branch in branch_list %} {% for branch in branch_list %}
<div class="btn-group"> <div class="btn-group">
@ -33,7 +34,23 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </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" <textarea class="input-block-level" rows="6" name="comment"
placeholder="Add a comment"></textarea> placeholder="Add a comment"></textarea>
<input type="hidden" id="addtask_milestone" name="milestone" value=""> <input type="hidden" id="addtask_milestone" name="milestone" value="">

View File

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

View File

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

View File

@ -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)

View File

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