Tasks are attached to milestones, not series
Make tasks related to milestones, and associate milestones to the actual *branches*. That's an elegant way to solve our need to track "backport to milestone-proposed" tasks, solving an old issue we had in Launchpad (and for which we were abusing "Fixreleased"). Each milestone is associated to a branch. There are three types of branches: master (the future), release (the milestone-proposed branch, when it's around), and stable/*. Milestones are mandatory, there is a default 'undefined' milestone for when we don't really know when that work will hit in the master branch. In normal times we'd have the following unreleased milestones: master branch -> undefined, havana-1, havana-2, havana-3 stable/grizzly branch -> 2013.1.3 When the milestone-proposed branch is created just before havana-1, we move the havana-1 milestone from master to release branch: master branch -> undefined, havana-2, havana-3 release branch -> havana-1 stable/grizzly branch -> 2013.1.3 That lets us track release-backporting tasks (targeted to release/havana-1) separately from normal work (targeted to master/*). At the final release, the release branch is just renamed to stable/* and a new release branch is created for the new cycle. Change-Id: Ia212ae9c40549fe484362cbd5b3f323a467edb76
This commit is contained in:
parent
ff8c4354ff
commit
bbb154e23f
39
README.rst
39
README.rst
@ -17,7 +17,7 @@ Current features
|
||||
|
||||
*Bug tracking*
|
||||
Like Launchpad Bugs, StoryBoard implements bugs as stories, with tasks that
|
||||
may affect various project/series combinations. You can currently create
|
||||
may affect various project/branch combinations. You can currently create
|
||||
bugs, tasks for bugs, edit their status, comment on them, etc. The current
|
||||
POC is incomplete: it does not do any sort of form client-side validation,
|
||||
and is missing search features, pagination, results ordering. This should
|
||||
@ -76,38 +76,13 @@ Future features
|
||||
a 1:1 relationship between tasks and merges (one merge = one task marked
|
||||
'Landed')
|
||||
|
||||
*Series tracking*
|
||||
A new tab for StoryBoard, giving you per-series and per-milestone views of
|
||||
progress. Would replace the need for status.o.o/releasestatus. Series and
|
||||
*Development cycle tracking*
|
||||
A new tab for StoryBoard, giving you per-cycle and per-milestone views of
|
||||
progress. Would replace the need for status.o.o/releasestatus. Cycles and
|
||||
milestones could be specified per project, although having a default, common
|
||||
set would avoid duplication (and allow cross-project milestone views).
|
||||
|
||||
*Story dependencies*
|
||||
Some stories relate to each other (duplicates, related, depend on...) and we
|
||||
should be able to access those easily.
|
||||
|
||||
*Official tags*
|
||||
Currently all tags are considered custom (grey color). A set of official tags
|
||||
should be created (with associated colors and autocomplete magic) for easier
|
||||
reuse of popular tags.
|
||||
|
||||
*Privileged actions*
|
||||
Currently everyone can do everything. Features prioritization, for example,
|
||||
should probably be restricted to PTL/drivers-style group.
|
||||
|
||||
*Embargoed vulnerabilities support*
|
||||
Support for private stories that can be accessed only by a per-story set of
|
||||
users.
|
||||
|
||||
*Patches in comments*
|
||||
Have the ability to attach patches to comments.
|
||||
|
||||
*Email notifications*
|
||||
We'd certainly need email notifications of some kind, too.
|
||||
|
||||
*Admin actions*
|
||||
Currently creating series/milestones/projects is done through the default
|
||||
Django admin app. StoryBoard could use something a bit more friendly.
|
||||
See https://github.com/ttx/storyboard/issues for more feature backlog.
|
||||
|
||||
|
||||
Install, test and run
|
||||
@ -142,8 +117,8 @@ Basic test using Django development server
|
||||
Run Django development server:
|
||||
./manage.py runserver
|
||||
|
||||
Create basic data (at least a series, a milestone, a project) through the
|
||||
admin server (using the admin credentials above) at:
|
||||
Create basic data (at least a master and release branch, a milestone, a
|
||||
project) through the admin server (using the admin credentials above) at:
|
||||
http://127.0.0.1:8000/admin/
|
||||
|
||||
Then log out and access the application at:
|
||||
|
@ -3,12 +3,12 @@
|
||||
<div class="hero-unit">
|
||||
<h1>StoryBoard</h1>
|
||||
<h3>A task tracking system for inter-related projects.</h3>
|
||||
<p>StoryBoard lets you track what needs to be done across projects and series. It is a proof-of-concept demo of what the ideal OpenStack task tracker would look like. It may or may not end up replacing Launchpad Bugs/Blueprints for OpenStack task tracking and release management.</p>
|
||||
<p>StoryBoard lets you track what needs to be done across projects and branches. It is a proof-of-concept demo of what the ideal OpenStack task tracker would look like. It may or may not end up replacing Launchpad Bugs/Blueprints for OpenStack task tracking and release management.</p>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<h2>Stories</h2>
|
||||
<p>It all begins with a <strong>story</strong>. A story is a bug report or proposed feature. Stories are then further split into <strong>tasks</strong>, which affect a given project and series. You can easily track backports of bugs to a specific series, or plan cross-project features.</p>
|
||||
<p>It all begins with a <strong>story</strong>. A story is a bug report or proposed feature. Stories are then further split into <strong>tasks</strong>, which affect a given project and branch. You can easily track backports of bugs to a specific branch, or plan cross-project features.</p>
|
||||
<p><a class="btn" href="/story">Access stories »</a></p>
|
||||
</div><!--/span-->
|
||||
<div class="span4">
|
||||
|
@ -15,11 +15,11 @@
|
||||
|
||||
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 Series
|
||||
|
||||
|
||||
admin.site.register(Branch)
|
||||
admin.site.register(Project)
|
||||
admin.site.register(Series)
|
||||
admin.site.register(Milestone)
|
||||
|
@ -24,14 +24,15 @@ class Project(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class Series(models.Model):
|
||||
SERIES_STATUS = (
|
||||
(0, 'Old'),
|
||||
(1, 'Supported'),
|
||||
(2, 'Active'),
|
||||
(3, 'Future'))
|
||||
name = models.CharField(max_length=50, primary_key=True)
|
||||
status = models.IntegerField(choices=SERIES_STATUS)
|
||||
class Branch(models.Model):
|
||||
BRANCH_STATUS = (
|
||||
('M', 'master'),
|
||||
('R', 'release'),
|
||||
('S', 'stable'),
|
||||
('U', 'unsupported'))
|
||||
name = models.CharField(max_length=50)
|
||||
short_name = models.CharField(max_length=20)
|
||||
status = models.CharField(max_length=1, choices=BRANCH_STATUS)
|
||||
release_date = models.DateTimeField()
|
||||
|
||||
def __unicode__(self):
|
||||
@ -43,8 +44,9 @@ class Series(models.Model):
|
||||
|
||||
class Milestone(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
series = models.ForeignKey(Series)
|
||||
active = models.BooleanField(default=True)
|
||||
branch = models.ForeignKey(Branch)
|
||||
released = models.BooleanField(default=False)
|
||||
undefined = models.BooleanField(default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
@ -11,7 +11,7 @@
|
||||
<th>Story</th>
|
||||
<th>Priority</th>
|
||||
<th>Task</th>
|
||||
<th>Series</th>
|
||||
<th>Branch</th>
|
||||
<th>Assignee</th>
|
||||
<th>Milestone</th>
|
||||
</tr>
|
||||
@ -24,9 +24,9 @@
|
||||
<td><span class="badge{{ task.story.priority|priobadge }}">
|
||||
{{ task.story.get_priority_display }}</span></td>
|
||||
<td>{{ task.title }}</td>
|
||||
<td>{{ task.series.name }}</td>
|
||||
<td>{{ task.milestone.branch.name }}</td>
|
||||
<td>{{ task.assignee.username }}</td>
|
||||
<td>{{ task.milestone.name }}</td>
|
||||
<td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -18,7 +18,6 @@ from django.db import models
|
||||
|
||||
from storyboard.projects.models import Milestone
|
||||
from storyboard.projects.models import Project
|
||||
from storyboard.projects.models import Series
|
||||
|
||||
|
||||
class Story(models.Model):
|
||||
@ -52,14 +51,13 @@ class Task(models.Model):
|
||||
story = models.ForeignKey(Story)
|
||||
title = models.CharField(max_length=100, blank=True)
|
||||
project = models.ForeignKey(Project)
|
||||
series = models.ForeignKey(Series)
|
||||
assignee = models.ForeignKey(User, blank=True, null=True)
|
||||
status = models.CharField(max_length=1, choices=TASK_STATUSES, default='T')
|
||||
milestone = models.ForeignKey(Milestone, blank=True, null=True)
|
||||
milestone = models.ForeignKey(Milestone)
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s %s/%s" % (
|
||||
self.story.id, self.project.name, self.series.name)
|
||||
self.story.id, self.project.name, self.branch.short_name)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
|
@ -14,23 +14,29 @@
|
||||
<input class="input-block-level" name="project" id="prependedInput"
|
||||
type="text" value="">
|
||||
</div>
|
||||
<label>Series</label>
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
{% for series in active_series %}
|
||||
{% if series.status == 2 %}
|
||||
<button type="button" data-value="{{ series.name }}"
|
||||
class="addtask_series btn btn-small active">Current ({{series.name}})</button>
|
||||
<label>Branch / Milestone</label>
|
||||
{% regroup milestones by branch as branch_list %}
|
||||
<div class="btn-toolbar" data-toggle="buttons-radio">
|
||||
{% for branch in branch_list %}
|
||||
<div class="btn-group">
|
||||
<button type="button" data-value="{{ milestone.id }}"
|
||||
class="btn btn-small disabled"><b>{{branch.grouper.name}}</b></button>
|
||||
{% for milestone in branch.list %}
|
||||
{% if milestone.branch.status == 'M' and 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="{{ series.name }}"
|
||||
class="addtask_series btn btn-small
|
||||
{% if series.status == 1 %}btn-success{% endif %}">{{ series.name }}</button>
|
||||
<button type="button" data-value="{{ milestone.id }}"
|
||||
class="addtask_milestone btn btn-small">{{ milestone.name }}</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<label class="after-buttongroup">Comment</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<label>Comment</label>
|
||||
<textarea class="input-block-level" rows="6" name="comment"
|
||||
placeholder="Add a comment"></textarea>
|
||||
<input type="hidden" id="addtask_series" name="series" value="">
|
||||
<input type="hidden" id="addtask_milestone" name="milestone" value="">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||
@ -39,7 +45,7 @@
|
||||
</div>
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
$(".addtask_series").click(function() {
|
||||
$("#addtask_series").val($(this).data("value"));
|
||||
$(".addtask_milestone").click(function() {
|
||||
$("#addtask_milestone").val($(this).data("value"));
|
||||
});
|
||||
</script>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3 id="deltaskLabel">Delete
|
||||
{{task.project.name}}/{{task.series.name}} task ?</h3>
|
||||
{{task.project.name}}/{{task.branch.shortname}} task ?</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>Comment</label>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3 id="edittaskLabel">Edit
|
||||
{{task.project.name}}/{{task.series.name}} task</h3>
|
||||
{{task.project.name}}/{{task.milestone.branch.short_name}} task</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>Title <small>(optional)</small></label>
|
||||
@ -18,20 +18,21 @@
|
||||
</div>
|
||||
<label>Milestone</label>
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
<button type="button" data-value=""
|
||||
class="btn btn-small edittask_ms{{task.id}}
|
||||
{% if not task.milestone %}active{% endif %}">
|
||||
None</button>
|
||||
{% if task.milestone not in milestones %}
|
||||
<button type="button" data-value="{{task.milestone.id}}"
|
||||
class="btn btn-small active edittask_ms{{task.id}} btn-success">
|
||||
{{ task.milestone.name }}</button>
|
||||
{% endif %}
|
||||
{% for milestone in milestones %}
|
||||
{% if milestone.series == task.series %}}
|
||||
{% if milestone.branch == task.milestone.branch %}}
|
||||
<button type="button" data-value="{{milestone.id}}"
|
||||
class="btn btn-small
|
||||
{% if task.milestone == milestone %}active{% endif %}
|
||||
{% if milestone.active or task.milestone == milestone %}edittask_ms{{task.id}}{%endif%}
|
||||
{% if not milestone.active %}btn-success
|
||||
{% if not milestone.released or task.milestone == milestone %}edittask_ms{{task.id}}{%endif%}
|
||||
{% if milestone.released %}btn-success
|
||||
{% if task.milestone != milestone %}disabled{%endif%}
|
||||
{% endif %}"
|
||||
{% if task.milestone != milestone and not milestone.active %}disabled="disabled"{%endif%}>
|
||||
{% if task.milestone != milestone and milestone.released %}disabled="disabled"{%endif%}>
|
||||
{{ milestone.name }}</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
@ -33,7 +33,7 @@
|
||||
<tr>
|
||||
<th>Task</th>
|
||||
<th>Project</th>
|
||||
<th>Series</th>
|
||||
<th>Branch</th>
|
||||
<th>Assignee</th>
|
||||
<th>Status</th>
|
||||
<th>Milestone</th>
|
||||
@ -44,10 +44,10 @@
|
||||
<tr class="{{ task.status|taskcolor }}">
|
||||
<td>{{ task.title }}</td>
|
||||
<td>{{ task.project.title }}</td>
|
||||
<td>{{ task.series.name }}</td>
|
||||
<td>{{ task.milestone.branch.name }}</td>
|
||||
<td>{{ task.assignee.username }}</td>
|
||||
<td>{{ task.get_status_display }}</td>
|
||||
<td>{{ task.milestone.name }}</td>
|
||||
<td>{% if not task.milestone.undefined %}{{ task.milestone.name }}{% endif %}</td>
|
||||
<td>
|
||||
<a href="#edittask{{ task.id }}" class="btn btn-micro" data-toggle="modal"><i class="icon-edit"></i></a>
|
||||
<a href="#deltask{{ task.id }}" class="btn btn-micro" data-toggle="modal"><i class="icon-remove"></i></a>
|
||||
|
@ -21,7 +21,6 @@ from django.views.decorators.http import require_POST
|
||||
|
||||
from storyboard.projects.models import Milestone
|
||||
from storyboard.projects.models import Project
|
||||
from storyboard.projects.models import Series
|
||||
from storyboard.stories.models import Comment
|
||||
from storyboard.stories.models import Story
|
||||
from storyboard.stories.models import StoryTag
|
||||
@ -37,14 +36,13 @@ def dashboard(request):
|
||||
|
||||
def view(request, storyid):
|
||||
story = Story.objects.get(id=storyid)
|
||||
active_series = Series.objects.filter(status__gt=0)
|
||||
milestones = Milestone.objects.all()
|
||||
milestones = Milestone.objects.filter(
|
||||
released=False).order_by('branch__release_date')
|
||||
return render(request, "stories.view.html", {
|
||||
'story': story,
|
||||
'milestones': milestones,
|
||||
'priorities': Story.STORY_PRIORITIES,
|
||||
'taskstatuses': Task.TASK_STATUSES,
|
||||
'active_series': active_series,
|
||||
})
|
||||
|
||||
|
||||
@ -96,13 +94,14 @@ def add_story(request):
|
||||
newstory.save()
|
||||
proposed_projects = request.POST['projects'].split()
|
||||
if proposed_projects:
|
||||
series = Series.objects.get(status=2)
|
||||
master_undefined_milestone = Milestone.objects.get(
|
||||
branch__status='M', undefined=True)
|
||||
tasks = []
|
||||
for project in proposed_projects:
|
||||
tasks.append(Task(
|
||||
story=newstory,
|
||||
project=Project.objects.get(name=project),
|
||||
series=series,
|
||||
milestone=master_undefined_milestone,
|
||||
))
|
||||
Task.objects.bulk_create(tasks)
|
||||
proposed_tags = set(request.POST['tags'].split())
|
||||
@ -129,19 +128,20 @@ def add_task(request, storyid):
|
||||
story = Story.objects.get(id=storyid)
|
||||
try:
|
||||
if request.POST['project']:
|
||||
if request.POST['series']:
|
||||
series = Series.objects.get(name=request.POST['series'])
|
||||
if request.POST['milestone']:
|
||||
milestone = Milestone.objects.get(id=request.POST['milestone'])
|
||||
else:
|
||||
series = Series.objects.get(status=2)
|
||||
milestone = Milestone.objects.get(branch__status='M',
|
||||
undefined=True)
|
||||
newtask = Task(
|
||||
story=story,
|
||||
title=request.POST['title'],
|
||||
project=Project.objects.get(name=request.POST['project']),
|
||||
series=series,
|
||||
milestone=milestone,
|
||||
)
|
||||
newtask.save()
|
||||
msg = "Added %s/%s task " % (
|
||||
newtask.project.name, newtask.series.name)
|
||||
newtask.project.name, newtask.milestone.branch.short_name)
|
||||
newcomment = Comment(story=story,
|
||||
action=msg,
|
||||
author=request.user,
|
||||
@ -162,13 +162,8 @@ def edit_task(request, taskid):
|
||||
if (task.title != request.POST['title']):
|
||||
actions.append("title")
|
||||
task.title = request.POST['title']
|
||||
if not request.POST['milestone']:
|
||||
milestone = None
|
||||
milestonename = "None"
|
||||
else:
|
||||
milestone = Milestone.objects.get(
|
||||
id=int(request.POST['milestone']))
|
||||
milestonename = milestone.name
|
||||
milestone = Milestone.objects.get(id=int(request.POST['milestone']))
|
||||
milestonename = milestone.name
|
||||
if (milestone != task.milestone):
|
||||
actions.append("milestone -> %s" % milestonename)
|
||||
task.milestone = milestone
|
||||
@ -186,7 +181,8 @@ def edit_task(request, taskid):
|
||||
actions.append("assignee -> %s" % assigneename)
|
||||
task.assignee = assignee
|
||||
if actions:
|
||||
msg = "Updated %s/%s task " % (task.project.name, task.series.name)
|
||||
msg = "Updated %s/%s task " % (task.project.name,
|
||||
task.milestone.branch.short_name)
|
||||
msg += ", ".join(actions)
|
||||
task.save()
|
||||
newcomment = Comment(story=task.story,
|
||||
@ -205,7 +201,8 @@ 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.series.name)
|
||||
msg = "Deleted %s/%s task" % (task.project.name,
|
||||
task.milestone.branch.short_name)
|
||||
newcomment = Comment(story=task.story,
|
||||
action=msg,
|
||||
author=request.user,
|
||||
|
Loading…
x
Reference in New Issue
Block a user