Skip to content
This repository has been archived by the owner on Aug 1, 2019. It is now read-only.

Commit

Permalink
Tasks are attached to milestones, not series
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ttx committed Jul 18, 2013
1 parent ff8c435 commit bbb154e
Show file tree
Hide file tree
Showing 11 changed files with 78 additions and 99 deletions.
39 changes: 7 additions & 32 deletions README.rst
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions storyboard/about/templates/about.welcome.html
Expand Up @@ -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 &raquo;</a></p>
</div><!--/span-->
<div class="span4">
Expand Down
4 changes: 2 additions & 2 deletions storyboard/projects/admin.py
Expand Up @@ -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)
22 changes: 12 additions & 10 deletions storyboard/projects/models.py
Expand Up @@ -24,14 +24,15 @@ def __unicode__(self):
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):
Expand All @@ -43,8 +44,9 @@ class Meta:

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
Expand Down
6 changes: 3 additions & 3 deletions storyboard/projects/templates/projects.list_tasks.html
Expand Up @@ -11,7 +11,7 @@ <h3>{{ title }} for {{ project.title }}</h3>
<th>Story</th>
<th>Priority</th>
<th>Task</th>
<th>Series</th>
<th>Branch</th>
<th>Assignee</th>
<th>Milestone</th>
</tr>
Expand All @@ -24,9 +24,9 @@ <h3>{{ title }} for {{ project.title }}</h3>
<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>
Expand Down
6 changes: 2 additions & 4 deletions storyboard/stories/models.py
Expand Up @@ -18,7 +18,6 @@

from storyboard.projects.models import Milestone
from storyboard.projects.models import Project
from storyboard.projects.models import Series


class Story(models.Model):
Expand Down Expand Up @@ -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):
Expand Down
32 changes: 19 additions & 13 deletions storyboard/stories/templates/stories.modal_addtask.html
Expand Up @@ -14,23 +14,29 @@ <h3 id="addtaskLabel">Add new task</h3>
<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>
Expand All @@ -39,7 +45,7 @@ <h3 id="addtaskLabel">Add new task</h3>
</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>
2 changes: 1 addition & 1 deletion storyboard/stories/templates/stories.modal_deltask.html
Expand Up @@ -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>
Expand Down
19 changes: 10 additions & 9 deletions storyboard/stories/templates/stories.modal_edittask.html
Expand Up @@ -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>
Expand All @@ -18,20 +18,21 @@ <h3 id="edittaskLabel">Edit
</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 %}
Expand Down
6 changes: 3 additions & 3 deletions storyboard/stories/templates/stories.view.html
Expand Up @@ -33,7 +33,7 @@ <h4>{{ story.title }}</h4>
<tr>
<th>Task</th>
<th>Project</th>
<th>Series</th>
<th>Branch</th>
<th>Assignee</th>
<th>Status</th>
<th>Milestone</th>
Expand All @@ -44,10 +44,10 @@ <h4>{{ story.title }}</h4>
<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>
Expand Down
37 changes: 17 additions & 20 deletions storyboard/stories/views.py
Expand Up @@ -21,7 +21,6 @@

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
Expand All @@ -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,
})


Expand Down Expand Up @@ -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())
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit bbb154e

Please sign in to comment.