Merge lp:~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker into lp:~linaro-automation/launchpad-work-items-tracker/linaro

Proposed by Mattias Backman
Status: Merged
Merged at revision: 294
Proposed branch: lp:~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker
Merge into: lp:~linaro-automation/launchpad-work-items-tracker/linaro
Diff against target: 576 lines (+416/-17) (has conflicts)
10 files modified
collect (+1/-8)
collect_roadmap (+200/-0)
generate-all (+9/-0)
html-report (+38/-0)
lpworkitems/collect_roadmap.py (+24/-0)
lpworkitems/database.py (+30/-1)
lpworkitems/models.py (+1/-8)
lpworkitems/models_roadmap.py (+33/-0)
report_tools.py (+73/-0)
utils.py (+7/-0)
Text conflict in report_tools.py
To merge this branch: bzr merge lp:~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Mattias Backman (community) Abstain
Review via email: mp+74441@code.launchpad.net

Description of the change

Hi,

This branch adds harvesting of the Linaro roadmap from KanbanTool. Everything is still very rough.

The "lanes" (2011Q3, 2011Q4, ... 2013) and "statuses" (Ready, Planning) are represented as workflow_stages in the data from KanbanTool and each workflow_stage has a reference to it's parent workflow_stage. We start by putting all the workflow_stages in a tree to be able to separate lanes from statuses.

The top workflow_stage seems to have no useful data and is discarded. The first level of workflow_stages below the root are the lanes which represent the quarterly planning and we create a Lane db item for each of the lanes found. The next level of workflow_stages represent the sub-statuses of each lane and we only use these to be able to map Kanban cards to the corresponding lane.

Each KanbanTool Task represents a Linaro roadmap card. A card is responsible for tracking it's own status which can be "Ready" or "Planning" according to the Lane information, or something like "Blocked" or "In progress" which will be determined by the linked blueprints.

We need to decide if it's ok to assume that interesting cards always will be two levels deep (lane + status) or if we will also display cards from for instance the "Maybe" lane which does not have sub workflow_stages.

To try it out you would do:
./collect_roadmap --database roadmapdb --config myconfig --board 9977 --token ABCDABCDABCD

where 9977 is the id of the sandbox Kanban board. The token need to be generated by an account with access to the board.

To generate the html page you would do:
./html-report -d roadmapdb --report-type roadmap > test.html

Thanks,

Mattias

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Hi,

Thanks for this, great to see it moving forward. Overall it looks good.

52 +def unicode_or_None(attr):
53 + if attr is None:
54 + return attr
55 + if isinstance(attr, unicode):
56 + return attr
57 + return attr.decode("utf-8")

It would be great to not duplicate this further (apologies for the duplication
that is currently there.)

If it's used could you put it in a module or something?

39 +def data_error(spec_url, msg, warning=False):

Deleting these sorts of unused things would be good. It would be better
to do them in a better fashion if they are needed in this script.

451 +def lanes(db):
452 + cur = db.cursor()
453 + cur.execute('SELECT name, lane_id from lane')
454 + return [(i[0], i[1]) for i in cur]

I'd really like to re-use the ORM classes here, rather than hand-writing
SQL. Perhaps we should be doing the work now to enable that?

The issue is that direct SQL access via the sqlite API interferes with
Storm's caching. So we would need to change all the sql to do through
Storm's SQL interface.

It's a lot of code to change, but not difficult.

Perhaps we could even add some sort of compatibility layer so that less
code has to change?

It doesn't have to stop this code from landing, but maybe it should
be the next thing, as it will make later work easier.

Also, do we want to add the code to actually generate the roadmap.html
on status.linaro.org?

Thanks,

James

review: Approve
Revision history for this message
Mattias Backman (mabac) wrote :

On Wed, Sep 7, 2011 at 11:09 PM, James Westby <email address hidden> wrote:
> Review: Approve
> Hi,
>
> Thanks for this, great to see it moving forward. Overall it looks good.
>
> 52      +def unicode_or_None(attr):
> 53      + if attr is None:
> 54      + return attr
> 55      + if isinstance(attr, unicode):
> 56      + return attr
> 57      + return attr.decode("utf-8")
>
> It would be great to not duplicate this further (apologies for the duplication
> that is currently there.)
>
> If it's used could you put it in a module or something?
>
> 39      +def data_error(spec_url, msg, warning=False):
>
> Deleting these sorts of unused things would be good. It would be better
> to do them in a better fashion if they are needed in this script.

