Merge lp:~nskaggs/qa-dashboard/contribtrack into lp:qa-dashboard
- contribtrack
- Merge into dev
Proposed by
Chris Johnston
Status: | Work in progress |
---|---|
Proposed branch: | lp:~nskaggs/qa-dashboard/contribtrack |
Merge into: | lp:qa-dashboard |
Diff against target: |
865 lines (+802/-1) 9 files modified
contribtrack/admin.py (+5/-0) contribtrack/management/commands/pull_qatracker.py (+208/-0) contribtrack/management/commands/qatracker.py (+492/-0) contribtrack/migrations/0001_initial.py (+65/-0) contribtrack/models.py (+13/-0) contribtrack/tests.py (+16/-0) contribtrack/views.py (+1/-0) qa_dashboard/settings.py (+1/-0) setup.sh (+1/-1) |
To merge this branch: | bzr merge lp:~nskaggs/qa-dashboard/contribtrack |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
QA Dashboard Developers | Pending | ||
Review via email: mp+162552@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Unmerged revisions
- 381. By Nicholas Skaggs
-
removed summing of values, instead store a daily value everyday
- 380. By Nicholas Skaggs
-
fix script to write data properly
- 379. By Nicholas Skaggs
-
first working version of contribtrack with qatracker pull
- 378. By Nicholas Skaggs
-
added contributors to model
- 377. By Nicholas Skaggs
-
skeleton for community results
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'contribtrack' |
2 | === added file 'contribtrack/__init__.py' |
3 | === added file 'contribtrack/admin.py' |
4 | --- contribtrack/admin.py 1970-01-01 00:00:00 +0000 |
5 | +++ contribtrack/admin.py 2013-05-06 01:14:24 +0000 |
6 | @@ -0,0 +1,5 @@ |
7 | +from django.contrib import admin |
8 | +from contribtrack.models import CommunityResult, CommunityContributor |
9 | + |
10 | +admin.site.register(CommunityResult) |
11 | +admin.site.register(CommunityContributor) |
12 | |
13 | === added directory 'contribtrack/management' |
14 | === added file 'contribtrack/management/__init__.py' |
15 | === added directory 'contribtrack/management/commands' |
16 | === added file 'contribtrack/management/commands/__init__.py' |
17 | === added file 'contribtrack/management/commands/pull_qatracker.py' |
18 | --- contribtrack/management/commands/pull_qatracker.py 1970-01-01 00:00:00 +0000 |
19 | +++ contribtrack/management/commands/pull_qatracker.py 2013-05-06 01:14:24 +0000 |
20 | @@ -0,0 +1,208 @@ |
21 | +# QA Dashboard |
22 | +# Copyright 2013 Canonical Ltd. |
23 | + |
24 | +# This program is free software: you can redistribute it and/or modify |
25 | +# it under the terms of the GNU Affero General Public License version |
26 | +# 3, as published by the Free Software Foundation. |
27 | + |
28 | +# This program is distributed in the hope that it will be useful, but |
29 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
30 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
31 | +# PURPOSE. See the GNU Affero General Public License for more details. |
32 | + |
33 | +# You should have received a copy of the GNU Affero General Public |
34 | +# License along with this program. If not, see |
35 | +# <http://www.gnu.org/licenses/>. |
36 | + |
37 | +import sys |
38 | +from qatracker import QATracker |
39 | +import argparse |
40 | +import datetime |
41 | +import calendar |
42 | +import json |
43 | +import re |
44 | +import os |
45 | +import time |
46 | +from django.core.management.base import BaseCommand |
47 | +from django.db.models import Max |
48 | +from optparse import make_option |
49 | + |
50 | +from contribtrack.models import ( |
51 | + CommunityResult, |
52 | + CommunityContributor, |
53 | +) |
54 | + |
55 | +def init(date, days): |
56 | + # Get min and max date in UTC |
57 | + if date: |
58 | + startDate = datetime.datetime.strptime(date, '%Y-%m-%d') |
59 | + else: |
60 | + #just default to right now |
61 | + startDate = datetime.datetime.strptime(datetime.datetime.today().strftime('%Y-%m-%d'),'%Y-%m-%d') |
62 | + endDate = datetime.datetime.strptime(startDate.strftime('%Y-%m-%d'), '%Y-%m-%d') + datetime.timedelta(days=int(days)-1) |
63 | + return startDate, endDate |
64 | + |
65 | +def generateTimestamp(date): |
66 | + #generate utc timestamp |
67 | + utcDate = datetime.datetime.strptime(date.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S') |
68 | + #print("utcDate: %s" % utcDate) |
69 | + return utcDate |
70 | + |
71 | +def getInstance(domain, dist): |
72 | + # Establish the XML-RPC connection (user and api-key are optional for read-only access) |
73 | + instance = QATracker("http://" + domain + ".qa.ubuntu.com/xmlrpc.php") |
74 | + |
75 | + #Get milestones |
76 | + milestones = [milestone for milestone in instance.get_milestones() if dist in milestone.title] |
77 | + |
78 | + if len(milestones) == 0: |
79 | + print("Milestones not found for domain %s and dist %s" % (domain,dist)) |
80 | + sys.exit(1) |
81 | + |
82 | + # Get a list of all products |
83 | + products = instance.get_products() |
84 | + |
85 | + if len(products) == 0: |
86 | + print("Products not found for domain %s and dist %s" % (domain,dist)) |
87 | + sys.exit(1) |
88 | + |
89 | + return milestones, products |
90 | + |
91 | +def getStats(milestones, products, date): |
92 | + results = 0 |
93 | + bugs = 0 |
94 | + cases = 0 |
95 | + builds = 0 |
96 | + images = 0 |
97 | + count = 0 |
98 | + stats = [] |
99 | + testers = {} |
100 | + #print("Processing %s milestones" % len(milestones)) |
101 | + for milestone in milestones: |
102 | + builds = milestone.get_builds() |
103 | + filteredbuilds = [] |
104 | + #print("%s builds for %s" % (len(builds),milestone.title)) |
105 | + |
106 | + #process for dates |
107 | + for build in builds: |
108 | + if build.date: |
109 | + buildDate = datetime.datetime.strptime(build.date.strftime('%Y-%m-%d'), '%Y-%m-%d') |
110 | + processDate = datetime.datetime.strptime(date.strftime('%Y-%m-%d'), '%Y-%m-%d') |
111 | + #print("Comparing %s to %s" % (build.date, date)) |
112 | + #product = [product for product in products if product.id == build.productid][0] |
113 | + #print("Comparing %s to %s for %s" % (buildDate, processDate, product.title)) |
114 | + #if build.date != date: |
115 | + if buildDate == processDate: |
116 | + #print("Added %s" % buildDate) |
117 | + filteredbuilds.append(build) |
118 | + |
119 | + |
120 | + #print("%s matching builds for %s from %s" % (len(filteredbuilds),processDate.strftime('%Y-%m-%d'), milestone.title)) |
121 | + print("%s matching builds from %s" % (len(filteredbuilds), milestone.title)) |
122 | + |
123 | + for build in filteredbuilds: |
124 | + count += 1 |
125 | + #print("Processing build %s of %s " % (count,len(filteredbuilds)),end='\r') |
126 | + print("Processing build %s of %s " % (count,len(filteredbuilds))) |
127 | + #clear stats |
128 | + image = {} |
129 | + image["testcases"] = 0 |
130 | + image["results"] = 0 |
131 | + image["testers"] = 0 |
132 | + |
133 | + images = images + 1 |
134 | + product = [product for product in products if product.id == build.productid][0] |
135 | + image["name"] = product.title |
136 | + #print(" Processing %s" % (product.title)) |
137 | + for testcase in product.get_testcases(milestone): |
138 | + cases += 1 |
139 | + image["testcases"] += 1 |
140 | + for result in build.get_results(testcase): |
141 | + results += 1 |
142 | + image["results"] += 1 |
143 | + if not result.reportername in testers: |
144 | + testers[result.reportername] = 0 |
145 | + testers[result.reportername] += 1 |
146 | + image["testers"] += 1 |
147 | + #store stats for image |
148 | + stats.append(image) |
149 | + |
150 | + # Number of testcases |
151 | + print("%s testers for %s" % (len(testers), milestone.title)) |
152 | + |
153 | + # Number of results |
154 | + print("%s results for %s" % (results, milestone.title)) |
155 | + |
156 | + return results,testers |
157 | + |
158 | +def processTracker(dist, domain, milestones, products, startDate, endDate): |
159 | + #loop through each date and create data |
160 | + print("Processing %s till %s" % (startDate.strftime('%Y-%m-%d'), endDate.strftime('%Y-%m-%d'))) |
161 | + date = startDate |
162 | + while date <= endDate: |
163 | + print("\nProcessing %s " % date.strftime('%Y-%m-%d')) |
164 | + timestamp = generateTimestamp(date) |
165 | + (dayResults, dayContributors) = getStats(milestones, products, date) |
166 | + |
167 | + #add / check for result |
168 | + dbResult, newResult = CommunityResult.objects.get_or_create(value = dayResults, ran_at = date, name = domain, release = dist) |
169 | + |
170 | + #add / check for person |
171 | + for person in dayContributors: |
172 | + dbContrib, newContrib = CommunityContributor.objects.get_or_create(launchpad_id = person, result = dbResult) |
173 | + |
174 | + #iterate to tomorrow |
175 | + date += datetime.timedelta(days=1) |
176 | + |
177 | +def main(domain, startDate, endDate, dist): |
178 | + #connect to tracker |
179 | + (milestones, products) = getInstance(domain, dist) |
180 | + |
181 | + #remove whitespace for filenaming |
182 | + dist = dist.replace(" ", "") |
183 | + |
184 | + #process |
185 | + processTracker(dist, domain, milestones, products, startDate, endDate) |
186 | + |
187 | + |
188 | +class Command(BaseCommand): |
189 | + #help = "Gather bootspeed results from jenkins.qa.ubuntu.com" |
190 | + #args = "[job name] [job name]..." |
191 | + |
192 | + option_list = BaseCommand.option_list + ( |
193 | + make_option( |
194 | + '-d', '--domain', |
195 | + dest='domain', |
196 | + help='Domain', |
197 | + type=str, |
198 | + ), |
199 | + make_option( |
200 | + '-D', '--days', |
201 | + dest='days', |
202 | + help='days', |
203 | + default=1, |
204 | + type=int, |
205 | + ), |
206 | + make_option( |
207 | + '--dist', |
208 | + dest='dist', |
209 | + help='dist', |
210 | + type=str, |
211 | + ), |
212 | + make_option( |
213 | + '--date', |
214 | + dest='date', |
215 | + help='date', |
216 | + type=str, |
217 | + ), |
218 | + ) |
219 | + def handle(self, *args, **options): |
220 | + domain = options.get('domain') |
221 | + days = int(options.get('days')) |
222 | + dist = options.get('dist') |
223 | + date = options.get('date') |
224 | + (startDate, endDate) = init(date, days) |
225 | + main(domain, startDate, endDate, dist) |
226 | + #for x in domain: |
227 | + # main(x, startDate, endDate, dist) |
228 | + |
229 | |
230 | === added file 'contribtrack/management/commands/qatracker.py' |
231 | --- contribtrack/management/commands/qatracker.py 1970-01-01 00:00:00 +0000 |
232 | +++ contribtrack/management/commands/qatracker.py 2013-05-06 01:14:24 +0000 |
233 | @@ -0,0 +1,492 @@ |
234 | +#!/usr/bin/python3 |
235 | +# -*- coding: utf-8 -*- |
236 | + |
237 | +# Copyright (C) 2011, 2012 Canonical Ltd. |
238 | +# Author: Stéphane Graber <stgraber@ubuntu.com> |
239 | + |
240 | +# This library is free software; you can redistribute it and/or |
241 | +# modify it under the terms of the GNU Lesser General Public |
242 | +# License as published by the Free Software Foundation; either |
243 | +# version 2.1 of the License, or (at your option) any later version. |
244 | + |
245 | +# This library is distributed in the hope that it will be useful, |
246 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
247 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
248 | +# Lesser General Public License for more details. |
249 | + |
250 | +# You should have received a copy of the GNU Lesser General Public |
251 | +# License along with this library; if not, write to the Free Software |
252 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
253 | +# USA |
254 | + |
255 | +try: |
256 | + import xmlrpc.client as xmlrpclib |
257 | +except ImportError: |
258 | + import xmlrpclib |
259 | + |
260 | +import base64 |
261 | +from datetime import datetime |
262 | + |
263 | +# Taken from qatracker/qatracker.modules (PHP code) |
264 | +# cat qatracker.module | grep " = array" | sed -e 's/^\$//g' \ |
265 | +# -e 's/array(/[/g' -e 's/);/]/g' -e "s/t('/\"/g" -e "s/')/\"/g" |
266 | +### AUTO-GENERATED -> |
267 | +qatracker_build_milestone_status = ["Active", "Re-building", "Disabled", |
268 | + "Superseded", "Ready"] |
269 | +qatracker_milestone_notify = ["No", "Yes"] |
270 | +qatracker_milestone_autofill = ["No", "Yes"] |
271 | +qatracker_milestone_status = ["Testing", "Released", "Archived"] |
272 | +qatracker_milestone_series_status = ["Active", "Disabled"] |
273 | +qatracker_milestone_series_manifest_status = ["Active", "Disabled"] |
274 | +qatracker_product_status = ["Active", "Disabled"] |
275 | +qatracker_product_type = ["iso", "package", "hardware"] |
276 | +qatracker_product_download_type = ["HTTP", "RSYNC", "ZSYNC", |
277 | + "GPG signature", "MD5 checksum", "Comment", |
278 | + "Torrent"] |
279 | +qatracker_testsuite_testcase_status = ["Mandatory", "Disabled", "Run-once", |
280 | + "Optional"] |
281 | +qatracker_result_result = ["Failed", "Passed", "In progress"] |
282 | +qatracker_result_status = ["Active", "Disabled"] |
283 | +### <- AUTO-GENERATED |
284 | + |
285 | + |
286 | +class QATrackerRPCObject(): |
287 | + """Base class for objects received over XML-RPC""" |
288 | + |
289 | + CONVERT_BOOL = [] |
290 | + CONVERT_DATE = [] |
291 | + CONVERT_INT = [] |
292 | + |
293 | + def __init__(self, tracker, rpc_dict): |
294 | + # Convert the dict we get from the API into an object |
295 | + |
296 | + for key in rpc_dict: |
297 | + key = unicode(key) |
298 | + if key in self.CONVERT_INT: |
299 | + try: |
300 | + setattr(self, key, int(rpc_dict[key])) |
301 | + except ValueError: |
302 | + setattr(self, key, None) |
303 | + elif key in self.CONVERT_BOOL: |
304 | + setattr(self, key, rpc_dict[key] == "true") |
305 | + elif key in self.CONVERT_DATE: |
306 | + try: |
307 | + setattr(self, key, datetime.strptime(rpc_dict[key], |
308 | + '%Y-%m-%d %H:%M:%S')) |
309 | + except ValueError: |
310 | + setattr(self, key, None) |
311 | + else: |
312 | + setattr(self, key, unicode(rpc_dict[key])) |
313 | + |
314 | + self.tracker = tracker |
315 | + |
316 | + def __repr__(self): |
317 | + return "%s: %s" % (self.__class__.__name__, self.title) |
318 | + |
319 | + |
320 | +class QATrackerBug(QATrackerRPCObject): |
321 | + """A bug entry""" |
322 | + |
323 | + CONVERT_INT = ['bugnumber', 'count'] |
324 | + CONVERT_DATE = ['earliest_report', 'latest_report'] |
325 | + |
326 | + def __repr__(self): |
327 | + return "%s: %s" % (self.__class__.__name__, self.bugnumber) |
328 | + |
329 | + |
330 | +class QATrackerBuild(QATrackerRPCObject): |
331 | + """A build entry""" |
332 | + |
333 | + CONVERT_INT = ['id', 'productid', 'userid', 'status'] |
334 | + CONVERT_DATE = ['date'] |
335 | + |
336 | + def __repr__(self): |
337 | + return "%s: %s" % (self.__class__.__name__, self.id) |
338 | + |
339 | + def add_result(self, testcase, result, comment='', hardware='', bugs={}): |
340 | + """Add a result to the build""" |
341 | + |
342 | + if (self.tracker.access not in ("user", "admin") and |
343 | + self.tracker.access is not None): |
344 | + raise Exception("Access denied, you need 'user' but are '%s'" % |
345 | + self.tracker.access) |
346 | + |
347 | + build_testcase = None |
348 | + |
349 | + # FIXME: Supporting 'str' containing the testcase name would be nice |
350 | + if isinstance(testcase, QATrackerTestcase): |
351 | + build_testcase = testcase.id |
352 | + elif isinstance(testcase, int): |
353 | + build_testcase = testcase |
354 | + |
355 | + if not build_testcase: |
356 | + raise IndexError("Couldn't find testcase: %s" % (testcase,)) |
357 | + |
358 | + if isinstance(result, list): |
359 | + raise TypeError("result must be a string or an integer") |
360 | + |
361 | + build_result = self.tracker._get_valid_id_list(qatracker_result_result, |
362 | + result) |
363 | + |
364 | + if not isinstance(bugs, dict): |
365 | + raise TypeError("bugs must be a dict") |
366 | + |
367 | + for bug in bugs: |
368 | + if not isinstance(bug, int) or bug <= 0: |
369 | + raise ValueError("A bugnumber must be a number >= 0") |
370 | + |
371 | + if not isinstance(bugs[bug], int) or bugs[bug] not in (0, 1): |
372 | + raise ValueError("A bugimportance must be in (0,1)") |
373 | + |
374 | + resultid = int(self.tracker.tracker.results.add(self.id, |
375 | + build_testcase, |
376 | + build_result[0], |
377 | + str(comment), |
378 | + str(hardware), |
379 | + bugs)) |
380 | + if resultid == -1: |
381 | + raise Exception("Couldn't post your result.") |
382 | + |
383 | + new_result = None |
384 | + for entry in self.get_results(build_testcase, 0): |
385 | + if entry.id == resultid: |
386 | + new_result = entry |
387 | + break |
388 | + |
389 | + return new_result |
390 | + |
391 | + def get_results(self, testcase, status=qatracker_result_status): |
392 | + """Get a list of results for the given build and testcase""" |
393 | + |
394 | + build_testcase = None |
395 | + |
396 | + # FIXME: Supporting 'str' containing the testcase name would be nice |
397 | + if isinstance(testcase, QATrackerTestcase): |
398 | + build_testcase = testcase.id |
399 | + elif isinstance(testcase, int): |
400 | + build_testcase = testcase |
401 | + |
402 | + if not build_testcase: |
403 | + raise IndexError("Couldn't find testcase: %s" % (testcase,)) |
404 | + |
405 | + record_filter = self.tracker._get_valid_id_list( |
406 | + qatracker_result_status, |
407 | + status) |
408 | + |
409 | + if len(record_filter) == 0: |
410 | + return [] |
411 | + |
412 | + results = [] |
413 | + for entry in self.tracker.tracker.results.get_list( |
414 | + self.id, build_testcase, list(record_filter)): |
415 | + results.append(QATrackerResult(self.tracker, entry)) |
416 | + |
417 | + return results |
418 | + |
419 | + |
420 | +class QATrackerMilestone(QATrackerRPCObject): |
421 | + """A milestone entry""" |
422 | + |
423 | + CONVERT_INT = ['id', 'status', 'series'] |
424 | + CONVERT_BOOL = ['notify'] |
425 | + |
426 | + def get_bugs(self): |
427 | + """Returns a list of all bugs linked to this milestone""" |
428 | + |
429 | + bugs = [] |
430 | + for entry in self.tracker.tracker.bugs.get_list(self.id): |
431 | + bugs.append(QATrackerBug(self.tracker, entry)) |
432 | + |
433 | + return bugs |
434 | + |
435 | + def add_build(self, product, version, note="", notify=True): |
436 | + """Add a build to the milestone""" |
437 | + |
438 | + if self.status != 0: |
439 | + raise TypeError("Only active milestones are accepted") |
440 | + |
441 | + if self.tracker.access != "admin" and self.tracker.access is not None: |
442 | + raise Exception("Access denied, you need 'admin' but are '%s'" % |
443 | + self.tracker.access) |
444 | + |
445 | + if not isinstance(notify, bool): |
446 | + raise TypeError("notify must be a boolean") |
447 | + |
448 | + build_product = None |
449 | + |
450 | + if isinstance(product, QATrackerProduct): |
451 | + build_product = product |
452 | + else: |
453 | + valid_products = self.tracker.get_products(0) |
454 | + |
455 | + for entry in valid_products: |
456 | + if (entry.title.lower() == str(product).lower() or |
457 | + entry.id == product): |
458 | + build_product = entry |
459 | + break |
460 | + |
461 | + if not build_product: |
462 | + raise IndexError("Couldn't find product: %s" % product) |
463 | + |
464 | + if build_product.status != 0: |
465 | + raise TypeError("Only active products are accepted") |
466 | + |
467 | + self.tracker.tracker.builds.add(build_product.id, self.id, |
468 | + str(version), str(note), notify) |
469 | + |
470 | + new_build = None |
471 | + for entry in self.get_builds(0): |
472 | + if (entry.productid == build_product.id |
473 | + and entry.version == str(version)): |
474 | + new_build = entry |
475 | + break |
476 | + |
477 | + return new_build |
478 | + |
479 | + def get_builds(self, status=qatracker_build_milestone_status): |
480 | + """Get a list of builds for the milestone""" |
481 | + |
482 | + record_filter = self.tracker._get_valid_id_list( |
483 | + qatracker_build_milestone_status, status) |
484 | + |
485 | + if len(record_filter) == 0: |
486 | + return [] |
487 | + |
488 | + builds = [] |
489 | + for entry in self.tracker.tracker.builds.get_list(self.id, |
490 | + list(record_filter)): |
491 | + builds.append(QATrackerBuild(self.tracker, entry)) |
492 | + |
493 | + return builds |
494 | + |
495 | + |
496 | +class QATrackerProduct(QATrackerRPCObject): |
497 | + CONVERT_INT = ['id', 'type', 'status'] |
498 | + |
499 | + def get_testcases(self, series, |
500 | + status=qatracker_testsuite_testcase_status): |
501 | + """Get a list of testcases associated with the product""" |
502 | + |
503 | + record_filter = self.tracker._get_valid_id_list( |
504 | + qatracker_testsuite_testcase_status, status) |
505 | + |
506 | + if len(record_filter) == 0: |
507 | + return [] |
508 | + |
509 | + if isinstance(series, QATrackerMilestone): |
510 | + seriesid = series.series |
511 | + elif isinstance(series, int): |
512 | + seriesid = series |
513 | + else: |
514 | + raise TypeError("series needs to be a valid QATrackerMilestone" |
515 | + " instance or an integer") |
516 | + |
517 | + testcases = [] |
518 | + for entry in self.tracker.tracker.testcases.get_list( |
519 | + self.id, seriesid, list(record_filter)): |
520 | + testcases.append(QATrackerTestcase(self.tracker, entry)) |
521 | + |
522 | + return testcases |
523 | + |
524 | + |
525 | +class QATrackerResult(QATrackerRPCObject): |
526 | + CONVERT_INT = ['id', 'reporterid', 'revisionid', 'result', 'changedby', |
527 | + 'status'] |
528 | + CONVERT_DATE = ['date', 'lastchange'] |
529 | + __deleted = False |
530 | + |
531 | + def __repr__(self): |
532 | + return "%s: %s" % (self.__class__.__name__, self.id) |
533 | + |
534 | + def delete(self): |
535 | + """Remove the result from the tracker""" |
536 | + |
537 | + if (self.tracker.access not in ("user", "admin") and |
538 | + self.tracker.access is not None): |
539 | + raise Exception("Access denied, you need 'user' but are '%s'" % |
540 | + self.tracker.access) |
541 | + |
542 | + if self.__deleted: |
543 | + raise IndexError("Result has already been removed") |
544 | + |
545 | + retval = self.tracker.tracker.results.delete(self.id) |
546 | + if retval is not True: |
547 | + raise Exception("Failed to remove result") |
548 | + |
549 | + self.status = 1 |
550 | + self.__deleted = True |
551 | + |
552 | + def save(self): |
553 | + """Save any change that happened on this entry""" |
554 | + |
555 | + if (self.tracker.access not in ("user", "admin") and |
556 | + self.tracker.access is not None): |
557 | + raise Exception("Access denied, you need 'user' but are '%s'" % |
558 | + self.tracker.access) |
559 | + |
560 | + if self.__deleted: |
561 | + raise IndexError("Result no longer exists") |
562 | + |
563 | + retval = self.tracker.tracker.results.update(self.id, self.result, |
564 | + self.comment, |
565 | + self.hardware, |
566 | + self.bugs) |
567 | + if retval is not True: |
568 | + raise Exception("Failed to update result") |
569 | + |
570 | + |
571 | +class QATrackerSeries(QATrackerRPCObject): |
572 | + CONVERT_INT = ['id', 'status'] |
573 | + |
574 | + def get_manifest(self, status=qatracker_milestone_series_manifest_status): |
575 | + """Get a list of products in the series' manifest""" |
576 | + |
577 | + record_filter = self.tracker._get_valid_id_list( |
578 | + qatracker_milestone_series_manifest_status, status) |
579 | + |
580 | + if len(record_filter) == 0: |
581 | + return [] |
582 | + |
583 | + manifest_entries = [] |
584 | + for entry in self.tracker.tracker.series.get_manifest( |
585 | + self.id, list(record_filter)): |
586 | + manifest_entries.append(QATrackerSeriesManifest( |
587 | + self.tracker, entry)) |
588 | + |
589 | + return manifest_entries |
590 | + |
591 | + |
592 | +class QATrackerSeriesManifest(QATrackerRPCObject): |
593 | + CONVERT_INT = ['id', 'productid', 'status'] |
594 | + |
595 | + def __repr__(self): |
596 | + return "%s: %s" % (self.__class__.__name__, self.product_title) |
597 | + |
598 | + |
599 | +class QATrackerTestcase(QATrackerRPCObject): |
600 | + CONVERT_INT = ['id', 'status', 'weight', 'suite'] |
601 | + |
602 | + |
603 | +class QATracker(): |
604 | + def __init__(self, url, username=None, password=None): |
605 | + class AuthTransport(xmlrpclib.Transport): |
606 | + def set_auth(self, auth): |
607 | + self.auth = auth |
608 | + |
609 | + def get_host_info(self, host): |
610 | + host, extra_headers, x509 = \ |
611 | + xmlrpclib.Transport.get_host_info(self, host) |
612 | + if extra_headers is None: |
613 | + extra_headers = [] |
614 | + extra_headers.append(('Authorization', 'Basic %s' % auth)) |
615 | + return host, extra_headers, x509 |
616 | + |
617 | + if username and password: |
618 | + try: |
619 | + auth = str(base64.b64encode( |
620 | + bytes('%s:%s' % (username, password), 'utf-8')), |
621 | + 'utf-8') |
622 | + except TypeError: |
623 | + auth = base64.b64encode('%s:%s' % (username, password)) |
624 | + |
625 | + transport = AuthTransport() |
626 | + transport.set_auth(auth) |
627 | + drupal = xmlrpclib.ServerProxy(url, transport=transport) |
628 | + else: |
629 | + drupal = xmlrpclib.ServerProxy(url) |
630 | + |
631 | + # Call listMethods() so if something is wrong we know it immediately |
632 | + drupal.system.listMethods() |
633 | + |
634 | + # Get our current access |
635 | + self.access = drupal.qatracker.get_access() |
636 | + |
637 | + self.tracker = drupal.qatracker |
638 | + |
639 | + def _get_valid_id_list(self, status_list, status): |
640 | + """ Get a list of valid keys and a list or just a single |
641 | + entry of input to check against the list of valid keys. |
642 | + The function looks for valid indexes and content, doing |
643 | + case insensitive checking for strings and returns a list |
644 | + of indexes for the list of valid keys. """ |
645 | + |
646 | + def process(status_list, status): |
647 | + valid_status = [entry.lower() for entry in status_list] |
648 | + |
649 | + if isinstance(status, int): |
650 | + if status < 0 or status >= len(valid_status): |
651 | + raise IndexError("Invalid status: %s" % status) |
652 | + return int(status) |
653 | + |
654 | + if isinstance(status, str): |
655 | + status = status.lower() |
656 | + if status not in valid_status: |
657 | + raise IndexError("Invalid status: %s" % status) |
658 | + return valid_status.index(status) |
659 | + |
660 | + raise TypeError("Invalid status type: %s (expected str or int)" % |
661 | + type(status)) |
662 | + |
663 | + record_filter = set() |
664 | + |
665 | + if isinstance(status, list): |
666 | + for entry in status: |
667 | + record_filter.add(process(status_list, entry)) |
668 | + else: |
669 | + record_filter.add(process(status_list, status)) |
670 | + |
671 | + return list(record_filter) |
672 | + |
673 | + def get_bugs(self): |
674 | + """Get a list of all bugs reported on the site""" |
675 | + |
676 | + bugs = [] |
677 | + for entry in self.tracker.bugs.get_list(0): |
678 | + bugs.append(QATrackerBug(self, entry)) |
679 | + |
680 | + return bugs |
681 | + |
682 | + def get_milestones(self, status=qatracker_milestone_status): |
683 | + """Get a list of all milestones""" |
684 | + |
685 | + record_filter = self._get_valid_id_list(qatracker_milestone_status, |
686 | + status) |
687 | + |
688 | + if len(record_filter) == 0: |
689 | + return [] |
690 | + |
691 | + milestones = [] |
692 | + for entry in self.tracker.milestones.get_list(list(record_filter)): |
693 | + milestones.append(QATrackerMilestone(self, entry)) |
694 | + |
695 | + return milestones |
696 | + |
697 | + def get_products(self, status=qatracker_product_status): |
698 | + """Get a list of all products""" |
699 | + |
700 | + record_filter = self._get_valid_id_list(qatracker_product_status, |
701 | + status) |
702 | + |
703 | + if len(record_filter) == 0: |
704 | + return [] |
705 | + |
706 | + products = [] |
707 | + for entry in self.tracker.products.get_list(list(record_filter)): |
708 | + products.append(QATrackerProduct(self, entry)) |
709 | + |
710 | + return products |
711 | + |
712 | + def get_series(self, status=qatracker_milestone_series_status): |
713 | + """Get a list of all series""" |
714 | + |
715 | + record_filter = self._get_valid_id_list( |
716 | + qatracker_milestone_series_status, status) |
717 | + |
718 | + if len(record_filter) == 0: |
719 | + return [] |
720 | + |
721 | + series = [] |
722 | + for entry in self.tracker.series.get_list(list(record_filter)): |
723 | + series.append(QATrackerSeries(self, entry)) |
724 | + |
725 | + return series |
726 | |
727 | === added directory 'contribtrack/migrations' |
728 | === added file 'contribtrack/migrations/0001_initial.py' |
729 | --- contribtrack/migrations/0001_initial.py 1970-01-01 00:00:00 +0000 |
730 | +++ contribtrack/migrations/0001_initial.py 2013-05-06 01:14:24 +0000 |
731 | @@ -0,0 +1,65 @@ |
732 | +# -*- coding: utf-8 -*- |
733 | +import datetime |
734 | +from south.db import db |
735 | +from south.v2 import SchemaMigration |
736 | +from django.db import models |
737 | + |
738 | + |
739 | +class Migration(SchemaMigration): |
740 | + |
741 | + def forwards(self, orm): |
742 | + # Adding model 'CommunityResult' |
743 | + db.create_table('community_results', ( |
744 | + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
745 | + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), |
746 | + ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), |
747 | + ('internal', self.gf('django.db.models.fields.BooleanField')(default=True)), |
748 | + ('publish', self.gf('django.db.models.fields.BooleanField')(default=True)), |
749 | + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), |
750 | + ('value', self.gf('django.db.models.fields.FloatField')()), |
751 | + ('ran_at', self.gf('django.db.models.fields.DateTimeField')()), |
752 | + ('jenkins_url', self.gf('django.db.models.fields.URLField')(max_length=200, null=True)), |
753 | + ('release', self.gf('django.db.models.fields.CharField')(max_length=4096)), |
754 | + )) |
755 | + db.send_create_signal('contribtrack', ['CommunityResult']) |
756 | + |
757 | + # Adding model 'CommunityContributor' |
758 | + db.create_table('community_contributors', ( |
759 | + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
760 | + ('launchpad_id', self.gf('django.db.models.fields.CharField')(max_length=4096)), |
761 | + ('result', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contribtrack.CommunityResult'])), |
762 | + )) |
763 | + db.send_create_signal('contribtrack', ['CommunityContributor']) |
764 | + |
765 | + |
766 | + def backwards(self, orm): |
767 | + # Deleting model 'CommunityResult' |
768 | + db.delete_table('community_results') |
769 | + |
770 | + # Deleting model 'CommunityContributor' |
771 | + db.delete_table('community_contributors') |
772 | + |
773 | + |
774 | + models = { |
775 | + 'contribtrack.communitycontributor': { |
776 | + 'Meta': {'object_name': 'CommunityContributor', 'db_table': "'community_contributors'"}, |
777 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
778 | + 'launchpad_id': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
779 | + 'result': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contribtrack.CommunityResult']"}) |
780 | + }, |
781 | + 'contribtrack.communityresult': { |
782 | + 'Meta': {'object_name': 'CommunityResult', 'db_table': "'community_results'"}, |
783 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
784 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
785 | + 'internal': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
786 | + 'jenkins_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), |
787 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), |
788 | + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
789 | + 'ran_at': ('django.db.models.fields.DateTimeField', [], {}), |
790 | + 'release': ('django.db.models.fields.CharField', [], {'max_length': '4096'}), |
791 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
792 | + 'value': ('django.db.models.fields.FloatField', [], {}) |
793 | + } |
794 | + } |
795 | + |
796 | + complete_apps = ['contribtrack'] |
797 | \ No newline at end of file |
798 | |
799 | === added file 'contribtrack/migrations/__init__.py' |
800 | === added file 'contribtrack/models.py' |
801 | --- contribtrack/models.py 1970-01-01 00:00:00 +0000 |
802 | +++ contribtrack/models.py 2013-05-06 01:14:24 +0000 |
803 | @@ -0,0 +1,13 @@ |
804 | +from django.db import models |
805 | +from performance.models import ResultBase |
806 | + |
807 | +class CommunityResult(ResultBase): |
808 | + class Meta: |
809 | + db_table = "community_results" |
810 | + release = models.CharField(max_length=4096) |
811 | + |
812 | +class CommunityContributor(models.Model): |
813 | + class Meta: |
814 | + db_table = "community_contributors" |
815 | + launchpad_id = models.CharField(max_length=4096) |
816 | + result = models.ForeignKey(CommunityResult) |
817 | |
818 | === added file 'contribtrack/tests.py' |
819 | --- contribtrack/tests.py 1970-01-01 00:00:00 +0000 |
820 | +++ contribtrack/tests.py 2013-05-06 01:14:24 +0000 |
821 | @@ -0,0 +1,16 @@ |
822 | +""" |
823 | +This file demonstrates writing tests using the unittest module. These will pass |
824 | +when you run "manage.py test". |
825 | + |
826 | +Replace this with more appropriate tests for your application. |
827 | +""" |
828 | + |
829 | +from django.test import TestCase |
830 | + |
831 | + |
832 | +class SimpleTest(TestCase): |
833 | + def test_basic_addition(self): |
834 | + """ |
835 | + Tests that 1 + 1 always equals 2. |
836 | + """ |
837 | + self.assertEqual(1 + 1, 2) |
838 | |
839 | === added file 'contribtrack/views.py' |
840 | --- contribtrack/views.py 1970-01-01 00:00:00 +0000 |
841 | +++ contribtrack/views.py 2013-05-06 01:14:24 +0000 |
842 | @@ -0,0 +1,1 @@ |
843 | +# Create your views here. |
844 | |
845 | === modified file 'qa_dashboard/settings.py' |
846 | --- qa_dashboard/settings.py 2013-04-18 19:10:24 +0000 |
847 | +++ qa_dashboard/settings.py 2013-05-06 01:14:24 +0000 |
848 | @@ -149,6 +149,7 @@ |
849 | 'smoke', |
850 | 'sru', |
851 | 'idle_power', |
852 | + 'contribtrack', |
853 | ) |
854 | |
855 | INSTALLED_APPS = ( |
856 | |
857 | === modified file 'setup.sh' |
858 | --- setup.sh 2013-05-04 00:08:00 +0000 |
859 | +++ setup.sh 2013-05-06 01:14:24 +0000 |
860 | @@ -1,4 +1,4 @@ |
861 | -VAGRANT_SETUP=/home/vagrant/vagrant-setup |
862 | +VAGRANT_SETUP=~/vagrant |
863 | |
864 | echo "Does the vagrant-setup directory exist?" |
865 | if [ ! -f $VAGRANT_SETUP/_dir ]; then |