Merge lp:~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker into lp:~linaro-automation/launchpad-work-items-tracker/linaro
- linaro-roadmap-tracker
- Merge into linaro
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby (community) | Approve | ||
Mattias Backman (community) | Abstain | ||
Review via email: mp+74441@code.launchpad.net |
Commit message
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
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_
> 53 + if attr is None:
> 54 + return attr
> 55 + if isinstance(attr, unicode):
> 56 + return attr
> 57 + return attr.decode(
>
> 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(
>
> 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:/
> You are the owner of lp:~mabac/launchpad-work-items-tracker/linaro-roadmap-tracker.
>
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
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.
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_
File "./html-report", line 329, in template_data
data.
File "/srv/status.
due_date_str = milestone_
File "/srv/status.
end_date = result.get_one()[0]
TypeError: 'NoneType' object is unsubscriptable
and this wasn't a good idea either:
for d in result.description:
perhaps this should be result.
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_
> File "./html-report", line 329, in template_data
> data.update(
> File "/srv/status.
> in days_left
> due_date_str = milestone_
> File "/srv/status.
> 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(
to
result = store.execute(
I wonder why this worked with cur.execute().
Mattias Backman (mabac) wrote : | # |
This didn't break anything since I didn't try to merge it yet. I commented on the wrong mp.
- 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.
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.
James Westby (james-w) wrote : | # |
246 +for lane in report_
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
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_
>
> 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.
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_
> >
> > 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.
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().
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
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 | + |
Hi,
Thanks for this, great to see it moving forward. Overall it looks good.
52 +def unicode_ or_None( attr): "utf-8" )
53 + if attr is None:
54 + return attr
55 + if isinstance(attr, unicode):
56 + return attr
57 + return attr.decode(
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