Sure, I'll fix the above cleanups.

>
> 451     +def lanes(db):
> 452     + cur = db.cursor()
> 453     + cur.execute('SELECT name, lane_id from lane')
> 454     + return [(i[0], i[1]) for i in cur]
>
> I'd really like to re-use the ORM classes here, rather than hand-writing
> SQL. Perhaps we should be doing the work now to enable that?

That would be good. I spent a lot of time trying to find examples of
that, but now I understand why I didn't find it.

>
> The issue is that direct SQL access via the sqlite API interferes with
> Storm's caching. So we would need to change all the sql to do through
> Storm's SQL interface.
>
> It's a lot of code to change, but not difficult.
>
> Perhaps we could even add some sort of compatibility layer so that less
> code has to change?

If we need all explicit sql queries to go away before we can use the
ORM classes, then that might be a good idea. I've looked over the sql
queries and there are a lot of them. On the other hand, if we get rid
of all the hand-written sql in one go, then we won't be stuck
maintaining the left-overs. I'll try to get an idea of how much work
it would be.

>
> It doesn't have to stop this code from landing, but maybe it should
> be the next thing, as it will make later work easier.
>
> Also, do we want to add the code to actually generate the roadmap.html
> on status.linaro.org?

Right, I think I've found how to do that now.

>
> Thanks,
>
> James
>
> --
> https://code.launchpad.net/~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker/+merge/74441
> You are the owner of lp:~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker.
>

Revision history for this message
James Westby (james-w) wrote :

On Thu, 08 Sep 2011 10:04:11 -0000, Mattias Backman <email address hidden> wrote:
> On Wed, Sep 7, 2011 at 11:09 PM, James Westby <email address hidden> wrote:
> If we need all explicit sql queries to go away before we can use the
> ORM classes, then that might be a good idea. I've looked over the sql
> queries and there are a lot of them. On the other hand, if we get rid
> of all the hand-written sql in one go, then we won't be stuck
> maintaining the left-overs. I'll try to get an idea of how much work
> it would be.

Nope, we just need to make the queries go through Storm's Store object.

store.execute(SQL) rather that cur = db.cursor(); cur.execute(SQL)

There is also a different in how to iterate over result sets, with storm
it is

  result = store.execute(SQL)
  for row in result:

with sqlite directly it is

  cur.execute(SQL)
  for row in cur:

So either making these translations, and passing around a Store object
rather than the sqlite db is what is needed.

Apologies for not being clear before. Getting rid of all the hand
written SQL would be good, but not a requirement to use the ORM for new
code.

Thanks,

James

Revision history for this message
Mattias Backman (mabac) wrote :

> store.execute(SQL) rather that cur = db.cursor(); cur.execute(SQL)
>
> There is also a different in how to iterate over result sets, with storm
> it is
>
>  result = store.execute(SQL)
>  for row in result:
>
> with sqlite directly it is
>
>  cur.execute(SQL)
>  for row in cur:

That sounds manageable. I'll have a go at it.

Revision history for this message
Mattias Backman (mabac) wrote :

Oops. This broke status.linaro.org when I merged it. It's reverted now so the next update should fix the pages, I hope.

I got a lot of these:

Traceback (most recent call last):
  File "./html-report", line 551, in <module>
    method(store, opts)
  File "./html-report", line 356, in burndown
    data = self.template_data(db, opts)
  File "./html-report", line 329, in template_data
    data.update(report_tools.days_left(db, milestone=opts.milestone))
  File "/srv/status.linaro.org/work-items-tracker/report_tools.py", line 657, in days_left
    due_date_str = milestone_due_date(store, milestone=milestone)
  File "/srv/status.linaro.org/work-items-tracker/report_tools.py", line 593, in milestone_due_date
    end_date = result.get_one()[0]
