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:
parent
620f29de13
commit
70a7ca6f6d
@ -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>
|
||||
|
22
storyboard/stories/utils.py
Normal file
22
storyboard/stories/utils.py
Normal 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)
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user