Merge lp:~nskaggs/qa-dashboard/contribtrack into lp:qa-dashboard

Proposed by Chris Johnston on 2013-05-06
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
Reviewer Review Type Date Requested Status
QA Dashboard Developers 2013-05-06 Pending
Review via email: mp+162552@code.launchpad.net
To post a comment you must log in.

Unmerged revisions

381. By Nicholas Skaggs on 2013-05-03

removed summing of values, instead store a daily value everyday

380. By Nicholas Skaggs on 2013-05-02

fix script to write data properly

379. By Nicholas Skaggs on 2013-05-02

first working version of contribtrack with qatracker pull

378. By Nicholas Skaggs on 2013-05-01

added contributors to model

377. By Nicholas Skaggs on 2013-04-30

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

Subscribers

People subscribed via source and target branches