TypeError: 'NoneType' object is unsubscriptable

and this wasn't a good idea either:
        for d in result.description:
            spec_dict[ d[0] ] = spec_row[ d[0] ]
perhaps this should be result.values('description') or something similar.

review: Needs Fixing
Revision history for this message
Mattias Backman (mabac) wrote :

> Oops. This broke status.linaro.org when I merged it. It's reverted now so the
> next update should fix the pages, I hope.
>
> I got a lot of these:
>
> Traceback (most recent call last):
> File "./html-report", line 551, in <module>
> method(store, opts)
> File "./html-report", line 356, in burndown
> data = self.template_data(db, opts)
> File "./html-report", line 329, in template_data
> data.update(report_tools.days_left(db, milestone=opts.milestone))
> File "/srv/status.linaro.org/work-items-tracker/report_tools.py", line 657,
> in days_left
> due_date_str = milestone_due_date(store, milestone=milestone)
> File "/srv/status.linaro.org/work-items-tracker/report_tools.py", line 593,
> in milestone_due_date
> end_date = result.get_one()[0]
> TypeError: 'NoneType' object is unsubscriptable

Strange. After changing to store.execute() I have to change
        result = store.execute('SELECT due_date FROM milestones WHERE name = ?', (milestone,))
to
        result = store.execute('SELECT due_date FROM milestones WHERE name = ?', (unicode(milestone), ))

I wonder why this worked with cur.execute().

Revision history for this message
Mattias Backman (mabac) wrote :

This didn't break anything since I didn't try to merge it yet. I commented on the wrong mp.

review: Abstain
298. By Mattias Backman

Merge change to use Storm for sql queries.

299. By Mattias Backman

Add code for generating pages.

300. By Mattias Backman

Clean up and move unicode_or_None to utils.py.

301. By Mattias Backman

Remove send_error_mails. Not sure if I should use it.

Revision history for this message
Mattias Backman (mabac) wrote :

Please ignore my latest comments on this mp, they where intended for the storm changes in another branch.

This should now create a roadmap.html and one roadmap-<lane>.html for each lane.

Revision history for this message
James Westby (james-w) wrote :

246 +for lane in report_tools.lanes(store):

I don't think we need to query again there, we can just use the "lanes"
variable that was used in the previous call.

This looks good, assuming that it will cope with there being no
lanes in the db for a while. (The pages aren't linked, so it's fine
if they are empty or missing or whatever, but it can't crash if there
are no lanes.)

Thanks,

James

review: Approve
Revision history for this message
Mattias Backman (mabac) wrote :

On Mon, Sep 12, 2011 at 9:29 PM, James Westby <email address hidden> wrote:
> Review: Approve
>
> 246     +for lane in report_tools.lanes(store):
>
> I don't think we need to query again there, we can just use the "lanes"
> variable that was used in the previous call.

Fixed. It was the first call that could go away.

>
> This looks good, assuming that it will cope with there being no
> lanes in the db for a while. (The pages aren't linked, so it's fine
> if they are empty or missing or whatever, but it can't crash if there
> are no lanes.)

I think it'll be fine, as long as the new tables are in the db. If the
Lanes table is empty, the roadmap.html will contain only the heading
and the other pages will not be created.

Revision history for this message
James Westby (james-w) wrote :

On Tue, 13 Sep 2011 07:13:23 -0000, Mattias Backman <email address hidden> wrote:
> On Mon, Sep 12, 2011 at 9:29 PM, James Westby <email address hidden> wrote:
> > Review: Approve
> >
> > 246     +for lane in report_tools.lanes(store):
> >
> > I don't think we need to query again there, we can just use the "lanes"
> > variable that was used in the previous call.
>
> Fixed. It was the first call that could go away.
>
> >
> > This looks good, assuming that it will cope with there being no
> > lanes in the db for a while. (The pages aren't linked, so it's fine
> > if they are empty or missing or whatever, but it can't crash if there
> > are no lanes.)
>
> I think it'll be fine, as long as the new tables are in the db. If the
> Lanes table is empty, the roadmap.html will contain only the heading
> and the other pages will not be created.

Yep, you can assume that the tables are present given that you coded the
migration, so this should be good to land whenever you like.

