From 40e25d04a8e70f6d0ba894e8a0ae61fd5fbb53e4 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 8 Jul 2011 19:40:46 +0200 Subject: [PATCH 001/429] Added tastypie app to example project and codespeed - Tastypie is a Django app to implement a RESTful API - Docu at http://django-tastypie.readthedocs.org/en/latest/tutorial.html --- example/requirements.txt | 1 + example/settings.py | 3 ++- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example/requirements.txt b/example/requirements.txt index 34cd48c0..006825a7 100644 --- a/example/requirements.txt +++ b/example/requirements.txt @@ -1,3 +1,4 @@ Django>=1.3 South>=0.7.3 +django-tastypie>=0.9.9 #-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar diff --git a/example/settings.py b/example/settings.py index 510beaed..9ba126fb 100644 --- a/example/settings.py +++ b/example/settings.py @@ -110,7 +110,8 @@ def process_exception(self, request, exception): 'django.contrib.admin', 'django.contrib.staticfiles', 'codespeed', - 'south' + 'south', + 'tastypie', ) STATIC_URL = '/static/' diff --git a/setup.py b/setup.py index 5a6a5f18..b292a630 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version='0.9.0', url='https://github.com/tobami/codespeed', license='GNU Lesser General Public License version 2.1', - install_requires=['django>=1.3', 'isodate', 'south'], + install_requires=['django>=1.3', 'isodate', 'south', 'django-tastypie'], packages=find_packages(exclude=['ez_setup', 'example']), description='A web application to monitor and analyze the performance of your code', include_package_data=True, From f013cbad481acccf9d64fb0d9ab317f282dc4242 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 8 Jul 2011 19:45:25 +0200 Subject: [PATCH 002/429] mv codespeed unittests to codespeed/tests/ and make it a module - Started to write REST API tests in additional tests file - Thus, mv the tests files in tests/ they get imported in __init__.py and found by the Django test runner --- codespeed/tests/__init__.py | 5 + codespeed/tests/tests.py | 373 +++++++++++++++++++++++++++++++++++ codespeed/tests/tests_api.py | 95 +++++++++ 3 files changed, 473 insertions(+) create mode 100644 codespeed/tests/__init__.py create mode 100644 codespeed/tests/tests.py create mode 100644 codespeed/tests/tests_api.py diff --git a/codespeed/tests/__init__.py b/codespeed/tests/__init__.py new file mode 100644 index 00000000..35e5445f --- /dev/null +++ b/codespeed/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from tests import * +from tests_api import * + diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py new file mode 100644 index 00000000..7ac73adb --- /dev/null +++ b/codespeed/tests/tests.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from time import sleep +import copy, json +import unittest + +from django.test import TestCase +from django.test.client import Client +from django.core.urlresolvers import reverse +from django.conf import settings +from codespeed.models import (Project, Benchmark, Revision, Branch, + Executable, Environment, Result, Report) +from codespeed import settings as default_settings +from codespeed import tests_api + + +class AddResultTest(TestCase): + + def setUp(self): + self.path = reverse('codespeed.views.add_result') + self.client = Client() + self.e = Environment(name='Dual Core', cpu='Core 2 Duo 8200') + self.e.save() + temp = datetime.today() + self.cdate = datetime( + temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) + self.data = { + 'commitid': '23', + 'branch': 'default', + 'project': 'MyProject', + 'executable': 'myexe O3 64bits', + 'benchmark': 'float', + 'environment': 'Dual Core', + 'result_value': 456, + } + + def test_add_correct_result(self): + """Add correct result data""" + response = self.client.post(self.path, self.data) + + # Check that we get a success response + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "Result data saved succesfully") + + # Check that the data was correctly saved + e = Environment.objects.get(name='Dual Core') + b = Benchmark.objects.get(name='float') + self.assertEquals(b.benchmark_type, "C") + self.assertEquals(b.units, "seconds") + self.assertEquals(b.lessisbetter, True) + p = Project.objects.get(name='MyProject') + branch = Branch.objects.get(name='default', project=p) + r = Revision.objects.get(commitid='23', branch=branch) + i = Executable.objects.get(name='myexe O3 64bits') + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 456) + + def test_add_non_default_result(self): + """Add result data with non-mandatory options""" + modified_data = copy.deepcopy(self.data) + modified_data['result_date'] = self.cdate + modified_data['revision_date'] = self.cdate + sleep(1.5)#ensure current date changes + modified_data['std_dev'] = 1.11111 + modified_data['max'] = 2 + modified_data['min'] = 1.0 + response = self.client.post(self.path, modified_data) + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "Result data saved succesfully") + e = Environment.objects.get(name='Dual Core') + p = Project.objects.get(name='MyProject') + branch = Branch.objects.get(name='default', project=p) + r = Revision.objects.get(commitid='23', branch=branch) + + # Tweak the resolution down to avoid failing over very slight differences: + self.assertEquals( + r.date.replace(microsecond=0), self.cdate.replace(microsecond=0)) + + i = Executable.objects.get(name='myexe O3 64bits') + b = Benchmark.objects.get(name='float') + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertEquals(res.std_dev, 1.11111) + self.assertEquals(res.val_max, 2) + self.assertEquals(res.val_min, 1) + + def test_bad_environment(self): + """Should return 400 when environment does not exist""" + bad_name = '10 Core' + self.data['environment'] = bad_name + response = self.client.post(self.path, self.data) + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, "Environment " + bad_name + " not found") + self.data['environment'] = 'Dual Core' + + def test_empty_argument(self): + """Should respond 400 when a POST request has an empty argument""" + for key in self.data: + backup = self.data[key] + self.data[key] = "" + response = self.client.post(self.path, self.data) + self.assertEquals(response.status_code, 400) + self.assertEquals( + response.content, 'Value for key "' + key + '" empty in request') + self.data[key] = backup + + def test_missing_argument(self): + """Should respond 400 when a POST request is missing an argument""" + for key in self.data: + backup = self.data[key] + del(self.data[key]) + response = self.client.post(self.path, self.data) + self.assertEquals(response.status_code, 400) + self.assertEquals( + response.content, 'Key "' + key + '" missing from request') + self.data[key] = backup + + def test_report_is_not_created(self): + '''Should not create a report when adding a single result''' + response = self.client.post(self.path, self.data) + number_of_reports = len(Report.objects.all()) + # After adding one result for one revision, there should be no reports + self.assertEquals(number_of_reports, 0) + + def test_report_is_created(self): + """Should create a report when adding a result for two revisions""" + response = self.client.post(self.path, self.data) + + modified_data = copy.deepcopy(self.data) + modified_data['commitid'] = "23233" + response = self.client.post(self.path, modified_data) + number_of_reports = len(Report.objects.all()) + # After adding a result for a second revision, a report should be created + self.assertEquals(number_of_reports, 1) + + def test_submit_data_with_none_timestamp(self): + """Should add a default revision date when timestamp is None""" + modified_data = copy.deepcopy(self.data) + # The value None will get urlencoded and converted to a "None" string + modified_data['revision_date'] = None + response = self.client.post(self.path, modified_data) + self.assertEquals(response.status_code, 202) + +class AddJSONResultsTest(TestCase): + def setUp(self): + self.path = reverse('codespeed.views.add_json_results') + self.client = Client() + self.e = Environment(name='bigdog', cpu='Core 2 Duo 8200') + self.e.save() + temp = datetime.today() + self.cdate = datetime( + temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) + + self.data = [ + {'commitid': '123', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 456,}, + {'commitid': '456', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 457,}, + {'commitid': '456', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards2', + 'environment': 'bigdog', + 'result_value': 34,}, + {'commitid': '789', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 458,}, + ] + + def test_add_correct_results(self): + """Should add all results when the request data is valid""" + response = self.client.post(self.path, {'json' : json.dumps(self.data)}) + + # Check that we get a success response + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "All result data saved successfully") + + # Check that the data was correctly saved + e = Environment.objects.get(name='bigdog') + b = Benchmark.objects.get(name='Richards') + self.assertEquals(b.benchmark_type, "C") + self.assertEquals(b.units, "seconds") + self.assertEquals(b.lessisbetter, True) + p = Project.objects.get(name='pypy') + branch = Branch.objects.get(name='default', project=p) + r = Revision.objects.get(commitid='123', branch=branch) + i = Executable.objects.get(name='pypy-c') + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 456) + resdate = res.date.strftime("%Y%m%dT%H%M%S") + selfdate = self.cdate.strftime("%Y%m%dT%H%M%S") + self.assertTrue(resdate, selfdate) + + r = Revision.objects.get(commitid='456', branch=branch) + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 457) + + r = Revision.objects.get(commitid='789', branch=branch) + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 458) + + def test_bad_environment(self): + """Add result associated with non-existing environment. + Only change one item in the list. + """ + data = self.data[0] + bad_name = 'bigdog1' + data['environment'] = bad_name + response = self.client.post(self.path, {'json' : json.dumps(self.data)}) + + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, "Environment " + bad_name + " not found") + data['environment'] = 'bigdog' + + def test_empty_argument(self): + '''Should return 400 when making a request with an empty argument''' + data = self.data[1] + for key in data: + backup = data[key] + data[key] = "" + response = self.client.post(self.path, + {'json' : json.dumps(self.data)}) + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, 'Value for key "' + key + '" empty in request') + data[key] = backup + + def test_missing_argument(self): + '''Should return 400 when making a request with a missing argument''' + data = self.data[2] + for key in data: + backup = data[key] + del(data[key]) + response = self.client.post(self.path, + {'json' : json.dumps(self.data)}) + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, 'Key "' + key + '" missing from request') + data[key] = backup + + def test_report_is_created(self): + '''Should create a report when adding json results for two revisions + plus a third revision with one result less than the last one''' + response = self.client.post(self.path, {'json' : json.dumps(self.data)}) + + # Check that we get a success response + self.assertEquals(response.status_code, 202) + + number_of_reports = len(Report.objects.all()) + # After adding 4 result for 3 revisions, only 2 reports should be created + # The third revision will need an extra result for Richards2 in order + # to trigger report creation + self.assertEquals(number_of_reports, 1) + + +class Timeline(TestCase): + fixtures = ["gettimeline_unittest.json"] + + def setUp(self): + self.client = Client() + + def test_fixture(self): + """Test the loaded fixture data + """ + env = Environment.objects.filter(name="Dual Core") + self.assertEquals(len(env), 1) + benchmarks = Benchmark.objects.filter(name="float") + self.assertEquals(len(benchmarks), 1) + self.assertEquals(benchmarks[0].units, "seconds") + results = benchmarks[0].results.all() + self.assertEquals(len(results), 8) + + def test_gettimelinedata(self): + """Test that gettimelinedata returns correct timeline data + """ + path = reverse('codespeed.views.gettimelinedata') + data = { + "exe": "1,2", + "base": "2+4", + "ben": "float", + "env": "1", + "revs": 2 + } + response = self.client.get(path, data) + self.assertEquals(response.status_code, 200) + responsedata = json.loads(response.content) + self.assertEquals( + responsedata['error'], "None", "there should be no errors") + self.assertEquals( + len(responsedata['timelines']), 1, "there should be 1 benchmark") + self.assertEquals( + len(responsedata['timelines'][0]['branches']['default']), + 2, + "there should be 2 timelines") + self.assertEquals( + len(responsedata['timelines'][0]['branches']['default']['1']), + 2, + "There are 2 datapoints") + self.assertEquals( + responsedata['timelines'][0]['branches']['default']['1'][1], + [u'2011-04-13 17:04:22', 2000.0, 1.11111, u'2', u'default'], + "Wrong data returned: ") + + +class CodespeedSettings(TestCase): + """Test codespeed.settings + """ + + def setUp(self): + self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()] + + def test_website_name(self): + """See if WEBSITENAME is set + """ + self.assertTrue(default_settings.WEBSITE_NAME) + self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite', + "Change codespeed settings in project.settings") + + def test_keys_in_settings(self): + """Check that all settings attributes from codespeed.settings exist + in django.conf.settings + """ + for k in self.cs_setting_keys: + self.assertTrue(hasattr(settings, k)) + + def test_settings_attributes(self): + """Check if all settings from codespeed.settings equals + django.conf.settings + """ + for k in self.cs_setting_keys: + self.assertEqual(getattr(settings, k), getattr(default_settings, k)) + + +#def suite(): + # api = tests_api.suite() + #return unittest.TestSuite([api]) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py new file mode 100644 index 00000000..5c2d2ab0 --- /dev/null +++ b/codespeed/tests/tests_api.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +""" +Tests related to RESTful API +""" + +from datetime import datetime +import copy, json +import unittest + +from django import test +from django.test.client import Client +from django.core.urlresolvers import reverse +from django.conf import settings +from codespeed.models import (Project, Benchmark, Revision, Branch, + Executable, Environment, Result, Report) +from codespeed import settings as default_settings + + +class FixtureTestCase(test.TestCase): + fixtures = ["gettimeline_unittest.json"] + + +class EnvironmentTest(FixtureTestCase): + """Test Environment() API + """ + + def setUp(self): + self.env1_data = dict( + name="env1", + cpu="cpu1", + memory="48kB", + os="ZX Spectrum OS", + kernel="2.6.32" + ) + self.env1 = Environment(**self.env1_data) + self.env1.save() + self.env2_data = dict( + name="env2", + cpu="z80", + memory="64kB", + os="ZX Spectrum OS", + kernel="2.6.32" + ) + self.client = Client() + super(FixtureTestCase, self).setUp() + + def test_dual_core(self): + response = self.client.get('/api/v1/environment/1/') + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['name'], "Dual Core") + + def test_env1(self): + response = self.client.get('/api/v1/environment/%s/' % (self.env1.id,)) + self.assertEquals(response.status_code, 200) + for k in self.env1_data.keys(): + self.assertEqual( + json.loads(response.content)[k], getattr(self.env1, k)) + + def test_env2_post(self): + response = self.client.post('/api/v1/environment/', + data=json.dumps(self.env2_data), + content_type='application/json') + self.assertEquals(response.status_code, 201) + response = self.client.get('/api/v1/environment/3/') + for k, v in self.env2_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_env2_put(self): + modified_data = copy.deepcopy(self.env2_data) + modified_data['name'] = "env2.2" + modified_data['memory'] = "128kB" + response = self.client.put('/api/v1/environment/3/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 201) + response = self.client.get('/api/v1/environment/3/') + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_env2_delete(self): + response = self.client.delete('/api/v1/environment/3/', + content_type='application/json') + self.assertEquals(response.status_code, 410) + + response = self.client.get('/api/v1/environment/3/') + self.assertEquals(response.status_code, 410) + +#def suite(): +# suite = unittest.TestSuite() +# suite.addTest(EnvironmentTest()) +# return suite + From 771d7abc2772ce00864bbda7e4455da97d3f9ae9 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 8 Jul 2011 19:49:40 +0200 Subject: [PATCH 003/429] Implemented simple API for Environment() - Let do tastypie the magic - Warning: no authorisation yet and no authentication --- codespeed/api.py | 37 ++++ codespeed/tests.py | 378 --------------------------------------- codespeed/tests/tests.py | 35 ++-- codespeed/urls.py | 5 + 4 files changed, 62 insertions(+), 393 deletions(-) create mode 100644 codespeed/api.py delete mode 100644 codespeed/tests.py diff --git a/codespeed/api.py b/codespeed/api.py new file mode 100644 index 00000000..dd057919 --- /dev/null +++ b/codespeed/api.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +"""RESTful API implementation + +Example: +GET Environment() data: + →curl -H "Accept: application/json" \ + http://127.0.0.1:8000/api/v1/environment/1/ + +POST Environment() data: + curl --dump-header - -H "Content-Type: application/json" -X POST \ + --data '{"name": "Single Core"}' \ + http://127.0.0.1:8000/api/v1/environment/ + +PUT Environment() data: + curl --dump-header - -H "Content-Type: application/json" -X PUT \ + --data '{"name": "Quad Core"}' \ + http://127.0.0.1:8000/api/v1/environment/2/ + +DELETE Environment() data: + →curl --dump-header - -H "Content-Type: application/json" -X DELETE \ + http://127.0.0.1:8000/api/v1/environment/2/ + +See http://django-tastypie.readthedocs.org/en/latest/interacting.html +""" + +from tastypie.resources import ModelResource +from tastypie.authorization import Authorization +from codespeed.models import Environment + + +class EnvironmentResource(ModelResource): + class Meta: + queryset = Environment.objects.all() + resource_name = 'environment' + authorization= Authorization() + diff --git a/codespeed/tests.py b/codespeed/tests.py deleted file mode 100644 index ee83f47b..00000000 --- a/codespeed/tests.py +++ /dev/null @@ -1,378 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import datetime, timedelta -from time import sleep -import copy, json - -from django.test import TestCase -from django.test.client import Client -from django.core.urlresolvers import reverse -from django.conf import settings -from codespeed.models import (Project, Benchmark, Revision, Branch, - Executable, Environment, Result, Report) -from codespeed import settings as default_settings - - -class AddResult(TestCase): - - def setUp(self): - self.path = reverse('codespeed.views.add_result') - self.client = Client() - self.e = Environment(name='Dual Core', cpu='Core 2 Duo 8200') - self.e.save() - temp = datetime.today() - self.cdate = datetime( - temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) - self.data = { - 'commitid': '23', - 'branch': 'default', - 'project': 'MyProject', - 'executable': 'myexe O3 64bits', - 'benchmark': 'float', - 'environment': 'Dual Core', - 'result_value': 456, - } - - def test_add_correct_result(self): - """Add correct result data""" - response = self.client.post(self.path, self.data) - - # Check that we get a success response - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") - - # Check that the data was correctly saved - e = Environment.objects.get(name='Dual Core') - b = Benchmark.objects.get(name='float') - self.assertEquals(b.benchmark_type, "C") - self.assertEquals(b.units, "seconds") - self.assertEquals(b.lessisbetter, True) - p = Project.objects.get(name='MyProject') - branch = Branch.objects.get(name='default', project=p) - r = Revision.objects.get(commitid='23', branch=branch) - i = Executable.objects.get(name='myexe O3 64bits') - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 456) - - def test_add_non_default_result(self): - """Add result data with non-mandatory options""" - modified_data = copy.deepcopy(self.data) - revision_date = self.cdate - timedelta(minutes=2) - modified_data['revision_date'] = revision_date - result_date = self.cdate + timedelta(minutes=2) - modified_data['result_date'] = result_date - modified_data['std_dev'] = 1.11111 - modified_data['max'] = 2 - modified_data['min'] = 1.0 - response = self.client.post(self.path, modified_data) - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") - e = Environment.objects.get(name='Dual Core') - p = Project.objects.get(name='MyProject') - branch = Branch.objects.get(name='default', project=p) - r = Revision.objects.get(commitid='23', branch=branch) - - # Tweak the resolution down to avoid failing over very slight differences: - self.assertEquals(r.date, revision_date) - - i = Executable.objects.get(name='myexe O3 64bits') - b = Benchmark.objects.get(name='float') - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertEquals(res.date, result_date) - self.assertEquals(res.std_dev, 1.11111) - self.assertEquals(res.val_max, 2) - self.assertEquals(res.val_min, 1) - - def test_bad_environment(self): - """Should return 400 when environment does not exist""" - bad_name = '10 Core' - self.data['environment'] = bad_name - response = self.client.post(self.path, self.data) - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, "Environment " + bad_name + " not found") - self.data['environment'] = 'Dual Core' - - def test_empty_argument(self): - """Should respond 400 when a POST request has an empty argument""" - for key in self.data: - backup = self.data[key] - self.data[key] = "" - response = self.client.post(self.path, self.data) - self.assertEquals(response.status_code, 400) - self.assertEquals( - response.content, 'Value for key "' + key + '" empty in request') - self.data[key] = backup - - def test_missing_argument(self): - """Should respond 400 when a POST request is missing an argument""" - for key in self.data: - backup = self.data[key] - del(self.data[key]) - response = self.client.post(self.path, self.data) - self.assertEquals(response.status_code, 400) - self.assertEquals( - response.content, 'Key "' + key + '" missing from request') - self.data[key] = backup - - def test_report_is_not_created(self): - '''Should not create a report when adding a single result''' - response = self.client.post(self.path, self.data) - number_of_reports = len(Report.objects.all()) - # After adding one result for one revision, there should be no reports - self.assertEquals(number_of_reports, 0) - - def test_report_is_created(self): - """Should create a report when adding a result for two revisions""" - response = self.client.post(self.path, self.data) - - modified_data = copy.deepcopy(self.data) - modified_data['commitid'] = "23233" - response = self.client.post(self.path, modified_data) - number_of_reports = len(Report.objects.all()) - # After adding a result for a second revision, a report should be created - self.assertEquals(number_of_reports, 1) - - def test_submit_data_with_none_timestamp(self): - """Should add a default revision date when timestamp is None""" - modified_data = copy.deepcopy(self.data) - # The value None will get urlencoded and converted to a "None" string - modified_data['revision_date'] = None - response = self.client.post(self.path, modified_data) - self.assertEquals(response.status_code, 202) - - def test_add_result_with_no_project(self): - """Should add a revision with the project""" - modified_data = copy.deepcopy(self.data) - modified_data['project'] = "My new project" - modified_data['executable'] = "My new executable" - response = self.client.post(self.path, modified_data) - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") - - -class AddJSONResults(TestCase): - def setUp(self): - self.path = reverse('codespeed.views.add_json_results') - self.client = Client() - self.e = Environment(name='bigdog', cpu='Core 2 Duo 8200') - self.e.save() - temp = datetime.today() - self.cdate = datetime( - temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) - - self.data = [ - {'commitid': '123', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 456,}, - {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 457,}, - {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards2', - 'environment': 'bigdog', - 'result_value': 34,}, - {'commitid': '789', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 458,}, - ] - - def test_add_correct_results(self): - """Should add all results when the request data is valid""" - response = self.client.post(self.path, {'json' : json.dumps(self.data)}) - - # Check that we get a success response - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "All result data saved successfully") - - # Check that the data was correctly saved - e = Environment.objects.get(name='bigdog') - b = Benchmark.objects.get(name='Richards') - self.assertEquals(b.benchmark_type, "C") - self.assertEquals(b.units, "seconds") - self.assertEquals(b.lessisbetter, True) - p = Project.objects.get(name='pypy') - branch = Branch.objects.get(name='default', project=p) - r = Revision.objects.get(commitid='123', branch=branch) - i = Executable.objects.get(name='pypy-c') - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 456) - resdate = res.date.strftime("%Y%m%dT%H%M%S") - selfdate = self.cdate.strftime("%Y%m%dT%H%M%S") - self.assertTrue(resdate, selfdate) - - r = Revision.objects.get(commitid='456', branch=branch) - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 457) - - r = Revision.objects.get(commitid='789', branch=branch) - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 458) - - def test_bad_environment(self): - """Add result associated with non-existing environment. - Only change one item in the list. - """ - data = self.data[0] - bad_name = 'bigdog1' - data['environment'] = bad_name - response = self.client.post(self.path, {'json' : json.dumps(self.data)}) - - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, "Environment " + bad_name + " not found") - data['environment'] = 'bigdog' - - def test_empty_argument(self): - '''Should return 400 when making a request with an empty argument''' - data = self.data[1] - for key in data: - backup = data[key] - data[key] = "" - response = self.client.post(self.path, - {'json' : json.dumps(self.data)}) - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, 'Value for key "' + key + '" empty in request') - data[key] = backup - - def test_missing_argument(self): - '''Should return 400 when making a request with a missing argument''' - data = self.data[2] - for key in data: - backup = data[key] - del(data[key]) - response = self.client.post(self.path, - {'json' : json.dumps(self.data)}) - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, 'Key "' + key + '" missing from request') - data[key] = backup - - def test_report_is_created(self): - '''Should create a report when adding json results for two revisions - plus a third revision with one result less than the last one''' - response = self.client.post(self.path, {'json' : json.dumps(self.data)}) - - # Check that we get a success response - self.assertEquals(response.status_code, 202) - - number_of_reports = len(Report.objects.all()) - # After adding 4 result for 3 revisions, only 2 reports should be created - # The third revision will need an extra result for Richards2 in order - # to trigger report creation - self.assertEquals(number_of_reports, 1) - - -class Timeline(TestCase): - fixtures = ["gettimeline_unittest.json"] - - def setUp(self): - self.client = Client() - - def test_fixture(self): - """Test the loaded fixture data - """ - env = Environment.objects.filter(name="Dual Core") - self.assertEquals(len(env), 1) - benchmarks = Benchmark.objects.filter(name="float") - self.assertEquals(len(benchmarks), 1) - self.assertEquals(benchmarks[0].units, "seconds") - results = benchmarks[0].results.all() - self.assertEquals(len(results), 8) - - def test_gettimelinedata(self): - """Test that gettimelinedata returns correct timeline data - """ - path = reverse('codespeed.views.gettimelinedata') - data = { - "exe": "1,2", - "base": "2+4", - "ben": "float", - "env": "1", - "revs": 2 - } - response = self.client.get(path, data) - self.assertEquals(response.status_code, 200) - responsedata = json.loads(response.content) - self.assertEquals( - responsedata['error'], "None", "there should be no errors") - self.assertEquals( - len(responsedata['timelines']), 1, "there should be 1 benchmark") - self.assertEquals( - len(responsedata['timelines'][0]['branches']['default']), - 2, - "there should be 2 timelines") - self.assertEquals( - len(responsedata['timelines'][0]['branches']['default']['1']), - 2, - "There are 2 datapoints") - self.assertEquals( - responsedata['timelines'][0]['branches']['default']['1'][1], - [u'2011-04-13 17:04:22', 2000.0, 1.11111, u'2', u'default'], - "Wrong data returned: ") - - -class CodespeedSettings(TestCase): - """Test codespeed.settings - """ - - def setUp(self): - self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()] - - def test_website_name(self): - """See if WEBSITENAME is set - """ - self.assertTrue(default_settings.WEBSITE_NAME) - self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite', - "Change codespeed settings in project.settings") - - def test_keys_in_settings(self): - """Check that all settings attributes from codespeed.settings exist - in django.conf.settings - """ - for k in self.cs_setting_keys: - self.assertTrue(hasattr(settings, k)) - - def test_settings_attributes(self): - """Check if all settings from codespeed.settings equals - django.conf.settings - """ - for k in self.cs_setting_keys: - self.assertEqual(getattr(settings, k), getattr(default_settings, k)) - diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index 7ac73adb..ee83f47b 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -from datetime import datetime +from datetime import datetime, timedelta from time import sleep import copy, json -import unittest from django.test import TestCase from django.test.client import Client @@ -11,10 +10,9 @@ from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) from codespeed import settings as default_settings -from codespeed import tests_api -class AddResultTest(TestCase): +class AddResult(TestCase): def setUp(self): self.path = reverse('codespeed.views.add_result') @@ -63,9 +61,10 @@ def test_add_correct_result(self): def test_add_non_default_result(self): """Add result data with non-mandatory options""" modified_data = copy.deepcopy(self.data) - modified_data['result_date'] = self.cdate - modified_data['revision_date'] = self.cdate - sleep(1.5)#ensure current date changes + revision_date = self.cdate - timedelta(minutes=2) + modified_data['revision_date'] = revision_date + result_date = self.cdate + timedelta(minutes=2) + modified_data['result_date'] = result_date modified_data['std_dev'] = 1.11111 modified_data['max'] = 2 modified_data['min'] = 1.0 @@ -78,8 +77,7 @@ def test_add_non_default_result(self): r = Revision.objects.get(commitid='23', branch=branch) # Tweak the resolution down to avoid failing over very slight differences: - self.assertEquals( - r.date.replace(microsecond=0), self.cdate.replace(microsecond=0)) + self.assertEquals(r.date, revision_date) i = Executable.objects.get(name='myexe O3 64bits') b = Benchmark.objects.get(name='float') @@ -89,6 +87,7 @@ def test_add_non_default_result(self): benchmark=b, environment=e ) + self.assertEquals(res.date, result_date) self.assertEquals(res.std_dev, 1.11111) self.assertEquals(res.val_max, 2) self.assertEquals(res.val_min, 1) @@ -150,7 +149,17 @@ def test_submit_data_with_none_timestamp(self): response = self.client.post(self.path, modified_data) self.assertEquals(response.status_code, 202) -class AddJSONResultsTest(TestCase): + def test_add_result_with_no_project(self): + """Should add a revision with the project""" + modified_data = copy.deepcopy(self.data) + modified_data['project'] = "My new project" + modified_data['executable'] = "My new executable" + response = self.client.post(self.path, modified_data) + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "Result data saved succesfully") + + +class AddJSONResults(TestCase): def setUp(self): self.path = reverse('codespeed.views.add_json_results') self.client = Client() @@ -219,7 +228,7 @@ def test_add_correct_results(self): resdate = res.date.strftime("%Y%m%dT%H%M%S") selfdate = self.cdate.strftime("%Y%m%dT%H%M%S") self.assertTrue(resdate, selfdate) - + r = Revision.objects.get(commitid='456', branch=branch) res = Result.objects.get( revision=r, @@ -367,7 +376,3 @@ def test_settings_attributes(self): for k in self.cs_setting_keys: self.assertEqual(getattr(settings, k), getattr(default_settings, k)) - -#def suite(): - # api = tests_api.suite() - #return unittest.TestSuite([api]) diff --git a/codespeed/urls.py b/codespeed/urls.py index 0c40ed2c..2c78f0ea 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -2,10 +2,14 @@ from django.conf.urls.defaults import * from django.core.urlresolvers import reverse from django.views.generic.simple import direct_to_template +from tastypie.api import Api from codespeed.feeds import LatestEntries +from codespeed.api import EnvironmentResource feeds = { 'latest': LatestEntries } +rest_api = Api(api_name='v1') +rest_api.register(EnvironmentResource()) urlpatterns = patterns('', (r'^$', direct_to_template, {'template': 'home.html'}), @@ -29,4 +33,5 @@ # URLs for adding results (r'^result/add/json/$', 'add_json_results'), (r'^result/add/$', 'add_result'), + (r'^api/', include(rest_api.urls)), ) From 99d3cb5b13fcb6855201546b3751233f95947dd4 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 8 Jul 2011 23:48:08 +0200 Subject: [PATCH 004/429] fixed tests imports --- codespeed/tests/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codespeed/tests/__init__.py b/codespeed/tests/__init__.py index 35e5445f..2c99e858 100644 --- a/codespeed/tests/__init__.py +++ b/codespeed/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from tests import * -from tests_api import * +from codespeed.tests.tests import * +from codespeed.tests.tests_api import * From acbeee853569165fa01f4f3d8a72845e5d6a1e1b Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 9 Jul 2011 00:15:23 +0200 Subject: [PATCH 005/429] Add tests from any file in codespeed/tests automatically - snippet from http://djangosnippets.org/snippets/1972/ integrated --- codespeed/tests/__init__.py | 78 ++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/codespeed/tests/__init__.py b/codespeed/tests/__init__.py index 2c99e858..aead8299 100644 --- a/codespeed/tests/__init__.py +++ b/codespeed/tests/__init__.py @@ -1,5 +1,79 @@ # -*- coding: utf-8 -*- -from codespeed.tests.tests import * -from codespeed.tests.tests_api import * +import os +import re +import unittest + +from django.utils.importlib import import_module +from django.test._doctest import DocTestSuite +from django.test.testcases import DocTestRunner +from django.test.simple import doctestOutputChecker + +loadTestsFromModule = unittest.defaultTestLoader.loadTestsFromModule + +DEFAULT_TESTFILE_PATTERN = re.compile(r'^[a-zA-Z0-9].*\.py') + +def get_suite(*names, **kwargs): + '''Creates (or updates) a ``TestSuite`` consisting of the tests under one or + more modules. + + See http://djangosnippets.org/snippets/1972/ + + This is useful for splitting a Django ``tests.py`` module into multiple test + modules under a ``tests`` directory without having to import them manually + in ``tests/__init__.py``. All you have to write in ``tests/__init__.py`` is:: + + from ... import get_suite + + # django.test looks for a function named suite that returns a TestSuite + suite = lambda: get_suite(__name__) + + This creates a suite consisting of all ``TestCase`` instances defined under + any ``test_.*py`` module under ``tests``. + + :param names: One or more module or package names to be added in the suite. + For module names, the respective module's TestCases are loaded. For package + names, the TestCases of all modules in the package directory satisfying + ``is_test_module`` are loaded. + :keyword is_test_module: Determines whether a file is a test module. It can + be a callable ``f(filename)`` or a regular expression (string or compiled) + that test module file names should ``match()``. + :keyword suite: If given, update this suite instead of creating a new one. + ''' + + suite = kwargs.get('suite') or unittest.TestSuite() + # determine is_test_module + is_test_module = kwargs.get('is_test_module', DEFAULT_TESTFILE_PATTERN) + if isinstance(is_test_module, basestring): # look for exact match + is_test_module = re.compile(is_test_module + '$').match + elif hasattr(is_test_module, 'match'): + is_test_module = is_test_module.match + # determine the test modules to be added in the suite and import them + modules = set() + for name in names: + package = import_module(name) + # if it's a single module just add it + if os.path.splitext(os.path.basename(package.__file__))[0] != '__init__': + modules.add(package) + else: # otherwise it's a package; add all test modules under the dir + modules.update( + import_module('.' + os.path.splitext(f)[0], package=name) + for f in os.listdir(os.path.dirname(package.__file__)) + if is_test_module(f)) + # add the modules to the suite + for module in modules: + # copied from django.test.simple.build_suite + if hasattr(module, 'suite'): + # if module has a suite() method, use it + suite.addTest(module.suite()) + else: # otherwise build the test suite ourselves. + suite.addTest(loadTestsFromModule(module)) + try: + suite.addTest(DocTestSuite(module, runner=DocTestRunner, + checker=doctestOutputChecker)) + except ValueError: # No doc tests + pass + return suite + +suite = lambda: get_suite(__name__) From 06f1c3a1eeadf8a8ef96d630a81d0eaa99e8b2fd Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 18 Jul 2011 22:43:37 +0200 Subject: [PATCH 006/429] Started UserRessource using APIKey auth --- codespeed/api.py | 19 ++++++++++++++++++- codespeed/tests/tests_api.py | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index dd057919..07007bda 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -24,11 +24,28 @@ See http://django-tastypie.readthedocs.org/en/latest/interacting.html """ +from django.contrib.auth.models import User +from django.db import models from tastypie.resources import ModelResource -from tastypie.authorization import Authorization +from tastypie.authorization import Authorization, DjangoAuthorization +from tastypie.authentication import ApiKeyAuthentication +from tastypie.models import create_api_key from codespeed.models import Environment +models.signals.post_save.connect(create_api_key, sender=User) + + +class UserResource(ModelResource): + class Meta: + queryset = User.objects.filter(is_active=True) + resource_name = 'auth/user' + excludes = ['email', 'password', 'is_superuser'] + # Add it here. + authorization = DjangoAuthorization() + authentication = ApiKeyAuthentication() + + class EnvironmentResource(ModelResource): class Meta: queryset = Environment.objects.all() diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 5c2d2ab0..5bbb360f 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -12,6 +12,7 @@ from django.test.client import Client from django.core.urlresolvers import reverse from django.conf import settings +from django.contrib.auth.models import User from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) from codespeed import settings as default_settings @@ -93,3 +94,15 @@ def test_env2_delete(self): # suite.addTest(EnvironmentTest()) # return suite + +class UserTest(FixtureTestCase): + """Test api user related stuff + """ + def setUp(self): + self.api_user = User.objects.create_user( + 'api', 'api@null.com', 'password') + self.api_user.save() + + def test_has_apikey(self): + self.assertTrue(hasattr(self.api_user, 'api_key')) + From 73639a4dfd37acd444167cfa31b6afff60930cf3 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 25 Jul 2011 18:09:31 +0200 Subject: [PATCH 007/429] Added tests for ApiKeyAuthentication - Tests stolen from tastypie to make sure ApiKeyAuth works here --- codespeed/api.py | 12 ++++--- codespeed/tests/tests_api.py | 64 ++++++++++++++++++++++++++++++------ codespeed/urls.py | 3 +- 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 07007bda..ab3356bb 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -27,6 +27,7 @@ from django.contrib.auth.models import User from django.db import models from tastypie.resources import ModelResource +from tastypie import fields from tastypie.authorization import Authorization, DjangoAuthorization from tastypie.authentication import ApiKeyAuthentication from tastypie.models import create_api_key @@ -39,14 +40,17 @@ class UserResource(ModelResource): class Meta: queryset = User.objects.filter(is_active=True) - resource_name = 'auth/user' - excludes = ['email', 'password', 'is_superuser'] + resource_name = 'user' + fields = ['username', 'first_name', 'last_name', 'last_login'] + #excludes = ['email', 'password', 'is_superuser'] # Add it here. - authorization = DjangoAuthorization() - authentication = ApiKeyAuthentication() + #authorization = DjangoAuthorization() + authorization= Authorization() + #authentication = ApiKeyAuthentication() class EnvironmentResource(ModelResource): + class Meta: queryset = Environment.objects.all() resource_name = 'environment' diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 5bbb360f..c0ed32e8 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -10,9 +10,13 @@ from django import test from django.test.client import Client +from django.http import HttpRequest from django.core.urlresolvers import reverse from django.conf import settings from django.contrib.auth.models import User +from tastypie.models import ApiKey, create_api_key +from tastypie.http import HttpUnauthorized +from tastypie.authentication import Authentication, ApiKeyAuthentication from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) from codespeed import settings as default_settings @@ -21,6 +25,11 @@ class FixtureTestCase(test.TestCase): fixtures = ["gettimeline_unittest.json"] + def setUp(self): + self.api_user = User.objects.create_user( + username='apiuser', email='api@foo.bar', password='password') + self.api_user.save() + class EnvironmentTest(FixtureTestCase): """Test Environment() API @@ -44,7 +53,7 @@ def setUp(self): kernel="2.6.32" ) self.client = Client() - super(FixtureTestCase, self).setUp() + super(EnvironmentTest, self).setUp() def test_dual_core(self): response = self.client.get('/api/v1/environment/1/') @@ -89,20 +98,55 @@ def test_env2_delete(self): response = self.client.get('/api/v1/environment/3/') self.assertEquals(response.status_code, 410) -#def suite(): -# suite = unittest.TestSuite() -# suite.addTest(EnvironmentTest()) -# return suite - class UserTest(FixtureTestCase): """Test api user related stuff """ - def setUp(self): - self.api_user = User.objects.create_user( - 'api', 'api@null.com', 'password') - self.api_user.save() def test_has_apikey(self): self.assertTrue(hasattr(self.api_user, 'api_key')) + +class ApiKeyAuthenticationTestCase(FixtureTestCase): + + def setUp(self): + super(ApiKeyAuthenticationTestCase, self).setUp() + ApiKey.objects.all().delete() + + def test_is_authenticated(self): + auth = ApiKeyAuthentication() + request = HttpRequest() + + # Simulate sending the signal. + user = User.objects.get(username='apiuser') + create_api_key(User, instance=user, created=True) + + # No username/api_key details should fail. + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Wrong username details. + request.GET['username'] = 'foo' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # No api_key. + request.GET['username'] = 'daniel' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Wrong user/api_key. + request.GET['username'] = 'daniel' + request.GET['api_key'] = 'foo' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Correct user/api_key. + user = User.objects.get(username='apiuser') + request.GET['username'] = 'apiuser' + request.GET['api_key'] = user.api_key.key + self.assertEqual(auth.is_authenticated(request), True) + + +#def suite(): +# suite = unittest.TestSuite() +# suite.addTest(EnvironmentTest()) +# return suite + + diff --git a/codespeed/urls.py b/codespeed/urls.py index 2c78f0ea..ccd49ad7 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -4,12 +4,13 @@ from django.views.generic.simple import direct_to_template from tastypie.api import Api from codespeed.feeds import LatestEntries -from codespeed.api import EnvironmentResource +from codespeed.api import UserResource, EnvironmentResource feeds = { 'latest': LatestEntries } rest_api = Api(api_name='v1') rest_api.register(EnvironmentResource()) +rest_api.register(UserResource()) urlpatterns = patterns('', (r'^$', direct_to_template, {'template': 'home.html'}), From 66b2093f0ad7c728a46717af7f120dd19d0b7a5e Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Fri, 29 Jul 2011 23:52:10 +0400 Subject: [PATCH 008/429] Fix typo. --- codespeed/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/views.py b/codespeed/views.py index 13692def..5e20ac50 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -892,7 +892,7 @@ def add_result(request): else: create_report_if_enough_data(response[0], response[1], response[2]) logging.debug("add_result: completed") - return HttpResponse("Result data saved succesfully", status=202) + return HttpResponse("Result data saved successfully", status=202) def add_json_results(request): From 68df835343693931142b60bdbf67dffb8de790e6 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Fri, 29 Jul 2011 23:52:31 +0400 Subject: [PATCH 009/429] Pass requeired argument. --- codespeed/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codespeed/views.py b/codespeed/views.py index 5e20ac50..ca4d9f0f 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -216,7 +216,7 @@ def comparison(request): # Check whether there exist appropiate executables if not getdefaultexecutable(): - return no_executables_error() + return no_executables_error(request) executables, exekeys = getcomparisonexes() checkedexecutables = [] @@ -606,7 +606,7 @@ def changes(request): defaultexecutable = getdefaultexecutable() if not defaultexecutable: - return no_executables_error() + return no_executables_error(request) if "exe" in data: try: From ab0ad214df32d8aea3298fbd5eba3fde6fd7a2b4 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 6 Sep 2011 17:37:19 +0400 Subject: [PATCH 010/429] Fix default setup (./manage.py runserver) --- example/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/settings.py b/example/settings.py index c9d4afe4..510beaed 100644 --- a/example/settings.py +++ b/example/settings.py @@ -51,7 +51,7 @@ # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/admin_media/' +ADMIN_MEDIA_PREFIX = '/static/admin/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'as%n_m#)^vee2pe91^^@c))sl7^c6t-9r8n)_69%)2yt+(la2&' From 323440a6e8d3375fefc0642585b6ffa29cde81dd Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 8 Sep 2011 21:12:25 +0200 Subject: [PATCH 011/429] Feed: only show reports in branch trunk --- codespeed/feeds.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codespeed/feeds.py b/codespeed/feeds.py index 35c16fcb..5c32482e 100644 --- a/codespeed/feeds.py +++ b/codespeed/feeds.py @@ -9,4 +9,6 @@ class LatestEntries(Feed): description = "Last benchmark runs" def items(self): - return Report.objects.order_by('-revision__date')[:10] + return Report.objects.filter( + revision__branch__name=settings.DEF_BRANCH + ).order_by('-revision__date')[:10] From 15763f6ebc8498b8177d09f525e70c37c19208ff Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 8 Sep 2011 21:13:20 +0200 Subject: [PATCH 012/429] Add settings option: DEF_TIMELINE_LIMIT sets default number of revisions to be plotted on the timeline --- codespeed/settings.py | 7 +++++-- codespeed/views.py | 2 +- example/settings.py | 17 ++++++++++------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/codespeed/settings.py b/codespeed/settings.py index cbd2d522..8331655e 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -2,7 +2,7 @@ """Default settings for Codespeed""" ## General default options ## -WEBSITE_NAME = "MySpeedSite" # This name will be used in the reports RSS feed +WEBSITE_NAME = "PyPy's Speed Center" # This name will be used in the reports RSS feed DEF_ENVIRONMENT = None #Name of the environment which should be selected as default @@ -38,7 +38,10 @@ # default when there are lots of benchmarks) # "mybench": will select benchmark named "mybench" -#timeline_branches = True # NOTE: Only the default branch is currently shown +DEF_TIMELINE_LIMIT = 50 # Default number of revisions to be plotted + # Possible values 10,50,200,1000 + +#TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown # Get timeline results for specific branches # Set to False if you want timeline plots and results only for trunk. diff --git a/codespeed/views.py b/codespeed/views.py index ca4d9f0f..f62ee329 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -496,7 +496,7 @@ def timeline(request): pass lastrevisions = [10, 50, 200, 1000] - defaultlast = 200 + defaultlast = settings.DEF_TIMELINE_LIMIT if 'revs' in data: if int(data['revs']) not in lastrevisions: lastrevisions.append(data['revs']) diff --git a/example/settings.py b/example/settings.py index 510beaed..83062fdf 100644 --- a/example/settings.py +++ b/example/settings.py @@ -51,7 +51,7 @@ # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/static/admin/' +ADMIN_MEDIA_PREFIX = '/admin_media/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'as%n_m#)^vee2pe91^^@c))sl7^c6t-9r8n)_69%)2yt+(la2&' @@ -119,12 +119,12 @@ def process_exception(self, request, exception): # Codespeed settings that can be overwritten here. ## General default options ## -#WEBSITE_NAME = "MySpeedSite" # This name will be used in the reports RSS feed +WEBSITE_NAME = "PyPy Speed Center" # This name will be used in the reports RSS feed #DEF_ENVIRONMENT = None #Name of the environment which should be selected as default -#DEF_BASELINE = None # Which executable + revision should be default as a baseline +DEF_BASELINE = {'executable': 'cpython', 'revision': '100'} # Which executable + revision should be default as a baseline # Given as the name of the executable and commitid of the revision # Example: defaultbaseline = {'executable': 'myexe', 'revision': '21'} @@ -132,11 +132,11 @@ def process_exception(self, request, exception): # Used by reports for the latest runs and changes view # Threshold that determines when a performance change over the last result is significant -#CHANGE_THRESHOLD = 3.0 +CHANGE_THRESHOLD = 5.0 # Threshold that determines when a performance change # over a number of revisions is significant -#TREND_THRESHOLD = 5.0 +TREND_THRESHOLD = 6.0 ## Changes view options ## #DEF_EXECUTABLE = None # Executable that should be chosen as default in the changes view @@ -149,6 +149,9 @@ def process_exception(self, request, exception): # "show_none": will just show a text message # "mybench": will select benchmark "mybench" +#DEF_TIMELINE_LIMIT = 50 # Default number of revisions to be plotted + # Possible values 10,50,200,1000 + #TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown # Get timeline results for specific branches # Set to False if you want timeline plots and results only for trunk. @@ -156,14 +159,14 @@ def process_exception(self, request, exception): ## Comparison view options ## #CHART_TYPE = 'normal bars' # The options are 'normal bars', 'stacked bars' and 'relative bars' -#NORMALIZATION = False # True will enable normalization as the default selection +NORMALIZATION = True # True will enable normalization as the default selection # in the Comparison view. The default normalization can be # chosen in the defaultbaseline setting #CHART_ORIENTATION = 'vertical' # 'vertical' or 'horizontal can be chosen as # default chart orientation -#COMP_EXECUTABLES = None # Which executable + revision should be checked as default +COMP_EXECUTABLES = [('pypy-c-jit', 'L'), ('pypy-c', 'L')] # Which executable + revision should be checked as default # Given as a list of tuples containing the # name of an executable + commitid of a revision # An 'L' denotes the last revision From 8ef0a8d62d933406251b9bbdf69a482294e88a88 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Fri, 9 Sep 2011 16:50:06 +0400 Subject: [PATCH 013/429] Promote RubySpec's usage of CodeSpeed as known non-Python project. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index fdc02d8c..a65df1f9 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ A web application to monitor and analyze the performance of your code. -Known to be used by [PyPy](http://speed.pypy.org), -[Twisted](http://speed.twistedmatrix.com). -and many more. +Known to be used by [PyPy](http://speed.pypy.org), [Twisted](http://speed.twistedmatrix.com), [RubySpec](http://speed.rubyspec.org/) and many more. For an overview of some application concepts see the [wiki page](https://github.com/tobami/codespeed/wiki/Overview) From 550629585d3d01f625d991d6421c0523a179d858 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sat, 10 Sep 2011 20:29:36 +0400 Subject: [PATCH 014/429] Fix tests. --- codespeed/tests.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/codespeed/tests.py b/codespeed/tests.py index ee83f47b..15ad4f80 100644 --- a/codespeed/tests.py +++ b/codespeed/tests.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from datetime import datetime, timedelta -from time import sleep import copy, json from django.test import TestCase @@ -38,7 +37,7 @@ def test_add_correct_result(self): # Check that we get a success response self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") + self.assertEquals(response.content, "Result data saved successfully") # Check that the data was correctly saved e = Environment.objects.get(name='Dual Core') @@ -70,7 +69,7 @@ def test_add_non_default_result(self): modified_data['min'] = 1.0 response = self.client.post(self.path, modified_data) self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") + self.assertEquals(response.content, "Result data saved successfully") e = Environment.objects.get(name='Dual Core') p = Project.objects.get(name='MyProject') branch = Branch.objects.get(name='default', project=p) @@ -156,7 +155,7 @@ def test_add_result_with_no_project(self): modified_data['executable'] = "My new executable" response = self.client.post(self.path, modified_data) self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved succesfully") + self.assertEquals(response.content, "Result data saved successfully") class AddJSONResults(TestCase): From 9428b2fb687e4fba2e2b1cfcf769564e2ef66f2d Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Sat, 10 Sep 2011 20:29:45 +0400 Subject: [PATCH 015/429] chmod +x --- example/manage.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 example/manage.py diff --git a/example/manage.py b/example/manage.py old mode 100644 new mode 100755 From c77373a68a904a9d74bbe876e17812c09283f95d Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 10 Sep 2011 21:46:52 +0200 Subject: [PATCH 016/429] Revert some changes introduced by 15763f6ebc8498b8177d09f525e70c37c19208ff --- codespeed/settings.py | 2 +- example/settings.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/codespeed/settings.py b/codespeed/settings.py index 8331655e..f2b1a343 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -2,7 +2,7 @@ """Default settings for Codespeed""" ## General default options ## -WEBSITE_NAME = "PyPy's Speed Center" # This name will be used in the reports RSS feed +WEBSITE_NAME = "MySpeedSite" # This name will be used in the reports RSS feed DEF_ENVIRONMENT = None #Name of the environment which should be selected as default diff --git a/example/settings.py b/example/settings.py index 83062fdf..b6a90d74 100644 --- a/example/settings.py +++ b/example/settings.py @@ -51,7 +51,7 @@ # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/admin_media/' +ADMIN_MEDIA_PREFIX = '/static/admin/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'as%n_m#)^vee2pe91^^@c))sl7^c6t-9r8n)_69%)2yt+(la2&' @@ -119,12 +119,12 @@ def process_exception(self, request, exception): # Codespeed settings that can be overwritten here. ## General default options ## -WEBSITE_NAME = "PyPy Speed Center" # This name will be used in the reports RSS feed +WEBSITE_NAME = "MySpeedSite" # This name will be used in the reports RSS feed #DEF_ENVIRONMENT = None #Name of the environment which should be selected as default -DEF_BASELINE = {'executable': 'cpython', 'revision': '100'} # Which executable + revision should be default as a baseline +#DEF_BASELINE = None # Which executable + revision should be default as a baseline # Given as the name of the executable and commitid of the revision # Example: defaultbaseline = {'executable': 'myexe', 'revision': '21'} @@ -132,11 +132,11 @@ def process_exception(self, request, exception): # Used by reports for the latest runs and changes view # Threshold that determines when a performance change over the last result is significant -CHANGE_THRESHOLD = 5.0 +#CHANGE_THRESHOLD = 3.0 # Threshold that determines when a performance change # over a number of revisions is significant -TREND_THRESHOLD = 6.0 +#TREND_THRESHOLD = 5.0 ## Changes view options ## #DEF_EXECUTABLE = None # Executable that should be chosen as default in the changes view @@ -159,14 +159,14 @@ def process_exception(self, request, exception): ## Comparison view options ## #CHART_TYPE = 'normal bars' # The options are 'normal bars', 'stacked bars' and 'relative bars' -NORMALIZATION = True # True will enable normalization as the default selection +#NORMALIZATION = False # True will enable normalization as the default selection # in the Comparison view. The default normalization can be # chosen in the defaultbaseline setting #CHART_ORIENTATION = 'vertical' # 'vertical' or 'horizontal can be chosen as # default chart orientation -COMP_EXECUTABLES = [('pypy-c-jit', 'L'), ('pypy-c', 'L')] # Which executable + revision should be checked as default +#COMP_EXECUTABLES = None # Which executable + revision should be checked as default # Given as a list of tuples containing the # name of an executable + commitid of a revision # An 'L' denotes the last revision From 46a7595e6df578abb95f5b52cdb4eab572eb67d6 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 11 Sep 2011 10:45:02 +0200 Subject: [PATCH 017/429] Tag 0.9.1 --- CHANGELOG | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 550a9a69..e8dbb40f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ == Change Log == +=== Version 0.9.1, September 11, 2011 === +* NEW: Stefan added the possibility to name the default branch other than "default" +* NEW: Danilo added executable grouping by project +* NEW: Made default number of data points in the timeline plots configurable +* FIX: Frank fixed a lot of issues with the deploy examples +* FIX: Other numerous fixes + === Version 0.9.0, July 17, 2011 === * NEW: Frank refactored the Django project and app. Now Codespeed is a proper, installable Django app, and there is a sample project in the "example" dir diff --git a/setup.py b/setup.py index 5a6a5f18..f7712d49 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ author='Miquel Torres', author_email='tobami@gmail.com', name='codespeed', - version='0.9.0', + version='0.9.1', url='https://github.com/tobami/codespeed', license='GNU Lesser General Public License version 2.1', install_requires=['django>=1.3', 'isodate', 'south'], From 8ecf33f6bb656f0f2307afe222fd9946652bcf40 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 12 Sep 2011 12:52:20 +0200 Subject: [PATCH 018/429] one line fix to make IE not fail completely in timeline view --- codespeed/templates/codespeed/timeline.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 5e6be49d..85364553 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -200,7 +200,7 @@ renderer:$.jqplot.DateAxisRenderer, pad: 1.01, autoscale:true, - showTicks: false, + showTicks: false } }, highlighter: {show:false}, From d279ac5a566a2d5b8fd54da1d1bd64ab48e02fbc Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 20 Oct 2011 15:14:46 +0200 Subject: [PATCH 019/429] Add docstring to api tests Split test_is_authenticated --- codespeed/tests/tests_api.py | 55 +++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index c0ed32e8..bf022cc5 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -55,19 +55,22 @@ def setUp(self): self.client = Client() super(EnvironmentTest, self).setUp() - def test_dual_core(self): + def test_get_environment(self): + """Should get an existing environment""" response = self.client.get('/api/v1/environment/1/') self.assertEquals(response.status_code, 200) self.assertEqual(json.loads(response.content)['name'], "Dual Core") - def test_env1(self): + def test_get_environment_all_fields(self): + """Should get all fields for an environment""" response = self.client.get('/api/v1/environment/%s/' % (self.env1.id,)) self.assertEquals(response.status_code, 200) for k in self.env1_data.keys(): self.assertEqual( json.loads(response.content)[k], getattr(self.env1, k)) - def test_env2_post(self): + def test_post(self): + """Should save a new environment""" response = self.client.post('/api/v1/environment/', data=json.dumps(self.env2_data), content_type='application/json') @@ -77,7 +80,8 @@ def test_env2_post(self): self.assertEqual( json.loads(response.content)[k], v) - def test_env2_put(self): + def test_put(self): + """Should modify an existing environment""" modified_data = copy.deepcopy(self.env2_data) modified_data['name'] = "env2.2" modified_data['memory'] = "128kB" @@ -90,7 +94,8 @@ def test_env2_put(self): self.assertEqual( json.loads(response.content)[k], v) - def test_env2_delete(self): + def test_delete(self): + """Should delete an environment""" response = self.client.delete('/api/v1/environment/3/', content_type='application/json') self.assertEquals(response.status_code, 410) @@ -112,41 +117,45 @@ class ApiKeyAuthenticationTestCase(FixtureTestCase): def setUp(self): super(ApiKeyAuthenticationTestCase, self).setUp() ApiKey.objects.all().delete() - - def test_is_authenticated(self): - auth = ApiKeyAuthentication() - request = HttpRequest() + self.auth = ApiKeyAuthentication() + self.request = HttpRequest() # Simulate sending the signal. user = User.objects.get(username='apiuser') create_api_key(User, instance=user, created=True) - # No username/api_key details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + def test_is_not_authenticated(self): + """Should return HttpUnauthorized when incorrect credentials are given""" + # No username/api_key details + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) # Wrong username details. - request.GET['username'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + self.request.GET['username'] = 'foo' + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) # No api_key. - request.GET['username'] = 'daniel' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + self.request.GET['username'] = 'daniel' + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) # Wrong user/api_key. - request.GET['username'] = 'daniel' - request.GET['api_key'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + self.request.GET['username'] = 'daniel' + self.request.GET['api_key'] = 'foo' + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) + def test_is_authenticated(self): + """Should correctly authenticate when using an existing user and key""" # Correct user/api_key. user = User.objects.get(username='apiuser') - request.GET['username'] = 'apiuser' - request.GET['api_key'] = user.api_key.key - self.assertEqual(auth.is_authenticated(request), True) + self.request.GET['username'] = 'apiuser' + self.request.GET['api_key'] = user.api_key.key + self.assertEqual(self.auth.is_authenticated(self.request), True) #def suite(): # suite = unittest.TestSuite() # suite.addTest(EnvironmentTest()) # return suite - - From f6744701c2c4976293bc8b4df9ae9ef2c09570c2 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Thu, 1 Dec 2011 23:27:49 +0100 Subject: [PATCH 020/429] Fix issue 86, Timeline plots fail when more than 4 plot series are selected See https://github.com/tobami/codespeed/issues/86 The JS code that places the legend outside of the plot was copied to the right place but not deleted where it was before. Fixed that. --- codespeed/templates/codespeed/timeline.html | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 85364553..75686e13 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -96,21 +96,6 @@ if (data['benchmark_description']) { $("#plotdescription").html('

' + data['benchmark'] + ': ' + data['benchmark_description'] + '

'); } - - if (series.length > 4) { - // Move legend outside plot area to unclutter - var labels = new Array(); - for (l in series) { - labels.push(series[l]['label'].length) - } - // offset ammount depends on the maximum label length - var offset = 55 + Math.max.apply( Math, labels ) * 5.4; - plotoptions.legend.location = 'ne'; - plotoptions.legend.xoffset = -offset; - $("#plot").css("margin-right", offset + 10); - var w = $("#plot").width(); - $("#plot").css('width', w - offset); - } } if (data["baseline"] != "None") { series.push({ From 70b5d9e9a388eeec780bdc1d285bf947581623d2 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Thu, 1 Dec 2011 23:41:45 +0100 Subject: [PATCH 021/429] Fixed a missing tag - Cudos to my html editor that found a missing closing ul tag --- codespeed/templates/codespeed/timeline.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 75686e13..559d70f3 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -389,12 +389,13 @@ {% endfor %} {% endfor %} -
    -
  • Baseline:

    - -
  • +
      +
    • Baseline:

      + +
    • +
From fbf875ffb8bc1cf3db2b3d8f6ac678ada5392c35 Mon Sep 17 00:00:00 2001 From: Reiner Date: Fri, 2 Dec 2011 15:46:27 +0100 Subject: [PATCH 022/429] Mention dependency on django-tastypie in readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a65df1f9..cc712b7c 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,17 @@ For an overview of some application concepts see the [wiki page](https://github. # Requirements -You will need Python 2.6+ and Django 1.1+ with South. +You will need Python 2.6+ and Django 1.1+ with South and Tastypie. In Debian and Ubuntu, they can be installed with: - sudo apt-get install python-django python-django-south + sudo apt-get install python-django python-django-south python-tastypie Instead of using distribution packages, you can use pip: sudo pip install django sudo pip install South + sudo pip install django-tastypie If you want version control integration, there are additional requirements: From 6cd50301264173083a39a3c68cf865317f1fc8a9 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Fri, 2 Dec 2011 17:38:53 +0100 Subject: [PATCH 023/429] Add option to control display of authors email address, close #32 Use SHOW_AUTHOR_EMAIL_ADDRESS to show or hide the email addresses in the changes view. --- codespeed/settings.py | 3 +++ codespeed/templates/codespeed/changes_logs.html | 4 ++-- codespeed/views.py | 3 ++- example/settings.py | 3 +++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/codespeed/settings.py b/codespeed/settings.py index f2b1a343..13d391d1 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -29,6 +29,9 @@ # Given as the name of the executable. # Example: defaultexecutable = "myexe" +SHOW_AUTHOR_EMAIL_ADDRESS = True # Whether to show the authors email address in the + # changes log + ## Timeline view options ## DEF_BENCHMARK = None # Default selected benchmark. Possible values: # None: will show a grid of plot thumbnails, or a diff --git a/codespeed/templates/codespeed/changes_logs.html b/codespeed/templates/codespeed/changes_logs.html index 66d0f0c6..95c45c50 100644 --- a/codespeed/templates/codespeed/changes_logs.html +++ b/codespeed/templates/codespeed/changes_logs.html @@ -13,7 +13,7 @@ {% endif %} - {% if log.author_email %} + {% if log.author_email and show_email_address %} {{ log.author }} {% else %} {{ log.author }} @@ -28,4 +28,4 @@ {% endfor %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/codespeed/views.py b/codespeed/views.py index f62ee329..ec162442 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -720,7 +720,8 @@ def displaylogs(request): error = repr(e) return render_to_response('codespeed/changes_logs.html', - {'error': error, 'logs': logs }, + {'error': error, 'logs': logs, + 'show_email_address': settings.SHOW_AUTHOR_EMAIL_ADDRESS}, context_instance=RequestContext(request)) diff --git a/example/settings.py b/example/settings.py index ad3af1a8..47844310 100644 --- a/example/settings.py +++ b/example/settings.py @@ -144,6 +144,9 @@ def process_exception(self, request, exception): # Given as the name of the executable. # Example: defaultexecutable = "myexe" +#SHOW_AUTHOR_EMAIL_ADDRESS = True # Whether to show the authors email address in the + # changes log + ## Timeline view options ## #DEF_BENCHMARK = "grid" # Default selected benchmark. Possible values: # "grid": will show the grid of plots From 0c6b8dc1b57eb0e113ae4563da4340b41dbd7399 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 2 Dec 2011 23:24:38 +0100 Subject: [PATCH 024/429] added ModelRessources for all models --- codespeed/api.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index ab3356bb..93dbee1b 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -26,22 +26,25 @@ from django.contrib.auth.models import User from django.db import models -from tastypie.resources import ModelResource +from tastypie.resources import ModelResource, Resource from tastypie import fields from tastypie.authorization import Authorization, DjangoAuthorization from tastypie.authentication import ApiKeyAuthentication from tastypie.models import create_api_key -from codespeed.models import Environment +from codespeed.models import Environment, Project, Result, Branch, Revision, Executable, Benchmark, Report models.signals.post_save.connect(create_api_key, sender=User) class UserResource(ModelResource): + """Ressource for Django User() + """ class Meta: queryset = User.objects.filter(is_active=True) resource_name = 'user' - fields = ['username', 'first_name', 'last_name', 'last_login'] + fields = ['username', 'first_name', 'last_name', 'email'] + allowed_methods = ['get'] #excludes = ['email', 'password', 'is_superuser'] # Add it here. #authorization = DjangoAuthorization() @@ -49,10 +52,76 @@ class Meta: #authentication = ApiKeyAuthentication() +class ProjectResource(ModelResource): + """Ressource for Project() + """ + + class Meta: + queryset = Project.objects.all() + authorization= Authorization() + + +class BranchResource(ModelResource): + """Ressource for Branch() + """ + + class Meta: + queryset = Branch.objects.all() + authorization= Authorization() + + +class RevisionResource(ModelResource): + """Ressource for Revision() + """ + + class Meta: + queryset = Revision.objects.all() + authorization= Authorization() + + +class ExecutableResource(ModelResource): + """Ressource for Executable() + """ + + class Meta: + queryset = Executable.objects.all() + authorization= Authorization() + + +class BenchmarkResource(ModelResource): + """Ressource for Benchmark() + """ + + class Meta: + queryset = Benchmark.objects.all() + authorization= Authorization() + + class EnvironmentResource(ModelResource): + """Ressource for Enviroment() + """ class Meta: queryset = Environment.objects.all() resource_name = 'environment' authorization= Authorization() + +class ResultResource(ModelResource): + """Ressource for Result() + """ + + class Meta: + queryset = Result.objects.all() + authorization= Authorization() + + +class ReportResource(ModelResource): + """Ressource for Report() + """ + + class Meta: + queryset = Report.objects.all() + authorization= Authorization() + + From 9faa669b0dc4f84a5fa2dda47b0369e790cdf5d8 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 2 Dec 2011 23:53:27 +0100 Subject: [PATCH 025/429] started resource for benchmark results --- codespeed/api.py | 26 ++++++++++++++++++++++++++ codespeed/urls.py | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index 93dbee1b..3a10a3e4 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -125,3 +125,29 @@ class Meta: authorization= Authorization() +class BenchmarkResultResource(Resource): + """Ressource for all the data of a benchmark result + + mandatory data + 'commitid', + 'branch', + 'project', + 'executable', + 'benchmark', + 'environment', + 'result_value', + """ + + revision = fields.ToOneField(RevisionResource, 'revision') + branch = fields.ToOneField(BranchResource, 'branch') + project = fields.ToOneField(ProjectResource, 'project') + executable = fields.ToOneField(ExecutableResource, 'executable') + benchmark = fields.ToOneField(BenchmarkResource, 'benchmark') + environment = fields.ToOneField(EnvironmentResource, 'environment') + result = fields.ToOneField(ResultResource, 'result') + + class Meta: + resource_name = 'benchmark-result' + authorization= Authorization() + allowed_methods = ['get', 'post', 'put', 'delete'] + diff --git a/codespeed/urls.py b/codespeed/urls.py index ccd49ad7..d2fe487d 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -4,13 +4,14 @@ from django.views.generic.simple import direct_to_template from tastypie.api import Api from codespeed.feeds import LatestEntries -from codespeed.api import UserResource, EnvironmentResource +from codespeed.api import UserResource, EnvironmentResource, BenchmarkResultResource feeds = { 'latest': LatestEntries } rest_api = Api(api_name='v1') rest_api.register(EnvironmentResource()) rest_api.register(UserResource()) +rest_api.register(BenchmarkResultResource()) urlpatterns = patterns('', (r'^$', direct_to_template, {'template': 'home.html'}), From 93031f1b44712518788b105b2944ddf9b58dff39 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 3 Dec 2011 13:32:25 +0100 Subject: [PATCH 026/429] fixed test, added Project tests, enabled Project restapi --- codespeed/tests/tests_api.py | 65 ++++++++++++++++++++++++++++++++++-- codespeed/urls.py | 3 +- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index bf022cc5..44af7244 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -96,11 +96,72 @@ def test_put(self): def test_delete(self): """Should delete an environment""" - response = self.client.delete('/api/v1/environment/3/', + response = self.client.get('/api/v1/environment/{0}/'.format(self.env1.id)) + self.assertEquals(response.status_code, 200) + response = self.client.delete('/api/v1/environment/{0}/'.format(self.env1.id), content_type='application/json') + self.assertEquals(response.status_code, 204) + + response = self.client.get('/api/v1/environment/{0}/'.format(self.env1.id)) self.assertEquals(response.status_code, 410) - response = self.client.get('/api/v1/environment/3/') + +class ProjectTest(FixtureTestCase): + """Test Environment() API + """ + + def setUp(self): + self.project_data = dict( + name="PyPy", + repo_type="M", + repo_path="ssh://hg@bitbucket.org/pypy/pypy", + repo_user="fridolin", + repo_pass="secret", + ) + self.project_data2 = dict( + name="project alpha", + repo_type="M", + repo_path="ssh://hg@bitbucket.org/pypy/pypy", + repo_user="alpha", + repo_pass="beta", + ) + self.project = Project(**self.project_data) + self.project.save() + self.client = Client() + super(ProjectTest, self).setUp() + + def test_get_project(self): + """Should get an existing project""" + response = self.client.get('/api/v1/project/{0}/'.format(self.project.id,)) + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['name'], "{0}".format(self.project_data['name'])) + + def test_get_project_all_fields(self): + """Should get all fields for an environment""" + response = self.client.get('/api/v1/project/%s/' % (self.project.id,)) + self.assertEquals(response.status_code, 200) + for k in self.project_data.keys(): + self.assertEqual( + json.loads(response.content)[k], getattr(self.project, k)) + + def test_post(self): + """Should save a new project""" + response = self.client.post('/api/v1/project/', + data=json.dumps(self.project_data2), + content_type='application/json') + self.assertEquals(response.status_code, 201) + response = self.client.get('/api/v1/project/{0}/'.format(self.project.id)) + for k, v in self.project_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_delete(self): + """Should delete an project""" + response = self.client.delete('/api/v1/project/{0}/'.format(self.project.id,), + content_type='application/json') + self.assertEquals(response.status_code, 204) + + response = self.client.get('/api/v1/project/{0}/'.format(self.project.id,)) self.assertEquals(response.status_code, 410) diff --git a/codespeed/urls.py b/codespeed/urls.py index d2fe487d..4e78f2a1 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -4,13 +4,14 @@ from django.views.generic.simple import direct_to_template from tastypie.api import Api from codespeed.feeds import LatestEntries -from codespeed.api import UserResource, EnvironmentResource, BenchmarkResultResource +from codespeed.api import UserResource, EnvironmentResource, BenchmarkResultResource, ProjectResource feeds = { 'latest': LatestEntries } rest_api = Api(api_name='v1') rest_api.register(EnvironmentResource()) rest_api.register(UserResource()) +rest_api.register(ProjectResource()) rest_api.register(BenchmarkResultResource()) urlpatterns = patterns('', From 7c465e1619fc2b6402dd38636de5ffef91af10c1 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 3 Dec 2011 23:45:25 +0100 Subject: [PATCH 027/429] on the way to BenchmarkResultResource, obj_get() first working implementation - While reading tastypie docu and try-and-error got obj_get() implemented. Yeah! Probably, it's all rubbish --- codespeed/api.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++- codespeed/urls.py | 7 ++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 3a10a3e4..f5a0a45c 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -23,6 +23,7 @@ See http://django-tastypie.readthedocs.org/en/latest/interacting.html """ +import logging from django.contrib.auth.models import User from django.db import models @@ -126,7 +127,9 @@ class Meta: class BenchmarkResultResource(Resource): - """Ressource for all the data of a benchmark result + """Ressource for all the data of a benchmark result. + + Primarily used to submit benchmark results mandatory data 'commitid', @@ -151,3 +154,63 @@ class Meta: authorization= Authorization() allowed_methods = ['get', 'post', 'put', 'delete'] + def get_object_list(self, request): + query = self._client().add('messages') + query.map("function(v) { var data = JSON.parse(v.values[0].data); return [[v.key, data]]; }") + results = [] + + for result in query.run(): + new_obj = RiakObject(initial=result[1]) + new_obj.uuid = result[0] + results.append(new_obj) + + return results + + def obj_get_list(self, request=None, **kwargs): + """Return all benchmark results ever + """ + return self.get_object_list(request) + + def obj_get(self, request=None, **kwargs): + pk = kwargs['pk'] + #logging.debug(kwargs) + d = { + 'xbenchmark': 2, + 'xrevision': 1, + 'branch': 1, + 'xexecutable': 1, + 'xenvironment': 1, + 'result': 1, + 'project': 1, + } + result = Result.objects.get(pk=pk) + result.project = result.executable.project + result.result = result + result.branch = result.revision + return result + + def obj_create(self, bundle, request=None, **kwargs): + bundle.obj = RiakObject(initial=kwargs) + bundle = self.full_hydrate(bundle) + bucket = self._bucket() + new_message = bucket.new(bundle.obj.uuid, data=bundle.obj.to_dict()) + new_message.store() + return bundle + + def obj_update(self, bundle, request=None, **kwargs): + return self.obj_create(bundle, request, **kwargs) + + def obj_delete_list(self, request=None, **kwargs): + bucket = self._bucket() + + for key in bucket.get_keys(): + obj = bucket.get(key) + obj.delete() + + def obj_delete(self, request=None, **kwargs): + bucket = self._bucket() + obj = bucket.get(kwargs['pk']) + obj.delete() + + def rollback(self, bundles): + pass diff --git a/codespeed/urls.py b/codespeed/urls.py index 4e78f2a1..854c5c91 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -4,7 +4,7 @@ from django.views.generic.simple import direct_to_template from tastypie.api import Api from codespeed.feeds import LatestEntries -from codespeed.api import UserResource, EnvironmentResource, BenchmarkResultResource, ProjectResource +from codespeed.api import UserResource, EnvironmentResource, BenchmarkResultResource, ProjectResource, ExecutableResource, BenchmarkResource, ResultResource, BranchResource, RevisionResource feeds = { 'latest': LatestEntries } @@ -12,6 +12,11 @@ rest_api.register(EnvironmentResource()) rest_api.register(UserResource()) rest_api.register(ProjectResource()) +rest_api.register(ExecutableResource()) +rest_api.register(BenchmarkResource()) +rest_api.register(ResultResource()) +rest_api.register(BranchResource()) +rest_api.register(RevisionResource()) rest_api.register(BenchmarkResultResource()) urlpatterns = patterns('', From 85b6e88b304fc63da6b58c98c6f3eb485d18c1f5 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 4 Dec 2011 00:00:06 +0100 Subject: [PATCH 028/429] same, same, but with Bundle() --- codespeed/api.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index f5a0a45c..91c3815b 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -27,6 +27,7 @@ from django.contrib.auth.models import User from django.db import models +from tastypie.bundle import Bundle from tastypie.resources import ModelResource, Resource from tastypie import fields from tastypie.authorization import Authorization, DjangoAuthorization @@ -172,6 +173,7 @@ def obj_get_list(self, request=None, **kwargs): return self.get_object_list(request) def obj_get(self, request=None, **kwargs): + bundle = Bundle pk = kwargs['pk'] #logging.debug(kwargs) d = { @@ -184,10 +186,11 @@ def obj_get(self, request=None, **kwargs): 'project': 1, } result = Result.objects.get(pk=pk) - result.project = result.executable.project - result.result = result - result.branch = result.revision - return result + bundle.obj = result + bundle.obj.project = bundle.obj.executable.project + bundle.obj.branch = bundle.obj.revision.branch + bundle.obj.result = result + return bundle.obj def obj_create(self, bundle, request=None, **kwargs): bundle.obj = RiakObject(initial=kwargs) From 802f5bbbc65fd1e0ffc66d73d6901873b8c59319 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 4 Dec 2011 00:04:44 +0100 Subject: [PATCH 029/429] fixed branch, cleanup - instead of branch the revision was returned - deleted debug stuff --- codespeed/api.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index f5a0a45c..db380da9 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -173,20 +173,10 @@ def obj_get_list(self, request=None, **kwargs): def obj_get(self, request=None, **kwargs): pk = kwargs['pk'] - #logging.debug(kwargs) - d = { - 'xbenchmark': 2, - 'xrevision': 1, - 'branch': 1, - 'xexecutable': 1, - 'xenvironment': 1, - 'result': 1, - 'project': 1, - } result = Result.objects.get(pk=pk) result.project = result.executable.project result.result = result - result.branch = result.revision + result.branch = result.revision.branch return result def obj_create(self, bundle, request=None, **kwargs): From fd3fce2118089280a1e0717bbfd86633ab8d09d4 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Fri, 2 Dec 2011 18:36:28 +0100 Subject: [PATCH 030/429] Move working copy handling from git/mercurial into project model --- codespeed/git.py | 15 ++++----------- codespeed/mercurial.py | 14 ++++---------- codespeed/models.py | 20 ++++++++++++++++++++ codespeed/tests/tests.py | 25 ++++++++++++++++++++++++- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/codespeed/git.py b/codespeed/git.py index 4b190522..8beb4458 100644 --- a/codespeed/git.py +++ b/codespeed/git.py @@ -7,15 +7,12 @@ def updaterepo(project, update=True): - repo_name = os.path.splitext(project.repo_path.split(os.sep)[-1])[0] - working_copy = os.path.join(settings.REPOSITORY_BASE_PATH, repo_name) - - if os.path.exists(working_copy): + if os.path.exists(project.working_copy): if not update: return p = Popen(['git', 'pull'], stdout=PIPE, stderr=PIPE, - cwd=working_copy) + cwd=project.working_copy) stdout, stderr = p.communicate() if p.returncode != 0: @@ -24,7 +21,7 @@ def updaterepo(project, update=True): else: return [{'error': False}] else: - cmd = ['git', 'clone', project.repo_path, repo_name] + cmd = ['git', 'clone', project.repo_path, project.repo_name] p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=settings.REPOSITORY_BASE_PATH) logging.debug('Cloning Git repo {0}for project {1}'.format( @@ -40,11 +37,6 @@ def updaterepo(project, update=True): def getlogs(endrev, startrev): updaterepo(endrev.branch.project, update=False) - # TODO: Move all of this onto the model so we can avoid needing to repeat it: - repo_name = os.path.splitext( - endrev.branch.project.repo_path.split(os.sep)[-1])[0] - working_copy = os.path.join(settings.REPOSITORY_BASE_PATH, repo_name) - cmd = ["git", "log", # NULL separated values delimited by 0x1e record separators # See PRETTY FORMATS in git-log(1): @@ -56,6 +48,7 @@ def getlogs(endrev, startrev): cmd.append("-1") # Only return one commit cmd.append(endrev.commitid) + working_copy = endrev.branch.project.working_copy p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=working_copy) stdout, stderr = p.communicate() diff --git a/codespeed/mercurial.py b/codespeed/mercurial.py index 884f23dc..b8de9ebf 100644 --- a/codespeed/mercurial.py +++ b/codespeed/mercurial.py @@ -6,15 +6,12 @@ def updaterepo(project, update=True): - repo_name = os.path.splitext(project.repo_path.split(os.sep)[-1])[0] - working_copy = os.path.join(settings.REPOSITORY_BASE_PATH, repo_name) - - if os.path.exists(working_copy): + if os.path.exists(project.working_copy): if not update: return p = Popen(['hg', 'pull', '-u'], stdout=PIPE, stderr=PIPE, - cwd=working_copy) + cwd=project.working_copy) stdout, stderr = p.communicate() if p.returncode != 0 or stderr: @@ -24,7 +21,7 @@ def updaterepo(project, update=True): return [{'error': False}] else: # Clone repo - cmd = ['hg', 'clone', project.repo_path, repo_name] + cmd = ['hg', 'clone', project.repo_path, project.repo_name] p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=settings.REPOSITORY_BASE_PATH) @@ -42,15 +39,12 @@ def updaterepo(project, update=True): def getlogs(endrev, startrev): updaterepo(endrev.branch.project, update=False) - # TODO: Move all of this onto the model so we can avoid needing to repeat it: - repo_name = os.path.splitext(endrev.branch.project.repo_path.split(os.sep)[-1])[0] - working_copy = os.path.join(settings.REPOSITORY_BASE_PATH, repo_name) - cmd = ["hg", "log", "-r", "%s:%s" % (endrev.commitid, startrev.commitid), "-b", "default", "--template", "{rev}:{node|short}\n{node}\n{author|user}\n{author|email}\n{date}\n{desc}\n=newlog=\n"] + working_copy = endrev.branch.project.working_copy p = Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=working_copy) stdout, stderr = p.communicate() diff --git a/codespeed/models.py b/codespeed/models.py index 39488c2e..e2c1c9df 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import os + from django.core.exceptions import ValidationError from django.db import models from django.utils import simplejson as json @@ -26,6 +28,24 @@ class Project(models.Model): def __unicode__(self): return self.name + @property + def repo_name(self): + # name not defined for None, GitHub or Subversion + if self.repo_type in ('N', 'H', 'S'): + error = 'Not supported for %s project' % self.get_repo_type_display() + raise AttributeError(error) + + return os.path.splitext(self.repo_path.split(os.sep)[-1])[0] + + @property + def working_copy(self): + # working copy exists for mercurial and git only + if self.repo_type in ('N', 'H', 'S'): + error = 'Not supported for %s project' % self.get_repo_type_display() + raise AttributeError(error) + + return os.path.join(settings.REPOSITORY_BASE_PATH, self.repo_name) + class Branch(models.Model): name = models.CharField(max_length=20) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index 15ad4f80..cc3db188 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from datetime import datetime, timedelta -import copy, json +import copy, json, os from django.test import TestCase from django.test.client import Client @@ -375,3 +375,26 @@ def test_settings_attributes(self): for k in self.cs_setting_keys: self.assertEqual(getattr(settings, k), getattr(default_settings, k)) + +class ProjectTest(TestCase): + """Test project model + """ + + def setUp(self): + self.github_project = Project(repo_type='H', repo_path='https://github.com/tobami/codespeed.git') + self.git_project = Project(repo_type='G', repo_path='/home/foo/codespeed') + + def test_repo_name(self): + """Test that only projects with local repositories have a repo_name attribute + """ + self.assertEqual(self.git_project.repo_name, 'codespeed') + + self.assertRaises(AttributeError, getattr, self.github_project, 'repo_name') + + def test_working_copy(self): + """Test that only projects with local repositories have a working_copy attribute + """ + self.assertEqual(self.git_project.working_copy, + os.path.join(settings.REPOSITORY_BASE_PATH, self.git_project.repo_name)) + + self.assertRaises(AttributeError, getattr, self.github_project, 'working_copy') From ec040d5491c6248e193514c151ccb8687e87ac70 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Tue, 6 Dec 2011 22:22:29 +0100 Subject: [PATCH 031/429] Move most javascript embedded in templates into separate files This is only the first step, only code that was moveable without much effort (read: only simple, replaceable template logic embedded) was moved. --- codespeed/static/js/changes.js | 82 ++++++ codespeed/static/js/comparison.js | 66 +++++ codespeed/static/js/timeline.js | 246 +++++++++++++++++ codespeed/templates/codespeed/changes.html | 89 +------ codespeed/templates/codespeed/comparison.html | 68 +---- codespeed/templates/codespeed/timeline.html | 248 +----------------- 6 files changed, 401 insertions(+), 398 deletions(-) create mode 100644 codespeed/static/js/changes.js create mode 100644 codespeed/static/js/comparison.js create mode 100644 codespeed/static/js/timeline.js diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js new file mode 100644 index 00000000..af02b6e8 --- /dev/null +++ b/codespeed/static/js/changes.js @@ -0,0 +1,82 @@ +function getConfiguration() { + var config = new Object(); + config["tre"] = $("#trend option:selected").val(); + config["rev"] = $("#revision option:selected").val(); + config["exe"] = $("input[name='executable']:checked").val(); + config["env"] = $("input[name='environment']:checked").val(); + return config; +} + +function permalinkToTimeline(benchmark, environment) { + window.location=timeline_url + "?ben=" + benchmark + "&env=" + environment; +} + +function colorTable() { + //color two colums to the right starting with index = last-1 + // Each because there is one table per units type + $(".tablesorter").each(function() { + // Find column index of the current change column (one before last) + var index = $(this).find("thead tr th").length -2; + var lessisbetter = $(this).find("th:eq(1) label").text(); + $(this).find(":not(thead) > tr").each(function() { + var change = $(this).children("td:eq("+index+")").text().slice(0, -1); + var trend = $(this).children("td:eq("+(index+1)+")").text().slice(0, -1); + // Check whether less is better + if (lessisbetter == "False") { + change = -change; + trend = -trend; + } + //Color change column + $(this).children("td:eq("+index+")").addClass(getColorcode(-change, changethres, -changethres)); + //Color trend column + $(this).children("td:eq("+(index+1)+")").addClass(getColorcode(-trend, trendthres, -trendthres)); + }); + }); +} + +function updateTable() { + colorTable(); + //Add permalink events to table rows + $(".tablesorter > tbody > tr").each(function() { + $(this).click(function () { + var environment = $("input[name='environment']:checked").val(); + permalinkToTimeline($(this).children("td:eq(0)").text(), environment); + }); + }); + //Add hover effect to rows + $(".tablesorter > tbody > tr > td").hover(function() { + $(this).parents('tr').addClass('highlight'); + }, function() { + $(this).parents('tr').removeClass('highlight'); + }); + //Configure table as tablesorter + $(".tablesorter").tablesorter({widgets: ['zebra']}); +} + +function refreshContent() { + var h = parseInt($("#content").css("height"));//get height for loading text + $("#contentwrap").fadeOut("fast", function() { + $(this).show(); + $(this).html(getLoadText("Loading...", h, true)); + $(this).load("table/", $.param(getConfiguration()), function(responseText) { updateTable(); }); + }); +} + +function changerevisions() { + // This function repopulates the revision selectbox everytime a new + // executable is selected that corresponds to a different project. + var executable = $("input[name='executable']:checked").val(); + if (projectmatrix[executable] != currentproject) { + $("#revision").html(revisionboxes[projectmatrix[executable]]); + currentproject = projectmatrix[executable]; + + //Give visual cue that the select box has changed + var bgc = $("#revision").parent().parent().css("backgroundColor"); + $("#revision").parent() + .animate({ backgroundColor: "#9DADC6" }, 200, function() { + // Animation complete. + $(this).animate({ backgroundColor: bgc }, 1500); + }); + } + refreshContent(); +} diff --git a/codespeed/static/js/comparison.js b/codespeed/static/js/comparison.js new file mode 100644 index 00000000..ea33321a --- /dev/null +++ b/codespeed/static/js/comparison.js @@ -0,0 +1,66 @@ +function getConfiguration() { + var config = new Object(); + config["exe"] = readCheckbox("input[name='executables']:checked"); + config["ben"] = readCheckbox("input[name='benchmarks']:checked"); + config["env"] = readCheckbox("input[name='environments']:checked"); + config["hor"] = $("input[name='direction']").is(':checked'); + config["bas"] = $("#baseline option:selected").val(); + config["chart"] = $("#chart_type option:selected").val(); + return config; +} + +function refreshContent() { + var conf = getConfiguration(); + var exes = conf['exe'].split(","); + var bens = conf['ben'].split(","); + var enviros = conf['env'].split(","); + var msg = ""; + + h = parseInt($("#plotwrapper").css("height"));//get height for error message + if (exes[0] == "") { + $("#plotwrapper").html('

No executables selected

'); + return false; + } else if (bens[0] == "") { + $("#plotwrapper").html('

No benchmarks selected

'); + return false; + } else if (enviros[0] == "") { + $("#plotwrapper").html('

No environments selected

'); + return false; + } else if (conf['chart'] == "relative bars" && conf['bas'] == "none") { + msg = '

For relative bar charts, you must select a baseline to normalize to.

' + $("#plotwrapper").html(msg); + return false; + } else if (conf['chart'] == "stacked bars" && conf['bas'] != "none") { + msg = '

Normalized stacked bars actually represent the weighted arithmetic sum, useful to spot which individual benchmarks take up the most time. Choosing different weightings from the "Normalization" menu will change the totals relative to one another. For the correct way to calculate total bars, the geometric mean must be used (see paper)

'; + } + + $("#plotwrapper").fadeOut("fast", function() { + $(this).html(msg).show(); + var plotcounter = 1; + for (unit in bench_units) { + var benchmarks = new Array(); + for (ben in bens) { + if ($.inArray(parseInt(bens[ben]), bench_units[unit][0]) != -1) { + benchmarks.push(bens[ben]); + } + } + if (benchmarks.length == 0) { continue; } + + var plotid = "plot" + plotcounter; + $("#plotwrapper").append('
'); + plotcounter++; + renderComparisonPlot(plotid, benchmarks, exes, enviros, conf['bas'], conf['chart'], conf['hor']); + } + }); +} + +function savedata(data) { + if (data['error'] != "None") { + h = parseInt($("#content").css("height"));//get height for error message + $("#cplot").html(getLoadText(data["error"], h)); + return 1; + } + delete data['error']; + compdata = data; + refreshContent(); +} diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js new file mode 100644 index 00000000..5fb01e42 --- /dev/null +++ b/codespeed/static/js/timeline.js @@ -0,0 +1,246 @@ +function setExeColors() { + // Set color data attribute for all executables + $("#executable > div.boxbody > ul > ul > li > input").each(function(index) { + var color_id = index; + while (color_id > seriesColors.length) { color_id -= seriesColors.length; } + $(this).data('color', seriesColors[color_id]); + }); +} + +function getColor(exe_id) { + return $("#executable > div.boxbody") + .find("input[value='"+exe_id+"']") + .data('color'); +} + +function shouldPlotEquidistant() { + return $("#equidistant").is(':checked'); +} + +function getConfiguration() { + var config = new Object(); + config["exe"] = readCheckbox("input[name='executable']:checked"); + config["base"] = $("#baseline option:selected").val(); + config["ben"] = $("input[name='benchmark']:checked").val(); + config["env"] = $("input[name='environments']:checked").val(); + config["revs"] = $("#revisions option:selected").val(); + + var branch = readCheckbox("input[name='branch']:checked"); + if (branch) + config["bran"] = branch; + config["equid"] = $("#equidistant").is(':checked') ? "on" : "off"; + return config; +} + +function permalinkToChanges(commitid, executableid, environment) { + window.location=changes_url + "?rev=" + commitid + "&" + "exe=" + executableid + "&env=" + environment; +} + +function OnMarkerClickHandler(ev, gridpos, datapos, neighbor, plot) { + if($("input[name='benchmark']:checked").val() == "grid") { return false; } + if (neighbor) { + var commitid = neighbor.data[3]; + // Get executable ID from the seriesindex array + var executableid = seriesindex[neighbor.seriesIndex]; + var environment = $("input[name='environments']:checked").val(); + permalinkToChanges(commitid, executableid, environment); + } +} + +function renderPlot(data) { + var plotdata = new Array(); + var series = new Array(); + var lastvalues = new Array();//hopefully the smallest values for determining significant digits. + seriesindex = new Array(); + for (branch in data["branches"]) { + // NOTE: Currently, only the "default" branch is shown in the timeline + for (exe_id in data["branches"][branch]) { + if (branch != "default") { label += " - " + branch; } + var label = $("label[for*='executable" + exe_id + "']").html(); + series.push({"label": label, "color": getColor(exe_id)}); + seriesindex.push(exe_id); + plotdata.push(data["branches"][branch][exe_id]); + lastvalues.push(data["branches"][branch][exe_id][0][1]); + } + //determine significant digits + var digits = 2; + var value = Math.min.apply( Math, lastvalues ); + if (value != 0) { + while( value < 1 ) { + value *= 10; + digits++; + } + } + $("#plotgrid").html('
'); + + if (data['benchmark_description']) { + $("#plotdescription").html('

' + data['benchmark'] + ': ' + data['benchmark_description'] + '

'); + } + } + if (data["baseline"] != "None") { + series.push({ + "label": $("#baseline option:selected").html(), "color": baselineColor, + showMarker: false, + lineWidth: 1.5 + }); + plotdata.push(data["baseline"]); + } + var plotoptions = { + title: {text: data['benchmark'], fontSize: '1.1em'}, + series: series, + axes:{ + yaxis:{ + label: data['units'] + data['lessisbetter'], + labelRenderer: $.jqplot.CanvasAxisLabelRenderer, + min: 0, autoscale:true, + tickOptions:{formatString:'%.' + digits + 'f'} + }, + xaxis:{ + renderer: (shouldPlotEquidistant()) ? $.jqplot.CategoryAxisRenderer : $.jqplot.DateAxisRenderer, + label: 'Commit date', + labelRenderer: $.jqplot.CanvasAxisLabelRenderer, + tickOptions:{formatString:'%b %d'}, + pad: 1.01, + autoscale:true, + rendererOptions:{sortMergedLabels:true} /* only relevant when + $.jqplot.CategoryAxisRenderer is used */ + } + }, + legend: {show: true, location: 'nw'}, + highlighter: { + tooltipLocation: 'nw', + yvalues: 4, + formatString:'
date:%s
result:%s
std dev:%s
commit:%s
' + }, + cursor:{zoom:true, showTooltip:false, clickReset:true} + } + if (series.length > 4) { + // Move legend outside plot area to unclutter + var labels = new Array(); + for (l in series) { + labels.push(series[l]['label'].length) + } + + var offset = 55 + Math.max.apply( Math, labels ) * 5.4; + plotoptions.legend.location = 'ne'; + plotoptions.legend.xoffset = -offset; + $("#plot").css("margin-right", offset + 10); + var w = $("#plot").width(); + $("#plot").css('width', w - offset); + } + //Render plot + plot = $.jqplot('plot', plotdata, plotoptions); +} + +function renderMiniplot(plotid, data) { + var plotdata = new Array(); + for (branch in data['branches']) { + var series = new Array(); + for (id in data['branches'][branch]) { + series.push({ + "label": $("label[for*='executable" + id + "']").html(), + "color": getColor(id) + }); + plotdata.push(data['branches'][branch][id]); + } + } + if (data["baseline"] != "None") { + series.push({ + "color": baselineColor, + showMarker: false, + lineWidth: 1.5 + }); + plotdata.push(data["baseline"]); + } + + var plotoptions = { + title: {text: data['benchmark'], fontSize: '1.1em'}, + seriesDefaults: {lineWidth: 2, markerOptions:{style:'circle', size: 6}}, + series: series, + axes: { + yaxis: { + min: 0, autoscale:true, showTicks: false + }, + xaxis: { + renderer:$.jqplot.DateAxisRenderer, + pad: 1.01, + autoscale:true, + showTicks: false + } + }, + highlighter: {show:false}, + cursor:{showTooltip: false, style: 'pointer'} + }; + plot = $.jqplot(plotid, plotdata, plotoptions); +} + +function render(data) { + $("#revisions").attr("disabled", false); + $("#equidistant").attr("disabled", false); + $("#plotgrid").html(""); + if(data["error"] != "None") { + h = parseInt($("#content").css("height"));//get height for error message + $("#plotgrid").html(getLoadText(data["error"], h, false)); + return 1; + } else if ($("input[name='benchmark']:checked").val() == "show_none") { + h = parseInt($("#content").css("height"));//get height for error message + $("#plotgrid").html(getLoadText("Please select a benchmark on the left", h, false)); + } else if (data["timelines"].length == 0) { + h = parseInt($("#content").css("height"));//get height for error message + $("#plotgrid").html(getLoadText("No data available", h, false)); + } else if ($("input[name='benchmark']:checked").val() == "grid"){ + //Render Grid of plots + $("#revisions").attr("disabled",true); + $("#equidistant").attr("disabled", true); + for (bench in data["timelines"]) { + plotid = "plot_" + data["timelines"][bench]["benchmark_id"]; + $("#plotgrid").append('
'); + $("#" + plotid).click(function() { + var benchid = $(this).attr("id").slice(5); + $("#benchmark_" + benchid).attr('checked', true); + updateUrl(); + }); + renderMiniplot(plotid, data["timelines"][bench]); + } + } else { + // render single plot when one benchmark is selected + renderPlot(data["timelines"][0]); + return 1; + } +} + +function refreshContent() { + var h = parseInt($("#content").css("height"));//get height for loading text + $("#plotgrid").fadeOut("fast", function() { + $("#plotgrid").html(getLoadText("Loading...", h, true)).show(); + $.getJSON("json/", getConfiguration(), render); + }); +} + +function updateUrl() { + cfg = getConfiguration(); + for (param in cfg) { + $.address.parameter(param, cfg[param]) + } + $.address.update(); +} + +function valueOrDefault(obj, defaultObj) { + return (obj) ? obj : defaultObj; +} + +function initializeSite(event) { + setValuesOfInputFields(event); + $("#revisions" ).change(updateUrl); + $("#baseline" ).change(updateUrl); + $("input[name='executable']" ).change(updateUrl); + $("input[name='branch']" ).change(updateUrl); + $("input[name='benchmark']" ).change(updateUrl); + $("input[name='environments']").change(updateUrl); + $("#equidistant" ).change(updateUrl); +} + +function refreshSite(event) { + setValuesOfInputFields(event); + refreshContent(); +} diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index c9d31373..4b48b171 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -3,6 +3,7 @@ {% block extra_head %} {{ block.super }} + + + diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 27331e24..82d18a7d 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -68,9 +68,9 @@ return false; }); - plotheight = $("#cplot").height(); + var plotheight = $("#cplot").height(); // Get comparison data - var h = parseInt($("#content").css("height"));//get height for loading text + var h = $("#content").height();//get height for loading text $("#cplot").html(getLoadText("Loading...", h, true)); $.getJSON("json/", savedata); }); diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 26216e79..35162c14 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -15,7 +15,7 @@ - + {% block extra_head %}{% endblock %} From ebf98733691bd936303fc1e8ab166032fa31110e Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Wed, 7 Dec 2011 17:18:29 +0100 Subject: [PATCH 038/429] Example for html5 data attributes instead of parsing other attributes --- codespeed/static/js/changes.js | 10 ++++++---- codespeed/templates/codespeed/changes_table.html | 16 ++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js index f6cc85bb..fb4b0564 100644 --- a/codespeed/static/js/changes.js +++ b/codespeed/static/js/changes.js @@ -16,11 +16,13 @@ function colorTable() { // Each because there is one table per units type $(".tablesorter").each(function() { // Find column index of the current change column (one before last) - var index = $(this).find("thead tr th").length -2; - var lessisbetter = $(this).find("th:eq(1) label").text(); + var index = $(this).find("thead tr th").length - 2; + var lessisbetter = $(this).data("lessisbetter"); + $(this).find(":not(thead) > tr").each(function() { - var change = $(this).children("td:eq("+index+")").text().slice(0, -1); - var trend = $(this).children("td:eq("+(index+1)+")").text().slice(0, -1); + var change = $(this).data("change"), + trend = $(this).data("trend"); + // Check whether less is better if (lessisbetter === "False") { change = -change; diff --git a/codespeed/templates/codespeed/changes_table.html b/codespeed/templates/codespeed/changes_table.html index a6f02635..5b0eafde 100644 --- a/codespeed/templates/codespeed/changes_table.html +++ b/codespeed/templates/codespeed/changes_table.html @@ -1,10 +1,10 @@
{% for units in tablelist %} - +
- + {% if units.has_stddev %}{% endif%} {% if units.hasmin %}{% endif%} {% if units.hasmax %}{% endif%} @@ -13,7 +13,7 @@ - + {% if units.hasmin %} @@ -24,7 +24,8 @@ -{% for row in units.rows|dictsort:"bench_name" %} +{% for row in units.rows|dictsort:"bench_name" %} + {% if units.has_stddev %} @@ -90,13 +91,12 @@ - +
Benchmark{{ units.units_title }} in {{ units.units }}{{ units.units_title }} in {{ units.units }}Std devMinMax
Average
{{ row.bench_name }} {{ row.result|floatformat:units.precission }}{{ row.std_dev|floatformat:units.precission }}Commit logs
Loading...
{% endifnotequal %} From 9b349b7c2adaf285b9666b7f16f887e33757a9b9 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Wed, 7 Dec 2011 17:51:39 +0100 Subject: [PATCH 039/429] Move non global code from codespeed.js into appropriate javascript file --- codespeed/static/js/changes.js | 8 + codespeed/static/js/codespeed.js | 319 ------------------------------ codespeed/static/js/comparison.js | 311 +++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 319 deletions(-) diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js index fb4b0564..292bc8c0 100644 --- a/codespeed/static/js/changes.js +++ b/codespeed/static/js/changes.js @@ -11,6 +11,14 @@ function permalinkToTimeline(benchmark, environment) { window.location=timeline_url + "?ben=" + benchmark + "&env=" + environment; } +//colors number based on a threshold +function getColorcode(change, theigh, tlow) { + var colorcode = "status-none"; + if(change < tlow) { colorcode = "status-red"; } + else if(change > theigh) { colorcode = "status-green"; } + return colorcode; +} + function colorTable() { //color two colums to the right starting with index = last-1 // Each because there is one table per units type diff --git a/codespeed/static/js/codespeed.js b/codespeed/static/js/codespeed.js index a509a938..55c788c4 100644 --- a/codespeed/static/js/codespeed.js +++ b/codespeed/static/js/codespeed.js @@ -30,322 +30,3 @@ function getLoadText(text, h, showloader) { loadtext += '

'; return loadtext; } - -//colors number based on a threshold -function getColorcode(change, theigh, tlow) { - var colorcode = "status-none"; - if(change < tlow) { colorcode = "status-red"; } - else if(change > theigh) { colorcode = "status-green"; } - return colorcode; -} - -function abortRender(plotid, message) { - $("#" + plotid).css("border", "dashed 1px grey"); - $("#" + plotid).css("padding", "1em"); - $("#" + plotid).css("width", "400px"); - $("#" + plotid).html(getLoadText(message, 0)); - return -1; -} - -function renderComparisonPlot(plotid, benchmarks, exes, enviros, baseline, chart, horizontal) { - var axislabel = ""; - var title = ""; - var baseline_is_empty = true; - if (baseline === "none") { - baseline_is_empty = false; - if (chart === "stacked bars") { title = "Cumulative "; } - title += unit; - axislabel = bench_units[unit][2] + bench_units[unit][1]; - } else { - if (chart === "stacked bars") { - title = "Cumulative " + unit + " normalized to " + $("label[for='exe_" + baseline + "']").text(); - } else if (chart === "relative bars") { - title = unit + " ratio to " + $("label[for='exe_" + baseline + "']").text(); - } else { - title = unit + " normalized to " + $("label[for='exe_" + baseline + "']").text(); - } - axislabel = "Ratio " + bench_units[unit][1]; - } - - var plotdata = []; - var ticks = []; - var series = []; - var barcounter = 0; - if (chart === "stacked bars" && horizontal) { exes.reverse(); } - if (chart === "normal bars" || chart === "relative bars") { - if (horizontal) { benchmarks.reverse(); } - // Add tick labels - for (var ben in benchmarks) { - var benchlabel = $("label[for='benchmark_" + benchmarks[ben] + "']").text(); - ticks.push(benchlabel); - } - // Add data - for (var i in exes) { - for (var j in enviros) { - var exe = $("label[for='exe_" + exes[i] + "']").text(); - var env = $("label[for='env_" + enviros[j] + "']").text(); - // for relative bars leave out (don't show) the baseline exe - if (chart === "relative bars") { - if (exe === $("label[for='exe_" + baseline + "']").text()) { - continue; - } - } - series.push({'label': exe + " @ " + env}); - var customdata = []; - var benchcounter = 0; - if (baseline !== "none") { - if (chart === "relative bars") { - axislabel = "<- worse - better ->"; - } - } - for (var b in benchmarks) { - benchcounter++; - barcounter++; - var val = compdata[exes[i]][enviros[j]][benchmarks[b]]; - if (val !== null) { - if (baseline !== "none") { - var baseval = compdata[baseline][enviros[j]][benchmarks[b]]; - if (baseval === null || baseval === 0) { - val = 0.0001; - } else { - baseline_is_empty = false; - val = val / baseval; - if (chart === "relative bars" && val > 1) { - val = -val; - } - } - } - } - //Add data - if (!horizontal) { - customdata.push(val); - } else { - customdata.push([val, benchcounter]); - } - } - plotdata.push(customdata); - } - } - } else if (chart === "stacked bars") { - // Add tick labels - for (var i in exes) { - for (var j in enviros) { - var exe = $("label[for='exe_" + exes[i] + "']").text(); - var env = $("label[for='env_" + enviros[j] + "']").text(); - ticks.push(exe + " @ " + env); - } - } - // Add data - for (var b in benchmarks) { - var benchlabel = $("label[for='benchmark_" + benchmarks[b] + "']").text(); - series.push({'label': benchlabel}); - var customdata = []; - var benchcounter = 0; - barcounter = 1; - for (var i in exes) { - for (var j in enviros) { - benchcounter++; - var exe = $("label[for='exe_" + exes[i] + "']").text(); - var env = $("label[for='env_" + enviros[j] + "']").text(); - var val = compdata[exes[i]][enviros[j]][benchmarks[b]]; - if (val !== null) { - if (baseline !== "none") { - var baseval = compdata[baseline][enviros[j]][benchmarks[b]]; - if (baseval === null || baseval === 0) { - var benchlabel = $("label[for='benchmark_" + benchmarks[b] + "']").text(); - var baselinelabel = $("label[for='exe_" + baseline + "']").text(); - var msg = ""+ title + "" + "

"; - msg += "Could not render plot because the chosen baseline has empty results for benchmark " + benchlabel; - return abortRender(plotid, msg); - } else { - baseline_is_empty = false; - val = val / baseval; - } - } - } - if (!horizontal) { - customdata.push(val); - } else { - customdata.push([val, benchcounter]); - } - } - } - plotdata.push(customdata); - } - } else { - // no valid chart type - return false; - } - - if (baseline_is_empty) { - var baselinelabel = $("label[for='exe_" + baseline + "']").text(); - var msg = ""+ title + "" + "

"; - msg += "Could not render plot because the chosen baseline is empty"; - return abortRender(plotid, msg); - } - - // Set plot options and size depending on: - // - Bar orientation (horizontal/vertical) - // - Screen width and number of bars being displayed - var plotwidth = $("#plotwrapper").width(); - var plotheight = $("#" + plotid).height(); - var barWidth = 20; - var w = 0; - var h = 0; - var plotoptions = []; - if (horizontal) { - plotoptions = { - title: title, - seriesDefaults: { - renderer: $.jqplot.BarRenderer, - rendererOptions: {barDirection: "horizontal", barPadding: 8, barMargin: 15} - }, - axesDefaults: { - tickRenderer: $.jqplot.CanvasAxisTickRenderer, - tickOptions: {angle: 0} - }, - axes: { - xaxis: { - min: 0, - autoscale: true, - label: axislabel, - labelRenderer: $.jqplot.CanvasAxisLabelRenderer - }, - yaxis: { - renderer: $.jqplot.CategoryAxisRenderer, - ticks: ticks - } - } - }; - - if (chart === "relative bars") { - plotoptions.axes.xaxis.min = null; - plotoptions.axes.xaxis.tickOptions = {formatString:'%.1fx'}; - } else if (chart ==="stacked bars") { -// plotoptions.axes.xaxis.min = null; - // Not good when there is a 0 bar. It even shows negative bars when all bars are 0 - } - - // Determine optimal height - if (chart ==="stacked bars") { - h = 90 + ticks.length * (plotoptions.seriesDefaults.rendererOptions.barPadding*2 + barWidth); - } else { - h = barcounter * (plotoptions.seriesDefaults.rendererOptions.barPadding*2 + barWidth) + benchcounter * plotoptions.seriesDefaults.rendererOptions.barMargin * 2; - } - // Adjust plot height - if (h > 700) { - h = h/2; - if (h < 700) { h = 700; } - else if (h > 2000) { h = 2000; } - plotoptions.seriesDefaults.rendererOptions.barPadding = 0; - plotoptions.seriesDefaults.rendererOptions.barMargin = 8; - plotoptions.seriesDefaults.shadow = false; - } else if (h < 300) { - h = 300; - plotoptions.seriesDefaults.rendererOptions.barPadding = 14; - plotoptions.seriesDefaults.rendererOptions.barMargin = 25; - } - w = plotwidth; - } else { - plotoptions = { - title: title, - seriesDefaults: { - renderer: $.jqplot.BarRenderer, - rendererOptions: {barDirection: "vertical", barPadding: 6, barMargin: 15} - }, - axesDefaults: { - tickRenderer: $.jqplot.CanvasAxisTickRenderer - }, - axes: { - xaxis: { - renderer: $.jqplot.CategoryAxisRenderer, - ticks: ticks, - tickOptions: {angle: 0} - }, - yaxis: { - min: 0, - autoscale: true,//no effect for some plots due to min = 0 - label: axislabel, - labelRenderer: $.jqplot.CanvasAxisLabelRenderer - } - } - }; - - w = barcounter * (plotoptions.seriesDefaults.rendererOptions.barPadding*2 + barWidth) + benchcounter * plotoptions.seriesDefaults.rendererOptions.barMargin * 2 + 60; - h = plotheight; - // Check if calculated width is greater than actually available width - if (w > plotwidth + 75) { - plotoptions.seriesDefaults.rendererOptions.barPadding = 0; - plotoptions.seriesDefaults.rendererOptions.barMargin = 10; - plotoptions.seriesDefaults.shadow = false; - plotoptions.axes.xaxis.tickOptions.angle = -30; - } - if (w > plotwidth) { - w = plotwidth; - } else if (w < 320) { - w = 320; - plotoptions.seriesDefaults.rendererOptions.barPadding = 15; - plotoptions.seriesDefaults.rendererOptions.barMargin = 25; - } - if (chart === "normal bars" && series.length === 1 && benchmarks.length > 1) { - plotoptions.axes.xaxis.tickOptions.angle = -30; - } else if (chart === "stacked bars") { - plotoptions.axes.xaxis.tickOptions.angle = -60; - plotoptions.seriesDefaults.rendererOptions.barMargin += 5; - $("#" + plotid).css("margin-left", "25px"); - h += 60; - } else if (chart === "relative bars") { - plotoptions.axes.yaxis.min = null; -// plotoptions.axes.yaxis.autoscale = false; //It triggers a bug sometimes - plotoptions.axes.yaxis.tickOptions = {formatString:'%.1fx'}; - plotoptions.axes.xaxis.tickOptions.angle = -30; - } - } - - plotoptions.legend = {show: true, location: 'ne'}; - plotoptions.series = series; - - // determine conditions for rendering the legend outside the plot area - var offplot = false; - if (!horizontal && series.length > 4) { offplot = true; } - else if (horizontal && series.length > 2*ticks.length) { offplot = true; } - - if (offplot) { - // Move legend outside plot area to unclutter - var labels = []; - for (l in series) { - labels.push(series[l].label.length); - } - - var offset = 55 + Math.max.apply( Math, labels ) * 5.4; - plotoptions.legend.xoffset = -offset; - $("#" + plotid).css("margin-right", offset + 10); - if (w + offset > plotwidth) { w = plotwidth - offset -20; } - } else if (!horizontal && ticks.length <= 2) { - plotoptions.legend = {show: true, location: 'se'}; - } - - // Set bar type - if (chart === "stacked bars") { - plotoptions.stackSeries = true; - } else if (chart === "relative bars") { - plotoptions.seriesDefaults.fill = true; - plotoptions.seriesDefaults.fillToZero = true; - plotoptions.seriesDefaults.useNegativeColors = false; - } - - // check that legend does not overflow - if (series.length > 14) { - if (!horizontal || (series.length > ticks.length)) { - var mb = $("#" + plotid).css("margin-bottom").slice(0, -2); - mb = parseInt(mb, 10) + 4 + (series.length - 14) * 22; - $("#" + plotid).css("margin-bottom", mb); - } - } - /* End set plot style */ - - // Render plot - $("#" + plotid).css('width', w); - $("#" + plotid).css('height', h); - $.jqplot(plotid, plotdata, plotoptions); -} diff --git a/codespeed/static/js/comparison.js b/codespeed/static/js/comparison.js index 643f1da0..5b3361d7 100644 --- a/codespeed/static/js/comparison.js +++ b/codespeed/static/js/comparison.js @@ -64,3 +64,314 @@ function savedata(data) { compdata = data; refreshContent(); } + +function abortRender(plotid, message) { + $("#" + plotid).css("border", "dashed 1px grey"); + $("#" + plotid).css("padding", "1em"); + $("#" + plotid).css("width", "400px"); + $("#" + plotid).html(getLoadText(message, 0)); + return -1; +} + +function renderComparisonPlot(plotid, benchmarks, exes, enviros, baseline, chart, horizontal) { + var axislabel = ""; + var title = ""; + var baseline_is_empty = true; + if (baseline === "none") { + baseline_is_empty = false; + if (chart === "stacked bars") { title = "Cumulative "; } + title += unit; + axislabel = bench_units[unit][2] + bench_units[unit][1]; + } else { + if (chart === "stacked bars") { + title = "Cumulative " + unit + " normalized to " + $("label[for='exe_" + baseline + "']").text(); + } else if (chart === "relative bars") { + title = unit + " ratio to " + $("label[for='exe_" + baseline + "']").text(); + } else { + title = unit + " normalized to " + $("label[for='exe_" + baseline + "']").text(); + } + axislabel = "Ratio " + bench_units[unit][1]; + } + + var plotdata = []; + var ticks = []; + var series = []; + var barcounter = 0; + if (chart === "stacked bars" && horizontal) { exes.reverse(); } + if (chart === "normal bars" || chart === "relative bars") { + if (horizontal) { benchmarks.reverse(); } + // Add tick labels + for (var ben in benchmarks) { + var benchlabel = $("label[for='benchmark_" + benchmarks[ben] + "']").text(); + ticks.push(benchlabel); + } + // Add data + for (var i in exes) { + for (var j in enviros) { + var exe = $("label[for='exe_" + exes[i] + "']").text(); + var env = $("label[for='env_" + enviros[j] + "']").text(); + // for relative bars leave out (don't show) the baseline exe + if (chart === "relative bars") { + if (exe === $("label[for='exe_" + baseline + "']").text()) { + continue; + } + } + series.push({'label': exe + " @ " + env}); + var customdata = []; + var benchcounter = 0; + if (baseline !== "none") { + if (chart === "relative bars") { + axislabel = "<- worse - better ->"; + } + } + for (var b in benchmarks) { + benchcounter++; + barcounter++; + var val = compdata[exes[i]][enviros[j]][benchmarks[b]]; + if (val !== null) { + if (baseline !== "none") { + var baseval = compdata[baseline][enviros[j]][benchmarks[b]]; + if (baseval === null || baseval === 0) { + val = 0.0001; + } else { + baseline_is_empty = false; + val = val / baseval; + if (chart === "relative bars" && val > 1) { + val = -val; + } + } + } + } + //Add data + if (!horizontal) { + customdata.push(val); + } else { + customdata.push([val, benchcounter]); + } + } + plotdata.push(customdata); + } + } + } else if (chart === "stacked bars") { + // Add tick labels + for (var i in exes) { + for (var j in enviros) { + var exe = $("label[for='exe_" + exes[i] + "']").text(); + var env = $("label[for='env_" + enviros[j] + "']").text(); + ticks.push(exe + " @ " + env); + } + } + // Add data + for (var b in benchmarks) { + var benchlabel = $("label[for='benchmark_" + benchmarks[b] + "']").text(); + series.push({'label': benchlabel}); + var customdata = []; + var benchcounter = 0; + barcounter = 1; + for (var i in exes) { + for (var j in enviros) { + benchcounter++; + var exe = $("label[for='exe_" + exes[i] + "']").text(); + var env = $("label[for='env_" + enviros[j] + "']").text(); + var val = compdata[exes[i]][enviros[j]][benchmarks[b]]; + if (val !== null) { + if (baseline !== "none") { + var baseval = compdata[baseline][enviros[j]][benchmarks[b]]; + if (baseval === null || baseval === 0) { + var benchlabel = $("label[for='benchmark_" + benchmarks[b] + "']").text(); + var baselinelabel = $("label[for='exe_" + baseline + "']").text(); + var msg = ""+ title + "" + "

"; + msg += "Could not render plot because the chosen baseline has empty results for benchmark " + benchlabel; + return abortRender(plotid, msg); + } else { + baseline_is_empty = false; + val = val / baseval; + } + } + } + if (!horizontal) { + customdata.push(val); + } else { + customdata.push([val, benchcounter]); + } + } + } + plotdata.push(customdata); + } + } else { + // no valid chart type + return false; + } + + if (baseline_is_empty) { + var baselinelabel = $("label[for='exe_" + baseline + "']").text(); + var msg = ""+ title + "" + "

"; + msg += "Could not render plot because the chosen baseline is empty"; + return abortRender(plotid, msg); + } + + // Set plot options and size depending on: + // - Bar orientation (horizontal/vertical) + // - Screen width and number of bars being displayed + var plotwidth = $("#plotwrapper").width(); + var plotheight = $("#" + plotid).height(); + var barWidth = 20; + var w = 0; + var h = 0; + var plotoptions = []; + if (horizontal) { + plotoptions = { + title: title, + seriesDefaults: { + renderer: $.jqplot.BarRenderer, + rendererOptions: {barDirection: "horizontal", barPadding: 8, barMargin: 15} + }, + axesDefaults: { + tickRenderer: $.jqplot.CanvasAxisTickRenderer, + tickOptions: {angle: 0} + }, + axes: { + xaxis: { + min: 0, + autoscale: true, + label: axislabel, + labelRenderer: $.jqplot.CanvasAxisLabelRenderer + }, + yaxis: { + renderer: $.jqplot.CategoryAxisRenderer, + ticks: ticks + } + } + }; + + if (chart === "relative bars") { + plotoptions.axes.xaxis.min = null; + plotoptions.axes.xaxis.tickOptions = {formatString:'%.1fx'}; + } else if (chart ==="stacked bars") { +// plotoptions.axes.xaxis.min = null; + // Not good when there is a 0 bar. It even shows negative bars when all bars are 0 + } + + // Determine optimal height + if (chart ==="stacked bars") { + h = 90 + ticks.length * (plotoptions.seriesDefaults.rendererOptions.barPadding*2 + barWidth); + } else { + h = barcounter * (plotoptions.seriesDefaults.rendererOptions.barPadding*2 + barWidth) + benchcounter * plotoptions.seriesDefaults.rendererOptions.barMargin * 2; + } + // Adjust plot height + if (h > 700) { + h = h/2; + if (h < 700) { h = 700; } + else if (h > 2000) { h = 2000; } + plotoptions.seriesDefaults.rendererOptions.barPadding = 0; + plotoptions.seriesDefaults.rendererOptions.barMargin = 8; + plotoptions.seriesDefaults.shadow = false; + } else if (h < 300) { + h = 300; + plotoptions.seriesDefaults.rendererOptions.barPadding = 14; + plotoptions.seriesDefaults.rendererOptions.barMargin = 25; + } + w = plotwidth; + } else { + plotoptions = { + title: title, + seriesDefaults: { + renderer: $.jqplot.BarRenderer, + rendererOptions: {barDirection: "vertical", barPadding: 6, barMargin: 15} + }, + axesDefaults: { + tickRenderer: $.jqplot.CanvasAxisTickRenderer + }, + axes: { + xaxis: { + renderer: $.jqplot.CategoryAxisRenderer, + ticks: ticks, + tickOptions: {angle: 0} + }, + yaxis: { + min: 0, + autoscale: true,//no effect for some plots due to min = 0 + label: axislabel, + labelRenderer: $.jqplot.CanvasAxisLabelRenderer + } + } + }; + + w = barcounter * (plotoptions.seriesDefaults.rendererOptions.barPadding*2 + barWidth) + benchcounter * plotoptions.seriesDefaults.rendererOptions.barMargin * 2 + 60; + h = plotheight; + // Check if calculated width is greater than actually available width + if (w > plotwidth + 75) { + plotoptions.seriesDefaults.rendererOptions.barPadding = 0; + plotoptions.seriesDefaults.rendererOptions.barMargin = 10; + plotoptions.seriesDefaults.shadow = false; + plotoptions.axes.xaxis.tickOptions.angle = -30; + } + if (w > plotwidth) { + w = plotwidth; + } else if (w < 320) { + w = 320; + plotoptions.seriesDefaults.rendererOptions.barPadding = 15; + plotoptions.seriesDefaults.rendererOptions.barMargin = 25; + } + if (chart === "normal bars" && series.length === 1 && benchmarks.length > 1) { + plotoptions.axes.xaxis.tickOptions.angle = -30; + } else if (chart === "stacked bars") { + plotoptions.axes.xaxis.tickOptions.angle = -60; + plotoptions.seriesDefaults.rendererOptions.barMargin += 5; + $("#" + plotid).css("margin-left", "25px"); + h += 60; + } else if (chart === "relative bars") { + plotoptions.axes.yaxis.min = null; +// plotoptions.axes.yaxis.autoscale = false; //It triggers a bug sometimes + plotoptions.axes.yaxis.tickOptions = {formatString:'%.1fx'}; + plotoptions.axes.xaxis.tickOptions.angle = -30; + } + } + + plotoptions.legend = {show: true, location: 'ne'}; + plotoptions.series = series; + + // determine conditions for rendering the legend outside the plot area + var offplot = false; + if (!horizontal && series.length > 4) { offplot = true; } + else if (horizontal && series.length > 2*ticks.length) { offplot = true; } + + if (offplot) { + // Move legend outside plot area to unclutter + var labels = []; + for (l in series) { + labels.push(series[l].label.length); + } + + var offset = 55 + Math.max.apply( Math, labels ) * 5.4; + plotoptions.legend.xoffset = -offset; + $("#" + plotid).css("margin-right", offset + 10); + if (w + offset > plotwidth) { w = plotwidth - offset -20; } + } else if (!horizontal && ticks.length <= 2) { + plotoptions.legend = {show: true, location: 'se'}; + } + + // Set bar type + if (chart === "stacked bars") { + plotoptions.stackSeries = true; + } else if (chart === "relative bars") { + plotoptions.seriesDefaults.fill = true; + plotoptions.seriesDefaults.fillToZero = true; + plotoptions.seriesDefaults.useNegativeColors = false; + } + + // check that legend does not overflow + if (series.length > 14) { + if (!horizontal || (series.length > ticks.length)) { + var mb = $("#" + plotid).css("margin-bottom").slice(0, -2); + mb = parseInt(mb, 10) + 4 + (series.length - 14) * 22; + $("#" + plotid).css("margin-bottom", mb); + } + } + /* End set plot style */ + + // Render plot + $("#" + plotid).css('width', w); + $("#" + plotid).css('height', h); + $.jqplot(plotid, plotdata, plotoptions); +} From 02de62c735550497d97c595269cc08bb7453dc70 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Wed, 7 Dec 2011 20:00:20 +0100 Subject: [PATCH 040/429] Pass raw data to changes.js and let js do the work JavaScript in the template only passes defaults and configuration. --- codespeed/static/js/changes.js | 75 ++++++++++++++++------ codespeed/templates/codespeed/changes.html | 38 +++++------ codespeed/views.py | 8 ++- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js index 292bc8c0..e25c99c6 100644 --- a/codespeed/static/js/changes.js +++ b/codespeed/static/js/changes.js @@ -1,3 +1,5 @@ +var currentproject, changethres, trendthres, projectmatrix, revisionboxes = {}; + function getConfiguration() { return { tre: $("#trend option:selected").val(), @@ -8,15 +10,14 @@ function getConfiguration() { } function permalinkToTimeline(benchmark, environment) { - window.location=timeline_url + "?ben=" + benchmark + "&env=" + environment; + window.location=TIMELINE_URL + "?ben=" + benchmark + "&env=" + environment; } //colors number based on a threshold function getColorcode(change, theigh, tlow) { - var colorcode = "status-none"; - if(change < tlow) { colorcode = "status-red"; } - else if(change > theigh) { colorcode = "status-green"; } - return colorcode; + if (change < tlow) { return "status-red"; } + else if (change > theigh) { return "status-green"; } + else { return "status-node"; } } function colorTable() { @@ -46,19 +47,18 @@ function colorTable() { function updateTable() { colorTable(); - //Add permalink events to table rows - $(".tablesorter > tbody > tr").each(function() { - $(this).click(function () { + + $(".tablesorter > tbody") + //Add permalink events to table rows + .on("click", "tr", function() { var environment = $("input[name='environment']:checked").val(); permalinkToTimeline($(this).children("td:eq(0)").text(), environment); + }) + //Add hover effect to rows + .on("hover", "tr", function() { + $(this).toggleClass("highlight"); }); - }); - //Add hover effect to rows - $(".tablesorter > tbody > tr > td").hover(function() { - $(this).parents('tr').addClass('highlight'); - }, function() { - $(this).parents('tr').removeClass('highlight'); - }); + //Configure table as tablesorter $(".tablesorter").tablesorter({widgets: ['zebra']}); } @@ -68,17 +68,19 @@ function refreshContent() { $("#contentwrap").fadeOut("fast", function() { $(this).show(); $(this).html(getLoadText("Loading...", h, true)); - $(this).load("table/", $.param(getConfiguration()), function(responseText) { updateTable(); }); + $(this).load("table/", $.param(getConfiguration()), function() { updateTable(); }); }); } -function changerevisions() { +function changeRevisions() { // This function repopulates the revision selectbox everytime a new // executable is selected that corresponds to a different project. - var executable = $("input[name='executable']:checked").val(); - if (projectmatrix[executable] !== currentproject) { - $("#revision").html(revisionboxes[projectmatrix[executable]]); - currentproject = projectmatrix[executable]; + var executable = $("input[name='executable']:checked").val(), + selected_project = projectmatrix[executable]; + + if (selected_project !== currentproject) { + $("#revision").html(revisionboxes[selected_project]); + currentproject = selected_project; //Give visual cue that the select box has changed var bgc = $("#revision").parent().parent().css("backgroundColor"); @@ -90,3 +92,34 @@ function changerevisions() { } refreshContent(); } + +function config(c) { + changethres = c.changethres; + trendthres = c.trendthres; +} + +function init(defaults) { + currentproject = defaults.project; + projectmatrix = defaults.projectmatrix; + + $.each(defaults.revisionlists, function(project, revs) { + var options = ""; + $.each(revs, function(index, r) { + options += ""; + }); + revisionboxes[project] = options; + }); + + $("#trend").val(defaults.trend); + $("#trend").change(refreshContent); + + $("#executable" + defaults.executable).attr('checked', true); + $("input[name='executable']").change(changeRevisions); + + $("#env" + defaults.environment).attr('checked', true); + $("input[name='environment']").change(refreshContent); + + $("#revision").html(revisionboxes[defaults.project]); + $("#revision").val(defaults.revision); + $("#revision").change(refreshContent); +} diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index 472dc87a..278d932e 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -6,30 +6,24 @@ - - - -{% endblock %} {% block navigation %} {{ block.super }} @@ -89,3 +60,33 @@
{% endblock %} + +{% block extra_body %} +{{ block.super }} + + + + +{% endblock %} diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 82d18a7d..bb36b878 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -1,9 +1,85 @@ {% extends "base.html" %} {% block title %}{{ block.super }}: Comparison{% endblock %} + {% block extra_head %} {{ block.super }} - +{% endblock %} + +{% block navigation %} + {{ block.super }} +{% endblock %} +{% block nav-comparison %}class="current">Comparison{% endblock %} + +{% block body %} + + +
+ Chart type: + + + Normalization: + + + + + + + +
+
+
+
+{% endblock %} + +{% block extra_body %} +{{ block.super }} + @@ -76,73 +152,3 @@ }); {% endblock %} -{% block navigation %} - {{ block.super }} -{% endblock %} -{% block nav-comparison %}class="current">Comparison{% endblock %} - -{% block body %} - - -
- Chart type: - - - Normalization: - - - - - - - -
-
-
-
-{% endblock %} diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 35162c14..abfb5192 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -1,9 +1,98 @@ {% extends "base.html" %} {% block title %}{{ block.super }}: Timeline{% endblock %} + {% block extra_head %} + +{% endblock %} + +{% block navigation %} + {{ block.super }} +{% endblock %} +{% block nav-timeline %}class="current">Timeline{% endblock %} + +{% block body %} + + +
+ + Show the last + results + + + + + + +
+
+
+
+
+{% endblock %} + +{% block extra_body %} {{ block.super }} - @@ -113,87 +202,3 @@ {% endblock %} -{% block navigation %} - {{ block.super }} -{% endblock %} -{% block nav-timeline %}class="current">Timeline{% endblock %} - -{% block body %} - - -
- - Show the last - results - - - - - - -
-
-
-
-
-{% endblock %} diff --git a/example/templates/base.html b/example/templates/base.html index 068276f1..1f202d7d 100644 --- a/example/templates/base.html +++ b/example/templates/base.html @@ -6,11 +6,6 @@ - - - {% block extra_head %}{% endblock %} @@ -52,5 +47,12 @@

{% block page_title %}SPEED CENTER{% endblock page_title %}

{% endblock %} + + + + + {% block extra_body %}{% endblock %} diff --git a/example/templates/home.html b/example/templates/home.html index 8860d2bd..80bcc4f1 100644 --- a/example/templates/home.html +++ b/example/templates/home.html @@ -1,6 +1,34 @@ {% extends "base.html" %} -{% block extra_head %} +{% block navigation %} +{% endblock navigation %} + +{% block body %} + +{% endblock body %} + +{% block extra_body %} {{ block.super }} {% endblock %} -{% block navigation %} -{% endblock navigation %} - -{% block body %} - -{% endblock body %} From 54d1642fea70699aa89c0b606431718a9d041726 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sun, 18 Dec 2011 14:02:39 +0100 Subject: [PATCH 104/429] Remove unused jQuery UI in changes page --- codespeed/templates/codespeed/changes.html | 1 - 1 file changed, 1 deletion(-) diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index a6a5887a..e38c09d2 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -65,7 +65,6 @@ {{ block.super }} - {% endblock %} From 2fb54ffaf60b1449c1f63d6832d152e05087990a Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sun, 18 Dec 2011 14:52:34 +0100 Subject: [PATCH 106/429] Move checkall/uncheckall code into codespeed.js --- codespeed/static/js/codespeed.js | 19 +++++++++++++++++++ codespeed/static/js/comparison.js | 19 +------------------ codespeed/templates/codespeed/timeline.html | 16 +--------------- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/codespeed/static/js/codespeed.js b/codespeed/static/js/codespeed.js index 55c788c4..c7896a50 100644 --- a/codespeed/static/js/codespeed.js +++ b/codespeed/static/js/codespeed.js @@ -30,3 +30,22 @@ function getLoadText(text, h, showloader) { loadtext += '

'; return loadtext; } + +$(function() { + // Check all and none links + $('.checkall').each(function() { + var inputs = $(this).parent().children("li").children("input"); + $(this).click(function() { + inputs.attr("checked", true); + return false; + }); + }); + + $('.uncheckall').each(function() { + var inputs = $(this).parent().children("li").children("input"); + $(this).click(function() { + inputs.attr("checked", false); + return false; + }); + }); +}); diff --git a/codespeed/static/js/comparison.js b/codespeed/static/js/comparison.js index eac79560..7e7eca63 100644 --- a/codespeed/static/js/comparison.js +++ b/codespeed/static/js/comparison.js @@ -404,24 +404,7 @@ function init(defaults) { $("#chart_type, #baseline, #direction, input[name='executables']," + "input[name='benchmarks'], input[name='environments']").change(refreshContent); - // Check all and none links - $('.checkall').each(function() { - var inputs = $(this).parent().children("li").children("input"); - $(this).click(function() { - inputs.attr("checked", true); - refreshContent(); - return false; - }); - }); - - $('.uncheckall').each(function() { - var inputs = $(this).parent().children("li").children("input"); - $(this).click(function() { - inputs.attr("checked", false); - refreshContent(); - return false; - }); - }); + $('.checkall, .uncheckall').click(refreshContent); $.ajaxSetup ({ cache: false diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index abfb5192..fb89e1ee 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -181,22 +181,8 @@ // Init and change handlers are set to the refreshContent handler $.address.init(initializeSite).change(refreshSite); - // Check all and none links - $('.checkall').click(function() { - $(this).parent().children("li").children("input").each(function() { - $(this).attr('checked', true); - }); - refreshContent(); - return false; - }); + $('.checkall, .uncheckall').click(refreshContent); - $('.uncheckall').click(function() { - $(this).parent().children("li").children("input").each(function() { - $(this).attr('checked', false); - }); - refreshContent(); - return false; - }); setExeColors(); }); From c039a746614b1f9bbf6b802d8adeb5b38b36d12a Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sun, 18 Dec 2011 15:16:26 +0100 Subject: [PATCH 107/429] Separated JS code from template on timeline page --- codespeed/static/js/timeline.js | 73 ++++++++++++++++- codespeed/templates/codespeed/timeline.html | 86 +++------------------ 2 files changed, 81 insertions(+), 78 deletions(-) diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index ece04143..68d4d733 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -1,3 +1,8 @@ +var seriesindex = [], + baselineColor = "#d8b83f", + seriesColors = ["#4bb2c5", "#EAA228", "#579575", "#953579", "#839557", "#ff5800", "#958c12", "#4b5de4", "#0085cc"], + defaults; + function setExeColors() { // Set color data attribute for all executables $("#executable > div.boxbody > ul > ul > li > input").each(function(index) { @@ -36,7 +41,7 @@ function getConfiguration() { } function permalinkToChanges(commitid, executableid, environment) { - window.location=changes_url + "?rev=" + commitid + "&" + "exe=" + executableid + "&env=" + environment; + window.location=CHANGES_URL + "?rev=" + commitid + "&" + "exe=" + executableid + "&env=" + environment; } function OnMarkerClickHandler(ev, gridpos, datapos, neighbor, plot) { @@ -248,3 +253,69 @@ function refreshSite(event) { setValuesOfInputFields(event); refreshContent(); } + +function setValuesOfInputFields(event) { + // Either set the default value, or the one parsed from the url + + // Reset all checkboxes + $("input:checkbox").removeAttr('checked'); + + $("#revisions").val(valueOrDefault(event.parameters.revs, defaults.revisions)); + $("#baseline").val(valueOrDefault(event.parameters.base, defaults.baseline)); + + // Set default selected executables + var executables = event.parameters.exe ? event.parameters.exe.split(',') : defaults.executables; + var sel = $("input[name='executable']"); + + $.each(executables, function(i, exe) { + sel.filter("[value='" + exe + "']").attr('checked', true); + }); + + // Set default selected branches + var branches = event.parameters.bran ? event.parameters.bran.split(',') : defaults.branches; + sel = $("input[name='branch']"); + + $.each(branches, function(i, b) { + sel.filter("[value='" + b + "']").attr('checked', true); + }); + + // Set default selected benchmark + var benchmark = valueOrDefault(event.parameters.ben, defaults.benchmark); + $("input:radio[name='benchmark']") + .filter("[value='" + benchmark + "']") + .attr('checked', true); + + // Set default selected environment + var environment = valueOrDefault(event.parameters.env, defaults.environment); + $("input:radio[name='environments']") + .filter("[value='" + environment + "']") + .attr('checked', true); + + // Add color legend to executable list + $("#executable div.boxbody > ul > ul > li > input").each(function() { + $(this).parent() + .find("div.seriescolor") + .css("background-color", getColor($(this).attr("id").slice(10))); + }); + + $("#baselinecolor").css("background-color", baselineColor); + $("#equidistant").attr('checked', valueOrDefault(event.parameters.equid, defaults.equidistant === "on")); +} + +function init(def) { + defaults = def; + + $.ajaxSetup ({ + cache: false + }); + + // Even listener for clicks on plot markers + $.jqplot.eventListenerHooks.push(['jqplotClick', OnMarkerClickHandler]); + + // Init and change handlers are set to the refreshContent handler + $.address.init(initializeSite).change(refreshSite); + + $('.checkall, .uncheckall').click(refreshContent); + + setExeColors(); +} diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index fb89e1ee..aae3fd07 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -104,86 +104,18 @@ {% endblock %} From 9c72c90f9586386ffc22771c38b93236b21b7288 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sun, 18 Dec 2011 17:29:26 +0100 Subject: [PATCH 108/429] Let page specific javascript handle the permalink Global code (codespeed.js) using functions from site specific javascript (e.g. changes.js) makes it hard to put this code into a closure, which is important to keep things clean and separated. It also doesn't help understanding the code. --- codespeed/static/js/changes.js | 4 ++++ codespeed/static/js/codespeed.js | 5 ----- codespeed/static/js/comparison.js | 4 ++++ codespeed/static/js/timeline.js | 4 ++++ codespeed/templates/codespeed/changes.html | 2 +- codespeed/templates/codespeed/comparison.html | 2 +- codespeed/templates/codespeed/timeline.html | 2 +- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js index e25c99c6..53ec8865 100644 --- a/codespeed/static/js/changes.js +++ b/codespeed/static/js/changes.js @@ -122,4 +122,8 @@ function init(defaults) { $("#revision").html(revisionboxes[defaults.project]); $("#revision").val(defaults.revision); $("#revision").change(refreshContent); + + $("#permalink").click(function() { + window.location = "?" + $.param(getConfiguration()); + }); } diff --git a/codespeed/static/js/codespeed.js b/codespeed/static/js/codespeed.js index c7896a50..bba35d6e 100644 --- a/codespeed/static/js/codespeed.js +++ b/codespeed/static/js/codespeed.js @@ -1,8 +1,3 @@ - -function permalink() { - window.location="?" + $.param(getConfiguration()); -} - function readCheckbox(el) { /* Builds a string that holds all checked values in an input form */ var config = ""; diff --git a/codespeed/static/js/comparison.js b/codespeed/static/js/comparison.js index 7e7eca63..e7e06505 100644 --- a/codespeed/static/js/comparison.js +++ b/codespeed/static/js/comparison.js @@ -414,4 +414,8 @@ function init(defaults) { var h = $("#content").height();//get height for loading text $("#cplot").html(getLoadText("Loading...", h, true)); $.getJSON("json/", savedata); + + $("#permalink").click(function() { + window.location = "?" + $.param(getConfiguration()); + }); } diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index 68d4d733..eb167c55 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -318,4 +318,8 @@ function init(def) { $('.checkall, .uncheckall').click(refreshContent); setExeColors(); + + $("#permalink").click(function() { + window.location = "?" + $.param(getConfiguration()); + }); } diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index e38c09d2..e4561829 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -54,7 +54,7 @@
Results for revision - +
diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 998ad952..0c9c85df 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -70,7 +70,7 @@ - +
diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index aae3fd07..fb594a1a 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -82,7 +82,7 @@ - +
From fbe9e779d706f04790d09994ebffae228580f012 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sun, 18 Dec 2011 17:56:50 +0100 Subject: [PATCH 109/429] Put javascript code into closures (module pattern) --- codespeed/static/js/changes.js | 14 ++++++++++++++ codespeed/static/js/comparison.js | 11 +++++++++++ codespeed/static/js/timeline.js | 12 ++++++++++++ codespeed/templates/codespeed/changes.html | 15 ++++++--------- codespeed/templates/codespeed/comparison.html | 2 +- codespeed/templates/codespeed/timeline.html | 2 +- 6 files changed, 45 insertions(+), 11 deletions(-) diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js index 53ec8865..4e39a817 100644 --- a/codespeed/static/js/changes.js +++ b/codespeed/static/js/changes.js @@ -1,3 +1,8 @@ +var Changes = (function(window){ + +// Localize globals +var TIMELINE_URL = window.TIMELINE_URL, getLoadText = window.getLoadText; + var currentproject, changethres, trendthres, projectmatrix, revisionboxes = {}; function getConfiguration() { @@ -126,4 +131,13 @@ function init(defaults) { $("#permalink").click(function() { window.location = "?" + $.param(getConfiguration()); }); + + refreshContent(); } + +return { + init: init, + config: config +}; + +})(window); diff --git a/codespeed/static/js/comparison.js b/codespeed/static/js/comparison.js index e7e06505..fc340dc0 100644 --- a/codespeed/static/js/comparison.js +++ b/codespeed/static/js/comparison.js @@ -1,3 +1,8 @@ +var Comparison = (function(window){ + +// Localize globals +var readCheckbox = window.readCheckbox, getLoadText = window.getLoadText; + var compdata, bench_units; function getConfiguration() { @@ -419,3 +424,9 @@ function init(defaults) { window.location = "?" + $.param(getConfiguration()); }); } + +return { + init: init +}; + +})(window); diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index eb167c55..dddd318d 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -1,3 +1,9 @@ +var Timeline = (function(window){ + +// Localize globals +var CHANGES_URL = window.CHANGES_URL, readCheckbox = window.readCheckbox, + getLoadText = window.getLoadText; + var seriesindex = [], baselineColor = "#d8b83f", seriesColors = ["#4bb2c5", "#EAA228", "#579575", "#953579", "#839557", "#ff5800", "#958c12", "#4b5de4", "#0085cc"], @@ -323,3 +329,9 @@ function init(def) { window.location = "?" + $.param(getConfiguration()); }); } + +return { + init: init +}; + +})(window); diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index e4561829..724e2682 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -69,8 +69,12 @@ var TIMELINE_URL = "{% url timeline %}"; $(function() { - // Configure defaults - init({ + Changes.config({ + changethres: {{ defaultchangethres }}, + trendthres: {{ defaulttrendthres }} + }); + + Changes.init({ project: "{{ defaultexecutable.project }}", executable: "{{ defaultexecutable.id }}", environment: "{{ defaultenvironment.id }}", @@ -79,13 +83,6 @@ projectmatrix: eval({{ projectmatrix|safe }}), revisionlists: eval({{ revisionlists|safe }}) }); - - config({ - changethres: {{ defaultchangethres }}, - trendthres: {{ defaulttrendthres }} - }); - - refreshContent(); }); {% endblock %} diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 0c9c85df..56147147 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -90,7 +90,7 @@ From 4d3bea3cffe4edcd7d70fc46c5e19474cc4bd012 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 20 Jan 2012 00:25:59 +0100 Subject: [PATCH 112/429] Added place holders for missing tests, removed one pointless unittest --- codespeed/api.py | 2 +- codespeed/tests/tests_api.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 32aadd4b..840bc4b8 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -292,7 +292,7 @@ def _check_data(self): )) # check optional data for key in [k for k in self.optional_keys \ - if k not in ('date')]: + if k not in ('date',)]: if key in self.data.keys(): try: self.data[key] = float(self.data[key]) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 6f789911..ad7a796f 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -259,6 +259,31 @@ def test_nonexistent(self): self.assertEquals(response.status_code, 404) +class BranchTest(FixtureTestCase): + """Test Branch() API""" + pass + + +class RevisionTest(FixtureTestCase): + """Test Branch() API""" + pass + + +class ExecutableTest(FixtureTestCase): + """Test Branch() API""" + pass + + +class BenchmarkTest(FixtureTestCase): + """Test Branch() API""" + pass + + +class ReportTest(FixtureTestCase): + """Test Branch() API""" + pass + + class UserTest(FixtureTestCase): """Test api user related stuff""" @@ -412,16 +437,6 @@ def test_non_exiting_items(self): self.assertEqual(bundle.obj.project.name, modified_data['project']) - def test_exiting_executable(self): - """ - Check that an exception is raised if the executable already exists but - the project changes. - """ - modified_data = copy.deepcopy(self.data1) - modified_data['commitid'] = '0b31bf33a469ac2cb1949666eea54d69a36c3724' - modified_data['project'] = 'Cython' - bundle = ResultBundle(**modified_data) - self.assertRaises(ImmediateHttpResponse, bundle.save) class ResultBundleResourceTestCase(FixtureTestCase): """Submitting new benchmark results""" From 2a6306432e973cdcfd324e81169bb8029d47b736 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 20 Jan 2012 10:39:09 +0100 Subject: [PATCH 113/429] added Branch:Project relation to api, added test for api.Branch --- codespeed/api.py | 2 + codespeed/tests/tests_api.py | 79 +++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index 840bc4b8..54ec0d97 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -78,6 +78,8 @@ class Meta: class BranchResource(ModelResource): """Ressource for Branch()""" + project = fields.ToOneField(ProjectResource, 'project') + class Meta: queryset = Branch.objects.all() authorization= Authorization() diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index ad7a796f..700f7602 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -261,7 +261,84 @@ def test_nonexistent(self): class BranchTest(FixtureTestCase): """Test Branch() API""" - pass + + def setUp(self): + self.branch1 = Branch.objects.get(pk=1) + self.project_data = dict( + name="PyPy", + repo_type="M", + repo_path="ssh://hg@bitbucket.org/pypy/pypy", + repo_user="fridolin", + repo_pass="secret", + ) + self.project = Project(**self.project_data) + self.project.save() + self.branch2_data = dict( + name="master2", + project='/api/v1/project/{0}/'.format(self.project.id) + ) + self.client = Client() + super(BranchTest, self).setUp() + + def test_get_branch(self): + """Should get an existing branch""" + response = self.client.get('/api/v1/branch/1/') + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['name'], "default") + self.assertEqual(json.loads(response.content)['project'], + "/api/v1/project/1/") + + def test_get_branch_all_fields(self): + """Should get all fields for an branch""" + response = self.client.get('/api/v1/branch/%s/' % (self.branch1.id,)) + self.assertEquals(response.status_code, 200) + self.assertEquals(json.loads(response.content)['name'], + self.branch1.name) + self.assertEquals(json.loads(response.content)['project'], + '/api/v1/project/1/') + self.assertEquals(json.loads(response.content)['resource_uri'], + '/api/v1/branch/%s/' %(self.branch1.id,)) + + def test_post(self): + """Should save a new branch""" + modified_data = copy.deepcopy(self.branch2_data) + response = self.client.post('/api/v1/branch/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 201) + id = response['Location'].rsplit('/', 2)[-2] + response = self.client.get('/api/v1/branch/{0}/'.format(id)) + for k, v in self.branch2_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + response = self.client.delete('/api/v1/branch/{0}/'.format(id), + content_type='application/json') + self.assertEquals(response.status_code, 204) + + def test_put(self): + """Should modify an existing environment""" + modified_data = copy.deepcopy(self.branch2_data) + modified_data['name'] = "tip" + response = self.client.put('/api/v1/branch/1/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 204) + response = self.client.get('/api/v1/branch/1/') + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_delete(self): + """Should delete a branch""" + response = self.client.get('/api/v1/branch/1/') + self.assertEquals(response.status_code, 200) + # from fixture + response = self.client.delete('/api/v1/branch/1/', + content_type='application/json') + self.assertEquals(response.status_code, 204) + + response = self.client.get('/api/v1/branch/1/') + self.assertEquals(response.status_code, 404) class RevisionTest(FixtureTestCase): From 2253ec8f0eee183be07a1ebaacf1ce48d1f2fdcd Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sat, 10 Dec 2011 14:04:45 +0100 Subject: [PATCH 114/429] Remove github-only changeset links on changes page Will be replaced by a more generic solution. --- codespeed/github.py | 1 - codespeed/templates/codespeed/changes_logs.html | 8 -------- 2 files changed, 9 deletions(-) diff --git a/codespeed/github.py b/codespeed/github.py index 004b77b6..ba93fe93 100644 --- a/codespeed/github.py +++ b/codespeed/github.py @@ -78,7 +78,6 @@ def retrieve_revision(commit_id, username, project, revision = None): 'author_email': commit['author']['email'], 'commitid': commit['id'], 'short_commit_id': commit['id'][0:7], - 'links': {'Github': 'http://github.com%s' % commit['url']}, 'parents': commit['parents']} def getlogs(endrev, startrev): diff --git a/codespeed/templates/codespeed/changes_logs.html b/codespeed/templates/codespeed/changes_logs.html index 95c45c50..db1a02c9 100644 --- a/codespeed/templates/codespeed/changes_logs.html +++ b/codespeed/templates/codespeed/changes_logs.html @@ -5,14 +5,6 @@ {{ log.date }} - {% if log.links %} -
    - {% for title, href in log.links.items %} -
  • {{ title }}
  • - {% endfor %} -
- {% endif %} - {% if log.author_email and show_email_address %} {{ log.author }} {% else %} From d4ce66e266f7d4a6b54834ff9f454856cf99d484 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sat, 3 Dec 2011 18:38:15 +0100 Subject: [PATCH 115/429] Link revisions on changes page, #35 The `commit_browsing_url` setting on a project allows to set a URL that will be used to link revisions on the changes page. Placeholders like {commitid} will be replaced by the appropriate information from the change log. Examples: https://github.com/tobami/codespeed/commit/{commitid} https://bitbucket.org/pypy/pypy/changeset/{commitid} --- ...__add_field_project_commit_browsing_url.py | 102 ++++++++++++++++++ codespeed/models.py | 4 + .../templates/codespeed/changes_logs.html | 6 +- .../templates/codespeed/changes_table.html | 5 +- codespeed/views.py | 5 + 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 codespeed/migrations/0011_auto__add_field_project_commit_browsing_url.py diff --git a/codespeed/migrations/0011_auto__add_field_project_commit_browsing_url.py b/codespeed/migrations/0011_auto__add_field_project_commit_browsing_url.py new file mode 100644 index 00000000..70d60277 --- /dev/null +++ b/codespeed/migrations/0011_auto__add_field_project_commit_browsing_url.py @@ -0,0 +1,102 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Project.commit_browsing_url' + db.add_column('codespeed_project', 'commit_browsing_url', self.gf('django.db.models.fields.CharField')(default='', max_length=200, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Project.commit_browsing_url' + db.delete_column('codespeed_project', 'commit_browsing_url') + + + models = { + 'codespeed.benchmark': { + 'Meta': {'object_name': 'Benchmark'}, + 'benchmark_type': ('django.db.models.fields.CharField', [], {'default': "'C'", 'max_length': '1'}), + 'default_on_comparison': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lessisbetter': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'units': ('django.db.models.fields.CharField', [], {'default': "'seconds'", 'max_length': '20'}), + 'units_title': ('django.db.models.fields.CharField', [], {'default': "'Time'", 'max_length': '30'}) + }, + 'codespeed.branch': { + 'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'Branch'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'branches'", 'to': "orm['codespeed.Project']"}) + }, + 'codespeed.environment': { + 'Meta': {'object_name': 'Environment'}, + 'cpu': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kernel': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'memory': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'os': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}) + }, + 'codespeed.executable': { + 'Meta': {'object_name': 'Executable'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'executables'", 'to': "orm['codespeed.Project']"}) + }, + 'codespeed.project': { + 'Meta': {'object_name': 'Project'}, + 'commit_browsing_url': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'repo_pass': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'repo_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'repo_type': ('django.db.models.fields.CharField', [], {'default': "'N'", 'max_length': '1'}), + 'repo_user': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'track': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'codespeed.report': { + 'Meta': {'unique_together': "(('revision', 'executable', 'environment'),)", 'object_name': 'Report'}, + '_tablecache': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'colorcode': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '10'}), + 'environment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reports'", 'to': "orm['codespeed.Environment']"}), + 'executable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reports'", 'to': "orm['codespeed.Executable']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'revision': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reports'", 'to': "orm['codespeed.Revision']"}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}) + }, + 'codespeed.result': { + 'Meta': {'unique_together': "(('revision', 'executable', 'benchmark', 'environment'),)", 'object_name': 'Result'}, + 'benchmark': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Benchmark']"}), + 'date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'environment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Environment']"}), + 'executable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Executable']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'revision': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Revision']"}), + 'std_dev': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'val_max': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'val_min': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'value': ('django.db.models.fields.FloatField', [], {}) + }, + 'codespeed.revision': { + 'Meta': {'unique_together': "(('commitid', 'branch'),)", 'object_name': 'Revision'}, + 'author': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'branch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['codespeed.Branch']"}), + 'commitid': ('django.db.models.fields.CharField', [], {'max_length': '42'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['codespeed.Project']"}), + 'tag': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}) + } + } + + complete_apps = ['codespeed'] diff --git a/codespeed/models.py b/codespeed/models.py index c2009a13..91f06482 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -24,6 +24,7 @@ class Project(models.Model): repo_path = models.CharField("Repository URL", blank=True, max_length=200) repo_user = models.CharField("Repository username", blank=True, max_length=100) repo_pass = models.CharField("Repository password", blank=True, max_length=100) + commit_browsing_url = models.CharField("Commit browsing URL", blank=True, max_length=200) track = models.BooleanField("Track changes", default=False) def __unicode__(self): @@ -73,6 +74,9 @@ class Revision(models.Model): def get_short_commitid(self): return self.commitid[:10] + def get_browsing_url(self): + return self.branch.project.commit_browsing_url.format(**self.__dict__) + def __unicode__(self): if self.date is None: date = None diff --git a/codespeed/templates/codespeed/changes_logs.html b/codespeed/templates/codespeed/changes_logs.html index db1a02c9..1c1ad758 100644 --- a/codespeed/templates/codespeed/changes_logs.html +++ b/codespeed/templates/codespeed/changes_logs.html @@ -11,7 +11,11 @@ {{ log.author }} {% endif %} commited - {{ log.short_commit_id|default:log.commitid }}: + {% if log.commit_browse_url %} + {{ log.short_commit_id|default:log.commitid }}: + {% else %} + {{ log.short_commit_id|default:log.commitid }}: + {% endif %}
{{ log.message|linebreaksbr|urlize }}
{% if log.body %} diff --git a/codespeed/templates/codespeed/changes_table.html b/codespeed/templates/codespeed/changes_table.html index 5b0eafde..50399982 100644 --- a/codespeed/templates/codespeed/changes_table.html +++ b/codespeed/templates/codespeed/changes_table.html @@ -74,7 +74,10 @@ - Commit{{ rev.commitid }} + + Commit + {% if rev.get_browsing_url %}{{ rev.commitid }}{% else %}{{ rev.commitid }}{% endif %} + Date{{ rev.date }} {% ifnotequal rev.branch.project.repo_type "N" %} Repo{{ rev.branch.project.repo_path }} diff --git a/codespeed/views.py b/codespeed/views.py index 9878f0e2..31e019ed 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -736,6 +736,11 @@ def displaylogs(request): logger.error("Unhandled exception displaying logs for %s: %s", rev, e, exc_info=True) error = repr(e) + # add commit browsing url to logs + project = rev.branch.project + for log in logs: + log['commit_browse_url'] = project.commit_browsing_url.format(**log) + return render_to_response('codespeed/changes_logs.html', {'error': error, 'logs': logs, 'show_email_address': settings.SHOW_AUTHOR_EMAIL_ADDRESS}, From 945c11ba5470066004a1608b9ae33109b046aa65 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Sun, 22 Jan 2012 19:40:36 +0100 Subject: [PATCH 116/429] Provide a default for commit browsing url in github repositories --- codespeed/models.py | 14 ++++++++++++++ codespeed/tests/tests.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/codespeed/models.py b/codespeed/models.py index 91f06482..e6ab79a9 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -8,6 +8,8 @@ from django.conf import settings +from codespeed.github import GITHUB_URL_RE + class Project(models.Model): REPO_TYPES = ( @@ -48,6 +50,18 @@ def working_copy(self): return os.path.join(settings.REPOSITORY_BASE_PATH, self.repo_name) + def save(self, *args, **kwargs): + """Provide a default for commit browsing url in github repositories.""" + if not self.commit_browsing_url and self.repo_type == 'H': + m = GITHUB_URL_RE.match(self.repo_path) + if m: + url = 'https://github.com/%s/%s/commit/{commitid}' % ( + m.group('username'), m.group('project') + ) + self.commit_browsing_url = url + + super(Project, self).save(*args, **kwargs) + class Branch(models.Model): name = models.CharField(max_length=20) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index 6af7be6a..b99a312d 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -431,3 +431,24 @@ def test_working_copy(self): self.assertRaises( AttributeError, getattr, self.github_project, 'working_copy') + + def test_github_browsing_url(self): + """If empty, the commit browsing url will be filled in with a default + value when using github repository. + """ + + # It should work with https:// as well as git:// urls + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://github.com/tobami/codespeed/commit/{commitid}') + + self.github_project.repo_path = 'git://github.com/tobami/codespeed.git' + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://github.com/tobami/codespeed/commit/{commitid}') + + # If filled in, commit browsing url should not change + self.github_project.commit_browsing_url = 'https://example.com/{commitid}' + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://example.com/{commitid}') From 22c8f03ace1dec642d9809a422902f07585d0f2e Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 23 Jan 2012 10:46:44 +0100 Subject: [PATCH 117/429] Be verbose about what key is missing from settings --- codespeed/tests/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index b99a312d..663f3d44 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -367,7 +367,8 @@ def test_keys_in_settings(self): in django.conf.settings """ for k in self.cs_setting_keys: - self.assertTrue(hasattr(settings, k)) + self.assertTrue(hasattr(settings, k), + "Key {0} is missing in settings.py.".format(k)) def test_settings_attributes(self): """Check if all settings from codespeed.settings equals From 7b7c1570df00ab40c1e8cdda3be71ef57f3fc304 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 23 Jan 2012 10:51:55 +0100 Subject: [PATCH 118/429] fixed api delete test - Note, that test is already improved in the add_api branch --- codespeed/tests/tests_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index bf022cc5..94c0d003 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -96,12 +96,12 @@ def test_put(self): def test_delete(self): """Should delete an environment""" - response = self.client.delete('/api/v1/environment/3/', + response = self.client.delete('/api/v1/environment/1/', content_type='application/json') - self.assertEquals(response.status_code, 410) + self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/environment/3/') - self.assertEquals(response.status_code, 410) + response = self.client.get('/api/v1/environment/1/') + self.assertEquals(response.status_code, 404) class UserTest(FixtureTestCase): From 86d952c2f0eaf675129f24ec59288b07c2379731 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Fri, 27 Jan 2012 16:07:13 +0100 Subject: [PATCH 119/429] Fix conditional include of excanvas.js on comparison page --- codespeed/templates/codespeed/comparison.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 56147147..9d9d95c2 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -79,7 +79,7 @@ {% block extra_body %} {{ block.super }} - + From 78368a9b85d5979e36e72c481d77fea99b7b6f57 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 21 Feb 2012 09:30:26 +0100 Subject: [PATCH 120/429] added tests for RevisionResource, extended RevisionResource --- codespeed/api.py | 3 ++ codespeed/tests/tests_api.py | 94 +++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 54ec0d97..4b9ee775 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -88,6 +88,9 @@ class Meta: class RevisionResource(ModelResource): """Ressource for Revision()""" + project = fields.ToOneField(ProjectResource, 'project') + branch = fields.ToOneField(BranchResource, 'branch') + class Meta: queryset = Revision.objects.all() authorization= Authorization() diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 700f7602..8778f084 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -342,8 +342,98 @@ def test_delete(self): class RevisionTest(FixtureTestCase): - """Test Branch() API""" - pass + """Test Revision() API""" + + def setUp(self): + DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S' + self.branch1 = Branch.objects.get(pk=1) + self.project1 = Project.objects.get(pk=1) + self.revision1_data = dict( + commitid="2a6306432e973cdcfd324e81169bb8029d47b736", + tag="tag", + date=datetime.now(), + message="Commit message\n - all bugs fixed\n - code 130% faster", + project=self.project1, + author="Alan T. ", + branch=self.branch1, + ) + self.revision1 = Revision(**self.revision1_data) + self.revision1.save() + self.revision2_data = dict( + commitid="4d3bea3cffe4edcd7d70fc46c5e19474cc4bd012", + tag="v1.0", + date=datetime.now().strftime(DATETIME_FORMAT), + message="Commit message\n - cleanup\n - all FIXMEs removed", + project='/api/v1/project/{0}/'.format(self.project1.id), + author="Chuck N. ", + branch='/api/v1/branch/{0}/'.format(self.branch1.id), + ) + self.client = Client() + super(RevisionTest, self).setUp() + + def test_get_revision(self): + """Should get an existing branch""" + response = self.client.get('/api/v1/revision/1/') + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['commitid'], "1") + self.assertEqual(json.loads(response.content)['project'], + "/api/v1/project/1/") + + def test_get_revision_all_fields(self): + """Should get all fields for a revision""" + response = self.client.get('/api/v1/revision/%s/' % (self.revision1.id,)) + self.assertEquals(response.status_code, 200) + self.assertEquals(json.loads(response.content)['commitid'], + self.revision1.commitid) + self.assertEquals(json.loads(response.content)['project'], + '/api/v1/project/%s/' % (self.project1.pk)) + self.assertEquals(json.loads(response.content)['branch'], + '/api/v1/branch/%s/' % (self.branch1.pk)) + self.assertEquals(json.loads(response.content)['tag'], + self.revision1_data['tag']) + self.assertEquals(json.loads(response.content)['message'], + self.revision1_data['message']) + + def test_post(self): + """Should save a new revision""" + modified_data = copy.deepcopy(self.revision2_data) + response = self.client.post('/api/v1/revision/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 201) + id = response['Location'].rsplit('/', 2)[-2] + response = self.client.get('/api/v1/revision/{0}/'.format(id)) + for k, v in self.revision2_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + response = self.client.delete('/api/v1/revision/{0}/'.format(id), + content_type='application/json') + self.assertEquals(response.status_code, 204) + + def test_put(self): + """Should modify an existing environment""" + modified_data = copy.deepcopy(self.revision2_data) + modified_data['tag'] = "v0.9.1" + response = self.client.put('/api/v1/revision/1/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 204) + response = self.client.get('/api/v1/revision/1/') + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_delete(self): + """Should delete a revision""" + response = self.client.get('/api/v1/revision/1/') + self.assertEquals(response.status_code, 200) + # from fixture + response = self.client.delete('/api/v1/revision/1/', + content_type='application/json') + self.assertEquals(response.status_code, 204) + + response = self.client.get('/api/v1/revision/1/') + self.assertEquals(response.status_code, 404) class ExecutableTest(FixtureTestCase): From b6ec83cfe867c8adeb8c0b19f71faeed9b973dcc Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 21 Feb 2012 13:08:41 +0100 Subject: [PATCH 121/429] added tests for ExecutableRessource --- codespeed/api.py | 1 + codespeed/tests/tests_api.py | 74 +++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index 4b9ee775..2d94dac2 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -98,6 +98,7 @@ class Meta: class ExecutableResource(ModelResource): """Ressource for Executable()""" + project = fields.ToOneField(ProjectResource, 'project') class Meta: diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 8778f084..ea9348e8 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -438,7 +438,79 @@ def test_delete(self): class ExecutableTest(FixtureTestCase): """Test Branch() API""" - pass + + def setUp(self): + self.executable1 = Executable.objects.get(pk=1) + self.project1 = Project.objects.get(pk=1) + self.executable2_data = dict( + name="sleep", + description="Sleep benchmark", + project= '/api/v1/project/{0}/'.format(self.project1.id), + ) + self.client = Client() + super(ExecutableTest, self).setUp() + + def test_get_executable(self): + """Should get an existing executable""" + response = self.client.get('/api/v1/executable/1/') + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['name'], + 'myexe O3 64bits') + self.assertEqual(json.loads(response.content)['project'], + "/api/v1/project/1/") + + def test_get_executable_all_fields(self): + """Should get all fields for an executable""" + response = self.client.get('/api/v1/executable/{0}/'.format( + self.executable1.id,)) + self.assertEquals(response.status_code, 200) + self.assertEquals(json.loads(response.content)['name'], + self.executable1.name) + self.assertEquals(json.loads(response.content)['project'], + '/api/v1/project/%s/' % (self.project1.pk)) + self.assertEquals(json.loads(response.content)['description'], + self.executable1.description) + + def test_post(self): + """Should save a new executable""" + modified_data = copy.deepcopy(self.executable2_data) + response = self.client.post('/api/v1/executable/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 201) + id = response['Location'].rsplit('/', 2)[-2] + response = self.client.get('/api/v1/executable/{0}/'.format(id)) + for k, v in self.executable2_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + response = self.client.delete('/api/v1/executable/{0}/'.format(id), + content_type='application/json') + self.assertEquals(response.status_code, 204) + + def test_put(self): + """Should modify an existing environment""" + modified_data = copy.deepcopy(self.executable2_data) + modified_data['name'] = "django" + response = self.client.put('/api/v1/executable/1/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 204) + response = self.client.get('/api/v1/executable/1/') + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_delete(self): + """Should delete a executable""" + response = self.client.get('/api/v1/executable/1/') + self.assertEquals(response.status_code, 200) + # from fixture + response = self.client.delete('/api/v1/executable/1/', + content_type='application/json') + self.assertEquals(response.status_code, 204) + + response = self.client.get('/api/v1/executable/1/') + self.assertEquals(response.status_code, 404) class BenchmarkTest(FixtureTestCase): From da80503c4423ae9d34036dc21786cc702773a8e4 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 21 Feb 2012 16:49:18 +0100 Subject: [PATCH 122/429] Covered all RESTful API resources by tests, changed Report() api - 100% coverage of all the resources provided by the tastypie API - changed ReportResource() to only accept GET requests. Reports are generated automatically. - fixed some typos --- codespeed/api.py | 26 +++--- codespeed/tests/tests_api.py | 158 +++++++++++++++++++++++++++++++++-- codespeed/urls.py | 3 +- 3 files changed, 169 insertions(+), 18 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 2d94dac2..a954724e 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -54,7 +54,8 @@ class UserResource(ModelResource): - """Ressource for Django User()""" + """Resource for Django User()""" + class Meta: queryset = User.objects.filter(is_active=True) resource_name = 'user' @@ -68,7 +69,7 @@ class Meta: class ProjectResource(ModelResource): - """Ressource for Project()""" + """Resource for Project()""" class Meta: queryset = Project.objects.all() @@ -76,7 +77,7 @@ class Meta: class BranchResource(ModelResource): - """Ressource for Branch()""" + """Resource for Branch()""" project = fields.ToOneField(ProjectResource, 'project') @@ -86,7 +87,7 @@ class Meta: class RevisionResource(ModelResource): - """Ressource for Revision()""" + """Resource for Revision()""" project = fields.ToOneField(ProjectResource, 'project') branch = fields.ToOneField(BranchResource, 'branch') @@ -97,7 +98,7 @@ class Meta: class ExecutableResource(ModelResource): - """Ressource for Executable()""" + """Resource for Executable()""" project = fields.ToOneField(ProjectResource, 'project') @@ -107,7 +108,7 @@ class Meta: class BenchmarkResource(ModelResource): - """Ressource for Benchmark()""" + """Resource for Benchmark()""" class Meta: queryset = Benchmark.objects.all() @@ -115,7 +116,7 @@ class Meta: class EnvironmentResource(ModelResource): - """Ressource for Enviroment()""" + """Resource for Enviroment()""" class Meta: queryset = Environment.objects.all() @@ -124,7 +125,7 @@ class Meta: class ResultResource(ModelResource): - """Ressource for Result()""" + """Resource for Result()""" class Meta: queryset = Result.objects.all() @@ -132,10 +133,15 @@ class Meta: class ReportResource(ModelResource): - """Ressource for Report()""" + """Resource for Report()""" + + revision = fields.ToOneField(RevisionResource, 'revision') + environment = fields.ToOneField(EnvironmentResource, 'environment') + executable = fields.ToOneField(ExecutableResource, 'executable') class Meta: queryset = Report.objects.all() + allowed_methods = ['get'] authorization= Authorization() @@ -331,7 +337,7 @@ def save(self): class ResultBundleResource(Resource): - """Ressource for all the data of a benchmark result. + """Resource for all the data of a benchmark result. Primarily used to submit benchmark results diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index ea9348e8..62d2c900 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -372,7 +372,7 @@ def setUp(self): super(RevisionTest, self).setUp() def test_get_revision(self): - """Should get an existing branch""" + """Should get an existing revision""" response = self.client.get('/api/v1/revision/1/') self.assertEquals(response.status_code, 200) self.assertEqual(json.loads(response.content)['commitid'], "1") @@ -411,7 +411,7 @@ def test_post(self): self.assertEquals(response.status_code, 204) def test_put(self): - """Should modify an existing environment""" + """Should modify an existing revision""" modified_data = copy.deepcopy(self.revision2_data) modified_data['tag'] = "v0.9.1" response = self.client.put('/api/v1/revision/1/', @@ -437,7 +437,7 @@ def test_delete(self): class ExecutableTest(FixtureTestCase): - """Test Branch() API""" + """Test Executable() API""" def setUp(self): self.executable1 = Executable.objects.get(pk=1) @@ -514,13 +514,157 @@ def test_delete(self): class BenchmarkTest(FixtureTestCase): - """Test Branch() API""" - pass + """Test Benchmark() API""" + + def setUp(self): + self.benchmark1 = Benchmark.objects.get(pk=1) + self.benchmark2_data = dict( + name="sleep", + benchmark_type = 'C', + description = 'fast faster fastest', + units_title = 'Time', + units = 'seconds', + lessisbetter = True, + default_on_comparison = True, + ) + self.benchmark2 = Benchmark(**self.benchmark2_data) + self.benchmark2.save() + self.client = Client() + super(BenchmarkTest, self).setUp() + + def test_get_benchmark(self): + """Should get an existing benchmark""" + response = self.client.get('/api/v1/benchmark/1/') + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['name'], + 'float') + self.assertEqual(json.loads(response.content)['units'], + "seconds") + + def test_get_benchmark_all_fields(self): + """Should get all fields for an benchmark""" + response = self.client.get('/api/v1/benchmark/{0}/'.format( + self.benchmark2.id,)) + self.assertEquals(response.status_code, 200) + for k, v in self.benchmark2_data.items(): + self.assertEqual(json.loads(response.content)[k], v) + + def test_post(self): + """Should save a new benchmark""" + modified_data = copy.deepcopy(self.benchmark2_data) + modified_data['name'] = 'wake' + response = self.client.post('/api/v1/benchmark/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 201) + id = response['Location'].rsplit('/', 2)[-2] + response = self.client.get('/api/v1/benchmark/{0}/'.format(id)) + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + response = self.client.delete('/api/v1/benchmark/{0}/'.format(id), + content_type='application/json') + self.assertEquals(response.status_code, 204) + + def test_put(self): + """Should modify an existing benchmark""" + modified_data = copy.deepcopy(self.benchmark2_data) + modified_data['name'] = "django" + response = self.client.put('/api/v1/benchmark/1/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 204) + response = self.client.get('/api/v1/benchmark/1/') + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_delete(self): + """Should delete a benchmark""" + response = self.client.get('/api/v1/benchmark/1/') + self.assertEquals(response.status_code, 200) + # from fixture + response = self.client.delete('/api/v1/benchmark/1/', + content_type='application/json') + self.assertEquals(response.status_code, 204) + + response = self.client.get('/api/v1/benchmark/1/') + self.assertEquals(response.status_code, 404) class ReportTest(FixtureTestCase): - """Test Branch() API""" - pass + """Test Report() API""" + + def setUp(self): + self.report1 = Report.objects.get(pk=1) + self.revision1 = Revision.objects.get(pk=1) + self.executable1 = Executable.objects.get(pk=1) + self.environment1 = Environment.objects.get(pk=1) + self.executable2_data = dict( + name="Fibo", + description="Fibonacci the Lame", + ) + self.project=Project.objects.get(pk=1) + self.executable2 = Executable(project=self.project, + **self.executable2_data) + self.executable2.save() + self.report2_data = dict( + revision=self.revision1, + environment=self.environment1, + executable=self.executable2, + ) + self.report2 = Report(**self.report2_data) + self.report2.save() + self.report2_data = dict( + revision='/api/v1/revision/{0}/'.format(self.revision1.id), + environment='/api/v1/environment/{0}/'.format(self.environment1.id), + executable='/api/v1/executable/{0}/'.format(self.executable2.id), + ) + self.client = Client() + super(ReportTest, self).setUp() + + def test_get_report(self): + """Should get an existing report""" + response = self.client.get('/api/v1/report/1/') + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['summary'], + 'float -50.0%') + self.assertEqual(json.loads(response.content)['colorcode'], + "green") + + def test_get_report_all_fields(self): + """Should get all fields for an report""" + response = self.client.get('/api/v1/report/{0}/'.format( + self.report2.id,)) + self.assertEquals(response.status_code, 200) + for k, v in self.report2_data.items(): + self.assertEqual(json.loads(response.content)[k], v) + + def test_post(self): + """Should save a new report""" + modified_data = copy.deepcopy(self.report2_data) + response = self.client.post('/api/v1/report/', + data=json.dumps(modified_data), + content_type='application/json') + # next has to be 405, otherwise would raise IntegrityError + self.assertEquals(response.status_code, 405) + + def test_put(self): + """Should modify an existing report""" + modified_data = copy.deepcopy(self.report2_data) + response = self.client.put('/api/v1/report/1/', + data=json.dumps(modified_data), + content_type='application/json') + self.assertEquals(response.status_code, 405) + + def test_delete(self): + """Should delete a report""" + response = self.client.get('/api/v1/report/1/') + self.assertEquals(response.status_code, 200) + # from fixture + response = self.client.delete('/api/v1/report/1/', + content_type='application/json') + self.assertEquals(response.status_code, 405) class UserTest(FixtureTestCase): diff --git a/codespeed/urls.py b/codespeed/urls.py index 568b5742..2efc515e 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -5,7 +5,7 @@ from tastypie.api import Api from codespeed.feeds import LatestEntries from codespeed.api import (UserResource, EnvironmentResource, - ProjectResource, ExecutableResource, + ProjectResource, ExecutableResource, ReportResource, BenchmarkResource, ResultResource, BranchResource, RevisionResource, ResultBundleResource) @@ -20,6 +20,7 @@ rest_api.register(ResultResource()) rest_api.register(BranchResource()) rest_api.register(RevisionResource()) +rest_api.register(ReportResource()) rest_api.register(ResultBundleResource()) urlpatterns = patterns('', From 931c26e7d4ee009b6e3a1d09ab80f861b25119d1 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 25 Feb 2012 09:41:36 +0100 Subject: [PATCH 123/429] Remove vim config from create_apikey, lots of pep8 fixes --- codespeed/admin.py | 17 ++++++-- codespeed/api.py | 30 +++++++------- codespeed/git.py | 3 +- codespeed/github.py | 12 +++--- codespeed/mercurial.py | 12 +++--- codespeed/models.py | 71 +++++++++++++++++++--------------- codespeed/settings.py | 2 +- codespeed/urls.py | 2 +- codespeed/views.py | 47 +++++++++++++--------- example/client.py | 8 ++-- example/settings.py | 2 +- tools/create_apikey.py | 26 ++++++------- tools/create_trunks.py | 17 ++++---- tools/migrate_script.py | 19 +++++---- tools/pypy/import_from_json.py | 3 +- tools/save_multiple_results.py | 6 ++- tools/save_single_result.py | 18 +++++---- tools/test_performance.py | 18 ++++++--- 18 files changed, 178 insertions(+), 135 deletions(-) diff --git a/codespeed/admin.py b/codespeed/admin.py index d2b4c7ac..5850f9fa 100644 --- a/codespeed/admin.py +++ b/codespeed/admin.py @@ -11,43 +11,52 @@ class ProjectAdmin(admin.ModelAdmin): admin.site.register(Project, ProjectAdmin) + class BranchAdmin(admin.ModelAdmin): list_display = ('name', 'project') admin.site.register(Branch, BranchAdmin) + class RevisionAdmin(admin.ModelAdmin): list_display = ('commitid', 'branch', 'tag', 'date') - list_filter = ('branch', 'tag', 'date') + list_filter = ('branch', 'tag', 'date') search_fields = ('commitid', 'tag') admin.site.register(Revision, RevisionAdmin) + class ExecutableAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'id', 'project') search_fields = ('name', 'description', 'project') admin.site.register(Executable, ExecutableAdmin) + class BenchmarkAdmin(admin.ModelAdmin): - list_display = ('name', 'benchmark_type', 'description', 'units_title', 'units', 'lessisbetter', 'default_on_comparison') + list_display = ('name', 'benchmark_type', 'description', 'units_title', + 'units', 'lessisbetter', 'default_on_comparison') ordering = ['name'] search_fields = ('name', 'description') admin.site.register(Benchmark, BenchmarkAdmin) + class EnvironmentAdmin(admin.ModelAdmin): list_display = ('name', 'cpu', 'memory', 'os', 'kernel') search_fields = ('name', 'cpu', 'memory', 'os', 'kernel') admin.site.register(Environment, EnvironmentAdmin) + class ResultAdmin(admin.ModelAdmin): - list_display = ('revision', 'benchmark', 'executable', 'environment', 'value', 'date', 'environment') - list_filter = ('date', 'environment', 'executable', 'benchmark') + list_display = ('revision', 'benchmark', 'executable', 'environment', + 'value', 'date', 'environment') + list_filter = ('date', 'environment', 'executable', 'benchmark') admin.site.register(Result, ResultAdmin) + class ReportAdmin(admin.ModelAdmin): list_display = ('revision', 'summary', 'colorcode') ordering = ['-revision'] diff --git a/codespeed/api.py b/codespeed/api.py index a954724e..a7d80207 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -64,8 +64,8 @@ class Meta: #excludes = ['email', 'password', 'is_superuser'] # Add it here. #authorization = DjangoAuthorization() - authorization= Authorization() - #authentication = ApiKeyAuthentication() + authorization = Authorization() + #authentication = ApiKeyAuthentication() class ProjectResource(ModelResource): @@ -73,7 +73,7 @@ class ProjectResource(ModelResource): class Meta: queryset = Project.objects.all() - authorization= Authorization() + authorization = Authorization() class BranchResource(ModelResource): @@ -83,7 +83,7 @@ class BranchResource(ModelResource): class Meta: queryset = Branch.objects.all() - authorization= Authorization() + authorization = Authorization() class RevisionResource(ModelResource): @@ -94,7 +94,7 @@ class RevisionResource(ModelResource): class Meta: queryset = Revision.objects.all() - authorization= Authorization() + authorization = Authorization() class ExecutableResource(ModelResource): @@ -104,7 +104,7 @@ class ExecutableResource(ModelResource): class Meta: queryset = Executable.objects.all() - authorization= Authorization() + authorization = Authorization() class BenchmarkResource(ModelResource): @@ -112,7 +112,7 @@ class BenchmarkResource(ModelResource): class Meta: queryset = Benchmark.objects.all() - authorization= Authorization() + authorization = Authorization() class EnvironmentResource(ModelResource): @@ -121,7 +121,7 @@ class EnvironmentResource(ModelResource): class Meta: queryset = Environment.objects.all() resource_name = 'environment' - authorization= Authorization() + authorization = Authorization() class ResultResource(ModelResource): @@ -129,7 +129,7 @@ class ResultResource(ModelResource): class Meta: queryset = Result.objects.all() - authorization= Authorization() + authorization = Authorization() class ReportResource(ModelResource): @@ -142,7 +142,7 @@ class ReportResource(ModelResource): class Meta: queryset = Report.objects.all() allowed_methods = ['get'] - authorization= Authorization() + authorization = Authorization() class ResultBundle(Bundle): @@ -183,13 +183,13 @@ def __init__(self, obj=None, **kwargs): self.obj = obj self._populate_by_obj() elif obj is None: - self.obj = Result() + self.obj = Result() else: raise ValueError("obj has to be an instance of models.Result") if self.data: self._check_data() - self.__data_validated = False #not used for now + self.__data_validated = False # not used for now super(ResultBundle, self).__init__(data=self.data, obj=self.obj) def _populate_obj_by_data(self): @@ -213,7 +213,7 @@ def populate(key): }.get(key, (None, None))() try: - self.obj.value = float(self.data['result_value']) + self.obj.value = float(self.data['result_value']) except ValueError, error: logging.error( "Result value: {0} cannot be converted to float. {1}".format( @@ -367,7 +367,7 @@ class ResultBundleResource(Resource): class Meta: resource_name = 'benchmark-result' - authorization= Authorization() + authorization = Authorization() allowed_methods = ['get', 'post', 'put', 'delete'] def get_resource_uri(self, bundle_or_obj): @@ -417,7 +417,6 @@ def obj_create(self, bundle, request=None, **kwargs): def obj_update(self, bundle, request=None, **kwargs): return self.obj_create(bundle, request, **kwargs) - def post_list(self, request, **kwargs): """ Creates a new resource/object with the provided data. @@ -458,4 +457,3 @@ def obj_delete(self, request=None, **kwargs): def rollback(self, bundles): pass - diff --git a/codespeed/git.py b/codespeed/git.py index 6c6de7b2..a16673c5 100644 --- a/codespeed/git.py +++ b/codespeed/git.py @@ -37,6 +37,7 @@ def updaterepo(project, update=True): else: return [{'error': False}] + def getlogs(endrev, startrev): updaterepo(endrev.branch.project, update=False) @@ -48,7 +49,7 @@ def getlogs(endrev, startrev): if endrev.commitid != startrev.commitid: cmd.append("%s...%s" % (startrev.commitid, endrev.commitid)) else: - cmd.append("-1") # Only return one commit + cmd.append("-1") # Only return one commit cmd.append(endrev.commitid) working_copy = endrev.branch.project.working_copy diff --git a/codespeed/github.py b/codespeed/github.py index ba93fe93..5a4f1c26 100644 --- a/codespeed/github.py +++ b/codespeed/github.py @@ -26,11 +26,12 @@ # revisions. GITHUB_REVISION_LIMIT = 10 + def updaterepo(project, update=True): return -def retrieve_revision(commit_id, username, project, revision = None): +def retrieve_revision(commit_id, username, project, revision=None): commit_url = 'http://github.com/api/v2/json/commits/show/%s/%s/%s' % ( username, project, commit_id) @@ -73,13 +74,14 @@ def retrieve_revision(commit_id, username, project, revision = None): return {'date': date, 'message': commit['message'], - 'body': "", # TODO: pretty-print diffs + 'body': "", # TODO: pretty-print diffs 'author': commit['author']['name'], 'author_email': commit['author']['email'], 'commitid': commit['id'], 'short_commit_id': commit['id'][0:7], 'parents': commit['parents']} + def getlogs(endrev, startrev): if endrev != startrev: revisions = endrev.branch.revisions.filter( @@ -108,15 +110,15 @@ def getlogs(endrev, startrev): logs.append(last_rev_data) revision_count += 1 ancestor_found = (startrev.commitid in [rev['id'] for rev in last_rev_data['parents']]) - + # Simple approach to find the startrev, stop after found or after # #GITHUB_REVISION_LIMIT revisions are fetched - while (revision_count < GITHUB_REVISION_LIMIT + while (revision_count < GITHUB_REVISION_LIMIT and not ancestor_found and len(last_rev_data['parents']) > 0): last_rev_data = retrieve_revision(last_rev_data['parents'][0]['id'], username, project) logs.append(last_rev_data) revision_count += 1 ancestor_found = (startrev.commitid in [rev['id'] for rev in last_rev_data['parents']]) - + return sorted(logs, key=lambda i: i['date'], reverse=True) diff --git a/codespeed/mercurial.py b/codespeed/mercurial.py index 6ac8ced3..5a670749 100644 --- a/codespeed/mercurial.py +++ b/codespeed/mercurial.py @@ -1,4 +1,5 @@ -import os, datetime +import os +import datetime from subprocess import Popen, PIPE import logging @@ -39,6 +40,7 @@ def updaterepo(project, update=True): else: return [{'error': False}] + def getlogs(endrev, startrev): updaterepo(endrev.branch.project, update=False) @@ -54,7 +56,7 @@ def getlogs(endrev, startrev): if p.returncode != 0: raise RuntimeError(str(stderr)) else: - stdout = stdout.rstrip('\n')#Remove last newline + stdout = stdout.rstrip('\n') # Remove last newline logs = [] for log in stdout.split("=newlog=\n"): elements = [] @@ -78,9 +80,9 @@ def getlogs(endrev, startrev): # Add changeset info logs.append({ - 'date': date, 'author': author_name, 'author_email': author_email, - 'message': message,'short_commit_id': short_commit_id, - 'commitid': commit_id}) + 'date': date, 'author': author_name, + 'author_email': author_email, 'message': message, + 'short_commit_id': short_commit_id, 'commitid': commit_id}) # Remove last log here because mercurial saves the short hast as commitid now if len(logs) > 1 and logs[-1].get('short_commit_id') == startrev.commitid: logs.pop() diff --git a/codespeed/models.py b/codespeed/models.py index e6ab79a9..9e143ee3 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -5,7 +5,6 @@ from django.core.urlresolvers import reverse from django.db import models from django.utils import simplejson as json - from django.conf import settings from codespeed.github import GITHUB_URL_RE @@ -59,7 +58,6 @@ def save(self, *args, **kwargs): m.group('username'), m.group('project') ) self.commit_browsing_url = url - super(Project, self).save(*args, **kwargs) @@ -75,7 +73,8 @@ class Meta: class Revision(models.Model): - commitid = models.CharField(max_length=42)#git and mercurial's SHA-1 length is 40 + # git and mercurial's SHA-1 length is 40 + commitid = models.CharField(max_length=42) tag = models.CharField(max_length=20, blank=True) date = models.DateTimeField(null=True) message = models.TextField(blank=True) @@ -181,11 +180,11 @@ class Meta: class Report(models.Model): - revision = models.ForeignKey(Revision, related_name="reports") + revision = models.ForeignKey(Revision, related_name="reports") environment = models.ForeignKey(Environment, related_name="reports") - executable = models.ForeignKey(Executable, related_name="reports") - summary = models.CharField(max_length=64, blank=True) - colorcode = models.CharField(max_length=10, default="none") + executable = models.ForeignKey(Executable, related_name="reports") + summary = models.CharField(max_length=64, blank=True) + colorcode = models.CharField(max_length=10, default="none") _tablecache = models.TextField(blank=True) def __unicode__(self): @@ -203,8 +202,9 @@ def save(self, *args, **kwargs): # Get default threshold values change_threshold = 3.0 - trend_threshold = 5.0 - if hasattr(settings, 'CHANGE_THRESHOLD') and settings.CHANGE_THRESHOLD != None: + trend_threshold = 5.0 + if (hasattr(settings, 'CHANGE_THRESHOLD') and + settings.CHANGE_THRESHOLD != None): change_threshold = settings.CHANGE_THRESHOLD if hasattr(settings, 'TREND_THRESHOLD') and settings.TREND_THRESHOLD: trend_threshold = settings.TREND_THRESHOLD @@ -218,7 +218,7 @@ def save(self, *args, **kwargs): color = self.getcolorcode(val, units['lessisbetter'], change_threshold) if self.is_big_change(val, color, average_change, average_change_color): # Do update biggest total change - average_change = val + average_change = val average_change_units = units['units_title'] average_change_color = color # Total trend @@ -227,7 +227,7 @@ def save(self, *args, **kwargs): color = self.getcolorcode(val, units['lessisbetter'], trend_threshold) if self.is_big_change(val, color, average_trend, average_trend_color): # Do update biggest total trend change - average_trend = val + average_trend = val average_trend_units = units['units_title'] average_trend_color = color for row in units['rows']: @@ -238,8 +238,8 @@ def save(self, *args, **kwargs): color = self.getcolorcode(val, units['lessisbetter'], change_threshold) if self.is_big_change(val, color, max_change, max_change_color): # Do update biggest single change - max_change = val - max_change_ben = row['bench_name'] + max_change = val + max_change_ben = row['bench_name'] max_change_color = color # Single trend val = row['trend'] @@ -248,8 +248,8 @@ def save(self, *args, **kwargs): color = self.getcolorcode(val, units['lessisbetter'], trend_threshold) if self.is_big_change(val, color, max_trend, max_trend_color): # Do update biggest single trend change - max_trend = val - max_trend_ben = row['bench_name'] + max_trend = val + max_trend_ben = row['bench_name'] max_trend_color = color # Reinitialize self.summary = "" @@ -311,7 +311,7 @@ def getcolorcode(self, val, lessisbetter, threshold): colorcode = "red" elif val > threshold: colorcode = "green" - return colorcode; + return colorcode def get_changes_table(self, trend_depth=10, force_save=False): # Determine whether required trend value is the default one @@ -329,7 +329,7 @@ def get_changes_table(self, trend_depth=10, force_save=False): branch=self.revision.branch ).filter( date__lte=self.revision.date - ).order_by('-date')[:trend_depth+1] + ).order_by('-date')[:trend_depth + 1] # Same as self.revision unless in a different branch lastrevision = lastrevisions[0] except: @@ -345,7 +345,7 @@ def get_changes_table(self, trend_depth=10, force_save=False): ).filter( executable=self.executable ) - pastrevisions = lastrevisions[trend_depth-2:trend_depth+1] + pastrevisions = lastrevisions[trend_depth - 2:trend_depth + 1] result_list = Result.objects.filter( revision=lastrevision @@ -363,26 +363,33 @@ def get_changes_table(self, trend_depth=10, force_save=False): hasmax = False has_stddev = False smallest = 1000 - totals = {'change': [], 'trend': [],} + totals = {'change': [], 'trend': []} for bench in Benchmark.objects.filter(units=units['units']): units_title = bench.units_title lessisbetter = bench.lessisbetter resultquery = result_list.filter(benchmark=bench, value__gt=0) - if not len(resultquery): continue + if not len(resultquery): + continue resobj = resultquery.filter(benchmark=bench)[0] std_dev = resobj.std_dev - if std_dev is not None: has_stddev = True - else: std_dev = "-" + if std_dev is not None: + has_stddev = True + else: + std_dev = "-" val_min = resobj.val_min - if val_min is not None: hasmin = True - else: val_min = "-" + if val_min is not None: + hasmin = True + else: + val_min = "-" val_max = resobj.val_max - if val_max is not None: hasmax = True - else: val_max = "-" + if val_max is not None: + hasmax = True + else: + val_max = "-" # Calculate percentage change relative to previous result result = resobj.value @@ -390,7 +397,7 @@ def get_changes_table(self, trend_depth=10, force_save=False): if len(change_list): c = change_list.filter(benchmark=bench) if c.count() and c[0].value and result: - change = (result - c[0].value)*100/c[0].value + change = (result - c[0].value) * 100 / c[0].value totals['change'].append(result / c[0].value) # Calculate trend: @@ -413,7 +420,7 @@ def get_changes_table(self, trend_depth=10, force_save=False): trend = "-" if average: average = average / averagecount - trend = (result - average)*100/average + trend = (result - average) * 100 / average totals['trend'].append(result / average) # Retain lowest number different than 0 @@ -440,12 +447,14 @@ def get_changes_table(self, trend_depth=10, force_save=False): totals[key] = "-" if totals['change'] != "-": - totals['change'] = (totals['change'] - 1) * 100#transform ratio to percentage + # Transform ratio + totals['change'] = (totals['change'] - 1) * 100 to percentage if totals['trend'] != "-": - totals['trend'] = (totals['trend'] - 1) * 100#transform ratio to percentage + # Transform ratio to percentage + totals['trend'] = (totals['trend'] - 1) * 100 # Calculate significant digits - digits = 2; + digits = 2 while smallest < 1: smallest *= 10 digits += 1 diff --git a/codespeed/settings.py b/codespeed/settings.py index 13d391d1..e2c79dcd 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -22,7 +22,7 @@ # Threshold that determines when a performance change # over a number of revisions is significant -TREND_THRESHOLD = 5.0 +TREND_THRESHOLD = 5.0 ## Changes view options ## DEF_EXECUTABLE = None # Executable that should be chosen as default in the changes view diff --git a/codespeed/urls.py b/codespeed/urls.py index 2efc515e..20b7d71c 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -9,7 +9,7 @@ BenchmarkResource, ResultResource, BranchResource, RevisionResource, ResultBundleResource) -feeds = { 'latest': LatestEntries } +feeds = {'latest': LatestEntries} rest_api = Api(api_name='v1') rest_api.register(EnvironmentResource()) diff --git a/codespeed/views.py b/codespeed/views.py index 31e019ed..1aec6706 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -25,6 +25,7 @@ def no_environment_error(request): 'message': 'You need to configure at least one Environment. Please go to the admin interface' % admin_url }, context_instance=RequestContext(request)) + def no_default_project_error(request): admin_url = reverse('admin:codespeed_project_changelist') return render_to_response('codespeed/nodata.html', { @@ -32,11 +33,13 @@ def no_default_project_error(request): 'Please go to the admin interface' % admin_url }, context_instance=RequestContext(request)) + def no_executables_error(request): return render_to_response('codespeed/nodata.html', { 'message': 'There needs to be at least one executable' }, context_instance=RequestContext(request)) + def no_data_found(request): return render_to_response('codespeed/nodata.html', { 'message': 'No data found' @@ -49,10 +52,11 @@ def getbaselineexecutables(): revs = Revision.objects.exclude(tag="").select_related('branch__project') maxlen = 22 for rev in revs: - #add executables that correspond to each tagged revision. + # Add executables that correspond to each tagged revision. for exe in [e for e in executables if e.project == rev.branch.project]: exestring = str(exe) - if len(exestring) > maxlen: exestring = str(exe)[0:maxlen] + "..." + if len(exestring) > maxlen: + exestring = str(exe)[0:maxlen] + "..." name = exestring + " " + rev.tag key = str(exe.id) + "+" + str(rev.id) baseline.append({ @@ -208,7 +212,7 @@ def getcomparisondata(request): compdata['error'] = "None" - return HttpResponse(json.dumps( compdata )) + return HttpResponse(json.dumps(compdata)) def comparison(request): @@ -281,7 +285,8 @@ def comparison(request): if 'ben' in data: checkedbenchmarks = [] for i in data['ben'].split(","): - if not i: continue + if not i: + continue try: checkedbenchmarks.append(Benchmark.objects.get(id=int(i))) except Benchmark.DoesNotExist: @@ -293,7 +298,8 @@ def comparison(request): charts = ['normal bars', 'stacked bars', 'relative bars'] # Don't show relative charts as an option if there is only one executable # Relative charts need normalization - if len(executables) == 1: charts.remove('relative bars') + if len(executables) == 1: + charts.remove('relative bars') selectedchart = charts[0] if 'chart' in data and data['chart'] in charts: @@ -312,7 +318,7 @@ def comparison(request): try: # TODO: Avoid calling twice getbaselineexecutables selectedbaseline = getbaselineexecutables()[1]['key'] - # Uncheck exe used for normalization + # Uncheck exe used for normalization try: checkedexecutables.remove(selectedbaseline) except ValueError: @@ -350,7 +356,7 @@ def gettimelinedata(request): executables = data.get('exe', "").split(",") if not filter(None, executables): timeline_list['error'] = "No executables selected" - return HttpResponse(json.dumps( timeline_list )) + return HttpResponse(json.dumps(timeline_list)) environment = None try: environment = get_object_or_404(Environment, id=data.get('env')) @@ -436,7 +442,7 @@ def gettimelinedata(request): if len(timeline['branches'][branch][exe]) > len(results): results = timeline['branches'][branch][exe] end = results[0][0] - start = results[len(results)-1][0] + start = results[len(results) - 1][0] timeline['baseline'] = [ [str(start), baselinevalue], [str(end), baselinevalue] @@ -447,7 +453,7 @@ def gettimelinedata(request): if not len(timeline_list['timelines']) and data['ben'] != 'show_none': response = 'No data found for the selected options' timeline_list['error'] = response - return HttpResponse(json.dumps( timeline_list )) + return HttpResponse(json.dumps(timeline_list)) def timeline(request): @@ -472,7 +478,8 @@ def timeline(request): checkedexecutables = [] if 'exe' in data: for i in data['exe'].split(","): - if not i: continue + if not i: + continue try: checkedexecutables.append(Executable.objects.get(id=int(i))) except Executable.DoesNotExist: @@ -537,7 +544,7 @@ def timeline(request): defaultbenchmark = data['ben'] else: defaultbenchmark = get_object_or_404(Benchmark, name=data['ben']) - + if 'equid' in data: defaultequid = data['equid'] else: @@ -560,7 +567,7 @@ def timeline(request): 'environments': enviros, 'branch_list': branch_list, 'defaultbranch': defaultbranch, - 'defaultequid' : defaultequid + 'defaultequid': defaultequid }, context_instance=RequestContext(request)) @@ -592,7 +599,8 @@ def getchangestable(request): def changes(request): - if request.method != 'GET': return HttpResponseNotAllowed('GET') + if request.method != 'GET': + return HttpResponseNotAllowed('GET') data = request.GET # Configuration of default parameters @@ -781,8 +789,8 @@ def saverevisioninfo(rev): if log: log = log[0] - rev.author = log['author'] - rev.date = log['date'] + rev.author = log['author'] + rev.date = log['date'] rev.message = log['message'] @@ -805,7 +813,7 @@ def validate_result(item): ] response = {} - error = True + error = True for key in mandatory_data: if not key in item: return 'Key "' + key + '" missing from request', error @@ -828,7 +836,8 @@ def create_report_if_enough_data(rev, exe, e): ).order_by('-date')[:2] if len(last_revs) > 1: current_results = rev.results.filter(executable=exe, environment=e) - last_results = last_revs[1].results.filter(executable=exe,environment=e) + last_results = last_revs[1].results.filter( + executable=exe, environment=e) # If there is are at least as many results as in the last revision, # create new report if len(current_results) >= len(last_results): @@ -881,9 +890,9 @@ def save_result(data): try: r = Result.objects.get( - revision=rev,executable=exe,benchmark=b,environment=env) + revision=rev, executable=exe, benchmark=b, environment=env) except Result.DoesNotExist: - r = Result(revision=rev,executable=exe,benchmark=b,environment=env) + r = Result(revision=rev, executable=exe, benchmark=b, environment=env) r.value = data["result_value"] if 'result_date' in data: diff --git a/example/client.py b/example/client.py index 7e1a15fa..d5b50eb1 100644 --- a/example/client.py +++ b/example/client.py @@ -67,7 +67,7 @@ def save_to_speedcenter(url=None, project=None, commitid=None, executable=None, if status == 202: logging.debug("Server %s: HTTP %s: %s", url, status, response) else: - raise IOError("Server %s returned HTTP %s" % (url, status)) + raise IOError("Server %s returned HTTP %s" % (url, status)) if __name__ == "__main__": @@ -76,7 +76,8 @@ def save_to_speedcenter(url=None, project=None, commitid=None, executable=None, parser = OptionParser() parser.add_option("--benchmark") parser.add_option("--commitid") - parser.add_option("--environment", help="Use a custom Codespeed environment") + parser.add_option("--environment", + help="Use a custom Codespeed environment") parser.add_option("--executable") parser.add_option("--max", type="float") parser.add_option("--min", type="float") @@ -86,7 +87,8 @@ def save_to_speedcenter(url=None, project=None, commitid=None, executable=None, parser.add_option("--result-value", type="float") parser.add_option("--revision_date") parser.add_option("--std-dev", type="float") - parser.add_option("--url", help="URL of your Codespeed server (e.g. http://codespeed.example.org)") + parser.add_option("--url", + help="URL of your Codespeed server (e.g. http://codespeed.example.org)") (options, args) = parser.parse_args() diff --git a/example/settings.py b/example/settings.py index 5d6f3468..b3b32f49 100644 --- a/example/settings.py +++ b/example/settings.py @@ -158,7 +158,7 @@ def process_exception(self, request, exception): #DEF_TIMELINE_LIMIT = 50 # Default number of revisions to be plotted # Possible values 10,50,200,1000 -#TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown +#TIMELINE_BRANCHES = True # NOTE: Only the default branch is currently shown # Get timeline results for specific branches # Set to False if you want timeline plots and results only for trunk. diff --git a/tools/create_apikey.py b/tools/create_apikey.py index b9fcea4a..ef48548d 100755 --- a/tools/create_apikey.py +++ b/tools/create_apikey.py @@ -1,48 +1,46 @@ #!/usr/bin/env python - # -*- coding: utf-8 -*- - -""" -Create an API key for all users that are active -""" +"""Create an API key for all users that are active""" import sys import os + ## Setup to import models from Django app ## def import_from_string(name): """helper to import module from a given string""" - components = name.split('.')[1:] return reduce(lambda mod, y: getattr(mod, y), components, __import__(name)) + sys.path.append(os.path.abspath('..')) -if os.environ.has_key('DJANGO_SETTINGS_MODULE'): + +if 'DJANGO_SETTINGS_MODULE' in os.environ: settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE']) else: try: - import settings # Assumed to be in the same directory. + import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write( "Error: Can't find the file 'settings.py' in the directory " "containing %r. It appears you've customized things.\nYou'll have " - "to run django-admin.py, passing it your settings module.\n(If the " - "file settings.py does indeed exist, it's causing an ImportError " + "to run django-admin.py, passing it your settings module.\n(If the" + " file settings.py does indeed exist, it's causing an ImportError " "somehow.)\n" % __file__) sys.exit(1) + from django.core.management import setup_environ setup_environ(settings) from django.contrib.auth.models import User from tastypie.models import ApiKey + def main(): for user in User.objects.all(is_active=True): - ApiKey.objects.create(user=user) + ApiKey.objects.create(user=user) + if __name__ == '__main__': main() - -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 : - diff --git a/tools/create_trunks.py b/tools/create_trunks.py index 785163b0..5514e3b9 100644 --- a/tools/create_trunks.py +++ b/tools/create_trunks.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - """ Create the default branch for all existing projects Starting v 0.8.0 that is mandatory. @@ -8,30 +7,32 @@ as the project settings.py. Otherwise you have to set the shell environment DJANGO_SETTINGS_MODULE """ - import sys import os + ## Setup to import models from Django app ## def import_from_string(name): """helper to import module from a given string""" - components = name.split('.')[1:] return reduce(lambda mod, y: getattr(mod, y), components, __import__(name)) + sys.path.append(os.path.abspath('..')) -if os.environ.has_key('DJANGO_SETTINGS_MODULE'): + + +if 'DJANGO_SETTINGS_MODULE' in os.environ: settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE']) else: try: - import settings # Assumed to be in the same directory. + import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write( "Error: Can't find the file 'settings.py' in the directory " "containing %r. It appears you've customized things.\nYou'll have " - "to run django-admin.py, passing it your settings module.\n(If the " - "file settings.py does indeed exist, it's causing an ImportError " + "to run django-admin.py, passing it your settings module.\n(If the" + " file settings.py does indeed exist, it's causing an ImportError " "somehow.)\n" % __file__) sys.exit(1) @@ -46,7 +47,7 @@ def main(): for proj in projects: if not proj.branches.filter(name='default'): - trunk = Branch(name='default', project = proj) + trunk = Branch(name='default', project=proj) trunk.save() print "Created branch 'default' for project {0}".format(proj) diff --git a/tools/migrate_script.py b/tools/migrate_script.py index b1e28884..e87379e9 100644 --- a/tools/migrate_script.py +++ b/tools/migrate_script.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- -import sys -import os - -""" -Add the default branch to all existing revisions +"""Adds the default branch to all existing revisions Note: This file is assumed to be in the same directory as the project settings.py. Otherwise you have to set the shell environment DJANGO_SETTINGS_MODULE """ +import sys +import os + ## Setup to import models from Django app ## def import_from_string(name): @@ -18,18 +17,19 @@ def import_from_string(name): return reduce(lambda mod, y: getattr(mod, y), components, __import__(name)) sys.path.append(os.path.abspath('..')) -if os.environ.has_key('DJANGO_SETTINGS_MODULE'): + +if 'DJANGO_SETTINGS_MODULE' in os.environ: settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE']) else: try: - import settings # Assumed to be in the same directory. + import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write( "Error: Can't find the file 'settings.py' in the directory " "containing %r. It appears you've customized things.\nYou'll have " - "to run django-admin.py, passing it your settings module.\n(If the " - "file settings.py does indeed exist, it's causing an ImportError " + "to run django-admin.py, passing it your settings module.\n(If the" + " file settings.py does indeed exist, it's causing an ImportError " "somehow.)\n" % __file__) sys.exit(1) @@ -50,4 +50,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/tools/pypy/import_from_json.py b/tools/pypy/import_from_json.py index bd68e8db..5d3eb21d 100644 --- a/tools/pypy/import_from_json.py +++ b/tools/pypy/import_from_json.py @@ -55,7 +55,8 @@ interpreter = INTERP int_options = "" options = "" - if result.has_key('options'): options = result['options'] + if 'options' in result: + options = result['options'] host = 'tannit' #saveresults.save(proj, revision, result['results'], options, interpreter, host) diff --git a/tools/save_multiple_results.py b/tools/save_multiple_results.py index ad6121a8..bc262e1a 100644 --- a/tools/save_multiple_results.py +++ b/tools/save_multiple_results.py @@ -2,7 +2,10 @@ #################################################### # Sample script that shows how to save result data using json # #################################################### -import urllib, urllib2, json +import urllib +import urllib2 +import json + # You need to enter the real URL and have the server running CODESPEED_URL = 'http://localhost:8000/' @@ -28,6 +31,7 @@ } ] + def add(data): #params = urllib.urlencode(data) response = "None" diff --git a/tools/save_single_result.py b/tools/save_single_result.py index 913cbd6d..830858e7 100644 --- a/tools/save_single_result.py +++ b/tools/save_single_result.py @@ -3,7 +3,8 @@ # Sample script that shows how to save result data # #################################################### from datetime import datetime -import urllib, urllib2 +import urllib +import urllib2 # You need to enter the real URL and have the server running CODESPEED_URL = 'http://localhost:8000/' @@ -13,7 +14,7 @@ # Mandatory fields data = { 'commitid': '14', - 'branch': 'default', # Always use default for trunk/master/tip + 'branch': 'default', # Always use default for trunk/master/tip 'project': 'MyProject', 'executable': 'myexe O3 64bits', 'benchmark': 'float', @@ -23,14 +24,15 @@ # Optional fields data.update({ - 'revision_date': current_date, # Optional. Default is taken either - # from VCS integration or from current date - 'result_date': current_date, # Optional, default is current date - 'std_dev': 1.11111, # Optional. Default is blank - 'max': 4001.6, # Optional. Default is blank - 'min': 3995.1, # Optional. Default is blank + 'revision_date': current_date, # Optional. Default is taken either + # from VCS integration or from current date + 'result_date': current_date, # Optional, default is current date + 'std_dev': 1.11111, # Optional. Default is blank + 'max': 4001.6, # Optional. Default is blank + 'min': 3995.1, # Optional. Default is blank }) + def add(data): params = urllib.urlencode(data) response = "None" diff --git a/tools/test_performance.py b/tools/test_performance.py index 90c6cb75..20408da4 100644 --- a/tools/test_performance.py +++ b/tools/test_performance.py @@ -1,9 +1,13 @@ -import timeit, urllib, sys +import timeit +import urllib +import sys + SPEEDURL = 'http://localhost:8000/' benchmarks = ['ai', 'django', 'spambayes', 'grid'] + def test_overview(): data = { "trend": 10, @@ -13,13 +17,14 @@ def test_overview(): "host": "bigdog", } params = urllib.urlencode(data) - page= urllib.urlopen(SPEEDURL + 'overview/table/?' + params) + page = urllib.urlopen(SPEEDURL + 'overview/table/?' + params) jsonstring = page.read() page.close() if not '' in jsonstring: print "bad overview response" sys.exit(1) + def test_timeline(bench): data = { "executables": "1,2,6", @@ -29,7 +34,7 @@ def test_timeline(bench): "revisions": 200 } params = urllib.urlencode(data) - page= urllib.urlopen(SPEEDURL + 'timeline/json/?' + params) + page = urllib.urlopen(SPEEDURL + 'timeline/json/?' + params) jsonstring = page.read() #print jsonstring page.close() @@ -44,13 +49,14 @@ def test_timeline(bench): print print "OVERVIEW RESULTS" print "min:", min(results) - print "avg:", sum(results)/len(results) + print "avg:", sum(results) / len(results) print print "TIMELINE RESULTS" for bench in benchmarks: - t = timeit.Timer('test_timeline("'+bench+'")', 'from __main__ import test_timeline') + t = timeit.Timer('test_timeline("' + bench + '")', + 'from __main__ import test_timeline') results = t.repeat(20, 1) print "benchmark =", bench print "min:", min(results) - print "avg:", sum(results)/len(results) + print "avg:", sum(results) / len(results) print From 257b528b301dfe61aa163f644fa9ae2df9644f70 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 25 Feb 2012 09:42:42 +0100 Subject: [PATCH 124/429] Put words from a comment line to the proper place --- codespeed/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codespeed/models.py b/codespeed/models.py index 9e143ee3..cb16149a 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -447,8 +447,8 @@ def get_changes_table(self, trend_depth=10, force_save=False): totals[key] = "-" if totals['change'] != "-": - # Transform ratio - totals['change'] = (totals['change'] - 1) * 100 to percentage + # Transform ratio to percentage + totals['change'] = (totals['change'] - 1) * 100 if totals['trend'] != "-": # Transform ratio to percentage totals['trend'] = (totals['trend'] - 1) * 100 From 1365ca4bf8a9ded344e93fe02287edab3c2e9593 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 17 May 2012 10:51:49 +0200 Subject: [PATCH 125/429] Require Django 1.3 and isodate Closes #99 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc712b7c..a9a37b18 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ For an overview of some application concepts see the [wiki page](https://github. # Requirements -You will need Python 2.6+ and Django 1.1+ with South and Tastypie. +You will need Python 2.6+ and Django 1.3+ with South and Tastypie. In Debian and Ubuntu, they can be installed with: @@ -19,6 +19,7 @@ Instead of using distribution packages, you can use pip: sudo pip install django sudo pip install South sudo pip install django-tastypie + sudo pip install isodate If you want version control integration, there are additional requirements: From 92ef86db8aecc308624cadd00e89369e931f4e49 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 17 May 2012 10:58:25 +0200 Subject: [PATCH 126/429] Don't execute south migrations for the test DB --- example/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/settings.py b/example/settings.py index b3b32f49..91401f65 100644 --- a/example/settings.py +++ b/example/settings.py @@ -5,7 +5,7 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG -BASEDIR = os.path.dirname(__file__) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) #: The directory which should contain checked out source repositories: REPOSITORY_BASE_PATH = os.path.join(BASEDIR, "repos") @@ -113,9 +113,9 @@ def process_exception(self, request, exception): 'south', 'tastypie', ) +SOUTH_TESTS_MIGRATE = False STATIC_URL = '/static/' - STATIC_ROOT = os.path.join(BASEDIR, "sitestatic") # Codespeed settings that can be overwritten here. From d5e5ebe20822d4fe2f77f84a4ac545d49b7fe43e Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 17 May 2012 11:23:22 +0200 Subject: [PATCH 127/429] Correctly catch repo getlogs() errors --- codespeed/views.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/codespeed/views.py b/codespeed/views.py index 1aec6706..4c903e2e 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -719,7 +719,14 @@ def reports(request): def displaylogs(request): rev = get_object_or_404(Revision, pk=request.GET.get('revisionid')) logs = [] - logs.append(rev) + logs.append( + { + 'date': str(rev.date), 'author': rev.author, + 'author_email': '', 'message': rev.message, + 'short_commit_id': rev.get_short_commitid(), + 'commitid': rev.commitid + } + ) error = False try: startrev = Revision.objects.filter( @@ -740,8 +747,10 @@ def displaylogs(request): logs = remotelogs else: error = 'no logs found' - except StandardError, e: - logger.error("Unhandled exception displaying logs for %s: %s", rev, e, exc_info=True) + except (StandardError, RuntimeError) as e: + logger.error( + "Unhandled exception displaying logs for %s: %s", + rev, e, exc_info=True) error = repr(e) # add commit browsing url to logs From 8ec39fe067770a632ad0a70fd8481d5022e7ddee Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Fri, 27 Apr 2012 12:01:23 +0200 Subject: [PATCH 128/429] added little example on how to create an environment from python --- tools/create_environment.py | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 tools/create_environment.py diff --git a/tools/create_environment.py b/tools/create_environment.py new file mode 100755 index 00000000..f20b8340 --- /dev/null +++ b/tools/create_environment.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +""" +Check if an environment exists, create otherwise +""" +import json +import urllib2 +from optparse import OptionParser, OptionError +from django.utils import simplejson + +CODESPEED_URL='http://speedcenter' + +def get_options(): + """Get the options and arguments + """ + parser = OptionParser() + + parser.add_option("-e", "--environment", dest="environment", + help="name of the environment to create") + + (options, args) = parser.parse_args() + + if not options.environment: + parser.error("No environment given") + + return options, args + +def is_environment(environment): + """check if environment does exist + + return: + True if it exist + False if it doesn't exist + """ + url = CODESPEED_URL + '/api/v1/environment/' + request = urllib2.Request(url) + opener = urllib2.build_opener() + try: + raw_data = opener.open(request) + except urllib2.HTTPError as e: + raise e + data = simplejson.load(raw_data) + if environment in [ env['name'] for env in data['objects']]: + return True + return False + +def create_environment(environment): + """create the environment + + return: + True if success + False if not created + """ + url = CODESPEED_URL + '/api/v1/environment/' + data = json.dumps({'name': environment}) + request = urllib2.Request(url, data, {'Content-Type': 'application/json'}) + try: + f = urllib2.urlopen(request) + response = f.read() + f.close() + except urllib2.HTTPError as e: + raise e + return response + +def main(): + (options, args) = get_options() + if is_environment(options.environment): + print "Found environment, doing nothing." + else: + print create_environment(options.environment) + +if __name__ == "__main__": + main() + From 73dcdb0ad1b2e43c249d61e1e025f0f570080405 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 26 May 2012 00:15:46 +0200 Subject: [PATCH 129/429] Added tests for Environment() to be changed to ApiKeyAuthentication and DjangoAuthorization - So far the API did not require any authentication and did not support any authorization. Starting with the simple Environment() model that will be changed by the next commit. Here are the supporting tests. They can be cleaned up in the future a little. - Some tests were copied from the django-tastypie test suite. That is not to check upon tastypie but to verify that tastypie is used correctly in codespeed. Change-Id: I70f083e1aa723b69c32a05ba2b8187fed2b5ebb8 --- codespeed/tests/tests_api.py | 346 +++++++++++++++++++++++++++++++++-- 1 file changed, 332 insertions(+), 14 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 62d2c900..1cb44e76 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -15,11 +15,13 @@ from django.http import HttpRequest from django.core.urlresolvers import reverse from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Permission +from tastypie.authorization import Authorization, ReadOnlyAuthorization, DjangoAuthorization from tastypie.exceptions import ImmediateHttpResponse from tastypie.models import ApiKey, create_api_key from tastypie.http import HttpUnauthorized -from tastypie.authentication import Authentication, ApiKeyAuthentication +from tastypie.authentication import ApiKeyAuthentication +from codespeed.api import EnvironmentResource from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) from codespeed.api import ResultBundle @@ -34,6 +36,66 @@ def setUp(self): self.api_user = User.objects.create_user( username='apiuser', email='api@foo.bar', password='password') self.api_user.save() + #create_api_key(User, instance=self.api_user, created=True) + john_doe = User.objects.create_user( + username='johndoe', email='api@foo.bar', password='password') + john_doe.save() + + +class UserTest(FixtureTestCase): + """Test api user related stuff""" + + def test_has_apikey(self): + self.assertTrue(hasattr(self.api_user, 'api_key')) + + def test_len_apikey(self): + """Test the key has a length""" + self.assertTrue(len(self.api_user.api_key.key) >= 1) + + def test_is_authenticated_header(self): + """Taken from tastypie test suite to ensure api key is generated for + new users correctly. + """ + + auth = ApiKeyAuthentication() + request = HttpRequest() + + # Simulate sending the signal. + john_doe = User.objects.get(username='johndoe') + #create_api_key(User, instance=john_doe, created=True) + + # No username/api_key details should fail. + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Wrong username details. + request.META['HTTP_AUTHORIZATION'] = 'foo' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # No api_key. + request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Wrong user/api_key. + request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel:pass' + self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) + + # Correct user/api_key. + john_doe = User.objects.get(username='johndoe') + request.META['HTTP_AUTHORIZATION'] = 'ApiKey johndoe:%s' % john_doe.api_key.key + self.assertEqual(auth.is_authenticated(request), True) + + # Capitalization shouldn't matter. + john_doe = User.objects.get(username='johndoe') + request.META['HTTP_AUTHORIZATION'] = 'aPiKeY johndoe:%s' % john_doe.api_key.key + self.assertEqual(auth.is_authenticated(request), True) + + def test_api_key(self): + # Correct user/api_key. + auth = ApiKeyAuthentication() + request = HttpRequest() + authorization='ApiKey %s:%s' % (self.api_user.username, self.api_user.api_key.key) + request.META['HTTP_AUTHORIZATION'] = authorization + self.assertEqual(auth.is_authenticated(request), True) class EnvironmentTest(FixtureTestCase): @@ -63,13 +125,13 @@ def setUp(self): self.client = Client() super(EnvironmentTest, self).setUp() - def test_get_environment(self): + def xtest_get_environment(self): """Should get an existing environment""" response = self.client.get('/api/v1/environment/1/') self.assertEquals(response.status_code, 200) self.assertEqual(json.loads(response.content)['name'], "Dual Core") - def test_get_environment_all_fields(self): + def xtest_get_environment_all_fields(self): """Should get all fields for an environment""" response = self.client.get('/api/v1/environment/%s/' % (self.env1.id,)) self.assertEquals(response.status_code, 200) @@ -77,7 +139,7 @@ def test_get_environment_all_fields(self): self.assertEqual( json.loads(response.content)[k], getattr(self.env1, k)) - def test_post(self): + def xtest_post(self): """Should save a new environment""" response = self.client.post('/api/v1/environment/', data=json.dumps(self.env2_data), @@ -92,7 +154,7 @@ def test_post(self): content_type='application/json') self.assertEquals(response.status_code, 204) - def test_put(self): + def xtest_put(self): """Should modify an existing environment""" modified_data = copy.deepcopy(self.env_db1_data) modified_data['name'] = "env2.2" @@ -106,7 +168,7 @@ def test_put(self): self.assertEqual( json.loads(response.content)[k], v) - def test_delete(self): + def xtest_delete(self): """Should delete an environment""" response = self.client.get('/api/v1/environment/1/') self.assertEquals(response.status_code, 200) @@ -132,6 +194,269 @@ def test_delete(self): self.assertEquals(response.status_code, 404) +class EnvironmentDjangoAuthorizationTestCase(FixtureTestCase): + """Test Environment() API""" + + def setUp(self): + super(EnvironmentDjangoAuthorizationTestCase, self).setUp() + self.add = Permission.objects.get_by_natural_key( + 'add_environment', 'codespeed', 'environment') + self.change = Permission.objects.get_by_natural_key( + 'change_environment', 'codespeed', 'environment') + self.delete = Permission.objects.get_by_natural_key( + 'delete_environment', 'codespeed', 'environment') + #self.user = User.objects.all()[0] + self.api_user.user_permissions.clear() + self.env1_data = dict( + name="env1", + cpu="cpu1", + memory="48kB", + os="ZX Spectrum OS", + kernel="2.6.32" + ) + self.env1 = Environment(**self.env1_data) + self.env1.save() + self.env2_data = dict( + name="env2", + cpu="z80", + memory="64kB", + os="ZX Spectrum OS", + kernel="2.6.32" + ) + env_db1 = Environment.objects.get(id=1) + self.env_db1_data = dict( + [(k, getattr(env_db1, k)) for k in self.env1_data.keys()] + ) + self.client = Client() + authorization='ApiKey {0}:{1}'.format(self.api_user.username, + self.api_user.api_key.key) + self.post_auth = {'HTTP_AUTHORIZATION': authorization} + + def test_no_perms(self): + # sanity check: user has no permissions + self.assertFalse(self.api_user.get_all_permissions()) + + request = HttpRequest() + request.method = 'GET' + request.user = self.api_user + # with no permissions, api is read-only + self.assertTrue(EnvironmentResource()._meta.authorization.is_authorized( + request)) + + for method in ('POST', 'PUT', 'DELETE'): + request.method = method + self.assertFalse( + EnvironmentResource()._meta.authorization.is_authorized(request) + ) + + def test_add_perm(self): + request = HttpRequest() + request.user = self.api_user + + # give add permission + request.user.user_permissions.add(self.add) + request.method = 'POST' + self.assertTrue( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + def test_change_perm(self): + request = HttpRequest() + request.user = self.api_user + + # give change permission + request.user.user_permissions.add(self.change) + request.method = 'PUT' + self.assertTrue( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + def test_delete_perm(self): + request = HttpRequest() + request.user = self.api_user + + # give delete permission + request.user.user_permissions.add(self.delete) + request.method = 'DELETE' + self.assertTrue( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + def test_all(self): + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + + for method in ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'DELETE', + 'PATCH'): + request.method = method + self.assertTrue( + EnvironmentResource()._meta.authorization.is_authorized(request) + ) + + def test_patch_perms(self): + request = HttpRequest() + request.user = self.api_user + request.method = 'PATCH' + + # Not enough. + request.user.user_permissions.add(self.add) + self.assertFalse( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + # Still not enough. + request.user.user_permissions.add(self.change) + self.assertFalse( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + # Much better. + request.user.user_permissions.add(self.delete) + # Nuke the perm cache. :/ + del request.user._perm_cache + self.assertTrue( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + def test_unrecognized_method(self): + request = HttpRequest() + self.api_user.user_permissions.clear() + request.user = self.api_user + + # Check a non-existent HTTP method. + request.method = 'EXPLODE' + self.assertFalse( + EnvironmentResource()._meta.authorization.is_authorized(request)) + + def test_get_environment(self): + """Should get an existing environment""" + response = self.client.get( + '/api/v1/environment/1/?username={0}&api_key={1}'.format( + self.api_user.username, self.api_user.api_key.key)) + self.assertEquals(response.status_code, 200) + self.assertEqual(json.loads(response.content)['name'], "Dual Core") + + def test_get_environment_all_fields(self): + """Should get all fields for an environment""" + response = self.client.get( + '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( + self.env1.pk, self.api_user.username, self.api_user.api_key.key) + ) + self.assertEquals(response.status_code, 200) + for k in self.env1_data.keys(): + self.assertEqual( + json.loads(response.content)[k], getattr(self.env1, k)) + + def test_post(self): + """Should save a new environment""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + + response = self.client.post('/api/v1/environment/', + data=json.dumps(self.env2_data), + content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/environment/', + data=json.dumps(self.env2_data), + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 201) + id = response['Location'].rsplit('/', 2)[-2] + #response = self.client.get('/api/v1/environment/{0}/'.format(id)) + response = self.client.get( + '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( + id, self.api_user.username, self.api_user.api_key.key) + ) + for k, v in self.env2_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + response = self.client.delete('/api/v1/environment/{0}/'.format(id), + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 204) + + def test_put(self): + """Should modify an existing environment""" + modified_data = copy.deepcopy(self.env_db1_data) + modified_data['name'] = "env2.2" + modified_data['memory'] = "128kB" + response = self.client.put('/api/v1/environment/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 401) + + request = HttpRequest() + request.user = self.api_user + request.user.user_permissions.add(self.change) + + response = self.client.put('/api/v1/environment/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 204) + response = self.client.get('/api/v1/environment/1/') + response = self.client.get( + '/api/v1/environment/1/?username={0}&api_key={1}'.format( + self.api_user.username, self.api_user.api_key.key) + ) + for k, v in modified_data.items(): + self.assertEqual( + json.loads(response.content)[k], v) + + def test_delete(self): + """Should delete an environment""" + response = self.client.get( + '/api/v1/environment/1/?username={0}&api_key={1}'.format( + self.api_user.username, self.api_user.api_key.key)) + self.assertEquals(response.status_code, 200) + # from fixture + response = self.client.delete('/api/v1/environment/1/', + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 401) + + request = HttpRequest() + request.user = self.api_user + request.user.user_permissions.add(self.delete) + response = self.client.delete('/api/v1/environment/1/', + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 204) + + response = self.client.get( + '/api/v1/environment/1/?username={0}&api_key={1}'.format( + self.api_user.username, self.api_user.api_key.key)) + self.assertEquals(response.status_code, 404) + + # from just created data + response = self.client.get( + '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( + self.env1.pk, self.api_user.username, self.api_user.api_key.key) + ) + self.assertEquals(response.status_code, 200) + response = self.client.delete( + '/api/v1/environment/{0}/'.format(self.env1.id), + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 204) + + response = self.client.get( + '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( + self.env1.pk, self.api_user.username, self.api_user.api_key.key) + ) + self.assertEquals(response.status_code, 404) + + class ProjectTest(FixtureTestCase): """Test Project() API""" @@ -667,13 +992,6 @@ def test_delete(self): self.assertEquals(response.status_code, 405) -class UserTest(FixtureTestCase): - """Test api user related stuff""" - - def test_has_apikey(self): - self.assertTrue(hasattr(self.api_user, 'api_key')) - - class ApiKeyAuthenticationTestCase(FixtureTestCase): def setUp(self): From 6f8f0185f0d1ff4cab49c4b7e7ddd6520e25605e Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 26 May 2012 00:21:42 +0200 Subject: [PATCH 130/429] Changed UserResource and EnvironmentResource to ApiAuthtentication and DjangoAuthorisation - The UserResource and EnvironmentResource are authenticated by the api key generated at user creation. Key handling is described in the Tastypie docu. - Both Resources are authorized via Django (E. g. changeable in the Admin). - For now even GET requests to the API have to be authenticated. That could be relaxed in the future. Change-Id: Ica28762aeff9cdde3a8099bdd6098ccd191c416c --- codespeed/api.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index a7d80207..de864949 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -43,7 +43,8 @@ from tastypie.resources import ModelResource, Resource from tastypie import fields from tastypie.authorization import Authorization, DjangoAuthorization -from tastypie.authentication import ApiKeyAuthentication +#from tastypie.authentication import ApiKeyAuthentication, Authentication, MultiAuthentication +from tastypie.authentication import Authentication, ApiKeyAuthentication, MultiAuthentication from tastypie.models import create_api_key from tastypie.utils.dict import dict_strip_unicode_keys from codespeed.models import (Environment, Project, Result, Branch, Revision, @@ -63,9 +64,9 @@ class Meta: allowed_methods = ['get'] #excludes = ['email', 'password', 'is_superuser'] # Add it here. - #authorization = DjangoAuthorization() - authorization = Authorization() - #authentication = ApiKeyAuthentication() + authorization = DjangoAuthorization() + #authorization = Authorization() + authentication = ApiKeyAuthentication() class ProjectResource(ModelResource): @@ -121,7 +122,10 @@ class EnvironmentResource(ModelResource): class Meta: queryset = Environment.objects.all() resource_name = 'environment' - authorization = Authorization() + authorization = DjangoAuthorization() + #authorization = Authorization() + authentication = ApiKeyAuthentication() + #authentication = MultiAuthentication(Authentication(), ApiKeyAuthentication()) class ResultResource(ModelResource): From 78df42aef37cffbd77859e01d18aabbbf5d306e8 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 26 May 2012 00:26:22 +0200 Subject: [PATCH 131/429] added a few more Middleware classes - Even if not required some additional Middleware classes are added to ensure compatibility. Change-Id: I91119d5e6e011f351ffde7bd1380e4e36af5ee98 --- example/settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/example/settings.py b/example/settings.py index 91401f65..ae030876 100644 --- a/example/settings.py +++ b/example/settings.py @@ -63,10 +63,17 @@ # 'django.template.loaders.eggs.load_template_source', ) +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + # 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', ) if DEBUG: From 021941a573432d2d609c622385db1eff2c2bc7ab Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 00:09:27 +0200 Subject: [PATCH 132/429] add tests for Project() authentication - Project() will use MultiAuthentication(ApiKeyAuthentication(), Authentication()) Thus, only GET requests will not need authentication. - Move user setup to generic FixtureTestCase class. That is common to all TestCases Change-Id: I5893faa293f32ab1c07a6523a951879933a60560 --- codespeed/tests/tests_api.py | 169 ++++++++++++----------------------- 1 file changed, 58 insertions(+), 111 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 1cb44e76..990a41dd 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -21,7 +21,7 @@ from tastypie.models import ApiKey, create_api_key from tastypie.http import HttpUnauthorized from tastypie.authentication import ApiKeyAuthentication -from codespeed.api import EnvironmentResource +from codespeed.api import EnvironmentResource, ProjectResource from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) from codespeed.api import ResultBundle @@ -36,10 +36,13 @@ def setUp(self): self.api_user = User.objects.create_user( username='apiuser', email='api@foo.bar', password='password') self.api_user.save() - #create_api_key(User, instance=self.api_user, created=True) + self.api_user.user_permissions.clear() john_doe = User.objects.create_user( username='johndoe', email='api@foo.bar', password='password') john_doe.save() + authorization='ApiKey {0}:{1}'.format(self.api_user.username, + self.api_user.api_key.key) + self.post_auth = {'HTTP_AUTHORIZATION': authorization} class UserTest(FixtureTestCase): @@ -62,7 +65,6 @@ def test_is_authenticated_header(self): # Simulate sending the signal. john_doe = User.objects.get(username='johndoe') - #create_api_key(User, instance=john_doe, created=True) # No username/api_key details should fail. self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) @@ -98,102 +100,6 @@ def test_api_key(self): self.assertEqual(auth.is_authenticated(request), True) -class EnvironmentTest(FixtureTestCase): - """Test Environment() API""" - - def setUp(self): - self.env1_data = dict( - name="env1", - cpu="cpu1", - memory="48kB", - os="ZX Spectrum OS", - kernel="2.6.32" - ) - self.env1 = Environment(**self.env1_data) - self.env1.save() - self.env2_data = dict( - name="env2", - cpu="z80", - memory="64kB", - os="ZX Spectrum OS", - kernel="2.6.32" - ) - env_db1 = Environment.objects.get(id=1) - self.env_db1_data = dict( - [(k, getattr(env_db1, k)) for k in self.env1_data.keys()] - ) - self.client = Client() - super(EnvironmentTest, self).setUp() - - def xtest_get_environment(self): - """Should get an existing environment""" - response = self.client.get('/api/v1/environment/1/') - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], "Dual Core") - - def xtest_get_environment_all_fields(self): - """Should get all fields for an environment""" - response = self.client.get('/api/v1/environment/%s/' % (self.env1.id,)) - self.assertEquals(response.status_code, 200) - for k in self.env1_data.keys(): - self.assertEqual( - json.loads(response.content)[k], getattr(self.env1, k)) - - def xtest_post(self): - """Should save a new environment""" - response = self.client.post('/api/v1/environment/', - data=json.dumps(self.env2_data), - content_type='application/json') - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - response = self.client.get('/api/v1/environment/{0}/'.format(id)) - for k, v in self.env2_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - response = self.client.delete('/api/v1/environment/{0}/'.format(id), - content_type='application/json') - self.assertEquals(response.status_code, 204) - - def xtest_put(self): - """Should modify an existing environment""" - modified_data = copy.deepcopy(self.env_db1_data) - modified_data['name'] = "env2.2" - modified_data['memory'] = "128kB" - response = self.client.put('/api/v1/environment/1/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/environment/1/') - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def xtest_delete(self): - """Should delete an environment""" - response = self.client.get('/api/v1/environment/1/') - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/environment/1/', - content_type='application/json') - self.assertEquals(response.status_code, 204) - - response = self.client.get('/api/v1/environment/1/') - self.assertEquals(response.status_code, 404) - - # from just created data - response = self.client.get( - '/api/v1/environment/{0}/'.format(self.env1.id)) - self.assertEquals(response.status_code, 200) - response = self.client.delete( - '/api/v1/environment/{0}/'.format(self.env1.id), - content_type='application/json') - self.assertEquals(response.status_code, 204) - - response = self.client.get( - '/api/v1/environment/{0}/'.format(self.env1.id)) - self.assertEquals(response.status_code, 404) - - class EnvironmentDjangoAuthorizationTestCase(FixtureTestCase): """Test Environment() API""" @@ -205,8 +111,6 @@ def setUp(self): 'change_environment', 'codespeed', 'environment') self.delete = Permission.objects.get_by_natural_key( 'delete_environment', 'codespeed', 'environment') - #self.user = User.objects.all()[0] - self.api_user.user_permissions.clear() self.env1_data = dict( name="env1", cpu="cpu1", @@ -228,9 +132,6 @@ def setUp(self): [(k, getattr(env_db1, k)) for k in self.env1_data.keys()] ) self.client = Client() - authorization='ApiKey {0}:{1}'.format(self.api_user.username, - self.api_user.api_key.key) - self.post_auth = {'HTTP_AUTHORIZATION': authorization} def test_no_perms(self): # sanity check: user has no permissions @@ -461,6 +362,13 @@ class ProjectTest(FixtureTestCase): """Test Project() API""" def setUp(self): + super(ProjectTest, self).setUp() + self.add = Permission.objects.get_by_natural_key( + 'add_project', 'codespeed', 'project') + self.change = Permission.objects.get_by_natural_key( + 'change_project', 'codespeed', 'project') + self.delete = Permission.objects.get_by_natural_key( + 'delete_project', 'codespeed', 'project') self.project_data = dict( name="PyPy", repo_type="M", @@ -478,7 +386,21 @@ def setUp(self): self.project = Project(**self.project_data) self.project.save() self.client = Client() - super(ProjectTest, self).setUp() + + def test_all(self): + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + + for method in ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'DELETE', + 'PATCH'): + request.method = method + self.assertTrue( + ProjectResource()._meta.authorization.is_authorized(request) + ) def test_get_project(self): """Should get an existing project""" @@ -498,12 +420,28 @@ def test_get_project_all_fields(self): def test_post(self): """Should save a new project""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + response = self.client.post('/api/v1/project/', data=json.dumps(self.project_data2), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/project/', + data=json.dumps(self.project_data2), + content_type='application/json', + **self.post_auth + ) self.assertEquals(response.status_code, 201) - response = self.client.get('/api/v1/project/{0}/'.format( - self.project.id)) + id = response['Location'].rsplit('/', 2)[-2] + response = self.client.get( + '/api/v1/project/{0}/?username={1}&api_key={2}'.format( + self.project.id, self.api_user.username, self.api_user.api_key.key) + ) for k, v in self.project_data.items(): self.assertEqual( json.loads(response.content)[k], v) @@ -511,12 +449,21 @@ def test_post(self): def test_delete(self): """Should delete an project""" response = self.client.delete('/api/v1/project/{0}/'.format( - self.project.id,), - content_type='application/json') - self.assertEquals(response.status_code, 204) + self.project.id,), content_type='application/json') + self.assertEquals(response.status_code, 401) + request = HttpRequest() + request.user = self.api_user + request.user.user_permissions.add(self.delete) + response = self.client.delete( + '/api/v1/project/{0}/'.format(self.project.id,), + content_type='application/json', + **self.post_auth + ) + self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/project/{0}/'.format( - self.project.id,)) + self.project.id,) + ) self.assertEquals(response.status_code, 404) From 7e625b382f57d95fde0c89ae77ef4695a164ecab Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 00:13:48 +0200 Subject: [PATCH 133/429] add MultiAuthentication(ApiKeyAuthentication(), Authentication()) to Project() - now the ProjectResource is authenticated by an api key for everything but GET requests. Change-Id: Ie238744bee0e44780d2f9dacc72e5fe0f708b7bc --- codespeed/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index de864949..87307227 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -74,7 +74,9 @@ class ProjectResource(ModelResource): class Meta: queryset = Project.objects.all() - authorization = Authorization() + authorization = DjangoAuthorization() + # Note, the order for MultiAuthentication matters! + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class BranchResource(ModelResource): From 829eb226026d172ca173b8c5e40a5dd205691bb8 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 10:37:23 +0200 Subject: [PATCH 134/429] added tests for BranchResource authtentication and authorization Change-Id: I7b286fc4dc3b0a6e80b441cd085b15daf79893a0 --- codespeed/tests/tests_api.py | 43 ++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 990a41dd..7c834337 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -535,6 +535,13 @@ class BranchTest(FixtureTestCase): """Test Branch() API""" def setUp(self): + super(BranchTest, self).setUp() + self.add = Permission.objects.get_by_natural_key( + 'add_branch', 'codespeed', 'branch') + self.change = Permission.objects.get_by_natural_key( + 'change_branch', 'codespeed', 'branch') + self.delete = Permission.objects.get_by_natural_key( + 'delete_branch', 'codespeed', 'branch') self.branch1 = Branch.objects.get(pk=1) self.project_data = dict( name="PyPy", @@ -550,7 +557,6 @@ def setUp(self): project='/api/v1/project/{0}/'.format(self.project.id) ) self.client = Client() - super(BranchTest, self).setUp() def test_get_branch(self): """Should get an existing branch""" @@ -573,10 +579,22 @@ def test_get_branch_all_fields(self): def test_post(self): """Should save a new branch""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.delete) + modified_data = copy.deepcopy(self.branch2_data) response = self.client.post('/api/v1/branch/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/branch/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth + ) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get('/api/v1/branch/{0}/'.format(id)) @@ -584,16 +602,28 @@ def test_post(self): self.assertEqual( json.loads(response.content)[k], v) response = self.client.delete('/api/v1/branch/{0}/'.format(id), - content_type='application/json') + content_type='application/json', + **self.post_auth + ) self.assertEquals(response.status_code, 204) def test_put(self): """Should modify an existing environment""" + request = HttpRequest() + request.user = self.api_user + request.user.user_permissions.add(self.change) + modified_data = copy.deepcopy(self.branch2_data) modified_data['name'] = "tip" response = self.client.put('/api/v1/branch/1/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.put('/api/v1/branch/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth + ) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/branch/1/') for k, v in modified_data.items(): @@ -602,11 +632,20 @@ def test_put(self): def test_delete(self): """Should delete a branch""" + request = HttpRequest() + request.user = self.api_user + request.user.user_permissions.add(self.delete) + response = self.client.get('/api/v1/branch/1/') self.assertEquals(response.status_code, 200) # from fixture response = self.client.delete('/api/v1/branch/1/', content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.delete('/api/v1/branch/1/', + content_type='application/json', + **self.post_auth + ) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/branch/1/') From 9f8c2494d668397ac6f80de3a65eefa197952de0 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 11:08:58 +0200 Subject: [PATCH 135/429] added BranchResource authentication and authorization Change-Id: I94242079ce6f140d6b02684cd4150b35acaec06e --- codespeed/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index 87307227..2cacffbd 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -86,7 +86,8 @@ class BranchResource(ModelResource): class Meta: queryset = Branch.objects.all() - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class RevisionResource(ModelResource): From bfbbdfd8cb6363bb011bc5ff04b9eb4af9ac7ddc Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 11:39:51 +0200 Subject: [PATCH 136/429] added tests for RevisionResource authentication and authorization Change-Id: I95110d4e7f613d5eebc0d1ee072fdd585936bd21 --- codespeed/tests/tests_api.py | 97 ++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 7c834337..3de11786 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -262,8 +262,7 @@ def test_post(self): response = self.client.post('/api/v1/environment/', data=json.dumps(self.env2_data), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] #response = self.client.get('/api/v1/environment/{0}/'.format(id)) @@ -276,8 +275,7 @@ def test_post(self): json.loads(response.content)[k], v) response = self.client.delete('/api/v1/environment/{0}/'.format(id), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 204) def test_put(self): @@ -288,8 +286,7 @@ def test_put(self): response = self.client.put('/api/v1/environment/1/', data=json.dumps(modified_data), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 401) request = HttpRequest() @@ -299,8 +296,7 @@ def test_put(self): response = self.client.put('/api/v1/environment/1/', data=json.dumps(modified_data), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/environment/1/') response = self.client.get( @@ -320,8 +316,7 @@ def test_delete(self): # from fixture response = self.client.delete('/api/v1/environment/1/', content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 401) request = HttpRequest() @@ -329,8 +324,7 @@ def test_delete(self): request.user.user_permissions.add(self.delete) response = self.client.delete('/api/v1/environment/1/', content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get( @@ -434,8 +428,7 @@ def test_post(self): response = self.client.post('/api/v1/project/', data=json.dumps(self.project_data2), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get( @@ -593,8 +586,7 @@ def test_post(self): response = self.client.post('/api/v1/branch/', data=json.dumps(modified_data), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get('/api/v1/branch/{0}/'.format(id)) @@ -603,8 +595,7 @@ def test_post(self): json.loads(response.content)[k], v) response = self.client.delete('/api/v1/branch/{0}/'.format(id), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 204) def test_put(self): @@ -622,8 +613,7 @@ def test_put(self): response = self.client.put('/api/v1/branch/1/', data=json.dumps(modified_data), content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/branch/1/') for k, v in modified_data.items(): @@ -644,8 +634,7 @@ def test_delete(self): self.assertEquals(response.status_code, 401) response = self.client.delete('/api/v1/branch/1/', content_type='application/json', - **self.post_auth - ) + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/branch/1/') @@ -656,6 +645,13 @@ class RevisionTest(FixtureTestCase): """Test Revision() API""" def setUp(self): + super(RevisionTest, self).setUp() + self.add = Permission.objects.get_by_natural_key( + 'add_revision', 'codespeed', 'revision') + self.change = Permission.objects.get_by_natural_key( + 'change_revision', 'codespeed', 'revision') + self.delete = Permission.objects.get_by_natural_key( + 'delete_revision', 'codespeed', 'revision') DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S' self.branch1 = Branch.objects.get(pk=1) self.project1 = Project.objects.get(pk=1) @@ -680,7 +676,6 @@ def setUp(self): branch='/api/v1/branch/{0}/'.format(self.branch1.id), ) self.client = Client() - super(RevisionTest, self).setUp() def test_get_revision(self): """Should get an existing revision""" @@ -707,10 +702,22 @@ def test_get_revision_all_fields(self): def test_post(self): """Should save a new revision""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + modified_data = copy.deepcopy(self.revision2_data) response = self.client.post('/api/v1/revision/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/revision/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get('/api/v1/revision/{0}/'.format(id)) @@ -718,16 +725,29 @@ def test_post(self): self.assertEqual( json.loads(response.content)[k], v) response = self.client.delete('/api/v1/revision/{0}/'.format(id), - content_type='application/json') + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) def test_put(self): """Should modify an existing revision""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + modified_data = copy.deepcopy(self.revision2_data) modified_data['tag'] = "v0.9.1" response = self.client.put('/api/v1/revision/1/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.put('/api/v1/revision/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/revision/1/') for k, v in modified_data.items(): @@ -736,11 +756,20 @@ def test_put(self): def test_delete(self): """Should delete a revision""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.delete) + response = self.client.get('/api/v1/revision/1/') self.assertEquals(response.status_code, 200) # from fixture response = self.client.delete('/api/v1/revision/1/', content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.delete('/api/v1/revision/1/', + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/revision/1/') @@ -751,6 +780,15 @@ class ExecutableTest(FixtureTestCase): """Test Executable() API""" def setUp(self): + super(ExecutableTest, self).setUp() + + self.add = Permission.objects.get_by_natural_key( + 'add_executable', 'codespeed', 'executable') + self.change = Permission.objects.get_by_natural_key( + 'change_executable', 'codespeed', 'executable') + self.delete = Permission.objects.get_by_natural_key( + 'delete_executable', 'codespeed', 'executable') + self.executable1 = Executable.objects.get(pk=1) self.project1 = Project.objects.get(pk=1) self.executable2_data = dict( @@ -759,7 +797,6 @@ def setUp(self): project= '/api/v1/project/{0}/'.format(self.project1.id), ) self.client = Client() - super(ExecutableTest, self).setUp() def test_get_executable(self): """Should get an existing executable""" @@ -784,10 +821,20 @@ def test_get_executable_all_fields(self): def test_post(self): """Should save a new executable""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + modified_data = copy.deepcopy(self.executable2_data) response = self.client.post('/api/v1/executable/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/executable/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get('/api/v1/executable/{0}/'.format(id)) From b5ed4fca0c8ae52f81d95dfb321dea345dd6606e Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 11:40:06 +0200 Subject: [PATCH 137/429] added RevisionResource authentication and authorization Change-Id: I693923c1ed5305b20075aa1591938c5cc0cf41cc --- codespeed/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 2cacffbd..6165aeb5 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -98,7 +98,8 @@ class RevisionResource(ModelResource): class Meta: queryset = Revision.objects.all() - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class ExecutableResource(ModelResource): @@ -108,7 +109,8 @@ class ExecutableResource(ModelResource): class Meta: queryset = Executable.objects.all() - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class BenchmarkResource(ModelResource): From a7e885685f3560ff70e711f73d8334436edd9f21 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 11:45:18 +0200 Subject: [PATCH 138/429] added tests for ExecutableResource authentication and authorization Change-Id: I8c870d82fba103506a212bab013910d19e30f4b9 --- codespeed/tests/tests_api.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 3de11786..ecfcb166 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -841,17 +841,31 @@ def test_post(self): for k, v in self.executable2_data.items(): self.assertEqual( json.loads(response.content)[k], v) + request.user.user_permissions.add(self.delete) response = self.client.delete('/api/v1/executable/{0}/'.format(id), - content_type='application/json') + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) def test_put(self): """Should modify an existing environment""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + modified_data = copy.deepcopy(self.executable2_data) modified_data['name'] = "django" response = self.client.put('/api/v1/executable/1/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.put('/api/v1/executable/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/executable/1/') for k, v in modified_data.items(): @@ -860,11 +874,20 @@ def test_put(self): def test_delete(self): """Should delete a executable""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.delete) + response = self.client.get('/api/v1/executable/1/') self.assertEquals(response.status_code, 200) # from fixture response = self.client.delete('/api/v1/executable/1/', content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.delete('/api/v1/executable/1/', + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/executable/1/') From 995d557ed1285c96018ec1751d85be89c3efedb9 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 12:14:51 +0200 Subject: [PATCH 139/429] added tests for BenchmarkResource authentication and authorization Change-Id: I2c63ed76070431b6737ff55aaafdc9bfbab6669d --- codespeed/tests/tests_api.py | 43 ++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index ecfcb166..3df57ef3 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -898,6 +898,13 @@ class BenchmarkTest(FixtureTestCase): """Test Benchmark() API""" def setUp(self): + super(BenchmarkTest, self).setUp() + self.add = Permission.objects.get_by_natural_key( + 'add_benchmark', 'codespeed', 'benchmark') + self.change = Permission.objects.get_by_natural_key( + 'change_benchmark', 'codespeed', 'benchmark') + self.delete = Permission.objects.get_by_natural_key( + 'delete_benchmark', 'codespeed', 'benchmark') self.benchmark1 = Benchmark.objects.get(pk=1) self.benchmark2_data = dict( name="sleep", @@ -911,7 +918,6 @@ def setUp(self): self.benchmark2 = Benchmark(**self.benchmark2_data) self.benchmark2.save() self.client = Client() - super(BenchmarkTest, self).setUp() def test_get_benchmark(self): """Should get an existing benchmark""" @@ -932,28 +938,53 @@ def test_get_benchmark_all_fields(self): def test_post(self): """Should save a new benchmark""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + modified_data = copy.deepcopy(self.benchmark2_data) modified_data['name'] = 'wake' response = self.client.post('/api/v1/benchmark/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/benchmark/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth + ) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get('/api/v1/benchmark/{0}/'.format(id)) for k, v in modified_data.items(): self.assertEqual( json.loads(response.content)[k], v) + request.user.user_permissions.add(self.delete) response = self.client.delete('/api/v1/benchmark/{0}/'.format(id), - content_type='application/json') + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) def test_put(self): """Should modify an existing benchmark""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + modified_data = copy.deepcopy(self.benchmark2_data) modified_data['name'] = "django" response = self.client.put('/api/v1/benchmark/1/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.put('/api/v1/benchmark/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/benchmark/1/') for k, v in modified_data.items(): @@ -962,11 +993,19 @@ def test_put(self): def test_delete(self): """Should delete a benchmark""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.delete) response = self.client.get('/api/v1/benchmark/1/') self.assertEquals(response.status_code, 200) # from fixture response = self.client.delete('/api/v1/benchmark/1/', content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.delete('/api/v1/benchmark/1/', + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) response = self.client.get('/api/v1/benchmark/1/') From 2c1f84a38aad171a0dfd9a412e0c689bee1e7b35 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 12:15:02 +0200 Subject: [PATCH 140/429] added BenchmarkResource authentication and authorization Change-Id: I7174ede3ee51270f9b1b75b8b9fde9431c3e6657 --- codespeed/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index 6165aeb5..bddf8f7f 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -118,7 +118,8 @@ class BenchmarkResource(ModelResource): class Meta: queryset = Benchmark.objects.all() - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class EnvironmentResource(ModelResource): From 2fc6c541d8c4ccb33b993c1feea7b0bc70908af4 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 21:19:16 +0200 Subject: [PATCH 141/429] added tests for ReportResource authentication and authorization Change-Id: Iefc4c8b344ff984f19fd4479776aaaa6f7ae0291 --- codespeed/tests/tests_api.py | 46 +++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 3df57ef3..eccd888e 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -1016,6 +1016,14 @@ class ReportTest(FixtureTestCase): """Test Report() API""" def setUp(self): + super(ReportTest, self).setUp() + self.add = Permission.objects.get_by_natural_key( + 'add_report', 'codespeed', 'report') + self.change = Permission.objects.get_by_natural_key( + 'change_report', 'codespeed', 'report') + self.delete = Permission.objects.get_by_natural_key( + 'delete_report', 'codespeed', 'report') + self.report1 = Report.objects.get(pk=1) self.revision1 = Revision.objects.get(pk=1) self.executable1 = Executable.objects.get(pk=1) @@ -1041,7 +1049,6 @@ def setUp(self): executable='/api/v1/executable/{0}/'.format(self.executable2.id), ) self.client = Client() - super(ReportTest, self).setUp() def test_get_report(self): """Should get an existing report""" @@ -1061,30 +1068,63 @@ def test_get_report_all_fields(self): self.assertEqual(json.loads(response.content)[k], v) def test_post(self): - """Should save a new report""" + """Should not save a new report""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + modified_data = copy.deepcopy(self.report2_data) response = self.client.post('/api/v1/report/', data=json.dumps(modified_data), content_type='application/json') + self.assertEquals(response.status_code, 405) + # next has to be 405 (method not allowed), + # otherwise would raise IntegrityError + response = self.client.post('/api/v1/report/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) # next has to be 405, otherwise would raise IntegrityError self.assertEquals(response.status_code, 405) def test_put(self): - """Should modify an existing report""" + """Should not modify an existing report""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + request.user.user_permissions.add(self.change) + request.user.user_permissions.add(self.delete) + modified_data = copy.deepcopy(self.report2_data) response = self.client.put('/api/v1/report/1/', data=json.dumps(modified_data), content_type='application/json') self.assertEquals(response.status_code, 405) + response = self.client.put('/api/v1/report/1/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) + self.assertEquals(response.status_code, 405) def test_delete(self): """Should delete a report""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.delete) + response = self.client.get('/api/v1/report/1/') self.assertEquals(response.status_code, 200) # from fixture response = self.client.delete('/api/v1/report/1/', content_type='application/json') self.assertEquals(response.status_code, 405) + response = self.client.delete('/api/v1/report/1/', + content_type='application/json', + **self.post_auth) + self.assertEquals(response.status_code, 405) class ApiKeyAuthenticationTestCase(FixtureTestCase): From 5d1d65c98d33df8eb3b2db1a5e4cf18cb6831a6f Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 21:19:31 +0200 Subject: [PATCH 142/429] added ReportResource authentication and authorization Change-Id: I897e7db9e0ad283cf7b93a68ed8f5b6ef8e26d93 --- codespeed/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index bddf8f7f..97275940 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -152,7 +152,8 @@ class ReportResource(ModelResource): class Meta: queryset = Report.objects.all() allowed_methods = ['get'] - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class ResultBundle(Bundle): From 19fea9513f8b5941f7721cd51dc91efba1ace5e6 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 23:47:34 +0200 Subject: [PATCH 143/429] changed to Multiauth for Environment, changed authentication/authorisation for ResultBundleResource - Environment now has the same Authentication/Authorization as all other ModelResources - Added same Authentication and Authorization to ResultBundleResource. Warning: Authorisation is not fully implemented yet! The old test will still pass. Change-Id: I2eb663296be172924c354061e80ac24841f716b2 --- codespeed/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 97275940..73e0892e 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -65,7 +65,6 @@ class Meta: #excludes = ['email', 'password', 'is_superuser'] # Add it here. authorization = DjangoAuthorization() - #authorization = Authorization() authentication = ApiKeyAuthentication() @@ -129,7 +128,6 @@ class Meta: queryset = Environment.objects.all() resource_name = 'environment' authorization = DjangoAuthorization() - #authorization = Authorization() authentication = ApiKeyAuthentication() #authentication = MultiAuthentication(Authentication(), ApiKeyAuthentication()) @@ -139,7 +137,8 @@ class ResultResource(ModelResource): class Meta: queryset = Result.objects.all() - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) class ReportResource(ModelResource): @@ -378,7 +377,8 @@ class ResultBundleResource(Resource): class Meta: resource_name = 'benchmark-result' - authorization = Authorization() + authorization = DjangoAuthorization() + authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) allowed_methods = ['get', 'post', 'put', 'delete'] def get_resource_uri(self, bundle_or_obj): From 54413858b234200a5a284a124c584cc1d6e67a72 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sun, 3 Jun 2012 23:51:33 +0200 Subject: [PATCH 144/429] few clean ups Change-Id: I46fd006147f45f9ba30b116cfaae60339a43ed31 --- codespeed/tests/tests_api.py | 100 ++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index eccd888e..0f300db5 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -45,6 +45,49 @@ def setUp(self): self.post_auth = {'HTTP_AUTHORIZATION': authorization} +class ApiKeyAuthenticationTestCase(FixtureTestCase): + + def setUp(self): + super(ApiKeyAuthenticationTestCase, self).setUp() + ApiKey.objects.all().delete() + self.auth = ApiKeyAuthentication() + self.request = HttpRequest() + + # Simulate sending the signal. + user = User.objects.get(username='apiuser') + create_api_key(User, instance=user, created=True) + + def test_is_not_authenticated(self): + """Should return HttpUnauthorized when incorrect credentials are given""" + # No username/api_key details + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) + + # Wrong username details. + self.request.GET['username'] = 'foo' + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) + + # No api_key. + self.request.GET['username'] = 'daniel' + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) + + # Wrong user/api_key. + self.request.GET['username'] = 'daniel' + self.request.GET['api_key'] = 'foo' + self.assertEqual(isinstance( + self.auth.is_authenticated(self.request), HttpUnauthorized), True) + + def test_is_authenticated(self): + """Should correctly authenticate when using an existing user and key""" + # Correct user/api_key. + user = User.objects.get(username='apiuser') + self.request.GET['username'] = 'apiuser' + self.request.GET['api_key'] = user.api_key.key + self.assertEqual(self.auth.is_authenticated(self.request), True) + + class UserTest(FixtureTestCase): """Test api user related stuff""" @@ -1023,7 +1066,7 @@ def setUp(self): 'change_report', 'codespeed', 'report') self.delete = Permission.objects.get_by_natural_key( 'delete_report', 'codespeed', 'report') - + self.report1 = Report.objects.get(pk=1) self.revision1 = Revision.objects.get(pk=1) self.executable1 = Executable.objects.get(pk=1) @@ -1127,52 +1170,10 @@ def test_delete(self): self.assertEquals(response.status_code, 405) -class ApiKeyAuthenticationTestCase(FixtureTestCase): - - def setUp(self): - super(ApiKeyAuthenticationTestCase, self).setUp() - ApiKey.objects.all().delete() - self.auth = ApiKeyAuthentication() - self.request = HttpRequest() - - # Simulate sending the signal. - user = User.objects.get(username='apiuser') - create_api_key(User, instance=user, created=True) - - def test_is_not_authenticated(self): - """Should return HttpUnauthorized when incorrect credentials are given""" - # No username/api_key details - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - # Wrong username details. - self.request.GET['username'] = 'foo' - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - # No api_key. - self.request.GET['username'] = 'daniel' - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - # Wrong user/api_key. - self.request.GET['username'] = 'daniel' - self.request.GET['api_key'] = 'foo' - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - def test_is_authenticated(self): - """Should correctly authenticate when using an existing user and key""" - # Correct user/api_key. - user = User.objects.get(username='apiuser') - self.request.GET['username'] = 'apiuser' - self.request.GET['api_key'] = user.api_key.key - self.assertEqual(self.auth.is_authenticated(self.request), True) - - class ResultBundleTestCase(FixtureTestCase): def setUp(self): + super(ResultBundleTestCase, self).setUp() self.data1 = { 'commitid': '2', 'branch': 'default', # Always use default for trunk/master/tip @@ -1279,6 +1280,16 @@ class ResultBundleResourceTestCase(FixtureTestCase): DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' def setUp(self): + + super(ResultBundleResourceTestCase, self).setUp() + + self.add = Permission.objects.get_by_natural_key( + 'add_result', 'codespeed', 'result') + self.change = Permission.objects.get_by_natural_key( + 'change_result', 'codespeed', 'result') + self.delete = Permission.objects.get_by_natural_key( + 'delete_result', 'codespeed', 'result') + self.data1 = { 'commitid': '2', 'branch': 'default', # Always use default for trunk/master/tip @@ -1305,6 +1316,7 @@ def setUp(self): self.project.save() self.env1 = Environment(name='Bulldozer') self.env1.save() + self.client = Client() def test_post_mandatory(self): """Should save a new result with only mandatory data""" From 5480f20477492145f50b1a05f5fb2855a818a5a2 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 5 Jun 2012 15:18:13 +0200 Subject: [PATCH 145/429] Changed ResultBundleResult Djangoauthorization to models.Result, some clean ups - for ResultBundle add, update, delete the Django access rights of the Result model is used - commented out some stuff that will be required maybe in future features Change-Id: If38a5230bf04137f823e8d7884fa29d41ea202ae --- codespeed/api.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 73e0892e..4d49c00d 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -134,6 +134,10 @@ class Meta: class ResultResource(ModelResource): """Resource for Result()""" + revision = fields.ToOneField(RevisionResource, 'revision') + executable = fields.ToOneField(ExecutableResource, 'executable') + benchmark = fields.ToOneField(BenchmarkResource, 'benchmark') + environment = fields.ToOneField(EnvironmentResource, 'environment') class Meta: queryset = Result.objects.all() @@ -274,7 +278,7 @@ def _populate_by_obj(self): # TODO (a8): add user to models setattr(self.obj, 'user', User.objects.get(pk=1)) #setattr(self.obj, 'user', None) - setattr(self.obj, 'notify', None) + #setattr(self.obj, 'notify', None) def _check_data(self): """See if all mandatory data is there""" @@ -362,7 +366,7 @@ class ResultBundleResource(Resource): not mandatory data 'notify' - Send notification to registered user if result varies from - previous results + previous results, currently not implemented """ revision = fields.ToOneField(RevisionResource, 'revision') @@ -372,13 +376,15 @@ class ResultBundleResource(Resource): benchmark = fields.ToOneField(BenchmarkResource, 'benchmark') environment = fields.ToOneField(EnvironmentResource, 'environment') result = fields.ToOneField(ResultResource, 'result') - user = fields.ToOneField(UserResource, 'user', null=True) - notify = fields.CharField(attribute='notify', null=True) + #user = fields.ToOneField(UserResource, 'user', null=True) + #notify = fields.CharField(attribute='notify', null=True) class Meta: resource_name = 'benchmark-result' + object_class = Result authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), Authentication()) + authentication = MultiAuthentication(ApiKeyAuthentication(), + Authentication()) allowed_methods = ['get', 'post', 'put', 'delete'] def get_resource_uri(self, bundle_or_obj): @@ -415,12 +421,12 @@ def obj_get(self, request=None, **kwargs): setattr(result, 'result', result) # TODO (a8): add user to models #setattr(result, 'user', User.objects.get(pk=1)) - setattr(result, 'user', None) + #setattr(result, 'user', None) #setattr(result, 'notify', None) return result def obj_create(self, bundle, request=None, **kwargs): - # FIXME (a8): Make full_hydrate work + # not calling hydrate here since bundle.save() has that functionality #bundle = self.full_hydrate(bundle) bundle.save() return bundle @@ -468,3 +474,6 @@ def obj_delete(self, request=None, **kwargs): def rollback(self, bundles): pass + + def detail_uri_kwargs(self): + pass From e7ba52f7a25f2053706fe3ac7371fdb53b717281 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 5 Jun 2012 15:22:51 +0200 Subject: [PATCH 146/429] Fixed tests to check if authentication and authorization for ResultBundleResource are correct - Note, the ResultBundle and ResultBundleResource will be re-written to better match tastypie soon. For now it is possible to upload data via authentication and authorization. Change-Id: If112476f4311358b56e3c7378565e910b9304db2 --- codespeed/tests/tests_api.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 0f300db5..29ba912c 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -1171,6 +1171,7 @@ def test_delete(self): class ResultBundleTestCase(FixtureTestCase): + """Test CRUD of results via API""" def setUp(self): super(ResultBundleTestCase, self).setUp() @@ -1320,9 +1321,19 @@ def setUp(self): def test_post_mandatory(self): """Should save a new result with only mandatory data""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + response = self.client.post('/api/v1/benchmark-result/', data=json.dumps(self.data1), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/benchmark-result/', + data=json.dumps(self.data1), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] result = Result.objects.get(pk=int(id)) @@ -1332,10 +1343,20 @@ def test_post_mandatory(self): def test_post_all_data(self): """Should save a new result with mandatory and optional data""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + data = dict(self.data1, **self.data_optional) response = self.client.post('/api/v1/benchmark-result/', data=json.dumps(data), content_type='application/json') + self.assertEquals(response.status_code, 401) + response = self.client.post('/api/v1/benchmark-result/', + data=json.dumps(data), + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 201) def test_get_one(self): @@ -1355,4 +1376,3 @@ def test_get_one(self): # suite = unittest.TestSuite() # suite.addTest(EnvironmentTest()) # return suite - From c2ea6b2d89bb82604503e14b4ffc360a764d44c4 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 11 Jun 2012 13:22:14 +0200 Subject: [PATCH 147/429] Improved docstrings and minor code cleanups Change-Id: I4268567cf26ff62e5659950485ff883b1dcc1f84 --- codespeed/api.py | 1 - codespeed/tests/tests_api.py | 34 +++++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 4d49c00d..0c4bf15b 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -43,7 +43,6 @@ from tastypie.resources import ModelResource, Resource from tastypie import fields from tastypie.authorization import Authorization, DjangoAuthorization -#from tastypie.authentication import ApiKeyAuthentication, Authentication, MultiAuthentication from tastypie.authentication import Authentication, ApiKeyAuthentication, MultiAuthentication from tastypie.models import create_api_key from tastypie.utils.dict import dict_strip_unicode_keys diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 29ba912c..62b46648 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -92,17 +92,17 @@ class UserTest(FixtureTestCase): """Test api user related stuff""" def test_has_apikey(self): + """User() should have an api key attr that was generated automatically.""" self.assertTrue(hasattr(self.api_user, 'api_key')) def test_len_apikey(self): - """Test the key has a length""" + """Should have api user key with a non-zero length.""" self.assertTrue(len(self.api_user.api_key.key) >= 1) def test_is_authenticated_header(self): """Taken from tastypie test suite to ensure api key is generated for - new users correctly. + new users correctly and tastypie is installed correctly. """ - auth = ApiKeyAuthentication() request = HttpRequest() @@ -135,7 +135,7 @@ def test_is_authenticated_header(self): self.assertEqual(auth.is_authenticated(request), True) def test_api_key(self): - # Correct user/api_key. + """User should be authenticated by it's api key.""" auth = ApiKeyAuthentication() request = HttpRequest() authorization='ApiKey %s:%s' % (self.api_user.username, self.api_user.api_key.key) @@ -177,6 +177,7 @@ def setUp(self): self.client = Client() def test_no_perms(self): + """User() should have only GET permission""" # sanity check: user has no permissions self.assertFalse(self.api_user.get_all_permissions()) @@ -194,6 +195,7 @@ def test_no_perms(self): ) def test_add_perm(self): + """User() should have add permission granted.""" request = HttpRequest() request.user = self.api_user @@ -204,6 +206,7 @@ def test_add_perm(self): EnvironmentResource()._meta.authorization.is_authorized(request)) def test_change_perm(self): + """User() should have change permission granted.""" request = HttpRequest() request.user = self.api_user @@ -214,6 +217,7 @@ def test_change_perm(self): EnvironmentResource()._meta.authorization.is_authorized(request)) def test_delete_perm(self): + """User() should have delete permission granted.""" request = HttpRequest() request.user = self.api_user @@ -224,6 +228,7 @@ def test_delete_perm(self): EnvironmentResource()._meta.authorization.is_authorized(request)) def test_all(self): + """User() should have add, change, delete permissions granted.""" request = HttpRequest() request.user = self.api_user @@ -239,6 +244,7 @@ def test_all(self): ) def test_patch_perms(self): + """User() should have patch (add, change, delete) permissions granted.""" request = HttpRequest() request.user = self.api_user request.method = 'PATCH' @@ -261,6 +267,7 @@ def test_patch_perms(self): EnvironmentResource()._meta.authorization.is_authorized(request)) def test_unrecognized_method(self): + """User() should not have the permission to call non-existent method.""" request = HttpRequest() self.api_user.user_permissions.clear() request.user = self.api_user @@ -271,13 +278,20 @@ def test_unrecognized_method(self): EnvironmentResource()._meta.authorization.is_authorized(request)) def test_get_environment(self): - """Should get an existing environment""" + """Should get an environment when given an existing ID""" response = self.client.get( '/api/v1/environment/1/?username={0}&api_key={1}'.format( self.api_user.username, self.api_user.api_key.key)) self.assertEquals(response.status_code, 200) self.assertEqual(json.loads(response.content)['name'], "Dual Core") + def test_get_non_existing_environment(self): + """Should return 404 when given a non existing environment ID""" + response = self.client.get( + '/api/v1/environment/999/?username={0}&api_key={1}'.format( + self.api_user.username, self.api_user.api_key.key)) + self.assertEquals(response.status_code, 404) + def test_get_environment_all_fields(self): """Should get all fields for an environment""" response = self.client.get( @@ -425,6 +439,7 @@ def setUp(self): self.client = Client() def test_all(self): + """User should have all permissions granted.""" request = HttpRequest() request.user = self.api_user @@ -1204,6 +1219,7 @@ def setUp(self): self.env1.save() def test_populate_and_save(self): + """Should populate ResultBundle() with data""" bundle = ResultBundle(**self.data1) bundle._populate_obj_by_data() # should raise exception if not OK @@ -1233,7 +1249,7 @@ def test_insufficient_data(self): self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) def test_date_attr_set(self): - """Check if date attr of Result() is set if not given""" + """Should add date attr to Result() obj if date is not given""" # date is set automatically modified_data = copy.deepcopy(self.data1) bundle = ResultBundle(**modified_data) @@ -1247,7 +1263,7 @@ def test_date_attr_set(self): self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) def test_optional_data(self): - """Check handling of optional data""" + """Should save optional data.""" data = dict(self.data1.items() + self.data_optional.items()) bundle = ResultBundle(**data) bundle.save() @@ -1259,8 +1275,8 @@ def test_optional_data(self): self.assertEqual(bundle.obj.val_min, float(self.data_optional['val_min'])) - def test_non_exiting_items(self): - """Check handling of optional data""" + def test_overwrite_exiting_items(self): + """Should overwrite existing attributes""" modified_data = copy.deepcopy(self.data1) modified_data['commitid'] = '0b31bf33a469ac2cb1949666eea54d69a36c3724' modified_data['project'] = 'Cython' From 2a8689e12298e7b252d9d6b51f889be411775b7f Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Mon, 11 Jun 2012 13:23:12 +0200 Subject: [PATCH 148/429] Changed django-tastypie to install via git from known working commit - The upcomming version of django-tastypie will have some features already used by the RESTful API. Thus, we have to install it from github. Once, django-tastypie is released it will changed back to the official stable release. Change-Id: Id3b361e93d61d711c7f0a3496a2ebabc19846e38 --- example/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/requirements.txt b/example/requirements.txt index 006825a7..160ad271 100644 --- a/example/requirements.txt +++ b/example/requirements.txt @@ -1,4 +1,4 @@ Django>=1.3 South>=0.7.3 -django-tastypie>=0.9.9 +-e git://github.com/toastdriven/django-tastypie.git@d9c86299118b4a1db9f646018169c1c45fd065df#egg=django-tastypie #-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar From 333e836d0f766087d14c84e4381ffd711b3dbe95 Mon Sep 17 00:00:00 2001 From: Matt Haggard Date: Thu, 14 Jun 2012 09:24:25 -0600 Subject: [PATCH 149/429] Adding missing install step --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a9a37b18..ad2fe200 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,7 @@ can take a long time. Please be patient. # Installation * Download the last stable release from - [http://github.com/tobami/codespeed/downloads](http://github.com/tobami/codespeed/downloads) - and unpack it + [http://github.com/tobami/codespeed/downloads](http://github.com/tobami/codespeed/downloads), unpack it and install it with `python setup.py install`. * To get started, you can use the `example` directory as a starting point for your Django project, which can be normally configured by editing `example/settings.py`. * For simplicity, you can use the default sqlite configuration, which will save the data to a database named `example/data.db` From 5ac8138062549a0e5f10465fb305b7f2cca65258 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 19 Jun 2012 22:34:43 +0200 Subject: [PATCH 150/429] Removed unused code from api - Removed a few lines that were thought to be required for future features. One step at a time. Change-Id: I0ae88d6144dacec717eac97a8f28ae17d2acad06 --- codespeed/api.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 0c4bf15b..0edf069b 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -274,10 +274,6 @@ def _populate_by_obj(self): self.obj.branch = self.obj.revision.branch #self.obj.result = self.obj setattr(self.obj, 'result', self.obj) - # TODO (a8): add user to models - setattr(self.obj, 'user', User.objects.get(pk=1)) - #setattr(self.obj, 'user', None) - #setattr(self.obj, 'notify', None) def _check_data(self): """See if all mandatory data is there""" @@ -375,8 +371,6 @@ class ResultBundleResource(Resource): benchmark = fields.ToOneField(BenchmarkResource, 'benchmark') environment = fields.ToOneField(EnvironmentResource, 'environment') result = fields.ToOneField(ResultResource, 'result') - #user = fields.ToOneField(UserResource, 'user', null=True) - #notify = fields.CharField(attribute='notify', null=True) class Meta: resource_name = 'benchmark-result' @@ -418,10 +412,6 @@ def obj_get(self, request=None, **kwargs): result.project = result.executable.project result.branch = result.revision.branch setattr(result, 'result', result) - # TODO (a8): add user to models - #setattr(result, 'user', User.objects.get(pk=1)) - #setattr(result, 'user', None) - #setattr(result, 'notify', None) return result def obj_create(self, bundle, request=None, **kwargs): From 5ff5c48e32b63694c019e7cfbb462897e6a4ae4d Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Thu, 21 Jun 2012 17:38:02 +0200 Subject: [PATCH 151/429] Changed test to use proper resource uris for ResultBundle and ResultBundleResource - previously data was provided similar to the tools/save_single_result.py Now it uses resource uris Change-Id: Icc85ec3d3882e3b6f0cde592a591a9832daea881 --- codespeed/tests/tests_api.py | 69 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 62b46648..e0e846d3 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -1191,12 +1191,12 @@ class ResultBundleTestCase(FixtureTestCase): def setUp(self): super(ResultBundleTestCase, self).setUp() self.data1 = { - 'commitid': '2', - 'branch': 'default', # Always use default for trunk/master/tip - 'project': 'MyProject', - 'executable': 'myexe O3 64bits', - 'benchmark': 'float', - 'environment': "Bulldozer", + 'commitid': '/api/v1/revision/2/', + 'branch': '/api/v1/branch/1/', # Always use default for trunk/master/tip + 'project': '/api/v1/project/2/', + 'executable': '/api/v1/executable/1/', + 'benchmark': '/api/v1/benchmark/1/', + 'environment': '/api/v1/environment/2/', 'result_value': 4000, } DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' @@ -1223,23 +1223,24 @@ def test_populate_and_save(self): bundle = ResultBundle(**self.data1) bundle._populate_obj_by_data() # should raise exception if not OK - bundle.save() + bundle.hydrate_and_save() self.assert_(True) def test_save_same_result_again(self): """Save a previously saved result. Expected is an IntegrityError""" modified_data = copy.deepcopy(self.data1) - modified_data['environment'] = "Dual Core" + modified_data['environment'] = '/api/v1/environment/1/' + modified_data['project'] = '/api/v1/project/1/' bundle = ResultBundle(**modified_data) bundle._populate_obj_by_data() - self.assertRaises(IntegrityError, bundle.save) + self.assertRaises(IntegrityError, bundle.hydrate_and_save) def test_for_nonexistent_environment(self): """Save data using non existing environment. Expected is an ImmediateHttpResponse """ modified_data = copy.deepcopy(self.data1) - modified_data['environment'] = "Foo the Bar" + modified_data['environment'] = '/api/v1/environment/3/' self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) def test_insufficient_data(self): @@ -1253,7 +1254,7 @@ def test_date_attr_set(self): # date is set automatically modified_data = copy.deepcopy(self.data1) bundle = ResultBundle(**modified_data) - bundle.save() + bundle.hydrate_and_save() self.assertIsInstance(bundle.obj.date, datetime) # date set by value modified_data['date'] = '2011-05-05 03:01:45' @@ -1266,7 +1267,7 @@ def test_optional_data(self): """Should save optional data.""" data = dict(self.data1.items() + self.data_optional.items()) bundle = ResultBundle(**data) - bundle.save() + bundle.hydrate_and_save() self.assertIsInstance(bundle.obj.date, datetime) self.assertEqual(bundle.obj.std_dev, float(self.data_optional['std_dev'])) @@ -1275,22 +1276,6 @@ def test_optional_data(self): self.assertEqual(bundle.obj.val_min, float(self.data_optional['val_min'])) - def test_overwrite_exiting_items(self): - """Should overwrite existing attributes""" - modified_data = copy.deepcopy(self.data1) - modified_data['commitid'] = '0b31bf33a469ac2cb1949666eea54d69a36c3724' - modified_data['project'] = 'Cython' - modified_data['benchmark'] = 'Django Template' - modified_data['executable'] = 'pypy-jit' - bundle = ResultBundle(**modified_data) - bundle.save() - self.assertEqual(bundle.obj.revision.commitid, - modified_data['commitid']) - self.assertEqual(bundle.obj.benchmark.name, - modified_data['benchmark']) - self.assertEqual(bundle.obj.project.name, - modified_data['project']) - class ResultBundleResourceTestCase(FixtureTestCase): """Submitting new benchmark results""" @@ -1308,12 +1293,12 @@ def setUp(self): 'delete_result', 'codespeed', 'result') self.data1 = { - 'commitid': '2', - 'branch': 'default', # Always use default for trunk/master/tip - 'project': 'MyProject', - 'executable': 'myexe O3 64bits', - 'benchmark': 'float', - 'environment': "Bulldozer", + 'commitid': '/api/v1/revision/2/', + 'branch': '/api/v1/branch/1/', # Always use default for trunk/master/tip + 'project': '/api/v1/project/2/', + 'executable': '/api/v1/executable/1/', + 'benchmark': '/api/v1/benchmark/1/', + 'environment': '/api/v1/environment/2/', 'result_value': 4000, } self.data_optional = { @@ -1375,6 +1360,22 @@ def test_post_all_data(self): **self.post_auth) self.assertEquals(response.status_code, 201) + def test_post_invalid_data(self): + """Should save a new result with mandatory and optional data""" + request = HttpRequest() + request.user = self.api_user + + request.user.user_permissions.add(self.add) + + modified_data = copy.deepcopy(self.data1) + # environment does not exist + modified_data['environment'] = '/api/v1/environment/5/' + response = self.client.post('/api/v1/benchmark-result/', + data=json.dumps(modified_data), + content_type='application/json', + **self.post_auth) + self.assertEquals(response.status_code, 400) + def test_get_one(self): """Should get a result bundle""" response = self.client.get('/api/v1/benchmark-result/1/', From 676ef784874323e3a9624b97df26441f3bba8cd1 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Thu, 21 Jun 2012 17:40:12 +0200 Subject: [PATCH 152/429] Changed ResultBundle and ResultBundleResource to use uris - make the tests pass Change-Id: Ib846cfd469fec9f17c5e6822ed428192082fa39b --- codespeed/api.py | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 0edf069b..0c911849 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -212,18 +212,19 @@ def _populate_obj_by_data(self): """ def populate(key): return { - 'project': lambda: Project.objects.get_or_create( - name=self.data['project']), - 'executable': lambda: Executable.objects.get_or_create( - name=self.data['executable'], project=self.obj.project - ), - 'benchmark': lambda: Benchmark.objects.get_or_create( - name=self.data['benchmark']), - 'environment': lambda: (Environment.objects.get( - name=self.data['environment']), False), - 'branch': lambda: Branch.objects.get_or_create( - name=self.data['branch'], project=self.obj.project), - }.get(key, (None, None))() + 'project': lambda: ProjectResource().get_via_uri( + self.data['project']), + 'executable': lambda: ExecutableResource().get_via_uri( + self.data['executable']), + 'benchmark': lambda: BenchmarkResource().get_via_uri( + self.data['benchmark']), + 'environment': lambda: EnvironmentResource().get_via_uri( + self.data['environment']), + 'branch': lambda: BranchResource().get_via_uri( + self.data['branch']), + 'revision': lambda: RevisionResource().get_via_uri( + self.data['commitid']), + }.get(key, None)() try: self.obj.value = float(self.data['result_value']) @@ -235,10 +236,10 @@ def populate(key): raise ImmediateHttpResponse( response=HttpBadRequest(u"Value needs to be a number")) for key in [k for k in self.mandatory_keys \ - if k not in ('result_value', 'revision')]: + if k not in ('result_value',)]: try: #populate - (item, created) = populate(key) + item = populate(key) setattr(self.obj, key, item) except Exception, error: logging.error("Data for field %s: %s not found. %s" % ( @@ -247,13 +248,6 @@ def populate(key): response=HttpBadRequest(u"Error finding: {0}={1}".format( key, self.data[key] ))) - - # find the revision - self.obj.revision, created = Revision.objects.get_or_create( - commitid=self.data['commitid'], - project=self.obj.project, - branch=self.obj.branch, - ) # populate optional data for key in [k for k in self.optional_keys \ if k not in ('date')]: @@ -293,8 +287,8 @@ def _check_data(self): # Check that the Environment exists try: - self.obj.environment = Environment.objects.get( - name=self.data['environment']) + self.obj.environment = EnvironmentResource().get_via_uri( + self.data['environment']) except Environment.DoesNotExist: error_text = 'Environment: {0} not found in database.'.format( self.data['environment']) @@ -336,7 +330,7 @@ def _check_data(self): raise ImmediateHttpResponse( response=HttpBadRequest(error_text)) - def save(self): + def hydrate_and_save(self): """Save self.obj which is an instance of Result() First populate the Result() instance with self.data @@ -393,7 +387,6 @@ def get_resource_uri(self, bundle_or_obj): if self._meta.api_name is not None: kwargs['api_name'] = self._meta.api_name - #FIXME (a8): reverse url should point to ResultResource() return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs) def get_object_list(self, request): @@ -416,8 +409,10 @@ def obj_get(self, request=None, **kwargs): def obj_create(self, bundle, request=None, **kwargs): # not calling hydrate here since bundle.save() has that functionality + # self.full_hydrate(bundle) will try to hydrate result which is not + # there yet #bundle = self.full_hydrate(bundle) - bundle.save() + bundle.hydrate_and_save() return bundle def obj_update(self, bundle, request=None, **kwargs): From 032c6279123e2eb1b623f7f9783bdbe34b34e773 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Thu, 21 Jun 2012 17:42:17 +0200 Subject: [PATCH 153/429] Added an example to save a single result - Using requests it saves a single result. Note, models like benchmark, environment etc. are not created automatically. That has to be done via the api beforehand. Change-Id: I07e57d03e91fa206a1bf0667c8c43e54c8a7e258 --- tools/save_single_result_via_api.py | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 tools/save_single_result_via_api.py diff --git a/tools/save_single_result_via_api.py b/tools/save_single_result_via_api.py new file mode 100755 index 00000000..74e83527 --- /dev/null +++ b/tools/save_single_result_via_api.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +""" +Submit a single result via the RESTful API using requests + +Note, that is just an example. You need to add an user +to get the apikey via the /admin +All resources in the result_data dict need to exist. +""" +import json +import requests + +def get_data(): + result_data = { + 'commitid': '/api/v1/revision/2/', + 'branch': '/api/v1/branch/1/', # Always use default for trunk/master/tip + 'project': '/api/v1/project/2/', + 'executable': '/api/v1/executable/1/', + 'benchmark': '/api/v1/benchmark/1/', + 'environment': '/api/v1/environment/2/', + 'result_value': 4000, + } + headers = {'content-type': 'application/json', + 'Authorization': 'ApiKey apiuser2:2ee0fa1a175ccc3b88b245e799d70470e5d53430'} + url = 'http://localhost:8000/api/v1/benchmark-result/' + return(url, result_data, headers) + + +def main(): + url, result_data, headers = get_data() + print "{0}: {1}".format(url, result_data) + r = requests.post(url, data=json.dumps(result_data), headers=headers) + print r + + +if __name__ == "__main__": + main() From 65a2ede809155ca8269357cff42ac14a34a85fe3 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 23 Jun 2012 22:52:27 +0200 Subject: [PATCH 154/429] fixed identation level Change-Id: I35616c76a237786075e4e7d70a780c4696ae1602 --- codespeed/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/api.py b/codespeed/api.py index 0c911849..73f64824 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -224,7 +224,7 @@ def populate(key): self.data['branch']), 'revision': lambda: RevisionResource().get_via_uri( self.data['commitid']), - }.get(key, None)() + }.get(key, None)() try: self.obj.value = float(self.data['result_value']) From 8f72b1dd024ac7e837625660d84f4c3a5f7a344f Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 23 Jun 2012 22:58:12 +0200 Subject: [PATCH 155/429] changed code formatting for switch, hopefully it's more readable Change-Id: If79c2cc98e642b1f6e9704d029d0ac986cac36a8 --- codespeed/api.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 73f64824..3a49a653 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -211,20 +211,18 @@ def _populate_obj_by_data(self): get everything except the result, 2nd try reverse lookup """ def populate(key): - return { - 'project': lambda: ProjectResource().get_via_uri( - self.data['project']), - 'executable': lambda: ExecutableResource().get_via_uri( - self.data['executable']), - 'benchmark': lambda: BenchmarkResource().get_via_uri( - self.data['benchmark']), - 'environment': lambda: EnvironmentResource().get_via_uri( - self.data['environment']), - 'branch': lambda: BranchResource().get_via_uri( - self.data['branch']), - 'revision': lambda: RevisionResource().get_via_uri( - self.data['commitid']), - }.get(key, None)() + return {'project': lambda: ProjectResource().get_via_uri( + self.data['project']), + 'executable': lambda: ExecutableResource().get_via_uri( + self.data['executable']), + 'benchmark': lambda: BenchmarkResource().get_via_uri( + self.data['benchmark']), + 'environment': lambda: EnvironmentResource().get_via_uri( + self.data['environment']), + 'branch': lambda: BranchResource().get_via_uri( + self.data['branch']), + 'revision': lambda: RevisionResource().get_via_uri( + self.data['commitid']),}.get(key, None)() try: self.obj.value = float(self.data['result_value']) From e413dab4137925960914cea6cff2324bbf2cde79 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Sat, 23 Jun 2012 23:01:28 +0200 Subject: [PATCH 156/429] fixed wrong identation Change-Id: Ibe81d4f88836f182c84d1baf458585a465d04d6b --- codespeed/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 3a49a653..ce2fa09e 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -329,12 +329,12 @@ def _check_data(self): response=HttpBadRequest(error_text)) def hydrate_and_save(self): - """Save self.obj which is an instance of Result() + """Save self.obj which is an instance of Result() - First populate the Result() instance with self.data - """ - self._populate_obj_by_data() - self.obj.save() + First populate the Result() instance with self.data + """ + self._populate_obj_by_data() + self.obj.save() class ResultBundleResource(Resource): From d3a6043ec430af5ef4ba8ee1bba4a972b27d4fa6 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 14 Aug 2012 14:41:46 +0200 Subject: [PATCH 157/429] add hierarchy to Benchmark model - Sometimes benchmarks have a big number of single results. It is favourable to keep them all. Thus, they might be organised in an hierarchical way. Example: To evaluate computer systems there might be one overall benchmark number. That number is calculated from sub benchmarks as CPU, hard disc I/O, RAM I/O, etc. - The single or the summarised result may be shown in the standard templates in a later version. For now all benchmarks are shown. Change-Id: I47062535bc0ec6e1d25ae857b4af74f207737c37 --- codespeed/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codespeed/models.py b/codespeed/models.py index cb16149a..4f83564f 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -132,6 +132,8 @@ class Benchmark(models.Model): ) name = models.CharField(unique=True, max_length=30) + parent = models.ForeignKey('self', verbose_name="parent", + help_text="allows to group benchmarks in hierarchies", null=True, default=None) benchmark_type = models.CharField(max_length=1, choices=B_TYPES, default='C') description = models.CharField(max_length=300, blank=True) units_title = models.CharField(max_length=30, default='Time') From 6140c073e791d416b7602eaefce3ee54e89529ec Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 14 Aug 2012 14:56:34 +0200 Subject: [PATCH 158/429] added isodate, updated tastypie in requirenments.txt - isodate was missing - django-tastypie updated to recent github version Change-Id: I2ac2cf323c01ef47658bb2167fa897bc415c6fb1 --- example/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/requirements.txt b/example/requirements.txt index 160ad271..aee60c96 100644 --- a/example/requirements.txt +++ b/example/requirements.txt @@ -1,4 +1,5 @@ Django>=1.3 South>=0.7.3 --e git://github.com/toastdriven/django-tastypie.git@d9c86299118b4a1db9f646018169c1c45fd065df#egg=django-tastypie +isodate==0.4.8 +-e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie #-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar From 3c674853d64362306cd1aacbd49e101258029233 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Tue, 14 Aug 2012 15:08:25 +0200 Subject: [PATCH 159/429] added South migration Change-Id: I5cc7749c6bb241bb5f6ef2788229baadd3827406 --- ...t__add_field_project_commit_browsing_ur.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 codespeed/migrations/0012_auto__add_field_benchmark_parent__add_field_project_commit_browsing_ur.py diff --git a/codespeed/migrations/0012_auto__add_field_benchmark_parent__add_field_project_commit_browsing_ur.py b/codespeed/migrations/0012_auto__add_field_benchmark_parent__add_field_project_commit_browsing_ur.py new file mode 100644 index 00000000..ee47d4f7 --- /dev/null +++ b/codespeed/migrations/0012_auto__add_field_benchmark_parent__add_field_project_commit_browsing_ur.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Benchmark.parent' + db.add_column('codespeed_benchmark', 'parent', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['codespeed.Benchmark'], null=True), + keep_default=False) + + # Adding field 'Project.commit_browsing_url' + db.add_column('codespeed_project', 'commit_browsing_url', + self.gf('django.db.models.fields.CharField')(default='', max_length=200, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Benchmark.parent' + db.delete_column('codespeed_benchmark', 'parent_id') + + # Deleting field 'Project.commit_browsing_url' + db.delete_column('codespeed_project', 'commit_browsing_url') + + + models = { + 'codespeed.benchmark': { + 'Meta': {'object_name': 'Benchmark'}, + 'benchmark_type': ('django.db.models.fields.CharField', [], {'default': "'C'", 'max_length': '1'}), + 'default_on_comparison': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lessisbetter': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['codespeed.Benchmark']", 'null': 'True'}), + 'units': ('django.db.models.fields.CharField', [], {'default': "'seconds'", 'max_length': '20'}), + 'units_title': ('django.db.models.fields.CharField', [], {'default': "'Time'", 'max_length': '30'}) + }, + 'codespeed.branch': { + 'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'Branch'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'branches'", 'to': "orm['codespeed.Project']"}) + }, + 'codespeed.environment': { + 'Meta': {'object_name': 'Environment'}, + 'cpu': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kernel': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'memory': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'os': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}) + }, + 'codespeed.executable': { + 'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'Executable'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'executables'", 'to': "orm['codespeed.Project']"}) + }, + 'codespeed.project': { + 'Meta': {'object_name': 'Project'}, + 'commit_browsing_url': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'repo_pass': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'repo_path': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'repo_type': ('django.db.models.fields.CharField', [], {'default': "'N'", 'max_length': '1'}), + 'repo_user': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'track': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'codespeed.report': { + 'Meta': {'unique_together': "(('revision', 'executable', 'environment'),)", 'object_name': 'Report'}, + '_tablecache': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'colorcode': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '10'}), + 'environment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reports'", 'to': "orm['codespeed.Environment']"}), + 'executable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reports'", 'to': "orm['codespeed.Executable']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'revision': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reports'", 'to': "orm['codespeed.Revision']"}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}) + }, + 'codespeed.result': { + 'Meta': {'unique_together': "(('revision', 'executable', 'benchmark', 'environment'),)", 'object_name': 'Result'}, + 'benchmark': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Benchmark']"}), + 'date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'environment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Environment']"}), + 'executable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Executable']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'revision': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'results'", 'to': "orm['codespeed.Revision']"}), + 'std_dev': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'val_max': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'val_min': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'value': ('django.db.models.fields.FloatField', [], {}) + }, + 'codespeed.revision': { + 'Meta': {'unique_together': "(('commitid', 'branch'),)", 'object_name': 'Revision'}, + 'author': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'branch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['codespeed.Branch']"}), + 'commitid': ('django.db.models.fields.CharField', [], {'max_length': '42'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['codespeed.Project']"}), + 'tag': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}) + } + } + + complete_apps = ['codespeed'] \ No newline at end of file From 6edb48f02f9aedb7f34ef51c97dad1f2f73e5073 Mon Sep 17 00:00:00 2001 From: Frank Becker Date: Thu, 16 Aug 2012 10:19:03 +0200 Subject: [PATCH 160/429] fix CHANGES_URL decleration - CHANGES_URL is used by timeline.js via window.CHANGES_URL. Thus timeline.js has to be loaded after the decleration in the template file. Change-Id: I1c95417472b2b728593fcbb7328d950b76f8c6df --- codespeed/templates/codespeed/timeline.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index bf70f466..fe1bf2d9 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -101,11 +101,11 @@ - - + + {% endblock %} diff --git a/sample_project/urls.py b/sample_project/urls.py index 34c3fca1..e9a33c93 100644 --- a/sample_project/urls.py +++ b/sample_project/urls.py @@ -4,7 +4,7 @@ from django.conf import settings from django.conf.urls.defaults import patterns, include, handler404, handler500 -from django.views.generic.simple import redirect_to +from django.views.generic import RedirectView from django.core.urlresolvers import reverse from django.contrib import admin @@ -16,7 +16,7 @@ urlpatterns += patterns( '', - #('^$', redirect_to, {'url': '/speed/'}), + #('^$', RedirectView.as_view(url='/speed/')), ) urlpatterns += patterns( diff --git a/setup.py b/setup.py index fe47942b..10f711ec 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version='0.9.1', url='https://github.com/tobami/codespeed', license='GNU Lesser General Public License version 2.1', - install_requires=['django==1.4', 'isodate', 'south<=2.0', 'django-tastypie'], + install_requires=['django==1.5.1', 'isodate', 'south<=2.0', 'django-tastypie'], packages=find_packages(exclude=['ez_setup', 'sample_project']), description='A web application to monitor and analyze the performance of your code', include_package_data=True, From 6bc57648c4aec38d0d8fa8cd7b8d4235dd0ed52f Mon Sep 17 00:00:00 2001 From: Rayan Date: Mon, 5 Aug 2013 05:19:31 +0200 Subject: [PATCH 185/429] modified requirements.txt for Django 1.5.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e718fc0a..d079ebed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==1.4 +Django==1.5.1 South<=2.0 isodate==0.4.8 -e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie From 5fe3831deb9d14cf844c1916f9cc8ecdb76a6984 Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Mon, 5 Aug 2013 21:18:13 -0400 Subject: [PATCH 186/429] Templates are now forward-compatible with >=1.4 --- codespeed/templates/codespeed/changes.html | 1 + codespeed/templates/codespeed/comparison.html | 1 + codespeed/templates/codespeed/timeline.html | 1 + requirements.txt | 2 +- sample_project/templates/base.html | 1 + sample_project/templates/home.html | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index ec9945d3..ebc09516 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load url from future %} {% block title %}{{ block.super }}: Changes{% endblock %} {% block navigation %} diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index fac96baa..30d80a59 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load url from future %} {% block title %}{{ block.super }}: Comparison{% endblock %} {% block extra_head %} diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 03ba8630..393d63ba 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load url from future %} {% block title %}{{ block.super }}: Timeline{% endblock %} {% block extra_head %} diff --git a/requirements.txt b/requirements.txt index d079ebed..857e8d2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==1.5.1 +Django>=1.4 South<=2.0 isodate==0.4.8 -e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie diff --git a/sample_project/templates/base.html b/sample_project/templates/base.html index 9a0e5a32..002f1e0f 100644 --- a/sample_project/templates/base.html +++ b/sample_project/templates/base.html @@ -1,3 +1,4 @@ +{% load url from future %} diff --git a/sample_project/templates/home.html b/sample_project/templates/home.html index 73248fae..04704dc0 100644 --- a/sample_project/templates/home.html +++ b/sample_project/templates/home.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load url from future %} {% block navigation %} {% endblock navigation %} From cfa0f2ddc6fa7597b9b374a9f994900148485289 Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Mon, 5 Aug 2013 22:21:02 -0400 Subject: [PATCH 187/429] Tastypie >=0.9.14 needed in order to work with Django >=1.5 --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 857e8d2b..8be9de3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django>=1.4 South<=2.0 isodate==0.4.8 --e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie -#django-tastypie==0.9.15 +#-e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie +django-tastypie==0.9.14 #-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar diff --git a/setup.py b/setup.py index 10f711ec..6ec79291 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version='0.9.1', url='https://github.com/tobami/codespeed', license='GNU Lesser General Public License version 2.1', - install_requires=['django==1.5.1', 'isodate', 'south<=2.0', 'django-tastypie'], + install_requires=['django>=1.4', 'isodate', 'south<=2.0', 'django-tastypie'], packages=find_packages(exclude=['ez_setup', 'sample_project']), description='A web application to monitor and analyze the performance of your code', include_package_data=True, From 91b7b2b6a248b8117a9e7b94fd6b836e4910e7c6 Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Tue, 6 Aug 2013 23:08:52 -0400 Subject: [PATCH 188/429] started fixing api tests for tastypie 0.9.15 --- codespeed/tests/tests_api.py | 111 +++++++++++++++++++++++------------ requirements.txt | 2 +- 2 files changed, 74 insertions(+), 39 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 7ac7264b..69f7728b 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -18,7 +18,7 @@ from django.contrib.auth.models import User, Permission from tastypie.authorization import (Authorization, ReadOnlyAuthorization, DjangoAuthorization) -from tastypie.exceptions import ImmediateHttpResponse +from tastypie.exceptions import ImmediateHttpResponse, Unauthorized from tastypie.models import ApiKey, create_api_key from tastypie.http import HttpUnauthorized from tastypie.authentication import ApiKeyAuthentication @@ -45,6 +45,20 @@ def setUp(self): self.api_user.api_key.key) self.post_auth = {'HTTP_AUTHORIZATION': authorization} + def generic_test_authorized(self, method, request, function_list, function_detail): + bundle = self.resource.build_bundle(request=request) + bundle.request.method = method + self.assertTrue(function_detail(self.resource.get_object_list(bundle.request)[0], + bundle)) + + def generic_test_unauthorized(self, method, request, function_list, function_detail): + bundle = self.resource.build_bundle(request=request) + bundle.request.method = method + self.assertRaises(Unauthorized, function_detail, + self.resource.get_object_list(bundle.request)[0], bundle) + + + class ApiKeyAuthenticationTestCase(FixtureTestCase): @@ -177,6 +191,8 @@ def setUp(self): [(k, getattr(env_db1, k)) for k in self.env1_data.keys()] ) self.client = Client() + self.resource = EnvironmentResource() + self.auth = self.resource._meta.authorization def test_no_perms(self): """User() should have only GET permission""" @@ -186,15 +202,18 @@ def test_no_perms(self): request = HttpRequest() request.method = 'GET' request.user = self.api_user + + # with no permissions, api is read-only - self.assertTrue(EnvironmentResource()._meta.authorization.is_authorized( - request)) + self.generic_test_authorized('GET',request, self.auth.read_list, + self.auth.read_detail) - for method in ('POST', 'PUT', 'DELETE'): - request.method = method - self.assertFalse( - EnvironmentResource()._meta.authorization.is_authorized(request) - ) + for method, function_list, function_detail in \ + (('POST',self.auth.create_list, self.auth.create_detail), + ('PUT',self.auth.update_list, self.auth.update_detail), + ('DELETE',self.auth.delete_list, self.auth.delete_detail)): + self.generic_test_unauthorized(method, request, function_list, + function_detail) def test_add_perm(self): """User() should have add permission granted.""" @@ -203,9 +222,9 @@ def test_add_perm(self): # give add permission request.user.user_permissions.add(self.add) - request.method = 'POST' - self.assertTrue( - EnvironmentResource()._meta.authorization.is_authorized(request)) + + self.generic_test_authorized('POST',request, self.auth.create_list, + self.auth.create_detail) def test_change_perm(self): """User() should have change permission granted.""" @@ -214,9 +233,9 @@ def test_change_perm(self): # give change permission request.user.user_permissions.add(self.change) - request.method = 'PUT' - self.assertTrue( - EnvironmentResource()._meta.authorization.is_authorized(request)) + + self.generic_test_authorized('PUT',request, self.auth.update_list, + self.auth.update_detail) def test_delete_perm(self): """User() should have delete permission granted.""" @@ -225,9 +244,9 @@ def test_delete_perm(self): # give delete permission request.user.user_permissions.add(self.delete) - request.method = 'DELETE' - self.assertTrue( - EnvironmentResource()._meta.authorization.is_authorized(request)) + + self.generic_test_authorized('DELETE',request, self.auth.delete_list, + self.auth.delete_detail) def test_all(self): """User() should have add, change, delete permissions granted.""" @@ -238,12 +257,19 @@ def test_all(self): request.user.user_permissions.add(self.change) request.user.user_permissions.add(self.delete) - for method in ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'DELETE', - 'PATCH'): - request.method = method - self.assertTrue( - EnvironmentResource()._meta.authorization.is_authorized(request) - ) + for method, function_list, function_detail in \ + (('GET',self.auth.read_list, self.auth.read_detail), + ('OPTIONS',self.auth.read_list, self.auth.read_detail), + ('HEAD',self.auth.read_list, self.auth.read_detail), + ('POST',self.auth.create_list, self.auth.create_detail), + ('PUT',self.auth.update_list, self.auth.update_detail), + ('PATCH',self.auth.read_list, self.auth.read_detail), + ('PATCH',self.auth.update_list, self.auth.update_detail), + ('PATCH',self.auth.delete_list, self.auth.delete_detail), + ('DELETE',self.auth.delete_list, self.auth.delete_detail)): + self.generic_test_authorized(method, request, function_list, + function_detail) + def test_patch_perms(self): """User() should have patch (add, change, delete) permissions granted.""" @@ -253,20 +279,22 @@ def test_patch_perms(self): # Not enough. request.user.user_permissions.add(self.add) - self.assertFalse( - EnvironmentResource()._meta.authorization.is_authorized(request)) + self.generic_test_unauthorized('PATCH', request, self.auth.update_list, + self.auth.update_detail) + self.generic_test_unauthorized('PATCH', request, self.auth.delete_list, + self.auth.delete_detail) # Still not enough. request.user.user_permissions.add(self.change) - self.assertFalse( - EnvironmentResource()._meta.authorization.is_authorized(request)) + self.generic_test_unauthorized('PATCH', request, self.auth.delete_list, + self.auth.delete_detail) # Much better. request.user.user_permissions.add(self.delete) # Nuke the perm cache. :/ del request.user._perm_cache - self.assertTrue( - EnvironmentResource()._meta.authorization.is_authorized(request)) + self.generic_test_authorized('PATCH', request, self.auth.delete_list, + self.auth.delete_detail) def test_unrecognized_method(self): """User() should not have the permission to call non-existent method.""" @@ -276,8 +304,8 @@ def test_unrecognized_method(self): # Check a non-existent HTTP method. request.method = 'EXPLODE' - self.assertFalse( - EnvironmentResource()._meta.authorization.is_authorized(request)) + self.generic_test_unauthorized('EXPLODE', request, self.auth.update_list, + self.auth.update_detail) def test_get_environment(self): """Should get an environment when given an existing ID""" @@ -439,6 +467,8 @@ def setUp(self): self.project = Project(**self.project_data) self.project.save() self.client = Client() + self.resource = ProjectResource() + self.auth = self.resource._meta.authorization def test_all(self): """User should have all permissions granted.""" @@ -449,13 +479,18 @@ def test_all(self): request.user.user_permissions.add(self.change) request.user.user_permissions.add(self.delete) - for method in ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'DELETE', - 'PATCH'): - request.method = method - self.assertTrue( - ProjectResource()._meta.authorization.is_authorized(request) - ) - + for method, function_list, function_detail in \ + (('GET',self.auth.read_list, self.auth.read_detail), + ('OPTIONS',self.auth.read_list, self.auth.read_detail), + ('HEAD',self.auth.read_list, self.auth.read_detail), + ('POST',self.auth.create_list, self.auth.create_detail), + ('PUT',self.auth.update_list, self.auth.update_detail), + ('PATCH',self.auth.read_list, self.auth.read_detail), + ('PATCH',self.auth.update_list, self.auth.update_detail), + ('PATCH',self.auth.delete_list, self.auth.delete_detail), + ('DELETE',self.auth.delete_list, self.auth.delete_detail)): + self.generic_test_authorized(method, request, function_list, + function_detail) def test_get_project(self): """Should get an existing project""" response = self.client.get('/api/v1/project/{0}/'.format( diff --git a/requirements.txt b/requirements.txt index 8be9de3c..c9aa8699 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ Django>=1.4 South<=2.0 isodate==0.4.8 #-e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie -django-tastypie==0.9.14 +django-tastypie==0.9.15 #-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar From 1d28fa23086ba344e0e0bd1a11042c1c2be5a34b Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Thu, 8 Aug 2013 00:25:31 -0400 Subject: [PATCH 189/429] ported API and API tests to tastypie 0.9.15, addresses issue #147 --- codespeed/api.py | 32 ++++++++++++++++---------------- codespeed/tests/tests_api.py | 22 +++++++--------------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 3d1357c8..91fc3654 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -39,7 +39,6 @@ from django.http import Http404 from django.shortcuts import get_object_or_404 from tastypie.bundle import Bundle -from tastypie.bundle import Bundle from tastypie.exceptions import ImmediateHttpResponse from tastypie.http import HttpBadRequest, HttpCreated, HttpNotImplemented from tastypie.resources import ModelResource, Resource @@ -136,9 +135,8 @@ class Meta: queryset = Environment.objects.all() resource_name = 'environment' authorization = DjangoAuthorization() - authentication = ApiKeyAuthentication() - #authentication = MultiAuthentication(Authentication(), - #ApiKeyAuthentication()) + authentication = MultiAuthentication(ApiKeyAuthentication(), + Authentication()) class ResultResource(ModelResource): @@ -201,8 +199,9 @@ class ResultBundle(Bundle): 'date', ) - def __init__(self, obj=None, **kwargs): + def __init__(self, obj=None, request=None, **kwargs): self.data = kwargs + self.request = request if isinstance(obj, Result): self.obj = obj @@ -215,7 +214,8 @@ def __init__(self, obj=None, **kwargs): if self.data: self._check_data() self.__data_validated = False # not used for now - super(ResultBundle, self).__init__(data=self.data, obj=self.obj) + super(ResultBundle, self).__init__(data=self.data, obj=self.obj, + request=self.request) def _populate_obj_by_data(self): """Database lookup @@ -224,17 +224,17 @@ def _populate_obj_by_data(self): """ def populate(key): return {'project': lambda: ProjectResource().get_via_uri( - self.data['project']), + self.data['project'], request=self.request), 'executable': lambda: ExecutableResource().get_via_uri( - self.data['executable']), + self.data['executable'], request=self.request), 'benchmark': lambda: BenchmarkResource().get_via_uri( - self.data['benchmark']), + self.data['benchmark'], request=self.request), 'environment': lambda: EnvironmentResource().get_via_uri( - self.data['environment']), + self.data['environment'], request=self.request), 'branch': lambda: BranchResource().get_via_uri( - self.data['branch']), + self.data['branch'], request=self.request), 'revision': lambda: RevisionResource().get_via_uri( - self.data['commitid'])}.get(key, None)() + self.data['commitid'], request=self.request)}.get(key, None)() try: self.obj.value = float(self.data['result_value']) @@ -299,7 +299,7 @@ def _check_data(self): # Check that the Environment exists try: self.obj.environment = EnvironmentResource().get_via_uri( - self.data['environment']) + self.data['environment'], request=self.request) except Environment.DoesNotExist: error_text = 'Environment: {0} not found in database.'.format( self.data['environment']) @@ -400,7 +400,7 @@ def get_resource_uri(self, bundle_or_obj): def get_object_list(self, request): results = Result.objects.all() - return [ResultBundle(obj=r).obj for r in results] + return [ResultBundle(obj=r, request=request).obj for r in results] def obj_get_list(self, request=None, **kwargs): """Return all benchmark results ever""" @@ -440,8 +440,8 @@ def post_list(self, request, **kwargs): format=request.META.get('CONTENT_TYPE', 'application/json') ) deserialized = self.alter_deserialized_list_data(request, deserialized) - bundle = ResultBundle(**dict_strip_unicode_keys(deserialized)) - self.is_valid(bundle, request) + bundle = ResultBundle(request=request, **dict_strip_unicode_keys(deserialized)) + self.is_valid(bundle) updated_bundle = self.obj_create(bundle, request=request) return HttpCreated(location=self.get_resource_uri(updated_bundle)) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 69f7728b..5f7ba81a 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -1224,6 +1224,8 @@ class ResultBundleTestCase(FixtureTestCase): def setUp(self): super(ResultBundleTestCase, self).setUp() + self.request = HttpRequest() + self.request.user = self.api_user self.data1 = { 'commitid': '/api/v1/revision/2/', 'branch': '/api/v1/branch/1/', # Always use default for trunk/master/tip @@ -1254,7 +1256,7 @@ def setUp(self): def test_populate_and_save(self): """Should populate ResultBundle() with data""" - bundle = ResultBundle(**self.data1) + bundle = ResultBundle(request=self.request,**self.data1) bundle._populate_obj_by_data() # should raise exception if not OK bundle.hydrate_and_save() @@ -1265,7 +1267,7 @@ def test_save_same_result_again(self): modified_data = copy.deepcopy(self.data1) modified_data['environment'] = '/api/v1/environment/1/' modified_data['project'] = '/api/v1/project/1/' - bundle = ResultBundle(**modified_data) + bundle = ResultBundle(request=self.request,**modified_data) bundle._populate_obj_by_data() self.assertRaises(IntegrityError, bundle.hydrate_and_save) @@ -1287,12 +1289,12 @@ def test_date_attr_set(self): """Should add date attr to Result() obj if date is not given""" # date is set automatically modified_data = copy.deepcopy(self.data1) - bundle = ResultBundle(**modified_data) + bundle = ResultBundle(request=self.request,**modified_data) bundle.hydrate_and_save() self.assertIsInstance(bundle.obj.date, datetime) # date set by value modified_data['date'] = '2011-05-05 03:01:45' - ResultBundle(**modified_data) + bundle = ResultBundle(request=self.request,**modified_data) # wrong date string modified_data['date'] = '2011-05-05T03:01:45' self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) @@ -1300,7 +1302,7 @@ def test_date_attr_set(self): def test_optional_data(self): """Should save optional data.""" data = dict(self.data1.items() + self.data_optional.items()) - bundle = ResultBundle(**data) + bundle = ResultBundle(request=self.request,**data) bundle.hydrate_and_save() self.assertIsInstance(bundle.obj.date, datetime) self.assertEqual(bundle.obj.std_dev, @@ -1364,11 +1366,6 @@ def test_post_mandatory(self): response = self.client.post('/api/v1/benchmark-result/', data=json.dumps(self.data1), content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/benchmark-result/', - data=json.dumps(self.data1), - content_type='application/json', - **self.post_auth) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] result = Result.objects.get(pk=int(id)) @@ -1387,11 +1384,6 @@ def test_post_all_data(self): response = self.client.post('/api/v1/benchmark-result/', data=json.dumps(data), content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/benchmark-result/', - data=json.dumps(data), - content_type='application/json', - **self.post_auth) self.assertEquals(response.status_code, 201) def test_post_invalid_data(self): From ec6bdc62f00cf7c2d38515283c401fbf1e81e697 Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Thu, 8 Aug 2013 18:42:02 -0400 Subject: [PATCH 190/429] updated tastypie requirement to include latest astimezone() fix --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c9aa8699..22a44a99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ Django>=1.4 South<=2.0 isodate==0.4.8 +# version 0.9.12 #-e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie -django-tastypie==0.9.15 +#django-tastypie==0.9.15 +# version 0.10.0 with astimezone() bug resolved by an added commit +-e git://github.com/toastdriven/django-tastypie.git@b78175d4c401e99f814fb6f287853a650542cbb8#egg=django-tastypie #-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar From a6f9151d7c52bd55f5ce78398d9e953d6ea6e8b7 Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Fri, 9 Aug 2013 19:49:05 -0400 Subject: [PATCH 191/429] only pep8 changes --- codespeed/api.py | 21 ++++-- codespeed/tests/tests.py | 77 +++++++++++----------- codespeed/tests/tests_api.py | 122 +++++++++++++++++++---------------- 3 files changed, 120 insertions(+), 100 deletions(-) diff --git a/codespeed/api.py b/codespeed/api.py index 91fc3654..0fd09b2a 100644 --- a/codespeed/api.py +++ b/codespeed/api.py @@ -224,17 +224,23 @@ def _populate_obj_by_data(self): """ def populate(key): return {'project': lambda: ProjectResource().get_via_uri( - self.data['project'], request=self.request), + self.data['project'], request=self.request), + 'executable': lambda: ExecutableResource().get_via_uri( - self.data['executable'], request=self.request), + self.data['executable'], request=self.request), + 'benchmark': lambda: BenchmarkResource().get_via_uri( - self.data['benchmark'], request=self.request), + self.data['benchmark'], request=self.request), + 'environment': lambda: EnvironmentResource().get_via_uri( - self.data['environment'], request=self.request), + self.data['environment'], request=self.request), + 'branch': lambda: BranchResource().get_via_uri( - self.data['branch'], request=self.request), + self.data['branch'], request=self.request), + 'revision': lambda: RevisionResource().get_via_uri( - self.data['commitid'], request=self.request)}.get(key, None)() + self.data['commitid'], + request=self.request)}.get(key, None)() try: self.obj.value = float(self.data['result_value']) @@ -440,7 +446,8 @@ def post_list(self, request, **kwargs): format=request.META.get('CONTENT_TYPE', 'application/json') ) deserialized = self.alter_deserialized_list_data(request, deserialized) - bundle = ResultBundle(request=request, **dict_strip_unicode_keys(deserialized)) + bundle = ResultBundle(request=request, + **dict_strip_unicode_keys(deserialized)) self.is_valid(bundle) updated_bundle = self.obj_create(bundle, request=request) return HttpCreated(location=self.get_resource_uri(updated_bundle)) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index fe0f1b45..b12dfda4 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -26,23 +26,23 @@ def setUp(self): self.cdate = datetime( temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) self.data = { - 'commitid': '23', - 'branch': 'default', - 'project': 'MyProject', - 'executable': 'myexe O3 64bits', - 'benchmark': 'float', - 'environment': 'Dual Core', - 'result_value': 456, + 'commitid': '23', + 'branch': 'default', + 'project': 'MyProject', + 'executable': 'myexe O3 64bits', + 'benchmark': 'float', + 'environment': 'Dual Core', + 'result_value': 456, } def test_add_correct_result(self): """Add correct result data""" response = self.client.post(self.path, self.data) - + # Check that we get a success response self.assertEquals(response.status_code, 202) self.assertEquals(response.content, "Result data saved successfully") - + # Check that the data was correctly saved e = Environment.objects.get(name='Dual Core') b = Benchmark.objects.get(name='float') @@ -171,36 +171,36 @@ def setUp(self): temp = datetime.today() self.cdate = datetime( temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) - + self.data = [ {'commitid': '123', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 456}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 456}, {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 457}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 457}, {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards2', - 'environment': 'bigdog', - 'result_value': 34}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards2', + 'environment': 'bigdog', + 'result_value': 34}, {'commitid': '789', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 458}, + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 458}, ] def test_add_correct_results(self): @@ -390,7 +390,8 @@ class TestViewHelpers(TestCase): def setUp(self): self.project = Project.objects.create(name='Test') - self.executable = Executable.objects.create(name='TestExecutable', project=self.project) + self.executable = Executable.objects.create( + name='TestExecutable', project=self.project) self.branch = Branch.objects.create(name='master', project=self.project) def test_get_baseline_executables(self): @@ -421,7 +422,8 @@ class ProjectTest(TestCase): """Test project model""" def setUp(self): - self.github_project = Project(repo_type='H', repo_path='https://github.com/tobami/codespeed.git') + self.github_project = Project(repo_type='H', + repo_path='https://github.com/tobami/codespeed.git') self.git_project = Project(repo_type='G', repo_path='/home/foo/codespeed') def test_repo_name(self): @@ -435,7 +437,8 @@ def test_working_copy(self): """Test that only projects with local repositories have a working_copy attribute """ self.assertEqual(self.git_project.working_copy, - os.path.join(settings.REPOSITORY_BASE_PATH, self.git_project.repo_name)) + os.path.join(settings.REPOSITORY_BASE_PATH, + self.git_project.repo_name)) self.assertRaises( AttributeError, getattr, self.github_project, 'working_copy') diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 5f7ba81a..4e5832c1 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -48,16 +48,15 @@ def setUp(self): def generic_test_authorized(self, method, request, function_list, function_detail): bundle = self.resource.build_bundle(request=request) bundle.request.method = method - self.assertTrue(function_detail(self.resource.get_object_list(bundle.request)[0], - bundle)) + self.assertTrue( + function_detail(self.resource.get_object_list(bundle.request)[0], + bundle)) def generic_test_unauthorized(self, method, request, function_list, function_detail): bundle = self.resource.build_bundle(request=request) bundle.request.method = method self.assertRaises(Unauthorized, function_detail, - self.resource.get_object_list(bundle.request)[0], bundle) - - + self.resource.get_object_list(bundle.request)[0], bundle) class ApiKeyAuthenticationTestCase(FixtureTestCase): @@ -104,6 +103,7 @@ def test_is_authenticated(self): class UserTest(FixtureTestCase): + """Test api user related stuff""" def test_has_apikey(self): @@ -160,6 +160,7 @@ def test_api_key(self): class EnvironmentDjangoAuthorizationTestCase(FixtureTestCase): + """Test Environment() API""" def setUp(self): @@ -203,15 +204,14 @@ def test_no_perms(self): request.method = 'GET' request.user = self.api_user - # with no permissions, api is read-only - self.generic_test_authorized('GET',request, self.auth.read_list, - self.auth.read_detail) + self.generic_test_authorized('GET', request, self.auth.read_list, + self.auth.read_detail) for method, function_list, function_detail in \ - (('POST',self.auth.create_list, self.auth.create_detail), - ('PUT',self.auth.update_list, self.auth.update_detail), - ('DELETE',self.auth.delete_list, self.auth.delete_detail)): + (('POST', self.auth.create_list, self.auth.create_detail), + ('PUT', self.auth.update_list, self.auth.update_detail), + ('DELETE', self.auth.delete_list, self.auth.delete_detail)): self.generic_test_unauthorized(method, request, function_list, function_detail) @@ -223,8 +223,8 @@ def test_add_perm(self): # give add permission request.user.user_permissions.add(self.add) - self.generic_test_authorized('POST',request, self.auth.create_list, - self.auth.create_detail) + self.generic_test_authorized('POST', request, self.auth.create_list, + self.auth.create_detail) def test_change_perm(self): """User() should have change permission granted.""" @@ -234,8 +234,8 @@ def test_change_perm(self): # give change permission request.user.user_permissions.add(self.change) - self.generic_test_authorized('PUT',request, self.auth.update_list, - self.auth.update_detail) + self.generic_test_authorized('PUT', request, self.auth.update_list, + self.auth.update_detail) def test_delete_perm(self): """User() should have delete permission granted.""" @@ -244,9 +244,9 @@ def test_delete_perm(self): # give delete permission request.user.user_permissions.add(self.delete) - - self.generic_test_authorized('DELETE',request, self.auth.delete_list, - self.auth.delete_detail) + + self.generic_test_authorized('DELETE', request, self.auth.delete_list, + self.auth.delete_detail) def test_all(self): """User() should have add, change, delete permissions granted.""" @@ -258,18 +258,17 @@ def test_all(self): request.user.user_permissions.add(self.delete) for method, function_list, function_detail in \ - (('GET',self.auth.read_list, self.auth.read_detail), - ('OPTIONS',self.auth.read_list, self.auth.read_detail), - ('HEAD',self.auth.read_list, self.auth.read_detail), - ('POST',self.auth.create_list, self.auth.create_detail), - ('PUT',self.auth.update_list, self.auth.update_detail), - ('PATCH',self.auth.read_list, self.auth.read_detail), - ('PATCH',self.auth.update_list, self.auth.update_detail), - ('PATCH',self.auth.delete_list, self.auth.delete_detail), - ('DELETE',self.auth.delete_list, self.auth.delete_detail)): + (('GET', self.auth.read_list, self.auth.read_detail), + ('OPTIONS', self.auth.read_list, self.auth.read_detail), + ('HEAD', self.auth.read_list, self.auth.read_detail), + ('POST', self.auth.create_list, self.auth.create_detail), + ('PUT', self.auth.update_list, self.auth.update_detail), + ('PATCH', self.auth.read_list, self.auth.read_detail), + ('PATCH', self.auth.update_list, self.auth.update_detail), + ('PATCH', self.auth.delete_list, self.auth.delete_detail), + ('DELETE', self.auth.delete_list, self.auth.delete_detail)): self.generic_test_authorized(method, request, function_list, - function_detail) - + function_detail) def test_patch_perms(self): """User() should have patch (add, change, delete) permissions granted.""" @@ -280,21 +279,21 @@ def test_patch_perms(self): # Not enough. request.user.user_permissions.add(self.add) self.generic_test_unauthorized('PATCH', request, self.auth.update_list, - self.auth.update_detail) + self.auth.update_detail) self.generic_test_unauthorized('PATCH', request, self.auth.delete_list, - self.auth.delete_detail) + self.auth.delete_detail) # Still not enough. request.user.user_permissions.add(self.change) self.generic_test_unauthorized('PATCH', request, self.auth.delete_list, - self.auth.delete_detail) + self.auth.delete_detail) # Much better. request.user.user_permissions.add(self.delete) # Nuke the perm cache. :/ del request.user._perm_cache self.generic_test_authorized('PATCH', request, self.auth.delete_list, - self.auth.delete_detail) + self.auth.delete_detail) def test_unrecognized_method(self): """User() should not have the permission to call non-existent method.""" @@ -304,7 +303,8 @@ def test_unrecognized_method(self): # Check a non-existent HTTP method. request.method = 'EXPLODE' - self.generic_test_unauthorized('EXPLODE', request, self.auth.update_list, + self.generic_test_unauthorized( + 'EXPLODE', request, self.auth.update_list, self.auth.update_detail) def test_get_environment(self): @@ -361,8 +361,8 @@ def test_post(self): self.assertEqual( json.loads(response.content)[k], v) response = self.client.delete('/api/v1/environment/{0}/'.format(id), - content_type='application/json', - **self.post_auth) + content_type='application/json', + **self.post_auth) self.assertEquals(response.status_code, 204) def test_put(self): @@ -440,6 +440,7 @@ def test_delete(self): class ProjectTest(FixtureTestCase): + """Test Project() API""" def setUp(self): @@ -463,7 +464,7 @@ def setUp(self): repo_path="ssh://hg@bitbucket.org/pypy/pypy", repo_user="alpha", repo_pass="beta", - ) + ) self.project = Project(**self.project_data) self.project.save() self.client = Client() @@ -480,17 +481,18 @@ def test_all(self): request.user.user_permissions.add(self.delete) for method, function_list, function_detail in \ - (('GET',self.auth.read_list, self.auth.read_detail), - ('OPTIONS',self.auth.read_list, self.auth.read_detail), - ('HEAD',self.auth.read_list, self.auth.read_detail), - ('POST',self.auth.create_list, self.auth.create_detail), - ('PUT',self.auth.update_list, self.auth.update_detail), - ('PATCH',self.auth.read_list, self.auth.read_detail), - ('PATCH',self.auth.update_list, self.auth.update_detail), - ('PATCH',self.auth.delete_list, self.auth.delete_detail), - ('DELETE',self.auth.delete_list, self.auth.delete_detail)): + (('GET', self.auth.read_list, self.auth.read_detail), + ('OPTIONS', self.auth.read_list, self.auth.read_detail), + ('HEAD', self.auth.read_list, self.auth.read_detail), + ('POST', self.auth.create_list, self.auth.create_detail), + ('PUT', self.auth.update_list, self.auth.update_detail), + ('PATCH', self.auth.read_list, self.auth.read_detail), + ('PATCH', self.auth.update_list, self.auth.update_detail), + ('PATCH', self.auth.delete_list, self.auth.delete_detail), + ('DELETE', self.auth.delete_list, self.auth.delete_detail)): self.generic_test_authorized(method, request, function_list, - function_detail) + function_detail) + def test_get_project(self): """Should get an existing project""" response = self.client.get('/api/v1/project/{0}/'.format( @@ -556,6 +558,7 @@ def test_delete(self): class ExecutableTest(FixtureTestCase): + """Test Executable() API""" def setUp(self): @@ -617,6 +620,7 @@ def test_nonexistent(self): class BranchTest(FixtureTestCase): + """Test Branch() API""" def setUp(self): @@ -634,7 +638,7 @@ def setUp(self): repo_path="ssh://hg@bitbucket.org/pypy/pypy", repo_user="fridolin", repo_pass="secret", - ) + ) self.project = Project(**self.project_data) self.project.save() self.branch2_data = dict( @@ -734,6 +738,7 @@ def test_delete(self): class RevisionTest(FixtureTestCase): + """Test Revision() API""" def setUp(self): @@ -869,6 +874,7 @@ def test_delete(self): class ExecutableTest(FixtureTestCase): + """Test Executable() API""" def setUp(self): @@ -987,6 +993,7 @@ def test_delete(self): class BenchmarkTest(FixtureTestCase): + """Test Benchmark() API""" def setUp(self): @@ -1045,7 +1052,7 @@ def test_post(self): data=json.dumps(modified_data), content_type='application/json', **self.post_auth - ) + ) self.assertEquals(response.status_code, 201) id = response['Location'].rsplit('/', 2)[-2] response = self.client.get('/api/v1/benchmark/{0}/'.format(id)) @@ -1105,6 +1112,7 @@ def test_delete(self): class ReportTest(FixtureTestCase): + """Test Report() API""" def setUp(self): @@ -1220,6 +1228,7 @@ def test_delete(self): class ResultBundleTestCase(FixtureTestCase): + """Test CRUD of results via API""" def setUp(self): @@ -1256,7 +1265,7 @@ def setUp(self): def test_populate_and_save(self): """Should populate ResultBundle() with data""" - bundle = ResultBundle(request=self.request,**self.data1) + bundle = ResultBundle(request=self.request, **self.data1) bundle._populate_obj_by_data() # should raise exception if not OK bundle.hydrate_and_save() @@ -1267,7 +1276,7 @@ def test_save_same_result_again(self): modified_data = copy.deepcopy(self.data1) modified_data['environment'] = '/api/v1/environment/1/' modified_data['project'] = '/api/v1/project/1/' - bundle = ResultBundle(request=self.request,**modified_data) + bundle = ResultBundle(request=self.request, **modified_data) bundle._populate_obj_by_data() self.assertRaises(IntegrityError, bundle.hydrate_and_save) @@ -1289,12 +1298,12 @@ def test_date_attr_set(self): """Should add date attr to Result() obj if date is not given""" # date is set automatically modified_data = copy.deepcopy(self.data1) - bundle = ResultBundle(request=self.request,**modified_data) + bundle = ResultBundle(request=self.request, **modified_data) bundle.hydrate_and_save() self.assertIsInstance(bundle.obj.date, datetime) # date set by value modified_data['date'] = '2011-05-05 03:01:45' - bundle = ResultBundle(request=self.request,**modified_data) + bundle = ResultBundle(request=self.request, **modified_data) # wrong date string modified_data['date'] = '2011-05-05T03:01:45' self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) @@ -1302,7 +1311,7 @@ def test_date_attr_set(self): def test_optional_data(self): """Should save optional data.""" data = dict(self.data1.items() + self.data_optional.items()) - bundle = ResultBundle(request=self.request,**data) + bundle = ResultBundle(request=self.request, **data) bundle.hydrate_and_save() self.assertIsInstance(bundle.obj.date, datetime) self.assertEqual(bundle.obj.std_dev, @@ -1314,6 +1323,7 @@ def test_optional_data(self): class ResultBundleResourceTestCase(FixtureTestCase): + """Submitting new benchmark results""" DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' @@ -1405,11 +1415,11 @@ def test_post_invalid_data(self): def test_get_one(self): """Should get a result bundle""" response = self.client.get('/api/v1/benchmark-result/1/', - content_type='application/json') + content_type='application/json') self.assertEquals(response.status_code, 200) response_data = json.loads(response.content) for k in ('project', 'result', 'branch', 'benchmark', 'environment', - 'executable', 'revision'): + 'executable', 'revision'): self.assertEqual( response_data[k], '/api/v1/{0}/1/'.format(k,)) From 0e4c0bd2f72cbef964296213d72f2a8090e8823a Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Fri, 9 Aug 2013 19:54:12 -0400 Subject: [PATCH 192/429] minor changes to increase readability in API tests --- codespeed/tests/tests_api.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py index 4e5832c1..124c9de6 100644 --- a/codespeed/tests/tests_api.py +++ b/codespeed/tests/tests_api.py @@ -208,10 +208,11 @@ def test_no_perms(self): self.generic_test_authorized('GET', request, self.auth.read_list, self.auth.read_detail) - for method, function_list, function_detail in \ - (('POST', self.auth.create_list, self.auth.create_detail), - ('PUT', self.auth.update_list, self.auth.update_detail), - ('DELETE', self.auth.delete_list, self.auth.delete_detail)): + methods = [('POST', self.auth.create_list, self.auth.create_detail), + ('PUT', self.auth.update_list, self.auth.update_detail), + ('DELETE', self.auth.delete_list, self.auth.delete_detail)] + + for method, function_list, function_detail in methods: self.generic_test_unauthorized(method, request, function_list, function_detail) @@ -257,8 +258,7 @@ def test_all(self): request.user.user_permissions.add(self.change) request.user.user_permissions.add(self.delete) - for method, function_list, function_detail in \ - (('GET', self.auth.read_list, self.auth.read_detail), + methods = [('GET', self.auth.read_list, self.auth.read_detail), ('OPTIONS', self.auth.read_list, self.auth.read_detail), ('HEAD', self.auth.read_list, self.auth.read_detail), ('POST', self.auth.create_list, self.auth.create_detail), @@ -266,7 +266,9 @@ def test_all(self): ('PATCH', self.auth.read_list, self.auth.read_detail), ('PATCH', self.auth.update_list, self.auth.update_detail), ('PATCH', self.auth.delete_list, self.auth.delete_detail), - ('DELETE', self.auth.delete_list, self.auth.delete_detail)): + ('DELETE', self.auth.delete_list, self.auth.delete_detail)] + + for method, function_list, function_detail in methods: self.generic_test_authorized(method, request, function_list, function_detail) @@ -480,8 +482,7 @@ def test_all(self): request.user.user_permissions.add(self.change) request.user.user_permissions.add(self.delete) - for method, function_list, function_detail in \ - (('GET', self.auth.read_list, self.auth.read_detail), + methods = [('GET', self.auth.read_list, self.auth.read_detail), ('OPTIONS', self.auth.read_list, self.auth.read_detail), ('HEAD', self.auth.read_list, self.auth.read_detail), ('POST', self.auth.create_list, self.auth.create_detail), @@ -489,7 +490,9 @@ def test_all(self): ('PATCH', self.auth.read_list, self.auth.read_detail), ('PATCH', self.auth.update_list, self.auth.update_detail), ('PATCH', self.auth.delete_list, self.auth.delete_detail), - ('DELETE', self.auth.delete_list, self.auth.delete_detail)): + ('DELETE', self.auth.delete_list, self.auth.delete_detail)] + + for method, function_list, function_detail in methods: self.generic_test_authorized(method, request, function_list, function_detail) From 53497670b958093d1bd1f137b61e146993d4628f Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Fri, 9 Aug 2013 20:01:21 -0400 Subject: [PATCH 193/429] cleaned requirements.txt and keeping tastypie=0.10.0 because earlier versions incompat. with Django 1.5 --- requirements.txt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 22a44a99..22d6321b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,4 @@ Django>=1.4 South<=2.0 isodate==0.4.8 -# version 0.9.12 -#-e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie -#django-tastypie==0.9.15 -# version 0.10.0 with astimezone() bug resolved by an added commit --e git://github.com/toastdriven/django-tastypie.git@b78175d4c401e99f814fb6f287853a650542cbb8#egg=django-tastypie -#-e git://github.com/robhudson/django-debug-toolbar.git#egg=django-debug-toolbar +django-tastypie==0.9.15 From edade1e3ac706b7b7ed958c3d63f30352aa3b4b2 Mon Sep 17 00:00:00 2001 From: Rayan Chikhi Date: Fri, 9 Aug 2013 20:10:18 -0400 Subject: [PATCH 194/429] oops, wrong version of tastypie in previous commit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22d6321b..5062a274 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Django>=1.4 South<=2.0 isodate==0.4.8 -django-tastypie==0.9.15 +-e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie From 6aacee93707032613017641ffa56d3d46f6fc9eb Mon Sep 17 00:00:00 2001 From: Phil Opaola Date: Mon, 16 Sep 2013 15:57:19 -0400 Subject: [PATCH 195/429] Update github.py to relfect newest version of github's API. Change changes_log/changes_table templates to use commit_id instead of commit_id --- codespeed/github.py | 39 ++++++++++--------- .../templates/codespeed/changes_logs.html | 4 +- .../templates/codespeed/changes_table.html | 6 +-- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/codespeed/github.py b/codespeed/github.py index 50992eae..3b13daa8 100644 --- a/codespeed/github.py +++ b/codespeed/github.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) GITHUB_URL_RE = re.compile( - r'^(?P\w+)://github.com/(?P[^/]+)/(?P[^/]+)[.]git$') + r'^(?P\w+)://github.com/(?P[^/]+)/(?P[^/]+)([.]git)?$') # We currently use a simple linear search of on a single parent to retrieve # the history. This is often good enough, but might miss the actual starting @@ -33,7 +33,7 @@ def updaterepo(project, update=True): def retrieve_revision(commit_id, username, project, revision=None): - commit_url = 'http://github.com/api/v2/json/commits/show/%s/%s/%s' % ( + commit_url = 'https://api.github.com/repos/%s/%s/git/commits/%s' % ( username, project, commit_id) commit_json = cache.get(commit_url) @@ -46,7 +46,7 @@ def retrieve_revision(commit_id, username, project, revision=None): commit_url, e, exc_info=True) raise e - if 'error' in commit_json: + if commit_json["message"] in ("Not Found", "Server Error",): # We'll still cache these for a brief period of time to avoid making too many requests: cache.set(commit_url, commit_json, 300) else: @@ -54,12 +54,10 @@ def retrieve_revision(commit_id, username, project, revision=None): # SCM diffs shouldn't change: cache.set(commit_url, commit_json, 86400 * 30) - if 'error' in commit_json: - raise RuntimeError("Unable to load %s: %s" % (commit_url, commit_json['error'])) + if commit_json["message"] in ("Not Found", "Server Error",): + raise RuntimeError("Unable to load %s: %s" % (commit_url, commit_json["message"])) - commit = commit_json['commit'] - - date = isodate.parse_datetime(commit['committed_date']) + date = isodate.parse_datetime(commit_json['committer']['date']) if revision: # Overwrite any existing data we might have for this revision since @@ -68,19 +66,19 @@ def retrieve_revision(commit_id, username, project, revision=None): # We need to convert the timezone-aware date to a naive (i.e. # timezone-less) date in UTC to avoid killing MySQL: revision.date = date.astimezone(isodate.tzinfo.Utc()).replace(tzinfo=None) - revision.author = commit['author']['name'] - revision.message = commit['message'] + revision.author = commit_json['author']['name'] + revision.message = commit_json['message'] revision.full_clean() revision.save() return {'date': date, - 'message': commit['message'], + 'message': commit_json['message'], 'body': "", # TODO: pretty-print diffs - 'author': commit['author']['name'], - 'author_email': commit['author']['email'], - 'commitid': commit['id'], - 'short_commit_id': commit['id'][0:7], - 'parents': commit['parents']} + 'author': commit_json['author']['name'], + 'author_email': commit_json['author']['email'], + 'commit_id': commit_json['sha'], + 'short_commit_id': commit_json['sha'][0:7], + 'parents': commit_json['parents']} def getlogs(endrev, startrev): @@ -90,6 +88,9 @@ def getlogs(endrev, startrev): else: revisions = [i for i in (startrev, endrev) if i.commitid] + if endrev.branch.project.repo_path[-1] == '/': + endrev.branch.project.repo_path = endrev.branch.project.repo_path[:-1] + m = GITHUB_URL_RE.match(endrev.branch.project.repo_path) if not m: @@ -110,16 +111,16 @@ def getlogs(endrev, startrev): last_rev_data = retrieve_revision(revision.commitid, username, project, revision) logs.append(last_rev_data) revision_count += 1 - ancestor_found = (startrev.commitid in [rev['id'] for rev in last_rev_data['parents']]) + ancestor_found = (startrev.commitid in [rev['sha'] for rev in last_rev_data['parents']]) # Simple approach to find the startrev, stop after found or after # #GITHUB_REVISION_LIMIT revisions are fetched while (revision_count < GITHUB_REVISION_LIMIT and not ancestor_found and len(last_rev_data['parents']) > 0): - last_rev_data = retrieve_revision(last_rev_data['parents'][0]['id'], username, project) + last_rev_data = retrieve_revision(last_rev_data['parents'][0]['sha'], username, project) logs.append(last_rev_data) revision_count += 1 - ancestor_found = (startrev.commitid in [rev['id'] for rev in last_rev_data['parents']]) + ancestor_found = (startrev.commitid in [rev['sha'] for rev in last_rev_data['parents']]) return sorted(logs, key=lambda i: i['date'], reverse=True) diff --git a/codespeed/templates/codespeed/changes_logs.html b/codespeed/templates/codespeed/changes_logs.html index 1c1ad758..af47c658 100644 --- a/codespeed/templates/codespeed/changes_logs.html +++ b/codespeed/templates/codespeed/changes_logs.html @@ -12,9 +12,9 @@ {% endif %} commited {% if log.commit_browse_url %} - {{ log.short_commit_id|default:log.commitid }}: + {{ log.short_commit_id|default:log.commit_id }}: {% else %} - {{ log.short_commit_id|default:log.commitid }}: + {{ log.short_commit_id|default:log.commit_id }}: {% endif %}
{{ log.message|linebreaksbr|urlize }}
diff --git a/codespeed/templates/codespeed/changes_table.html b/codespeed/templates/codespeed/changes_table.html index 50399982..f45d0e65 100644 --- a/codespeed/templates/codespeed/changes_table.html +++ b/codespeed/templates/codespeed/changes_table.html @@ -76,7 +76,7 @@
- + {% ifnotequal rev.branch.project.repo_type "N" %} @@ -94,12 +94,12 @@ - +
Commit{% if rev.get_browsing_url %}{{ rev.commitid }}{% else %}{{ rev.commitid }}{% endif %}{% if rev.get_browsing_url %}{{ rev.commit_id }}{% else %}{{ rev.commit_id }}{% endif %}
Date{{ rev.date }}
Commit logs
Loading...
{% endifnotequal %} From 0c96c028b0b4c0ad2ae56a2f97a718d7a36c7aaf Mon Sep 17 00:00:00 2001 From: Phil Opaola Date: Mon, 23 Sep 2013 12:00:40 -0400 Subject: [PATCH 196/429] Revert commit_id to commitid in changes_logs.html and github.py to maintain functionality with alternative VCSes --- codespeed/github.py | 2 +- codespeed/templates/codespeed/changes_logs.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codespeed/github.py b/codespeed/github.py index 3b13daa8..35ac538e 100644 --- a/codespeed/github.py +++ b/codespeed/github.py @@ -76,7 +76,7 @@ def retrieve_revision(commit_id, username, project, revision=None): 'body': "", # TODO: pretty-print diffs 'author': commit_json['author']['name'], 'author_email': commit_json['author']['email'], - 'commit_id': commit_json['sha'], + 'commitid': commit_json['sha'], 'short_commit_id': commit_json['sha'][0:7], 'parents': commit_json['parents']} diff --git a/codespeed/templates/codespeed/changes_logs.html b/codespeed/templates/codespeed/changes_logs.html index af47c658..1c1ad758 100644 --- a/codespeed/templates/codespeed/changes_logs.html +++ b/codespeed/templates/codespeed/changes_logs.html @@ -12,9 +12,9 @@ {% endif %} commited {% if log.commit_browse_url %} - {{ log.short_commit_id|default:log.commit_id }}: + {{ log.short_commit_id|default:log.commitid }}: {% else %} - {{ log.short_commit_id|default:log.commit_id }}: + {{ log.short_commit_id|default:log.commitid }}: {% endif %}
{{ log.message|linebreaksbr|urlize }}
From 344dc07e02d674ef39ec1d9f1059ce1be2eb052f Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 3 Oct 2013 14:54:58 -0400 Subject: [PATCH 197/429] codespeed does not work with Django 1.5, let's req the most stable working version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5062a274..f2a3bbd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=1.4 +Django==1.4.8 South<=2.0 isodate==0.4.8 -e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie From 24260a1ad6c896ac2af0cc7dd2ab77316ce47b62 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 3 Oct 2013 14:55:44 -0400 Subject: [PATCH 198/429] Pip has django-tastypie, let's use it Makes deployments easier --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f2a3bbd5..2b01ccc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Django==1.4.8 South<=2.0 isodate==0.4.8 --e git://github.com/toastdriven/django-tastypie.git@5397dec04fe83092a56ba14a843731f2aa08184d#egg=django-tastypie +django-tastypie==0.10.0 From 7ad708163a2b525cd7c5c2a995c79dfe97e07e99 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Sun, 6 Oct 2013 15:55:02 -0400 Subject: [PATCH 199/429] Supports Django 1.5.1+ --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2b01ccc1..518cfc03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==1.4.8 +Django>=1.5.1 South<=2.0 isodate==0.4.8 django-tastypie==0.10.0 From 44e7bf6e05c52a90f29ee8c07a41563d8c8872ff Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Sun, 6 Oct 2013 16:26:59 -0400 Subject: [PATCH 200/429] Back to Django 1.4+ --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 518cfc03..ed569aec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=1.5.1 +Django>=1.4 South<=2.0 isodate==0.4.8 django-tastypie==0.10.0 From df6d12d17331f61cfa7bd5e4a4499aa91c1fa91f Mon Sep 17 00:00:00 2001 From: Phil Opaola Date: Mon, 7 Oct 2013 16:39:21 -0400 Subject: [PATCH 201/429] Update test_github_browsing_url --- codespeed/tests/tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index b12dfda4..64288fc6 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -451,12 +451,14 @@ def test_github_browsing_url(self): # It should work with https:// as well as git:// urls self.github_project.save() self.assertEquals(self.github_project.commit_browsing_url, - 'https://github.com/tobami/codespeed/commit/{commitid}') + 'https://github.com/tobami/codespeed.git/' + 'commit/{commitid}') self.github_project.repo_path = 'git://github.com/tobami/codespeed.git' self.github_project.save() self.assertEquals(self.github_project.commit_browsing_url, - 'https://github.com/tobami/codespeed/commit/{commitid}') + 'https://github.com/tobami/codespeed.git/' + 'commit/{commitid}') # If filled in, commit browsing url should not change self.github_project.commit_browsing_url = 'https://example.com/{commitid}' From c044dde86cc4b588521fcbb8dddca76a986f3627 Mon Sep 17 00:00:00 2001 From: Phil Opaola Date: Mon, 7 Oct 2013 16:55:16 -0400 Subject: [PATCH 202/429] Rename commit_id to commitid in changes_table.html --- codespeed/templates/codespeed/changes_table.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codespeed/templates/codespeed/changes_table.html b/codespeed/templates/codespeed/changes_table.html index f45d0e65..c3fce744 100644 --- a/codespeed/templates/codespeed/changes_table.html +++ b/codespeed/templates/codespeed/changes_table.html @@ -74,9 +74,9 @@ - + Commit - {% if rev.get_browsing_url %}{{ rev.commit_id }}{% else %}{{ rev.commit_id }}{% endif %} + {% if rev.get_browsing_url %}{{ rev.commitid }}{% else %}{{ rev.commitid }}{% endif %} Date{{ rev.date }} {% ifnotequal rev.branch.project.repo_type "N" %} @@ -94,12 +94,12 @@ Commit logs - + Loading... {% endifnotequal %} From eb7933e182b7e460a77ba2c42b4c0779f1d0f63f Mon Sep 17 00:00:00 2001 From: Phil Opaola Date: Mon, 7 Oct 2013 17:00:52 -0400 Subject: [PATCH 203/429] Update changes_table.html --- codespeed/templates/codespeed/changes_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/templates/codespeed/changes_table.html b/codespeed/templates/codespeed/changes_table.html index c3fce744..50399982 100644 --- a/codespeed/templates/codespeed/changes_table.html +++ b/codespeed/templates/codespeed/changes_table.html @@ -74,7 +74,7 @@ - + Commit {% if rev.get_browsing_url %}{{ rev.commitid }}{% else %}{{ rev.commitid }}{% endif %} From 8ea2dc2202280b8be9829e6d0d25ccf6e78b8aca Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Wed, 13 Nov 2013 19:21:03 +0100 Subject: [PATCH 204/429] Remove tastypie dependency --- codespeed/tests/tests_api.py | 1434 ---------------------------------- codespeed/urls.py | 18 - sample_project/settings.py | 2 - sample_project/urls.py | 10 - 4 files changed, 1464 deletions(-) delete mode 100644 codespeed/tests/tests_api.py diff --git a/codespeed/tests/tests_api.py b/codespeed/tests/tests_api.py deleted file mode 100644 index 124c9de6..00000000 --- a/codespeed/tests/tests_api.py +++ /dev/null @@ -1,1434 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests related to RESTful API -""" -from datetime import datetime -import copy -import json -import logging -import unittest - -from django import test -from django.db.utils import IntegrityError -from django.test.client import Client -from django.http import HttpRequest -from django.core.urlresolvers import reverse -from django.conf import settings -from django.contrib.auth.models import User, Permission -from tastypie.authorization import (Authorization, ReadOnlyAuthorization, - DjangoAuthorization) -from tastypie.exceptions import ImmediateHttpResponse, Unauthorized -from tastypie.models import ApiKey, create_api_key -from tastypie.http import HttpUnauthorized -from tastypie.authentication import ApiKeyAuthentication - -from codespeed.api import EnvironmentResource, ProjectResource -from codespeed.models import (Project, Benchmark, Revision, Branch, - Executable, Environment, Result, Report) -from codespeed.api import ResultBundle -from codespeed import settings as default_settings - - -class FixtureTestCase(test.TestCase): - fixtures = ["gettimeline_unittest.json"] - - def setUp(self): - self.api_user = User.objects.create_user( - username='apiuser', email='api@foo.bar', password='password') - self.api_user.save() - self.api_user.user_permissions.clear() - john_doe = User.objects.create_user( - username='johndoe', email='api@foo.bar', password='password') - john_doe.save() - authorization = 'ApiKey {0}:{1}'.format(self.api_user.username, - self.api_user.api_key.key) - self.post_auth = {'HTTP_AUTHORIZATION': authorization} - - def generic_test_authorized(self, method, request, function_list, function_detail): - bundle = self.resource.build_bundle(request=request) - bundle.request.method = method - self.assertTrue( - function_detail(self.resource.get_object_list(bundle.request)[0], - bundle)) - - def generic_test_unauthorized(self, method, request, function_list, function_detail): - bundle = self.resource.build_bundle(request=request) - bundle.request.method = method - self.assertRaises(Unauthorized, function_detail, - self.resource.get_object_list(bundle.request)[0], bundle) - - -class ApiKeyAuthenticationTestCase(FixtureTestCase): - - def setUp(self): - super(ApiKeyAuthenticationTestCase, self).setUp() - ApiKey.objects.all().delete() - self.auth = ApiKeyAuthentication() - self.request = HttpRequest() - - # Simulate sending the signal. - user = User.objects.get(username='apiuser') - create_api_key(User, instance=user, created=True) - - def test_is_not_authenticated(self): - """Should return HttpUnauthorized when incorrect credentials are given""" - # No username/api_key details - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - # Wrong username details. - self.request.GET['username'] = 'foo' - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - # No api_key. - self.request.GET['username'] = 'daniel' - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - # Wrong user/api_key. - self.request.GET['username'] = 'daniel' - self.request.GET['api_key'] = 'foo' - self.assertEqual(isinstance( - self.auth.is_authenticated(self.request), HttpUnauthorized), True) - - def test_is_authenticated(self): - """Should correctly authenticate when using an existing user and key""" - # Correct user/api_key. - user = User.objects.get(username='apiuser') - self.request.GET['username'] = 'apiuser' - self.request.GET['api_key'] = user.api_key.key - self.assertEqual(self.auth.is_authenticated(self.request), True) - - -class UserTest(FixtureTestCase): - - """Test api user related stuff""" - - def test_has_apikey(self): - """User() should have an api key attr that was generated automatically.""" - self.assertTrue(hasattr(self.api_user, 'api_key')) - - def test_len_apikey(self): - """Should have api user key with a non-zero length.""" - self.assertTrue(len(self.api_user.api_key.key) >= 1) - - def test_is_authenticated_header(self): - """Taken from tastypie test suite to ensure api key is generated for - new users correctly and tastypie is installed correctly. - """ - auth = ApiKeyAuthentication() - request = HttpRequest() - - # Simulate sending the signal. - john_doe = User.objects.get(username='johndoe') - - # No username/api_key details should fail. - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong username details. - request.META['HTTP_AUTHORIZATION'] = 'foo' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # No api_key. - request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Wrong user/api_key. - request.META['HTTP_AUTHORIZATION'] = 'ApiKey daniel:pass' - self.assertEqual(isinstance(auth.is_authenticated(request), HttpUnauthorized), True) - - # Correct user/api_key. - john_doe = User.objects.get(username='johndoe') - request.META['HTTP_AUTHORIZATION'] = 'ApiKey johndoe:%s' % john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - - # Capitalization shouldn't matter. - john_doe = User.objects.get(username='johndoe') - request.META['HTTP_AUTHORIZATION'] = 'aPiKeY johndoe:%s' % john_doe.api_key.key - self.assertEqual(auth.is_authenticated(request), True) - - def test_api_key(self): - """User should be authenticated by it's api key.""" - auth = ApiKeyAuthentication() - request = HttpRequest() - authorization = 'ApiKey %s:%s' % (self.api_user.username, - self.api_user.api_key.key) - request.META['HTTP_AUTHORIZATION'] = authorization - self.assertEqual(auth.is_authenticated(request), True) - - -class EnvironmentDjangoAuthorizationTestCase(FixtureTestCase): - - """Test Environment() API""" - - def setUp(self): - super(EnvironmentDjangoAuthorizationTestCase, self).setUp() - self.add = Permission.objects.get_by_natural_key( - 'add_environment', 'codespeed', 'environment') - self.change = Permission.objects.get_by_natural_key( - 'change_environment', 'codespeed', 'environment') - self.delete = Permission.objects.get_by_natural_key( - 'delete_environment', 'codespeed', 'environment') - self.env1_data = dict( - name="env1", - cpu="cpu1", - memory="48kB", - os="ZX Spectrum OS", - kernel="2.6.32" - ) - self.env1 = Environment(**self.env1_data) - self.env1.save() - self.env2_data = dict( - name="env2", - cpu="z80", - memory="64kB", - os="ZX Spectrum OS", - kernel="2.6.32" - ) - env_db1 = Environment.objects.get(id=1) - self.env_db1_data = dict( - [(k, getattr(env_db1, k)) for k in self.env1_data.keys()] - ) - self.client = Client() - self.resource = EnvironmentResource() - self.auth = self.resource._meta.authorization - - def test_no_perms(self): - """User() should have only GET permission""" - # sanity check: user has no permissions - self.assertFalse(self.api_user.get_all_permissions()) - - request = HttpRequest() - request.method = 'GET' - request.user = self.api_user - - # with no permissions, api is read-only - self.generic_test_authorized('GET', request, self.auth.read_list, - self.auth.read_detail) - - methods = [('POST', self.auth.create_list, self.auth.create_detail), - ('PUT', self.auth.update_list, self.auth.update_detail), - ('DELETE', self.auth.delete_list, self.auth.delete_detail)] - - for method, function_list, function_detail in methods: - self.generic_test_unauthorized(method, request, function_list, - function_detail) - - def test_add_perm(self): - """User() should have add permission granted.""" - request = HttpRequest() - request.user = self.api_user - - # give add permission - request.user.user_permissions.add(self.add) - - self.generic_test_authorized('POST', request, self.auth.create_list, - self.auth.create_detail) - - def test_change_perm(self): - """User() should have change permission granted.""" - request = HttpRequest() - request.user = self.api_user - - # give change permission - request.user.user_permissions.add(self.change) - - self.generic_test_authorized('PUT', request, self.auth.update_list, - self.auth.update_detail) - - def test_delete_perm(self): - """User() should have delete permission granted.""" - request = HttpRequest() - request.user = self.api_user - - # give delete permission - request.user.user_permissions.add(self.delete) - - self.generic_test_authorized('DELETE', request, self.auth.delete_list, - self.auth.delete_detail) - - def test_all(self): - """User() should have add, change, delete permissions granted.""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - methods = [('GET', self.auth.read_list, self.auth.read_detail), - ('OPTIONS', self.auth.read_list, self.auth.read_detail), - ('HEAD', self.auth.read_list, self.auth.read_detail), - ('POST', self.auth.create_list, self.auth.create_detail), - ('PUT', self.auth.update_list, self.auth.update_detail), - ('PATCH', self.auth.read_list, self.auth.read_detail), - ('PATCH', self.auth.update_list, self.auth.update_detail), - ('PATCH', self.auth.delete_list, self.auth.delete_detail), - ('DELETE', self.auth.delete_list, self.auth.delete_detail)] - - for method, function_list, function_detail in methods: - self.generic_test_authorized(method, request, function_list, - function_detail) - - def test_patch_perms(self): - """User() should have patch (add, change, delete) permissions granted.""" - request = HttpRequest() - request.user = self.api_user - request.method = 'PATCH' - - # Not enough. - request.user.user_permissions.add(self.add) - self.generic_test_unauthorized('PATCH', request, self.auth.update_list, - self.auth.update_detail) - self.generic_test_unauthorized('PATCH', request, self.auth.delete_list, - self.auth.delete_detail) - - # Still not enough. - request.user.user_permissions.add(self.change) - self.generic_test_unauthorized('PATCH', request, self.auth.delete_list, - self.auth.delete_detail) - - # Much better. - request.user.user_permissions.add(self.delete) - # Nuke the perm cache. :/ - del request.user._perm_cache - self.generic_test_authorized('PATCH', request, self.auth.delete_list, - self.auth.delete_detail) - - def test_unrecognized_method(self): - """User() should not have the permission to call non-existent method.""" - request = HttpRequest() - self.api_user.user_permissions.clear() - request.user = self.api_user - - # Check a non-existent HTTP method. - request.method = 'EXPLODE' - self.generic_test_unauthorized( - 'EXPLODE', request, self.auth.update_list, - self.auth.update_detail) - - def test_get_environment(self): - """Should get an environment when given an existing ID""" - response = self.client.get( - '/api/v1/environment/1/?username={0}&api_key={1}'.format( - self.api_user.username, self.api_user.api_key.key)) - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], "Dual Core") - - def test_get_non_existing_environment(self): - """Should return 404 when given a non existing environment ID""" - response = self.client.get( - '/api/v1/environment/999/?username={0}&api_key={1}'.format( - self.api_user.username, self.api_user.api_key.key)) - self.assertEquals(response.status_code, 404) - - def test_get_environment_all_fields(self): - """Should get all fields for an environment""" - response = self.client.get( - '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( - self.env1.pk, self.api_user.username, self.api_user.api_key.key) - ) - self.assertEquals(response.status_code, 200) - for k in self.env1_data.keys(): - self.assertEqual( - json.loads(response.content)[k], getattr(self.env1, k)) - - def test_post(self): - """Should save a new environment""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - response = self.client.post('/api/v1/environment/', - data=json.dumps(self.env2_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/environment/', - data=json.dumps(self.env2_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - #response = self.client.get('/api/v1/environment/{0}/'.format(id)) - response = self.client.get( - '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( - id, self.api_user.username, self.api_user.api_key.key) - ) - for k, v in self.env2_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - response = self.client.delete('/api/v1/environment/{0}/'.format(id), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - def test_put(self): - """Should modify an existing environment""" - modified_data = copy.deepcopy(self.env_db1_data) - modified_data['name'] = "env2.2" - modified_data['memory'] = "128kB" - response = self.client.put('/api/v1/environment/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 401) - - request = HttpRequest() - request.user = self.api_user - request.user.user_permissions.add(self.change) - - response = self.client.put('/api/v1/environment/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/environment/1/') - response = self.client.get( - '/api/v1/environment/1/?username={0}&api_key={1}'.format( - self.api_user.username, self.api_user.api_key.key) - ) - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def test_delete(self): - """Should delete an environment""" - response = self.client.get( - '/api/v1/environment/1/?username={0}&api_key={1}'.format( - self.api_user.username, self.api_user.api_key.key)) - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/environment/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 401) - - request = HttpRequest() - request.user = self.api_user - request.user.user_permissions.add(self.delete) - response = self.client.delete('/api/v1/environment/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - response = self.client.get( - '/api/v1/environment/1/?username={0}&api_key={1}'.format( - self.api_user.username, self.api_user.api_key.key)) - self.assertEquals(response.status_code, 404) - - # from just created data - response = self.client.get( - '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( - self.env1.pk, self.api_user.username, self.api_user.api_key.key) - ) - self.assertEquals(response.status_code, 200) - response = self.client.delete( - '/api/v1/environment/{0}/'.format(self.env1.id), - content_type='application/json', - **self.post_auth - ) - self.assertEquals(response.status_code, 204) - - response = self.client.get( - '/api/v1/environment/{0}/?username={1}&api_key={2}'.format( - self.env1.pk, self.api_user.username, self.api_user.api_key.key) - ) - self.assertEquals(response.status_code, 404) - - -class ProjectTest(FixtureTestCase): - - """Test Project() API""" - - def setUp(self): - super(ProjectTest, self).setUp() - self.add = Permission.objects.get_by_natural_key( - 'add_project', 'codespeed', 'project') - self.change = Permission.objects.get_by_natural_key( - 'change_project', 'codespeed', 'project') - self.delete = Permission.objects.get_by_natural_key( - 'delete_project', 'codespeed', 'project') - self.project_data = dict( - name="PyPy", - repo_type="M", - repo_path="ssh://hg@bitbucket.org/pypy/pypy", - repo_user="fridolin", - repo_pass="secret", - ) - self.project_data2 = dict( - name="project alpha", - repo_type="M", - repo_path="ssh://hg@bitbucket.org/pypy/pypy", - repo_user="alpha", - repo_pass="beta", - ) - self.project = Project(**self.project_data) - self.project.save() - self.client = Client() - self.resource = ProjectResource() - self.auth = self.resource._meta.authorization - - def test_all(self): - """User should have all permissions granted.""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - methods = [('GET', self.auth.read_list, self.auth.read_detail), - ('OPTIONS', self.auth.read_list, self.auth.read_detail), - ('HEAD', self.auth.read_list, self.auth.read_detail), - ('POST', self.auth.create_list, self.auth.create_detail), - ('PUT', self.auth.update_list, self.auth.update_detail), - ('PATCH', self.auth.read_list, self.auth.read_detail), - ('PATCH', self.auth.update_list, self.auth.update_detail), - ('PATCH', self.auth.delete_list, self.auth.delete_detail), - ('DELETE', self.auth.delete_list, self.auth.delete_detail)] - - for method, function_list, function_detail in methods: - self.generic_test_authorized(method, request, function_list, - function_detail) - - def test_get_project(self): - """Should get an existing project""" - response = self.client.get('/api/v1/project/{0}/'.format( - self.project.id,)) - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], "{0}".format( - self.project_data['name'])) - - def test_get_project_all_fields(self): - """Should get all fields for a project""" - response = self.client.get('/api/v1/project/%s/' % (self.project.id,)) - self.assertEquals(response.status_code, 200) - for k in self.project_data.keys(): - self.assertEqual( - json.loads(response.content)[k], getattr(self.project, k)) - - def test_post(self): - """Should save a new project""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - response = self.client.post('/api/v1/project/', - data=json.dumps(self.project_data2), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/project/', - data=json.dumps(self.project_data2), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - response = self.client.get( - '/api/v1/project/{0}/?username={1}&api_key={2}'.format( - self.project.id, self.api_user.username, self.api_user.api_key.key) - ) - for k, v in self.project_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def test_delete(self): - """Should delete an project""" - response = self.client.delete('/api/v1/project/{0}/'.format( - self.project.id,), content_type='application/json') - self.assertEquals(response.status_code, 401) - - request = HttpRequest() - request.user = self.api_user - request.user.user_permissions.add(self.delete) - response = self.client.delete( - '/api/v1/project/{0}/'.format(self.project.id,), - content_type='application/json', - **self.post_auth - ) - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/project/{0}/'.format( - self.project.id,) - ) - self.assertEquals(response.status_code, 404) - - -class ExecutableTest(FixtureTestCase): - - """Test Executable() API""" - - def setUp(self): - self.data = dict(name="Fibo", description="Fibonacci the Lame",) - # project is a ForeignKey and is not added automatically by tastypie - self.project = Project.objects.get(pk=1) - self.executable = Executable(project=self.project, **self.data) - self.executable.save() - self.client = Client() - super(ExecutableTest, self).setUp() - - def test_get_executable(self): - """Should get an existing executable""" - response = self.client.get('/api/v1/executable/{0}/'.format( - self.executable.id,)) - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], "{0}".format( - self.data['name'])) - - def test_get_executable_all_fields(self): - """Should get all fields for an executable""" - response = self.client.get('/api/v1/executable/%s/' % ( - self.executable.id,)) - self.assertEquals(response.status_code, 200) - for k in self.data.keys(): - self.assertEqual( - json.loads(response.content)[k], self.data[k]) - - def test_post(self): - """Should save a new executable""" - modified_data = copy.deepcopy(self.data) - modified_data['name'] = 'nbody' - modified_data['project'] = '/api/v1/project/{0}/'.format(self.project.pk) - response = self.client.post('/api/v1/executable/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 201) - response = self.client.get('/api/v1/executable/{0}/'.format( - self.executable.id)) - response_data = json.loads(response.content) - for k, v in self.data.items(): - self.assertEqual(response_data[k], v) - executable = Executable.objects.get(pk=int(response_data['id'])) - self.assertEquals(executable.project, self.project) - - def test_delete(self): - """Should delete an project""" - response = self.client.delete('/api/v1/executable/{0}/'.format( - self.executable.id,), content_type='application/json') - self.assertEquals(response.status_code, 204) - - response = self.client.get('/api/v1/executable/{0}/'.format( - self.executable.id,)) - - def test_nonexistent(self): - """Requesting an environment that doesn't exist should return a 404""" - response = self.client.get('/api/v1/environment/3333333/') - self.assertEquals(response.status_code, 404) - - -class BranchTest(FixtureTestCase): - - """Test Branch() API""" - - def setUp(self): - super(BranchTest, self).setUp() - self.add = Permission.objects.get_by_natural_key( - 'add_branch', 'codespeed', 'branch') - self.change = Permission.objects.get_by_natural_key( - 'change_branch', 'codespeed', 'branch') - self.delete = Permission.objects.get_by_natural_key( - 'delete_branch', 'codespeed', 'branch') - self.branch1 = Branch.objects.get(pk=1) - self.project_data = dict( - name="PyPy", - repo_type="M", - repo_path="ssh://hg@bitbucket.org/pypy/pypy", - repo_user="fridolin", - repo_pass="secret", - ) - self.project = Project(**self.project_data) - self.project.save() - self.branch2_data = dict( - name="master2", - project='/api/v1/project/{0}/'.format(self.project.id) - ) - self.client = Client() - - def test_get_branch(self): - """Should get an existing branch""" - response = self.client.get('/api/v1/branch/1/') - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], "default") - self.assertEqual(json.loads(response.content)['project'], - "/api/v1/project/1/") - - def test_get_branch_all_fields(self): - """Should get all fields for an branch""" - response = self.client.get('/api/v1/branch/%s/' % (self.branch1.id,)) - self.assertEquals(response.status_code, 200) - self.assertEquals(json.loads(response.content)['name'], - self.branch1.name) - self.assertEquals(json.loads(response.content)['project'], - '/api/v1/project/1/') - self.assertEquals(json.loads(response.content)['resource_uri'], - '/api/v1/branch/%s/' % (self.branch1.id,)) - - def test_post(self): - """Should save a new branch""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.delete) - - modified_data = copy.deepcopy(self.branch2_data) - response = self.client.post('/api/v1/branch/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/branch/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - response = self.client.get('/api/v1/branch/{0}/'.format(id)) - for k, v in self.branch2_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - response = self.client.delete('/api/v1/branch/{0}/'.format(id), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - def test_put(self): - """Should modify an existing environment""" - request = HttpRequest() - request.user = self.api_user - request.user.user_permissions.add(self.change) - - modified_data = copy.deepcopy(self.branch2_data) - modified_data['name'] = "tip" - response = self.client.put('/api/v1/branch/1/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.put('/api/v1/branch/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/branch/1/') - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def test_delete(self): - """Should delete a branch""" - request = HttpRequest() - request.user = self.api_user - request.user.user_permissions.add(self.delete) - - response = self.client.get('/api/v1/branch/1/') - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/branch/1/', - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.delete('/api/v1/branch/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - response = self.client.get('/api/v1/branch/1/') - self.assertEquals(response.status_code, 404) - - -class RevisionTest(FixtureTestCase): - - """Test Revision() API""" - - def setUp(self): - super(RevisionTest, self).setUp() - self.add = Permission.objects.get_by_natural_key( - 'add_revision', 'codespeed', 'revision') - self.change = Permission.objects.get_by_natural_key( - 'change_revision', 'codespeed', 'revision') - self.delete = Permission.objects.get_by_natural_key( - 'delete_revision', 'codespeed', 'revision') - DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S' - self.branch1 = Branch.objects.get(pk=1) - self.project1 = Project.objects.get(pk=1) - self.revision1_data = dict( - commitid="2a6306432e973cdcfd324e81169bb8029d47b736", - tag="tag", - date=datetime.now(), - message="Commit message\n - all bugs fixed\n - code 130% faster", - project=self.project1, - author="Alan T. ", - branch=self.branch1, - ) - self.revision1 = Revision(**self.revision1_data) - self.revision1.save() - self.revision2_data = dict( - commitid="4d3bea3cffe4edcd7d70fc46c5e19474cc4bd012", - tag="v1.0", - date=datetime.now().strftime(DATETIME_FORMAT), - message="Commit message\n - cleanup\n - all FIXMEs removed", - project='/api/v1/project/{0}/'.format(self.project1.id), - author="Chuck N. ", - branch='/api/v1/branch/{0}/'.format(self.branch1.id), - ) - self.client = Client() - - def test_get_revision(self): - """Should get an existing revision""" - response = self.client.get('/api/v1/revision/1/') - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['commitid'], "1") - self.assertEqual(json.loads(response.content)['project'], - "/api/v1/project/1/") - - def test_get_revision_all_fields(self): - """Should get all fields for a revision""" - response = self.client.get('/api/v1/revision/%s/' % (self.revision1.id,)) - self.assertEquals(response.status_code, 200) - self.assertEquals(json.loads(response.content)['commitid'], - self.revision1.commitid) - self.assertEquals(json.loads(response.content)['project'], - '/api/v1/project/%s/' % (self.project1.pk)) - self.assertEquals(json.loads(response.content)['branch'], - '/api/v1/branch/%s/' % (self.branch1.pk)) - self.assertEquals(json.loads(response.content)['tag'], - self.revision1_data['tag']) - self.assertEquals(json.loads(response.content)['message'], - self.revision1_data['message']) - - def test_post(self): - """Should save a new revision""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - modified_data = copy.deepcopy(self.revision2_data) - response = self.client.post('/api/v1/revision/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/revision/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - response = self.client.get('/api/v1/revision/{0}/'.format(id)) - for k, v in self.revision2_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - response = self.client.delete('/api/v1/revision/{0}/'.format(id), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - def test_put(self): - """Should modify an existing revision""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - modified_data = copy.deepcopy(self.revision2_data) - modified_data['tag'] = "v0.9.1" - response = self.client.put('/api/v1/revision/1/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.put('/api/v1/revision/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/revision/1/') - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def test_delete(self): - """Should delete a revision""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.delete) - - response = self.client.get('/api/v1/revision/1/') - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/revision/1/', - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.delete('/api/v1/revision/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - response = self.client.get('/api/v1/revision/1/') - self.assertEquals(response.status_code, 404) - - -class ExecutableTest(FixtureTestCase): - - """Test Executable() API""" - - def setUp(self): - super(ExecutableTest, self).setUp() - - self.add = Permission.objects.get_by_natural_key( - 'add_executable', 'codespeed', 'executable') - self.change = Permission.objects.get_by_natural_key( - 'change_executable', 'codespeed', 'executable') - self.delete = Permission.objects.get_by_natural_key( - 'delete_executable', 'codespeed', 'executable') - - self.executable1 = Executable.objects.get(pk=1) - self.project1 = Project.objects.get(pk=1) - self.executable2_data = dict( - name="sleep", - description="Sleep benchmark", - project='/api/v1/project/{0}/'.format(self.project1.id), - ) - self.client = Client() - - def test_get_executable(self): - """Should get an existing executable""" - response = self.client.get('/api/v1/executable/1/') - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], - 'myexe O3 64bits') - self.assertEqual(json.loads(response.content)['project'], - "/api/v1/project/1/") - - def test_get_executable_all_fields(self): - """Should get all fields for an executable""" - response = self.client.get('/api/v1/executable/{0}/'.format( - self.executable1.id,)) - self.assertEquals(response.status_code, 200) - self.assertEquals(json.loads(response.content)['name'], - self.executable1.name) - self.assertEquals(json.loads(response.content)['project'], - '/api/v1/project/%s/' % (self.project1.pk)) - self.assertEquals(json.loads(response.content)['description'], - self.executable1.description) - - def test_post(self): - """Should save a new executable""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - - modified_data = copy.deepcopy(self.executable2_data) - response = self.client.post('/api/v1/executable/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/executable/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - response = self.client.get('/api/v1/executable/{0}/'.format(id)) - for k, v in self.executable2_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - request.user.user_permissions.add(self.delete) - response = self.client.delete('/api/v1/executable/{0}/'.format(id), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - def test_put(self): - """Should modify an existing environment""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - modified_data = copy.deepcopy(self.executable2_data) - modified_data['name'] = "django" - response = self.client.put('/api/v1/executable/1/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.put('/api/v1/executable/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/executable/1/') - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def test_delete(self): - """Should delete a executable""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.delete) - - response = self.client.get('/api/v1/executable/1/') - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/executable/1/', - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.delete('/api/v1/executable/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - response = self.client.get('/api/v1/executable/1/') - self.assertEquals(response.status_code, 404) - - -class BenchmarkTest(FixtureTestCase): - - """Test Benchmark() API""" - - def setUp(self): - super(BenchmarkTest, self).setUp() - self.add = Permission.objects.get_by_natural_key( - 'add_benchmark', 'codespeed', 'benchmark') - self.change = Permission.objects.get_by_natural_key( - 'change_benchmark', 'codespeed', 'benchmark') - self.delete = Permission.objects.get_by_natural_key( - 'delete_benchmark', 'codespeed', 'benchmark') - self.benchmark1 = Benchmark.objects.get(pk=1) - self.benchmark2_data = dict( - name="sleep", - benchmark_type='C', - description='fast faster fastest', - units_title='Time', - units='seconds', - lessisbetter=True, - default_on_comparison=True, - ) - self.benchmark2 = Benchmark(**self.benchmark2_data) - self.benchmark2.save() - self.client = Client() - - def test_get_benchmark(self): - """Should get an existing benchmark""" - response = self.client.get('/api/v1/benchmark/1/') - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['name'], - 'float') - self.assertEqual(json.loads(response.content)['units'], - "seconds") - - def test_get_benchmark_all_fields(self): - """Should get all fields for an benchmark""" - response = self.client.get('/api/v1/benchmark/{0}/'.format( - self.benchmark2.id,)) - self.assertEquals(response.status_code, 200) - for k, v in self.benchmark2_data.items(): - self.assertEqual(json.loads(response.content)[k], v) - - def test_post(self): - """Should save a new benchmark""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - - modified_data = copy.deepcopy(self.benchmark2_data) - modified_data['name'] = 'wake' - response = self.client.post('/api/v1/benchmark/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.post('/api/v1/benchmark/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth - ) - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - response = self.client.get('/api/v1/benchmark/{0}/'.format(id)) - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - request.user.user_permissions.add(self.delete) - response = self.client.delete('/api/v1/benchmark/{0}/'.format(id), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - def test_put(self): - """Should modify an existing benchmark""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - modified_data = copy.deepcopy(self.benchmark2_data) - modified_data['name'] = "django" - response = self.client.put('/api/v1/benchmark/1/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.put('/api/v1/benchmark/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - response = self.client.get('/api/v1/benchmark/1/') - for k, v in modified_data.items(): - self.assertEqual( - json.loads(response.content)[k], v) - - def test_delete(self): - """Should delete a benchmark""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.delete) - response = self.client.get('/api/v1/benchmark/1/') - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/benchmark/1/', - content_type='application/json') - self.assertEquals(response.status_code, 401) - response = self.client.delete('/api/v1/benchmark/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 204) - - response = self.client.get('/api/v1/benchmark/1/') - self.assertEquals(response.status_code, 404) - - -class ReportTest(FixtureTestCase): - - """Test Report() API""" - - def setUp(self): - super(ReportTest, self).setUp() - self.add = Permission.objects.get_by_natural_key( - 'add_report', 'codespeed', 'report') - self.change = Permission.objects.get_by_natural_key( - 'change_report', 'codespeed', 'report') - self.delete = Permission.objects.get_by_natural_key( - 'delete_report', 'codespeed', 'report') - - self.report1 = Report.objects.get(pk=1) - self.revision1 = Revision.objects.get(pk=1) - self.executable1 = Executable.objects.get(pk=1) - self.environment1 = Environment.objects.get(pk=1) - self.executable2_data = dict( - name="Fibo", - description="Fibonacci the Lame", - ) - self.project = Project.objects.get(pk=1) - self.executable2 = Executable(project=self.project, - **self.executable2_data) - self.executable2.save() - self.report2_data = dict( - revision=self.revision1, - environment=self.environment1, - executable=self.executable2, - ) - self.report2 = Report(**self.report2_data) - self.report2.save() - self.report2_data = dict( - revision='/api/v1/revision/{0}/'.format(self.revision1.id), - environment='/api/v1/environment/{0}/'.format(self.environment1.id), - executable='/api/v1/executable/{0}/'.format(self.executable2.id), - ) - self.client = Client() - - def test_get_report(self): - """Should get an existing report""" - response = self.client.get('/api/v1/report/1/') - self.assertEquals(response.status_code, 200) - self.assertEqual(json.loads(response.content)['summary'], - 'float -50.0%') - self.assertEqual(json.loads(response.content)['colorcode'], - "green") - - def test_get_report_all_fields(self): - """Should get all fields for an report""" - response = self.client.get('/api/v1/report/{0}/'.format( - self.report2.id,)) - self.assertEquals(response.status_code, 200) - for k, v in self.report2_data.items(): - self.assertEqual(json.loads(response.content)[k], v) - - def test_post(self): - """Should not save a new report""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - - modified_data = copy.deepcopy(self.report2_data) - response = self.client.post('/api/v1/report/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 405) - # next has to be 405 (method not allowed), - # otherwise would raise IntegrityError - response = self.client.post('/api/v1/report/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - # next has to be 405, otherwise would raise IntegrityError - self.assertEquals(response.status_code, 405) - - def test_put(self): - """Should not modify an existing report""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - request.user.user_permissions.add(self.change) - request.user.user_permissions.add(self.delete) - - modified_data = copy.deepcopy(self.report2_data) - response = self.client.put('/api/v1/report/1/', - data=json.dumps(modified_data), - content_type='application/json') - self.assertEquals(response.status_code, 405) - response = self.client.put('/api/v1/report/1/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 405) - - def test_delete(self): - """Should delete a report""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.delete) - - response = self.client.get('/api/v1/report/1/') - self.assertEquals(response.status_code, 200) - # from fixture - response = self.client.delete('/api/v1/report/1/', - content_type='application/json') - self.assertEquals(response.status_code, 405) - response = self.client.delete('/api/v1/report/1/', - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 405) - - -class ResultBundleTestCase(FixtureTestCase): - - """Test CRUD of results via API""" - - def setUp(self): - super(ResultBundleTestCase, self).setUp() - self.request = HttpRequest() - self.request.user = self.api_user - self.data1 = { - 'commitid': '/api/v1/revision/2/', - 'branch': '/api/v1/branch/1/', # Always use default for trunk/master/tip - 'project': '/api/v1/project/2/', - 'executable': '/api/v1/executable/1/', - 'benchmark': '/api/v1/benchmark/1/', - 'environment': '/api/v1/environment/2/', - 'result_value': 4000, - } - DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' - self.data_optional = { - 'std_dev': 0.2, - 'val_min': 2.23, - 'val_max': 3.42, - 'date': datetime.now().strftime(DATETIME_FORMAT), - } - project_data = dict( - name="PyPy", - repo_type="M", - repo_path="ssh://hg@bitbucket.org/pypy/pypy", - repo_user="fridolin", - repo_pass="secret", - ) - self.project = Project(**project_data) - self.project.save() - self.env1 = Environment(name='Bulldozer') - self.env1.save() - - def test_populate_and_save(self): - """Should populate ResultBundle() with data""" - bundle = ResultBundle(request=self.request, **self.data1) - bundle._populate_obj_by_data() - # should raise exception if not OK - bundle.hydrate_and_save() - self.assert_(True) - - def test_save_same_result_again(self): - """Save a previously saved result. Expected is an IntegrityError""" - modified_data = copy.deepcopy(self.data1) - modified_data['environment'] = '/api/v1/environment/1/' - modified_data['project'] = '/api/v1/project/1/' - bundle = ResultBundle(request=self.request, **modified_data) - bundle._populate_obj_by_data() - self.assertRaises(IntegrityError, bundle.hydrate_and_save) - - def test_for_nonexistent_environment(self): - """Save data using non existing environment. Expected is an - ImmediateHttpResponse - """ - modified_data = copy.deepcopy(self.data1) - modified_data['environment'] = '/api/v1/environment/3/' - self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) - - def test_insufficient_data(self): - """See if Result() is saved w/ insufficient data""" - modified_data = copy.deepcopy(self.data1) - modified_data.pop('environment') - self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) - - def test_date_attr_set(self): - """Should add date attr to Result() obj if date is not given""" - # date is set automatically - modified_data = copy.deepcopy(self.data1) - bundle = ResultBundle(request=self.request, **modified_data) - bundle.hydrate_and_save() - self.assertIsInstance(bundle.obj.date, datetime) - # date set by value - modified_data['date'] = '2011-05-05 03:01:45' - bundle = ResultBundle(request=self.request, **modified_data) - # wrong date string - modified_data['date'] = '2011-05-05T03:01:45' - self.assertRaises(ImmediateHttpResponse, ResultBundle, **modified_data) - - def test_optional_data(self): - """Should save optional data.""" - data = dict(self.data1.items() + self.data_optional.items()) - bundle = ResultBundle(request=self.request, **data) - bundle.hydrate_and_save() - self.assertIsInstance(bundle.obj.date, datetime) - self.assertEqual(bundle.obj.std_dev, - float(self.data_optional['std_dev'])) - self.assertEqual(bundle.obj.val_max, - float(self.data_optional['val_max'])) - self.assertEqual(bundle.obj.val_min, - float(self.data_optional['val_min'])) - - -class ResultBundleResourceTestCase(FixtureTestCase): - - """Submitting new benchmark results""" - - DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' - - def setUp(self): - super(ResultBundleResourceTestCase, self).setUp() - - self.add = Permission.objects.get_by_natural_key( - 'add_result', 'codespeed', 'result') - self.change = Permission.objects.get_by_natural_key( - 'change_result', 'codespeed', 'result') - self.delete = Permission.objects.get_by_natural_key( - 'delete_result', 'codespeed', 'result') - - self.data1 = { - 'commitid': '/api/v1/revision/2/', - 'branch': '/api/v1/branch/1/', # Always use default for trunk/master/tip - 'project': '/api/v1/project/2/', - 'executable': '/api/v1/executable/1/', - 'benchmark': '/api/v1/benchmark/1/', - 'environment': '/api/v1/environment/2/', - 'result_value': 4000, - } - self.data_optional = { - 'std_dev': 0.2, - 'val_min': 2.23, - 'val_max': 3.42, - 'date': datetime.now().strftime(self.DATETIME_FORMAT), - } - project_data = dict( - name="PyPy", - repo_type="M", - repo_path="ssh://hg@bitbucket.org/pypy/pypy", - repo_user="fridolin", - repo_pass="secret", - ) - self.project = Project(**project_data) - self.project.save() - self.env1 = Environment(name='Bulldozer') - self.env1.save() - self.client = Client() - - def test_post_mandatory(self): - """Should save a new result with only mandatory data""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - - response = self.client.post('/api/v1/benchmark-result/', - data=json.dumps(self.data1), - content_type='application/json') - self.assertEquals(response.status_code, 201) - id = response['Location'].rsplit('/', 2)[-2] - result = Result.objects.get(pk=int(id)) - # just to make the point - self.assertIsInstance(result, Result) - self.assertEqual(result.value, self.data1['result_value']) - - def test_post_all_data(self): - """Should save a new result with mandatory and optional data""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - - data = dict(self.data1, **self.data_optional) - response = self.client.post('/api/v1/benchmark-result/', - data=json.dumps(data), - content_type='application/json') - self.assertEquals(response.status_code, 201) - - def test_post_invalid_data(self): - """Should save a new result with mandatory and optional data""" - request = HttpRequest() - request.user = self.api_user - - request.user.user_permissions.add(self.add) - - modified_data = copy.deepcopy(self.data1) - # environment does not exist - modified_data['environment'] = '/api/v1/environment/5/' - response = self.client.post('/api/v1/benchmark-result/', - data=json.dumps(modified_data), - content_type='application/json', - **self.post_auth) - self.assertEquals(response.status_code, 400) - - def test_get_one(self): - """Should get a result bundle""" - response = self.client.get('/api/v1/benchmark-result/1/', - content_type='application/json') - self.assertEquals(response.status_code, 200) - response_data = json.loads(response.content) - for k in ('project', 'result', 'branch', 'benchmark', 'environment', - 'executable', 'revision'): - self.assertEqual( - response_data[k], - '/api/v1/{0}/1/'.format(k,)) - - -#def suite(): -# suite = unittest.TestSuite() -# suite.addTest(EnvironmentTest()) -# return suite diff --git a/codespeed/urls.py b/codespeed/urls.py index f954ecdc..38ab4e53 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -2,28 +2,11 @@ from django.conf.urls.defaults import * from django.core.urlresolvers import reverse from django.views.generic import TemplateView -from tastypie.api import Api from codespeed.feeds import LatestEntries -from codespeed.api import (UserResource, EnvironmentResource, - ProjectResource, ExecutableResource, ReportResource, - BenchmarkResource, ResultResource, BranchResource, - RevisionResource, ResultBundleResource) feeds = {'latest': LatestEntries} -rest_api = Api(api_name='v1') -rest_api.register(EnvironmentResource()) -rest_api.register(UserResource()) -rest_api.register(ProjectResource()) -rest_api.register(ExecutableResource()) -rest_api.register(BenchmarkResource()) -rest_api.register(ResultResource()) -rest_api.register(BranchResource()) -rest_api.register(RevisionResource()) -rest_api.register(ReportResource()) -rest_api.register(ResultBundleResource()) - urlpatterns = patterns('', (r'^$', TemplateView.as_view(template_name='home.html')), (r'^about/$', TemplateView.as_view(template_name='about.html')), @@ -46,5 +29,4 @@ # URLs for adding results (r'^result/add/json/$', 'add_json_results'), (r'^result/add/$', 'add_result'), - (r'^api/', include(rest_api.urls)), ) diff --git a/sample_project/settings.py b/sample_project/settings.py index 52ad668b..02228c25 100644 --- a/sample_project/settings.py +++ b/sample_project/settings.py @@ -113,12 +113,10 @@ def process_exception(self, request, exception): 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', - #'django.contrib.sites', 'django.contrib.admin', 'django.contrib.staticfiles', 'codespeed', 'south', - 'tastypie', ) SOUTH_TESTS_MIGRATE = False diff --git a/sample_project/urls.py b/sample_project/urls.py index e9a33c93..6a5ddce3 100644 --- a/sample_project/urls.py +++ b/sample_project/urls.py @@ -12,17 +12,7 @@ urlpatterns = patterns('', (r'^admin/', include(admin.site.urls)), -) - -urlpatterns += patterns( - '', - #('^$', RedirectView.as_view(url='/speed/')), -) - -urlpatterns += patterns( - '', (r'^', include('codespeed.urls')), - #(r'^speed/', include('codespeed.urls')), ) if settings.DEBUG: From 05c001328eb22fc7aa1fc606097631453a2205af Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Wed, 13 Nov 2013 19:22:04 +0100 Subject: [PATCH 205/429] Remove tastypie from requirements --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed569aec..821662d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ Django>=1.4 South<=2.0 isodate==0.4.8 -django-tastypie==0.10.0 From f3eef2c84214e4e0d477a6a9a6122d935fefb69c Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Thu, 14 Nov 2013 21:03:44 +0100 Subject: [PATCH 206/429] Added config for Travis CI, and adapted README.md Signed-off-by: Stefan Marr --- .travis.yml | 26 ++++++++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 30 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..0fd60054 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: python +python: + - "2.5" + - "2.6" + - "2.7" + - "3.2" + - "3.3" + # does not have headers provided, please ask https://launchpad.net/~pypy/+archive/ppa + # maintainers to fix their pypy-dev package. + - "pypy" + +env: + - DJANGO_VERSION=1.4.4 + - DJANGO_VERSION=1.5.1 + - DJANGO_VERSION=1.6 + +# command to install dependencies +install: + - pip install -q Django==$DJANGO_VERSION + - python setup.py install + +# command to run tests +script: + - python setup.py test + - cd sample_project + - python manage.py test diff --git a/README.md b/README.md index 7a1e652d..cb2a036c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Known to be used by [PyPy](http://speed.pypy.org), [Twisted](http://speed.twiste For an overview of some application concepts see the [wiki page](https://github.com/tobami/codespeed/wiki/Overview) +# Build Status + +The current build status on Travis CI is: [![Build Status](https://travis-ci.org/tobami/codespeed.png?branch=master)](https://travis-ci.org/tobami/codespeed) + # Requirements You will need Python 2.6+ and Django 1.3+ with South isodate and Tastypie. From e0532ad45ed7c7ef66999bf5d8cbccc12f328f3f Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Thu, 14 Nov 2013 21:11:06 +0100 Subject: [PATCH 207/429] Reduce test matrix to main versions of Python Signed-off-by: Stefan Marr --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0fd60054..e4f50e54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ language: python python: - - "2.5" - - "2.6" +# - "2.5" +# - "2.6" - "2.7" - - "3.2" +# - "3.2" - "3.3" - # does not have headers provided, please ask https://launchpad.net/~pypy/+archive/ppa - # maintainers to fix their pypy-dev package. - "pypy" env: From dd6ce096b41232b508ffb21258731ac126dbb633 Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Thu, 14 Nov 2013 21:16:40 +0100 Subject: [PATCH 208/429] Removed django-tastypie from setup.py This was missing from the tastypie removal commit. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6ec79291..8628e539 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version='0.9.1', url='https://github.com/tobami/codespeed', license='GNU Lesser General Public License version 2.1', - install_requires=['django>=1.4', 'isodate', 'south<=2.0', 'django-tastypie'], + install_requires=['django>=1.4', 'isodate', 'south<=2.0'], packages=find_packages(exclude=['ez_setup', 'sample_project']), description='A web application to monitor and analyze the performance of your code', include_package_data=True, From fc4a95fa6b3054d7b9cea1384cce2276c822ae68 Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Sat, 16 Nov 2013 21:18:34 +0100 Subject: [PATCH 209/429] Restrict testing to Python 2.7 Signed-off-by: Stefan Marr --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e4f50e54..868055c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ python: # - "2.6" - "2.7" # - "3.2" - - "3.3" - - "pypy" +# - "3.3" +# - "pypy" env: - DJANGO_VERSION=1.4.4 From 1302e7d5a71486a0dc546d07682274b7f77195c9 Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Sat, 16 Nov 2013 21:19:13 +0100 Subject: [PATCH 210/429] Show build status directly under the title, without text Signed-off-by: Stefan Marr --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index cb2a036c..c62d0fe4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Codespeed +[![Build Status](https://travis-ci.org/tobami/codespeed.png?branch=master)](https://travis-ci.org/tobami/codespeed) A web application to monitor and analyze the performance of your code. @@ -6,10 +7,6 @@ Known to be used by [PyPy](http://speed.pypy.org), [Twisted](http://speed.twiste For an overview of some application concepts see the [wiki page](https://github.com/tobami/codespeed/wiki/Overview) -# Build Status - -The current build status on Travis CI is: [![Build Status](https://travis-ci.org/tobami/codespeed.png?branch=master)](https://travis-ci.org/tobami/codespeed) - # Requirements You will need Python 2.6+ and Django 1.3+ with South isodate and Tastypie. From a17a59cd49e181e13f8a880a4a15882df7274e77 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 17 Nov 2013 18:07:03 +0100 Subject: [PATCH 211/429] Add test fixtures to build --- .travis.yml | 4 +- MANIFEST.in | 1 + codespeed/fixtures/gettimeline_unittest.json | 1 - codespeed/fixtures/timeline_tests.json | 447 +++++++++++++++++++ codespeed/tests/tests.py | 20 +- 5 files changed, 457 insertions(+), 16 deletions(-) delete mode 100644 codespeed/fixtures/gettimeline_unittest.json create mode 100644 codespeed/fixtures/timeline_tests.json diff --git a/.travis.yml b/.travis.yml index 868055c4..0bb30f4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ env: - DJANGO_VERSION=1.4.4 - DJANGO_VERSION=1.5.1 - DJANGO_VERSION=1.6 - + # command to install dependencies install: - pip install -q Django==$DJANGO_VERSION @@ -21,4 +21,4 @@ install: script: - python setup.py test - cd sample_project - - python manage.py test + - python manage.py test codespeed diff --git a/MANIFEST.in b/MANIFEST.in index 3e90a87d..70e9ce36 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.md include LICENSE +recursive-include codespeed/fixtures timeline_tests.json recursive-include codespeed/templates/codespeed * recursive-include codespeed/static * diff --git a/codespeed/fixtures/gettimeline_unittest.json b/codespeed/fixtures/gettimeline_unittest.json deleted file mode 100644 index 48952e20..00000000 --- a/codespeed/fixtures/gettimeline_unittest.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 1, "model": "codespeed.project", "fields": {"repo_type": "N", "name": "MyProject", "track": true, "repo_user": "", "repo_pass": "", "repo_path": ""}}, {"pk": 2, "model": "codespeed.project", "fields": {"repo_type": "N", "name": "Other", "track": true, "repo_user": "", "repo_pass": "", "repo_path": ""}}, {"pk": 3, "model": "codespeed.project", "fields": {"repo_type": "N", "name": "BaseProj", "track": false, "repo_user": "", "repo_pass": "", "repo_path": ""}}, {"pk": 1, "model": "codespeed.branch", "fields": {"project": 1, "name": "default"}}, {"pk": 2, "model": "codespeed.branch", "fields": {"project": 1, "name": "feature"}}, {"pk": 3, "model": "codespeed.branch", "fields": {"project": 2, "name": "default"}}, {"pk": 4, "model": "codespeed.branch", "fields": {"project": 3, "name": "default"}}, {"pk": 1, "model": "codespeed.revision", "fields": {"project": 1, "author": "", "commitid": "1", "tag": "", "branch": 1, "date": "2011-04-12 16:43:20", "message": ""}}, {"pk": 2, "model": "codespeed.revision", "fields": {"project": 1, "author": "", "commitid": "2", "tag": "", "branch": 1, "date": "2011-04-13 17:04:22", "message": ""}}, {"pk": 3, "model": "codespeed.revision", "fields": {"project": 1, "author": "", "commitid": "3", "tag": "", "branch": 2, "date": "2011-04-13 12:03:38", "message": ""}}, {"pk": 4, "model": "codespeed.revision", "fields": {"project": 1, "author": "", "commitid": "4", "tag": "", "branch": 2, "date": "2011-04-14 18:04:51", "message": ""}}, {"pk": 5, "model": "codespeed.revision", "fields": {"project": 1, "author": "", "commitid": "5", "tag": "", "branch": 1, "date": "2011-04-14 19:13:05", "message": ""}}, {"pk": 6, "model": "codespeed.revision", "fields": {"project": 2, "author": "", "commitid": "1", "tag": "", "branch": 3, "date": "2011-04-13 12:05:11", "message": ""}}, {"pk": 7, "model": "codespeed.revision", "fields": {"project": 2, "author": "", "commitid": "2", "tag": "", "branch": 3, "date": "2011-04-14 12:05:22", "message": ""}}, {"pk": 8, "model": "codespeed.revision", "fields": {"project": 3, "author": "", "commitid": "444", "tag": "1.0", "branch": 4, "date": "2011-05-24 09:37:32", "message": ""}}, {"pk": 1, "model": "codespeed.executable", "fields": {"project": 1, "name": "myexe O3 64bits", "description": ""}}, {"pk": 2, "model": "codespeed.executable", "fields": {"project": 2, "name": "exe", "description": ""}}, {"pk": 3, "model": "codespeed.executable", "fields": {"project": 3, "name": "baseExe", "description": ""}}, {"pk": 1, "model": "codespeed.benchmark", "fields": {"name": "float", "benchmark_type": "C", "units_title": "Time", "units": "seconds", "lessisbetter": true, "description": ""}}, {"pk": 2, "model": "codespeed.benchmark", "fields": {"name": "int", "benchmark_type": "C", "units_title": "Time", "units": "seconds", "lessisbetter": true, "description": ""}}, {"pk": 1, "model": "codespeed.environment", "fields": {"kernel": "", "memory": "", "os": "", "name": "Dual Core", "cpu": ""}}, {"pk": 1, "model": "codespeed.result", "fields": {"executable": 1, "benchmark": 1, "value": 4000.0, "environment": 1, "val_min": 3995.0999999999999, "std_dev": 1.11111, "date": "2011-04-13 19:04:00", "val_max": 4001.5999999999999, "revision": 1}}, {"pk": 2, "model": "codespeed.result", "fields": {"executable": 1, "benchmark": 1, "value": 2000.0, "environment": 1, "val_min": 1995.0999999999999, "std_dev": 1.11111, "date": "2011-04-13 17:06:19", "val_max": 2001.5999999999999, "revision": 2}}, {"pk": 3, "model": "codespeed.result", "fields": {"executable": 1, "benchmark": 1, "value": 2100.0, "environment": 1, "val_min": 995.10000000000002, "std_dev": 1.11111, "date": "2011-04-13 17:06:47", "val_max": 1001.6, "revision": 5}}, {"pk": 4, "model": "codespeed.result", "fields": {"executable": 1, "benchmark": 1, "value": 1800.0, "environment": 1, "val_min": 3995.0999999999999, "std_dev": 1.11111, "date": "2011-04-13 19:06:11", "val_max": 4001.5999999999999, "revision": 3}}, {"pk": 5, "model": "codespeed.result", "fields": {"executable": 2, "benchmark": 1, "value": 1000.0, "environment": 1, "val_min": 995.10000000000002, "std_dev": 1.11111, "date": "2011-04-13 19:11:50", "val_max": 1001.6, "revision": 6}}, {"pk": 6, "model": "codespeed.result", "fields": {"executable": 1, "benchmark": 1, "value": 3000.0, "environment": 1, "val_min": 2995.0999999999999, "std_dev": 1.11111, "date": "2011-04-13 19:06:58", "val_max": 3001.5999999999999, "revision": 4}}, {"pk": 7, "model": "codespeed.result", "fields": {"executable": 2, "benchmark": 1, "value": 500.0, "environment": 1, "val_min": 495.10000000000002, "std_dev": 1.11111, "date": "2011-04-13 19:12:23", "val_max": 501.60000000000002, "revision": 7}}, {"pk": 8, "model": "codespeed.result", "fields": {"executable": 3, "benchmark": 1, "value": 1850.0, "environment": 1, "val_min": null, "std_dev": null, "date": "2011-05-24 09:38:18", "val_max": null, "revision": 8}}, {"pk": 1, "model": "codespeed.report", "fields": {"executable": 1, "colorcode": "green", "summary": "float -50.0%", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 1995.0999999999999, \"std_dev\": 1.11111, \"result\": 2000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 2001.5999999999999, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", "revision": 2}}, {"pk": 2, "model": "codespeed.report", "fields": {"executable": 1, "colorcode": "green", "summary": "float -50.0%", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 995.10000000000002, \"std_dev\": 1.11111, \"result\": 1000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 1001.6, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", "revision": 5}}, {"pk": 3, "model": "codespeed.report", "fields": {"executable": 1, "colorcode": "green", "summary": "float -50.0%", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 1995.0999999999999, \"std_dev\": 1.11111, \"result\": 2000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 2001.5999999999999, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", "revision": 3}}, {"pk": 4, "model": "codespeed.report", "fields": {"executable": 2, "colorcode": "none", "summary": "", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 995.10000000000002, \"std_dev\": 1.11111, \"result\": 1000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 1001.6, \"change\": \"-\"}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": \"-\"}}]", "revision": 6}}, {"pk": 5, "model": "codespeed.report", "fields": {"executable": 1, "colorcode": "none", "summary": "", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 3995.0999999999999, \"std_dev\": 1.11111, \"result\": 4000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 4001.5999999999999, \"change\": \"-\"}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": \"-\"}}]", "revision": 1}}, {"pk": 6, "model": "codespeed.report", "fields": {"executable": 1, "colorcode": "green", "summary": "float -50.0%", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 1995.0999999999999, \"std_dev\": 1.11111, \"result\": 2000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 2001.5999999999999, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", "revision": 4}}, {"pk": 7, "model": "codespeed.report", "fields": {"executable": 2, "colorcode": "green", "summary": "float -50.0%", "environment": 1, "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 495.10000000000002, \"std_dev\": 1.11111, \"result\": 500.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 501.60000000000002, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", "revision": 7}}] \ No newline at end of file diff --git a/codespeed/fixtures/timeline_tests.json b/codespeed/fixtures/timeline_tests.json new file mode 100644 index 00000000..0292f39b --- /dev/null +++ b/codespeed/fixtures/timeline_tests.json @@ -0,0 +1,447 @@ +[ + { + "pk": 1, + "model": "codespeed.project", + "fields": { + "repo_type": "N", + "name": "MyProject", + "commit_browsing_url": "", + "repo_user": "", + "track": true, + "repo_pass": "", + "repo_path": "" + } + }, + { + "pk": 2, + "model": "codespeed.project", + "fields": { + "repo_type": "N", + "name": "Other", + "commit_browsing_url": "", + "repo_user": "", + "track": true, + "repo_pass": "", + "repo_path": "" + } + }, + { + "pk": 3, + "model": "codespeed.project", + "fields": { + "repo_type": "N", + "name": "BaseProj", + "commit_browsing_url": "", + "repo_user": "", + "track": false, + "repo_pass": "", + "repo_path": "" + } + }, + { + "pk": 1, + "model": "codespeed.branch", + "fields": { + "project": 1, + "name": "default" + } + }, + { + "pk": 2, + "model": "codespeed.branch", + "fields": { + "project": 1, + "name": "feature" + } + }, + { + "pk": 3, + "model": "codespeed.branch", + "fields": { + "project": 2, + "name": "default" + } + }, + { + "pk": 4, + "model": "codespeed.branch", + "fields": { + "project": 3, + "name": "default" + } + }, + { + "pk": 1, + "model": "codespeed.revision", + "fields": { + "project": 1, + "author": "", + "commitid": "1", + "tag": "", + "branch": 1, + "date": "2011-04-12T16:43:20", + "message": "" + } + }, + { + "pk": 2, + "model": "codespeed.revision", + "fields": { + "project": 1, + "author": "", + "commitid": "2", + "tag": "", + "branch": 1, + "date": "2011-04-13T17:04:22", + "message": "" + } + }, + { + "pk": 3, + "model": "codespeed.revision", + "fields": { + "project": 1, + "author": "", + "commitid": "3", + "tag": "", + "branch": 2, + "date": "2011-04-13T12:03:38", + "message": "" + } + }, + { + "pk": 4, + "model": "codespeed.revision", + "fields": { + "project": 1, + "author": "", + "commitid": "4", + "tag": "", + "branch": 2, + "date": "2011-04-14T18:04:51", + "message": "" + } + }, + { + "pk": 5, + "model": "codespeed.revision", + "fields": { + "project": 1, + "author": "", + "commitid": "5", + "tag": "", + "branch": 1, + "date": "2011-04-14T19:13:05", + "message": "" + } + }, + { + "pk": 6, + "model": "codespeed.revision", + "fields": { + "project": 2, + "author": "", + "commitid": "1", + "tag": "", + "branch": 3, + "date": "2011-04-13T12:05:11", + "message": "" + } + }, + { + "pk": 7, + "model": "codespeed.revision", + "fields": { + "project": 2, + "author": "", + "commitid": "2", + "tag": "", + "branch": 3, + "date": "2011-04-14T12:05:22", + "message": "" + } + }, + { + "pk": 8, + "model": "codespeed.revision", + "fields": { + "project": 3, + "author": "", + "commitid": "444", + "tag": "1.0", + "branch": 4, + "date": "2011-05-24T09:37:32", + "message": "" + } + }, + { + "pk": 1, + "model": "codespeed.executable", + "fields": { + "project": 1, + "name": "myexe O3 64bits", + "description": "" + } + }, + { + "pk": 2, + "model": "codespeed.executable", + "fields": { + "project": 2, + "name": "exe", + "description": "" + } + }, + { + "pk": 3, + "model": "codespeed.executable", + "fields": { + "project": 3, + "name": "baseExe", + "description": "" + } + }, + { + "pk": 1, + "model": "codespeed.benchmark", + "fields": { + "parent": null, + "name": "float", + "benchmark_type": "C", + "default_on_comparison": true, + "units_title": "Time", + "units": "seconds", + "lessisbetter": true, + "description": "" + } + }, + { + "pk": 2, + "model": "codespeed.benchmark", + "fields": { + "parent": null, + "name": "int", + "benchmark_type": "C", + "default_on_comparison": true, + "units_title": "Time", + "units": "seconds", + "lessisbetter": true, + "description": "" + } + }, + { + "pk": 1, + "model": "codespeed.environment", + "fields": { + "kernel": "", + "memory": "", + "os": "", + "name": "Dual Core", + "cpu": "" + } + }, + { + "pk": 1, + "model": "codespeed.result", + "fields": { + "executable": 1, + "benchmark": 1, + "value": 4000.0, + "environment": 1, + "val_min": 3995.1, + "std_dev": 1.11111, + "date": "2011-04-13T19:04:00", + "val_max": 4001.6, + "revision": 1 + } + }, + { + "pk": 2, + "model": "codespeed.result", + "fields": { + "executable": 1, + "benchmark": 1, + "value": 2000.0, + "environment": 1, + "val_min": 1995.1, + "std_dev": 1.11111, + "date": "2011-04-13T17:06:19", + "val_max": 2001.6, + "revision": 2 + } + }, + { + "pk": 3, + "model": "codespeed.result", + "fields": { + "executable": 1, + "benchmark": 1, + "value": 2100.0, + "environment": 1, + "val_min": 995.1, + "std_dev": 1.11111, + "date": "2011-04-13T17:06:47", + "val_max": 1001.6, + "revision": 5 + } + }, + { + "pk": 4, + "model": "codespeed.result", + "fields": { + "executable": 1, + "benchmark": 1, + "value": 1800.0, + "environment": 1, + "val_min": 3995.1, + "std_dev": 1.11111, + "date": "2011-04-13T19:06:11", + "val_max": 4001.6, + "revision": 3 + } + }, + { + "pk": 5, + "model": "codespeed.result", + "fields": { + "executable": 2, + "benchmark": 1, + "value": 1000.0, + "environment": 1, + "val_min": 995.1, + "std_dev": 1.11111, + "date": "2011-04-13T19:11:50", + "val_max": 1001.6, + "revision": 6 + } + }, + { + "pk": 6, + "model": "codespeed.result", + "fields": { + "executable": 1, + "benchmark": 1, + "value": 3000.0, + "environment": 1, + "val_min": 2995.1, + "std_dev": 1.11111, + "date": "2011-04-13T19:06:58", + "val_max": 3001.6, + "revision": 4 + } + }, + { + "pk": 7, + "model": "codespeed.result", + "fields": { + "executable": 2, + "benchmark": 1, + "value": 500.0, + "environment": 1, + "val_min": 495.1, + "std_dev": 1.11111, + "date": "2011-04-13T19:12:23", + "val_max": 501.6, + "revision": 7 + } + }, + { + "pk": 8, + "model": "codespeed.result", + "fields": { + "executable": 3, + "benchmark": 1, + "value": 1850.0, + "environment": 1, + "val_min": null, + "std_dev": null, + "date": "2011-05-24T09:38:18", + "val_max": null, + "revision": 8 + } + }, + { + "pk": 1, + "model": "codespeed.report", + "fields": { + "executable": 1, + "colorcode": "green", + "summary": "float -50.0%", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 1995.0999999999999, \"std_dev\": 1.11111, \"result\": 2000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 2001.5999999999999, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", + "revision": 2 + } + }, + { + "pk": 2, + "model": "codespeed.report", + "fields": { + "executable": 1, + "colorcode": "green", + "summary": "float -50.0%", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 995.10000000000002, \"std_dev\": 1.11111, \"result\": 1000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 1001.6, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", + "revision": 5 + } + }, + { + "pk": 3, + "model": "codespeed.report", + "fields": { + "executable": 1, + "colorcode": "green", + "summary": "float -50.0%", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 1995.0999999999999, \"std_dev\": 1.11111, \"result\": 2000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 2001.5999999999999, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", + "revision": 3 + } + }, + { + "pk": 4, + "model": "codespeed.report", + "fields": { + "executable": 2, + "colorcode": "none", + "summary": "", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 995.10000000000002, \"std_dev\": 1.11111, \"result\": 1000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 1001.6, \"change\": \"-\"}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": \"-\"}}]", + "revision": 6 + } + }, + { + "pk": 5, + "model": "codespeed.report", + "fields": { + "executable": 1, + "colorcode": "none", + "summary": "", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 3995.0999999999999, \"std_dev\": 1.11111, \"result\": 4000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 4001.5999999999999, \"change\": \"-\"}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": \"-\"}}]", + "revision": 1 + } + }, + { + "pk": 6, + "model": "codespeed.report", + "fields": { + "executable": 1, + "colorcode": "green", + "summary": "float -50.0%", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 1995.0999999999999, \"std_dev\": 1.11111, \"result\": 2000.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 2001.5999999999999, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", + "revision": 4 + } + }, + { + "pk": 7, + "model": "codespeed.report", + "fields": { + "executable": 2, + "colorcode": "green", + "summary": "float -50.0%", + "environment": 1, + "_tablecache": "[{\"units_title\": \"Time\", \"rows\": [{\"bench_name\": \"float\", \"val_min\": 495.10000000000002, \"std_dev\": 1.11111, \"result\": 500.0, \"trend\": \"-\", \"bench_description\": \"\", \"val_max\": 501.60000000000002, \"change\": -50.0}], \"hasmax\": true, \"precission\": 2, \"units\": \"seconds\", \"has_stddev\": true, \"hasmin\": true, \"lessisbetter\": true, \"totals\": {\"trend\": \"-\", \"change\": -50.0}}]", + "revision": 7 + } + } +] \ No newline at end of file diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index 64288fc6..bb584d35 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -5,7 +5,6 @@ import os from django.test import TestCase -from django.test.client import Client from django.core.urlresolvers import reverse from django.conf import settings @@ -15,11 +14,10 @@ from codespeed import settings as default_settings -class AddResult(TestCase): +class TestAddResult(TestCase): def setUp(self): self.path = reverse('codespeed.views.add_result') - self.client = Client() self.e = Environment(name='Dual Core', cpu='Core 2 Duo 8200') self.e.save() temp = datetime.today() @@ -162,10 +160,10 @@ def test_add_result_with_no_project(self): self.assertEquals(response.content, "Result data saved successfully") -class AddJSONResults(TestCase): +class TestAddJSONResults(TestCase): + def setUp(self): self.path = reverse('codespeed.views.add_json_results') - self.client = Client() self.e = Environment(name='bigdog', cpu='Core 2 Duo 8200') self.e.save() temp = datetime.today() @@ -306,11 +304,8 @@ def test_report_is_created(self): self.assertEquals(number_of_reports, 1) -class Timeline(TestCase): - fixtures = ["gettimeline_unittest.json"] - - def setUp(self): - self.client = Client() +class TestTimeline(TestCase): + fixtures = ["timeline_tests.json"] def test_fixture(self): """Test the loaded fixture data @@ -355,7 +350,7 @@ def test_gettimelinedata(self): "Wrong data returned: ") -class CodespeedSettings(TestCase): +class TestCodespeedSettings(TestCase): """Test codespeed.settings """ @@ -418,8 +413,7 @@ def test_get_baseline_executables(self): self.assertEqual(len(result), 3) -class ProjectTest(TestCase): - """Test project model""" +class TestProject(TestCase): def setUp(self): self.github_project = Project(repo_type='H', From 626d9e1e393a0ffd10ebec23d7a7aea68a630981 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 17 Nov 2013 21:29:40 +0100 Subject: [PATCH 212/429] Add setup.py classifiers, keywords and download_url --- setup.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8628e539..498032be 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,28 @@ from setuptools import setup, find_packages setup( - author='Miquel Torres', - author_email='tobami@gmail.com', name='codespeed', version='0.9.1', + author='Miquel Torres', + author_email='tobami@gmail.com', url='https://github.com/tobami/codespeed', + download_url="https://github.com/tobami/codespeed/tags", license='GNU Lesser General Public License version 2.1', + keywords=["benchmarking", "visualization"], install_requires=['django>=1.4', 'isodate', 'south<=2.0'], packages=find_packages(exclude=['ez_setup', 'sample_project']), description='A web application to monitor and analyze the performance of your code', include_package_data=True, zip_safe=False, + classifiers =[ + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + ] ) From ed57ec7acb14103be83c08aaadc7804f2c0d789f Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 17 Nov 2013 21:30:06 +0100 Subject: [PATCH 213/429] Add Python 2.6, Django 1.3 to travis configuration --- .gitignore | 2 ++ .travis.yml | 7 ++++--- sample_project/templates/base.html | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c1c2ad32..b359fbcc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.db sample_project/repos/* override +build +dist diff --git a/.travis.yml b/.travis.yml index 0bb30f4c..cd79a159 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,17 @@ language: python python: # - "2.5" -# - "2.6" + - "2.6" - "2.7" # - "3.2" # - "3.3" # - "pypy" env: - - DJANGO_VERSION=1.4.4 - - DJANGO_VERSION=1.5.1 - DJANGO_VERSION=1.6 + - DJANGO_VERSION=1.5.5 + - DJANGO_VERSION=1.4.9 + - DJANGO_VERSION=1.3.7 # command to install dependencies install: diff --git a/sample_project/templates/base.html b/sample_project/templates/base.html index 002f1e0f..3e4c3a09 100644 --- a/sample_project/templates/base.html +++ b/sample_project/templates/base.html @@ -4,7 +4,7 @@ {% block title %}MyProject's Speed Center{% endblock %} - + {% block extra_head %}{% endblock %} From fb23f8fa9bf78623bd1e15e710f3000b751c694f Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:02:50 +0100 Subject: [PATCH 214/429] Don't use deprecated urls.defaults import --- .travis.yml | 1 - sample_project/urls.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cd79a159..4da65821 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ env: - DJANGO_VERSION=1.6 - DJANGO_VERSION=1.5.5 - DJANGO_VERSION=1.4.9 - - DJANGO_VERSION=1.3.7 # command to install dependencies install: diff --git a/sample_project/urls.py b/sample_project/urls.py index 6a5ddce3..f57659cc 100644 --- a/sample_project/urls.py +++ b/sample_project/urls.py @@ -3,7 +3,7 @@ import os.path from django.conf import settings -from django.conf.urls.defaults import patterns, include, handler404, handler500 +from django.conf.urls import patterns, include, handler404, handler500 from django.views.generic import RedirectView from django.core.urlresolvers import reverse from django.contrib import admin From 64df48ca149655fc376d6cb8d23437f4e3f8074f Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:22:46 +0100 Subject: [PATCH 215/429] New style manage.py --- manage.py | 9 ++++++++ sample_project/manage.py | 11 --------- sample_project/settings.py | 7 +----- tools/create_apikey.py | 46 -------------------------------------- 4 files changed, 10 insertions(+), 63 deletions(-) create mode 100755 manage.py delete mode 100755 sample_project/manage.py delete mode 100755 tools/create_apikey.py diff --git a/manage.py b/manage.py new file mode 100755 index 00000000..1c3713d6 --- /dev/null +++ b/manage.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +import os, sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_project.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/sample_project/manage.py b/sample_project/manage.py deleted file mode 100755 index 5e78ea97..00000000 --- a/sample_project/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/sample_project/settings.py b/sample_project/settings.py index 02228c25..971101c8 100644 --- a/sample_project/settings.py +++ b/sample_project/settings.py @@ -64,11 +64,6 @@ # 'django.template.loaders.eggs.load_template_source', ) -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - # 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', -) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -88,7 +83,7 @@ def process_exception(self, request, exception): (request.build_absolute_uri(), traceback.format_exc())) # And add it to the middleware classes - MIDDLEWARE_CLASSES += ('settings.LogUncatchedErrors',) + MIDDLEWARE_CLASSES += ('sample_project.settings.LogUncatchedErrors',) # set shown level of logging output to debug logging.basicConfig(level=logging.DEBUG) diff --git a/tools/create_apikey.py b/tools/create_apikey.py deleted file mode 100755 index ef48548d..00000000 --- a/tools/create_apikey.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Create an API key for all users that are active""" -import sys -import os - - -## Setup to import models from Django app ## -def import_from_string(name): - """helper to import module from a given string""" - components = name.split('.')[1:] - return reduce(lambda mod, y: getattr(mod, y), components, __import__(name)) - - -sys.path.append(os.path.abspath('..')) - -if 'DJANGO_SETTINGS_MODULE' in os.environ: - settings = import_from_string(os.environ['DJANGO_SETTINGS_MODULE']) -else: - try: - import settings # Assumed to be in the same directory. - except ImportError: - import sys - sys.stderr.write( - "Error: Can't find the file 'settings.py' in the directory " - "containing %r. It appears you've customized things.\nYou'll have " - "to run django-admin.py, passing it your settings module.\n(If the" - " file settings.py does indeed exist, it's causing an ImportError " - "somehow.)\n" % __file__) - sys.exit(1) - - -from django.core.management import setup_environ -setup_environ(settings) - -from django.contrib.auth.models import User -from tastypie.models import ApiKey - - -def main(): - for user in User.objects.all(is_active=True): - ApiKey.objects.create(user=user) - - -if __name__ == '__main__': - main() From f48394d97de7c2dbc2d6f08eb86976a72b6431ed Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:34:02 +0100 Subject: [PATCH 216/429] Remove another urls.defaults import --- codespeed/urls.py | 2 +- sample_project/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codespeed/urls.py b/codespeed/urls.py index 38ab4e53..4a00204c 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from django.conf.urls.defaults import * +from django.conf.urls import patterns, include, url from django.core.urlresolvers import reverse from django.views.generic import TemplateView diff --git a/sample_project/urls.py b/sample_project/urls.py index f57659cc..ce9145e4 100644 --- a/sample_project/urls.py +++ b/sample_project/urls.py @@ -3,7 +3,7 @@ import os.path from django.conf import settings -from django.conf.urls import patterns, include, handler404, handler500 +from django.conf.urls import patterns, include from django.views.generic import RedirectView from django.core.urlresolvers import reverse from django.contrib import admin From 4b77963c901464307716645c377662348c9cc87d Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:37:53 +0100 Subject: [PATCH 217/429] Remove deprecated testcases.DocTestRunner --- .travis.yml | 1 - codespeed/tests/__init__.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4da65821..131e9524 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,4 @@ install: # command to run tests script: - python setup.py test - - cd sample_project - python manage.py test codespeed diff --git a/codespeed/tests/__init__.py b/codespeed/tests/__init__.py index 2eba701d..7fbaa939 100644 --- a/codespeed/tests/__init__.py +++ b/codespeed/tests/__init__.py @@ -4,9 +4,6 @@ import unittest from django.utils.importlib import import_module -from django.test._doctest import DocTestSuite -from django.test.testcases import DocTestRunner -from django.test.simple import doctestOutputChecker loadTestsFromModule = unittest.defaultTestLoader.loadTestsFromModule @@ -68,11 +65,6 @@ def get_suite(*names, **kwargs): suite.addTest(module.suite()) else: # otherwise build the test suite ourselves. suite.addTest(loadTestsFromModule(module)) - try: - suite.addTest(DocTestSuite(module, runner=DocTestRunner, - checker=doctestOutputChecker)) - except ValueError: # No doc tests - pass return suite suite = lambda: get_suite(__name__) From e2e7c7310f2e532ba612514bfc65efb1cc0b35f4 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:42:43 +0100 Subject: [PATCH 218/429] Update README to new manage.py location at the root directory --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c62d0fe4..9d3aa933 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ can take a long time. Please be patient. [github.com/tobami/codespeed/tags](https://github.com/tobami/codespeed/tags), unpack it and install it with `python setup.py install`. * To get started, you can use the `sample_project` directory as a starting point for your Django project, which can be normally configured by editing `sample_project/settings.py`. * For simplicity, you can use the default sqlite configuration, which will save - the data to a database named `sample_project/data.db` -* Create the DB by changing to the `sample_project/` directory and running: + the data to a database named `data.db` +* Create the DB by typing from the root directory: python manage.py syncdb @@ -61,9 +61,9 @@ modify `sample_project/settings.py` and set `DEBUG = False`. If you want to test drive Codespeed, you can use the testdata.json fixtures to have a working data set to browse. -* From the `sample_project/` directory, type: +* From the root directory, type: - ./manage.py loaddata ../codespeed/fixtures/testdata.json + ./manage.py loaddata codespeed/fixtures/testdata.json ## Starting from scratch From a26cac44dd6f3661850cc573eb5025ee353534a1 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:45:56 +0100 Subject: [PATCH 219/429] Actually remove the api.py module --- codespeed/api.py | 478 ----------------------------------------------- 1 file changed, 478 deletions(-) delete mode 100644 codespeed/api.py diff --git a/codespeed/api.py b/codespeed/api.py deleted file mode 100644 index 0fd09b2a..00000000 --- a/codespeed/api.py +++ /dev/null @@ -1,478 +0,0 @@ -# -*- coding: utf-8 -*- - -"""RESTful API implementation - -Example: -GET Environment() data: - curl -H "Accept: application/json" \ - http://127.0.0.1:8000/api/v1/environment/1/ - -POST Environment() data: - curl --dump-header - -H "Content-Type: application/json" -X POST \ - --data '{"name": "Single Core"}' \ - http://127.0.0.1:8000/api/v1/environment/ - -PUT Environment() data: - curl --dump-header - -H "Content-Type: application/json" -X PUT \ - --data '{"name": "Quad Core"}' \ - http://127.0.0.1:8000/api/v1/environment/2/ - -DELETE Environment() data: - curl --dump-header - -H "Content-Type: application/json" -X DELETE \ - http://127.0.0.1:8000/api/v1/environment/2/ - -PUT a full result: - curl --dump-header - -H "Content-Type: application/json" -X POST \ - --data '{"commitid": "4", "branch": "default", \ - "project": "MyProject", "executable": "myexe O3 64bits", \ - "benchmark": "float", "environment": "Quad Core", \ - "result_value": 4000, "result": "4000"}' \ - http://127.0.0.1:8000/api/v1/benchmark-result/ - -See http://django-tastypie.readthedocs.org/en/latest/interacting.html -""" -import logging -from datetime import datetime - -from django.contrib.auth.models import User -from django.db import models -from django.http import Http404 -from django.shortcuts import get_object_or_404 -from tastypie.bundle import Bundle -from tastypie.exceptions import ImmediateHttpResponse -from tastypie.http import HttpBadRequest, HttpCreated, HttpNotImplemented -from tastypie.resources import ModelResource, Resource -from tastypie import fields -from tastypie.authorization import Authorization, DjangoAuthorization -from tastypie.authentication import (Authentication, ApiKeyAuthentication, - MultiAuthentication) -from tastypie.models import create_api_key -from tastypie.utils.dict import dict_strip_unicode_keys - -from codespeed.models import (Environment, Project, Result, Branch, Revision, - Executable, Benchmark, Report) - - -models.signals.post_save.connect(create_api_key, sender=User) - - -class UserResource(ModelResource): - """Resource for Django User()""" - - class Meta: - queryset = User.objects.filter(is_active=True) - resource_name = 'user' - fields = ['username', 'first_name', 'last_name', 'email'] - allowed_methods = ['get'] - #excludes = ['email', 'password', 'is_superuser'] - # Add it here. - authorization = DjangoAuthorization() - authentication = ApiKeyAuthentication() - - -class ProjectResource(ModelResource): - """Resource for Project()""" - - class Meta: - queryset = Project.objects.all() - authorization = DjangoAuthorization() - # Note, the order for MultiAuthentication matters! - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class BranchResource(ModelResource): - """Resource for Branch()""" - - project = fields.ToOneField(ProjectResource, 'project') - - class Meta: - queryset = Branch.objects.all() - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class RevisionResource(ModelResource): - """Resource for Revision()""" - - project = fields.ToOneField(ProjectResource, 'project') - branch = fields.ToOneField(BranchResource, 'branch') - - class Meta: - queryset = Revision.objects.all() - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class ExecutableResource(ModelResource): - """Resource for Executable()""" - - project = fields.ToOneField(ProjectResource, 'project') - - class Meta: - queryset = Executable.objects.all() - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class BenchmarkResource(ModelResource): - """Resource for Benchmark()""" - - class Meta: - queryset = Benchmark.objects.all() - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class EnvironmentResource(ModelResource): - """Resource for Enviroment()""" - - class Meta: - queryset = Environment.objects.all() - resource_name = 'environment' - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class ResultResource(ModelResource): - """Resource for Result()""" - revision = fields.ToOneField(RevisionResource, 'revision') - executable = fields.ToOneField(ExecutableResource, 'executable') - benchmark = fields.ToOneField(BenchmarkResource, 'benchmark') - environment = fields.ToOneField(EnvironmentResource, 'environment') - - class Meta: - queryset = Result.objects.all() - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class ReportResource(ModelResource): - """Resource for Report()""" - - revision = fields.ToOneField(RevisionResource, 'revision') - environment = fields.ToOneField(EnvironmentResource, 'environment') - executable = fields.ToOneField(ExecutableResource, 'executable') - - class Meta: - queryset = Report.objects.all() - allowed_methods = ['get'] - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - - -class ResultBundle(Bundle): - """tastypie.api.Bundle class to deal with submitted results. - - Note, to populate the Bundle.obj with data .save() - has to be called first. - - FIXME (a8): add models.Data if they do not exist in DB - """ - DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' - - # order of mandatory_keys must not be changed - # FIXME (a8): refactor result_value to just value - mandatory_keys = ( - 'revision', - 'project', - 'executable', - 'benchmark', - 'environment', - 'branch', - 'result_value', - ) - - # Note, views.add_result() expects result_date. Here it's the - # same as the Result() attribute - optional_keys = ( - 'std_dev', - 'val_min', - 'val_max', - 'date', - ) - - def __init__(self, obj=None, request=None, **kwargs): - self.data = kwargs - self.request = request - - if isinstance(obj, Result): - self.obj = obj - self._populate_by_obj() - elif obj is None: - self.obj = Result() - else: - raise ValueError("obj has to be an instance of models.Result") - - if self.data: - self._check_data() - self.__data_validated = False # not used for now - super(ResultBundle, self).__init__(data=self.data, obj=self.obj, - request=self.request) - - def _populate_obj_by_data(self): - """Database lookup - - get everything except the result, 2nd try reverse lookup - """ - def populate(key): - return {'project': lambda: ProjectResource().get_via_uri( - self.data['project'], request=self.request), - - 'executable': lambda: ExecutableResource().get_via_uri( - self.data['executable'], request=self.request), - - 'benchmark': lambda: BenchmarkResource().get_via_uri( - self.data['benchmark'], request=self.request), - - 'environment': lambda: EnvironmentResource().get_via_uri( - self.data['environment'], request=self.request), - - 'branch': lambda: BranchResource().get_via_uri( - self.data['branch'], request=self.request), - - 'revision': lambda: RevisionResource().get_via_uri( - self.data['commitid'], - request=self.request)}.get(key, None)() - - try: - self.obj.value = float(self.data['result_value']) - except ValueError, error: - logging.error( - "Result value: {0} cannot be converted to float. {1}".format( - self.data['result_value'], error - )) - raise ImmediateHttpResponse( - response=HttpBadRequest(u"Value needs to be a number")) - for key in [k for k in self.mandatory_keys - if k not in ('result_value',)]: - try: - #populate - item = populate(key) - setattr(self.obj, key, item) - except Exception, error: - logging.error("Data for field %s: %s not found. %s" % ( - key, self.data[key], error)) - raise ImmediateHttpResponse( - response=HttpBadRequest(u"Error finding: {0}={1}".format( - key, self.data[key] - ))) - # populate optional data - for key in [k for k in self.optional_keys - if k not in ('date')]: - if key in self.data.keys(): - setattr(self.obj, key, self.data[key]) - - if 'date' in self.data.keys(): - self.obj.date = self.data['date'] - else: - self.obj.date = datetime.now() - - def _populate_by_obj(self): - """set attributes to make obj match ResultBundleResource - - FIXME: That looks very wrong here. - """ - self.obj.project = self.obj.executable.project - self.obj.branch = self.obj.revision.branch - #self.obj.result = self.obj - setattr(self.obj, 'result', self.obj) - - def _check_data(self): - """See if all mandatory data is there""" - # check if all mandatory keys are there - for key in [k for k in self.mandatory_keys - if k not in ('revision')]: - if not key in self.data.keys(): - error_text = u"You need to provide key: {0}".format(key) - logging.error(error_text) - raise ImmediateHttpResponse( - response=HttpBadRequest(error_text)) - # check for data - elif not self.data[key]: - error_text = 'Value for key {0} is empty.'.format(key) - logging.error(error_text) - raise ImmediateHttpResponse( - response=HttpBadRequest(error_text)) - - # Check that the Environment exists - try: - self.obj.environment = EnvironmentResource().get_via_uri( - self.data['environment'], request=self.request) - except Environment.DoesNotExist: - error_text = 'Environment: {0} not found in database.'.format( - self.data['environment']) - logging.error(error_text) - raise ImmediateHttpResponse( - response=HttpBadRequest( - error_text - )) - except Exception as e: - error_text = ('Error while looking up Environment: ' - '{0}, {1}.'.format(self.data['environment'], e)) - logging.error(error_text) - raise ImmediateHttpResponse(response=HttpBadRequest(error_text)) - # check optional data - for key in [k for k in self.optional_keys - if k not in ('date',)]: - if key in self.data.keys(): - try: - self.data[key] = float(self.data[key]) - except ValueError: - error_text = u"{0} cannot be casted to float.".format( - self.data[key]) - logging.error(error_text) - raise ImmediateHttpResponse( - response=HttpBadRequest(error_text)) - - if 'date' in self.data.keys(): - #FIXME (a8): make that more robust for different json date formats - try: - self.data['date'] = datetime.strptime(self.data['date'], - self.DATETIME_FORMAT) - except ValueError: - error_text = u"Cannot convert date {0} into datetime.".format( - self.data['date']) - logging.error(error_text) - raise ImmediateHttpResponse( - response=HttpBadRequest(error_text)) - - def hydrate_and_save(self): - """Save self.obj which is an instance of Result() - - First populate the Result() instance with self.data - """ - self._populate_obj_by_data() - self.obj.save() - - -class ResultBundleResource(Resource): - """Resource for all the data of a benchmark result. - - Primarily used to submit benchmark results - - mandatory data - 'commitid', - 'branch', - 'project', - 'executable', - 'benchmark', - 'environment', - 'result_value', - - not mandatory data - 'notify' - Send notification to registered user if result varies - from previous results, currently not implemented - """ - - revision = fields.ToOneField(RevisionResource, 'revision') - branch = fields.ToOneField(BranchResource, 'branch') - project = fields.ToOneField(ProjectResource, 'project') - executable = fields.ToOneField(ExecutableResource, 'executable') - benchmark = fields.ToOneField(BenchmarkResource, 'benchmark') - environment = fields.ToOneField(EnvironmentResource, 'environment') - result = fields.ToOneField(ResultResource, 'result') - - class Meta: - resource_name = 'benchmark-result' - object_class = Result - authorization = DjangoAuthorization() - authentication = MultiAuthentication(ApiKeyAuthentication(), - Authentication()) - allowed_methods = ['get', 'post', 'put', 'delete'] - - def get_resource_uri(self, bundle_or_obj): - kwargs = { - 'resource_name': self._meta.resource_name, - } - - if isinstance(bundle_or_obj, Bundle): - kwargs['pk'] = bundle_or_obj.obj.pk - else: - kwargs['pk'] = bundle_or_obj.pk - - if self._meta.api_name is not None: - kwargs['api_name'] = self._meta.api_name - - return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs) - - def get_object_list(self, request): - results = Result.objects.all() - - return [ResultBundle(obj=r, request=request).obj for r in results] - - def obj_get_list(self, request=None, **kwargs): - """Return all benchmark results ever""" - return self.get_object_list(request) - - def obj_get(self, request=None, **kwargs): - """get the ResultBundle with the result_id as the primary key""" - pk = kwargs['pk'] - result = Result.objects.get(pk=pk) - result.project = result.executable.project - result.branch = result.revision.branch - setattr(result, 'result', result) - return result - - def obj_create(self, bundle, request=None, **kwargs): - # not calling hydrate here since bundle.save() has that functionality - # self.full_hydrate(bundle) will try to hydrate result which is not - # there yet - #bundle = self.full_hydrate(bundle) - bundle.hydrate_and_save() - return bundle - - def obj_update(self, bundle, request=None, **kwargs): - return self.obj_create(bundle, request, **kwargs) - - def post_list(self, request, **kwargs): - """ - Creates a new resource/object with the provided data. - - Calls ``obj_create`` with the provided data and returns a response - with the new resource's location. - - If a new resource is created, return ``HttpCreated`` (201 Created). - """ - deserialized = self.deserialize( - request, request.raw_post_data, - format=request.META.get('CONTENT_TYPE', 'application/json') - ) - deserialized = self.alter_deserialized_list_data(request, deserialized) - bundle = ResultBundle(request=request, - **dict_strip_unicode_keys(deserialized)) - self.is_valid(bundle) - updated_bundle = self.obj_create(bundle, request=request) - return HttpCreated(location=self.get_resource_uri(updated_bundle)) - - def post_detail(self, request, **kwargs): - """ - Creates a new subcollection of the resource under a resource. - - This is not implemented by default because most people's data models - aren't self-referential. - - If a new resource is created, return ``HttpCreated`` (201 Created). - """ - return HttpNotImplemented() - - def obj_delete_list(self, request=None, **kwargs): - return HttpNotImplemented() - - def obj_delete(self, request=None, **kwargs): - #obj = Result.objects.get(pk=kwargs['pk']) - #obj.delete() - return HttpNotImplemented() - - def rollback(self, bundles): - pass - - def detail_uri_kwargs(self): - pass From 231033dcb772dd93ef37b4e240c45f5eefdcca2d Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 19 Nov 2013 22:48:57 +0100 Subject: [PATCH 220/429] Update installation procedure --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9d3aa933..2a6c63d1 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ For an overview of some application concepts see the [wiki page](https://github. # Requirements -You will need Python 2.6+ and Django 1.3+ with South isodate and Tastypie. +You will need Python 2.6+ and Django 1.4+ with South and isodate -In Debian and Ubuntu, they can be installed with: +To install all needed dependencies using pip: - sudo apt-get install python-django python-django-south python-tastypie + pip install -r requirements.txt -Instead of using distribution packages, you can use pip: +To install dependencies and the codespeed Django app: - sudo pip install -r requirements.txt + python setup.py install If you want version control integration, there are additional requirements: From 2713832d6b99333d06f1fa629de7653fa4eb50db Mon Sep 17 00:00:00 2001 From: Elliot Saba Date: Wed, 20 Nov 2013 09:24:08 -0800 Subject: [PATCH 221/429] Change date formatting from just str() to isodate(), for better browser compatibility --- codespeed/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/views.py b/codespeed/views.py index 98ad7345..071c0f2a 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -437,7 +437,7 @@ def gettimelinedata(request): std_dev = res.std_dev results.append( [ - str(res.revision.date), res.value, std_dev, + res.revision.date.isoformat(), res.value, std_dev, res.revision.get_short_commitid(), branch ] ) From 6c5e3c8c275f294db080f7ae3bb28dfc4819eb02 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 23 Nov 2013 20:46:38 +0100 Subject: [PATCH 222/429] Don't use deprecated django.utils json module. Use stdlib instead --- codespeed/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/models.py b/codespeed/models.py index e41f5d2e..f879d0bf 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import os +import json from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models -from django.utils import simplejson as json from django.conf import settings from codespeed.github import GITHUB_URL_RE From 787b9a21db1c6b5c5fdf884ad344300e2d537462 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 23 Nov 2013 20:54:17 +0100 Subject: [PATCH 223/429] Use stdlib json --- codespeed/github.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/codespeed/github.py b/codespeed/github.py index 35ac538e..10444f29 100644 --- a/codespeed/github.py +++ b/codespeed/github.py @@ -8,14 +8,11 @@ import logging import urllib import re -import isodate +import json +import isodate from django.core.cache import cache -# Import from here on the off-chance someone is using a really old Python: -from django.utils import simplejson as json - - logger = logging.getLogger(__name__) GITHUB_URL_RE = re.compile( From 977a54c23ed8495e2c61d656286e1e96c65cd22f Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 23 Nov 2013 21:01:31 +0100 Subject: [PATCH 224/429] Fix date format in test_gettimelinedata --- codespeed/tests/tests.py | 2 +- codespeed/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index bb584d35..c670f187 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -346,7 +346,7 @@ def test_gettimelinedata(self): "There are 2 datapoints") self.assertEquals( responsedata['timelines'][0]['branches']['default']['1'][1], - [u'2011-04-13 17:04:22', 2000.0, 1.11111, u'2', u'default'], + [u'2011-04-13T17:04:22', 2000.0, 1.11111, u'2', u'default'], "Wrong data returned: ") diff --git a/codespeed/urls.py b/codespeed/urls.py index 4a00204c..95b64925 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -28,5 +28,5 @@ urlpatterns += patterns('codespeed.views', # URLs for adding results (r'^result/add/json/$', 'add_json_results'), - (r'^result/add/$', 'add_result'), + (r'^result/add/$', 'add_result'), ) From aea70a22910d1d33c985c1c1d6b8c00bab500257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20MATHIEU?= Date: Sun, 8 Dec 2013 17:56:59 +0100 Subject: [PATCH 225/429] Better SVN error message. Error messages reported by Pysvn are usually quite good. They easily allow to understand the issue and then fix it. Codespeed swallows this nice error message to replace it by a generic one hiding what was wrong. For example is anonymous users are not allowed, you will never be able to figure it out with the "Could not resolve" message. It seems better to expose the reported error as is rather than trying to "improve" it. --- codespeed/subversion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/codespeed/subversion.py b/codespeed/subversion.py index 86fdadc8..adbb1574 100644 --- a/codespeed/subversion.py +++ b/codespeed/subversion.py @@ -33,9 +33,8 @@ def get_login(realm, username, may_save): pysvn.opt_revision_kind.number, newrev.commitid ) ) - except pysvn.ClientError: - raise RuntimeError( - "Could not resolve '" + newrev.branch.project.repo_path + "'") + except pysvn.ClientError as e: + raise RuntimeError(e.args) except ValueError: raise RuntimeError( "'%s' is an invalid subversion revision number" % newrev.commitid) From 8ae7a931be731c499c595a5bb1886bd5dc7e99b1 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 26 Dec 2013 21:37:17 +0100 Subject: [PATCH 226/429] Use branch default name from settings --- codespeed/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/models.py b/codespeed/models.py index f879d0bf..f6a7e1b4 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -98,7 +98,7 @@ def __unicode__(self): else: date = self.date.strftime("%h %d, %H:%M") string = " - ".join(filter(None, (date, self.commitid, self.tag))) - if self.branch.name != "default": + if self.branch.name != settings.DEF_BRANCH: string += " - " + self.branch.name return string From 4c1802eaa7385c6bc21d2aa4ed4aa953e11c10b3 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Thu, 26 Dec 2013 21:38:36 +0100 Subject: [PATCH 227/429] Update README: templates inherit from base.html, fixes #165 --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2a6c63d1..c66f159e 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,7 @@ values needed for your customizations: ### Site-wide Changes -All pages inherit from the `site_base.html` template, which -extends `base.html`. To change every page on the site simply edit (`sample_project/templates/site_base.html`) which extends `base.html` and override +All pages inherit from the `base.html` template. To change every page on the site simply edit (`sample_project/templates/base.html`) and override the appropriate block: * Custom title: you may replace the default "My Speed Center" for the title @@ -152,7 +151,7 @@ same name. * About page: create `sample_project/override/templates/about.html`: - {% extends "site_base.html" %} + {% extends "base.html" %} {% block title %}{{ block.super }}: About this project{% endblock %} {% block body %} From fd5c74e71a27fa6fa6e93da8958a14ac27898906 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Fri, 27 Dec 2013 16:30:29 +0100 Subject: [PATCH 228/429] Use %b instead of %h for datetime formatting. Fixes #163 --- codespeed/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/models.py b/codespeed/models.py index f6a7e1b4..edacb205 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -96,7 +96,7 @@ def __unicode__(self): if self.date is None: date = None else: - date = self.date.strftime("%h %d, %H:%M") + date = self.date.strftime("%b %d, %H:%M") string = " - ".join(filter(None, (date, self.commitid, self.tag))) if self.branch.name != settings.DEF_BRANCH: string += " - " + self.branch.name From 66315feb8a514259f322ca35db4c8d391b3e7aac Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Fri, 27 Dec 2013 17:11:22 +0100 Subject: [PATCH 229/429] Release 0.10.0 --- .travis.yml | 1 - CHANGELOG | 13 +++++++++++++ setup.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 131e9524..9be93cf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: -# - "2.5" - "2.6" - "2.7" # - "3.2" diff --git a/CHANGELOG b/CHANGELOG index e8dbb40f..cd91f7e3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,18 @@ == Change Log == +=== Version 0.10.0, December 27, 2013 === +* NEW #108: AlekSi made executable names uniques per project instead of globally +* NEW #148: staticfloat added the ability to set benchmark fields when saving results for a new benchmark +* NEW #149, #161: rchikhi and others fixed Django compatibility with versions 1.4-1.6 +* NEW #158: smarr added initial travis configuration. Continuous integration is now enabled for Codespeed +* NEW #32: squiddy added option to control display of authors email address +* OPTIMIZATION #98: squiddy greatly reduced the number of necessary queries for getting comparison data, getting down the response time by an order of magnitude +* FIX #86: a8 fixed timeline plot failing when more than 4 series were selected +* FIX #130: alkino fixed migrations 0006 and 0012 +* FIX #151: philangist migrated Github integration to API v3 +* FIX #85: a8 fixed timeline view on IE + + === Version 0.9.1, September 11, 2011 === * NEW: Stefan added the possibility to name the default branch other than "default" * NEW: Danilo added executable grouping by project diff --git a/setup.py b/setup.py index 498032be..a19b896c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='codespeed', - version='0.9.1', + version='0.10.0', author='Miquel Torres', author_email='tobami@gmail.com', url='https://github.com/tobami/codespeed', From 5b6b7b793f07128d0f88bb1ed6e5a2f128558ec4 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 4 Jan 2014 11:33:01 +0100 Subject: [PATCH 230/429] Update classifiers, copyright --- LICENSE | 6 +++--- setup.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index b0e0b3c4..a1b3d0b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,8 @@ All files in this work, are now covered by the following copyright notice. Please note that included libraries in the media/ directory may have their own license. - -Copyright (c) 2009-2010 Miquel Torres - + +Copyright (c) 2009-2014 Miquel Torres + This file is part of Codespeed. Codespeed is free software; you can redistribute it and/or diff --git a/setup.py b/setup.py index a19b896c..49bae9fa 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,9 @@ 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', ] ) From 9c444087cfcee0d828a8e485938de6ad89723b29 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 4 Jan 2014 11:33:11 +0100 Subject: [PATCH 231/429] Remove unused site_base.html --- sample_project/templates/site_base.html | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 sample_project/templates/site_base.html diff --git a/sample_project/templates/site_base.html b/sample_project/templates/site_base.html deleted file mode 100644 index 90455eee..00000000 --- a/sample_project/templates/site_base.html +++ /dev/null @@ -1,3 +0,0 @@ -{% extends "base.html" %} - -{# This exists only to be overriden #} \ No newline at end of file From e6e1986063082fc43772c6ff657bd7ac75678792 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 4 Jan 2014 11:56:05 +0100 Subject: [PATCH 232/429] Clarify installation --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c66f159e..9ac1dd2e 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,13 @@ Known to be used by [PyPy](http://speed.pypy.org), [Twisted](http://speed.twiste For an overview of some application concepts see the [wiki page](https://github.com/tobami/codespeed/wiki/Overview) -# Requirements - -You will need Python 2.6+ and Django 1.4+ with South and isodate - -To install all needed dependencies using pip: +# Installation - pip install -r requirements.txt +You will need Python 2.6 or 2.7. To install dependencies and the codespeed Django app: - python setup.py install + pip install codespeed If you want version control integration, there are additional requirements: @@ -30,8 +26,6 @@ If you want version control integration, there are additional requirements: Codespeed will try to clone the repo, which depending on the size of the project can take a long time. Please be patient. -# Installation - * Download the last stable release from [github.com/tobami/codespeed/tags](https://github.com/tobami/codespeed/tags), unpack it and install it with `python setup.py install`. * To get started, you can use the `sample_project` directory as a starting point for your Django project, which can be normally configured by editing `sample_project/settings.py`. @@ -42,7 +36,7 @@ can take a long time. Please be patient. python manage.py syncdb * Create an admin user in the process. -* Migrate to the new DB Schema: +* Execute DB migrations: python manage.py migrate @@ -54,6 +48,7 @@ The codespeed installation can now be accessed by navigating to `http://localhos **Note**: for production, you should configure a real server like Apache or nginx (refer to the [Django docs](http://docs.djangoproject.com/en/dev/howto/deployment/)). You should also modify `sample_project/settings.py` and set `DEBUG = False`. +[`sample_project/README.md`](https://github.com/tobami/codespeed/tree/master/sample_project/README.md) also describes some production settings. # Codespeed configuration From 71333b8812aae7faf523cc3e1b9f27e359f203f1 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 4 Jan 2014 11:57:31 +0100 Subject: [PATCH 233/429] Better explain sample_project installation. Partly addresses #166 --- sample_project/README.md | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/sample_project/README.md b/sample_project/README.md index e0a6e1fb..5a98d484 100644 --- a/sample_project/README.md +++ b/sample_project/README.md @@ -4,8 +4,6 @@ Codespeed uses the Web framework [Django](http://djangoproject.com/). To get a Codespeed instance running you need to set up a Django Project. This directory is just such a project for your reference and a jump start to create your own. -This file is written in Markdown. - ## For the impatient Warning: It is recommended to use [virtualenv](http://pypi.python.org/pypi/virtualenv) to avoid installing @@ -15,35 +13,33 @@ environments. ### Testing with the built-in Development Server That will give you *just* the Django development server version. Please -refer to *Installing for Production* for serious installations. You have been warned! +refer to *Installing for Production* for serious installations. It is assumed you are in the root directory of the Codespeed software. 1. Install the Python pip module `which pip >/dev/null || easy_install pip` (You might be required to use sudo) -2. You *must* copy the example directory to your project. (Prevents updates on +2. You *must* copy the `sample_project` directory to your project. (Prevents updates on git tracked files in the future.) Let's call it speedcenter - `cp -r example speedcenter` -3. Enter that directory - `cd speedcenter` -4. Install Django, Codespeed and other dependencies using pip - `pip install -r requirements.txt` - (You might be required to use sudo) -5. Add codespeed to your Python path + `cp -r sample_project speedcenter` +3a. (When configuring your own project) `pip install codespeeed` +3b. (For Codespeed development) Install Django and other dependencies using pip + `pip install -r requirements.txt`. This will not install codespeed itself, as we want runserver to only "see" the local codespeed copy +4. Add codespeed to your Python path Either `export PYTHONPATH=../:$PYTHONPATH` or - `ln -s ../codespeed .` -6. Initialise the Django Database + `ln -s ./codespeed ./sample_project` +5. Initialise the Django Database `python manage.py syncdb` (Yes, add a superuser.) `python manage.py migrate` Optionally, you may want to load the fixture data for a try `python manage.py loaddata ../codespeed/fixtures/testdata.json` -7. Finally, start the Django development server. +6. Finally, start the Django development server. `python manage.py runserver` -8. Enjoy. +7. Enjoy. `python -m webbrowser -n http://localhost:8000` ## Installing for production @@ -133,12 +129,4 @@ Please, also refer to the [Django URL dispatcher docu] ### Codespeed settings The main config file is `settings.py`. There you configure everything related -to your set up. It FIXME (a8) to be continued... - -FIXME (a8 2011-04-29): Write more ... - -* Point to Django docu for DB, template engine, ... -* Point to codespeed config in settings.py -* Point to wsgi config for Apache ... -* Write up "Installing for production" section -... +to your set up. From f400be47f37d2aeec60dc88a86a544b255d8b9cf Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 5 Jan 2014 10:41:18 +0100 Subject: [PATCH 234/429] Reintroduce base_site.html to override individual template blocks --- README.md | 11 ++++++----- .../templates/codespeed}/base.html | 0 codespeed/templates/codespeed/base_site.html | 3 +++ codespeed/templates/codespeed/changes.html | 2 +- codespeed/templates/codespeed/comparison.html | 2 +- codespeed/templates/codespeed/nodata.html | 2 +- codespeed/templates/codespeed/timeline.html | 2 +- sample_project/README.md | 6 +++--- sample_project/templates/404.html | 2 +- sample_project/templates/500.html | 2 +- sample_project/templates/about.html | 2 +- sample_project/templates/codespeed/base_site.html | 5 +++++ sample_project/templates/home.html | 2 +- 13 files changed, 25 insertions(+), 16 deletions(-) rename {sample_project/templates => codespeed/templates/codespeed}/base.html (100%) create mode 100644 codespeed/templates/codespeed/base_site.html create mode 100644 sample_project/templates/codespeed/base_site.html diff --git a/README.md b/README.md index 9ac1dd2e..1396d520 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ section). ## Custom Settings -You may override any of the default settings by creating the file -`sample_project/override/settings.py`. It is strongly recommended that you only override the +You may override any of the default settings by setting them in +`sample_project/settings.py`. It is strongly recommended that you only override the settings you need by importing the default settings and replacing only the values needed for your customizations: @@ -110,13 +110,14 @@ values needed for your customizations: ### Site-wide Changes -All pages inherit from the `base.html` template. To change every page on the site simply edit (`sample_project/templates/base.html`) and override +All pages inherit from the `base.html` template. To change every page on the site +simply edit (`sample_project/templates/codespeed/base_site.html`) and override the appropriate block: * Custom title: you may replace the default "My Speed Center" for the title block with your prefered value: - {% block title} + {% block title %} My Project's Speed Center {% endblock %} @@ -146,7 +147,7 @@ same name. * About page: create `sample_project/override/templates/about.html`: - {% extends "base.html" %} + {% extends "codespeed/base_site.html" %} {% block title %}{{ block.super }}: About this project{% endblock %} {% block body %} diff --git a/sample_project/templates/base.html b/codespeed/templates/codespeed/base.html similarity index 100% rename from sample_project/templates/base.html rename to codespeed/templates/codespeed/base.html diff --git a/codespeed/templates/codespeed/base_site.html b/codespeed/templates/codespeed/base_site.html new file mode 100644 index 00000000..87740917 --- /dev/null +++ b/codespeed/templates/codespeed/base_site.html @@ -0,0 +1,3 @@ +{% extends "codespeed/base.html" %} + +{# This exists only to be overriden #} diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html index ebc09516..3d8e7f05 100644 --- a/codespeed/templates/codespeed/changes.html +++ b/codespeed/templates/codespeed/changes.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% load url from future %} {% block title %}{{ block.super }}: Changes{% endblock %} diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 30d80a59..79953b8b 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% load url from future %} {% block title %}{{ block.super }}: Comparison{% endblock %} diff --git a/codespeed/templates/codespeed/nodata.html b/codespeed/templates/codespeed/nodata.html index 575e4ddb..969ede42 100644 --- a/codespeed/templates/codespeed/nodata.html +++ b/codespeed/templates/codespeed/nodata.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% block body %}

{{ message|safe }}

diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 393d63ba..0ac46877 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% load url from future %} {% block title %}{{ block.super }}: Timeline{% endblock %} diff --git a/sample_project/README.md b/sample_project/README.md index 5a98d484..eb0c916a 100644 --- a/sample_project/README.md +++ b/sample_project/README.md @@ -110,14 +110,14 @@ Assumed you have a [Debian](http://www.debian.org) like system. ### Using your own Templates Just edit your very own Django templates in `speedcenter/templates`. A good -start is `base.html` the root of all templates. +start is `codespeed/base.html` the root of all templates. If you need to change the codespeed templates: 1. Copy the templates from the codespeed module into your Django project folder. `cp -r codespeed/templates/codespeed speedcenter/templates/` 2. Edit the templates in speedcenter/templates/codespeed/*html Please, also refer to the [Django template docu] -(http://docs.djangoproject.com/en/1.3/ref/templates/) +(http://docs.djangoproject.com/en/1.4/ref/templates/) ### Changing the URL Scheme If you don't want to have your speedcenter in the root url you can change urls.py. @@ -125,7 +125,7 @@ Comment (add a '#' at the beginning) line number 25 `(r'^', include('cod...` and uncomment the next line `(r'^speed/', include('cod...` (Note, Python is picky about indentation). Please, also refer to the [Django URL dispatcher docu] -(http://docs.djangoproject.com/en/1.3/topics/http/urls/). +(http://docs.djangoproject.com/en/1.4/topics/http/urls/). ### Codespeed settings The main config file is `settings.py`. There you configure everything related diff --git a/sample_project/templates/404.html b/sample_project/templates/404.html index 09d204a3..f627be7f 100644 --- a/sample_project/templates/404.html +++ b/sample_project/templates/404.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% block title %}Page not found{% endblock %} diff --git a/sample_project/templates/500.html b/sample_project/templates/500.html index af785c0c..567378e6 100644 --- a/sample_project/templates/500.html +++ b/sample_project/templates/500.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% block title %}Page unavailable{% endblock %} diff --git a/sample_project/templates/about.html b/sample_project/templates/about.html index 931cf62b..7eebe2e7 100644 --- a/sample_project/templates/about.html +++ b/sample_project/templates/about.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "codespeed/base_site.html" %} {% block title %}{{ block.super }}: About this site{% endblock %} {% block body %} diff --git a/codespeed/templates/codespeed/changes_table.html b/codespeed/templates/codespeed/changes_table.html index ec067f31..0b2094b4 100644 --- a/codespeed/templates/codespeed/changes_table.html +++ b/codespeed/templates/codespeed/changes_table.html @@ -1,6 +1,5 @@ {% load percentages %} -
{% for units in tablelist %} @@ -64,45 +63,3 @@
Kernel{{ env.kernel }}
-
- -
- - - - - - - - - - - - - - - {% ifnotequal rev.branch.project.repo_type "N" %} - - {% endifnotequal %} - -
Revision
Commit{% if rev.get_browsing_url %}{{ rev.commitid }}{% else %}{{ rev.commitid }}{% endif %}
Date{{ rev.date }}
Repo{{ rev.branch.project.repo_path }}
- -{% ifnotequal exe.project.repo_type 'N' %} - - - - - - - - - - - - -
Commit logs
Loading...
-{% endifnotequal %} -
diff --git a/codespeed/views.py b/codespeed/views.py index 66b3cc20..70ff2f50 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -611,7 +611,7 @@ def getchangestable(request): '

No results for this ' 'parameters

') - return render_to_response('codespeed/changes_table.html', { + return render_to_response('codespeed/changes_data.html', { 'tablelist': tablelist, 'trendconfig': trendconfig, 'rev': selectedrev, From 0d8f6aec4fbab8b5e867bd2a6d49ae7153e6bdf7 Mon Sep 17 00:00:00 2001 From: Joachim Breitner Date: Sat, 2 Aug 2014 17:02:59 +0200 Subject: [PATCH 247/429] New color "light green" for positive trends to distinguish them from non-trend improvements. --- codespeed/models.py | 14 ++++++++++---- codespeed/static/css/main.css | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/codespeed/models.py b/codespeed/models.py index 58a68f77..13b5f0e3 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -283,8 +283,11 @@ def save(self, *args, **kwargs): self.summary = "Average %s trend %s" % ( average_trend_units.lower(), self.updown(average_trend)) - self.colorcode = average_trend_color == "red"\ - and "yellow" or average_trend_color + # use lighter colors for trend results: + if average_trend_color == "red": + self.colorcode = "yellow" + elif average_trend_color == "green": + self.colorcode = "lightgreen" # Single benchmark trend if max_trend_color != "none" and self.colorcode != "red": if (self.colorcode == "none" or @@ -292,8 +295,11 @@ def save(self, *args, **kwargs): self.summary = "%s trend %s" % ( max_trend_ben, self.updown(max_trend)) - self.colorcode = max_trend_color == "red"\ - and "yellow" or max_trend_color + # use lighter colors for trend results: + if max_trend_color == "red": + self.colorcode = "yellow" + elif max_trend_color == "green": + self.colorcode = "lightgreen" super(Report, self).save(*args, **kwargs) diff --git a/codespeed/static/css/main.css b/codespeed/static/css/main.css index 3b6909a8..e3668f1a 100644 --- a/codespeed/static/css/main.css +++ b/codespeed/static/css/main.css @@ -358,6 +358,8 @@ table.tablesorter tbody tr td.status-green, td.status-green { background-color: tr.status-green td.summary { color: #9FD54D; font-weight: bold; } table.tablesorter tbody tr td.status-yellow, td.status-yellow { background-color: #FFD843; } tr.status-yellow td.summary { color: #FFD843; font-weight: bold; } +table.tablesorter tbody tr td.status-lightgreen, td.status-lightgreen { background-color: #c2e927; } +tr.status-lightgreen td.summary { color: #c2e927; font-weight: bold; } table.tablesorter tbody tr.highlight td, tr.highlight td { background-color: #9DADC6 !important; From 2538ca71fe2e5b6c9032ff7675f55132d69a2c39 Mon Sep 17 00:00:00 2001 From: Joachim Breitner Date: Sat, 2 Aug 2014 17:30:13 +0200 Subject: [PATCH 248/429] Simplify classification logic for the summaries (changes results before trends, averages before individual results) --- codespeed/models.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/codespeed/models.py b/codespeed/models.py index 13b5f0e3..5b58e985 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -265,21 +265,24 @@ def save(self, *args, **kwargs): self.colorcode = "none" # Save summary in order of priority + # (changes results before trends, averages before individual results) + # Average change if average_change_color != "none": self.summary = "Average %s %s" % ( average_change_units.lower(), self.updown(average_change)) self.colorcode = average_change_color + # Single benchmark change - if max_change_color != "none" and self.colorcode != "red": + elif max_change_color != "none": self.summary = "%s %s" % ( max_change_ben, self.updown(max_change)) self.colorcode = max_change_color # Average trend - if average_trend_color != "none" and self.colorcode == "none": + elif average_trend_color != "none": self.summary = "Average %s trend %s" % ( average_trend_units.lower(), self.updown(average_trend)) @@ -288,18 +291,17 @@ def save(self, *args, **kwargs): self.colorcode = "yellow" elif average_trend_color == "green": self.colorcode = "lightgreen" + # Single benchmark trend - if max_trend_color != "none" and self.colorcode != "red": - if (self.colorcode == "none" or - (self.colorcode == "green" and "trend" not in self.summary)): - self.summary = "%s trend %s" % ( - max_trend_ben, - self.updown(max_trend)) - # use lighter colors for trend results: - if max_trend_color == "red": - self.colorcode = "yellow" - elif max_trend_color == "green": - self.colorcode = "lightgreen" + elif max_trend_color != "none": + self.summary = "%s trend %s" % ( + max_trend_ben, + self.updown(max_trend)) + # use lighter colors for trend results: + if max_trend_color == "red": + self.colorcode = "yellow" + elif max_trend_color == "green": + self.colorcode = "lightgreen" super(Report, self).save(*args, **kwargs) From 8eda93100c04fc01dbccd7c6a1425256450170e3 Mon Sep 17 00:00:00 2001 From: Joachim Breitner Date: Sun, 3 Aug 2014 17:09:05 +0200 Subject: [PATCH 249/429] Integrate code review --- codespeed/feeds.py | 4 +--- codespeed/urls.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/codespeed/feeds.py b/codespeed/feeds.py index 474fdfde..345c26f2 100644 --- a/codespeed/feeds.py +++ b/codespeed/feeds.py @@ -2,8 +2,6 @@ from codespeed.models import Report from django.conf import settings from django.db.models import Q -from django.shortcuts import render_to_response - class ResultFeed(Feed): title = settings.WEBSITE_NAME @@ -40,7 +38,7 @@ def result_filter(self): return Q(revision__branch__name=settings.DEF_BRANCH) class LatestSignificantEntries(ResultFeed): - description = "Last significant benchmark runs" + description = "Last benchmark runs with significant changes" def result_filter(self): return Q(revision__branch__name=settings.DEF_BRANCH, diff --git a/codespeed/urls.py b/codespeed/urls.py index 0583331e..a6b5ed10 100644 --- a/codespeed/urls.py +++ b/codespeed/urls.py @@ -3,7 +3,7 @@ from django.core.urlresolvers import reverse from django.views.generic import TemplateView -from codespeed.feeds import * +from codespeed.feeds import LatestEntries, LatestSignificantEntries urlpatterns = patterns('', From 471b2dc54b057e7a8eac4ec97184243bb350439a Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 3 Aug 2014 22:49:35 +0200 Subject: [PATCH 250/429] Add test for reports view --- codespeed/feeds.py | 8 ++++++-- codespeed/tests/test_views.py | 32 ++++++++++++++++++++++++++++++++ codespeed/tests/tests.py | 3 +-- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 codespeed/tests/test_views.py diff --git a/codespeed/feeds.py b/codespeed/feeds.py index 345c26f2..911fe395 100644 --- a/codespeed/feeds.py +++ b/codespeed/feeds.py @@ -3,6 +3,7 @@ from django.conf import settings from django.db.models import Q + class ResultFeed(Feed): title = settings.WEBSITE_NAME link = "/changes/" @@ -13,7 +14,8 @@ def items(self): .order_by('-revision__date')[:10] def item_title(self, item): - return "%s: %s" % (item.revision.get_short_commitid(), item.item_description()) + return "%s: %s" % (item.revision.get_short_commitid(), + item.item_description()) description_template = "codespeed/changes_table.html" @@ -31,15 +33,17 @@ def get_context_data(self, **kwargs): 'env': report.environment, } + class LatestEntries(ResultFeed): description = "Last benchmark runs" def result_filter(self): return Q(revision__branch__name=settings.DEF_BRANCH) + class LatestSignificantEntries(ResultFeed): description = "Last benchmark runs with significant changes" def result_filter(self): return Q(revision__branch__name=settings.DEF_BRANCH, - colorcode__in = ('red','green')) + colorcode__in=('red', 'green')) diff --git a/codespeed/tests/test_views.py b/codespeed/tests/test_views.py new file mode 100644 index 00000000..8b2c861e --- /dev/null +++ b/codespeed/tests/test_views.py @@ -0,0 +1,32 @@ +from django.test import TestCase +from django.core.urlresolvers import reverse + +from codespeed.models import Environment + + +class TestAddResult(TestCase): + + def setUp(self): + self.path = reverse('codespeed.views.reports') + self.e = Environment.objects.create(name='Dual Core', cpu='Core 2 Duo 8200') + self.data = { + 'commitid': 'abcd1', + 'branch': 'default', + 'project': 'MyProject', + 'executable': 'myexe O3 64bits', + 'benchmark': 'float', + 'environment': 'Dual Core', + 'result_value': 200, + } + resp = self.client.post(reverse('codespeed.views.add_result'), self.data) + self.assertEqual(resp.status_code, 202) + self.data['commitid'] = "abcd2" + self.client.post(reverse('codespeed.views.add_result'), self.data) + assert resp.status_code == 202 + + def test_reports(self): + response = self.client.get(self.path) + + self.assertEqual(response.status_code, 200) + self.assertIn('Latest Results', response.content) + self.assertIn(self.data['commitid'], response.content) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py index c670f187..be411cfb 100644 --- a/codespeed/tests/tests.py +++ b/codespeed/tests/tests.py @@ -18,8 +18,7 @@ class TestAddResult(TestCase): def setUp(self): self.path = reverse('codespeed.views.add_result') - self.e = Environment(name='Dual Core', cpu='Core 2 Duo 8200') - self.e.save() + self.e = Environment.objects.create(name='Dual Core', cpu='Core 2 Duo 8200') temp = datetime.today() self.cdate = datetime( temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) From 3b1c5dc753b64be0ab2304568558825ae922f342 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 3 Aug 2014 23:02:45 +0200 Subject: [PATCH 251/429] Use require_GET decorator to disallow non-GET requests --- codespeed/tests/test_views.py | 9 ++++++++- codespeed/views.py | 33 +++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/codespeed/tests/test_views.py b/codespeed/tests/test_views.py index 8b2c861e..9d1113ae 100644 --- a/codespeed/tests/test_views.py +++ b/codespeed/tests/test_views.py @@ -21,12 +21,19 @@ def setUp(self): resp = self.client.post(reverse('codespeed.views.add_result'), self.data) self.assertEqual(resp.status_code, 202) self.data['commitid'] = "abcd2" + self.data['result_value'] = 150 self.client.post(reverse('codespeed.views.add_result'), self.data) - assert resp.status_code == 202 + self.assertEqual(resp.status_code, 202) def test_reports(self): response = self.client.get(self.path) self.assertEqual(response.status_code, 200) self.assertIn('Latest Results', response.content) + self.assertIn('Latest Significant Results', response.content) self.assertIn(self.data['commitid'], response.content) + + def test_reports_post_returns_405(self): + response = self.client.post(self.path, {}) + + self.assertEqual(response.status_code, 405) diff --git a/codespeed/views.py b/codespeed/views.py index 70ff2f50..d3558993 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- from datetime import datetime -from itertools import chain import json import logging +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse from django.http import (HttpResponse, Http404, HttpResponseNotAllowed, HttpResponseBadRequest) from django.shortcuts import get_object_or_404, render_to_response +from django.views.decorators.http import require_GET +from django.views.decorators.csrf import csrf_exempt from django.template import RequestContext -from django.core.exceptions import ValidationError -from django.core.urlresolvers import reverse from django.conf import settings -from django.views.decorators.csrf import csrf_exempt from codespeed.models import (Environment, Report, Project, Revision, Result, Executable, Benchmark, Branch) @@ -193,11 +193,8 @@ def getcomparisonexes(): return all_executables, exekeys +@require_GET def getcomparisondata(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') - data = request.GET - executables, exekeys = getcomparisonexes() benchmarks = Benchmark.objects.all() environments = Environment.objects.all() @@ -226,9 +223,8 @@ def getcomparisondata(request): return HttpResponse(json.dumps(compdata)) +@require_GET def comparison(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET # Configuration of default parameters @@ -361,9 +357,8 @@ def comparison(request): }, context_instance=RequestContext(request)) +@require_GET def gettimelinedata(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET timeline_list = {'error': 'None', 'timelines': []} @@ -475,9 +470,8 @@ def gettimelinedata(request): return HttpResponse(json.dumps(timeline_list)) +@require_GET def timeline(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET ## Configuration of default parameters ## @@ -590,6 +584,7 @@ def timeline(request): }, context_instance=RequestContext(request)) +@require_GET def getchangestable(request): executable = get_object_or_404(Executable, pk=request.GET.get('exe')) environment = get_object_or_404(Environment, pk=request.GET.get('env')) @@ -620,9 +615,8 @@ def getchangestable(request): }, context_instance=RequestContext(request)) +@require_GET def changes(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') data = request.GET # Configuration of default parameters @@ -726,10 +720,8 @@ def changes(request): }, context_instance=RequestContext(request)) +@require_GET def reports(request): - if request.method != 'GET': - return HttpResponseNotAllowed('GET') - context = {} context['reports'] = \ @@ -747,6 +739,7 @@ def reports(request): context, context_instance=RequestContext(request)) +@require_GET def displaylogs(request): rev = get_object_or_404(Revision, pk=request.GET.get('revisionid')) logs = [] @@ -796,6 +789,7 @@ def displaylogs(request): context_instance=RequestContext(request)) +@require_GET def getcommitlogs(rev, startrev, update=False): logs = [] @@ -853,7 +847,6 @@ def validate_result(item): 'result_value', ] - response = {} error = True for key in mandatory_data: if not key in item: From b732cd6b9e3b13ead3e09ab63896c4d725c7b33a Mon Sep 17 00:00:00 2001 From: Joachim Breitner Date: Thu, 16 Oct 2014 12:40:24 +0200 Subject: [PATCH 252/429] Remove @require_GET from getcommitlog As it is not a view. Fixes #175. --- codespeed/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/codespeed/views.py b/codespeed/views.py index d3558993..09319fe0 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -789,7 +789,6 @@ def displaylogs(request): context_instance=RequestContext(request)) -@require_GET def getcommitlogs(rev, startrev, update=False): logs = [] From 1bc316fe20dde1c3411e7062303d8f683d34996f Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 4 Nov 2014 22:04:14 +0100 Subject: [PATCH 253/429] Add filters to admin models. Fixes #179 --- codespeed/admin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/codespeed/admin.py b/codespeed/admin.py index 22878c22..f70b76b5 100644 --- a/codespeed/admin.py +++ b/codespeed/admin.py @@ -14,13 +14,14 @@ class ProjectAdmin(admin.ModelAdmin): class BranchAdmin(admin.ModelAdmin): list_display = ('name', 'project') + list_filter = ('project',) admin.site.register(Branch, BranchAdmin) class RevisionAdmin(admin.ModelAdmin): list_display = ('commitid', 'branch', 'tag', 'date') - list_filter = ('branch', 'tag', 'date') + list_filter = ('branch__project', 'branch', 'tag', 'date') search_fields = ('commitid', 'tag') admin.site.register(Revision, RevisionAdmin) @@ -28,6 +29,7 @@ class RevisionAdmin(admin.ModelAdmin): class ExecutableAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'id', 'project') + list_filter = ('project',) search_fields = ('name', 'description', 'project') admin.site.register(Executable, ExecutableAdmin) @@ -36,6 +38,7 @@ class ExecutableAdmin(admin.ModelAdmin): class BenchmarkAdmin(admin.ModelAdmin): list_display = ('name', 'benchmark_type', 'description', 'units_title', 'units', 'lessisbetter', 'default_on_comparison') + list_filter = ('lessisbetter',) ordering = ['name'] search_fields = ('name', 'description') @@ -52,7 +55,7 @@ class EnvironmentAdmin(admin.ModelAdmin): class ResultAdmin(admin.ModelAdmin): list_display = ('revision', 'benchmark', 'executable', 'environment', 'value', 'date', 'environment') - list_filter = ('date', 'environment', 'executable', 'benchmark') + list_filter = ('environment', 'executable', 'date', 'benchmark') admin.site.register(Result, ResultAdmin) @@ -62,8 +65,10 @@ def recalculate_report(modeladmin, request, queryset): report.save() recalculate_report.short_description = "Recalculate reports" + class ReportAdmin(admin.ModelAdmin): list_display = ('revision', 'summary', 'colorcode') + list_filter = ('environment', 'executable') ordering = ['-revision'] actions = [recalculate_report] From 38eaa7317c620f1e314c2f6abdc41925db166d0a Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Tue, 4 Nov 2014 22:06:57 +0100 Subject: [PATCH 254/429] Add plural name for Branch model --- codespeed/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codespeed/models.py b/codespeed/models.py index 5b58e985..51a5e06b 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -73,6 +73,7 @@ def __unicode__(self): class Meta: unique_together = ("name", "project") + verbose_name_plural = "branches" class Revision(models.Model): From 47b50a717bd5a28f8513ff9a3c492d8629cad4f6 Mon Sep 17 00:00:00 2001 From: Kevin Modzelewski Date: Wed, 5 Nov 2014 20:21:08 -0800 Subject: [PATCH 255/429] codespeed doesn't work with Django 1.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 821662d4..e6c44b84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -Django>=1.4 +Django>=1.4,<1.7 South<=2.0 isodate==0.4.8 From b9f57e42a69e4a07443c1bc7b1237ac2b56ed356 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 22 Mar 2015 11:11:04 +0100 Subject: [PATCH 256/429] Show better error message when no project has track=True --- codespeed/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codespeed/views.py b/codespeed/views.py index 09319fe0..157bf87a 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -233,7 +233,7 @@ def comparison(request): return no_environment_error(request) checkedenviros = get_default_environment(enviros, data, multi=True) - if not len(Project.objects.all()): + if not len(Project.objects.filter(track=True)): return no_default_project_error(request) # Check whether there exist appropiate executables @@ -639,6 +639,9 @@ def changes(request): return no_environment_error(request) defaultenv = get_default_environment(enviros, data) + if not len(Project.objects.filter(track=True)): + return no_default_project_error(request) + defaultexecutable = getdefaultexecutable() if not defaultexecutable: return no_executables_error(request) From ccb422e07e56a788880ad6e95e9d7c6daf9bca95 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 22 Mar 2015 11:12:40 +0100 Subject: [PATCH 257/429] Default to track=True for new projects --- codespeed/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/models.py b/codespeed/models.py index 51a5e06b..1e17afca 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -29,7 +29,7 @@ class Project(models.Model): blank=True, max_length=100) commit_browsing_url = models.CharField("Commit browsing URL", blank=True, max_length=200) - track = models.BooleanField("Track changes", default=False) + track = models.BooleanField("Track changes", default=True) def __unicode__(self): return self.name From fa5c6aab0c5111ba3b801d2be4bed92688a3fc96 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sun, 22 Mar 2015 11:23:53 +0100 Subject: [PATCH 258/429] Fix executable admin search field. Fixes #139 --- codespeed/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codespeed/admin.py b/codespeed/admin.py index f70b76b5..99148fb6 100644 --- a/codespeed/admin.py +++ b/codespeed/admin.py @@ -30,7 +30,8 @@ class RevisionAdmin(admin.ModelAdmin): class ExecutableAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'id', 'project') list_filter = ('project',) - search_fields = ('name', 'description', 'project') + ordering = ['name'] + search_fields = ('name', 'description', 'project__name') admin.site.register(Executable, ExecutableAdmin) @@ -47,6 +48,7 @@ class BenchmarkAdmin(admin.ModelAdmin): class EnvironmentAdmin(admin.ModelAdmin): list_display = ('name', 'cpu', 'memory', 'os', 'kernel') + ordering = ['name'] search_fields = ('name', 'cpu', 'memory', 'os', 'kernel') admin.site.register(Environment, EnvironmentAdmin) From f91ab12c0a85d0aa92bea781fedebacf722b1115 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 30 Apr 2015 14:29:50 +1000 Subject: [PATCH 259/429] Correct install_requires to match requirements.txt --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 49bae9fa..a1e3b1aa 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ download_url="https://github.com/tobami/codespeed/tags", license='GNU Lesser General Public License version 2.1', keywords=["benchmarking", "visualization"], - install_requires=['django>=1.4', 'isodate', 'south<=2.0'], + install_requires=['django>=1.4,<1.7', 'isodate==0.4.8', 'south<=2.0'], packages=find_packages(exclude=['ez_setup', 'sample_project']), description='A web application to monitor and analyze the performance of your code', include_package_data=True, From 7cd9dc2db802ab6c84e39d60658db9c062cdd6da Mon Sep 17 00:00:00 2001 From: Malcolm Parsons Date: Thu, 2 Jul 2015 13:44:47 +0100 Subject: [PATCH 260/429] Get mercurial logs for non-default branches --- codespeed/mercurial.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codespeed/mercurial.py b/codespeed/mercurial.py index 5a670749..d1cf0b49 100644 --- a/codespeed/mercurial.py +++ b/codespeed/mercurial.py @@ -45,8 +45,7 @@ def getlogs(endrev, startrev): updaterepo(endrev.branch.project, update=False) cmd = ["hg", "log", - "-r", "%s:%s" % (endrev.commitid, startrev.commitid), - "-b", "default", + "-r", "%s::%s" % (startrev.commitid, endrev.commitid), "--template", "{rev}:{node|short}\n{node}\n{author|user}\n{author|email}\n{date}\n{desc}\n=newlog=\n"] working_copy = endrev.branch.project.working_copy From 4d6b325a8b5bac870c4f27954a4ea66ee1cce9c7 Mon Sep 17 00:00:00 2001 From: Malcolm Parsons Date: Fri, 16 Oct 2015 16:22:44 +0100 Subject: [PATCH 261/429] Remove duplicate environment column from Result admin --- codespeed/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/admin.py b/codespeed/admin.py index 99148fb6..b74ced44 100644 --- a/codespeed/admin.py +++ b/codespeed/admin.py @@ -56,7 +56,7 @@ class EnvironmentAdmin(admin.ModelAdmin): class ResultAdmin(admin.ModelAdmin): list_display = ('revision', 'benchmark', 'executable', 'environment', - 'value', 'date', 'environment') + 'value', 'date') list_filter = ('environment', 'executable', 'date', 'benchmark') admin.site.register(Result, ResultAdmin) From 3d5ec866fa4c9794e98413e6b47fc426c1171218 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 19:12:37 +0100 Subject: [PATCH 262/429] Update test matrix. Remove Django 1.5, commented out Python 3 entries --- .travis.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9be93cf2..85268825 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,16 +2,11 @@ language: python python: - "2.6" - "2.7" -# - "3.2" -# - "3.3" -# - "pypy" env: - - DJANGO_VERSION=1.6 - - DJANGO_VERSION=1.5.5 - - DJANGO_VERSION=1.4.9 + - DJANGO_VERSION=1.6.11 + - DJANGO_VERSION=1.4.22 -# command to install dependencies install: - pip install -q Django==$DJANGO_VERSION - python setup.py install From cbaac723b8d9e93b0ccce0173d41865585217a9f Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 19:36:25 +0100 Subject: [PATCH 263/429] Split model and settings tests out to their own modules --- codespeed/tests/test_models.py | 58 ++++ codespeed/tests/test_settings.py | 34 +++ codespeed/tests/test_views.py | 387 +++++++++++++++++++++++++- codespeed/tests/tests.py | 460 ------------------------------- 4 files changed, 474 insertions(+), 465 deletions(-) create mode 100644 codespeed/tests/test_models.py create mode 100644 codespeed/tests/test_settings.py delete mode 100644 codespeed/tests/tests.py diff --git a/codespeed/tests/test_models.py b/codespeed/tests/test_models.py new file mode 100644 index 00000000..74a802c8 --- /dev/null +++ b/codespeed/tests/test_models.py @@ -0,0 +1,58 @@ +import os + +from django.conf import settings +from django.test import TestCase + +from codespeed.models import Project + + +class TestProject(TestCase): + + def setUp(self): + self.github_project = Project( + repo_type='H', repo_path='https://github.com/tobami/codespeed.git') + self.git_project = Project(repo_type='G', + repo_path='/home/foo/codespeed') + + def test_repo_name(self): + """Test that only projects with local repositories have a repo_name attribute + """ + self.assertEqual(self.git_project.repo_name, 'codespeed') + + self.assertRaises(AttributeError, getattr, + self.github_project, 'repo_name') + + def test_working_copy(self): + """Test that only projects with local repositories have a working_copy + attribute + + """ + self.assertEqual(self.git_project.working_copy, + os.path.join(settings.REPOSITORY_BASE_PATH, + self.git_project.repo_name)) + + self.assertRaises( + AttributeError, getattr, self.github_project, 'working_copy') + + def test_github_browsing_url(self): + """If empty, the commit browsing url will be filled in with a default + value when using github repository. + """ + + # It should work with https:// as well as git:// urls + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://github.com/tobami/codespeed.git/' + 'commit/{commitid}') + + self.github_project.repo_path = 'git://github.com/tobami/codespeed.git' + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://github.com/tobami/codespeed.git/' + 'commit/{commitid}') + + # If filled in, commit browsing url should not change + self.github_project.commit_browsing_url = 'https://example.com/{commitid}' + self.github_project.save() + self.assertEquals(self.github_project.commit_browsing_url, + 'https://example.com/{commitid}') diff --git a/codespeed/tests/test_settings.py b/codespeed/tests/test_settings.py new file mode 100644 index 00000000..f525bd22 --- /dev/null +++ b/codespeed/tests/test_settings.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.test import TestCase + +from codespeed import settings as default_settings + + +class TestCodespeedSettings(TestCase): + """Test codespeed.settings + """ + + def setUp(self): + self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()] + + def test_website_name(self): + """See if WEBSITENAME is set + """ + self.assertTrue(default_settings.WEBSITE_NAME) + self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite', + "Change codespeed settings in project.settings") + + def test_keys_in_settings(self): + """Check that all settings attributes from codespeed.settings exist + in django.conf.settings + """ + for k in self.cs_setting_keys: + self.assertTrue(hasattr(settings, k), + "Key {0} is missing in settings.py.".format(k)) + + def test_settings_attributes(self): + """Check if all settings from codespeed.settings equals + django.conf.settings + """ + for k in self.cs_setting_keys: + self.assertEqual(getattr(settings, k), getattr(default_settings, k)) diff --git a/codespeed/tests/test_views.py b/codespeed/tests/test_views.py index 9d1113ae..7b0f78a0 100644 --- a/codespeed/tests/test_views.py +++ b/codespeed/tests/test_views.py @@ -1,14 +1,358 @@ +# -*- coding: utf-8 -*- +from datetime import datetime, timedelta +import copy +import json + from django.test import TestCase from django.core.urlresolvers import reverse -from codespeed.models import Environment +from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, + Environment, Result, Report) +from codespeed.views import getbaselineexecutables class TestAddResult(TestCase): def setUp(self): - self.path = reverse('codespeed.views.reports') - self.e = Environment.objects.create(name='Dual Core', cpu='Core 2 Duo 8200') + self.path = reverse('codespeed.views.add_result') + self.e = Environment.objects.create(name='Dual Core', + cpu='Core 2 Duo 8200') + temp = datetime.today() + self.cdate = datetime( + temp.year, temp.month, temp.day, + temp.hour, temp.minute, temp.second) + self.data = { + 'commitid': '23', + 'branch': 'default', + 'project': 'MyProject', + 'executable': 'myexe O3 64bits', + 'benchmark': 'float', + 'environment': 'Dual Core', + 'result_value': 456, + } + + def test_add_correct_result(self): + """Add correct result data""" + response = self.client.post(self.path, self.data) + + # Check that we get a success response + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "Result data saved successfully") + + # Check that the data was correctly saved + e = Environment.objects.get(name='Dual Core') + b = Benchmark.objects.get(name='float') + self.assertEquals(b.benchmark_type, "C") + self.assertEquals(b.units, "seconds") + self.assertEquals(b.lessisbetter, True) + p = Project.objects.get(name='MyProject') + branch = Branch.objects.get(name='default', project=p) + r = Revision.objects.get(commitid='23', branch=branch) + i = Executable.objects.get(name='myexe O3 64bits') + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 456) + + def test_add_non_default_result(self): + """Add result data with non-mandatory options""" + modified_data = copy.deepcopy(self.data) + revision_date = self.cdate - timedelta(minutes=2) + modified_data['revision_date'] = revision_date + result_date = self.cdate + timedelta(minutes=2) + modified_data['result_date'] = result_date + modified_data['std_dev'] = 1.11111 + modified_data['max'] = 2 + modified_data['min'] = 1.0 + response = self.client.post(self.path, modified_data) + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "Result data saved successfully") + e = Environment.objects.get(name='Dual Core') + p = Project.objects.get(name='MyProject') + branch = Branch.objects.get(name='default', project=p) + r = Revision.objects.get(commitid='23', branch=branch) + + # Tweak the resolution down to avoid failing over very slight differences: + self.assertEquals(r.date, revision_date) + + i = Executable.objects.get(name='myexe O3 64bits') + b = Benchmark.objects.get(name='float') + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertEquals(res.date, result_date) + self.assertEquals(res.std_dev, 1.11111) + self.assertEquals(res.val_max, 2) + self.assertEquals(res.val_min, 1) + + def test_bad_environment(self): + """Should return 400 when environment does not exist""" + bad_name = '10 Core' + self.data['environment'] = bad_name + response = self.client.post(self.path, self.data) + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, "Environment " + bad_name + " not found") + self.data['environment'] = 'Dual Core' + + def test_empty_argument(self): + """Should respond 400 when a POST request has an empty argument""" + for key in self.data: + backup = self.data[key] + self.data[key] = "" + response = self.client.post(self.path, self.data) + self.assertEquals(response.status_code, 400) + self.assertEquals( + response.content, 'Value for key "' + key + '" empty in request') + self.data[key] = backup + + def test_missing_argument(self): + """Should respond 400 when a POST request is missing an argument""" + for key in self.data: + backup = self.data[key] + del(self.data[key]) + response = self.client.post(self.path, self.data) + self.assertEquals(response.status_code, 400) + self.assertEquals( + response.content, 'Key "' + key + '" missing from request') + self.data[key] = backup + + def test_report_is_not_created(self): + """Should not create a report when adding a single result""" + self.client.post(self.path, self.data) + number_of_reports = len(Report.objects.all()) + # After adding one result for one revision, there should be no reports + self.assertEquals(number_of_reports, 0) + + def test_report_is_created(self): + """Should create a report when adding a result for two revisions""" + # First result does not create report + self.client.post(self.path, self.data) + + modified_data = copy.deepcopy(self.data) + modified_data['commitid'] = "23233" + # Second result should trigger report creation + self.client.post(self.path, modified_data) + number_of_reports = len(Report.objects.all()) + self.assertEquals(number_of_reports, 1) + + def test_submit_data_with_none_timestamp(self): + """Should add a default revision date when timestamp is None""" + modified_data = copy.deepcopy(self.data) + # The value None will get urlencoded and converted to a "None" string + modified_data['revision_date'] = None + response = self.client.post(self.path, modified_data) + self.assertEquals(response.status_code, 202) + + def test_add_result_with_no_project(self): + """Should add a revision with the project""" + modified_data = copy.deepcopy(self.data) + modified_data['project'] = "My new project" + modified_data['executable'] = "My new executable" + response = self.client.post(self.path, modified_data) + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, "Result data saved successfully") + + +class TestAddJSONResults(TestCase): + + def setUp(self): + self.path = reverse('codespeed.views.add_json_results') + self.e = Environment(name='bigdog', cpu='Core 2 Duo 8200') + self.e.save() + temp = datetime.today() + self.cdate = datetime( + temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) + + self.data = [ + {'commitid': '123', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 456}, + {'commitid': '456', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 457}, + {'commitid': '456', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards2', + 'environment': 'bigdog', + 'result_value': 34}, + {'commitid': '789', + 'project': 'pypy', + 'branch': 'default', + 'executable': 'pypy-c', + 'benchmark': 'Richards', + 'environment': 'bigdog', + 'result_value': 458}, + ] + + def test_add_correct_results(self): + """Should add all results when the request data is valid""" + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) + + # Check that we get a success response + self.assertEquals(response.status_code, 202) + self.assertEquals(response.content, + "All result data saved successfully") + + # Check that the data was correctly saved + e = Environment.objects.get(name='bigdog') + b = Benchmark.objects.get(name='Richards') + self.assertEquals(b.benchmark_type, "C") + self.assertEquals(b.units, "seconds") + self.assertEquals(b.lessisbetter, True) + p = Project.objects.get(name='pypy') + branch = Branch.objects.get(name='default', project=p) + r = Revision.objects.get(commitid='123', branch=branch) + i = Executable.objects.get(name='pypy-c') + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 456) + resdate = res.date.strftime("%Y%m%dT%H%M%S") + selfdate = self.cdate.strftime("%Y%m%dT%H%M%S") + self.assertTrue(resdate, selfdate) + + r = Revision.objects.get(commitid='456', branch=branch) + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 457) + + r = Revision.objects.get(commitid='789', branch=branch) + res = Result.objects.get( + revision=r, + executable=i, + benchmark=b, + environment=e + ) + self.assertTrue(res.value, 458) + + def test_bad_environment(self): + """Add result associated with non-existing environment. + Only change one item in the list. + """ + data = self.data[0] + bad_name = 'bigdog1' + data['environment'] = bad_name + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) + + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, "Environment " + bad_name + " not found") + data['environment'] = 'bigdog' + + def test_empty_argument(self): + '''Should return 400 when making a request with an empty argument''' + data = self.data[1] + for key in data: + backup = data[key] + data[key] = "" + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, 'Value for key "' + key + '" empty in request') + data[key] = backup + + def test_missing_argument(self): + '''Should return 400 when making a request with a missing argument''' + data = self.data[2] + for key in data: + backup = data[key] + del(data[key]) + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) + self.assertEquals(response.status_code, 400) + self.assertEquals(response.content, 'Key "' + key + '" missing from request') + data[key] = backup + + def test_report_is_created(self): + '''Should create a report when adding json results for two revisions + plus a third revision with one result less than the last one''' + response = self.client.post(self.path, + {'json': json.dumps(self.data)}) + + # Check that we get a success response + self.assertEquals(response.status_code, 202) + + number_of_reports = len(Report.objects.all()) + # After adding 4 result for 3 revisions, only 2 reports should be created + # The third revision will need an extra result for Richards2 in order + # to trigger report creation + self.assertEquals(number_of_reports, 1) + + +class TestTimeline(TestCase): + fixtures = ["timeline_tests.json"] + + def test_fixture(self): + """Test the loaded fixture data + """ + env = Environment.objects.filter(name="Dual Core") + self.assertEquals(len(env), 1) + benchmarks = Benchmark.objects.filter(name="float") + self.assertEquals(len(benchmarks), 1) + self.assertEquals(benchmarks[0].units, "seconds") + results = benchmarks[0].results.all() + self.assertEquals(len(results), 8) + + def test_gettimelinedata(self): + """Test that gettimelinedata returns correct timeline data + """ + path = reverse('codespeed.views.gettimelinedata') + data = { + "exe": "1,2", + "base": "2+4", + "ben": "float", + "env": "1", + "revs": 2 + } + response = self.client.get(path, data) + self.assertEquals(response.status_code, 200) + responsedata = json.loads(response.content) + self.assertEquals( + responsedata['error'], "None", "there should be no errors") + self.assertEquals( + len(responsedata['timelines']), 1, "there should be 1 benchmark") + self.assertEquals( + len(responsedata['timelines'][0]['branches']['default']), + 2, + "there should be 2 timelines") + self.assertEquals( + len(responsedata['timelines'][0]['branches']['default']['1']), + 2, + "There are 2 datapoints") + self.assertEquals( + responsedata['timelines'][0]['branches']['default']['1'][1], + [u'2011-04-13T17:04:22', 2000.0, 1.11111, u'2', u'default'], + "Wrong data returned: ") + + +class TestReports(TestCase): + + def setUp(self): + Environment.objects.create(name='Dual Core', cpu='Core 2 Duo 8200') self.data = { 'commitid': 'abcd1', 'branch': 'default', @@ -26,7 +370,7 @@ def setUp(self): self.assertEqual(resp.status_code, 202) def test_reports(self): - response = self.client.get(self.path) + response = self.client.get(reverse('codespeed.views.reports')) self.assertEqual(response.status_code, 200) self.assertIn('Latest Results', response.content) @@ -34,6 +378,39 @@ def test_reports(self): self.assertIn(self.data['commitid'], response.content) def test_reports_post_returns_405(self): - response = self.client.post(self.path, {}) + response = self.client.post(reverse('codespeed.views.reports'), {}) self.assertEqual(response.status_code, 405) + + +class TestViewHelpers(TestCase): + """Test helper functions in codespeed.views""" + + def setUp(self): + self.project = Project.objects.create(name='Test') + self.executable = Executable.objects.create( + name='TestExecutable', project=self.project) + self.branch = Branch.objects.create(name='master', project=self.project) + + def test_get_baseline_executables(self): + # No revisions, no baseline + result = getbaselineexecutables() + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['executable'], 'none') + + # Check that a tagged revision will be included as baseline + revision1 = Revision.objects.create(commitid='1', tag='0.1', branch=self.branch) + result = getbaselineexecutables() + self.assertEqual(len(result), 2) + self.assertEqual(result[0]['executable'], 'none') + self.assertEqual(result[1]['executable'], self.executable) + self.assertEqual(result[1]['revision'], revision1) + + revision2 = Revision.objects.create(commitid='2', tag='0.2', branch=self.branch) + result = getbaselineexecutables() + self.assertEqual(len(result), 3) + + # An untagged revision will not be available as baseline + Revision.objects.create(commitid='3', branch=self.branch) + result = getbaselineexecutables() + self.assertEqual(len(result), 3) diff --git a/codespeed/tests/tests.py b/codespeed/tests/tests.py deleted file mode 100644 index be411cfb..00000000 --- a/codespeed/tests/tests.py +++ /dev/null @@ -1,460 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import datetime, timedelta -import copy -import json -import os - -from django.test import TestCase -from django.core.urlresolvers import reverse -from django.conf import settings - -from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, - Environment, Result, Report) -from codespeed.views import getbaselineexecutables -from codespeed import settings as default_settings - - -class TestAddResult(TestCase): - - def setUp(self): - self.path = reverse('codespeed.views.add_result') - self.e = Environment.objects.create(name='Dual Core', cpu='Core 2 Duo 8200') - temp = datetime.today() - self.cdate = datetime( - temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) - self.data = { - 'commitid': '23', - 'branch': 'default', - 'project': 'MyProject', - 'executable': 'myexe O3 64bits', - 'benchmark': 'float', - 'environment': 'Dual Core', - 'result_value': 456, - } - - def test_add_correct_result(self): - """Add correct result data""" - response = self.client.post(self.path, self.data) - - # Check that we get a success response - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved successfully") - - # Check that the data was correctly saved - e = Environment.objects.get(name='Dual Core') - b = Benchmark.objects.get(name='float') - self.assertEquals(b.benchmark_type, "C") - self.assertEquals(b.units, "seconds") - self.assertEquals(b.lessisbetter, True) - p = Project.objects.get(name='MyProject') - branch = Branch.objects.get(name='default', project=p) - r = Revision.objects.get(commitid='23', branch=branch) - i = Executable.objects.get(name='myexe O3 64bits') - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 456) - - def test_add_non_default_result(self): - """Add result data with non-mandatory options""" - modified_data = copy.deepcopy(self.data) - revision_date = self.cdate - timedelta(minutes=2) - modified_data['revision_date'] = revision_date - result_date = self.cdate + timedelta(minutes=2) - modified_data['result_date'] = result_date - modified_data['std_dev'] = 1.11111 - modified_data['max'] = 2 - modified_data['min'] = 1.0 - response = self.client.post(self.path, modified_data) - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved successfully") - e = Environment.objects.get(name='Dual Core') - p = Project.objects.get(name='MyProject') - branch = Branch.objects.get(name='default', project=p) - r = Revision.objects.get(commitid='23', branch=branch) - - # Tweak the resolution down to avoid failing over very slight differences: - self.assertEquals(r.date, revision_date) - - i = Executable.objects.get(name='myexe O3 64bits') - b = Benchmark.objects.get(name='float') - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertEquals(res.date, result_date) - self.assertEquals(res.std_dev, 1.11111) - self.assertEquals(res.val_max, 2) - self.assertEquals(res.val_min, 1) - - def test_bad_environment(self): - """Should return 400 when environment does not exist""" - bad_name = '10 Core' - self.data['environment'] = bad_name - response = self.client.post(self.path, self.data) - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, "Environment " + bad_name + " not found") - self.data['environment'] = 'Dual Core' - - def test_empty_argument(self): - """Should respond 400 when a POST request has an empty argument""" - for key in self.data: - backup = self.data[key] - self.data[key] = "" - response = self.client.post(self.path, self.data) - self.assertEquals(response.status_code, 400) - self.assertEquals( - response.content, 'Value for key "' + key + '" empty in request') - self.data[key] = backup - - def test_missing_argument(self): - """Should respond 400 when a POST request is missing an argument""" - for key in self.data: - backup = self.data[key] - del(self.data[key]) - response = self.client.post(self.path, self.data) - self.assertEquals(response.status_code, 400) - self.assertEquals( - response.content, 'Key "' + key + '" missing from request') - self.data[key] = backup - - def test_report_is_not_created(self): - '''Should not create a report when adding a single result''' - response = self.client.post(self.path, self.data) - number_of_reports = len(Report.objects.all()) - # After adding one result for one revision, there should be no reports - self.assertEquals(number_of_reports, 0) - - def test_report_is_created(self): - """Should create a report when adding a result for two revisions""" - response = self.client.post(self.path, self.data) - - modified_data = copy.deepcopy(self.data) - modified_data['commitid'] = "23233" - response = self.client.post(self.path, modified_data) - number_of_reports = len(Report.objects.all()) - # After adding a result for a second revision, a report should be created - self.assertEquals(number_of_reports, 1) - - def test_submit_data_with_none_timestamp(self): - """Should add a default revision date when timestamp is None""" - modified_data = copy.deepcopy(self.data) - # The value None will get urlencoded and converted to a "None" string - modified_data['revision_date'] = None - response = self.client.post(self.path, modified_data) - self.assertEquals(response.status_code, 202) - - def test_add_result_with_no_project(self): - """Should add a revision with the project""" - modified_data = copy.deepcopy(self.data) - modified_data['project'] = "My new project" - modified_data['executable'] = "My new executable" - response = self.client.post(self.path, modified_data) - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, "Result data saved successfully") - - -class TestAddJSONResults(TestCase): - - def setUp(self): - self.path = reverse('codespeed.views.add_json_results') - self.e = Environment(name='bigdog', cpu='Core 2 Duo 8200') - self.e.save() - temp = datetime.today() - self.cdate = datetime( - temp.year, temp.month, temp.day, temp.hour, temp.minute, temp.second) - - self.data = [ - {'commitid': '123', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 456}, - {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 457}, - {'commitid': '456', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards2', - 'environment': 'bigdog', - 'result_value': 34}, - {'commitid': '789', - 'project': 'pypy', - 'branch': 'default', - 'executable': 'pypy-c', - 'benchmark': 'Richards', - 'environment': 'bigdog', - 'result_value': 458}, - ] - - def test_add_correct_results(self): - """Should add all results when the request data is valid""" - response = self.client.post(self.path, - {'json': json.dumps(self.data)}) - - # Check that we get a success response - self.assertEquals(response.status_code, 202) - self.assertEquals(response.content, - "All result data saved successfully") - - # Check that the data was correctly saved - e = Environment.objects.get(name='bigdog') - b = Benchmark.objects.get(name='Richards') - self.assertEquals(b.benchmark_type, "C") - self.assertEquals(b.units, "seconds") - self.assertEquals(b.lessisbetter, True) - p = Project.objects.get(name='pypy') - branch = Branch.objects.get(name='default', project=p) - r = Revision.objects.get(commitid='123', branch=branch) - i = Executable.objects.get(name='pypy-c') - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 456) - resdate = res.date.strftime("%Y%m%dT%H%M%S") - selfdate = self.cdate.strftime("%Y%m%dT%H%M%S") - self.assertTrue(resdate, selfdate) - - r = Revision.objects.get(commitid='456', branch=branch) - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 457) - - r = Revision.objects.get(commitid='789', branch=branch) - res = Result.objects.get( - revision=r, - executable=i, - benchmark=b, - environment=e - ) - self.assertTrue(res.value, 458) - - def test_bad_environment(self): - """Add result associated with non-existing environment. - Only change one item in the list. - """ - data = self.data[0] - bad_name = 'bigdog1' - data['environment'] = bad_name - response = self.client.post(self.path, - {'json': json.dumps(self.data)}) - - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, "Environment " + bad_name + " not found") - data['environment'] = 'bigdog' - - def test_empty_argument(self): - '''Should return 400 when making a request with an empty argument''' - data = self.data[1] - for key in data: - backup = data[key] - data[key] = "" - response = self.client.post(self.path, - {'json': json.dumps(self.data)}) - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, 'Value for key "' + key + '" empty in request') - data[key] = backup - - def test_missing_argument(self): - '''Should return 400 when making a request with a missing argument''' - data = self.data[2] - for key in data: - backup = data[key] - del(data[key]) - response = self.client.post(self.path, - {'json': json.dumps(self.data)}) - self.assertEquals(response.status_code, 400) - self.assertEquals(response.content, 'Key "' + key + '" missing from request') - data[key] = backup - - def test_report_is_created(self): - '''Should create a report when adding json results for two revisions - plus a third revision with one result less than the last one''' - response = self.client.post(self.path, - {'json': json.dumps(self.data)}) - - # Check that we get a success response - self.assertEquals(response.status_code, 202) - - number_of_reports = len(Report.objects.all()) - # After adding 4 result for 3 revisions, only 2 reports should be created - # The third revision will need an extra result for Richards2 in order - # to trigger report creation - self.assertEquals(number_of_reports, 1) - - -class TestTimeline(TestCase): - fixtures = ["timeline_tests.json"] - - def test_fixture(self): - """Test the loaded fixture data - """ - env = Environment.objects.filter(name="Dual Core") - self.assertEquals(len(env), 1) - benchmarks = Benchmark.objects.filter(name="float") - self.assertEquals(len(benchmarks), 1) - self.assertEquals(benchmarks[0].units, "seconds") - results = benchmarks[0].results.all() - self.assertEquals(len(results), 8) - - def test_gettimelinedata(self): - """Test that gettimelinedata returns correct timeline data - """ - path = reverse('codespeed.views.gettimelinedata') - data = { - "exe": "1,2", - "base": "2+4", - "ben": "float", - "env": "1", - "revs": 2 - } - response = self.client.get(path, data) - self.assertEquals(response.status_code, 200) - responsedata = json.loads(response.content) - self.assertEquals( - responsedata['error'], "None", "there should be no errors") - self.assertEquals( - len(responsedata['timelines']), 1, "there should be 1 benchmark") - self.assertEquals( - len(responsedata['timelines'][0]['branches']['default']), - 2, - "there should be 2 timelines") - self.assertEquals( - len(responsedata['timelines'][0]['branches']['default']['1']), - 2, - "There are 2 datapoints") - self.assertEquals( - responsedata['timelines'][0]['branches']['default']['1'][1], - [u'2011-04-13T17:04:22', 2000.0, 1.11111, u'2', u'default'], - "Wrong data returned: ") - - -class TestCodespeedSettings(TestCase): - """Test codespeed.settings - """ - - def setUp(self): - self.cs_setting_keys = [key for key in dir(default_settings) if key.isupper()] - - def test_website_name(self): - """See if WEBSITENAME is set - """ - self.assertTrue(default_settings.WEBSITE_NAME) - self.assertEqual(default_settings.WEBSITE_NAME, 'MySpeedSite', - "Change codespeed settings in project.settings") - - def test_keys_in_settings(self): - """Check that all settings attributes from codespeed.settings exist - in django.conf.settings - """ - for k in self.cs_setting_keys: - self.assertTrue(hasattr(settings, k), - "Key {0} is missing in settings.py.".format(k)) - - def test_settings_attributes(self): - """Check if all settings from codespeed.settings equals - django.conf.settings - """ - for k in self.cs_setting_keys: - self.assertEqual(getattr(settings, k), getattr(default_settings, k)) - - -class TestViewHelpers(TestCase): - """Test helper functions in codespeed.views""" - - def setUp(self): - self.project = Project.objects.create(name='Test') - self.executable = Executable.objects.create( - name='TestExecutable', project=self.project) - self.branch = Branch.objects.create(name='master', project=self.project) - - def test_get_baseline_executables(self): - # No revisions, no baseline - result = getbaselineexecutables() - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['executable'], 'none') - - # Check that a tagged revision will be included as baseline - revision1 = Revision.objects.create(commitid='1', tag='0.1', branch=self.branch) - result = getbaselineexecutables() - self.assertEqual(len(result), 2) - self.assertEqual(result[0]['executable'], 'none') - self.assertEqual(result[1]['executable'], self.executable) - self.assertEqual(result[1]['revision'], revision1) - - revision2 = Revision.objects.create(commitid='2', tag='0.2', branch=self.branch) - result = getbaselineexecutables() - self.assertEqual(len(result), 3) - - # An untagged revision will not be available as baseline - Revision.objects.create(commitid='3', branch=self.branch) - result = getbaselineexecutables() - self.assertEqual(len(result), 3) - - -class TestProject(TestCase): - - def setUp(self): - self.github_project = Project(repo_type='H', - repo_path='https://github.com/tobami/codespeed.git') - self.git_project = Project(repo_type='G', repo_path='/home/foo/codespeed') - - def test_repo_name(self): - """Test that only projects with local repositories have a repo_name attribute - """ - self.assertEqual(self.git_project.repo_name, 'codespeed') - - self.assertRaises(AttributeError, getattr, self.github_project, 'repo_name') - - def test_working_copy(self): - """Test that only projects with local repositories have a working_copy attribute - """ - self.assertEqual(self.git_project.working_copy, - os.path.join(settings.REPOSITORY_BASE_PATH, - self.git_project.repo_name)) - - self.assertRaises( - AttributeError, getattr, self.github_project, 'working_copy') - - def test_github_browsing_url(self): - """If empty, the commit browsing url will be filled in with a default - value when using github repository. - """ - - # It should work with https:// as well as git:// urls - self.github_project.save() - self.assertEquals(self.github_project.commit_browsing_url, - 'https://github.com/tobami/codespeed.git/' - 'commit/{commitid}') - - self.github_project.repo_path = 'git://github.com/tobami/codespeed.git' - self.github_project.save() - self.assertEquals(self.github_project.commit_browsing_url, - 'https://github.com/tobami/codespeed.git/' - 'commit/{commitid}') - - # If filled in, commit browsing url should not change - self.github_project.commit_browsing_url = 'https://example.com/{commitid}' - self.github_project.save() - self.assertEquals(self.github_project.commit_browsing_url, - 'https://example.com/{commitid}') From a96c5f80c9f4a7c546615635b075f19b4a569e61 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 19:48:35 +0100 Subject: [PATCH 264/429] Move view helper functions to domain module --- codespeed/domain.py | 106 ++++++++++++++++++++++++++++++ codespeed/tests/test_domain.py | 40 ++++++++++++ codespeed/tests/test_models.py | 1 + codespeed/tests/test_views.py | 37 +---------- codespeed/views.py | 115 +++------------------------------ 5 files changed, 158 insertions(+), 141 deletions(-) create mode 100644 codespeed/domain.py create mode 100644 codespeed/tests/test_domain.py diff --git a/codespeed/domain.py b/codespeed/domain.py new file mode 100644 index 00000000..7677b022 --- /dev/null +++ b/codespeed/domain.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +import logging + +from django.conf import settings +from codespeed.models import Executable, Revision + +logger = logging.getLogger(__name__) + + +def get_default_environment(enviros, data, multi=False): + """Returns the default environment. Preference level is: + * Present in URL parameters (permalinks) + * Value in settings.py + * First Environment ID + + """ + defaultenviros = [] + # Use permalink values + if 'env' in data: + for env_value in data['env'].split(","): + for env in enviros: + try: + env_id = int(env_value) + except ValueError: + # Not an int + continue + for env in enviros: + if env_id == env.id: + defaultenviros.append(env) + if not multi: + break + # Use settings.py value + if not defaultenviros and not multi: + if (hasattr(settings, 'DEF_ENVIRONMENT') and + settings.DEF_ENVIRONMENT is not None): + for env in enviros: + if settings.DEF_ENVIRONMENT == env.name: + defaultenviros.append(env) + break + # Last fallback + if not defaultenviros: + defaultenviros = enviros + if multi: + return defaultenviros + else: + return defaultenviros[0] + + +def getbaselineexecutables(): + baseline = [{ + 'key': "none", + 'name': "None", + 'executable': "none", + 'revision': "none", + }] + executables = Executable.objects.select_related('project') + revs = Revision.objects.exclude(tag="").select_related('branch__project') + maxlen = 22 + for rev in revs: + # Add executables that correspond to each tagged revision. + for exe in [e for e in executables if e.project == rev.branch.project]: + exestring = str(exe) + if len(exestring) > maxlen: + exestring = str(exe)[0:maxlen] + "..." + name = exestring + " " + rev.tag + key = str(exe.id) + "+" + str(rev.id) + baseline.append({ + 'key': key, + 'executable': exe, + 'revision': rev, + 'name': name, + }) + # move default to first place + if hasattr(settings, 'DEF_BASELINE') and settings.DEF_BASELINE is not None: + try: + exename = settings.DEF_BASELINE['executable'] + commitid = settings.DEF_BASELINE['revision'] + for base in baseline: + if base['key'] == "none": + continue + if (base['executable'].name == exename and + base['revision'].commitid == commitid): + baseline.remove(base) + baseline.insert(1, base) + break + except KeyError: + # TODO: write to server logs + # error in settings.DEF_BASELINE + pass + return baseline + + +def getdefaultexecutable(): + default = None + if (hasattr(settings, 'DEF_EXECUTABLE') and + settings.DEF_EXECUTABLE is not None): + try: + default = Executable.objects.get(name=settings.DEF_EXECUTABLE) + except Executable.DoesNotExist: + pass + if default is None: + execquery = Executable.objects.filter(project__track=True) + if len(execquery): + default = execquery[0] + + return default diff --git a/codespeed/tests/test_domain.py b/codespeed/tests/test_domain.py new file mode 100644 index 00000000..44eb1fb4 --- /dev/null +++ b/codespeed/tests/test_domain.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from django.test import TestCase + +from codespeed.models import Project, Executable, Branch, Revision +from codespeed.views import getbaselineexecutables + + +class TestGetBaselineExecutables(TestCase): + """Test helper functions in codespeed.views""" + + def setUp(self): + self.project = Project.objects.create(name='Test') + self.executable = Executable.objects.create( + name='TestExecutable', project=self.project) + self.branch = Branch.objects.create(name='master', + project=self.project) + + def test_get_baseline_executables(self): + # No revisions, no baseline + result = getbaselineexecutables() + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['executable'], 'none') + + # Check that a tagged revision will be included as baseline + revision1 = Revision.objects.create(commitid='1', tag='0.1', + branch=self.branch) + result = getbaselineexecutables() + self.assertEqual(len(result), 2) + self.assertEqual(result[0]['executable'], 'none') + self.assertEqual(result[1]['executable'], self.executable) + self.assertEqual(result[1]['revision'], revision1) + + Revision.objects.create(commitid='2', tag='0.2', branch=self.branch) + result = getbaselineexecutables() + self.assertEqual(len(result), 3) + + # An untagged revision will not be available as baseline + Revision.objects.create(commitid='3', branch=self.branch) + result = getbaselineexecutables() + self.assertEqual(len(result), 3) diff --git a/codespeed/tests/test_models.py b/codespeed/tests/test_models.py index 74a802c8..eb4d04ab 100644 --- a/codespeed/tests/test_models.py +++ b/codespeed/tests/test_models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os from django.conf import settings diff --git a/codespeed/tests/test_views.py b/codespeed/tests/test_views.py index 7b0f78a0..e65a9ffb 100644 --- a/codespeed/tests/test_views.py +++ b/codespeed/tests/test_views.py @@ -8,7 +8,6 @@ from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) -from codespeed.views import getbaselineexecutables class TestAddResult(TestCase): @@ -362,7 +361,8 @@ def setUp(self): 'environment': 'Dual Core', 'result_value': 200, } - resp = self.client.post(reverse('codespeed.views.add_result'), self.data) + resp = self.client.post(reverse('codespeed.views.add_result'), + self.data) self.assertEqual(resp.status_code, 202) self.data['commitid'] = "abcd2" self.data['result_value'] = 150 @@ -381,36 +381,3 @@ def test_reports_post_returns_405(self): response = self.client.post(reverse('codespeed.views.reports'), {}) self.assertEqual(response.status_code, 405) - - -class TestViewHelpers(TestCase): - """Test helper functions in codespeed.views""" - - def setUp(self): - self.project = Project.objects.create(name='Test') - self.executable = Executable.objects.create( - name='TestExecutable', project=self.project) - self.branch = Branch.objects.create(name='master', project=self.project) - - def test_get_baseline_executables(self): - # No revisions, no baseline - result = getbaselineexecutables() - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['executable'], 'none') - - # Check that a tagged revision will be included as baseline - revision1 = Revision.objects.create(commitid='1', tag='0.1', branch=self.branch) - result = getbaselineexecutables() - self.assertEqual(len(result), 2) - self.assertEqual(result[0]['executable'], 'none') - self.assertEqual(result[1]['executable'], self.executable) - self.assertEqual(result[1]['revision'], revision1) - - revision2 = Revision.objects.create(commitid='2', tag='0.2', branch=self.branch) - result = getbaselineexecutables() - self.assertEqual(len(result), 3) - - # An untagged revision will not be available as baseline - Revision.objects.create(commitid='3', branch=self.branch) - result = getbaselineexecutables() - self.assertEqual(len(result), 3) diff --git a/codespeed/views.py b/codespeed/views.py index 157bf87a..691508c0 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -15,7 +15,8 @@ from codespeed.models import (Environment, Report, Project, Revision, Result, Executable, Benchmark, Branch) - +from codespeed.domain import (get_default_environment, getbaselineexecutables, + getdefaultexecutable) logger = logging.getLogger(__name__) @@ -51,104 +52,6 @@ def no_data_found(request): }, context_instance=RequestContext(request)) -def getbaselineexecutables(): - baseline = [{ - 'key': "none", - 'name': "None", - 'executable': "none", - 'revision': "none", - }] - executables = Executable.objects.select_related('project') - revs = Revision.objects.exclude(tag="").select_related('branch__project') - maxlen = 22 - for rev in revs: - # Add executables that correspond to each tagged revision. - for exe in [e for e in executables if e.project == rev.branch.project]: - exestring = str(exe) - if len(exestring) > maxlen: - exestring = str(exe)[0:maxlen] + "..." - name = exestring + " " + rev.tag - key = str(exe.id) + "+" + str(rev.id) - baseline.append({ - 'key': key, - 'executable': exe, - 'revision': rev, - 'name': name, - }) - # move default to first place - if hasattr(settings, 'DEF_BASELINE') and settings.DEF_BASELINE is not None: - try: - exename = settings.DEF_BASELINE['executable'] - commitid = settings.DEF_BASELINE['revision'] - for base in baseline: - if base['key'] == "none": - continue - if (base['executable'].name == exename and - base['revision'].commitid == commitid): - baseline.remove(base) - baseline.insert(1, base) - break - except KeyError: - # TODO: write to server logs - #error in settings.DEF_BASELINE - pass - return baseline - - -def get_default_environment(enviros, data, multi=False): - """Returns the default environment. Preference level is: - * Present in URL parameters (permalinks) - * Value in settings.py - * First Environment ID - - """ - defaultenviros = [] - # Use permalink values - if 'env' in data: - for env_value in data['env'].split(","): - for env in enviros: - try: - env_id = int(env_value) - except ValueError: - # Not an int - continue - for env in enviros: - if env_id == env.id: - defaultenviros.append(env) - if not multi: - break - # Use settings.py value - if not defaultenviros and not multi: - if (hasattr(settings, 'DEF_ENVIRONMENT') and - settings.DEF_ENVIRONMENT is not None): - for env in enviros: - if settings.DEF_ENVIRONMENT == env.name: - defaultenviros.append(env) - break - # Last fallback - if not defaultenviros: - defaultenviros = enviros - if multi: - return defaultenviros - else: - return defaultenviros[0] - - -def getdefaultexecutable(): - default = None - if hasattr(settings, 'DEF_EXECUTABLE') and settings.DEF_EXECUTABLE is not None: - try: - default = Executable.objects.get(name=settings.DEF_EXECUTABLE) - except Executable.DoesNotExist: - pass - if default is None: - execquery = Executable.objects.filter(project__track=True) - if len(execquery): - default = execquery[0] - - return default - - def getcomparisonexes(): all_executables = {} exekeys = [] @@ -689,7 +592,7 @@ def changes(request): selectedrevision = Revision.objects.get( commitid__startswith=commitid, branch=branch ) - if not selectedrevision in revisionlists[selectedrevision.project.name]: + if selectedrevision not in revisionlists[selectedrevision.project.name]: revisionlists[selectedrevision.project.name].append(selectedrevision) except Revision.DoesNotExist: selectedrevision = lastrevisions[0] @@ -735,11 +638,11 @@ def reports(request): context['significant_reports'] = \ Report.objects.filter( revision__branch__name=settings.DEF_BRANCH, - colorcode__in = ('red','green') + colorcode__in=('red', 'green') ).order_by('-revision__date')[:10] - return render_to_response('codespeed/reports.html', - context, context_instance=RequestContext(request)) + return render_to_response('codespeed/reports.html', context, + context_instance=RequestContext(request)) @require_GET @@ -780,7 +683,7 @@ def displaylogs(request): rev, e, exc_info=True) error = repr(e) - # add commit browsing url to logs + # Add commit browsing url to logs project = rev.branch.project for log in logs: log['commit_browse_url'] = project.commit_browsing_url.format(**log) @@ -851,7 +754,7 @@ def validate_result(item): error = True for key in mandatory_data: - if not key in item: + if key not in item: return 'Key "' + key + '" missing from request', error elif key in item and item[key] == "": return 'Value for key "' + key + '" empty in request', error @@ -995,7 +898,7 @@ def add_json_results(request): if error: logger.debug( "add_json_results: could not save item %d because %s" % ( - i, response)) + i, response)) return HttpResponseBadRequest(response) else: unique_reports.add(response) From 0d130e9c69ce0c6e148f734b10965d25fa812988 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 20:02:36 +0100 Subject: [PATCH 265/429] Move remaining non-view functions to domain module --- codespeed/domain.py | 219 +++++++++++++++++++++++++++++++++++++++++++- codespeed/views.py | 219 +------------------------------------------- 2 files changed, 221 insertions(+), 217 deletions(-) diff --git a/codespeed/domain.py b/codespeed/domain.py index 7677b022..d30d7d29 100644 --- a/codespeed/domain.py +++ b/codespeed/domain.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- import logging +from datetime import datetime +from django.core.exceptions import ValidationError from django.conf import settings -from codespeed.models import Executable, Revision +from codespeed.models import (Environment, Executable, Revision, + Project, Branch, Benchmark, + Result, Report) logger = logging.getLogger(__name__) @@ -104,3 +108,216 @@ def getdefaultexecutable(): default = execquery[0] return default + + +def getcomparisonexes(): + all_executables = {} + exekeys = [] + baselines = getbaselineexecutables() + for proj in Project.objects.all(): + executables = [] + executablekeys = [] + maxlen = 20 + # add all tagged revs for any project + for exe in baselines: + if exe['key'] is not "none" and exe['executable'].project == proj: + executablekeys.append(exe['key']) + executables.append(exe) + + # add latest revs of the project + branches = Branch.objects.filter(project=proj) + for branch in branches: + try: + rev = Revision.objects.filter(branch=branch).latest('date') + except Revision.DoesNotExist: + continue + # Now only append when tag == "", + # because we already added tagged revisions + if rev.tag == "": + for exe in Executable.objects.filter(project=proj): + exestring = str(exe) + if len(exestring) > maxlen: + exestring = str(exe)[0:maxlen] + "..." + name = exestring + " latest" + if branch.name != 'default': + name += " in branch '" + branch.name + "'" + key = str(exe.id) + "+L+" + branch.name + executablekeys.append(key) + executables.append({ + 'key': key, + 'executable': exe, + 'revision': rev, + 'name': name, + }) + all_executables[proj] = executables + exekeys += executablekeys + return all_executables, exekeys + + +def getcommitlogs(rev, startrev, update=False): + logs = [] + + if rev.branch.project.repo_type == 'S': + from subversion import getlogs, updaterepo + elif rev.branch.project.repo_type == 'M': + from mercurial import getlogs, updaterepo + elif rev.branch.project.repo_type == 'G': + from git import getlogs, updaterepo + elif rev.branch.project.repo_type == 'H': + from github import getlogs, updaterepo + else: + if rev.branch.project.repo_type not in ("N", ""): + logger.warning("Don't know how to retrieve logs from %s project", + rev.branch.project.get_repo_type_display()) + return logs + + if update: + updaterepo(rev.branch.project) + + logs = getlogs(rev, startrev) + + # Remove last log because the startrev log shouldn't be shown + if len(logs) > 1 and logs[-1].get('commitid') == startrev.commitid: + logs.pop() + + return logs + + +def saverevisioninfo(rev): + log = getcommitlogs(rev, rev, update=True) + + if log: + log = log[0] + rev.author = log['author'] + rev.date = log['date'] + rev.message = log['message'] + + +def validate_result(item): + """ + Validates that a result dictionary has all needed parameters + + It returns a tuple + Environment, False when no errors where found + Errormessage, True when there is an error + """ + mandatory_data = [ + 'commitid', + 'branch', + 'project', + 'executable', + 'benchmark', + 'environment', + 'result_value', + ] + + error = True + for key in mandatory_data: + if key not in item: + return 'Key "' + key + '" missing from request', error + elif key in item and item[key] == "": + return 'Value for key "' + key + '" empty in request', error + + # Check that the Environment exists + try: + e = Environment.objects.get(name=item['environment']) + error = False + return e, error + except Environment.DoesNotExist: + return "Environment %(environment)s not found" % item, error + + +def create_report_if_enough_data(rev, exe, e): + """Triggers Report creation when there are enough results""" + last_revs = Revision.objects.filter( + branch=rev.branch + ).order_by('-date')[:2] + if len(last_revs) > 1: + current_results = rev.results.filter(executable=exe, environment=e) + last_results = last_revs[1].results.filter( + executable=exe, environment=e) + # If there is are at least as many results as in the last revision, + # create new report + if len(current_results) >= len(last_results): + logger.debug("create_report_if_enough_data: About to create new report") + report, created = Report.objects.get_or_create( + executable=exe, environment=e, revision=rev + ) + report.full_clean() + report.save() + logger.debug("create_report_if_enough_data: Created new report.") + + +def save_result(data): + res, error = validate_result(data) + if error: + return res, True + else: + assert(isinstance(res, Environment)) + env = res + + p, created = Project.objects.get_or_create(name=data["project"]) + branch, created = Branch.objects.get_or_create(name=data["branch"], + project=p) + b, created = Benchmark.objects.get_or_create(name=data["benchmark"]) + + if created: + if "description" in data: + b.description = data["description"] + if "units" in data: + b.units = data["units"] + if "units_title" in data: + b.units_title = data["units_title"] + if "lessisbetter" in data: + b.lessisbetter = data["lessisbetter"] + b.full_clean() + b.save() + + try: + rev = branch.revisions.get(commitid=data['commitid']) + except Revision.DoesNotExist: + rev_date = data.get("revision_date") + # "None" (as string) can happen when we urlencode the POST data + if not rev_date or rev_date in ["", "None"]: + rev_date = datetime.today() + rev = Revision(branch=branch, project=p, commitid=data['commitid'], + date=rev_date) + try: + rev.full_clean() + except ValidationError as e: + return str(e), True + if p.repo_type not in ("N", ""): + try: + saverevisioninfo(rev) + except RuntimeError as e: + logger.warning("unable to save revision %s info: %s", rev, e, + exc_info=True) + rev.save() + + exe, created = Executable.objects.get_or_create( + name=data['executable'], + project=p + ) + + try: + r = Result.objects.get( + revision=rev, executable=exe, benchmark=b, environment=env) + except Result.DoesNotExist: + r = Result(revision=rev, executable=exe, benchmark=b, environment=env) + + r.value = data["result_value"] + if 'result_date' in data: + r.date = data["result_date"] + elif rev.date: + r.date = rev.date + else: + r.date = datetime.now() + + r.std_dev = data.get('std_dev') + r.val_min = data.get('min') + r.val_max = data.get('max') + + r.full_clean() + r.save() + + return (rev, exe, env), False diff --git a/codespeed/views.py b/codespeed/views.py index 691508c0..24189416 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from datetime import datetime import json import logging -from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.http import (HttpResponse, Http404, HttpResponseNotAllowed, HttpResponseBadRequest) @@ -16,7 +14,9 @@ from codespeed.models import (Environment, Report, Project, Revision, Result, Executable, Benchmark, Branch) from codespeed.domain import (get_default_environment, getbaselineexecutables, - getdefaultexecutable) + getdefaultexecutable, getcomparisonexes, + getcommitlogs, save_result, + create_report_if_enough_data) logger = logging.getLogger(__name__) @@ -52,50 +52,6 @@ def no_data_found(request): }, context_instance=RequestContext(request)) -def getcomparisonexes(): - all_executables = {} - exekeys = [] - baselines = getbaselineexecutables() - for proj in Project.objects.all(): - executables = [] - executablekeys = [] - maxlen = 20 - # add all tagged revs for any project - for exe in baselines: - if exe['key'] is not "none" and exe['executable'].project == proj: - executablekeys.append(exe['key']) - executables.append(exe) - - # add latest revs of the project - branches = Branch.objects.filter(project=proj) - for branch in branches: - try: - rev = Revision.objects.filter(branch=branch).latest('date') - except Revision.DoesNotExist: - continue - # Now only append when tag == "", - # because we already added tagged revisions - if rev.tag == "": - for exe in Executable.objects.filter(project=proj): - exestring = str(exe) - if len(exestring) > maxlen: - exestring = str(exe)[0:maxlen] + "..." - name = exestring + " latest" - if branch.name != 'default': - name += " in branch '" + branch.name + "'" - key = str(exe.id) + "+L+" + branch.name - executablekeys.append(key) - executables.append({ - 'key': key, - 'executable': exe, - 'revision': rev, - 'name': name, - }) - all_executables[proj] = executables - exekeys += executablekeys - return all_executables, exekeys - - @require_GET def getcomparisondata(request): executables, exekeys = getcomparisonexes() @@ -695,175 +651,6 @@ def displaylogs(request): context_instance=RequestContext(request)) -def getcommitlogs(rev, startrev, update=False): - logs = [] - - if rev.branch.project.repo_type == 'S': - from subversion import getlogs, updaterepo - elif rev.branch.project.repo_type == 'M': - from mercurial import getlogs, updaterepo - elif rev.branch.project.repo_type == 'G': - from git import getlogs, updaterepo - elif rev.branch.project.repo_type == 'H': - from github import getlogs, updaterepo - else: - if rev.branch.project.repo_type not in ("N", ""): - logger.warning("Don't know how to retrieve logs from %s project", - rev.branch.project.get_repo_type_display()) - return logs - - if update: - updaterepo(rev.branch.project) - - logs = getlogs(rev, startrev) - - # Remove last log because the startrev log shouldn't be shown - if len(logs) > 1 and logs[-1].get('commitid') == startrev.commitid: - logs.pop() - - return logs - - -def saverevisioninfo(rev): - log = getcommitlogs(rev, rev, update=True) - - if log: - log = log[0] - rev.author = log['author'] - rev.date = log['date'] - rev.message = log['message'] - - -def validate_result(item): - """ - Validates that a result dictionary has all needed parameters - - It returns a tuple - Environment, False when no errors where found - Errormessage, True when there is an error - """ - mandatory_data = [ - 'commitid', - 'branch', - 'project', - 'executable', - 'benchmark', - 'environment', - 'result_value', - ] - - error = True - for key in mandatory_data: - if key not in item: - return 'Key "' + key + '" missing from request', error - elif key in item and item[key] == "": - return 'Value for key "' + key + '" empty in request', error - - # Check that the Environment exists - try: - e = Environment.objects.get(name=item['environment']) - error = False - return e, error - except Environment.DoesNotExist: - return "Environment %(environment)s not found" % item, error - - -def create_report_if_enough_data(rev, exe, e): - """Triggers Report creation when there are enough results""" - last_revs = Revision.objects.filter( - branch=rev.branch - ).order_by('-date')[:2] - if len(last_revs) > 1: - current_results = rev.results.filter(executable=exe, environment=e) - last_results = last_revs[1].results.filter( - executable=exe, environment=e) - # If there is are at least as many results as in the last revision, - # create new report - if len(current_results) >= len(last_results): - logger.debug("create_report_if_enough_data: About to create new report") - report, created = Report.objects.get_or_create( - executable=exe, environment=e, revision=rev - ) - report.full_clean() - report.save() - logger.debug("create_report_if_enough_data: Created new report.") - - -def save_result(data): - res, error = validate_result(data) - if error: - return res, True - else: - assert(isinstance(res, Environment)) - env = res - - p, created = Project.objects.get_or_create(name=data["project"]) - branch, created = Branch.objects.get_or_create(name=data["branch"], - project=p) - b, created = Benchmark.objects.get_or_create(name=data["benchmark"]) - - if created: - if "description" in data: - b.description = data["description"] - if "units" in data: - b.units = data["units"] - if "units_title" in data: - b.units_title = data["units_title"] - if "lessisbetter" in data: - b.lessisbetter = data["lessisbetter"] - b.full_clean() - b.save() - - try: - rev = branch.revisions.get(commitid=data['commitid']) - except Revision.DoesNotExist: - rev_date = data.get("revision_date") - # "None" (as string) can happen when we urlencode the POST data - if not rev_date or rev_date in ["", "None"]: - rev_date = datetime.today() - rev = Revision(branch=branch, project=p, commitid=data['commitid'], - date=rev_date) - try: - rev.full_clean() - except ValidationError as e: - return str(e), True - if p.repo_type not in ("N", ""): - try: - saverevisioninfo(rev) - except RuntimeError as e: - logger.warning("unable to save revision %s info: %s", rev, e, - exc_info=True) - rev.save() - - exe, created = Executable.objects.get_or_create( - name=data['executable'], - project=p - ) - - try: - r = Result.objects.get( - revision=rev, executable=exe, benchmark=b, environment=env) - except Result.DoesNotExist: - r = Result(revision=rev, executable=exe, benchmark=b, environment=env) - - r.value = data["result_value"] - if 'result_date' in data: - r.date = data["result_date"] - elif rev.date: - r.date = rev.date - else: - r.date = datetime.now() - - r.std_dev = data.get('std_dev') - r.val_min = data.get('min') - r.val_max = data.get('max') - - r.full_clean() - r.save() - - return (rev, exe, env), False - - @csrf_exempt def add_result(request): if request.method != 'POST': From 02a2039438fd658022bad5bf8e57e874481db691 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 20:09:16 +0100 Subject: [PATCH 266/429] Use require_POST instead of manually checking method --- codespeed/tests/test_views.py | 6 ++++++ codespeed/views.py | 15 +++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/codespeed/tests/test_views.py b/codespeed/tests/test_views.py index e65a9ffb..bcafaad4 100644 --- a/codespeed/tests/test_views.py +++ b/codespeed/tests/test_views.py @@ -199,6 +199,12 @@ def setUp(self): 'result_value': 458}, ] + def test_get_returns_405(self): + response = self.client.get(self.path, + {'json': json.dumps(self.data)}) + + self.assertEquals(response.status_code, 405) + def test_add_correct_results(self): """Should add all results when the request data is valid""" response = self.client.post(self.path, diff --git a/codespeed/views.py b/codespeed/views.py index 24189416..7bdd8725 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -3,10 +3,9 @@ import logging from django.core.urlresolvers import reverse -from django.http import (HttpResponse, Http404, HttpResponseNotAllowed, - HttpResponseBadRequest) +from django.http import HttpResponse, Http404, HttpResponseBadRequest from django.shortcuts import get_object_or_404, render_to_response -from django.views.decorators.http import require_GET +from django.views.decorators.http import require_GET, require_POST from django.views.decorators.csrf import csrf_exempt from django.template import RequestContext from django.conf import settings @@ -652,12 +651,9 @@ def displaylogs(request): @csrf_exempt +@require_POST def add_result(request): - if request.method != 'POST': - return HttpResponseNotAllowed('POST') - data = request.POST - - response, error = save_result(data) + response, error = save_result(request.POST) if error: logger.error("Could not save result: " + response) return HttpResponseBadRequest(response) @@ -668,9 +664,8 @@ def add_result(request): @csrf_exempt +@require_POST def add_json_results(request): - if request.method != 'POST': - return HttpResponseNotAllowed('POST') if not request.POST.get('json'): return HttpResponseBadRequest("No key 'json' in POST payload") data = json.loads(request.POST['json']) From c204d55aa7fca1596dc25a91addb9b14cfef2651 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 20:17:40 +0100 Subject: [PATCH 267/429] Add python3 future imports --- codespeed/domain.py | 2 ++ codespeed/models.py | 2 ++ codespeed/views.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/codespeed/domain.py b/codespeed/domain.py index d30d7d29..cbd5792e 100644 --- a/codespeed/domain.py +++ b/codespeed/domain.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + import logging from datetime import datetime from django.core.exceptions import ValidationError diff --git a/codespeed/models.py b/codespeed/models.py index 1e17afca..54c475c5 100644 --- a/codespeed/models.py +++ b/codespeed/models.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + import os import json diff --git a/codespeed/views.py b/codespeed/views.py index 7bdd8725..07e4a4b1 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + import json import logging From 9893b87de02bdc1ea6256207de5cc010b095b3a5 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 20:37:37 +0100 Subject: [PATCH 268/429] Don't import unicode_literals as it breaks the comparison view Fixes #189 --- codespeed/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codespeed/domain.py b/codespeed/domain.py index cbd5792e..0abf0565 100644 --- a/codespeed/domain.py +++ b/codespeed/domain.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import import logging from datetime import datetime From 509b7d5c8b9fe98f00ca8d7bd3af63f3d90c1ba1 Mon Sep 17 00:00:00 2001 From: Miquel Torres Date: Sat, 31 Oct 2015 21:12:34 +0100 Subject: [PATCH 269/429] Upgrade jqplot to 1.0.8 --- codespeed/static/js/jqplot/README.md | 3 ++ codespeed/static/js/jqplot/excanvas.min.js | 4 ++- .../js/jqplot/jqplot.barRenderer.min.js | 17 ++-------- .../jqplot.canvasAxisLabelRenderer.min.js | 17 ++-------- .../jqplot.canvasAxisTickRenderer.min.js | 17 ++-------- .../jqplot/jqplot.canvasTextRenderer.min.js | 17 ++-------- .../jqplot/jqplot.categoryAxisRenderer.min.js | 33 ++----------------- .../static/js/jqplot/jqplot.cursor.min.js | 17 ++-------- .../js/jqplot/jqplot.dateAxisRenderer.min.js | 17 ++-------- .../js/jqplot/jqplot.highlighter.min.js | 17 ++-------- .../static/js/jqplot/jquery.jqplot.min.css | 2 +- .../static/js/jqplot/jquery.jqplot.min.js | 17 ++-------- 12 files changed, 34 insertions(+), 144 deletions(-) create mode 100644 codespeed/static/js/jqplot/README.md diff --git a/codespeed/static/js/jqplot/README.md b/codespeed/static/js/jqplot/README.md new file mode 100644 index 00000000..bf429c2e --- /dev/null +++ b/codespeed/static/js/jqplot/README.md @@ -0,0 +1,3 @@ +# jqPlot + +version: 1.0.8 diff --git a/codespeed/static/js/jqplot/excanvas.min.js b/codespeed/static/js/jqplot/excanvas.min.js index 896786ef..83373a53 100644 --- a/codespeed/static/js/jqplot/excanvas.min.js +++ b/codespeed/static/js/jqplot/excanvas.min.js @@ -1 +1,3 @@ -if(!document.createElement("canvas").getContext){(function(){var Y=Math;var q=Y.round;var o=Y.sin;var B=Y.cos;var H=Y.abs;var N=Y.sqrt;var d=10;var f=d/2;function A(){return this.context_||(this.context_=new D(this))}var v=Array.prototype.slice;function g(j,m,p){var i=v.call(arguments,2);return function(){return j.apply(m,i.concat(v.call(arguments)))}}function ad(i){return String(i).replace(/&/g,"&").replace(/"/g,""")}function R(j){if(!j.namespaces.g_vml_){j.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!j.namespaces.g_o_){j.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!j.styleSheets.ex_canvas_){var i=j.createStyleSheet();i.owningElement.id="ex_canvas_";i.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}R(document);var e={init:function(i){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var j=i||document;j.createElement("canvas");j.attachEvent("onreadystatechange",g(this.init_,this,j))}},init_:function(p){var m=p.getElementsByTagName("canvas");for(var j=0;j1){m--}if(6*m<1){return j+(i-j)*6*m}else{if(2*m<1){return i}else{if(3*m<2){return j+(i-j)*(2/3-m)*6}else{return j}}}}function F(j){var ae,Z=1;j=String(j);if(j.charAt(0)=="#"){ae=j}else{if(/^rgb/.test(j)){var p=M(j);var ae="#",af;for(var m=0;m<3;m++){if(p[m].indexOf("%")!=-1){af=Math.floor(c(p[m])*255)}else{af=Number(p[m])}ae+=n[u(af,0,255)]}Z=p[3]}else{if(/^hsl/.test(j)){var p=M(j);ae=I(p);Z=p[3]}else{ae=b[j]||j}}}return{color:ae,alpha:Z}}var r={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var L={};function E(i){if(L[i]){return L[i]}var p=document.createElement("div");var m=p.style;try{m.font=i}catch(j){}return L[i]={style:m.fontStyle||r.style,variant:m.fontVariant||r.variant,weight:m.fontWeight||r.weight,size:m.fontSize||r.size,family:m.fontFamily||r.family}}function w(m,j){var i={};for(var af in m){i[af]=m[af]}var ae=parseFloat(j.currentStyle.fontSize),Z=parseFloat(m.size);if(typeof m.size=="number"){i.size=m.size}else{if(m.size.indexOf("px")!=-1){i.size=Z}else{if(m.size.indexOf("em")!=-1){i.size=ae*Z}else{if(m.size.indexOf("%")!=-1){i.size=(ae/100)*Z}else{if(m.size.indexOf("pt")!=-1){i.size=ae*(4/3)*Z}else{i.size=ae}}}}}i.size*=0.981;return i}function aa(i){return i.style+" "+i.variant+" "+i.weight+" "+i.size+"px "+i.family}function S(i){switch(i){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function D(j){this.m_=C();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=d*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=j;var i=j.ownerDocument.createElement("div");i.style.width=j.clientWidth+"px";i.style.height=j.clientHeight+"px";i.style.overflow="hidden";i.style.position="absolute";j.appendChild(i);this.element_=i;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var t=D.prototype;t.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};t.beginPath=function(){this.currentPath_=[]};t.moveTo=function(j,i){var m=this.getCoords_(j,i);this.currentPath_.push({type:"moveTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};t.lineTo=function(j,i){var m=this.getCoords_(j,i);this.currentPath_.push({type:"lineTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};t.bezierCurveTo=function(m,j,ai,ah,ag,ae){var i=this.getCoords_(ag,ae);var af=this.getCoords_(m,j);var Z=this.getCoords_(ai,ah);K(this,af,Z,i)};function K(i,Z,m,j){i.currentPath_.push({type:"bezierCurveTo",cp1x:Z.x,cp1y:Z.y,cp2x:m.x,cp2y:m.y,x:j.x,y:j.y});i.currentX_=j.x;i.currentY_=j.y}t.quadraticCurveTo=function(ag,m,j,i){var af=this.getCoords_(ag,m);var ae=this.getCoords_(j,i);var ah={x:this.currentX_+2/3*(af.x-this.currentX_),y:this.currentY_+2/3*(af.y-this.currentY_)};var Z={x:ah.x+(ae.x-this.currentX_)/3,y:ah.y+(ae.y-this.currentY_)/3};K(this,ah,Z,ae)};t.arc=function(aj,ah,ai,ae,j,m){ai*=d;var an=m?"at":"wa";var ak=aj+B(ae)*ai-f;var am=ah+o(ae)*ai-f;var i=aj+B(j)*ai-f;var al=ah+o(j)*ai-f;if(ak==i&&!m){ak+=0.125}var Z=this.getCoords_(aj,ah);var ag=this.getCoords_(ak,am);var af=this.getCoords_(i,al);this.currentPath_.push({type:an,x:Z.x,y:Z.y,radius:ai,xStart:ag.x,yStart:ag.y,xEnd:af.x,yEnd:af.y})};t.rect=function(m,j,i,p){this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath()};t.strokeRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.stroke();this.currentPath_=Z};t.fillRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.fill();this.currentPath_=Z};t.createLinearGradient=function(j,p,i,m){var Z=new U("gradient");Z.x0_=j;Z.y0_=p;Z.x1_=i;Z.y1_=m;return Z};t.createRadialGradient=function(p,ae,m,j,Z,i){var af=new U("gradientradial");af.x0_=p;af.y0_=ae;af.r0_=m;af.x1_=j;af.y1_=Z;af.r1_=i;return af};t.drawImage=function(ao,m){var ah,af,aj,aw,am,ak,aq,ay;var ai=ao.runtimeStyle.width;var an=ao.runtimeStyle.height;ao.runtimeStyle.width="auto";ao.runtimeStyle.height="auto";var ag=ao.width;var au=ao.height;ao.runtimeStyle.width=ai;ao.runtimeStyle.height=an;if(arguments.length==3){ah=arguments[1];af=arguments[2];am=ak=0;aq=aj=ag;ay=aw=au}else{if(arguments.length==5){ah=arguments[1];af=arguments[2];aj=arguments[3];aw=arguments[4];am=ak=0;aq=ag;ay=au}else{if(arguments.length==9){am=arguments[1];ak=arguments[2];aq=arguments[3];ay=arguments[4];ah=arguments[5];af=arguments[6];aj=arguments[7];aw=arguments[8]}else{throw Error("Invalid number of arguments")}}}var ax=this.getCoords_(ah,af);var p=aq/2;var j=ay/2;var av=[];var i=10;var ae=10;av.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",av.join(""))};t.stroke=function(aj){var ah=[];var Z=false;var m=10;var ak=10;ah.push("ai.x){ai.x=j.x}if(ae.y==null||j.yai.y){ai.y=j.y}}}ah.push(' ">');if(!aj){y(this,ah)}else{G(this,ah,ae,ai)}ah.push("");this.element_.insertAdjacentHTML("beforeEnd",ah.join(""))};function y(m,ae){var j=F(m.strokeStyle);var p=j.color;var Z=j.alpha*m.globalAlpha;var i=m.lineScale_*m.lineWidth;if(i<1){Z*=i}ae.push("')}function G(ao,ag,aI,ap){var ah=ao.fillStyle;var az=ao.arcScaleX_;var ay=ao.arcScaleY_;var j=ap.x-aI.x;var p=ap.y-aI.y;if(ah instanceof U){var al=0;var aD={x:0,y:0};var av=0;var ak=1;if(ah.type_=="gradient"){var aj=ah.x0_/az;var m=ah.y0_/ay;var ai=ah.x1_/az;var aK=ah.y1_/ay;var aH=ao.getCoords_(aj,m);var aG=ao.getCoords_(ai,aK);var ae=aG.x-aH.x;var Z=aG.y-aH.y;al=Math.atan2(ae,Z)*180/Math.PI;if(al<0){al+=360}if(al<0.000001){al=0}}else{var aH=ao.getCoords_(ah.x0_,ah.y0_);aD={x:(aH.x-aI.x)/j,y:(aH.y-aI.y)/p};j/=az*d;p/=ay*d;var aB=Y.max(j,p);av=2*ah.r0_/aB;ak=2*ah.r1_/aB-av}var at=ah.colors_;at.sort(function(aL,i){return aL.offset-i.offset});var an=at.length;var ar=at[0].color;var aq=at[an-1].color;var ax=at[0].alpha*ao.globalAlpha;var aw=at[an-1].alpha*ao.globalAlpha;var aC=[];for(var aF=0;aF')}else{if(ah instanceof T){if(j&&p){var af=-aI.x;var aA=-aI.y;ag.push("')}}else{var aJ=F(ao.fillStyle);var au=aJ.color;var aE=aJ.alpha*ao.globalAlpha;ag.push('')}}}t.fill=function(){this.stroke(true)};t.closePath=function(){this.currentPath_.push({type:"close"})};t.getCoords_=function(p,j){var i=this.m_;return{x:d*(p*i[0][0]+j*i[1][0]+i[2][0])-f,y:d*(p*i[0][1]+j*i[1][1]+i[2][1])-f}};t.save=function(){var i={};x(this,i);this.aStack_.push(i);this.mStack_.push(this.m_);this.m_=J(C(),this.m_)};t.restore=function(){if(this.aStack_.length){x(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function k(i){return isFinite(i[0][0])&&isFinite(i[0][1])&&isFinite(i[1][0])&&isFinite(i[1][1])&&isFinite(i[2][0])&&isFinite(i[2][1])}function X(j,i,p){if(!k(i)){return}j.m_=i;if(p){var Z=i[0][0]*i[1][1]-i[0][1]*i[1][0];j.lineScale_=N(H(Z))}}t.translate=function(m,j){var i=[[1,0,0],[0,1,0],[m,j,1]];X(this,J(i,this.m_),false)};t.rotate=function(j){var p=B(j);var m=o(j);var i=[[p,m,0],[-m,p,0],[0,0,1]];X(this,J(i,this.m_),false)};t.scale=function(m,j){this.arcScaleX_*=m;this.arcScaleY_*=j;var i=[[m,0,0],[0,j,0],[0,0,1]];X(this,J(i,this.m_),true)};t.transform=function(Z,p,af,ae,j,i){var m=[[Z,p,0],[af,ae,0],[j,i,1]];X(this,J(m,this.m_),true)};t.setTransform=function(ae,Z,ag,af,p,j){var i=[[ae,Z,0],[ag,af,0],[p,j,1]];X(this,i,true)};t.drawText_=function(ak,ai,ah,an,ag){var am=this.m_,aq=1000,j=0,ap=aq,af={x:0,y:0},ae=[];var i=w(E(this.font),this.element_);var p=aa(i);var ar=this.element_.currentStyle;var Z=this.textAlign.toLowerCase();switch(Z){case"left":case"center":case"right":break;case"end":Z=ar.direction=="ltr"?"right":"left";break;case"start":Z=ar.direction=="rtl"?"right":"left";break;default:Z="left"}switch(this.textBaseline){case"hanging":case"top":af.y=i.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":af.y=-i.size/2.25;break}switch(Z){case"right":j=aq;ap=0.05;break;case"center":j=ap=aq/2;break}var ao=this.getCoords_(ai+af.x,ah+af.y);ae.push('');if(ag){y(this,ae)}else{G(this,ae,{x:-j,y:0},{x:ap,y:i.size})}var al=am[0][0].toFixed(3)+","+am[1][0].toFixed(3)+","+am[0][1].toFixed(3)+","+am[1][1].toFixed(3)+",0,0";var aj=q(ao.x/d)+","+q(ao.y/d);ae.push('','','');this.element_.insertAdjacentHTML("beforeEnd",ae.join(""))};t.fillText=function(m,i,p,j){this.drawText_(m,i,p,j,false)};t.strokeText=function(m,i,p,j){this.drawText_(m,i,p,j,true)};t.measureText=function(m){if(!this.textMeasureEl_){var i='';this.element_.insertAdjacentHTML("beforeEnd",i);this.textMeasureEl_=this.element_.lastChild}var j=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(j.createTextNode(m));return{width:this.textMeasureEl_.offsetWidth}};t.clip=function(){};t.arcTo=function(){};t.createPattern=function(j,i){return new T(j,i)};function U(i){this.type_=i;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}U.prototype.addColorStop=function(j,i){i=F(i);this.colors_.push({offset:j,color:i.color,alpha:i.alpha})};function T(j,i){Q(j);switch(i){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=i;break;default:O("SYNTAX_ERR")}this.src_=j.src;this.width_=j.width;this.height_=j.height}function O(i){throw new P(i)}function Q(i){if(!i||i.nodeType!=1||i.tagName!="IMG"){O("TYPE_MISMATCH_ERR")}if(i.readyState!="complete"){O("INVALID_STATE_ERR")}}function P(i){this.code=this[i];this.message=i+": DOM Exception "+this.code}var W=P.prototype=new Error;W.INDEX_SIZE_ERR=1;W.DOMSTRING_SIZE_ERR=2;W.HIERARCHY_REQUEST_ERR=3;W.WRONG_DOCUMENT_ERR=4;W.INVALID_CHARACTER_ERR=5;W.NO_DATA_ALLOWED_ERR=6;W.NO_MODIFICATION_ALLOWED_ERR=7;W.NOT_FOUND_ERR=8;W.NOT_SUPPORTED_ERR=9;W.INUSE_ATTRIBUTE_ERR=10;W.INVALID_STATE_ERR=11;W.SYNTAX_ERR=12;W.INVALID_MODIFICATION_ERR=13;W.NAMESPACE_ERR=14;W.INVALID_ACCESS_ERR=15;W.VALIDATION_ERR=16;W.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=e;CanvasRenderingContext2D=D;CanvasGradient=U;CanvasPattern=T;DOMException=P})()}; \ No newline at end of file +/* jqPlot @VERSION | (c) 2009-2013 Chris Leonello | jplot.com + jsDate | (c) 2010-2013 Chris Leonello + */if(!document.createElement("canvas").getContext){(function(){var ab=Math;var n=ab.round;var l=ab.sin;var A=ab.cos;var H=ab.abs;var N=ab.sqrt;var d=10;var f=d/2;var z=+navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];function y(){return this.context_||(this.context_=new D(this))}var t=Array.prototype.slice;function g(j,m,p){var i=t.call(arguments,2);return function(){return j.apply(m,i.concat(t.call(arguments)))}}function af(i){return String(i).replace(/&/g,"&").replace(/"/g,""")}function Y(m,j,i){if(!m.namespaces[j]){m.namespaces.add(j,i,"#default#VML")}}function R(j){Y(j,"g_vml_","urn:schemas-microsoft-com:vml");Y(j,"g_o_","urn:schemas-microsoft-com:office:office");if(!j.styleSheets.ex_canvas_){var i=j.createStyleSheet();i.owningElement.id="ex_canvas_";i.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}R(document);var e={init:function(i){var j=i||document;j.createElement("canvas");j.attachEvent("onreadystatechange",g(this.init_,this,j))},init_:function(p){var m=p.getElementsByTagName("canvas");for(var j=0;j1){m--}if(6*m<1){return j+(i-j)*6*m}else{if(2*m<1){return i}else{if(3*m<2){return j+(i-j)*(2/3-m)*6}else{return j}}}}var C={};function F(j){if(j in C){return C[j]}var ag,Z=1;j=String(j);if(j.charAt(0)=="#"){ag=j}else{if(/^rgb/.test(j)){var p=M(j);var ag="#",ah;for(var m=0;m<3;m++){if(p[m].indexOf("%")!=-1){ah=Math.floor(c(p[m])*255)}else{ah=+p[m]}ag+=k[r(ah,0,255)]}Z=+p[3]}else{if(/^hsl/.test(j)){var p=M(j);ag=I(p);Z=p[3]}else{ag=b[j]||j}}}return C[j]={color:ag,alpha:Z}}var o={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var L={};function E(i){if(L[i]){return L[i]}var p=document.createElement("div");var m=p.style;try{m.font=i}catch(j){}return L[i]={style:m.fontStyle||o.style,variant:m.fontVariant||o.variant,weight:m.fontWeight||o.weight,size:m.fontSize||o.size,family:m.fontFamily||o.family}}function u(m,j){var i={};for(var ah in m){i[ah]=m[ah]}var ag=parseFloat(j.currentStyle.fontSize),Z=parseFloat(m.size);if(typeof m.size=="number"){i.size=m.size}else{if(m.size.indexOf("px")!=-1){i.size=Z}else{if(m.size.indexOf("em")!=-1){i.size=ag*Z}else{if(m.size.indexOf("%")!=-1){i.size=(ag/100)*Z}else{if(m.size.indexOf("pt")!=-1){i.size=Z/0.75}else{i.size=ag}}}}}i.size*=0.981;i.family="'"+i.family.replace(/(\'|\")/g,"").replace(/\s*,\s*/g,"', '")+"'";return i}function ac(i){return i.style+" "+i.variant+" "+i.weight+" "+i.size+"px "+i.family}var s={butt:"flat",round:"round"};function S(i){return s[i]||"square"}function D(i){this.m_=B();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=d*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var m="width:"+i.clientWidth+"px;height:"+i.clientHeight+"px;overflow:hidden;position:absolute";var j=i.ownerDocument.createElement("div");j.style.cssText=m;i.appendChild(j);var p=j.cloneNode(false);p.style.backgroundColor="red";p.style.filter="alpha(opacity=0)";i.appendChild(p);this.element_=j;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var q=D.prototype;q.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};q.beginPath=function(){this.currentPath_=[]};q.moveTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"moveTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.lineTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"lineTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.bezierCurveTo=function(m,j,ak,aj,ai,ag){var i=V(this,ai,ag);var ah=V(this,m,j);var Z=V(this,ak,aj);K(this,ah,Z,i)};function K(i,Z,m,j){i.currentPath_.push({type:"bezierCurveTo",cp1x:Z.x,cp1y:Z.y,cp2x:m.x,cp2y:m.y,x:j.x,y:j.y});i.currentX_=j.x;i.currentY_=j.y}q.quadraticCurveTo=function(ai,m,j,i){var ah=V(this,ai,m);var ag=V(this,j,i);var aj={x:this.currentX_+2/3*(ah.x-this.currentX_),y:this.currentY_+2/3*(ah.y-this.currentY_)};var Z={x:aj.x+(ag.x-this.currentX_)/3,y:aj.y+(ag.y-this.currentY_)/3};K(this,aj,Z,ag)};q.arc=function(al,aj,ak,ag,j,m){ak*=d;var ap=m?"at":"wa";var am=al+A(ag)*ak-f;var ao=aj+l(ag)*ak-f;var i=al+A(j)*ak-f;var an=aj+l(j)*ak-f;if(am==i&&!m){am+=0.125}var Z=V(this,al,aj);var ai=V(this,am,ao);var ah=V(this,i,an);this.currentPath_.push({type:ap,x:Z.x,y:Z.y,radius:ak,xStart:ai.x,yStart:ai.y,xEnd:ah.x,yEnd:ah.y})};q.rect=function(m,j,i,p){this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath()};q.strokeRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.stroke();this.currentPath_=Z};q.fillRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.fill();this.currentPath_=Z};q.createLinearGradient=function(j,p,i,m){var Z=new U("gradient");Z.x0_=j;Z.y0_=p;Z.x1_=i;Z.y1_=m;return Z};q.createRadialGradient=function(p,ag,m,j,Z,i){var ah=new U("gradientradial");ah.x0_=p;ah.y0_=ag;ah.r0_=m;ah.x1_=j;ah.y1_=Z;ah.r1_=i;return ah};q.drawImage=function(aq,m){var aj,ah,al,ay,ao,am,at,aA;var ak=aq.runtimeStyle.width;var ap=aq.runtimeStyle.height;aq.runtimeStyle.width="auto";aq.runtimeStyle.height="auto";var ai=aq.width;var aw=aq.height;aq.runtimeStyle.width=ak;aq.runtimeStyle.height=ap;if(arguments.length==3){aj=arguments[1];ah=arguments[2];ao=am=0;at=al=ai;aA=ay=aw}else{if(arguments.length==5){aj=arguments[1];ah=arguments[2];al=arguments[3];ay=arguments[4];ao=am=0;at=ai;aA=aw}else{if(arguments.length==9){ao=arguments[1];am=arguments[2];at=arguments[3];aA=arguments[4];aj=arguments[5];ah=arguments[6];al=arguments[7];ay=arguments[8]}else{throw Error("Invalid number of arguments")}}}var az=V(this,aj,ah);var p=at/2;var j=aA/2;var ax=[];var i=10;var ag=10;ax.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",ax.join(""))};q.stroke=function(al){var aj=[];var Z=false;var m=10;var am=10;aj.push("ak.x){ak.x=j.x}if(ag.y==null||j.yak.y){ak.y=j.y}}}aj.push(' ">');if(!al){w(this,aj)}else{G(this,aj,ag,ak)}aj.push("");this.element_.insertAdjacentHTML("beforeEnd",aj.join(""))};function w(m,ag){var j=F(m.strokeStyle);var p=j.color;var Z=j.alpha*m.globalAlpha;var i=m.lineScale_*m.lineWidth;if(i<1){Z*=i}ag.push("')}function G(aq,ai,aK,ar){var aj=aq.fillStyle;var aB=aq.arcScaleX_;var aA=aq.arcScaleY_;var j=ar.x-aK.x;var p=ar.y-aK.y;if(aj instanceof U){var an=0;var aF={x:0,y:0};var ax=0;var am=1;if(aj.type_=="gradient"){var al=aj.x0_/aB;var m=aj.y0_/aA;var ak=aj.x1_/aB;var aM=aj.y1_/aA;var aJ=V(aq,al,m);var aI=V(aq,ak,aM);var ag=aI.x-aJ.x;var Z=aI.y-aJ.y;an=Math.atan2(ag,Z)*180/Math.PI;if(an<0){an+=360}if(an<0.000001){an=0}}else{var aJ=V(aq,aj.x0_,aj.y0_);aF={x:(aJ.x-aK.x)/j,y:(aJ.y-aK.y)/p};j/=aB*d;p/=aA*d;var aD=ab.max(j,p);ax=2*aj.r0_/aD;am=2*aj.r1_/aD-ax}var av=aj.colors_;av.sort(function(aN,i){return aN.offset-i.offset});var ap=av.length;var au=av[0].color;var at=av[ap-1].color;var az=av[0].alpha*aq.globalAlpha;var ay=av[ap-1].alpha*aq.globalAlpha;var aE=[];for(var aH=0;aH')}else{if(aj instanceof T){if(j&&p){var ah=-aK.x;var aC=-aK.y;ai.push("')}}else{var aL=F(aq.fillStyle);var aw=aL.color;var aG=aL.alpha*aq.globalAlpha;ai.push('')}}}q.fill=function(){this.stroke(true)};q.closePath=function(){this.currentPath_.push({type:"close"})};function V(j,Z,p){var i=j.m_;return{x:d*(Z*i[0][0]+p*i[1][0]+i[2][0])-f,y:d*(Z*i[0][1]+p*i[1][1]+i[2][1])-f}}q.save=function(){var i={};v(this,i);this.aStack_.push(i);this.mStack_.push(this.m_);this.m_=J(B(),this.m_)};q.restore=function(){if(this.aStack_.length){v(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function h(i){return isFinite(i[0][0])&&isFinite(i[0][1])&&isFinite(i[1][0])&&isFinite(i[1][1])&&isFinite(i[2][0])&&isFinite(i[2][1])}function aa(j,i,p){if(!h(i)){return}j.m_=i;if(p){var Z=i[0][0]*i[1][1]-i[0][1]*i[1][0];j.lineScale_=N(H(Z))}}q.translate=function(m,j){var i=[[1,0,0],[0,1,0],[m,j,1]];aa(this,J(i,this.m_),false)};q.rotate=function(j){var p=A(j);var m=l(j);var i=[[p,m,0],[-m,p,0],[0,0,1]];aa(this,J(i,this.m_),false)};q.scale=function(m,j){this.arcScaleX_*=m;this.arcScaleY_*=j;var i=[[m,0,0],[0,j,0],[0,0,1]];aa(this,J(i,this.m_),true)};q.transform=function(Z,p,ah,ag,j,i){var m=[[Z,p,0],[ah,ag,0],[j,i,1]];aa(this,J(m,this.m_),true)};q.setTransform=function(ag,Z,ai,ah,p,j){var i=[[ag,Z,0],[ai,ah,0],[p,j,1]];aa(this,i,true)};q.drawText_=function(am,ak,aj,ap,ai){var ao=this.m_,at=1000,j=0,ar=at,ah={x:0,y:0},ag=[];var i=u(E(this.font),this.element_);var p=ac(i);var au=this.element_.currentStyle;var Z=this.textAlign.toLowerCase();switch(Z){case"left":case"center":case"right":break;case"end":Z=au.direction=="ltr"?"right":"left";break;case"start":Z=au.direction=="rtl"?"right":"left";break;default:Z="left"}switch(this.textBaseline){case"hanging":case"top":ah.y=i.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":ah.y=-i.size/2.25;break}switch(Z){case"right":j=at;ar=0.05;break;case"center":j=ar=at/2;break}var aq=V(this,ak+ah.x,aj+ah.y);ag.push('');if(ai){w(this,ag)}else{G(this,ag,{x:-j,y:0},{x:ar,y:i.size})}var an=ao[0][0].toFixed(3)+","+ao[1][0].toFixed(3)+","+ao[0][1].toFixed(3)+","+ao[1][1].toFixed(3)+",0,0";var al=n(aq.x/d+1-ao[0][0])+","+n(aq.y/d-2*ao[1][0]);ag.push('','','');this.element_.insertAdjacentHTML("beforeEnd",ag.join(""))};q.fillText=function(m,i,p,j){this.drawText_(m,i,p,j,false)};q.strokeText=function(m,i,p,j){this.drawText_(m,i,p,j,true)};q.measureText=function(m){if(!this.textMeasureEl_){var i='';this.element_.insertAdjacentHTML("beforeEnd",i);this.textMeasureEl_=this.element_.lastChild}var j=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(j.createTextNode(m));return{width:this.textMeasureEl_.offsetWidth}};q.clip=function(){};q.arcTo=function(){};q.createPattern=function(j,i){return new T(j,i)};function U(i){this.type_=i;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}U.prototype.addColorStop=function(j,i){i=F(i);this.colors_.push({offset:j,color:i.color,alpha:i.alpha})};function T(j,i){Q(j);switch(i){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=i;break;default:O("SYNTAX_ERR")}this.src_=j.src;this.width_=j.width;this.height_=j.height}function O(i){throw new P(i)}function Q(i){if(!i||i.nodeType!=1||i.tagName!="IMG"){O("TYPE_MISMATCH_ERR")}if(i.readyState!="complete"){O("INVALID_STATE_ERR")}}function P(i){this.code=this[i];this.message=i+": DOM Exception "+this.code}var X=P.prototype=new Error;X.INDEX_SIZE_ERR=1;X.DOMSTRING_SIZE_ERR=2;X.HIERARCHY_REQUEST_ERR=3;X.WRONG_DOCUMENT_ERR=4;X.INVALID_CHARACTER_ERR=5;X.NO_DATA_ALLOWED_ERR=6;X.NO_MODIFICATION_ALLOWED_ERR=7;X.NOT_FOUND_ERR=8;X.NOT_SUPPORTED_ERR=9;X.INUSE_ATTRIBUTE_ERR=10;X.INVALID_STATE_ERR=11;X.SYNTAX_ERR=12;X.INVALID_MODIFICATION_ERR=13;X.NAMESPACE_ERR=14;X.INVALID_ACCESS_ERR=15;X.VALIDATION_ERR=16;X.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=e;CanvasRenderingContext2D=D;CanvasGradient=U;CanvasPattern=T;DOMException=P;G_vmlCanvasManager._version=888})()}; \ No newline at end of file diff --git a/codespeed/static/js/jqplot/jqplot.barRenderer.min.js b/codespeed/static/js/jqplot/jqplot.barRenderer.min.js index f53635d5..93d7a45c 100644 --- a/codespeed/static/js/jqplot/jqplot.barRenderer.min.js +++ b/codespeed/static/js/jqplot/jqplot.barRenderer.min.js @@ -1,14 +1,3 @@ -/** - * Copyright (c) 2009 Chris Leonello - * jqPlot is currently available for use in all personal or commercial projects - * under both the MIT and GPL version 2.0 licenses. This means that you can - * choose the license that best suits your project and use it accordingly. - * - * Although not required, the author would appreciate an email letting him - * know of any substantial use of jqPlot. You can reach the author at: - * chris dot leonello at gmail dot com or see http://www.jqplot.com/info.php . - * - * If you are feeling kind and generous, consider supporting the project by - * making a donation at: http://www.jqplot.com/donate.php . - */ -(function(b){b.jqplot.BarRenderer=function(){b.jqplot.LineRenderer.call(this)};b.jqplot.BarRenderer.prototype=new b.jqplot.LineRenderer();b.jqplot.BarRenderer.prototype.constructor=b.jqplot.BarRenderer;b.jqplot.BarRenderer.prototype.init=function(d){this.barPadding=8;this.barMargin=10;this.barDirection="vertical";this.barWidth=null;this.shadowOffset=2;this.shadowDepth=5;this.shadowAlpha=0.08;this.waterfall=false;this.varyBarColor=false;b.extend(true,this,d);this.fill=true;if(this.waterfall){this.fillToZero=false;this.disableStack=true}if(this.barDirection=="vertical"){this._primaryAxis="_xaxis";this._stackAxis="y";this.fillAxis="y"}else{this._primaryAxis="_yaxis";this._stackAxis="x";this.fillAxis="x"}var e={lineJoin:"miter",lineCap:"round",fill:true,isarc:false,strokeStyle:this.color,fillStyle:this.color,closePath:this.fill};this.renderer.shapeRenderer.init(e);var c={lineJoin:"miter",lineCap:"round",fill:true,isarc:false,angle:this.shadowAngle,offset:this.shadowOffset,alpha:this.shadowAlpha,depth:this.shadowDepth,closePath:this.fill};this.renderer.shadowRenderer.init(c)};function a(h,g,f,c){if(this.rendererOptions.barDirection=="horizontal"){this._stackAxis="x";this._primaryAxis="_yaxis"}if(this.rendererOptions.waterfall==true){this._data=b.extend(true,[],this.data);var e=0;var j=(!this.rendererOptions.barDirection||this.rendererOptions.barDirection=="vertical")?1:0;for(var d=0;d0){this.data[d][j]+=this.data[d-1][j]}}this.data[this.data.length]=(j==1)?[this.data.length+1,e]:[e,this.data.length+1];this._data[this._data.length]=(j==1)?[this._data.length+1,e]:[e,this._data.length+1]}}b.jqplot.preSeriesInitHooks.push(a);b.jqplot.BarRenderer.prototype.calcSeriesNumbers=function(){var g=0;var h=0;var f=this[this._primaryAxis];var e,d,j;for(var c=0;c0&&u0&&u0){this.data[q][u]+=this.data[q-1][u]}}this.data[this.data.length]=(u==1)?[this.data.length+1,s]:[s,this.data.length+1];this._data[this._data.length]=(u==1)?[this._data.length+1,s]:[s,this._data.length+1]}if(this.rendererOptions.groups>1){this.breakOnNull=true;var n=this.data.length;var v=parseInt(n/this.rendererOptions.groups,10);var r=0;for(var q=v;q570)?n[p]*0.8:n[p]+0.3*(255-n[p]);n[p]=parseInt(n[p],10)}q.push("rgb("+n[0]+","+n[1]+","+n[2]+")")}return q}function i(v,u,s,t,o){var q=v,w=v-1,n,p,r=(o==="x")?0:1;if(q>0){p=t.series[w]._plotData[u][r];if((s*p)<0){n=i(w,u,s,t,o)}else{n=t.series[w].gridData[u][r]}}else{n=(r===0)?t.series[q]._xaxis.series_u2p(0):t.series[q]._yaxis.series_u2p(0)}return n}d.jqplot.BarRenderer.prototype.draw=function(E,L,q,G){var I;var A=d.extend({},q);var w=(A.shadow!=undefined)?A.shadow:this.shadow;var O=(A.showLine!=undefined)?A.showLine:this.showLine;var F=(A.fill!=undefined)?A.fill:this.fill;var p=this.xaxis;var J=this.yaxis;var y=this._xaxis.series_u2p;var K=this._yaxis.series_u2p;var D,C;this._dataColors=[];this._barPoints=[];if(this.barWidth==null){this.renderer.setBarWidth.call(this)}var N=this._plotSeriesInfo=this.renderer.calcSeriesNumbers.call(this);var x=N[0];var v=N[1];var s=N[2];var H=[];if(this._stack){this._barNudge=0}else{this._barNudge=(-Math.abs(v/2-0.5)+s)*(this.barWidth+this.barPadding)}if(O){var u=new d.jqplot.ColorGenerator(this.negativeSeriesColors);var B=new d.jqplot.ColorGenerator(this.seriesColors);var M=u.get(this.index);if(!this.useNegativeColors){M=A.fillStyle}var t=A.fillStyle;var r;var P;var o;if(this.barDirection=="vertical"){for(var I=0;I0&&I=0){o=this._yaxis.series_u2p(0)}else{if(this._yaxis.min>0){o=E.canvas.height}else{o=0}}}else{if(this.waterfall&&I==this.gridData.length-1){if(this._yaxis.min<=0&&this._yaxis.max>=0){o=this._yaxis.series_u2p(0)}else{if(this._yaxis.min>0){o=E.canvas.height}else{o=0}}}else{o=E.canvas.height}}}}}if((this.fillToZero&&this._plotData[I][1]<0)||(this.waterfall&&this._data[I][1]<0)){if(this.varyBarColor&&!this._stack){if(this.useNegativeColors){A.fillStyle=u.next()}else{A.fillStyle=B.next()}}else{A.fillStyle=M}}else{if(this.varyBarColor&&!this._stack){A.fillStyle=B.next()}else{A.fillStyle=t}}if(!this.fillToZero||this._plotData[I][1]>=0){H.push([r-this.barWidth/2,o]);H.push([r-this.barWidth/2,L[I][1]]);H.push([r+this.barWidth/2,L[I][1]]);H.push([r+this.barWidth/2,o])}else{H.push([r-this.barWidth/2,L[I][1]]);H.push([r-this.barWidth/2,o]);H.push([r+this.barWidth/2,o]);H.push([r+this.barWidth/2,L[I][1]])}this._barPoints.push(H);if(w&&!this._stack){var z=d.extend(true,{},A);delete z.fillStyle;this.renderer.shadowRenderer.draw(E,H,z)}var n=A.fillStyle||this.color;this._dataColors.push(n);this.renderer.shapeRenderer.draw(E,H,A)}}else{if(this.barDirection=="horizontal"){for(var I=0;I0&&I=0){P=this._xaxis.series_u2p(0)}else{if(this._xaxis.min>0){P=0}else{P=0}}}else{if(this.waterfall&&I==this.gridData.length-1){if(this._xaxis.min<=0&&this._xaxis.max>=0){P=this._xaxis.series_u2p(0)}else{if(this._xaxis.min>0){P=0}else{P=E.canvas.width}}}else{P=0}}}}}if((this.fillToZero&&this._plotData[I][0]<0)||(this.waterfall&&this._data[I][0]<0)){if(this.varyBarColor&&!this._stack){if(this.useNegativeColors){A.fillStyle=u.next()}else{A.fillStyle=B.next()}}else{A.fillStyle=M}}else{if(this.varyBarColor&&!this._stack){A.fillStyle=B.next()}else{A.fillStyle=t}}if(!this.fillToZero||this._plotData[I][0]>=0){H.push([P,r+this.barWidth/2]);H.push([P,r-this.barWidth/2]);H.push([L[I][0],r-this.barWidth/2]);H.push([L[I][0],r+this.barWidth/2])}else{H.push([L[I][0],r+this.barWidth/2]);H.push([L[I][0],r-this.barWidth/2]);H.push([P,r-this.barWidth/2]);H.push([P,r+this.barWidth/2])}this._barPoints.push(H);if(w&&!this._stack){var z=d.extend(true,{},A);delete z.fillStyle;this.renderer.shadowRenderer.draw(E,H,z)}var n=A.fillStyle||this.color;this._dataColors.push(n);this.renderer.shapeRenderer.draw(E,H,A)}}}}if(this.highlightColors.length==0){this.highlightColors=d.jqplot.computeHighlightColors(this._dataColors)}else{if(typeof(this.highlightColors)=="string"){var N=this.highlightColors;this.highlightColors=[];for(var I=0;I528||(d[0]==528&&d[1]>=16)){this._textRenderer=new a.jqplot.CanvasFontRenderer(e)}}else{if(a.browser.mozilla){var d=a.browser.version.split(".");if(d[0]>1||(d[0]==1&&d[1]>=9&&d[2]>0)){this._textRenderer=new a.jqplot.CanvasFontRenderer(e)}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(e)}}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(e)}}}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(e)}};a.jqplot.CanvasAxisLabelRenderer.prototype.init=function(b){a.extend(true,this,b);this._textRenderer.init({fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily})};a.jqplot.CanvasAxisLabelRenderer.prototype.getWidth=function(d){if(this._elem){return this._elem.outerWidth(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.sin(f.angle)*e)+Math.abs(Math.cos(f.angle)*c);return b}};a.jqplot.CanvasAxisLabelRenderer.prototype.getHeight=function(d){if(this._elem){return this._elem.outerHeight(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.cos(f.angle)*e)+Math.abs(Math.sin(f.angle)*c);return b}};a.jqplot.CanvasAxisLabelRenderer.prototype.getAngleRad=function(){var b=this.angle*Math.PI/180;return b};a.jqplot.CanvasAxisLabelRenderer.prototype.draw=function(c){var e=document.createElement("canvas");this._textRenderer.setText(this.label,c);var b=this.getWidth(c);var d=this.getHeight(c);e.width=b;e.height=d;e.style.width=b;e.style.height=d;e.style.position="absolute";this._domelem=e;this._elem=a(e);this._elem.addClass("jqplot-"+this.axis+"-label");return this._elem};a.jqplot.CanvasAxisLabelRenderer.prototype.pack=function(){if(a.browser.msie){window.G_vmlCanvasManager.init_(document);this._domelem=window.G_vmlCanvasManager.initElement(this._domelem)}var b=this._elem.get(0).getContext("2d");this._textRenderer.draw(b,this.label)}})(jQuery); \ No newline at end of file +/* jqPlot 1.0.8r1250 | (c) 2009-2013 Chris Leonello | jplot.com + jsDate | (c) 2010-2013 Chris Leonello + */(function(a){a.jqplot.CanvasAxisLabelRenderer=function(b){this.angle=0;this.axis;this.show=true;this.showLabel=true;this.label="";this.fontFamily='"Trebuchet MS", Arial, Helvetica, sans-serif';this.fontSize="11pt";this.fontWeight="normal";this.fontStretch=1;this.textColor="#666666";this.enableFontSupport=true;this.pt2px=null;this._elem;this._ctx;this._plotWidth;this._plotHeight;this._plotDimensions={height:null,width:null};a.extend(true,this,b);if(b.angle==null&&this.axis!="xaxis"&&this.axis!="x2axis"){this.angle=-90}var c={fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily};if(this.pt2px){c.pt2px=this.pt2px}if(this.enableFontSupport){if(a.jqplot.support_canvas_text()){this._textRenderer=new a.jqplot.CanvasFontRenderer(c)}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(c)}}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(c)}};a.jqplot.CanvasAxisLabelRenderer.prototype.init=function(b){a.extend(true,this,b);this._textRenderer.init({fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily})};a.jqplot.CanvasAxisLabelRenderer.prototype.getWidth=function(d){if(this._elem){return this._elem.outerWidth(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.sin(f.angle)*e)+Math.abs(Math.cos(f.angle)*c);return b}};a.jqplot.CanvasAxisLabelRenderer.prototype.getHeight=function(d){if(this._elem){return this._elem.outerHeight(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.cos(f.angle)*e)+Math.abs(Math.sin(f.angle)*c);return b}};a.jqplot.CanvasAxisLabelRenderer.prototype.getAngleRad=function(){var b=this.angle*Math.PI/180;return b};a.jqplot.CanvasAxisLabelRenderer.prototype.draw=function(c,f){if(this._elem){if(a.jqplot.use_excanvas&&window.G_vmlCanvasManager.uninitElement!==undefined){window.G_vmlCanvasManager.uninitElement(this._elem.get(0))}this._elem.emptyForce();this._elem=null}var e=f.canvasManager.getCanvas();this._textRenderer.setText(this.label,c);var b=this.getWidth(c);var d=this.getHeight(c);e.width=b;e.height=d;e.style.width=b;e.style.height=d;e=f.canvasManager.initCanvas(e);this._elem=a(e);this._elem.css({position:"absolute"});this._elem.addClass("jqplot-"+this.axis+"-label");e=null;return this._elem};a.jqplot.CanvasAxisLabelRenderer.prototype.pack=function(){this._textRenderer.draw(this._elem.get(0).getContext("2d"),this.label)}})(jQuery); \ No newline at end of file diff --git a/codespeed/static/js/jqplot/jqplot.canvasAxisTickRenderer.min.js b/codespeed/static/js/jqplot/jqplot.canvasAxisTickRenderer.min.js index ea02683b..19875be4 100644 --- a/codespeed/static/js/jqplot/jqplot.canvasAxisTickRenderer.min.js +++ b/codespeed/static/js/jqplot/jqplot.canvasAxisTickRenderer.min.js @@ -1,14 +1,3 @@ -/** - * Copyright (c) 2009 Chris Leonello - * jqPlot is currently available for use in all personal or commercial projects - * under both the MIT and GPL version 2.0 licenses. This means that you can - * choose the license that best suits your project and use it accordingly. - * - * Although not required, the author would appreciate an email letting him - * know of any substantial use of jqPlot. You can reach the author at: - * chris dot leonello at gmail dot com or see http://www.jqplot.com/info.php . - * - * If you are feeling kind and generous, consider supporting the project by - * making a donation at: http://www.jqplot.com/donate.php . - */ -(function(a){a.jqplot.CanvasAxisTickRenderer=function(b){this.mark="outside";this.showMark=true;this.showGridline=true;this.isMinorTick=false;this.angle=0;this.markSize=4;this.show=true;this.showLabel=true;this.labelPosition="auto";this.label="";this.value=null;this._styles={};this.formatter=a.jqplot.DefaultTickFormatter;this.formatString="";this.fontFamily='"Trebuchet MS", Arial, Helvetica, sans-serif';this.fontSize="11px";this.fontWeight="normal";this.fontStretch=1;this.textColor="#666666";this.enableFontSupport=false;this.pt2px=null;this._elem;this._ctx;this._plotWidth;this._plotHeight;this._plotDimensions={height:null,width:null};a.extend(true,this,b);var e={fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily};if(this.pt2px){e.pt2px=this.pt2px}if(this.enableFontSupport){if(a.browser.safari){var d=a.browser.version.split(".");for(var c=0;c528||(d[0]==528&&d[1]>=16)){this._textRenderer=new a.jqplot.CanvasFontRenderer(e)}}else{if(a.browser.mozilla){var d=a.browser.version.split(".");if(d[0]>1||(d[0]==1&&d[1]>=9&&d[2]>0)){this._textRenderer=new a.jqplot.CanvasFontRenderer(e)}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(e)}}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(e)}}}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(e)}};a.jqplot.CanvasAxisTickRenderer.prototype.init=function(b){a.extend(true,this,b);this._textRenderer.init({fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily})};a.jqplot.CanvasAxisTickRenderer.prototype.getWidth=function(d){if(this._elem){return this._elem.outerWidth(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.sin(f.angle)*e)+Math.abs(Math.cos(f.angle)*c);return b}};a.jqplot.CanvasAxisTickRenderer.prototype.getHeight=function(d){if(this._elem){return this._elem.outerHeight(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.cos(f.angle)*e)+Math.abs(Math.sin(f.angle)*c);return b}};a.jqplot.CanvasAxisTickRenderer.prototype.getAngleRad=function(){var b=this.angle*Math.PI/180;return b};a.jqplot.CanvasAxisTickRenderer.prototype.setTick=function(b,d,c){this.value=b;if(c){this.isMinorTick=true}return this};a.jqplot.CanvasAxisTickRenderer.prototype.draw=function(c){if(!this.label){this.label=this.formatter(this.formatString,this.value)}var e=document.createElement("canvas");this._textRenderer.setText(this.label,c);var b=this.getWidth(c);var d=this.getHeight(c);e.width=b;e.height=d;e.style.width=b;e.style.height=d;e.style.textAlign="left";e.style.position="absolute";this._domelem=e;this._elem=a(e);this._elem.css(this._styles);this._elem.addClass("jqplot-"+this.axis+"-tick");return this._elem};a.jqplot.CanvasAxisTickRenderer.prototype.pack=function(){if(a.browser.msie){window.G_vmlCanvasManager.init_(document);this._domelem=window.G_vmlCanvasManager.initElement(this._domelem)}var b=this._elem.get(0).getContext("2d");this._textRenderer.draw(b,this.label)}})(jQuery); \ No newline at end of file +/* jqPlot 1.0.8r1250 | (c) 2009-2013 Chris Leonello | jplot.com + jsDate | (c) 2010-2013 Chris Leonello + */(function(a){a.jqplot.CanvasAxisTickRenderer=function(b){this.mark="outside";this.showMark=true;this.showGridline=true;this.isMinorTick=false;this.angle=0;this.markSize=4;this.show=true;this.showLabel=true;this.labelPosition="auto";this.label="";this.value=null;this._styles={};this.formatter=a.jqplot.DefaultTickFormatter;this.formatString="";this.prefix="";this.fontFamily='"Trebuchet MS", Arial, Helvetica, sans-serif';this.fontSize="10pt";this.fontWeight="normal";this.fontStretch=1;this.textColor="#666666";this.enableFontSupport=true;this.pt2px=null;this._elem;this._ctx;this._plotWidth;this._plotHeight;this._plotDimensions={height:null,width:null};a.extend(true,this,b);var c={fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily};if(this.pt2px){c.pt2px=this.pt2px}if(this.enableFontSupport){if(a.jqplot.support_canvas_text()){this._textRenderer=new a.jqplot.CanvasFontRenderer(c)}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(c)}}else{this._textRenderer=new a.jqplot.CanvasTextRenderer(c)}};a.jqplot.CanvasAxisTickRenderer.prototype.init=function(b){a.extend(true,this,b);this._textRenderer.init({fontSize:this.fontSize,fontWeight:this.fontWeight,fontStretch:this.fontStretch,fillStyle:this.textColor,angle:this.getAngleRad(),fontFamily:this.fontFamily})};a.jqplot.CanvasAxisTickRenderer.prototype.getWidth=function(d){if(this._elem){return this._elem.outerWidth(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.sin(f.angle)*e)+Math.abs(Math.cos(f.angle)*c);return b}};a.jqplot.CanvasAxisTickRenderer.prototype.getHeight=function(d){if(this._elem){return this._elem.outerHeight(true)}else{var f=this._textRenderer;var c=f.getWidth(d);var e=f.getHeight(d);var b=Math.abs(Math.cos(f.angle)*e)+Math.abs(Math.sin(f.angle)*c);return b}};a.jqplot.CanvasAxisTickRenderer.prototype.getTop=function(b){if(this._elem){return this._elem.position().top}else{return null}};a.jqplot.CanvasAxisTickRenderer.prototype.getAngleRad=function(){var b=this.angle*Math.PI/180;return b};a.jqplot.CanvasAxisTickRenderer.prototype.setTick=function(b,d,c){this.value=b;if(c){this.isMinorTick=true}return this};a.jqplot.CanvasAxisTickRenderer.prototype.draw=function(c,f){if(!this.label){this.label=this.prefix+this.formatter(this.formatString,this.value)}if(this._elem){if(a.jqplot.use_excanvas&&window.G_vmlCanvasManager.uninitElement!==undefined){window.G_vmlCanvasManager.uninitElement(this._elem.get(0))}this._elem.emptyForce();this._elem=null}var e=f.canvasManager.getCanvas();this._textRenderer.setText(this.label,c);var b=this.getWidth(c);var d=this.getHeight(c);e.width=b;e.height=d;e.style.width=b;e.style.height=d;e.style.textAlign="left";e.style.position="absolute";e=f.canvasManager.initCanvas(e);this._elem=a(e);this._elem.css(this._styles);this._elem.addClass("jqplot-"+this.axis+"-tick");e=null;return this._elem};a.jqplot.CanvasAxisTickRenderer.prototype.pack=function(){this._textRenderer.draw(this._elem.get(0).getContext("2d"),this.label)}})(jQuery); \ No newline at end of file diff --git a/codespeed/static/js/jqplot/jqplot.canvasTextRenderer.min.js b/codespeed/static/js/jqplot/jqplot.canvasTextRenderer.min.js index 8e10d118..90199dd1 100644 --- a/codespeed/static/js/jqplot/jqplot.canvasTextRenderer.min.js +++ b/codespeed/static/js/jqplot/jqplot.canvasTextRenderer.min.js @@ -1,14 +1,3 @@ -/** - * Copyright (c) 2009 Chris Leonello - * jqPlot is currently available for use in all personal or commercial projects - * under both the MIT and GPL version 2.0 licenses. This means that you can - * choose the license that best suits your project and use it accordingly. - * - * Although not required, the author would appreciate an email letting him - * know of any substantial use of jqPlot. You can reach the author at: - * chris dot leonello at gmail dot com or see http://www.jqplot.com/info.php . - * - * If you are feeling kind and generous, consider supporting the project by - * making a donation at: http://www.jqplot.com/donate.php . - */ -(function(a){a.jqplot.CanvasTextRenderer=function(b){this.fontStyle="normal";this.fontVariant="normal";this.fontWeight="normal";this.fontSize="10px";this.fontFamily="sans-serif";this.fontStretch=1;this.fillStyle="#666666";this.angle=0;this.textAlign="start";this.textBaseline="alphabetic";this.text;this.width;this.height;this.pt2px=1.28;a.extend(true,this,b);this.normalizedFontSize=this.normalizeFontSize(this.fontSize);this.setHeight()};a.jqplot.CanvasTextRenderer.prototype.init=function(b){a.extend(true,this,b);this.normalizedFontSize=this.normalizeFontSize(this.fontSize);this.setHeight()};a.jqplot.CanvasTextRenderer.prototype.normalizeFontSize=function(b){b=String(b);n=parseFloat(b);if(b.indexOf("px")>-1){return n/this.pt2px}else{if(b.indexOf("pt")>-1){return n}else{if(b.indexOf("em")>-1){return n*12}else{if(b.indexOf("%")>-1){return n*12/100}else{return n/this.pt2px}}}}};a.jqplot.CanvasTextRenderer.prototype.fontWeight2Float=function(b){if(Number(b)){return b/400}else{switch(b){case"normal":return 1;break;case"bold":return 1.75;break;case"bolder":return 2.25;break;case"lighter":return 0.75;break;default:return 1;break}}};a.jqplot.CanvasTextRenderer.prototype.getText=function(){return this.text};a.jqplot.CanvasTextRenderer.prototype.setText=function(c,b){this.text=c;this.setWidth(b);return this};a.jqplot.CanvasTextRenderer.prototype.getWidth=function(b){return this.width};a.jqplot.CanvasTextRenderer.prototype.setWidth=function(c,b){if(!b){this.width=this.measure(c,this.text)}else{this.width=b}return this};a.jqplot.CanvasTextRenderer.prototype.getHeight=function(b){return this.height};a.jqplot.CanvasTextRenderer.prototype.setHeight=function(b){if(!b){this.height=this.normalizedFontSize*this.pt2px}else{this.height=b}return this};a.jqplot.CanvasTextRenderer.prototype.letter=function(b){return this.letters[b]};a.jqplot.CanvasTextRenderer.prototype.ascent=function(){return this.normalizedFontSize};a.jqplot.CanvasTextRenderer.prototype.descent=function(){return 7*this.normalizedFontSize/25};a.jqplot.CanvasTextRenderer.prototype.measure=function(d,f){var e=0;var b=f.length;for(i=0;i30)?2:2+(30-this.normalizedFontSize)/20;t.lineWidth=u*k*this.fontWeight2Float(this.fontWeight);for(var g=0;g":{width:24,points:[[4,18],[20,9],[4,0]]},"?":{width:18,points:[[3,16],[3,17],[4,19],[5,20],[7,21],[11,21],[13,20],[14,19],[15,17],[15,15],[14,13],[13,12],[9,10],[9,7],[-1,-1],[9,2],[8,1],[9,0],[10,1],[9,2]]},"@":{width:27,points:[[18,13],[17,15],[15,16],[12,16],[10,15],[9,14],[8,11],[8,8],[9,6],[11,5],[14,5],[16,6],[17,8],[-1,-1],[12,16],[10,14],[9,11],[9,8],[10,6],[11,5],[-1,-1],[18,16],[17,8],[17,6],[19,5],[21,5],[23,7],[24,10],[24,12],[23,15],[22,17],[20,19],[18,20],[15,21],[12,21],[9,20],[7,19],[5,17],[4,15],[3,12],[3,9],[4,6],[5,4],[7,2],[9,1],[12,0],[15,0],[18,1],[20,2],[21,3],[-1,-1],[19,16],[18,8],[18,6],[19,5]]},A:{width:18,points:[[9,21],[1,0],[-1,-1],[9,21],[17,0],[-1,-1],[4,7],[14,7]]},B:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[-1,-1],[4,11],[13,11],[16,10],[17,9],[18,7],[18,4],[17,2],[16,1],[13,0],[4,0]]},C:{width:21,points:[[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5]]},D:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[11,21],[14,20],[16,18],[17,16],[18,13],[18,8],[17,5],[16,3],[14,1],[11,0],[4,0]]},E:{width:19,points:[[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11],[-1,-1],[4,0],[17,0]]},F:{width:18,points:[[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11]]},G:{width:21,points:[[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[18,8],[-1,-1],[13,8],[18,8]]},H:{width:22,points:[[4,21],[4,0],[-1,-1],[18,21],[18,0],[-1,-1],[4,11],[18,11]]},I:{width:8,points:[[4,21],[4,0]]},J:{width:16,points:[[12,21],[12,5],[11,2],[10,1],[8,0],[6,0],[4,1],[3,2],[2,5],[2,7]]},K:{width:21,points:[[4,21],[4,0],[-1,-1],[18,21],[4,7],[-1,-1],[9,12],[18,0]]},L:{width:17,points:[[4,21],[4,0],[-1,-1],[4,0],[16,0]]},M:{width:24,points:[[4,21],[4,0],[-1,-1],[4,21],[12,0],[-1,-1],[20,21],[12,0],[-1,-1],[20,21],[20,0]]},N:{width:22,points:[[4,21],[4,0],[-1,-1],[4,21],[18,0],[-1,-1],[18,21],[18,0]]},O:{width:22,points:[[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21]]},P:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,14],[17,12],[16,11],[13,10],[4,10]]},Q:{width:22,points:[[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21],[-1,-1],[12,4],[18,-2]]},R:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[4,11],[-1,-1],[11,11],[18,0]]},S:{width:20,points:[[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]]},T:{width:16,points:[[8,21],[8,0],[-1,-1],[1,21],[15,21]]},U:{width:22,points:[[4,21],[4,6],[5,3],[7,1],[10,0],[12,0],[15,1],[17,3],[18,6],[18,21]]},V:{width:18,points:[[1,21],[9,0],[-1,-1],[17,21],[9,0]]},W:{width:24,points:[[2,21],[7,0],[-1,-1],[12,21],[7,0],[-1,-1],[12,21],[17,0],[-1,-1],[22,21],[17,0]]},X:{width:20,points:[[3,21],[17,0],[-1,-1],[17,21],[3,0]]},Y:{width:18,points:[[1,21],[9,11],[9,0],[-1,-1],[17,21],[9,11]]},Z:{width:20,points:[[17,21],[3,0],[-1,-1],[3,21],[17,21],[-1,-1],[3,0],[17,0]]},"[":{width:14,points:[[4,25],[4,-7],[-1,-1],[5,25],[5,-7],[-1,-1],[4,25],[11,25],[-1,-1],[4,-7],[11,-7]]},"\\":{width:14,points:[[0,21],[14,-3]]},"]":{width:14,points:[[9,25],[9,-7],[-1,-1],[10,25],[10,-7],[-1,-1],[3,25],[10,25],[-1,-1],[3,-7],[10,-7]]},"^":{width:16,points:[[6,15],[8,18],[10,15],[-1,-1],[3,12],[8,17],[13,12],[-1,-1],[8,17],[8,0]]},_:{width:16,points:[[0,-2],[16,-2]]},"`":{width:10,points:[[6,21],[5,20],[4,18],[4,16],[5,15],[6,16],[5,17]]},a:{width:19,points:[[15,14],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},b:{width:19,points:[[4,21],[4,0],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]]},c:{width:18,points:[[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},d:{width:19,points:[[15,21],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},e:{width:18,points:[[3,8],[15,8],[15,10],[14,12],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},f:{width:12,points:[[10,21],[8,21],[6,20],[5,17],[5,0],[-1,-1],[2,14],[9,14]]},g:{width:19,points:[[15,14],[15,-2],[14,-5],[13,-6],[11,-7],[8,-7],[6,-6],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},h:{width:19,points:[[4,21],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]]},i:{width:8,points:[[3,21],[4,20],[5,21],[4,22],[3,21],[-1,-1],[4,14],[4,0]]},j:{width:10,points:[[5,21],[6,20],[7,21],[6,22],[5,21],[-1,-1],[6,14],[6,-3],[5,-6],[3,-7],[1,-7]]},k:{width:17,points:[[4,21],[4,0],[-1,-1],[14,14],[4,4],[-1,-1],[8,8],[15,0]]},l:{width:8,points:[[4,21],[4,0]]},m:{width:30,points:[[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0],[-1,-1],[15,10],[18,13],[20,14],[23,14],[25,13],[26,10],[26,0]]},n:{width:19,points:[[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]]},o:{width:19,points:[[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3],[16,6],[16,8],[15,11],[13,13],[11,14],[8,14]]},p:{width:19,points:[[4,14],[4,-7],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]]},q:{width:19,points:[[15,14],[15,-7],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},r:{width:13,points:[[4,14],[4,0],[-1,-1],[4,8],[5,11],[7,13],[9,14],[12,14]]},s:{width:17,points:[[14,11],[13,13],[10,14],[7,14],[4,13],[3,11],[4,9],[6,8],[11,7],[13,6],[14,4],[14,3],[13,1],[10,0],[7,0],[4,1],[3,3]]},t:{width:12,points:[[5,21],[5,4],[6,1],[8,0],[10,0],[-1,-1],[2,14],[9,14]]},u:{width:19,points:[[4,14],[4,4],[5,1],[7,0],[10,0],[12,1],[15,4],[-1,-1],[15,14],[15,0]]},v:{width:16,points:[[2,14],[8,0],[-1,-1],[14,14],[8,0]]},w:{width:22,points:[[3,14],[7,0],[-1,-1],[11,14],[7,0],[-1,-1],[11,14],[15,0],[-1,-1],[19,14],[15,0]]},x:{width:17,points:[[3,14],[14,0],[-1,-1],[14,14],[3,0]]},y:{width:16,points:[[2,14],[8,0],[-1,-1],[14,14],[8,0],[6,-4],[4,-6],[2,-7],[1,-7]]},z:{width:17,points:[[14,14],[3,0],[-1,-1],[3,14],[14,14],[-1,-1],[3,0],[14,0]]},"{":{width:14,points:[[9,25],[7,24],[6,23],[5,21],[5,19],[6,17],[7,16],[8,14],[8,12],[6,10],[-1,-1],[7,24],[6,22],[6,20],[7,18],[8,17],[9,15],[9,13],[8,11],[4,9],[8,7],[9,5],[9,3],[8,1],[7,0],[6,-2],[6,-4],[7,-6],[-1,-1],[6,8],[8,6],[8,4],[7,2],[6,1],[5,-1],[5,-3],[6,-5],[7,-6],[9,-7]]},"|":{width:8,points:[[4,25],[4,-7]]},"}":{width:14,points:[[5,25],[7,24],[8,23],[9,21],[9,19],[8,17],[7,16],[6,14],[6,12],[8,10],[-1,-1],[7,24],[8,22],[8,20],[7,18],[6,17],[5,15],[5,13],[6,11],[10,9],[6,7],[5,5],[5,3],[6,1],[7,0],[8,-2],[8,-4],[7,-6],[-1,-1],[8,8],[6,6],[6,4],[7,2],[8,1],[9,-1],[9,-3],[8,-5],[7,-6],[5,-7]]},"~":{width:24,points:[[3,6],[3,8],[4,11],[6,12],[8,12],[10,11],[14,8],[16,7],[18,7],[20,8],[21,10],[-1,-1],[3,8],[4,10],[6,11],[8,11],[10,10],[14,7],[16,6],[18,6],[20,7],[21,10],[21,12]]}};a.jqplot.CanvasFontRenderer=function(b){b=b||{};if(!b.pt2px){b.pt2px=1.5}a.jqplot.CanvasTextRenderer.call(this,b)};a.jqplot.CanvasFontRenderer.prototype=new a.jqplot.CanvasTextRenderer({});a.jqplot.CanvasFontRenderer.prototype.constructor=a.jqplot.CanvasFontRenderer;a.jqplot.CanvasFontRenderer.prototype.measure=function(c,e){var d=this.fontSize+" "+this.fontFamily;c.save();c.font=d;var b=c.measureText(e).width;c.restore();return b};a.jqplot.CanvasFontRenderer.prototype.draw=function(e,g){var c=0;var h=this.height*0.72;e.save();var d,b;if((-Math.PI/2<=this.angle&&this.angle<=0)||(Math.PI*3/2<=this.angle&&this.angle<=Math.PI*2)){d=0;b=-Math.sin(this.angle)*this.width}else{if((0-1){return c/this.pt2px}else{if(b.indexOf("pt")>-1){return c}else{if(b.indexOf("em")>-1){return c*12}else{if(b.indexOf("%")>-1){return c*12/100}else{return c/this.pt2px}}}}};a.jqplot.CanvasTextRenderer.prototype.fontWeight2Float=function(b){if(Number(b)){return b/400}else{switch(b){case"normal":return 1;break;case"bold":return 1.75;break;case"bolder":return 2.25;break;case"lighter":return 0.75;break;default:return 1;break}}};a.jqplot.CanvasTextRenderer.prototype.getText=function(){return this.text};a.jqplot.CanvasTextRenderer.prototype.setText=function(c,b){this.text=c;this.setWidth(b);return this};a.jqplot.CanvasTextRenderer.prototype.getWidth=function(b){return this.width};a.jqplot.CanvasTextRenderer.prototype.setWidth=function(c,b){if(!b){this.width=this.measure(c,this.text)}else{this.width=b}return this};a.jqplot.CanvasTextRenderer.prototype.getHeight=function(b){return this.height};a.jqplot.CanvasTextRenderer.prototype.setHeight=function(b){if(!b){this.height=this.normalizedFontSize*this.pt2px}else{this.height=b}return this};a.jqplot.CanvasTextRenderer.prototype.letter=function(b){return this.letters[b]};a.jqplot.CanvasTextRenderer.prototype.ascent=function(){return this.normalizedFontSize};a.jqplot.CanvasTextRenderer.prototype.descent=function(){return 7*this.normalizedFontSize/25};a.jqplot.CanvasTextRenderer.prototype.measure=function(d,g){var f=0;var b=g.length;for(var e=0;e30)?2:2+(30-this.normalizedFontSize)/20;s.lineWidth=t*k*this.fontWeight2Float(this.fontWeight);for(var g=0;g":{width:24,points:[[4,18],[20,9],[4,0]]},"?":{width:18,points:[[3,16],[3,17],[4,19],[5,20],[7,21],[11,21],[13,20],[14,19],[15,17],[15,15],[14,13],[13,12],[9,10],[9,7],[-1,-1],[9,2],[8,1],[9,0],[10,1],[9,2]]},"@":{width:27,points:[[18,13],[17,15],[15,16],[12,16],[10,15],[9,14],[8,11],[8,8],[9,6],[11,5],[14,5],[16,6],[17,8],[-1,-1],[12,16],[10,14],[9,11],[9,8],[10,6],[11,5],[-1,-1],[18,16],[17,8],[17,6],[19,5],[21,5],[23,7],[24,10],[24,12],[23,15],[22,17],[20,19],[18,20],[15,21],[12,21],[9,20],[7,19],[5,17],[4,15],[3,12],[3,9],[4,6],[5,4],[7,2],[9,1],[12,0],[15,0],[18,1],[20,2],[21,3],[-1,-1],[19,16],[18,8],[18,6],[19,5]]},A:{width:18,points:[[9,21],[1,0],[-1,-1],[9,21],[17,0],[-1,-1],[4,7],[14,7]]},B:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[-1,-1],[4,11],[13,11],[16,10],[17,9],[18,7],[18,4],[17,2],[16,1],[13,0],[4,0]]},C:{width:21,points:[[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5]]},D:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[11,21],[14,20],[16,18],[17,16],[18,13],[18,8],[17,5],[16,3],[14,1],[11,0],[4,0]]},E:{width:19,points:[[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11],[-1,-1],[4,0],[17,0]]},F:{width:18,points:[[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11]]},G:{width:21,points:[[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[18,8],[-1,-1],[13,8],[18,8]]},H:{width:22,points:[[4,21],[4,0],[-1,-1],[18,21],[18,0],[-1,-1],[4,11],[18,11]]},I:{width:8,points:[[4,21],[4,0]]},J:{width:16,points:[[12,21],[12,5],[11,2],[10,1],[8,0],[6,0],[4,1],[3,2],[2,5],[2,7]]},K:{width:21,points:[[4,21],[4,0],[-1,-1],[18,21],[4,7],[-1,-1],[9,12],[18,0]]},L:{width:17,points:[[4,21],[4,0],[-1,-1],[4,0],[16,0]]},M:{width:24,points:[[4,21],[4,0],[-1,-1],[4,21],[12,0],[-1,-1],[20,21],[12,0],[-1,-1],[20,21],[20,0]]},N:{width:22,points:[[4,21],[4,0],[-1,-1],[4,21],[18,0],[-1,-1],[18,21],[18,0]]},O:{width:22,points:[[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21]]},P:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,14],[17,12],[16,11],[13,10],[4,10]]},Q:{width:22,points:[[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21],[-1,-1],[12,4],[18,-2]]},R:{width:21,points:[[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[4,11],[-1,-1],[11,11],[18,0]]},S:{width:20,points:[[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]]},T:{width:16,points:[[8,21],[8,0],[-1,-1],[1,21],[15,21]]},U:{width:22,points:[[4,21],[4,6],[5,3],[7,1],[10,0],[12,0],[15,1],[17,3],[18,6],[18,21]]},V:{width:18,points:[[1,21],[9,0],[-1,-1],[17,21],[9,0]]},W:{width:24,points:[[2,21],[7,0],[-1,-1],[12,21],[7,0],[-1,-1],[12,21],[17,0],[-1,-1],[22,21],[17,0]]},X:{width:20,points:[[3,21],[17,0],[-1,-1],[17,21],[3,0]]},Y:{width:18,points:[[1,21],[9,11],[9,0],[-1,-1],[17,21],[9,11]]},Z:{width:20,points:[[17,21],[3,0],[-1,-1],[3,21],[17,21],[-1,-1],[3,0],[17,0]]},"[":{width:14,points:[[4,25],[4,-7],[-1,-1],[5,25],[5,-7],[-1,-1],[4,25],[11,25],[-1,-1],[4,-7],[11,-7]]},"\\":{width:14,points:[[0,21],[14,-3]]},"]":{width:14,points:[[9,25],[9,-7],[-1,-1],[10,25],[10,-7],[-1,-1],[3,25],[10,25],[-1,-1],[3,-7],[10,-7]]},"^":{width:16,points:[[6,15],[8,18],[10,15],[-1,-1],[3,12],[8,17],[13,12],[-1,-1],[8,17],[8,0]]},_:{width:16,points:[[0,-2],[16,-2]]},"`":{width:10,points:[[6,21],[5,20],[4,18],[4,16],[5,15],[6,16],[5,17]]},a:{width:19,points:[[15,14],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},b:{width:19,points:[[4,21],[4,0],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]]},c:{width:18,points:[[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},d:{width:19,points:[[15,21],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},e:{width:18,points:[[3,8],[15,8],[15,10],[14,12],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},f:{width:12,points:[[10,21],[8,21],[6,20],[5,17],[5,0],[-1,-1],[2,14],[9,14]]},g:{width:19,points:[[15,14],[15,-2],[14,-5],[13,-6],[11,-7],[8,-7],[6,-6],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},h:{width:19,points:[[4,21],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]]},i:{width:8,points:[[3,21],[4,20],[5,21],[4,22],[3,21],[-1,-1],[4,14],[4,0]]},j:{width:10,points:[[5,21],[6,20],[7,21],[6,22],[5,21],[-1,-1],[6,14],[6,-3],[5,-6],[3,-7],[1,-7]]},k:{width:17,points:[[4,21],[4,0],[-1,-1],[14,14],[4,4],[-1,-1],[8,8],[15,0]]},l:{width:8,points:[[4,21],[4,0]]},m:{width:30,points:[[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0],[-1,-1],[15,10],[18,13],[20,14],[23,14],[25,13],[26,10],[26,0]]},n:{width:19,points:[[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]]},o:{width:19,points:[[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3],[16,6],[16,8],[15,11],[13,13],[11,14],[8,14]]},p:{width:19,points:[[4,14],[4,-7],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]]},q:{width:19,points:[[15,14],[15,-7],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]]},r:{width:13,points:[[4,14],[4,0],[-1,-1],[4,8],[5,11],[7,13],[9,14],[12,14]]},s:{width:17,points:[[14,11],[13,13],[10,14],[7,14],[4,13],[3,11],[4,9],[6,8],[11,7],[13,6],[14,4],[14,3],[13,1],[10,0],[7,0],[4,1],[3,3]]},t:{width:12,points:[[5,21],[5,4],[6,1],[8,0],[10,0],[-1,-1],[2,14],[9,14]]},u:{width:19,points:[[4,14],[4,4],[5,1],[7,0],[10,0],[12,1],[15,4],[-1,-1],[15,14],[15,0]]},v:{width:16,points:[[2,14],[8,0],[-1,-1],[14,14],[8,0]]},w:{width:22,points:[[3,14],[7,0],[-1,-1],[11,14],[7,0],[-1,-1],[11,14],[15,0],[-1,-1],[19,14],[15,0]]},x:{width:17,points:[[3,14],[14,0],[-1,-1],[14,14],[3,0]]},y:{width:16,points:[[2,14],[8,0],[-1,-1],[14,14],[8,0],[6,-4],[4,-6],[2,-7],[1,-7]]},z:{width:17,points:[[14,14],[3,0],[-1,-1],[3,14],[14,14],[-1,-1],[3,0],[14,0]]},"{":{width:14,points:[[9,25],[7,24],[6,23],[5,21],[5,19],[6,17],[7,16],[8,14],[8,12],[6,10],[-1,-1],[7,24],[6,22],[6,20],[7,18],[8,17],[9,15],[9,13],[8,11],[4,9],[8,7],[9,5],[9,3],[8,1],[7,0],[6,-2],[6,-4],[7,-6],[-1,-1],[6,8],[8,6],[8,4],[7,2],[6,1],[5,-1],[5,-3],[6,-5],[7,-6],[9,-7]]},"|":{width:8,points:[[4,25],[4,-7]]},"}":{width:14,points:[[5,25],[7,24],[8,23],[9,21],[9,19],[8,17],[7,16],[6,14],[6,12],[8,10],[-1,-1],[7,24],[8,22],[8,20],[7,18],[6,17],[5,15],[5,13],[6,11],[10,9],[6,7],[5,5],[5,3],[6,1],[7,0],[8,-2],[8,-4],[7,-6],[-1,-1],[8,8],[6,6],[6,4],[7,2],[8,1],[9,-1],[9,-3],[8,-5],[7,-6],[5,-7]]},"~":{width:24,points:[[3,6],[3,8],[4,11],[6,12],[8,12],[10,11],[14,8],[16,7],[18,7],[20,8],[21,10],[-1,-1],[3,8],[4,10],[6,11],[8,11],[10,10],[14,7],[16,6],[18,6],[20,7],[21,10],[21,12]]}};a.jqplot.CanvasFontRenderer=function(b){b=b||{};if(!b.pt2px){b.pt2px=1.5}a.jqplot.CanvasTextRenderer.call(this,b)};a.jqplot.CanvasFontRenderer.prototype=new a.jqplot.CanvasTextRenderer({});a.jqplot.CanvasFontRenderer.prototype.constructor=a.jqplot.CanvasFontRenderer;a.jqplot.CanvasFontRenderer.prototype.measure=function(c,e){var d=this.fontSize+" "+this.fontFamily;c.save();c.font=d;var b=c.measureText(e).width;c.restore();return b};a.jqplot.CanvasFontRenderer.prototype.draw=function(e,g){var c=0;var h=this.height*0.72;e.save();var d,b;if((-Math.PI/2<=this.angle&&this.angle<=0)||(Math.PI*3/2<=this.angle&&this.angle<=Math.PI*2)){d=0;b=-Math.sin(this.angle)*this.width}else{if((0b.max||b.max==null){b.max=h[c][0]}}else{if(h[c][1]b.max||b.max==null){b.max=h[c][1]}}}}if(this.groupLabels.length){this.groups=this.groupLabels.length}};a.jqplot.CategoryAxisRenderer.prototype.createTicks=function(){var D=this._ticks;var z=this.ticks;var F=this.name;var C=this._dataBounds;var v,A;var q,w;var d,c;var b,x;if(z.length){if(this.groups>1&&!this._grouped){var r=z.length;var p=parseInt(r/this.groups,10);var e=0;for(var x=p;x1&&!this._grouped){var r=y.length;var p=parseInt(r/this.groups,10);var e=0;for(var x=p;x0&&o
');if(this.name=="xaxis"||this.name=="x2axis"){this._elem.width(this._plotDimensions.width)}else{this._elem.height(this._plotDimensions.height)}this.labelOptions.axis=this.name;this._label=new this.labelRenderer(this.labelOptions);if(this._label.show){var g=this._label.draw(b,j);g.appendTo(this._elem)}var f=this._ticks;for(var e=0;e
');g.html(this.groupLabels[e]);this._groupLabels.push(g);g.appendTo(this._elem)}}return this._elem};a.jqplot.CategoryAxisRenderer.prototype.set=function(){var e=0;var m;var k=0;var f=0;var d=(this._label==null)?false:this._label.show;if(this.show){var n=this._ticks;for(var c=0;ce){e=m}}}var j=0;for(var c=0;cj){j=m}}if(d){k=this._label._elem.outerWidth(true);f=this._label._elem.outerHeight(true)}if(this.name=="xaxis"){e+=j+f;this._elem.css({height:e+"px",left:"0px",bottom:"0px"})}else{if(this.name=="x2axis"){e+=j+f;this._elem.css({height:e+"px",left:"0px",top:"0px"})}else{if(this.name=="yaxis"){e+=j+k;this._elem.css({width:e+"px",left:"0px",top:"0px"});if(d&&this._label.constructor==a.jqplot.AxisLabelRenderer){this._label._elem.css("width",k+"px")}}else{e+=j+k;this._elem.css({width:e+"px",right:"0px",top:"0px"});if(d&&this._label.constructor==a.jqplot.AxisLabelRenderer){this._label._elem.css("width",k+"px")}}}}}};a.jqplot.CategoryAxisRenderer.prototype.pack=function(e,c){var C=this._ticks;var v=this.max;var s=this.min;var n=c.max;var l=c.min;var q=(this._label==null)?false:this._label.show;var x;for(var r in e){this._elem.css(r,e[r])}this._offsets=c;var g=n-l;var k=v-s;this.p2u=function(h){return(h-l)*k/g+s};this.u2p=function(h){return(h-s)*g/k+l};if(this.name=="xaxis"||this.name=="x2axis"){this.series_u2p=function(h){return(h-s)*g/k};this.series_p2u=function(h){return h*k/g+s}}else{this.series_u2p=function(h){return(h-v)*g/k};this.series_p2u=function(h){return h*k/g+v}}if(this.show){if(this.name=="xaxis"||this.name=="x2axis"){for(x=0;x0){b=-o._textRenderer.height*Math.cos(-o._textRenderer.angle)/2}else{b=-o.getHeight()+o._textRenderer.height*Math.cos(o._textRenderer.angle)/2}break;case"middle":b=-o.getHeight()/2;break;default:b=-o.getHeight()/2;break}}else{b=-o.getHeight()/2}var D=this.u2p(o.value)+b+"px";o._elem.css("top",D);o.pack()}}var z=["left",0];if(q){var y=this._label._elem.outerHeight(true);this._label._elem.css("top",n-g/2-y/2+"px");if(this.name=="yaxis"){this._label._elem.css("left","0px");z=["left",this._label._elem.outerWidth(true)]}else{this._label._elem.css("right","0px");z=["right",this._label._elem.outerWidth(true)]}this._label.pack()}var d=parseInt(this._ticks.length/this.groups,10);for(x=0;xb.max||b.max==null){b.max=h[c][0]}}else{if(h[c][1]b.max||b.max==null){b.max=h[c][1]}}}}if(this.groupLabels.length){this.groups=this.groupLabels.length}};a.jqplot.CategoryAxisRenderer.prototype.createTicks=function(){var D=this._ticks;var z=this.ticks;var F=this.name;var C=this._dataBounds;var v,A;var q,w;var d,c;var b,x;if(z.length){if(this.groups>1&&!this._grouped){var r=z.length;var p=parseInt(r/this.groups,10);var e=0;for(var x=p;x1&&!this._grouped){var r=y.length;var p=parseInt(r/this.groups,10);var e=0;for(var x=p;x0&&o');if(this.name=="xaxis"||this.name=="x2axis"){this._elem.width(this._plotDimensions.width)}else{this._elem.height(this._plotDimensions.height)}this.labelOptions.axis=this.name;this._label=new this.labelRenderer(this.labelOptions);if(this._label.show){var g=this._label.draw(b,j);g.appendTo(this._elem)}var f=this._ticks;for(var e=0;e');g.html(this.groupLabels[e]);this._groupLabels.push(g);g.appendTo(this._elem)}}return this._elem};a.jqplot.CategoryAxisRenderer.prototype.set=function(){var e=0;var m;var k=0;var f=0;var d=(this._label==null)?false:this._label.show;if(this.show){var n=this._ticks;for(var c=0;ce){e=m}}}var j=0;for(var c=0;cj){j=m}}if(d){k=this._label._elem.outerWidth(true);f=this._label._elem.outerHeight(true)}if(this.name=="xaxis"){e+=j+f;this._elem.css({height:e+"px",left:"0px",bottom:"0px"})}else{if(this.name=="x2axis"){e+=j+f;this._elem.css({height:e+"px",left:"0px",top:"0px"})}else{if(this.name=="yaxis"){e+=j+k;this._elem.css({width:e+"px",left:"0px",top:"0px"});if(d&&this._label.constructor==a.jqplot.AxisLabelRenderer){this._label._elem.css("width",k+"px")}}else{e+=j+k;this._elem.css({width:e+"px",right:"0px",top:"0px"});if(d&&this._label.constructor==a.jqplot.AxisLabelRenderer){this._label._elem.css("width",k+"px")}}}}}};a.jqplot.CategoryAxisRenderer.prototype.pack=function(e,c){var C=this._ticks;var v=this.max;var s=this.min;var n=c.max;var l=c.min;var q=(this._label==null)?false:this._label.show;var x;for(var r in e){this._elem.css(r,e[r])}this._offsets=c;var g=n-l;var k=v-s;if(!this.reverse){this.u2p=function(h){return(h-s)*g/k+l};this.p2u=function(h){return(h-l)*k/g+s};if(this.name=="xaxis"||this.name=="x2axis"){this.series_u2p=function(h){return(h-s)*g/k};this.series_p2u=function(h){return h*k/g+s}}else{this.series_u2p=function(h){return(h-v)*g/k};this.series_p2u=function(h){return h*k/g+v}}}else{this.u2p=function(h){return l+(v-h)*g/k};this.p2u=function(h){return s+(h-l)*k/g};if(this.name=="xaxis"||this.name=="x2axis"){this.series_u2p=function(h){return(v-h)*g/k};this.series_p2u=function(h){return h*k/g+v}}else{this.series_u2p=function(h){return(s-h)*g/k};this.series_p2u=function(h){return h*k/g+s}}}if(this.show){if(this.name=="xaxis"||this.name=="x2axis"){for(x=0;x=this._ticks.length-1){continue}if(this._ticks[u]._elem&&this._ticks[u].label!=" "){var o=this._ticks[u]._elem;var r=o.position();B+=r.left+o.outerWidth(true)/2;f++}}B=B/f;this._groupLabels[x].css({left:(B-this._groupLabels[x].outerWidth(true)/2)});this._groupLabels[x].css(z[0],z[1])}}else{for(x=0;x0){b=-o._textRenderer.height*Math.cos(-o._textRenderer.angle)/2}else{b=-o.getHeight()+o._textRenderer.height*Math.cos(o._textRenderer.angle)/2}break;case"middle":b=-o.getHeight()/2;break;default:b=-o.getHeight()/2;break}}else{b=-o.getHeight()/2}var D=this.u2p(o.value)+b+"px";o._elem.css("top",D);o.pack()}}var z=["left",0];if(q){var y=this._label._elem.outerHeight(true);this._label._elem.css("top",n-g/2-y/2+"px");if(this.name=="yaxis"){this._label._elem.css("left","0px");z=["left",this._label._elem.outerWidth(true)]}else{this._label._elem.css("right","0px");z=["right",this._label._elem.outerWidth(true)]}this._label.pack()}var d=parseInt(this._ticks.length/this.groups,10)+1;for(x=0;x=this._ticks.length-1){continue}if(this._ticks[u]._elem&&this._ticks[u].label!=" "){var o=this._ticks[u]._elem;var r=o.position();B+=r.top+o.outerHeight()/2;f++}}B=B/f;this._groupLabels[x].css({top:B-this._groupLabels[x].outerHeight()/2});this._groupLabels[x].css(z[0],z[1])}}}}})(jQuery); \ No newline at end of file diff --git a/codespeed/static/js/jqplot/jqplot.cursor.min.js b/codespeed/static/js/jqplot/jqplot.cursor.min.js index 42156dee..51580bad 100644 --- a/codespeed/static/js/jqplot/jqplot.cursor.min.js +++ b/codespeed/static/js/jqplot/jqplot.cursor.min.js @@ -1,14 +1,3 @@ -/** - * Copyright (c) 2009 Chris Leonello - * jqPlot is currently available for use in all personal or commercial projects - * under both the MIT and GPL version 2.0 licenses. This means that you can - * choose the license that best suits your project and use it accordingly. - * - * Although not required, the author would appreciate an email letting him - * know of any substantial use of jqPlot. You can reach the author at: - * chris dot leonello at gmail dot com or see http://www.jqplot.com/info.php . - * - * If you are feeling kind and generous, consider supporting the project by - * making a donation at: http://www.jqplot.com/donate.php . - */ -(function(i){i.jqplot.Cursor=function(o){this.style="crosshair";this.previousCursor="auto";this.show=i.jqplot.config.enablePlugins;this.showTooltip=true;this.followMouse=false;this.tooltipLocation="se";this.tooltipOffset=6;this.showTooltipGridPosition=false;this.showTooltipUnitPosition=true;this.showTooltipDataPosition=false;this.tooltipFormatString="%.4P, %.4P";this.useAxesFormatters=true;this.tooltipAxisGroups=[];this.zoom=false;this.zoomProxy=false;this.zoomTarget=false;this.clickReset=false;this.dblClickReset=true;this.showVerticalLine=false;this.showHorizontalLine=false;this.constrainZoomTo="none";this.shapeRenderer=new i.jqplot.ShapeRenderer();this._zoom={start:[],end:[],started:false,zooming:false,isZoomed:false,axes:{start:{},end:{}}};this._tooltipElem;this.zoomCanvas;this.cursorCanvas;this.intersectionThreshold=2;this.showCursorLegend=false;this.cursorLegendFormatString=i.jqplot.Cursor.cursorLegendFormatString;i.extend(true,this,o)};i.jqplot.Cursor.cursorLegendFormatString="%s x:%s, y:%s";i.jqplot.Cursor.init=function(t,r,q){var o=q||{};this.plugins.cursor=new i.jqplot.Cursor(o.cursor);var u=this.plugins.cursor;if(u.show){i.jqplot.eventListenerHooks.push(["jqplotMouseEnter",b]);i.jqplot.eventListenerHooks.push(["jqplotMouseLeave",f]);i.jqplot.eventListenerHooks.push(["jqplotMouseMove",h]);if(u.showCursorLegend){q.legend=q.legend||{};q.legend.renderer=i.jqplot.CursorLegendRenderer;q.legend.formatString=this.plugins.cursor.cursorLegendFormatString;q.legend.show=true}if(u.zoom){i.jqplot.eventListenerHooks.push(["jqplotMouseDown",a]);i.jqplot.eventListenerHooks.push(["jqplotMouseUp",n]);if(u.clickReset){i.jqplot.eventListenerHooks.push(["jqplotClick",j])}if(u.dblClickReset){i.jqplot.eventListenerHooks.push(["jqplotDblClick",c])}}this.resetZoom=function(){var x=this.axes;if(!u.zoomProxy){for(var w in x){x[w].reset()}this.redraw()}else{var v=this.plugins.cursor.zoomCanvas._ctx;v.clearRect(0,0,v.canvas.width,v.canvas.height)}this.plugins.cursor._zoom.isZoomed=false;this.target.trigger("jqplotResetZoom",[this,this.plugins.cursor])};if(u.showTooltipDataPosition){u.showTooltipUnitPosition=false;u.showTooltipGridPosition=false;if(o.cursor.tooltipFormatString==undefined){u.tooltipFormatString=i.jqplot.Cursor.cursorLegendFormatString}}}};i.jqplot.Cursor.postDraw=function(){var w=this.plugins.cursor;w.zoomCanvas=new i.jqplot.GenericCanvas();this.eventCanvas._elem.before(w.zoomCanvas.createElement(this._gridPadding,"jqplot-zoom-canvas",this._plotDimensions));var v=w.zoomCanvas.setContext();w._tooltipElem=i('');w.zoomCanvas._elem.before(w._tooltipElem);if(w.showVerticalLine||w.showHorizontalLine){w.cursorCanvas=new i.jqplot.GenericCanvas();this.eventCanvas._elem.before(w.cursorCanvas.createElement(this._gridPadding,"jqplot-cursor-canvas",this._plotDimensions));var v=w.cursorCanvas.setContext()}if(w.showTooltipUnitPosition){if(w.tooltipAxisGroups.length===0){var r=this.series;var t;var o=[];for(var q=0;q6&&Math.abs(w.y-z._zoom.start[1])>6)||(z.constrainZoomTo=="x"&&Math.abs(w.x-z._zoom.start[0])>6)||(z.constrainZoomTo=="y"&&Math.abs(w.y-z._zoom.start[1])>6)){if(!x.plugins.cursor.zoomProxy){for(var o in t){if(z._zoom.axes[o]==undefined){z._zoom.axes[o]={};z._zoom.axes[o].numberTicks=y[o].numberTicks;z._zoom.axes[o].tickInterval=y[o].tickInterval;z._zoom.axes[o].daTickInterval=y[o].daTickInterval;z._zoom.axes[o].min=y[o].min;z._zoom.axes[o].max=y[o].max}if((z.constrainZoomTo=="none")||(z.constrainZoomTo=="x"&&o.charAt(0)=="x")||(z.constrainZoomTo=="y"&&o.charAt(0)=="y")){dp=t[o];if(dp!=null){if(dp>r[o]){y[o].min=r[o];y[o].max=dp}else{span=r[o]-dp;y[o].max=r[o];y[o].min=dp}y[o].tickInterval=null;y[o].daTickInterval=null;y[o]._ticks=[]}}}C.clearRect(0,0,C.canvas.width,C.canvas.height);x.redraw();z._zoom.isZoomed=true}x.target.trigger("jqplotZoom",[w,t,x,B])}};i.jqplot.preInitHooks.push(i.jqplot.Cursor.init);i.jqplot.postDrawHooks.push(i.jqplot.Cursor.postDraw);function e(D,q,A){var F=A.plugins.cursor;var v="";var J=false;if(F.showTooltipGridPosition){v=D.x+", "+D.y;J=true}if(F.showTooltipUnitPosition){var C;for(var B=0;B"}if(F.useAxesFormatters){var z=A.axes[C[0]]._ticks[0].formatter;var o=A.axes[C[1]]._ticks[0].formatter;var G=A.axes[C[0]]._ticks[0].formatString;var u=A.axes[C[1]]._ticks[0].formatString;v+=z(G,q[C[0]])+", "+o(u,q[C[1]])}else{v+=i.jqplot.sprintf(F.tooltipFormatString,q[C[0]],q[C[1]])}J=true}}if(F.showTooltipDataPosition){var t=A.series;var I=d(A,D.x,D.y);var J=false;for(var B=0;B"}v+=i.jqplot.sprintf(F.tooltipFormatString,r,y,w);J=true}}}}F._tooltipElem.html(v)}function g(C,A){var E=A.plugins.cursor;var z=E.cursorCanvas._ctx;z.clearRect(0,0,z.canvas.width,z.canvas.height);if(E.showVerticalLine){E.shapeRenderer.draw(z,[[C.x,0],[C.x,z.canvas.height]])}if(E.showHorizontalLine){E.shapeRenderer.draw(z,[[0,C.y],[z.canvas.width,C.y]])}var G=d(A,C.x,C.y);if(E.showCursorLegend){var q=i(A.targetId+" td.jqplot-cursor-legend-label");for(var B=0;By[0]){q=y[0];o=u[0]-y[0]}else{q=u[0];o=y[0]-u[0]}if(u[1]>y[1]){v=y[1];x=u[1]-y[1]}else{v=u[1];x=y[1]-u[1]}r.fillStyle="rgba(0,0,0,0.2)";r.strokeStyle="#999999";r.lineWidth=1;r.clearRect(0,0,r.canvas.width,r.canvas.height);r.fillRect(0,0,r.canvas.width,r.canvas.height);r.clearRect(q,v,o,x);r.strokeRect(q,v,o,x)}i.jqplot.CursorLegendRenderer=function(o){i.jqplot.TableLegendRenderer.call(this,o);this.formatString="%s"};i.jqplot.CursorLegendRenderer.prototype=new i.jqplot.TableLegendRenderer();i.jqplot.CursorLegendRenderer.prototype.constructor=i.jqplot.CursorLegendRenderer;i.jqplot.CursorLegendRenderer.prototype.draw=function(){if(this.show){var u=this._series;this._elem=i('
');var x=false;for(var t=0;t').appendTo(this._elem);C.data("seriesIndex",y);i('
').appendTo(C);var E=i('');E.appendTo(C);E.data("seriesIndex",y);if(this.escapeHtml){E.text(B)}else{E.html(B)}}return this._elem}})(jQuery); \ No newline at end of file +/* jqPlot 1.0.8r1250 | (c) 2009-2013 Chris Leonello | jplot.com + jsDate | (c) 2010-2013 Chris Leonello + */(function(j){j.jqplot.Cursor=function(q){this.style="crosshair";this.previousCursor="auto";this.show=j.jqplot.config.enablePlugins;this.showTooltip=true;this.followMouse=false;this.tooltipLocation="se";this.tooltipOffset=6;this.showTooltipGridPosition=false;this.showTooltipUnitPosition=true;this.showTooltipDataPosition=false;this.tooltipFormatString="%.4P, %.4P";this.useAxesFormatters=true;this.tooltipAxisGroups=[];this.zoom=false;this.zoomProxy=false;this.zoomTarget=false;this.looseZoom=true;this.clickReset=false;this.dblClickReset=true;this.showVerticalLine=false;this.showHorizontalLine=false;this.constrainZoomTo="none";this.shapeRenderer=new j.jqplot.ShapeRenderer();this._zoom={start:[],end:[],started:false,zooming:false,isZoomed:false,axes:{start:{},end:{}},gridpos:{},datapos:{}};this._tooltipElem;this.zoomCanvas;this.cursorCanvas;this.intersectionThreshold=2;this.showCursorLegend=false;this.cursorLegendFormatString=j.jqplot.Cursor.cursorLegendFormatString;this._oldHandlers={onselectstart:null,ondrag:null,onmousedown:null};this.constrainOutsideZoom=true;this.showTooltipOutsideZoom=false;this.onGrid=false;j.extend(true,this,q)};j.jqplot.Cursor.cursorLegendFormatString="%s x:%s, y:%s";j.jqplot.Cursor.init=function(t,s,r){var q=r||{};this.plugins.cursor=new j.jqplot.Cursor(q.cursor);var u=this.plugins.cursor;if(u.show){j.jqplot.eventListenerHooks.push(["jqplotMouseEnter",b]);j.jqplot.eventListenerHooks.push(["jqplotMouseLeave",f]);j.jqplot.eventListenerHooks.push(["jqplotMouseMove",i]);if(u.showCursorLegend){r.legend=r.legend||{};r.legend.renderer=j.jqplot.CursorLegendRenderer;r.legend.formatString=this.plugins.cursor.cursorLegendFormatString;r.legend.show=true}if(u.zoom){j.jqplot.eventListenerHooks.push(["jqplotMouseDown",a]);if(u.clickReset){j.jqplot.eventListenerHooks.push(["jqplotClick",k])}if(u.dblClickReset){j.jqplot.eventListenerHooks.push(["jqplotDblClick",c])}}this.resetZoom=function(){var x=this.axes;if(!u.zoomProxy){for(var w in x){x[w].reset();x[w]._ticks=[];if(u._zoom.axes[w]!==undefined){x[w]._autoFormatString=u._zoom.axes[w].tickFormatString}}this.redraw()}else{var v=this.plugins.cursor.zoomCanvas._ctx;v.clearRect(0,0,v.canvas.width,v.canvas.height);v=null}this.plugins.cursor._zoom.isZoomed=false;this.target.trigger("jqplotResetZoom",[this,this.plugins.cursor])};if(u.showTooltipDataPosition){u.showTooltipUnitPosition=false;u.showTooltipGridPosition=false;if(q.cursor.tooltipFormatString==undefined){u.tooltipFormatString=j.jqplot.Cursor.cursorLegendFormatString}}}};j.jqplot.Cursor.postDraw=function(){var x=this.plugins.cursor;if(x.zoomCanvas){x.zoomCanvas.resetCanvas();x.zoomCanvas=null}if(x.cursorCanvas){x.cursorCanvas.resetCanvas();x.cursorCanvas=null}if(x._tooltipElem){x._tooltipElem.emptyForce();x._tooltipElem=null}if(x.zoom){x.zoomCanvas=new j.jqplot.GenericCanvas();this.eventCanvas._elem.before(x.zoomCanvas.createElement(this._gridPadding,"jqplot-zoom-canvas",this._plotDimensions,this));x.zoomCanvas.setContext()}var v=document.createElement("div");x._tooltipElem=j(v);v=null;x._tooltipElem.addClass("jqplot-cursor-tooltip");x._tooltipElem.css({position:"absolute",display:"none"});if(x.zoomCanvas){x.zoomCanvas._elem.before(x._tooltipElem)}else{this.eventCanvas._elem.before(x._tooltipElem)}if(x.showVerticalLine||x.showHorizontalLine){x.cursorCanvas=new j.jqplot.GenericCanvas();this.eventCanvas._elem.before(x.cursorCanvas.createElement(this._gridPadding,"jqplot-cursor-canvas",this._plotDimensions,this));x.cursorCanvas.setContext()}if(x.showTooltipUnitPosition){if(x.tooltipAxisGroups.length===0){var t=this.series;var u;var q=[];for(var r=0;r6&&Math.abs(G.y-I._zoom.start[1])>6)||(I.constrainZoomTo=="x"&&Math.abs(G.x-I._zoom.start[0])>6)||(I.constrainZoomTo=="y"&&Math.abs(G.y-I._zoom.start[1])>6)){if(!C.plugins.cursor.zoomProxy){for(var y in t){if(I._zoom.axes[y]==undefined){I._zoom.axes[y]={};I._zoom.axes[y].numberTicks=F[y].numberTicks;I._zoom.axes[y].tickInterval=F[y].tickInterval;I._zoom.axes[y].daTickInterval=F[y].daTickInterval;I._zoom.axes[y].min=F[y].min;I._zoom.axes[y].max=F[y].max;I._zoom.axes[y].tickFormatString=(F[y].tickOptions!=null)?F[y].tickOptions.formatString:""}if((I.constrainZoomTo=="none")||(I.constrainZoomTo=="x"&&y.charAt(0)=="x")||(I.constrainZoomTo=="y"&&y.charAt(0)=="y")){z=t[y];if(z!=null){if(z>w[y]){v=w[y];x=z}else{D=w[y]-z;v=z;x=w[y]}q=F[y];H=null;if(q.alignTicks){if(q.name==="x2axis"&&C.axes.xaxis.show){H=C.axes.xaxis.numberTicks}else{if(q.name.charAt(0)==="y"&&q.name!=="yaxis"&&q.name!=="yMidAxis"&&C.axes.yaxis.show){H=C.axes.yaxis.numberTicks}}}if(this.looseZoom&&(F[y].renderer.constructor===j.jqplot.LinearAxisRenderer||F[y].renderer.constructor===j.jqplot.LogAxisRenderer)){J=j.jqplot.LinearTickGenerator(v,x,q._scalefact,H);if(F[y].tickInset&&J[0]F[y].max-F[y].tickInset*F[y].tickInterval){J[1]-=J[4];J[2]-=1}if(F[y].renderer.constructor===j.jqplot.LogAxisRenderer&&J[0]"}if(J.useAxesFormatters){for(var D=0;D"}w+=j.jqplot.sprintf(J.tooltipFormatString,t,z,x);N=true}}}}J._tooltipElem.html(w)}function g(C,A){var E=A.plugins.cursor;var z=E.cursorCanvas._ctx;z.clearRect(0,0,z.canvas.width,z.canvas.height);if(E.showVerticalLine){E.shapeRenderer.draw(z,[[C.x,0],[C.x,z.canvas.height]])}if(E.showHorizontalLine){E.shapeRenderer.draw(z,[[0,C.y],[z.canvas.width,C.y]])}var G=d(A,C.x,C.y);if(E.showCursorLegend){var r=j(A.targetId+" td.jqplot-cursor-legend-label");for(var B=0;B0;r--){s=v[r-1];if(q[s].show){u[s]=q[s].series_p2u(w[s.charAt(0)])}}return{offsets:t,gridPos:w,dataPos:u}}function h(z){var x=z.data.plot;var y=x.plugins.cursor;if(y.show&&y.zoom&&y._zoom.started&&!y.zoomTarget){z.preventDefault();var B=y.zoomCanvas._ctx;var v=o(z);var w=v.gridPos;var t=v.dataPos;y._zoom.gridpos=w;y._zoom.datapos=t;y._zoom.zooming=true;var u=w.x;var s=w.y;var A=B.canvas.height;var q=B.canvas.width;if(y.showTooltip&&!y.onGrid&&y.showTooltipOutsideZoom){e(w,t,x);if(y.followMouse){n(w,x)}}if(y.constrainZoomTo=="x"){y._zoom.end=[u,A]}else{if(y.constrainZoomTo=="y"){y._zoom.end=[q,s]}else{y._zoom.end=[u,s]}}var r=window.getSelection;if(document.selection&&document.selection.empty){document.selection.empty()}else{if(r&&!r().isCollapsed){r().collapse()}}l.call(y);B=null}}function a(w,s,r,x,t){var v=t.plugins.cursor;if(t.plugins.mobile){j(document).one("vmouseup.jqplot_cursor",{plot:t},p)}else{j(document).one("mouseup.jqplot_cursor",{plot:t},p)}var u=t.axes;if(document.onselectstart!=undefined){v._oldHandlers.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!=undefined){v._oldHandlers.ondrag=document.ondrag;document.ondrag=function(){return false}}if(document.onmousedown!=undefined){v._oldHandlers.onmousedown=document.onmousedown;document.onmousedown=function(){return false}}if(v.zoom){if(!v.zoomProxy){var y=v.zoomCanvas._ctx;y.clearRect(0,0,y.canvas.width,y.canvas.height);y=null}if(v.constrainZoomTo=="x"){v._zoom.start=[s.x,0]}else{if(v.constrainZoomTo=="y"){v._zoom.start=[0,s.y]}else{v._zoom.start=[s.x,s.y]}}v._zoom.started=true;for(var q in r){v._zoom.axes.start[q]=r[q]}if(t.plugins.mobile){j(document).bind("vmousemove.jqplotCursor",{plot:t},h)}else{j(document).bind("mousemove.jqplotCursor",{plot:t},h)}}}function p(y){var v=y.data.plot;var x=v.plugins.cursor;if(x.zoom&&x._zoom.zooming&&!x.zoomTarget){var u=x._zoom.gridpos.x;var r=x._zoom.gridpos.y;var t=x._zoom.datapos;var z=x.zoomCanvas._ctx.canvas.height;var q=x.zoomCanvas._ctx.canvas.width;var w=v.axes;if(x.constrainOutsideZoom&&!x.onGrid){if(u<0){u=0}else{if(u>q){u=q}}if(r<0){r=0}else{if(r>z){r=z}}for(var s in t){if(t[s]){if(s.charAt(0)=="x"){t[s]=w[s].series_p2u(u)}else{t[s]=w[s].series_p2u(r)}}}}if(x.constrainZoomTo=="x"){r=z}else{if(x.constrainZoomTo=="y"){u=q}}x._zoom.end=[u,r];x._zoom.gridpos={x:u,y:r};x.doZoom(x._zoom.gridpos,t,v,x)}x._zoom.started=false;x._zoom.zooming=false;j(document).unbind("mousemove.jqplotCursor",h);if(document.onselectstart!=undefined&&x._oldHandlers.onselectstart!=null){document.onselectstart=x._oldHandlers.onselectstart;x._oldHandlers.onselectstart=null}if(document.ondrag!=undefined&&x._oldHandlers.ondrag!=null){document.ondrag=x._oldHandlers.ondrag;x._oldHandlers.ondrag=null}if(document.onmousedown!=undefined&&x._oldHandlers.onmousedown!=null){document.onmousedown=x._oldHandlers.onmousedown;x._oldHandlers.onmousedown=null}}function l(){var y=this._zoom.start;var u=this._zoom.end;var s=this.zoomCanvas._ctx;var r,v,x,q;if(u[0]>y[0]){r=y[0];q=u[0]-y[0]}else{r=u[0];q=y[0]-u[0]}if(u[1]>y[1]){v=y[1];x=u[1]-y[1]}else{v=u[1];x=y[1]-u[1]}s.fillStyle="rgba(0,0,0,0.2)";s.strokeStyle="#999999";s.lineWidth=1;s.clearRect(0,0,s.canvas.width,s.canvas.height);s.fillRect(0,0,s.canvas.width,s.canvas.height);s.clearRect(r,v,q,x);s.strokeRect(r,v,q,x);s=null}j.jqplot.CursorLegendRenderer=function(q){j.jqplot.TableLegendRenderer.call(this,q);this.formatString="%s"};j.jqplot.CursorLegendRenderer.prototype=new j.jqplot.TableLegendRenderer();j.jqplot.CursorLegendRenderer.prototype.constructor=j.jqplot.CursorLegendRenderer;j.jqplot.CursorLegendRenderer.prototype.draw=function(){if(this._elem){this._elem.emptyForce();this._elem=null}if(this.show){var w=this._series,A;var r=document.createElement("table");this._elem=j(r);r=null;this._elem.addClass("jqplot-legend jqplot-cursor-legend");this._elem.css("position","absolute");var q=false;for(var x=0;x').appendTo(this._elem);E.data("seriesIndex",s);j('
').appendTo(E);var G=j('');G.appendTo(E);G.data("seriesIndex",s);if(this.escapeHtml){G.text(D)}else{G.html(D)}E=null;G=null}return this._elem}})(jQuery); \ No newline at end of file diff --git a/codespeed/static/js/jqplot/jqplot.dateAxisRenderer.min.js b/codespeed/static/js/jqplot/jqplot.dateAxisRenderer.min.js index 8d1013b3..74178015 100644 --- a/codespeed/static/js/jqplot/jqplot.dateAxisRenderer.min.js +++ b/codespeed/static/js/jqplot/jqplot.dateAxisRenderer.min.js @@ -1,14 +1,3 @@ -/** - * Copyright (c) 2009 Chris Leonello - * jqPlot is currently available for use in all personal or commercial projects - * under both the MIT and GPL version 2.0 licenses. This means that you can - * choose the license that best suits your project and use it accordingly. - * - * Although not required, the author would appreciate an email letting him - * know of any substantial use of jqPlot. You can reach the author at: - * chris dot leonello at gmail dot com or see http://www.jqplot.com/info.php . - * - * If you are feeling kind and generous, consider supporting the project by - * making a donation at: http://www.jqplot.com/donate.php . - */ -(function(a){a.jqplot.DateAxisRenderer=function(){a.jqplot.LinearAxisRenderer.call(this)};a.jqplot.DateAxisRenderer.prototype=new a.jqplot.LinearAxisRenderer();a.jqplot.DateAxisRenderer.prototype.constructor=a.jqplot.DateAxisRenderer;a.jqplot.DateTickFormatter=function(b,c){if(!b){b="%Y/%m/%d"}return Date.create(c).strftime(b)};a.jqplot.DateAxisRenderer.prototype.init=function(f){this.tickOptions.formatter=a.jqplot.DateTickFormatter;this.daTickInterval=null;this._daTickInterval=null;a.extend(true,this,f);var c=this._dataBounds;for(var g=0;gc.max||c.max==null){c.max=l[e][0]}}else{l[e][1]=Date.create(l[e][1]).getTime();b[e][1]=Date.create(l[e][1]).getTime();k[e][1]=Date.create(l[e][1]).getTime();if(l[e][1]c.max||c.max==null){c.max=l[e][1]}}}}};a.jqplot.DateAxisRenderer.prototype.reset=function(){this.min=this._min;this.max=this._max;this.tickInterval=this._tickInterval;this.numberTicks=this._numberTicks;this.daTickInterval=this._daTickInterval};a.jqplot.DateAxisRenderer.prototype.createTicks=function(){var v=this._ticks;var r=this.ticks;var w=this.name;var u=this._dataBounds;var o,s;var m,p;var d,c;var b,q;if(r.length){for(q=0;q200){this.numberTicks=parseInt(3+(o-200)/100,10)}else{this.numberTicks=2}}}if(this.daTickInterval==null){this.daTickInterval=[j/(this.numberTicks-1)/1000,"seconds"]}for(var q=0;qC.max)||C.max==null){C.max=y[r][0]}if(r>0){o=Math.abs(y[r][0]-y[r-1][0]);u.intervals.push(o);if(u.frequencies.hasOwnProperty(o)){u.frequencies[o]+=1}else{u.frequencies[o]=1}}x+=o}else{y[r][1]=new h.jsDate(y[r][1]).getTime();A[r][1]=new h.jsDate(y[r][1]).getTime();z[r][1]=new h.jsDate(y[r][1]).getTime();if((y[r][1]!=null&&y[r][1]C.max)||C.max==null){C.max=y[r][1]}if(r>0){o=Math.abs(y[r][1]-y[r-1][1]);u.intervals.push(o);if(u.frequencies.hasOwnProperty(o)){u.frequencies[o]+=1}else{u.frequencies[o]=1}}}x+=o}if(D.renderer.bands){if(D.renderer.bands.hiData.length){var w=D.renderer.bands.hiData;for(var r=0,q=w.length;rC.max)||C.max==null){C.max=w[r][0]}}else{w[r][1]=new h.jsDate(w[r][1]).getTime();if((w[r][1]!=null&&w[r][1]>C.max)||C.max==null){C.max=w[r][1]}}}}if(D.renderer.bands.lowData.length){var w=D.renderer.bands.lowData;for(var r=0,q=w.length;r6){D=6}}var V=new h.jsDate(ae).setDate(1).setHours(0,0,0,0);var q=new h.jsDate(J);var z=new h.jsDate(J).setDate(1).setHours(0,0,0,0);if(q.getTime()!==z.getTime()){z=z.add(1,"month")}var S=z.diff(V,"month");ab=Math.ceil(S/D)+1;this.min=V.getTime();this.max=V.clone().add((ab-1)*D,"month").getTime();this.numberTicks=ab;for(var aa=0;aa200){this.numberTicks=parseInt(3+(n-200)/100,10)}else{this.numberTicks=2}}}O=B/(this.numberTicks-1)/1000;if(this.daTickInterval==null){this.daTickInterval=[O,"seconds"]}for(var aa=0;aa