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

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
Reviewer Review Type Date Requested Status
QA Dashboard Developers Pending
Review via email: mp+162552@code.launchpad.net
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

Subscribers

People subscribed via source and target branches