Thanks,

James

302. By Mattias Backman

Move roadmap generation to happen after all the other pages are generated.

Revision history for this message
Mattias Backman (mabac) wrote :

I moved the roadmap generation to happen last so it won't break the rest of status.linaro.org, just in case.

The Storm changes have to land before this branch so we don't mix cursor.execute() with store.execute().

Revision history for this message
James Westby (james-w) wrote :

On Fri, 16 Sep 2011 09:58:27 -0000, Mattias Backman <email address hidden> wrote:
> I moved the roadmap generation to happen last so it won't break the rest of status.linaro.org, just in case.

That shouldn't be needed. The actual page is generated in a subprocess,
so the vast majority of the code won't crash the entire thing. Not a
problem to have this though for that small bit of code that runs the
subprocess.

> The Storm changes have to land before this branch so we don't mix cursor.execute() with store.execute().

Ok,

This is good to land whenever you are comfortable doing so.

Thanks,

James

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'collect'
2--- collect 2011-09-06 13:07:42 +0000
3+++ collect 2011-09-16 09:57:23 +0000
4@@ -23,6 +23,7 @@
5 TeamParticipation,
6 Workitem,
7 )
8+from utils import unicode_or_None
9 import report_tools
10
11
12@@ -59,14 +60,6 @@
13 return link.decode("utf-8")
14
15
16-def unicode_or_None(attr):
17- if attr is None:
18- return attr
19- if isinstance(attr, unicode):
20- return attr
21- return attr.decode("utf-8")
22-
23-
24 import simplejson
25 _orig_loads = simplejson.loads
26 def loads(something):
27
28=== added file 'collect_roadmap'
29--- collect_roadmap 1970-01-01 00:00:00 +0000
30+++ collect_roadmap 2011-09-16 09:57:23 +0000
31@@ -0,0 +1,200 @@
32+#!/usr/bin/python
33+#
34+# Pull items from the Linaro roadmap in Kanbantool and put them into a database.
35+
36+import urllib2, re, sys, optparse, smtplib, pwd, os
37+import simplejson
38+import logging
39+from email.mime.text import MIMEText
40+
41+from lpworkitems.collect_roadmap import CollectorStore
42+from lpworkitems.database import get_store
43+from lpworkitems.error_collector import (
44+ ErrorCollector,
45+ StderrErrorCollector,
46+ )
47+from lpworkitems.models_roadmap import (
48+ Lane,
49+ Card,
50+ )
51+from utils import unicode_or_None
52+import report_tools
53+
54+
55+# An ErrorCollector to collect the data errors for later reporting
56+error_collector = None
57+
58+
59+logger = logging.getLogger("linaroroadmap")
60+
61+
62+def dbg(msg):
63+ '''Print out debugging message if debugging is enabled.'''
64+ logger.debug(msg)
65+
66+
67+def get_kanban_url(item_url, api_token):
68+ base_url = 'https://linaro.kanbantool.com/api/v1'
69+ return "%s/%s?api_token=%s" % (base_url, item_url, api_token)
70+
71+
72+def get_kanban_data(url):
73+ data = None
74+ try:
75+ data = simplejson.load(urllib2.urlopen(url))
76+ except urllib2.HTTPError, e:
77+ print "HTTP error: %d" % e.code
78+ except urllib2.URLError, e:
79+ print "Network error: %s" % e.reason.args[1]
80+
81+ return data
82+
83+
84+def kanban_import_lanes(collector, workflow_stages):
85+ nodes = {}
86+ root_node_id = None
87+
88+ # Iterate over all workflow_stages which may be in any order.
89+ for workflow_stage in workflow_stages:
90+ parent_id = workflow_stage['parent_id']
91+ if parent_id is None:
92+ assert root_node_id is None, 'We have already found the root node.'
93+ root_node_id = workflow_stage['id']
94+ else:
95+ if parent_id not in nodes:
96+ nodes[parent_id] = []
97+ # Add child workflow_stage
98+ nodes[parent_id].append(workflow_stage)
99+
100+ statuses = []
101+ for node in nodes[root_node_id]:
102+ assert node['parent_id'] == root_node_id
103+ model_lane = Lane(unicode_or_None(node['name']),
104+ node['id'])
105+ collector.store_lane(model_lane)
106+ node_id = node['id']
107+ if node_id in nodes:
108+ statuses.extend(nodes[node_id])
109+ return statuses
110+
111+
112+def kanban_import_cards(collector, tasks, status_list):
113+
114+ for task in tasks:
115+ status_id = task['task']['workflow_stage_id']
116+ assert status_id is not None
117+ task_status = None
118+ for status in status_list:
119+ if status['id'] == status_id:
120+ task_status = status
121+ break
122+ if task_status is not None:
123+ lane_id = task_status['parent_id']
124+ assert lane_id is not None
125+ model_card = Card(unicode_or_None(task['task']['name']),
126+ task['task']['id'], lane_id)
127+ model_card.status = unicode_or_None(task_status['name'])
128+ collector.store_card(model_card)
129+ else:
130+ print "Task '%s' does not have a status." % task['task']['name']
131+
132+
133+def kanban_import(collector, cfg, board_id, api_token):
134+ '''Collect roadmap items from KanbanTool into DB.'''
135+ board_url = get_kanban_url('boards/%s.json' % board_id, api_token)
136+ board = get_kanban_data(board_url)
137+ status_list = kanban_import_lanes(collector,
138+ board['board']['workflow_stages'])
139+
140+ tasks_url = get_kanban_url('boards/%s/tasks.json' % board_id, api_token)
141+ tasks = get_kanban_data(tasks_url)
142+ kanban_import_cards(collector, tasks, status_list)
143+
144+
145+########################################################################
146+#
147+# Program operations and main
148+#
149+########################################################################
150+
151+def parse_argv():
152+ '''Parse CLI arguments.
153+
154+ Return (options, args) tuple.
155+ '''
156+ optparser = optparse.OptionParser()
157+ optparser.add_option('-d', '--database',
158+ help='Path to database', dest='database', metavar='PATH')
159+ optparser.add_option('-c', '--config',
160+ help='Path to configuration file', dest='config', metavar='PATH')
161+ optparser.add_option('--debug', action='store_true', default=False,
162+ help='Enable debugging output in parsing routines')
163+ optparser.add_option('--mail', action='store_true', default=False,
164+ help='Send data errors as email (according to "error_config" map in '
165+ 'config file) instead of printing to stderr', dest='mail')
166+ optparser.add_option('--board',
167+ help='Board id at Kanbantool', dest='board')
168+ optparser.add_option('--token',
169+ help='API token for authentication', dest='api_token')
170+
171+ (opts, args) = optparser.parse_args()
172+
173+ if not opts.database:
174+ optparser.error('No database given')
175+ if not opts.config:
176+ optparser.error('No config given')
177+
178+ return opts, args
179+
180+
181+def setup_logging(debug):
182+ ch = logging.StreamHandler()
183+ ch.setLevel(logging.INFO)
184+ formatter = logging.Formatter("%(message)s")
185+ ch.setFormatter(formatter)
186+ logger.setLevel(logging.INFO)
187+ logger.addHandler(ch)
188+ if debug:
189+ ch.setLevel(logging.DEBUG)
190+ formatter = logging.Formatter(
191+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
192+ ch.setFormatter(formatter)
193+ logger.setLevel(logging.DEBUG)
194+
195+
196+def main():
197+ report_tools.fix_stdouterr()
198+
199+ (opts, args) = parse_argv()
200+
201+ setup_logging(opts.debug)
202+
203+ global error_collector
204+ if opts.mail:
205+ error_collector = ErrorCollector()
206+ else:
207+ error_collector = StderrErrorCollector()
208+
209+ cfg = report_tools.load_config(opts.config)
210+
211+ lock_path = opts.database + ".collect_lock"
212+ lock_f = open(lock_path, "wb")
213+ if report_tools.lock_file(lock_f) is None:
214+ print "Another instance is already running"
215+ sys.exit(0)
216+
217+ store = get_store(opts.database)
218+ collector = CollectorStore(store, '', error_collector)
219+
220+ collector.clear_lanes()
221+ collector.clear_cards()
222+
223+ kanban_import(collector, cfg, opts.board, opts.api_token)
224+
225+ store.commit()
226+
227+ os.unlink(lock_path)
228+
229+
230+if __name__ == '__main__':
231+ main()
232
233=== modified file 'generate-all'
234--- generate-all 2011-09-12 19:06:34 +0000
235+++ generate-all 2011-09-16 09:57:23 +0000
236@@ -152,6 +152,15 @@
237 basename = os.path.join(opts.output_dir, 'index')
238 report_tools.status_overview(my_path, opts.database, basename, opts.config, root=opts.root)
239
240+# roadmap overview
241+basename = os.path.join(opts.output_dir, 'roadmap')
242+report_tools.roadmap_overview(my_path, opts.database, basename, opts.config, root=opts.root)
243+
244+# roadmap lanes
245+for lane in report_tools.lanes(store):
246+ basename = os.path.join(opts.output_dir, 'roadmap-' + lane.name)
247+ report_tools.roadmap_pages(my_path, opts.database, basename, opts.config, lane, root=opts.root)
248+
249
250 def copy_files(source_dir):
251 for filename in os.listdir(source_dir):
252
253=== modified file 'html-report'
254--- html-report 2011-09-12 19:06:34 +0000
255+++ html-report 2011-09-16 09:57:23 +0000
256@@ -449,6 +449,42 @@
257 print report_tools.fill_template(
258 "workitem_list.html", data, theme=opts.theme)
259
260+ def roadmap(self, store, opts):
261+ if not opts.title:
262+ title = 'Linaro Roadmap'
263+ else:
264+ title = opts.title
265+
266+ data = self.template_data(store, opts)
267+ lanes = report_tools.lanes(store)
268+
269+ data.update(dict(lanes=lanes))
270+ data.update(dict(page_title=title))
271+ data.update(dict(page_type="roadmap"))
272+ print report_tools.fill_template(
273+ "roadmap.html", data, theme=opts.theme)
274+
275+
276+ def roadmap_page(self, store, opts):
277+ if opts.lane is None:
278+ print "<h1>Error, no lane specified.</h1>"
279+ if not opts.title:
280+ title = opts.lane
281+ else:
282+ title = opts.title
283+
284+ data = self.template_data(store, opts)
285+ lane = report_tools.lane(store, opts.lane).one()
286+ statuses = []
287+ for status, cards in report_tools.statuses(store, lane):
288+ statuses.append(dict(name=status, cards=cards))
289+
290+ data.update(dict(statuses=statuses))
291+ data.update(dict(page_type="roadmap_lane"))
292+ data.update(dict(lane=title))
293+ print report_tools.fill_template(
294+ "roadmap_lane.html", data, theme=opts.theme)
295+
296
297 class WorkitemsOnDate(object):
298
299@@ -527,6 +563,8 @@
300 help="Include all milestones targetted to this date.")
301 optparser.add_option('--theme', dest="theme",
302 help="The theme to use.", default="linaro")
303+ optparser.add_option('--lane',
304+ help='Roadmap lane', dest='lane')
305
306 (opts, args) = optparser.parse_args()
307 if not opts.database:
308
309=== added file 'lpworkitems/collect_roadmap.py'
310--- lpworkitems/collect_roadmap.py 1970-01-01 00:00:00 +0000
311+++ lpworkitems/collect_roadmap.py 2011-09-16 09:57:23 +0000
312@@ -0,0 +1,24 @@
313+from lpworkitems import models_roadmap
314+
315+
316+class CollectorStore(object):
317+
318+ def __init__(self, store, base_url, error_collector):
319+ self.store = store
320+ self.base_url = base_url
321+ self.error_collector = error_collector
322+
323+ def _clear_all(self, *find_args):
324+ self.store.find(*find_args).remove()
325+
326+ def clear_lanes(self):
327+ self._clear_all(models_roadmap.Lane)
328+
329+ def clear_cards(self):
330+ self._clear_all(models_roadmap.Card)
331+
332+ def store_lane(self, lane):
333+ self.store.add(lane)
334+
335+ def store_card(self, card):
336+ self.store.add(card)
337
338=== modified file 'lpworkitems/database.py'
339--- lpworkitems/database.py 2011-06-24 19:09:26 +0000
340+++ lpworkitems/database.py 2011-09-16 09:57:23 +0000
341@@ -7,7 +7,7 @@
342 store.execute('''CREATE TABLE version (
343 db_layout_ref INT NOT NULL
344 )''')
345- store.execute('''INSERT INTO version VALUES (10)''')
346+ store.execute('''INSERT INTO version VALUES (11)''')
347
348 store.execute('''CREATE TABLE specs (
349 name VARCHAR(255) PRIMARY KEY,
350@@ -90,6 +90,19 @@
351 display_name VARCHAR(50)
352 )''')
353
354+ store.execute('''CREATE TABLE lane (
355+ name VARCHAR(200) NOT NULL,
356+ lane_id NOT NULL,
357+ cards REFERENCES card(card_id)
358+ )''')
359+
360+ store.execute('''CREATE TABLE card (
361+ name VARCHAR(200) NOT NULL,
362+ card_id NOT NULL,
363+ status VARCHAR(50),
364+ lane_id REFERENCES lane(lane_id)
365+ )''')
366+
367
368 def upgrade_if_needed(store):
369 # upgrade DB layout
370@@ -177,6 +190,21 @@
371 )''')
372 store.execute('UPDATE version SET db_layout_ref = 10')
373 ver = 10
374+ if ver == 10:
375+ store.execute('''CREATE TABLE lane (
376+ name VARCHAR(200) NOT NULL,
377+ lane_id NOT NULL,
378+ cards REFERENCES card(card_id)
379+ )''')
380+
381+ store.execute('''CREATE TABLE card (
382+ name VARCHAR(200) NOT NULL,
383+ card_id NOT NULL,
384+ status VARCHAR(50),
385+ lane_id REFERENCES lane(lane_id)
386+ )''')
387+ store.execute('UPDATE version SET db_layout_ref = 11')
388+ ver = 11
389
390
391 def get_store(dbpath):
392@@ -205,5 +233,6 @@
393 store.execute('''CREATE INDEX work_items_date_idx ON work_items (date)''')
394 store.execute('''CREATE INDEX work_items_status_idx ON work_items (status)''')
395
396+
397 def create_v6_indexes(store):
398 store.execute('''CREATE INDEX work_items_assignee_milestone_idx on work_items(assignee,milestone)''')
399
400=== modified file 'lpworkitems/models.py'
401--- lpworkitems/models.py 2011-03-24 15:37:28 +0000
402+++ lpworkitems/models.py 2011-09-16 09:57:23 +0000
403@@ -1,17 +1,10 @@
404 import datetime
405 import re
406+from utils import unicode_or_None
407
408 from storm.locals import Date, Reference, ReferenceSet, Unicode
409
410
411-def unicode_or_None(attr):
412- if attr is None:
413- return attr
414- if isinstance(attr, unicode):
415- return attr
416- return attr.decode("utf-8")
417-
418-
419 def fill_blueprint_info_from_launchpad(model_bp, lp_bp):
420 model_bp.name = unicode_or_None(lp_bp.name)
421 model_bp.url = unicode_or_None(lp_bp.web_link)
422
423=== added file 'lpworkitems/models_roadmap.py'
424--- lpworkitems/models_roadmap.py 1970-01-01 00:00:00 +0000
425+++ lpworkitems/models_roadmap.py 2011-09-16 09:57:23 +0000
426@@ -0,0 +1,33 @@
427+import datetime
428+import re
429+from utils import unicode_or_None
430+
431+from storm.locals import Date, Reference, ReferenceSet, Unicode, Int
432+
433+
434+class Card(object):
435+
436+ __storm_table__ = "card"
437+
438+ name = Unicode()
439+ status = Unicode()
440+ card_id = Int(primary=True)
441+ lane_id = Int()
442+
443+ def __init__(self, name, card_id, lane_id):
444+ self.lane_id = lane_id
445+ self.card_id = card_id
446+ self.name = name
447+
448+
449+class Lane(object):
450+
451+ __storm_table__ = "lane"
452+
453+ name = Unicode()
454+ lane_id = Int(primary=True)
455+ cards = ReferenceSet(lane_id, Card.lane_id)
456+
457+ def __init__(self, name, lane_id):
458+ self.lane_id = lane_id
459+ self.name = name
460
461=== modified file 'report_tools.py'
462--- report_tools.py 2011-09-12 19:06:34 +0000
463+++ report_tools.py 2011-09-16 09:57:23 +0000
464@@ -9,6 +9,10 @@
465 from cgi import escape
466 import errno
467 import fcntl
468+from lpworkitems.models_roadmap import (
469+ Lane,
470+ Card,
471+)
472
473 valid_states = [u'inprogress', u'blocked', u'todo', u'done', u'postponed']
474 state_labels = [u'In Progress', u'Blocked', u'Todo', u'Done', u'Postponed']
475@@ -180,6 +184,39 @@
476 fh.close()
477
478
479+def roadmap_overview(my_path, database, basename, config, root=None):
480+ cfg = load_config(config)
481+ fh = open(basename + '.html', 'w')
482+ try:
483+ args = [os.path.join(my_path, 'html-report'), '-d', database]
484+ args += ['--report-type', 'roadmap']
485+ if root:
486+ args += ['--root', root]
487+ report_args(args, theme=get_theme(cfg))
488+ proc = Popen(args, stdout=fh)
489+ print basename + '.html'
490+ proc.wait()
491+ finally:
492+ fh.close()
493+
494+
495+def roadmap_pages(my_path, database, basename, config, lane, root=None):
496+ cfg = load_config(config)
497+ fh = open(basename + '.html', 'w')
498+ try:
499+ args = [os.path.join(my_path, 'html-report'), '-d', database]
500+ args += ['--report-type', 'roadmap_page']
501+ args += ['--lane', lane.name]
502+ if root:
503+ args += ['--root', root]
504+ report_args(args, theme=get_theme(cfg))
505+ proc = Popen(args, stdout=fh)
506+ print basename + '.html'
507+ proc.wait()
508+ finally:
509+ fh.close()
510+
511+
512 def run_reports(my_path, database, basename, config, milestone=None, team=None,
513 user=None, trend_starts=None, trend_override=None, burnup=False, root=None, date=None):
514
515@@ -886,6 +923,7 @@
516 return rv
517
518
519+<<<<<<< TREE
520 def subteams(db, team):
521 cur = db.cursor()
522 cur.execute('SELECT name from team_structure where team = ?', (team,))
523@@ -899,6 +937,41 @@
524
525
526 def milestone_list(db):
527+=======
528+def lanes(store):
529+ return store.find(Lane)
530+
531+
532+def lane(store, lane):
533+ return store.find(Lane, Lane.name == unicode(lane))
534+
535+
536+def lane_cards(store, lane):
537+ return store.find(Card, Card.lane_id == lane.lane_id)
538+
539+
540+def statuses(store, lane):
541+ result = []
542+ for status in store.find(Card.status,
543+ Card.lane_id == lane.lane_id).config(distinct=True):
544+ result.append((status, store.find(Card,
545+ Card.lane_id == lane.lane_id,
546+ Card.status == status)))
547+ return result
548+
549+
550+def subteams(store, team):
551+ result = store.execute('SELECT name from team_structure where team = ?', (team,))
552+ return [i[0] for i in result]
553+
554+
555+def all_teams(store):
556+ result = store.execute('SELECT DISTINCT team from teams where team is not NULL')
557+ return [i[0] for i in result]
558+
559+
560+def milestone_list(store):
561+>>>>>>> MERGE-SOURCE
562 milestones = {}
563
564 cur = db.cursor()
565
566=== added file 'utils.py'
567--- utils.py 1970-01-01 00:00:00 +0000
568+++ utils.py 2011-09-16 09:57:23 +0000
569@@ -0,0 +1,7 @@
570+def unicode_or_None(attr):
571+ if attr is None:
572+ return attr
573+ if isinstance(attr, unicode):
574+ return attr
575+ return attr.decode("utf-8")
576+

Subscribers

People subscribed via source and target branches