├── .coveragerc ├── .env ├── .gitignore ├── .travis.yml ├── LICENSE ├── Procfile ├── Procfile.dev ├── README.md ├── README_HEROKU.md ├── Vagrantfile ├── authentication ├── __init__.py ├── backend.py ├── serializers.py ├── tests.py └── views.py ├── cdws_api ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── import.py ├── migrations │ └── __init__.py ├── serializers.py ├── testdata │ ├── empty-test-report.xml │ ├── junit-test-report-notime.xml │ ├── junit-test-report.xml │ ├── nunit-test-report.xml │ ├── qttestxunit-test-report.xml │ └── xcprettyjunit-test-report.xml ├── tests.py ├── views.py └── xml_parser.py ├── comments ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20141217_2314.py │ └── __init__.py └── models.py ├── common ├── __init__.py ├── admin.py ├── context_processors.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_settings.py │ └── __init__.py ├── models.py ├── storage.py ├── tasks.py └── templates │ ├── login.html │ └── logout.html ├── config.py ├── dev-requirements.txt ├── manage.py ├── metrics ├── __init__.py ├── admin.py ├── handlers.py ├── jira.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150703_0937.py │ ├── 0003_auto_20150707_1622.py │ ├── 0004_metric_error.py │ ├── 0005_metric_weight.py │ ├── 0006_auto_20150727_1145.py │ └── __init__.py ├── models.py └── tasks.py ├── pycd ├── __init__.py ├── celery.py ├── disablecsrf.py ├── settings.py ├── urls.py └── wsgi.py ├── requirements.txt ├── run_coveralls.py ├── runtime.txt ├── stages ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150727_1145.py │ └── __init__.py └── models.py ├── test-requirements.txt ├── testreport ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20141022_1358.py │ ├── 0003_auto_20141023_1423.py │ ├── 0004_auto_20141029_0900.py │ ├── 0005_auto_20141117_0734.py │ ├── 0006_auto_20141117_0737.py │ ├── 0007_auto_20141117_1052.py │ ├── 0008_auto_20141117_1348.py │ ├── 0009_auto_20141117_1517.py │ ├── 0010_auto_20141117_2134.py │ ├── 0011_auto_20141121_1306.py │ ├── 0012_auto_20141121_1307.py │ ├── 0013_auto_20141126_1308.py │ ├── 0014_auto_20141208_0930.py │ ├── 0015_auto_20141208_1330.py │ ├── 0016_auto_20141209_0734.py │ ├── 0017_auto_20141210_1646.py │ ├── 0018_auto_20141216_1834.py │ ├── 0019_auto_20141217_2133.py │ ├── 0020_auto_20141217_2134.py │ ├── 0021_auto_20141217_2314.py │ ├── 0022_auto_20141218_0830.py │ ├── 0023_auto_20141223_1300.py │ ├── 0024_auto_20150309_1013.py │ ├── 0025_auto_20150309_1149.py │ ├── 0026_testresult_launch_item_id.py │ ├── 0027_auto_20150527_1204.py │ ├── 0028_auto_20150613_1727.py │ ├── 0029_testplan_description.py │ ├── 0030_launch_duration.py │ ├── 0031_extuser.py │ ├── 0032_auto_20151023_1055.py │ ├── 0033_auto_20151110_0925.py │ ├── 0034_extuser_dashboards.py │ ├── 0035_testplan_summary.py │ ├── 0036_auto_20151218_1144.py │ ├── 0037_auto_20151223_1643.py │ ├── 0038_testplan_show_in_twodays.py │ ├── 0039_auto_20160120_1606.py │ ├── 0040_extuser_result_preview.py │ ├── 0041_build.py │ ├── 0042_add_xml_parser_user.py │ ├── 0043_auto_20160413_1040.py │ └── __init__.py ├── models.py ├── tasks.py ├── templates │ └── base.html ├── tests.py └── views.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = .tox/* 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Required 2 | DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres 3 | BROKER_URL=amqp://guest:guest@127.0.0.1:5672/cdws 4 | CDWS_WORKING_DIR=/tmp/cdws 5 | COOKIE_DOMAIN= 6 | CDWS_API_HOSTNAME=localhost:8000 7 | 8 | # Optional (with defaults) 9 | DEBUG=True 10 | #SECRET_KEY= 11 | #LANGUAGE_CODE=en-us 12 | #TIME_ZONE='Europe/Moscow' 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install coveralls 4 | - pip install tox 5 | script: 6 | - tox 7 | after_success: 8 | - coveralls 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 2GIS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn pycd.wsgi --config config.py -b 0.0.0.0:$PORT 2 | beat: python manage.py celery beat -S djcelery.schedulers.DatabaseScheduler 3 | worker: python manage.py celery worker -Q default -l INFO 4 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | cmd: gunicorn pycd.wsgi --config config.py -b 0.0.0.0:8000 2 | default_worker: python manage.py celery worker -Q default -l DEBUG 3 | launcher_worker: python manage.py celery worker -Q launcher -l DEBUG 4 | beat: python manage.py celery beat -S djcelery.schedulers.DatabaseScheduler 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Badger-api [![Build Status](https://travis-ci.org/2gis/badger-api.svg?branch=master)](https://travis-ci.org/2gis/badger-api) [![Coverage Status](https://coveralls.io/repos/2gis/badger-api/badge.svg?branch=master&service=github)](https://coveralls.io/github/2gis/badger-api?branch=master) 2 | Badger-api is an open source backend service (REST API) for [Badger] (https://github.com/2gis/badger) (AngularJS web UI). 3 | 4 | # Installation 5 | 6 | ### Development version | Deploy to Heroku 7 | 8 | Install dependencies: 9 | ```bash 10 | apt-get install -y python3 python3-dev python3-pip python3-setuptools python-virtualenv python-tox 11 | apt-get install -y libpq-dev libcurl4-openssl-dev libsasl2-dev 12 | ``` 13 | 14 | Clone repository: 15 | ```bash 16 | git clone https://github.com/2gis/badger-api.git 17 | cd badger-api 18 | ``` 19 | 20 | Install [Vagrant] (https://www.vagrantup.com/downloads.html), [Docker] (http://docs.docker.com/linux/started/) and type: 21 | ```bash 22 | vagrant up 23 | ``` 24 | *Vagrant will start two Docker containers with postgresql and rabbitmq.* 25 | 26 | Install requirements and run tests: 27 | ```bash 28 | tox 29 | ``` 30 | 31 | Activate virtual env: 32 | ```bash 33 | source .tox/py34/bin/activate 34 | ``` 35 | 36 | Install dev requirements and create database model: 37 | ```bash 38 | pip install -r dev-requirements.txt 39 | honcho run ./manage.py syncdb 40 | ``` 41 | 42 | Run api + celery: 43 | ```bash 44 | honcho start -f Procfile.dev 45 | ``` 46 | 47 | Now your api is available at http://localhost:8000/api/ 48 | 49 | 50 | # Usage 51 | 52 | To start adding content to api, you need a user. Run following command to create it: 53 | ```bash 54 | honcho run ./manage.py createsuperuser 55 | ``` 56 | 57 | The Django admin site is available at http://localhost:8000/admin/ 58 | 59 | ### Secret key 60 | 61 | For production usage you need secret key. Don't forget to [generate it] (https://gist.github.com/mattseymour/9205591) and add to .env 62 | 63 | -------------------------------------------------------------------------------- /README_HEROKU.md: -------------------------------------------------------------------------------- 1 | # Badger-api [![Build Status](https://travis-ci.org/2gis/badger-api.svg?branch=master)](https://travis-ci.org/2gis/badger-api) [![Coverage Status](https://coveralls.io/repos/2gis/badger-api/badge.svg?branch=master&service=github)](https://coveralls.io/github/2gis/badger-api?branch=master) 2 | Badger-api is an open source backend service (REST API) for [Badger] (https://github.com/2gis/badger) (AngularJS web UI). 3 | 4 | # Installation 5 | 6 | ### Development version | Deploy to Heroku 7 | 8 | Clone repository: 9 | ```bash 10 | git clone https://github.com/2gis/badger-api.git 11 | cd badger-api 12 | ``` 13 | 14 | Create an app on Heroku: 15 | ```bash 16 | heroku create appname 17 | ``` 18 | 19 | Install CloudAMQP add-on: 20 | ```bash 21 | heroku addons:create cloudamqp 22 | ``` 23 | 24 | Configure you app: 25 | ```bash 26 | heroku config | grep CLOUDAMQP_URL 27 | heroku config:set BROKER_URL=amqp://user:pass@ec2.clustername.cloudamqp.com/vhost 28 | heroku config:set CDWS_API_HOSTNAME=appname.herokuapp.com 29 | ``` 30 | 31 | Sync database: 32 | ```bash 33 | heroku run python manage.py syncdb 34 | ``` 35 | 36 | Copy worker process from Procfile.dev to Procfile: 37 | ```bash 38 | worker: python manage.py celery worker -O fair -l DEBUG 39 | ``` 40 | 41 | Save your changes: 42 | ```bash 43 | git add . 44 | git commit -m "save my changes" 45 | ``` 46 | 47 | Deploy app: 48 | ```bash 49 | git push heroku master 50 | ``` 51 | 52 | Start processes: 53 | ```bash 54 | heroku ps:scale worker=1 beat=1 web=1 55 | ``` 56 | 57 | 58 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | 3 | config.vm.define "postgres" do |v| 4 | v.vm.provider "docker" do |d| 5 | d.image = "library/postgres:9.4" 6 | d.create_args = ["-p", "5432:5432", "-e", "POSTGRES_PASSWORD=postgres"] 7 | end 8 | end 9 | 10 | config.vm.define "rabbitmq" do |v| 11 | v.vm.provider "docker" do |d| 12 | d.image = "library/rabbitmq" 13 | d.create_args = ["--hostname=cdws", "-p", "5672:5672", "-e", "RABBITMQ_DEFAULT_VHOST=cdws"] 14 | end 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/authentication/__init__.py -------------------------------------------------------------------------------- /authentication/backend.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.contrib.auth.backends import ModelBackend 5 | 6 | import ldap3 7 | 8 | import logging 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | server = ldap3.Server(settings.AUTH_LDAP3_SERVER_URI) 13 | connection = None 14 | 15 | 16 | def get_connection(**kwargs): 17 | if 'dn' in kwargs and 'password' in kwargs: 18 | return ldap3.Connection(server, user=kwargs['dn'], 19 | password=kwargs['password'], read_only=True) 20 | return ldap3.Connection( 21 | server, 22 | user=settings.AUTH_LDAP3_SEARCH_USER_DN, 23 | password=settings.AUTH_LDAP3_SEARCH_USER_PASSWORD, 24 | read_only=True) 25 | 26 | 27 | def _get_dn_and_attributes_by_params(conn, params): 28 | if not conn.bind(): 29 | log.error('Unable to bind with search user: {}'.format( 30 | conn.last_error)) 31 | return None, None 32 | if not conn.search(**params): 33 | log.error('Search with params: {}, failed: {}'.format(params, 34 | conn.result)) 35 | return None, None 36 | log.debug('Search with base {search_base} ' 37 | 'and filter {search_filter} done'.format(**params)) 38 | log.debug('Response is: {}'.format(conn.response)) 39 | return conn.response[0]['dn'], conn.response[0]['attributes'] 40 | 41 | 42 | def create_or_update_user(params, username): 43 | try: 44 | user = User.objects.get(username__exact=username) 45 | for key, value in iter(params.items()): 46 | setattr(user, key, value) 47 | except ObjectDoesNotExist as e: 48 | log.debug(e) 49 | log.debug('Try to save user with params: {}'.format(params)) 50 | user = User(**params) 51 | user.save() 52 | return user 53 | 54 | 55 | class ADBackend(ModelBackend): 56 | def authenticate(self, username=None, password=None, **kwargs): 57 | if password == '': 58 | log.debug('Password is not set for authentication, return') 59 | return None 60 | log.debug('Authenticate username={username}, ' 61 | 'password=***'.format(username=username)) 62 | required_attributes = list(filter( 63 | lambda item: item is not None and item != '', 64 | settings.AUTH_LDAP3_ATTRIBUTES_MAPPING.values())) 65 | params = { 66 | 'search_base': settings.AUTH_LDAP3_SEARCH_BASE, 67 | 'search_filter': settings.AUTH_LDAP3_SEARCH_FILTER.format( 68 | username=username), 69 | 'attributes': required_attributes 70 | } 71 | dn, attributes = _get_dn_and_attributes_by_params(get_connection(), 72 | params) 73 | if dn is None: 74 | return None 75 | conn = get_connection(dn=dn, password=password) 76 | if not conn.bind(): 77 | log.debug('Bind for dn: {} failed: {}'.format(dn, 78 | conn.last_error)) 79 | return None 80 | params = {} 81 | for key, value in iter( 82 | settings.AUTH_LDAP3_ATTRIBUTES_MAPPING.items()): 83 | if value is not None: 84 | if isinstance(attributes[value], list): 85 | params[key] = attributes[value][0] 86 | else: 87 | params[key] = attributes[value] 88 | return create_or_update_user(params, username) 89 | -------------------------------------------------------------------------------- /authentication/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth.models import User 3 | from testreport.models import ExtUser 4 | 5 | 6 | class ExtUserSerializer(serializers.ModelSerializer): 7 | dashboards = serializers.ReadOnlyField(source='get_dashboards') 8 | 9 | class Meta: 10 | model = ExtUser 11 | fields = ('default_project', 'launches_on_page', 12 | 'testresults_on_page', 'dashboards', 'result_preview') 13 | 14 | 15 | class AccountSerializer(serializers.ModelSerializer): 16 | settings = ExtUserSerializer(many=False, read_only=True) 17 | 18 | class Meta: 19 | model = User 20 | fields = ('id', 'email', 'username', 'first_name', 'last_name', 21 | 'date_joined', 'is_active', 'is_staff', 'settings') 22 | read_only_fields = ('date_joined', ) 23 | -------------------------------------------------------------------------------- /authentication/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from django.test import Client 4 | 5 | import json 6 | 7 | 8 | class TestsBase(TestCase): 9 | correct_codes = [200, 201] 10 | user = None 11 | user_login = 'user' 12 | user_plain_password = 'qweqwe' 13 | 14 | def setUp(self): 15 | self.user = User.objects.create_user(self.user_login, 16 | 'user@domain.tld', 17 | self.user_plain_password) 18 | self.user.save() 19 | 20 | def tearDown(self): 21 | self.user.delete() 22 | 23 | 24 | class AuthTests(TestsBase): 25 | 26 | def test_login(self): 27 | c = Client() 28 | response = c.post('/api/auth/login/', 29 | data=json.dumps({ 30 | 'username': self.user_login, 31 | 'password': self.user_plain_password}), 32 | content_type='application/json') 33 | 34 | self.assertEqual(response.status_code, 200) 35 | 36 | def test_invalid_login(self): 37 | c = Client() 38 | response = c.post('/api/auth/login/', 39 | data=json.dumps({ 40 | 'username': self.user_login, 41 | 'password': 'wrong_password'}), 42 | content_type='application/json') 43 | self.assertEqual(response.status_code, 401) 44 | 45 | def test_get(self): 46 | c = Client() 47 | c.post('/api/auth/login/', 48 | data=json.dumps({ 49 | 'username': self.user_login, 50 | 'password': self.user_plain_password}), 51 | content_type='application/json') 52 | 53 | response = c.get('/api/auth/get') 54 | self.assertEqual(response.status_code, 200) 55 | 56 | def test_logout(self): 57 | c = Client() 58 | c.post('/api/auth/login/', 59 | data=json.dumps({ 60 | 'username': self.user_login, 61 | 'password': self.user_plain_password}), 62 | content_type='application/json') 63 | 64 | response = c.get('/api/auth/logout/') 65 | self.assertEqual(response.status_code, 200) 66 | 67 | def test_get_default_settings(self): 68 | c = Client() 69 | c.post('/api/auth/login/', 70 | data=json.dumps({ 71 | 'username': self.user_login, 72 | 'password': self.user_plain_password}), 73 | content_type='application/json') 74 | 75 | response = c.get('/api/auth/get') 76 | self.assertEqual(response.status_code, 200) 77 | content = json.loads( 78 | response.content.decode('utf-8', errors='replace')) 79 | self.assertEqual(10, content['settings']['launches_on_page']) 80 | self.assertEqual(25, content['settings']['testresults_on_page']) 81 | self.assertFalse(content['settings']['default_project']) 82 | self.assertEqual([], content['settings']['dashboards']) 83 | self.assertFalse(content['settings']['result_preview']) 84 | 85 | def test_update(self): 86 | c = Client() 87 | c.post('/api/auth/login/', 88 | data=json.dumps({ 89 | 'username': self.user_login, 90 | 'password': self.user_plain_password}), 91 | content_type='application/json') 92 | 93 | response = c.post('/api/auth/update', 94 | data=json.dumps({ 95 | 'default_project': 1, 96 | 'launches_on_page': 25, 97 | 'testresults_on_page': 50}), 98 | content_type='application/json') 99 | self.assertEqual(response.status_code, 200) 100 | 101 | response = c.get('/api/auth/get') 102 | self.assertEqual(response.status_code, 200) 103 | content = json.loads( 104 | response.content.decode('utf-8', errors='replace')) 105 | self.assertEqual(25, content['settings']['launches_on_page']) 106 | self.assertEqual(50, content['settings']['testresults_on_page']) 107 | self.assertEqual(1, content['settings']['default_project']) 108 | self.assertEqual([], content['settings']['dashboards']) 109 | 110 | def test_update_preview(self): 111 | c = Client() 112 | c.post('/api/auth/login/', 113 | data=json.dumps({ 114 | 'username': self.user_login, 115 | 'password': self.user_plain_password}), 116 | content_type='application/json') 117 | 118 | response = c.post('/api/auth/update', 119 | data=json.dumps({ 120 | 'result_preview': 'tail'}), 121 | content_type='application/json') 122 | self.assertEqual(response.status_code, 200) 123 | 124 | response = c.get('/api/auth/get') 125 | self.assertEqual(response.status_code, 200) 126 | content = json.loads( 127 | response.content.decode('utf-8', errors='replace')) 128 | self.assertEqual('tail', content['settings']['result_preview']) 129 | 130 | def test_update_dashboards(self): 131 | c = Client() 132 | c.post('/api/auth/login/', 133 | data=json.dumps({ 134 | 'username': self.user_login, 135 | 'password': self.user_plain_password}), 136 | content_type='application/json') 137 | 138 | response = c.post('/api/auth/update', 139 | data=json.dumps({ 140 | 'dashboards': [{ 141 | 'name': 'first', 142 | 'testplans': [1, 2, 3] 143 | }, { 144 | 'name': 'second', 145 | 'testplans': [1, 2, 3] 146 | }]}), 147 | content_type='application/json') 148 | self.assertEqual(response.status_code, 200) 149 | 150 | response = c.get('/api/auth/get') 151 | self.assertEqual(response.status_code, 200) 152 | content = json.loads( 153 | response.content.decode('utf-8', errors='replace')) 154 | dashboards = content['settings']['dashboards'] 155 | self.assertEqual(2, len(dashboards)) 156 | 157 | def test_update_dashboards_empty(self): 158 | c = Client() 159 | c.post('/api/auth/login/', 160 | data=json.dumps({ 161 | 'username': self.user_login, 162 | 'password': self.user_plain_password}), 163 | content_type='application/json') 164 | 165 | response = c.post('/api/auth/update', 166 | data=json.dumps({ 167 | 'dashboards': []}), 168 | content_type='application/json') 169 | self.assertEqual(response.status_code, 200) 170 | 171 | response = c.get('/api/auth/get') 172 | self.assertEqual(response.status_code, 200) 173 | content = json.loads( 174 | response.content.decode('utf-8', errors='replace')) 175 | dashboards = content['settings']['dashboards'] 176 | self.assertEqual(0, len(dashboards)) 177 | 178 | def test_update_dashboards_empty_string(self): 179 | c = Client() 180 | c.post('/api/auth/login/', 181 | data=json.dumps({ 182 | 'username': self.user_login, 183 | 'password': self.user_plain_password}), 184 | content_type='application/json') 185 | 186 | response = c.post('/api/auth/update', 187 | data=json.dumps({ 188 | 'dashboards': ''}), 189 | content_type='application/json') 190 | self.assertEqual(response.status_code, 200) 191 | 192 | response = c.get('/api/auth/get') 193 | self.assertEqual(response.status_code, 200) 194 | content = json.loads( 195 | response.content.decode('utf-8', errors='replace')) 196 | dashboards = content['settings']['dashboards'] 197 | self.assertEqual(0, len(dashboards)) 198 | 199 | def test_update_bad(self): 200 | c = Client() 201 | c.post('/api/auth/login/', 202 | data=json.dumps({ 203 | 'username': self.user_login, 204 | 'password': self.user_plain_password}), 205 | content_type='application/json') 206 | 207 | response = c.get('/api/auth/get') 208 | self.assertEqual(response.status_code, 200) 209 | response = c.post('/api/auth/update', 210 | data='', content_type='application/json') 211 | self.assertEqual(response.status_code, 400) 212 | 213 | def test_update_not_auth(self): 214 | c = Client() 215 | response = c.post('/api/auth/update', 216 | data='', content_type='application/json') 217 | self.assertEqual(response.status_code, 401) 218 | -------------------------------------------------------------------------------- /authentication/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate, login, logout 2 | from rest_framework import status, views 3 | from rest_framework.response import Response 4 | from authentication.serializers import AccountSerializer 5 | from testreport.models import ExtUser 6 | 7 | import logging 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class LoginView(views.APIView): 12 | 13 | def post(self, request, format=None): 14 | username = request.data.get('username', None) 15 | password = request.data.get('password', None) 16 | account = authenticate(username=username, password=password) 17 | if account is not None: 18 | if account.is_active: 19 | if not hasattr(account, 'settings'): 20 | account.settings = ExtUser() 21 | account.settings.save() 22 | login(request, account) 23 | serialized = AccountSerializer(account) 24 | return Response(serialized.data) 25 | else: 26 | return Response({ 27 | 'status': 'Unauthorized', 28 | 'message': 'This account has been disabled.' 29 | }, status=status.HTTP_401_UNAUTHORIZED) 30 | else: 31 | return Response({ 32 | 'status': 'Unauthorized', 33 | 'message': 'Authentication failed' 34 | }, status=status.HTTP_401_UNAUTHORIZED) 35 | 36 | 37 | class IsAuthorizedView(views.APIView): 38 | 39 | def get(self, request, format=None): 40 | if request.user.is_authenticated(): 41 | return Response(AccountSerializer(request.user).data, 42 | status=status.HTTP_200_OK) 43 | else: 44 | return Response({ 45 | 'status': 'Unauthorized', 46 | 'message': 'Unauthorized' 47 | }, status=status.HTTP_401_UNAUTHORIZED) 48 | 49 | 50 | class UpdateSettingsView(views.APIView): 51 | 52 | def post(self, request): 53 | if request.user.is_authenticated(): 54 | if not request.data: 55 | return Response({ 56 | 'message': 'Incorrect data in request' 57 | }, status=status.HTTP_400_BAD_REQUEST) 58 | for key, value in request.data.items(): 59 | if hasattr(request.user.settings, key): 60 | if key == 'dashboards': 61 | request.user.settings.set_dashboards(value) 62 | else: 63 | setattr(request.user.settings, key, value) 64 | request.user.settings.save() 65 | return Response({ 66 | 'message': 'Profile settings successfully updated' 67 | }, status=status.HTTP_200_OK) 68 | else: 69 | return Response({ 70 | 'status': 'Unauthorized', 71 | 'message': 'Unauthorized' 72 | }, status=status.HTTP_401_UNAUTHORIZED) 73 | 74 | 75 | class LogoutView(views.APIView): 76 | 77 | def get(self, request, format=None): 78 | if request.user.is_authenticated(): 79 | logout(request) 80 | return Response({ 81 | 'status': 'Success', 82 | 'message': 'Logout done.' 83 | }, status=status.HTTP_200_OK) 84 | else: 85 | return Response({ 86 | 'status': 'Unauthorized', 87 | 'message': 'Unauthorized' 88 | }, status=status.HTTP_401_UNAUTHORIZED) 89 | -------------------------------------------------------------------------------- /cdws_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/cdws_api/__init__.py -------------------------------------------------------------------------------- /cdws_api/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/cdws_api/management/__init__.py -------------------------------------------------------------------------------- /cdws_api/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/cdws_api/management/commands/__init__.py -------------------------------------------------------------------------------- /cdws_api/management/commands/import.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | 3 | from testreport.models import Launch 4 | from testreport.models import TestPlan 5 | from testreport.models import TestResult 6 | from testreport.models import PASSED, FAILED, BLOCKED, SKIPPED 7 | 8 | from common.models import Project 9 | 10 | from optparse import make_option 11 | 12 | import os 13 | import xml.dom.minidom 14 | import logging 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class Command(BaseCommand): 20 | option_list = BaseCommand.option_list + ( 21 | make_option('--project-name', 22 | help='Name of the project for import'), 23 | make_option('--test-plan-name', 24 | help='Name of the testplan for import'), 25 | make_option('--launch-id', 26 | default=None, 27 | help='Launch id'), 28 | make_option('--started-by', 29 | default=None, 30 | help='Url to web service which start launch'), 31 | make_option('--save', 32 | action='store_true', 33 | default=False, 34 | help='Launch id') 35 | ) 36 | launch = None 37 | buffer = [] 38 | 39 | def handle(self, *args, **options): 40 | if options['project_name'] is None: 41 | raise CommandError('--project-name is not specified') 42 | if options['test_plan_name'] is None: 43 | raise CommandError('--test-plan-name is not specified') 44 | if options['started_by'] is None: 45 | raise CommandError('--started-by is not specified') 46 | 47 | (project, new) = Project.objects.get_or_create( 48 | name=options['project_name']) 49 | (test_plan, new) = TestPlan.objects.get_or_create( 50 | name=options['test_plan_name'], project=project) 51 | if options['launch_id'] is None: 52 | self.launch = Launch(test_plan=test_plan, 53 | started_by=options['started_by']) 54 | if options['save']: 55 | self.launch.save() 56 | log.info('REPORT_URL=http://autotests.cd.test/launch/{0}/'. 57 | format(self.launch.id)) 58 | else: 59 | log.info('Try to get launch with id = %s', options['launch_id']) 60 | self.launch = Launch.objects.get(id=options['launch_id']) 61 | log.info('Using next launch: %s', self.launch) 62 | 63 | for file_path in args: 64 | self.load_file(file_path, self.launch) 65 | if options['save']: 66 | TestResult.objects.bulk_create(self.buffer) 67 | if self.launch.counts['failed'] > 0: 68 | log.info('BUILD_IS_UNSTABLE') 69 | 70 | def load_file(self, file_path, launch): 71 | if os.stat(file_path)[6] == 0: 72 | return 73 | log.info('Loading "%s"', file_path) 74 | dom = xml.dom.minidom.parse(file_path) 75 | self.parse(dom) 76 | 77 | def parse(self, element, path=''): 78 | if element.nodeName == 'testcase': 79 | self.create_test_result(element, path) 80 | if element.nodeName == 'testsuite': 81 | path += element.getAttribute('name') + '/' 82 | if element.hasChildNodes(): 83 | for node in element.childNodes: 84 | if node.nodeType == node.ELEMENT_NODE: 85 | self.parse(node, path) 86 | 87 | def create_test_result(self, element, path): 88 | data = { 89 | 'launch': self.launch, 90 | 'name': element.getAttribute('name'), 91 | 'suite': (path[:125] + '...') if len(path) > 125 else path, 92 | 'state': BLOCKED, 93 | 'duration': element.getAttribute('time'), 94 | 'failure_reason': '' 95 | } 96 | error = self.get_node(element, ['error', 'failure']) 97 | skipped = self.get_node(element, ['skipped']) 98 | if skipped is not None: 99 | data['state'] = SKIPPED 100 | data['failure_reason'] = 'Type: {0} : {1}'.format( 101 | skipped.getAttribute('type'), 102 | self.get_text(skipped.childNodes)) 103 | else: 104 | data['state'] = PASSED 105 | if error is not None: 106 | data['state'] = FAILED 107 | data['failure_reason'] = 'Type: {0} : {1}'.format( 108 | error.getAttribute('type'), self.get_text(error.childNodes)) 109 | self.buffer.append(TestResult(**data)) 110 | 111 | def get_node(self, element, names): 112 | for node in element.childNodes: 113 | if node.nodeName in names: 114 | return node 115 | return None 116 | 117 | def get_text(self, nodelist): 118 | rc = [] 119 | for node in nodelist: 120 | if node.nodeType == node.TEXT_NODE: 121 | rc.append(node.data.encode('utf-8', errors='replace')) 122 | return ''.join(rc) 123 | -------------------------------------------------------------------------------- /cdws_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/cdws_api/migrations/__init__.py -------------------------------------------------------------------------------- /cdws_api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.utils import six 2 | 3 | from common.models import Project, Settings 4 | 5 | from testreport.models import TestPlan 6 | from testreport.models import Launch 7 | from testreport.models import Build 8 | from testreport.models import TestResult 9 | from testreport.models import LaunchItem 10 | from testreport.models import Bug 11 | from stages.models import Stage 12 | from metrics.models import Metric, MetricValue 13 | 14 | from authentication.serializers import AccountSerializer 15 | 16 | from comments.models import Comment 17 | 18 | from rest_framework import serializers 19 | 20 | import logging 21 | 22 | 23 | log = logging.getLogger(__name__) 24 | 25 | 26 | class SettingsSerializer(serializers.ModelSerializer): 27 | class Meta: 28 | model = Settings 29 | fields = ('key', 'value') 30 | 31 | 32 | class ProjectSerializer(serializers.ModelSerializer): 33 | settings = SettingsSerializer(many=True, read_only=True) 34 | 35 | class Meta: 36 | model = Project 37 | fields = ('id', 'name', 'settings') 38 | 39 | 40 | class TestPlanSerializer(serializers.ModelSerializer): 41 | class Meta: 42 | model = TestPlan 43 | 44 | 45 | class TaskResultField(serializers.DictField): 46 | @staticmethod 47 | def child_to_representation(value): 48 | if isinstance(value, bytes): 49 | value = value.decode('utf-8', errors='replace') 50 | return value 51 | 52 | def to_representation(self, value): 53 | """ 54 | List of object instances -> List of dicts of primitive datatypes. 55 | """ 56 | return dict([ 57 | (six.text_type(key), self.child_to_representation(val)) 58 | for key, val in iter(value.items()) 59 | ]) 60 | 61 | 62 | class AsyncResultSerializer(serializers.Serializer): 63 | id = serializers.CharField() 64 | result = TaskResultField() 65 | status = serializers.CharField() 66 | 67 | def update(self, instance, validated_data): 68 | log.info("Update: {}".format(validated_data)) 69 | 70 | def create(self, validated_data): 71 | log.info("Create: {}".format(validated_data)) 72 | 73 | 74 | class TasksResultField(serializers.DictField): 75 | def to_representation(self, value): 76 | output = {} 77 | for key, value in iter(value.items()): 78 | try: 79 | output[key] = LaunchItemSerializer( 80 | LaunchItem.objects.get(pk=value)).data 81 | except LaunchItem.DoesNotExist: 82 | output[key] = LaunchItemSerializer(LaunchItem()).data 83 | return output 84 | 85 | 86 | class BuildSerializer(serializers.ModelSerializer): 87 | last_commits = serializers.ReadOnlyField(source='get_last_commits') 88 | 89 | class Meta: 90 | model = Build 91 | fields = ('version', 'hash', 'branch', 'last_commits', 92 | 'commit_message', 'commit_author') 93 | 94 | 95 | class LaunchSerializer(serializers.ModelSerializer): 96 | counts = serializers.ReadOnlyField() 97 | tasks = TasksResultField(source='get_tasks', read_only=True) 98 | parameters = serializers.ReadOnlyField(source='get_parameters') 99 | build = BuildSerializer(read_only=True) 100 | 101 | class Meta: 102 | model = Launch 103 | fields = ('id', 'test_plan', 'created', 'counts', 'tasks', 104 | 'state', 'started_by', 'created', 'finished', 'parameters', 105 | 'duration', 'build') 106 | 107 | 108 | class TestResultSerializer(serializers.ModelSerializer): 109 | class Meta: 110 | model = TestResult 111 | 112 | 113 | class LaunchItemSerializer(serializers.ModelSerializer): 114 | class Meta: 115 | model = LaunchItem 116 | 117 | 118 | class CommentSerializer(serializers.ModelSerializer): 119 | user_data = AccountSerializer(source='user', read_only=True) 120 | 121 | class Meta: 122 | model = Comment 123 | fields = ('id', 'comment', 'submit_date', 'content_type', 'object_pk', 124 | 'user', 'user_data') 125 | 126 | 127 | class BugSerializer(serializers.ModelSerializer): 128 | status = serializers.ReadOnlyField(source='get_state') 129 | 130 | class Meta: 131 | model = Bug 132 | fields = ('id', 'externalId', 'name', 'status', 'regexp', 'updated') 133 | 134 | 135 | class StageSerializer(serializers.ModelSerializer): 136 | class Meta: 137 | model = Stage 138 | fields = ('id', 'name', 'text', 'state', 139 | 'link', 'project', 'updated', 'weight') 140 | 141 | 142 | class MetricSerializer(serializers.ModelSerializer): 143 | schedule = serializers.ReadOnlyField(source='get_schedule_as_cron') 144 | 145 | class Meta: 146 | model = Metric 147 | 148 | 149 | class MetricValueSerializer(serializers.ModelSerializer): 150 | class Meta: 151 | model = MetricValue 152 | -------------------------------------------------------------------------------- /cdws_api/testdata/empty-test-report.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/cdws_api/testdata/empty-test-report.xml -------------------------------------------------------------------------------- /cdws_api/testdata/junit-test-report-notime.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Failure message 7 | 8 | 9 | Error message 10 | System-out 11 | 12 | 13 | Skipped message 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /cdws_api/testdata/junit-test-report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Failure message 7 | System-out 8 | 9 | 10 | Error message 11 | System-out 12 | 13 | 14 | Skipped message 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /cdws_api/testdata/nunit-test-report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /cdws_api/testdata/qttestxunit-test-report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /cdws_api/testdata/xcprettyjunit-test-report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | file_name.swift:94 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /cdws_api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework import status 3 | from rest_framework import mixins 4 | 5 | from rest_framework_xml.renderers import XMLRenderer 6 | from rest_framework.parsers import BaseParser, FileUploadParser 7 | from defusedxml import ElementTree 8 | 9 | from rest_framework.exceptions import ParseError 10 | 11 | from rest_framework.permissions import IsAuthenticatedOrReadOnly 12 | from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly 13 | from rest_framework.response import Response 14 | from rest_framework.filters import DjangoFilterBackend 15 | from rest_framework.filters import OrderingFilter 16 | from rest_framework.filters import SearchFilter 17 | from rest_framework.decorators import detail_route, list_route 18 | from rest_framework.views import APIView 19 | from rest_framework.authentication import SessionAuthentication 20 | from rest_framework.authentication import BasicAuthentication 21 | 22 | from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned 23 | from django.core.files.uploadedfile import InMemoryUploadedFile 24 | 25 | from rest_framework_bulk import ListBulkCreateAPIView 26 | 27 | from common.storage import get_s3_connection, get_or_create_bucket 28 | from common.models import Project, Settings 29 | from common.tasks import launch_process 30 | from testreport.tasks import create_environment 31 | 32 | from cdws_api.serializers import ProjectSerializer 33 | from cdws_api.serializers import LaunchSerializer 34 | from cdws_api.serializers import LaunchItemSerializer 35 | from cdws_api.serializers import TestResultSerializer 36 | from cdws_api.serializers import TestPlanSerializer 37 | from cdws_api.serializers import AsyncResultSerializer 38 | from cdws_api.serializers import CommentSerializer 39 | from cdws_api.serializers import BugSerializer 40 | from cdws_api.serializers import StageSerializer 41 | from cdws_api.serializers import MetricSerializer, MetricValueSerializer 42 | 43 | from testreport.models import TestPlan 44 | from testreport.models import Launch 45 | from testreport.models import Build 46 | from testreport.models import TestResult 47 | from testreport.models import LaunchItem 48 | from testreport.models import Bug 49 | from testreport.models import INITIALIZED, ASYNC_CALL, INIT_SCRIPT, CONCLUSIVE 50 | from testreport.models import STOPPED, IN_PROGRESS, FINISHED 51 | from testreport.models import get_issue_fields_from_bts 52 | 53 | from stages.models import Stage 54 | 55 | from metrics.models import Metric, MetricValue 56 | from metrics.handlers import HANDLER_CHOICES 57 | from metrics.tasks import restore_metric_values 58 | 59 | from testreport.tasks import finalize_launch 60 | from testreport.tasks import parse_xml 61 | 62 | from django.contrib.contenttypes.models import ContentType 63 | from django.utils import timezone 64 | from django.db.models import Q, Count 65 | 66 | from comments.models import Comment 67 | 68 | from djcelery.models import TaskMeta, CrontabSchedule, PeriodicTask 69 | 70 | from celery.utils import uuid 71 | 72 | from django.conf import settings 73 | from pycd.celery import app 74 | 75 | import datetime 76 | import logging 77 | import celery 78 | import copy 79 | import os 80 | import socket 81 | 82 | 83 | log = logging.getLogger(__name__) 84 | 85 | 86 | class GetOrCreateViewSet(mixins.RetrieveModelMixin, 87 | mixins.UpdateModelMixin, 88 | mixins.DestroyModelMixin, 89 | mixins.ListModelMixin, 90 | mixins.CreateModelMixin, 91 | viewsets.GenericViewSet): 92 | permission_classes = (IsAuthenticatedOrReadOnly, ) 93 | 94 | def find_duplicate(self, serializer): 95 | raise Exception('Please take care about realization \ 96 | of find_duplicate for your instance') 97 | 98 | def create(self, request, *args, **kwargs): 99 | serializer = self.get_serializer(data=request.data) 100 | try: 101 | # :INFO: In any trouble, ask v.reyder for next 2 lines 102 | self.object = self.find_duplicate(serializer) 103 | serializer = serializer.__class__(self.object) 104 | headers = self.get_success_headers(serializer.data) 105 | return Response(serializer.data, status=status.HTTP_200_OK, 106 | headers=headers) 107 | except ObjectDoesNotExist as e: 108 | log.debug(e) 109 | serializer.is_valid(raise_exception=True) 110 | self.perform_create(serializer) 111 | headers = self.get_success_headers(serializer.data) 112 | return Response(serializer.data, status=status.HTTP_201_CREATED, 113 | headers=headers) 114 | 115 | 116 | class ProjectViewSet(GetOrCreateViewSet): 117 | queryset = Project.objects.all() 118 | serializer_class = ProjectSerializer 119 | model = Project 120 | 121 | filter_backends = (DjangoFilterBackend, ) 122 | filter_fields = ('name', ) 123 | 124 | def find_duplicate(self, serializer): 125 | return Project.objects.get(name=serializer.initial_data['name']) 126 | 127 | @detail_route(methods=['post'], 128 | permission_classes=[DjangoModelPermissionsOrAnonReadOnly], 129 | url_path='settings') 130 | def set_settings(self, request, pk=None): 131 | (settings, new) = Settings.objects.get_or_create( 132 | project=Project.objects.get(id=pk), key=request.data['key']) 133 | settings.value = request.data['value'] 134 | settings.save() 135 | return Response(status=status.HTTP_201_CREATED, data={'message': 'ok'}) 136 | 137 | @detail_route(methods=['post'], 138 | permission_classes=[DjangoModelPermissionsOrAnonReadOnly], 139 | url_path='settings/delete') 140 | def delete_settings(self, request, pk=None): 141 | settings = Settings.objects.filter( 142 | project=Project.objects.get(id=pk), 143 | key=request.data['key'], value=request.data['value']) 144 | settings.delete() 145 | return Response(status=status.HTTP_200_OK, data={'message': 'ok'}) 146 | 147 | 148 | class TestPlanViewSet(GetOrCreateViewSet): 149 | queryset = TestPlan.objects.all() 150 | serializer_class = TestPlanSerializer 151 | model = TestPlan 152 | 153 | filter_backends = (DjangoFilterBackend, ) 154 | filter_fields = ('id', 'project', 'name') 155 | 156 | @list_route(methods=['get']) 157 | def custom_list(self, request, *args, **kwargs): 158 | if 'project_id__in' in request.GET \ 159 | and request.GET['project_id__in'] != '': 160 | self.queryset = self.queryset.filter( 161 | project_id__in=request.GET['project_id__in'].split(',')) 162 | if 'id__in' in request.GET and request.GET['id__in'] != '': 163 | self.queryset = self.queryset.filter( 164 | id__in=request.GET['id__in'].split(',')) 165 | return self.list(request, *args, **kwargs) 166 | 167 | def create(self, request, *args, **kwargs): 168 | request.data['owner'] = request.user.id 169 | return super(TestPlanViewSet, self).create(request, *args, **kwargs) 170 | 171 | def find_duplicate(self, serializer): 172 | if 'name' not in serializer.initial_data \ 173 | or 'project' not in serializer.initial_data: 174 | raise ObjectDoesNotExist 175 | return TestPlan.objects.get( 176 | name=serializer.initial_data['name'], 177 | project=serializer.initial_data['project']) 178 | 179 | @detail_route(methods=['post'], 180 | permission_classes=[IsAuthenticatedOrReadOnly]) 181 | def execute(self, request, pk=None): 182 | workspace_path = os.path.join( 183 | settings.CDWS_WORKING_DIR, 184 | timezone.now().strftime('%Y-%m-%d-%H-%M-%f')) 185 | post_data = request.data 186 | options = request.data['options'] 187 | json_file = None 188 | if 'json_file' in post_data: 189 | json_file = post_data['json_file'] 190 | 191 | test_plan = TestPlan.objects.get(pk=pk) 192 | 193 | # launch create 194 | launch = Launch(test_plan=test_plan, 195 | started_by=options['started_by'], 196 | state=INITIALIZED) 197 | launch.save() 198 | 199 | build = Build(launch=launch, 200 | version=options.get('version'), 201 | branch=options.get('branch'), 202 | hash=options.get('hash')) 203 | build.save() 204 | 205 | # env create 206 | env = {'WORKSPACE': 207 | os.path.join(settings.CDWS_DEPLOY_DIR, workspace_path), 208 | 'HOME': 209 | os.path.join(settings.CDWS_DEPLOY_DIR, workspace_path)} 210 | if 'env' in post_data: 211 | for key, value in iter(post_data['env'].items()): 212 | env[key] = value 213 | env['REPORT_API_URL'] = 'https://{0}/{1}'.format( 214 | settings.CDWS_API_HOSTNAME, settings.CDWS_API_PATH) 215 | # environment values should be string for exec 216 | env['TESTPLAN_ID'] = str(test_plan.id) 217 | env['LAUNCH_ID'] = str(launch.id) 218 | env['WORKSPACE_URL'] = 'http://{}/{}/'.format( 219 | settings.CELERY_HOST, workspace_path) 220 | 221 | # queryset create 222 | if 'launch_items' in post_data: 223 | try: 224 | launch_items = test_plan.launchitem_set.filter( 225 | id__in=post_data['launch_items']).order_by('id') 226 | except (KeyError, ValueError) as e: 227 | return Response(status=status.HTTP_400_BAD_REQUEST, 228 | data={'message': '{}'.format(e)}) 229 | else: 230 | launch_items = test_plan.launchitem_set.all().order_by('id') 231 | 232 | mapping = {} 233 | init_task = None 234 | async_tasks = [] 235 | conclusive_tasks = [] 236 | 237 | create_env_task = create_environment.subtask( 238 | [env, json_file], immutable=True, soft_time_limit=1200) 239 | final_task = finalize_launch.subtask( 240 | [launch.id], {}, soft_time_limit=3600, immutable=True) 241 | 242 | is_init_task_present = False 243 | for launch_item in launch_items: 244 | item_uuid = uuid() 245 | # Write LAUNCH_ITEM_ID to environment of each process 246 | item_env = copy.copy(env) 247 | item_env['LAUNCH_ITEM_ID'] = str(launch_item.id) 248 | subtask = launch_process.subtask( 249 | [launch_item.command, launch_item.type], {'env': item_env}, 250 | immutable=True, 251 | soft_time_limit=launch_item.timeout, 252 | options={'task_id': item_uuid}) 253 | 254 | if launch_item.type == INIT_SCRIPT: 255 | if not is_init_task_present: 256 | is_init_task_present = True 257 | init_task = subtask 258 | mapping[item_uuid] = launch_item.id 259 | elif launch_item.type == ASYNC_CALL: 260 | async_tasks.append(subtask) 261 | mapping[item_uuid] = launch_item.id 262 | elif launch_item.type == CONCLUSIVE: 263 | conclusive_tasks.append(subtask) 264 | mapping[item_uuid] = launch_item.id 265 | else: 266 | msg = ('There is launch item with type {0} which not ' 267 | 'supported, please fix this.').format(launch_item.type) 268 | return Response(status=status.HTTP_400_BAD_REQUEST, 269 | data={'message': msg}) 270 | # update launch 271 | launch.set_tasks(mapping) 272 | launch.set_parameters({ 273 | 'options': options, 274 | 'env': {} if 'env' not in post_data else post_data['env'], 275 | 'json_file': json_file 276 | }) 277 | launch.save() 278 | 279 | # error handling 280 | if init_task is None: 281 | msg = ('Initial script for test plan "{0}" with id "{1}" ' 282 | 'does not exist or not selected. ' 283 | 'Currently selected items: {2}').format( 284 | test_plan.name, test_plan.id, launch_items) 285 | launch.delete() 286 | return Response(status=status.HTTP_400_BAD_REQUEST, 287 | data={'message': msg}) 288 | # pass sequence 289 | sequence = [create_env_task, init_task, celery.group(async_tasks)] 290 | sequence += conclusive_tasks 291 | sequence.append(final_task) 292 | 293 | try: 294 | log.info("Chain={}".format(celery.chain(sequence)())) 295 | except Exception as e: 296 | return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, 297 | data={'message': '{}'.format(e)}) 298 | 299 | return Response(data={'launch_id': launch.id}, 300 | status=status.HTTP_200_OK) 301 | 302 | 303 | class LaunchViewSet(viewsets.ModelViewSet): 304 | queryset = Launch.objects.all() 305 | serializer_class = LaunchSerializer 306 | filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter) 307 | filter_fields = ('test_plan', 'id', 'created', 'state', 308 | 'build__version', 'build__hash', 'build__branch') 309 | search_fields = ('started_by',) 310 | 311 | @detail_route(methods=['get'], 312 | permission_classes=[IsAuthenticatedOrReadOnly]) 313 | def terminate_tasks(self, request, pk=None): 314 | try: 315 | launch = Launch.objects.get(id=pk) 316 | tasks = {} 317 | for key, v in iter(launch.get_tasks().items()): 318 | # Don't save tasks with status PENDING, due PENDING mean 319 | # unknown status too. 320 | if app.AsyncResult(key).state != 'PENDING': 321 | tasks[key] = v 322 | app.control.revoke(key, terminate=True, signal='SIGTERM') 323 | launch.set_tasks(tasks) 324 | launch.save() 325 | finalize_launch(pk, STOPPED) 326 | except Launch.DoesNotExist: 327 | return Response( 328 | data={ 329 | 'message': 'Launch with id={} does not exist'.format(pk)}, 330 | status=status.HTTP_404_NOT_FOUND) 331 | except Exception as e: 332 | return Response( 333 | data={ 334 | 'message': 'Unable to terminate tasks for launch id={},' 335 | ' due to {}'.format(pk, e)}, 336 | status=status.HTTP_500_INTERNAL_SERVER_ERROR) 337 | return Response( 338 | data={'message': 'Termination done.'}, 339 | status=status.HTTP_200_OK) 340 | 341 | @list_route(methods=['get']) 342 | def custom_list(self, request, *args, **kwargs): 343 | if 'days' in request.GET: 344 | delta = datetime.datetime.today() - datetime.timedelta( 345 | days=int(request.GET['days'])) 346 | self.queryset = self.queryset.filter(created__gt=delta) 347 | if 'testplan_id__in' in request.GET \ 348 | and request.GET['testplan_id__in'] != '': 349 | self.queryset = self.queryset.filter( 350 | test_plan_id__in=request.GET['testplan_id__in'].split(',')) 351 | if 'from' in request.GET: 352 | from_date = request.GET['from'] 353 | to_date = datetime.datetime.today() 354 | if 'to' in request.GET: 355 | to_date = request.GET['to'] 356 | self.queryset = self.queryset.filter( 357 | created__range=(from_date, to_date)) 358 | if 'build_hash__in' in request.GET \ 359 | and request.GET['build_hash__in'] != '': 360 | self.queryset = self.queryset.filter( 361 | build__hash__in=request.GET['build_hash__in'].split(',')) 362 | if 'results_group_count' in request.GET \ 363 | and request.GET['results_group_count'] != '': 364 | launch = Launch.objects.get(id=request.GET['results_group_count']) 365 | 366 | results = TestResult.objects.\ 367 | filter(launch=launch, state=request.GET['state']).\ 368 | values('launch_item_id').\ 369 | annotate(count=Count('launch_item_id')) 370 | return Response(data={'results': results}, 371 | status=status.HTTP_200_OK) 372 | return self.list(request, *args, **kwargs) 373 | 374 | @detail_route(methods=['get'], 375 | permission_classes=[IsAuthenticatedOrReadOnly]) 376 | def calculate_counts(self, request, pk=None): 377 | try: 378 | Launch.objects.get(id=pk).calculate_counts() 379 | except Launch.DoesNotExist: 380 | return Response( 381 | data={ 382 | 'message': 'Launch with id {} does not exist'.format(pk)}, 383 | status=status.HTTP_404_NOT_FOUND) 384 | return Response( 385 | data={'message': 'Calculation done.'}, 386 | status=status.HTTP_200_OK) 387 | 388 | @detail_route(methods=['post'], 389 | permission_classes=[IsAuthenticatedOrReadOnly]) 390 | def update_metrics(self, request, pk=None): 391 | if 'metrics' in request.data and request.data['metrics'] != '': 392 | if type(request.data['metrics']) is not dict: 393 | return Response( 394 | status=status.HTTP_400_BAD_REQUEST, 395 | data={ 396 | 'message': 'Invalid format for metrics \'{0}\',' 397 | ' expect object'.format(request.data['metrics'])}) 398 | try: 399 | launch = Launch.objects.get(id=pk) 400 | params = launch.get_parameters() 401 | params['metrics'] = request.data['metrics'] 402 | launch.set_parameters(params) 403 | launch.save() 404 | return Response(status=status.HTTP_200_OK, 405 | data=LaunchSerializer(launch).data) 406 | except Launch.DoesNotExist: 407 | return Response( 408 | data={'message': 409 | 'Launch with id={} does not exist'.format(pk)}, 410 | status=status.HTTP_404_NOT_FOUND) 411 | return Response(status=status.HTTP_400_BAD_REQUEST, 412 | data={'message': 'No metrics in post request: ' 413 | '{0}'.format(request.data)}) 414 | 415 | 416 | class TestResultViewSet(ListBulkCreateAPIView, 417 | viewsets.GenericViewSet, 418 | mixins.RetrieveModelMixin): 419 | queryset = TestResult.objects.all() 420 | serializer_class = TestResultSerializer 421 | model = TestResult 422 | filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter) 423 | search_fields = ('$suite', '$name', '$failure_reason') 424 | filter_fields = ('id', 'state', 'name', 'launch', 425 | 'duration', 'launch_item_id') 426 | 427 | @list_route(methods=['get']) 428 | def custom_list(self, request, *args, **kwargs): 429 | days = 100 430 | if 'launch_id__in' in request.GET \ 431 | and request.GET['launch_id__in'] != '': 432 | self.queryset = self.queryset.filter( 433 | launch_id__in=request.GET['launch_id__in'].split(',')) 434 | if 'state__in' in request.GET and request.GET['state__in'] != '': 435 | self.queryset = self.queryset.filter( 436 | state__in=request.GET['state__in'].split(',')) 437 | if 'days' in request.GET and request.GET['days'] != '': 438 | days = int(request.GET['days']) 439 | if 'history' in request.GET and request.GET['history'] != '': 440 | result = TestResult.objects.get(id=request.GET['history']) 441 | launch = Launch.objects.get(id=result.launch_id) 442 | delta = datetime.datetime.today() - datetime.timedelta(days=days) 443 | launches = Launch.objects.filter( 444 | test_plan_id=launch.test_plan_id, created__gt=delta) 445 | 446 | ids = [] 447 | for l in launches: 448 | ids.append(l.id) 449 | 450 | self.queryset = self.queryset.\ 451 | filter(launch_id__in=ids).\ 452 | filter(name=result.name, suite=result.suite).\ 453 | order_by('-launch') 454 | return self.list(request, *args, **kwargs) 455 | 456 | 457 | class TestResultNegativeViewSet(TestResultViewSet): 458 | search_fields = ('$failure_reason', ) 459 | 460 | 461 | class LaunchItemViewSet(viewsets.ModelViewSet): 462 | queryset = LaunchItem.objects.all() 463 | serializer_class = LaunchItemSerializer 464 | permission_classes = (IsAuthenticatedOrReadOnly, ) 465 | 466 | filter_backends = (DjangoFilterBackend, OrderingFilter) 467 | filter_fields = ('id', 'name', 'test_plan', 'type') 468 | 469 | 470 | class TaskResultViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin): 471 | serializer_class = AsyncResultSerializer 472 | queryset = TaskMeta.objects.all() 473 | 474 | def retrieve(self, request, *args, **kwargs): 475 | log.info(kwargs) 476 | serializer = self.get_serializer( 477 | launch_process.AsyncResult(kwargs['pk'])) 478 | return Response(serializer.data) 479 | 480 | 481 | class CommentViewSet(viewsets.ModelViewSet): 482 | queryset = Comment.objects.all() 483 | serializer_class = CommentSerializer 484 | permission_classes = (IsAuthenticatedOrReadOnly, ) 485 | 486 | filter_backends = (DjangoFilterBackend, OrderingFilter,) 487 | filter_fields = ('id', 'user', 'content_type', 'object_pk') 488 | 489 | def create(self, request, *args, **kwargs): 490 | ct = ContentType.objects.get(name__exact=request.data['content_type']) 491 | request.data['content_type'] = ct.id 492 | request.data['user'] = request.user.id 493 | return super(CommentViewSet, self).create(request, *args, **kwargs) 494 | 495 | 496 | class BugViewSet(viewsets.ModelViewSet): 497 | queryset = Bug.objects.all() 498 | serializer_class = BugSerializer 499 | permission_classes = (IsAuthenticatedOrReadOnly, ) 500 | filter_backends = (DjangoFilterBackend, OrderingFilter, ) 501 | filter_fields = ('id', 'externalId') 502 | 503 | def create(self, request, *args, **kwargs): 504 | log.info('Check issue {} for existing'. 505 | format(request.data['externalId'])) 506 | response = get_issue_fields_from_bts(request.data['externalId']) 507 | 508 | errors = [] 509 | if 'errors' in response: 510 | errors += response['errors'] 511 | if 'errorMessages' in response: 512 | errors += response['errorMessages'] 513 | if len(errors) != 0: 514 | return Response( 515 | data={'message': '\n'.join(errors)}, 516 | status=status.HTTP_400_BAD_REQUEST) 517 | 518 | Bug.objects.create(externalId=request.data['externalId'], 519 | regexp=request.data['regexp'], 520 | state=response['status']['name'], 521 | name=response['summary']) 522 | return Response(status=status.HTTP_201_CREATED) 523 | 524 | @list_route(methods=['get']) 525 | def custom_list(self, request, *args, **kwargs): 526 | if 'issue_names__in' in request.GET \ 527 | and request.GET['issue_names__in'] != '': 528 | issue_names = request.GET['issue_names__in'].split(',') 529 | query = Q() 530 | for issue_name in issue_names: 531 | query = query | Q(externalId__startswith=issue_name) 532 | self.queryset = Bug.objects.filter(query) 533 | 534 | return self.list(request, *args, **kwargs) 535 | 536 | 537 | class StageViewSet(GetOrCreateViewSet): 538 | queryset = Stage.objects.all() 539 | serializer_class = StageSerializer 540 | 541 | filter_backends = (DjangoFilterBackend, ) 542 | filter_fields = ('id', 'project') 543 | 544 | def find_duplicate(self, serializer): 545 | return Stage.objects.get( 546 | name=serializer.init_data['name'], 547 | project=serializer.init_data['project']) 548 | 549 | 550 | class UnsafeSessionAuthentication(SessionAuthentication): 551 | def enforce_csrf(self, request): 552 | return 553 | 554 | 555 | class JenkinsViewSet(APIView): 556 | authentication_classes = (UnsafeSessionAuthentication,) 557 | 558 | def post(self, request, format=None, project=None): 559 | name = request.data['name'] 560 | build_phase = request.data['build']['phase'] 561 | build_full_url = request.data['build']['full_url'] 562 | 563 | try: 564 | project = Project.objects.get(name=project) 565 | except ObjectDoesNotExist: 566 | return Response( 567 | data={'message': 'Project {0} does not exist'.format(project)}, 568 | status=status.HTTP_404_NOT_FOUND) 569 | 570 | try: 571 | build_number = Settings.objects.get( 572 | project=project, key='current_build').value 573 | except ObjectDoesNotExist: 574 | build_number = '' 575 | 576 | log.debug(build_number) 577 | (stage, new) = Stage.objects.get_or_create( 578 | name=name, project=project) 579 | 580 | stage.link = build_full_url 581 | stage.state = self._get_build_state(request.data) 582 | if build_number != '': 583 | stage.text = '{0} (build {1})'.format(build_phase, build_number) 584 | else: 585 | stage.text = '{0}'.format(build_phase) 586 | stage.save() 587 | 588 | return Response(data={'message': 'Done.'}, 589 | status=status.HTTP_201_CREATED) 590 | 591 | def _get_build_state(self, data): 592 | if 'status' not in data['build']: 593 | return 'warning' 594 | status = data['build']['status'] 595 | if status == 'SUCCESS': 596 | return 'success' 597 | elif status == 'FAILURE': 598 | return 'danger' 599 | else: 600 | return 'warning' 601 | 602 | 603 | class CustomXmlParser(BaseParser): 604 | media_type = 'text/xml' 605 | 606 | def parse(self, stream, media_type=None, parser_context=None): 607 | """ 608 | Parses the incoming bytestream as XML and returns the resulting data. 609 | """ 610 | parser_context = parser_context or {} 611 | encoding = parser_context.get('encoding', 'utf-8') 612 | parser = ElementTree.DefusedXMLParser(encoding=encoding) 613 | try: 614 | tree = ElementTree.parse(stream, parser=parser, forbid_dtd=True) 615 | except (ElementTree.ParseError, ValueError) as e: 616 | raise ParseError('XML parse error - {}'.format(e)) 617 | return tree.getroot() 618 | 619 | 620 | class RundeckViewSet(APIView): 621 | authentication_classes = (UnsafeSessionAuthentication,) 622 | renderer_classes = (XMLRenderer,) 623 | parser_classes = (CustomXmlParser, ) 624 | 625 | def post(self, request, format=None, project=None): 626 | root = request.data 627 | job_status = 'unknown' 628 | group_name = 'unknown' 629 | href = settings.RUNDECK_URL 630 | for child in root.iter(): 631 | if child.tag == 'execution': 632 | job_status = child.attrib['status'] 633 | href = child.attrib['href'] 634 | elif child.tag == 'job': 635 | for group in child.iter('group'): 636 | group_name = group.text 637 | 638 | try: 639 | project = Project.objects.get(name=project) 640 | except ObjectDoesNotExist: 641 | return Response(status=status.HTTP_404_NOT_FOUND) 642 | 643 | try: 644 | build_number = Settings.objects.get( 645 | project=project, key='current_build').value 646 | except ObjectDoesNotExist: 647 | build_number = '' 648 | 649 | (stage, new) = Stage.objects.get_or_create( 650 | name=group_name, project=project) 651 | 652 | stage.link = href 653 | stage.state = self._get_build_state(job_status) 654 | if build_number != '': 655 | stage.text = '{0} (build {1})'.format( 656 | job_status.upper(), build_number) 657 | else: 658 | stage.text = '{}'.format(job_status.upper()) 659 | # if job_status != 'succeeded': 660 | # stage.text = '%s, AVG %s sec. (build %s)' % 661 | # (job_status, float(duration) / 1000, build_number, ) 662 | stage.save() 663 | return Response(data={'message': 'Done.'}, 664 | status=status.HTTP_201_CREATED) 665 | 666 | def _get_build_state(self, status): 667 | if status == 'succeeded': 668 | return 'success' 669 | elif status == 'failed': 670 | return 'danger' 671 | else: 672 | return 'warning' 673 | 674 | 675 | class MetricViewSet(viewsets.ModelViewSet): 676 | queryset = Metric.objects.all() 677 | serializer_class = MetricSerializer 678 | permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) 679 | filter_backends = (DjangoFilterBackend, OrderingFilter, ) 680 | filter_fields = ('project', ) 681 | 682 | def crontab_create(self, schedule): 683 | cron_template = schedule.split() 684 | (crontab, new) = CrontabSchedule.objects.get_or_create( 685 | minute=cron_template[0], 686 | hour=cron_template[1], 687 | day_of_month=cron_template[2], 688 | month_of_year=cron_template[3], 689 | day_of_week=cron_template[4] 690 | ) 691 | return crontab 692 | 693 | def create(self, request, *args, **kwargs): 694 | if 'project' not in request.data: 695 | return Response( 696 | status=status.HTTP_400_BAD_REQUEST, 697 | data={'message': 'Field "project" is required'}) 698 | if 'name' not in request.data or request.data['name'] == '': 699 | return Response( 700 | status=status.HTTP_400_BAD_REQUEST, 701 | data={'message': 'Field "name" is required'}) 702 | if not any(request.data['handler'] in choice 703 | for choice in HANDLER_CHOICES): 704 | return Response( 705 | status=status.HTTP_400_BAD_REQUEST, 706 | data={'message': 'Handler "{}" is not a valid choice' 707 | .format(request.data['handler'])}) 708 | project = Project.objects.get(pk=request.data['project']) 709 | crontab = self.crontab_create(request.data['schedule']) 710 | 711 | periodic_task = PeriodicTask.objects.create( 712 | name=uuid(), 713 | task='metrics.tasks.run_metric_calculation', 714 | crontab_id=crontab.id, 715 | enabled=True 716 | ) 717 | 718 | message = {'message': 'Metric already exist, choose another name'} 719 | try: 720 | Metric.objects.get(project=project, name=request.data['name']) 721 | return Response(status=status.HTTP_400_BAD_REQUEST, 722 | data=message) 723 | except ObjectDoesNotExist: 724 | metric = Metric.objects.create( 725 | name=request.data['name'], 726 | project=project, 727 | schedule=periodic_task, 728 | query=request.data['query'], 729 | handler=request.data['handler'], 730 | weight=request.data['weight'], 731 | ) 732 | periodic_task.args = [metric.id] 733 | periodic_task.save() 734 | 735 | if 'query_period' in request.data: 736 | restore_metric_values.apply_async(args=[ 737 | metric.id, 738 | request.data['query_period'], 739 | request.data['query_step'], 740 | request.data['handler'], 741 | request.data['query_field']]) 742 | 743 | return Response(status=status.HTTP_201_CREATED, 744 | data=MetricSerializer(metric).data) 745 | except MultipleObjectsReturned: 746 | return Response(status=status.HTTP_400_BAD_REQUEST, data=message) 747 | except Exception as e: 748 | log.error(e) 749 | return Response( 750 | status=status.HTTP_500_INTERNAL_SERVER_ERROR, 751 | data={'message': e}) 752 | 753 | def update(self, request, pk=None, *args, **kwargs): 754 | try: 755 | metric = Metric.objects.get(pk=pk) 756 | except ObjectDoesNotExist: 757 | return Response(status=status.HTTP_404_NOT_FOUND, 758 | data={'message': 'Metric not found'}) 759 | 760 | if 'schedule' in request.data: 761 | crontab = self.crontab_create(request.data['schedule']) 762 | periodic_task = PeriodicTask.objects.get(id=metric.schedule_id) 763 | periodic_task.crontab_id = crontab.id 764 | periodic_task.save() 765 | request.data['schedule'] = periodic_task.id 766 | 767 | message = {'message': 'Metric already exist, choose another name'} 768 | try: 769 | Metric.objects.exclude(pk=metric.id)\ 770 | .get(project=metric.project_id, name=request.data['name']) 771 | return Response(status=status.HTTP_400_BAD_REQUEST, 772 | data=message) 773 | except ObjectDoesNotExist: 774 | return super(MetricViewSet, self).update(request, *args, **kwargs) 775 | except MultipleObjectsReturned: 776 | return Response(status=status.HTTP_400_BAD_REQUEST, data=message) 777 | except Exception as e: 778 | log.error(e) 779 | return Response( 780 | status=status.HTTP_500_INTERNAL_SERVER_ERROR, 781 | data={'message': e}) 782 | 783 | def destroy(self, request, *args, **kwargs): 784 | instance = self.get_object() 785 | self.perform_destroy(instance) 786 | 787 | PeriodicTask.objects.get(pk=instance.schedule_id).delete() 788 | 789 | return Response( 790 | status=status.HTTP_200_OK, 791 | data={'message': 'Metric and all values deleted'}) 792 | 793 | 794 | class MetricValueViewSet(viewsets.ModelViewSet): 795 | queryset = MetricValue.objects.all() 796 | serializer_class = MetricValueSerializer 797 | filter_backends = (DjangoFilterBackend, OrderingFilter, ) 798 | filter_fields = ('metric_id', ) 799 | 800 | @list_route(methods=['get']) 801 | def custom_list(self, request, *args, **kwargs): 802 | if 'days' in request.GET: 803 | delta = datetime.datetime.today() - datetime.timedelta( 804 | days=int(request.GET['days'])) 805 | self.queryset = self.queryset.filter(created__gt=delta) 806 | if 'from' in request.GET: 807 | from_date = request.GET['from'] 808 | to_date = datetime.datetime.today() 809 | if 'to' in request.GET: 810 | to_date = request.GET['to'] 811 | self.queryset = self.queryset.filter( 812 | created__range=(from_date, to_date)) 813 | return self.list(request, *args, **kwargs) 814 | 815 | 816 | class UnsafeBasicAuthentication(BasicAuthentication): 817 | def enforce_csrf(self, request): 818 | return 819 | 820 | 821 | class ReportFileViewSet(APIView): 822 | authentication_classes = (UnsafeBasicAuthentication,) 823 | permission_classes = (IsAuthenticatedOrReadOnly, ) 824 | parser_classes = (FileUploadParser,) 825 | 826 | def get_launch(self, launch_id): 827 | return Launch.objects.get(id=launch_id) 828 | 829 | def create_launch(self, plan_id, state): 830 | launch = Launch.objects.create( 831 | test_plan_id=plan_id, state=state, 832 | started_by='http://{}'.format(socket.getfqdn())) 833 | return launch 834 | 835 | def post(self, request, filename, testplan_id=None, xunit_format=None): 836 | s3_connection = get_s3_connection() 837 | file_obj = request.data['file'] 838 | 839 | launch_id = None 840 | params = None 841 | if xunit_format not in \ 842 | ['junit', 'nunit', 'qttestxunit', 'xcprettyjunit']: 843 | return Response(data={'message': 'Unknown file format'}, 844 | status=status.HTTP_400_BAD_REQUEST) 845 | if 'file' not in request.data: 846 | return Response(status=status.HTTP_400_BAD_REQUEST, 847 | data={'message': 'No file or empty file received'}) 848 | if 'launch' in request.data: 849 | launch_id = request.data['launch'] 850 | if 'data' in request.data: 851 | if isinstance(request.data['data'], InMemoryUploadedFile): 852 | params = request.data['data'].read().decode('utf8') 853 | elif request.data['data'] != '': 854 | params = request.data['data'] 855 | 856 | log.info('Create launch') 857 | if testplan_id is not None: 858 | state = IN_PROGRESS if s3_connection is not None else FINISHED 859 | if launch_id is not None: 860 | launch = self.get_launch(launch_id) 861 | else: 862 | launch = self.create_launch(testplan_id, state=state) 863 | 864 | if s3_connection is not None: 865 | bucket = get_or_create_bucket(s3_connection) 866 | report_key = bucket.new_key(uuid()) 867 | report_key.set_contents_from_string(file_obj.read()) 868 | 869 | log.debug('Xml file "{}" created in bucket "{}"'.format( 870 | report_key.name, settings.S3_BUCKET_NAME)) 871 | 872 | parse_xml.apply_async(kwargs={'s3_conn': True, 873 | 's3_key_name': report_key.name, 874 | 'xunit_format': xunit_format, 875 | 'launch_id': launch.id, 876 | 'params': params}) 877 | else: 878 | log.info('Connection to storage is not set in settings, ' 879 | 'parse xml synchronously') 880 | parse_xml(xunit_format=xunit_format, 881 | launch_id=launch.id, 882 | params=params, 883 | file_content=file_obj.read()) 884 | return Response(status=status.HTTP_200_OK, 885 | data={'launch_id': launch.id}) 886 | return Response(status=status.HTTP_400_BAD_REQUEST) 887 | -------------------------------------------------------------------------------- /cdws_api/xml_parser.py: -------------------------------------------------------------------------------- 1 | from testreport.models import Launch, TestResult, Build 2 | from testreport.models import FINISHED 3 | from testreport.models import PASSED, FAILED, SKIPPED, BLOCKED 4 | 5 | from django.conf import settings 6 | 7 | import datetime 8 | import logging 9 | import socket 10 | import xml.dom.minidom 11 | import json 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def get_launch(launch_id): 17 | return Launch.objects.get(id=launch_id) 18 | 19 | 20 | def create_launch(plan_id): 21 | launch = Launch.objects.create( 22 | test_plan_id=plan_id, state=FINISHED, 23 | started_by='http://{}'.format(socket.getfqdn()), 24 | finished=datetime.datetime.now()) 25 | return launch 26 | 27 | 28 | class XmlParser: 29 | buffer = [] 30 | launch_id = None 31 | buffer_size = 100 32 | total_duration = 0 33 | 34 | def __init__(self, launch_id): 35 | self.launch_id = launch_id 36 | 37 | def load_string(self, file_content): 38 | log.info('Loading file_content') 39 | dom = xml.dom.minidom.parseString(file_content) 40 | self.parse(dom) 41 | 42 | def get_node(self, element, names): 43 | for node in element.childNodes: 44 | if node.nodeName in names: 45 | return node 46 | return None 47 | 48 | def get_text(self, nodelist): 49 | rc = [] 50 | for node in nodelist: 51 | if node.nodeType in [node.TEXT_NODE, node.CDATA_SECTION_NODE, 52 | node.COMMENT_NODE]: 53 | rc.append(node.data) 54 | return ''.join(rc) 55 | 56 | def update_duration(self, launch): 57 | log.info('Updating total duration for launch {}'.format(launch.id)) 58 | if launch.duration is None: 59 | launch.duration = self.total_duration 60 | launch.save() 61 | 62 | 63 | class JunitParser(XmlParser): 64 | def parse(self, element, path=''): 65 | if element.nodeName == 'testcase': 66 | self.create_test_result(element, path) 67 | if element.nodeName == 'testsuite': 68 | path += element.getAttribute('name') + '/' 69 | if element.hasChildNodes(): 70 | for node in element.childNodes: 71 | if node.nodeType == node.ELEMENT_NODE: 72 | self.parse(node, path) 73 | 74 | def create_test_result(self, element, path): 75 | result = TestResult.objects.create(launch_id=self.launch_id) 76 | duration = element.getAttribute('time') 77 | 78 | if duration == '': 79 | result.duration = 0 80 | else: 81 | result.duration = duration 82 | self.total_duration += float(result.duration) 83 | 84 | result.name = element.getAttribute('name')[:127] 85 | result.suite = path[:125] 86 | result.state = BLOCKED 87 | result.failure_reason = '' 88 | 89 | failure = self.get_node(element, ['failure']) 90 | error = self.get_node(element, ['error']) 91 | skipped = self.get_node(element, ['skipped']) 92 | if skipped is not None: 93 | result.state = SKIPPED 94 | result.failure_reason = self.get_text(skipped.childNodes) 95 | elif failure is not None: 96 | result.state = FAILED 97 | result.failure_reason = self.get_text(failure.childNodes) 98 | elif error is not None: 99 | result.failure_reason = self.get_text(error.childNodes) 100 | else: 101 | result.state = PASSED 102 | 103 | if not element.getAttribute('format'): 104 | system_out = self.get_node(element, 'system-out') 105 | if system_out is not None: 106 | result.failure_reason += self.get_text(system_out.childNodes) 107 | 108 | result.save() 109 | 110 | 111 | class QtTestXunitParser(JunitParser): 112 | def create_test_result(self, element, path): 113 | result = TestResult.objects.create(launch_id=self.launch_id) 114 | 115 | result.duration = 0 116 | result.name = element.getAttribute('name')[:127] 117 | result.suite = path[:125] 118 | result.state = BLOCKED 119 | result.failure_reason = '' 120 | 121 | test_result = element.getAttribute('result') 122 | if test_result == 'pass': 123 | result.state = PASSED 124 | elif test_result == '': 125 | result.state = SKIPPED 126 | result.failure_reason = self.get_text(element.childNodes) 127 | elif test_result == 'fail': 128 | result.state = FAILED 129 | failure = self.get_node(element, ['failure']) 130 | logs = self.get_text(element.childNodes) 131 | failure_reason = failure.getAttribute('message') 132 | if logs: 133 | failure_reason = '{}\n\nLogs:{}'.format(failure_reason, logs) 134 | result.failure_reason = failure_reason 135 | 136 | result.save() 137 | 138 | 139 | class XCPrettyJunitParser(JunitParser): 140 | def create_test_result(self, element, path): 141 | """ 142 | https://github.com/supermarin/xcpretty/blob/master/lib/xcpretty/reporters/junit.rb 143 | """ 144 | result = TestResult.objects.create(launch_id=self.launch_id) 145 | 146 | result.duration = 0 147 | result.name = element.getAttribute('name')[:127] 148 | result.suite = path[:125] 149 | result.state = BLOCKED 150 | result.failure_reason = '' 151 | 152 | failure = self.get_node(element, ['failure']) 153 | skipped = self.get_node(element, ['skipped']) 154 | if skipped is not None: 155 | result.state = SKIPPED 156 | elif failure is not None: 157 | result.state = FAILED 158 | failure_reason = failure.getAttribute('message') 159 | failure_code_line = self.get_text(failure.childNodes) 160 | result.failure_reason = "{}\n\n{}".format(failure_code_line, 161 | failure_reason) 162 | else: 163 | result.state = PASSED 164 | 165 | result.save() 166 | 167 | 168 | class NunitParser(XmlParser): 169 | def parse(self, element, path=''): 170 | if element.nodeName == 'test-case': 171 | self.create_test_result(element) 172 | if element.hasChildNodes(): 173 | for node in element.childNodes: 174 | if node.nodeType == node.ELEMENT_NODE: 175 | self.parse(node, path) 176 | 177 | def create_test_result(self, element): 178 | result = TestResult.objects.create(launch_id=self.launch_id) 179 | result.state = BLOCKED 180 | result.failure_reason = '' 181 | duration = element.getAttribute('time') 182 | 183 | if duration == '': 184 | result.duration = 0 185 | else: 186 | result.duration = duration 187 | self.total_duration += float(result.duration) 188 | 189 | if element.getAttribute('result') in ['Ignored', 'Inconclusive']: 190 | if element.getAttribute('result') == 'Ignored': 191 | result.state = SKIPPED 192 | if element.getAttribute('result') == 'Inconclusive': 193 | result.state = BLOCKED 194 | reason = self.get_node(element, ['reason']) 195 | message = self.get_node(reason, ['message']) 196 | result.failure_reason = self.get_text(message.childNodes) 197 | 198 | if element.getAttribute('result') in ['Failure', 'Error']: 199 | if element.getAttribute('result') == 'Failure': 200 | result.state = FAILED 201 | if element.getAttribute('result') == 'Error': 202 | result.state = BLOCKED 203 | failure = self.get_node(element, ['failure']) 204 | message = self.get_node(failure, ['message']) 205 | trace = self.get_node(failure, ['stack-trace']) 206 | failure_reason = self.get_text(message.childNodes) 207 | if trace is not None and self.get_text(trace.childNodes) != '': 208 | failure_reason += '\n\nStackTrace:\n' 209 | failure_reason += self.get_text(trace.childNodes) 210 | result.failure_reason = failure_reason 211 | 212 | if element.getAttribute('result') == 'Success': 213 | result.state = PASSED 214 | 215 | result.name = element.getAttribute('name')[:127] 216 | result.save() 217 | 218 | 219 | def xml_parser_func(format, file_content, launch_id, params): 220 | launch = get_launch(launch_id) 221 | if params is not None and launch.parameters == '{}': 222 | launch.parameters = params 223 | params_json = json.loads(params) 224 | if 'options' in params_json \ 225 | and params_json['options']['started_by'] != '': 226 | if params_json['options'].get('duration') is not None: 227 | launch.duration = float(params_json['options']['duration']) 228 | launch.started_by = params_json['options']['started_by'] 229 | 230 | commits = params_json['options'].get('last_commits') 231 | if commits is not None \ 232 | and len(commits) > settings.LAST_COMMITS_SIZE: 233 | commits = commits[:settings.LAST_COMMITS_SIZE] 234 | 235 | build_hash = params_json['options'].get('hash') 236 | if build_hash is None and commits is not None: 237 | build_hash = commits[0] 238 | 239 | build = Build( 240 | launch=launch, 241 | version=params_json['options'].get('version'), 242 | branch=params_json['options'].get('branch'), 243 | hash=build_hash, 244 | commit_message=params_json['options'].get('commit_message'), 245 | commit_author=params_json['options'].get('commit_author')) 246 | build.set_last_commits(commits) 247 | build.save() 248 | 249 | if format == 'nunit': 250 | parser = NunitParser(launch.id) 251 | elif format == 'junit': 252 | parser = JunitParser(launch.id) 253 | elif format == 'qttestxunit': 254 | parser = QtTestXunitParser(launch.id) 255 | elif format == 'xcprettyjunit': 256 | parser = XCPrettyJunitParser(launch.id) 257 | 258 | parser.load_string(file_content) 259 | parser.update_duration(launch) 260 | -------------------------------------------------------------------------------- /comments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/comments/__init__.py -------------------------------------------------------------------------------- /comments/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib import admin 4 | from django.contrib.auth import get_user_model 5 | from django.utils.translation import ugettext_lazy as _, ungettext 6 | 7 | from comments.models import Comment 8 | 9 | 10 | class UsernameSearch(object): 11 | """The User object may not be auth.User, so we need to provide 12 | a mechanism for issuing the equivalent of a .filter(user__username=...) 13 | search in CommentAdmin. 14 | """ 15 | def __str__(self): 16 | return 'user__%s' % get_user_model().USERNAME_FIELD 17 | 18 | 19 | class CommentsAdmin(admin.ModelAdmin): 20 | fieldsets = ( 21 | (None, {'fields': ('content_type', 'object_pk')}), 22 | (_('Content'), {'fields': ('user', 'comment')}), 23 | (_('Metadata'), {'fields': ('submit_date',)}), 24 | ) 25 | 26 | list_display = ('content_type', 'object_pk') 27 | list_filter = ('submit_date',) 28 | date_hierarchy = 'submit_date' 29 | ordering = ('-submit_date',) 30 | raw_id_fields = ('user',) 31 | search_fields = ('comment', UsernameSearch()) 32 | 33 | def _bulk_flag(self, request, queryset, action, done_message): 34 | """ 35 | Flag, approve, or remove some comments from an admin action. Actually 36 | calls the `action` argument to perform the heavy lifting. 37 | """ 38 | n_comments = 0 39 | for comment in queryset: 40 | action(request, comment) 41 | n_comments += 1 42 | 43 | msg = ungettext('1 comment was successfully {action}.', 44 | '{count} comments were successfully {action}.', 45 | n_comments) 46 | self.message_user(request, msg.format({ 47 | 'count': n_comments, 'action': done_message(n_comments)})) 48 | 49 | 50 | admin.site.register(Comment, CommentsAdmin) 51 | -------------------------------------------------------------------------------- /comments/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('contenttypes', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Comment', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('object_pk', models.TextField(verbose_name='object ID')), 21 | ('comment', models.TextField(max_length=3000, verbose_name='comment')), 22 | ('submit_date', models.DateTimeField(default=None, verbose_name='date/time submitted')), 23 | ('content_type', models.ForeignKey(related_name='content_type_set_for_comment', verbose_name='content type', to='contenttypes.ContentType')), 24 | ('user', models.ForeignKey(related_name='comment_comments', verbose_name='user', blank=True, to=settings.AUTH_USER_MODEL, null=True)), 25 | ], 26 | options={ 27 | 'ordering': ('submit_date',), 28 | 'verbose_name': 'comment', 29 | 'verbose_name_plural': 'comments', 30 | 'permissions': [('can_moderate', 'Can moderate comments')], 31 | }, 32 | bases=(models.Model,), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /comments/migrations/0002_auto_20141217_2314.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('comments', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='comment', 16 | name='submit_date', 17 | field=models.DateTimeField(default=None, verbose_name='date/time submitted', blank=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /comments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/comments/migrations/__init__.py -------------------------------------------------------------------------------- /comments/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.contenttypes import generic 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.db import models 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.utils import timezone 7 | from django.utils.encoding import python_2_unicode_compatible, force_text 8 | 9 | COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) 10 | 11 | 12 | class CommentManager(models.Manager): 13 | 14 | def for_model(self, model): 15 | """ 16 | QuerySet for all comments for a particular model (either an instance or 17 | a class). 18 | """ 19 | ct = ContentType.objects.get_for_model(model) 20 | qs = self.get_query_set().filter(content_type=ct) 21 | if isinstance(model, models.Model): 22 | qs = qs.filter(object_pk=force_text(model._get_pk_val())) 23 | return qs 24 | 25 | 26 | @python_2_unicode_compatible 27 | class Comment(models.Model): 28 | """ 29 | A user comment about some object. 30 | """ 31 | content_type = models.ForeignKey( 32 | ContentType, 33 | verbose_name=_('content type'), 34 | related_name="content_type_set_for_%(class)s") 35 | object_pk = models.TextField(_('object ID')) 36 | 37 | content_object = generic.GenericForeignKey(ct_field="content_type", 38 | fk_field="object_pk") 39 | # Who posted this comment? If ``user`` is set then it was an authenticated 40 | # user; otherwise at least user_name should have been set and the comment 41 | # was posted by a non-authenticated user. 42 | user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), 43 | blank=True, null=True, 44 | related_name="%(class)s_comments") 45 | comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) 46 | 47 | # Metadata about the comment 48 | submit_date = models.DateTimeField(_('date/time submitted'), default=None, 49 | blank=True) 50 | 51 | # Manager 52 | objects = CommentManager() 53 | 54 | class Meta: 55 | ordering = ('submit_date',) 56 | permissions = [("can_moderate", "Can moderate comments")] 57 | verbose_name = _('comment') 58 | verbose_name_plural = _('comments') 59 | 60 | def __str__(self): 61 | return "{0}: {1}...".format(self.user, self.comment[:50]) 62 | 63 | def save(self, *args, **kwargs): 64 | if self.submit_date is None: 65 | self.submit_date = timezone.now() 66 | super(Comment, self).save(*args, **kwargs) 67 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/common/__init__.py -------------------------------------------------------------------------------- /common/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/common/admin.py -------------------------------------------------------------------------------- /common/context_processors.py: -------------------------------------------------------------------------------- 1 | from common.models import Project 2 | 3 | 4 | def projects(request): 5 | return {'projects': Project.objects.all()} 6 | -------------------------------------------------------------------------------- /common/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Project', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('name', models.CharField(unique=True, max_length=128, verbose_name='Name')), 18 | ], 19 | options={ 20 | }, 21 | bases=(models.Model,), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /common/migrations/0002_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('common', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Settings', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('key', models.CharField(max_length=255)), 19 | ('value', models.TextField()), 20 | ('project', models.ForeignKey(related_name='settings', to='common.Project')), 21 | ], 22 | options={ 23 | }, 24 | bases=(models.Model,), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /common/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/common/migrations/__init__.py -------------------------------------------------------------------------------- /common/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext as _ 3 | 4 | 5 | class Project(models.Model): 6 | name = models.CharField(_('Name'), max_length=128, unique=True) 7 | 8 | def __str__(self): 9 | return 'Project: {0}'.format(self.name) 10 | 11 | 12 | class Settings(models.Model): 13 | project = models.ForeignKey(Project, related_name='settings') 14 | key = models.CharField(max_length=255) 15 | value = models.TextField() 16 | 17 | def __str__(self): 18 | return "{}={}".format(self.key, self.value) 19 | -------------------------------------------------------------------------------- /common/storage.py: -------------------------------------------------------------------------------- 1 | import boto 2 | import boto.s3.connection 3 | 4 | from django.conf import settings 5 | 6 | import logging 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | def get_s3_connection(): 11 | 12 | if settings.S3_ACCESS_KEY and settings.S3_SECRET_KEY and settings.S3_HOST: 13 | log.debug('Connecting to {}, with secure connection is {}'. 14 | format(settings.S3_HOST, settings.S3_SECURE_CONNECTION)) 15 | return boto.connect_s3( 16 | aws_access_key_id=settings.S3_ACCESS_KEY, 17 | aws_secret_access_key=settings.S3_SECRET_KEY, 18 | host=settings.S3_HOST, 19 | is_secure=settings.S3_SECURE_CONNECTION, 20 | calling_format=boto.s3.connection.OrdinaryCallingFormat()) 21 | return None 22 | 23 | 24 | def get_or_create_bucket(s3_connection): 25 | bucket = s3_connection.get_bucket(settings.S3_BUCKET_NAME) 26 | if bucket is None: 27 | bucket = s3_connection.create_bucket(settings.S3_BUCKET_NAME) 28 | return bucket 29 | -------------------------------------------------------------------------------- /common/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from celery.exceptions import SoftTimeLimitExceeded 3 | from celery.exceptions import Ignore 4 | from celery import states 5 | 6 | from testreport.models import INIT_SCRIPT 7 | from testreport.tasks import finalize_launch 8 | 9 | import celery 10 | import subprocess 11 | import logging 12 | import datetime 13 | import signal 14 | import psutil 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | def kill_proc_tree(pid, including_parent=True): 21 | parent = psutil.Process(pid) 22 | children = parent.children(recursive=True) 23 | for child in children: 24 | child.kill() 25 | psutil.wait_procs(children, timeout=5) 26 | if including_parent: 27 | parent.kill() 28 | parent.wait(5) 29 | 30 | 31 | @celery.task(time_limit=43200, bind=True) 32 | def launch_process(self, cmd, task_type=None, env={}): 33 | pid = None 34 | 35 | def sigterm_handler(signum, frame): 36 | log.debug('Get "{}", starting handler'.format(signum)) 37 | if pid is None: 38 | log.warn("Pid is None, nothing to kill...") 39 | return 40 | kill_proc_tree(pid) 41 | 42 | signal.signal(signal.SIGTERM, sigterm_handler) 43 | 44 | start = datetime.datetime.now() 45 | cmd = cmd.replace('\n', ';').replace('\r', '') 46 | result = { 47 | 'cmd': cmd, 48 | 'env': env, 49 | 'stdout': None, 50 | 'stderr': None, 51 | 'return_code': 0, 52 | } 53 | cwd = '/tmp/' 54 | if 'WORKSPACE' in env: 55 | cwd = env['WORKSPACE'] 56 | try: 57 | cmd = subprocess.Popen(['bash', '-c', cmd], stdout=subprocess.PIPE, 58 | stderr=subprocess.PIPE, env=env, cwd=cwd, 59 | universal_newlines=False) 60 | pid = cmd.pid 61 | result['stdout'], result['stderr'] = cmd.communicate() 62 | result['return_code'] = cmd.returncode 63 | # If INIT_SCRIPT task returns non-zero code we finalize launch 64 | # and raise Ignore exception to force the worker to ignore 65 | # current task and all tasks in its callback 66 | # http://docs.celeryproject.org/en/3.1/userguide/tasks.html#ignore 67 | if result['return_code'] != 0 and task_type == INIT_SCRIPT: 68 | self.update_state(state=states.FAILURE, meta=result) 69 | finalize_launch(launch_id=env['LAUNCH_ID']) 70 | raise Ignore() 71 | except subprocess.CalledProcessError as e: 72 | result['stdout'] = e.output 73 | result['return_code'] = e.returncode 74 | except OSError as e: 75 | result['stderr'] = e.strerror 76 | result['return_code'] = 127 77 | except SoftTimeLimitExceeded as e: 78 | result['stderr'] = 'Soft timeout limit exceeded. {}'.format(e) 79 | result['return_code'] = 1 80 | end = datetime.datetime.now() 81 | result['start'] = start.isoformat() 82 | result['end'] = end.isoformat() 83 | result['delta'] = (end - start).total_seconds() 84 | 85 | return result 86 | -------------------------------------------------------------------------------- /common/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block container %} 3 |
4 | {% if form.errors %} 5 | 6 | {% endif %} 7 |
8 | {% csrf_token %} 9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /common/templates/logout.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block container %} 3 |
4 | 5 |
6 | {% endblock %} 7 | 8 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | errorlog = '-' 2 | accesslog = '-' 3 | access_log_format = '{"remote_ip": "%(h)s", "uri": "%(r)s", ' \ 4 | '"response_code": %(s)s, "request_time": %(L)s}' 5 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | honcho==0.6.6 2 | ipython 3 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pycd.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /metrics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/metrics/__init__.py -------------------------------------------------------------------------------- /metrics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from metrics.models import Metric, MetricValue 3 | 4 | 5 | class MetricAdmin(admin.ModelAdmin): 6 | class Meta: 7 | model = Metric 8 | 9 | 10 | class MetricValueAdmin(admin.ModelAdmin): 11 | class Meta: 12 | model = MetricValue 13 | 14 | 15 | admin.site.register(Metric, MetricAdmin) 16 | admin.site.register(MetricValue, MetricValueAdmin) 17 | -------------------------------------------------------------------------------- /metrics/handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | log = logging.getLogger(__name__) 3 | 4 | 5 | HANDLER_CHOICES = ( 6 | ('count', 'Calculate count value'), 7 | ('cycletime', 'Calculate cycle time'), 8 | ('leadtime', 'Calculate lead time') 9 | ) 10 | 11 | 12 | def count(data): 13 | return len(data) 14 | 15 | 16 | def cycletime(data): 17 | if len(data) == 0: 18 | return 0 19 | 20 | avg_cycle_time = 0 21 | for issue in data: 22 | avg_cycle_time += issue.get_cycle_time().total_seconds() 23 | return int(avg_cycle_time / len(data)) 24 | 25 | 26 | def leadtime(data): 27 | if len(data) == 0: 28 | return 0 29 | 30 | avg_lead_time = 0 31 | for issue in data: 32 | avg_lead_time += issue.get_lead_time().total_seconds() 33 | 34 | return int(avg_lead_time / len(data)) 35 | -------------------------------------------------------------------------------- /metrics/jira.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import logging 4 | 5 | from django.conf import settings 6 | 7 | from rest_framework.exceptions import APIException 8 | 9 | from datetime import datetime, timedelta 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def request_jira_api(query, fields=None, 15 | max_results=settings.TRACKING_SYSTEM_MAX_RESULTS, 16 | expand=None): 17 | url = 'https://{}{}'.format( 18 | settings.BUG_TRACKING_SYSTEM_HOST, 19 | settings.TRACKING_SYSTEM_SEARCH_PATH) 20 | data = json.dumps({'jql': query, 21 | 'maxResults': max_results, 22 | 'fields': fields, 23 | 'expand': expand}) 24 | auth = (settings.BUG_TRACKING_SYSTEM_LOGIN, 25 | settings.BUG_TRACKING_SYSTEM_PASSWORD) 26 | headers = {'Content-Type': 'application/json'} 27 | 28 | response = None 29 | 30 | try: 31 | log.info('url="{}", data="{}", headers="{}"'. 32 | format(url, data, headers)) 33 | response = requests.post(url=url, data=data, 34 | auth=auth, headers=headers) 35 | result = response.json() 36 | except Exception as e: 37 | log.error('Some problems appeared during connection to "{}", ' 38 | 'auth="{}", data="{}", headers="{}", response="{}"'. 39 | format(url, auth, data, headers, response)) 40 | raise e 41 | 42 | log.debug(result) 43 | errors = [] 44 | if 'errors' in result: 45 | errors += result['errors'] 46 | if 'errorMessages' in result: 47 | errors += result['errorMessages'] 48 | if len(errors) != 0: 49 | log.debug(errors) 50 | raise APIException( 51 | "Tracking system: '{}'".format('\n'.join(errors))) 52 | return build_objects(result) 53 | 54 | 55 | def build_objects(issues_dict): 56 | if 'issues' in issues_dict: 57 | issues_dict = issues_dict['issues'] 58 | output = [] 59 | for issue in issues_dict: 60 | output.append(JiraIssue(issue)) 61 | return output 62 | 63 | 64 | class JiraIssue(object): 65 | source = None 66 | 67 | def __init__(self, issue): 68 | self.source = issue 69 | 70 | def _string_to_date(self, string_date): 71 | return datetime.strptime(string_date[:19], '%Y-%m-%dT%H:%M:%S') 72 | 73 | def _get_timedelta(self, start_date, end_date, exclude_weekend=False): 74 | if exclude_weekend: 75 | weekends = 0 76 | current_date = start_date 77 | while current_date <= end_date: 78 | if current_date.weekday() in [5, 6]: 79 | weekends += 1 80 | current_date += timedelta(days=1) 81 | return end_date - start_date - timedelta(days=weekends) 82 | 83 | return end_date - start_date 84 | 85 | def get_datetime(self, field): 86 | if field == 'finish_date': 87 | return self._get_finish_date() 88 | elif field == 'created': 89 | return self._get_create_date() 90 | elif field == 'resolutiondate': 91 | return self._get_resolution_date() 92 | else: 93 | raise RuntimeError( 94 | 'Unable to get date field with name {}'.format(field)) 95 | 96 | def _get_create_date(self): 97 | return datetime.strptime( 98 | self.source['fields']['created'][:10], '%Y-%m-%d') 99 | 100 | def _get_resolution_date(self): 101 | return datetime.strptime( 102 | self.source['fields']['resolutiondate'][:10], '%Y-%m-%d') 103 | 104 | def _get_finish_date(self): 105 | return self.handle_changelog('finish_date') 106 | 107 | def get_cycle_time(self): 108 | return self._get_timedelta( 109 | self.handle_changelog('cycletime_start_date'), 110 | self._get_finish_date(), 111 | exclude_weekend=True) 112 | 113 | def get_lead_time(self): 114 | return self._get_timedelta( 115 | self._string_to_date(self.source['fields']['created']), 116 | self._get_finish_date(), 117 | exclude_weekend=True) 118 | 119 | def handle_changelog(self, type): 120 | for history in self.source['changelog']['histories']: 121 | for item in history['items']: 122 | if item['field'] == 'status': 123 | finish_date = self._string_to_date(history['created']) 124 | if item['fromString'] == 'Взят в бэклог': 125 | cycletime_start_date = \ 126 | self._string_to_date(history['created']) 127 | if item['toString'] == 'Open': 128 | cycletime_start_date = \ 129 | self._string_to_date(history['created']) 130 | 131 | if type == 'finish_date': 132 | return finish_date 133 | if type == 'cycletime_start_date': 134 | return cycletime_start_date 135 | -------------------------------------------------------------------------------- /metrics/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.core.validators 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('common', '0002_settings'), 12 | ('djcelery', '__first__'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Metric', 18 | fields=[ 19 | ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), 20 | ('name', models.CharField(max_length=128)), 21 | ('select', models.TextField(blank=True, default='')), 22 | ('handler', models.CharField(choices=[('count', 'Calculate count value'), ('average', 'Calculate average value')], max_length=128)), 23 | ('project', models.ForeignKey(to='common.Project')), 24 | ('schedule', models.ForeignKey(to='djcelery.PeriodicTask')), 25 | ], 26 | options={ 27 | }, 28 | bases=(models.Model,), 29 | ), 30 | migrations.CreateModel( 31 | name='MetricValue', 32 | fields=[ 33 | ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), 34 | ('value', models.FloatField(validators=[django.core.validators.MinValueValidator(0.0)], default=None)), 35 | ('created', models.DateTimeField(auto_now_add=True)), 36 | ('settings', models.ForeignKey(to='metrics.Metric')), 37 | ], 38 | options={ 39 | }, 40 | bases=(models.Model,), 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /metrics/migrations/0002_auto_20150703_0937.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('metrics', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='metric', 16 | old_name='select', 17 | new_name='query', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /metrics/migrations/0003_auto_20150707_1622.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('metrics', '0002_auto_20150703_0937'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='metricvalue', 16 | old_name='settings', 17 | new_name='metric', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /metrics/migrations/0004_metric_error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('metrics', '0003_auto_20150707_1622'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='metric', 16 | name='error', 17 | field=models.TextField(blank=True, default=''), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /metrics/migrations/0005_metric_weight.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('metrics', '0004_metric_error'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='metric', 16 | name='weight', 17 | field=models.IntegerField(default=1), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /metrics/migrations/0006_auto_20150727_1145.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('metrics', '0005_metric_weight'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='metricvalue', 16 | name='created', 17 | field=models.DateTimeField(), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /metrics/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/metrics/migrations/__init__.py -------------------------------------------------------------------------------- /metrics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MinValueValidator 3 | 4 | from djcelery.models import PeriodicTask 5 | 6 | from common.models import Project 7 | from metrics.handlers import HANDLER_CHOICES 8 | 9 | from django.utils import timezone 10 | 11 | 12 | class Metric(models.Model): 13 | project = models.ForeignKey(Project) 14 | name = models.CharField(max_length=128) 15 | query = models.TextField(default='', blank=True) 16 | schedule = models.ForeignKey(PeriodicTask) 17 | handler = models.CharField(max_length=128, choices=HANDLER_CHOICES) 18 | error = models.TextField(default='', blank=True) 19 | weight = models.IntegerField(default=1) 20 | 21 | def __str__(self): 22 | return '{} -> Metric: {}'.format(self.project, self.name) 23 | 24 | def get_schedule_as_cron(self): 25 | cron = self.schedule.crontab 26 | return '{} {} {} {} {}'.format(cron.minute, cron.hour, 27 | cron.day_of_month, cron.month_of_year, 28 | cron.day_of_week) 29 | 30 | 31 | class MetricValue(models.Model): 32 | metric = models.ForeignKey(Metric) 33 | value = models.FloatField(default=None, 34 | validators=[MinValueValidator(0.0)]) 35 | created = models.DateTimeField() 36 | 37 | def __str__(self): 38 | return self.value 39 | 40 | def save(self, force_insert=False, force_update=False, using=None, 41 | update_fields=None): 42 | if self.created is None: 43 | self.created = timezone.now() 44 | super().save(force_insert, force_update, using, update_fields) 45 | -------------------------------------------------------------------------------- /metrics/tasks.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | 3 | from metrics.models import Metric, MetricValue 4 | from metrics import handlers 5 | 6 | from metrics.jira import request_jira_api 7 | 8 | import celery 9 | from datetime import timedelta 10 | 11 | from django.conf import settings 12 | 13 | import logging 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @celery.task() 18 | def run_metric_calculation(metric_id): 19 | log.info('Getting metric {} from DB'.format(metric_id)) 20 | try: 21 | metric = Metric.objects.get(pk=metric_id) 22 | except ObjectDoesNotExist: 23 | log.error('Metric with id {} was not found in DB'.format(metric_id)) 24 | raise ObjectDoesNotExist 25 | 26 | expand = None 27 | fields = ['created', 'resolutiondate'] 28 | if metric.handler in ['cycletime']: 29 | expand = ['changelog'] 30 | 31 | method = getattr(handlers, metric.handler) 32 | if settings.JIRA_INTEGRATION: 33 | try: 34 | data = request_jira_api(metric.query, fields=fields, expand=expand) 35 | except Exception as e: 36 | metric.error = e 37 | metric.save() 38 | raise e 39 | 40 | log.info('Handling selected data started') 41 | try: 42 | value = method(data) # handler value 43 | log.info('Handling selected data finished') 44 | except Exception as e: 45 | log.error('Selected data "{}" can not be handle by {}-handler: {}'. 46 | format(data, metric.handler, e)) 47 | metric.error = e 48 | metric.save() 49 | raise e 50 | 51 | metric.error = '' 52 | metric.save() 53 | log.info('Creating metric value for metric {}'.format(metric.name)) 54 | MetricValue.objects.create(metric=metric, value=value) 55 | log.info('Metric created') 56 | else: 57 | log.info('Jira integration is off. ' 58 | 'If you want to use this feature, turn it on.') 59 | 60 | 61 | @celery.task() 62 | def restore_metric_values(metric_id, query, step, handler, handler_field): 63 | expand = None 64 | if handler in ['cycletime', 'leadtime']: 65 | expand = ['changelog'] 66 | 67 | if settings.JIRA_INTEGRATION: 68 | try: 69 | method = getattr(handlers, handler) 70 | data = request_jira_api( 71 | query=query, 72 | fields=['created', 'resolutiondate'], 73 | expand=expand) 74 | 75 | results = group_issues_by_step(data, int(step), handler_field) 76 | 77 | except Exception as e: 78 | raise e 79 | 80 | for date, group in results.items(): 81 | log.info('Handling selected data started') 82 | try: 83 | value = method(group) # handler value 84 | log.info('Handling selected data finished') 85 | except Exception as e: 86 | log.error( 87 | 'Selected data "{}" can not be handle by {}-handler: {}'. 88 | format(data, handler, e)) 89 | raise e 90 | MetricValue.objects.create(metric_id=metric_id, value=value, 91 | created=date) 92 | else: 93 | log.info('Jira integration is off. ' 94 | 'If you want to use this feature, turn it on.') 95 | 96 | 97 | def group_issues_by_step(issues, step_in_days, field): 98 | sorted_issues = sorted( 99 | issues, key=lambda item: item.get_datetime(field)) 100 | 101 | start_date = sorted_issues[-1].get_datetime(field) 102 | current_interval = start_date - timedelta(days=step_in_days) 103 | 104 | group_issues = {} 105 | tmp = [] 106 | while len(sorted_issues) != 0: 107 | issue = sorted_issues.pop() 108 | if issue.get_datetime(field) >= current_interval: 109 | tmp.append(issue) 110 | else: 111 | group_issues[(current_interval + timedelta(days=step_in_days)) 112 | .strftime('%Y-%m-%d')] = tmp 113 | current_interval = current_interval - timedelta(days=step_in_days) 114 | sorted_issues.append(issue) 115 | tmp = [] 116 | 117 | if len(tmp) != 0: 118 | group_issues[(current_interval + timedelta(days=step_in_days)) 119 | .strftime('%Y-%m-%d')] = tmp 120 | 121 | return group_issues 122 | -------------------------------------------------------------------------------- /pycd/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import # NOQA 2 | from .celery import app as celery_app # NOQA 3 | -------------------------------------------------------------------------------- /pycd/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from celery import Celery 4 | from django.conf import settings 5 | 6 | import os 7 | 8 | os.environ.setdefault('DJANGO_SETTING_MODULE', 'pycd.settings') 9 | app = Celery('pycd') 10 | app.config_from_object('django.conf:settings') 11 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 12 | 13 | 14 | @app.task(bind=True) 15 | def debug_task(self): 16 | print('Request: {0!r}'.format(self.request)) 17 | -------------------------------------------------------------------------------- /pycd/disablecsrf.py: -------------------------------------------------------------------------------- 1 | class DisableCSRF(object): 2 | def process_request(self, request): 3 | setattr(request, '_dont_enforce_csrf_checks', True) 4 | -------------------------------------------------------------------------------- /pycd/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import dj_database_url 4 | 5 | from kombu import Exchange, Queue 6 | 7 | # SECURITY WARNING: keep the secret key used in production secret! 8 | SECRET_KEY = os.environ.get( 9 | 'SECRET_KEY', 10 | 'SM@g_V0u%eN$/jJb_lRZUOYnpo=P*{aS9nUPj~kWD2iMB%vkDy') 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | env_debug = os.environ.get('DEBUG', 'False') 14 | DEBUG = True if env_debug == 'True' else False 15 | TEMPLATE_DEBUG = DEBUG 16 | ALLOWED_HOSTS = ['*'] 17 | 18 | # Application definition 19 | INSTALLED_APPS = ( 20 | 'django.contrib.staticfiles', 21 | 'django.contrib.auth', 22 | 'django.contrib.contenttypes', 23 | 'django.contrib.sessions', 24 | 'django.contrib.messages', 25 | 'django.contrib.admin', 26 | 'corsheaders', 27 | 'comments', 28 | 'rest_framework', 29 | 'djcelery', 30 | 'common', 31 | 'testreport', 32 | 'cdws_api', 33 | 'stages', 34 | 'metrics', 35 | ) 36 | 37 | MIDDLEWARE_CLASSES = ( 38 | 'django.contrib.sessions.middleware.SessionMiddleware', 39 | 'corsheaders.middleware.CorsMiddleware', 40 | 'django.middleware.common.CommonMiddleware', 41 | 'pycd.disablecsrf.DisableCSRF', 42 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 43 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware', 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 46 | ) 47 | 48 | AUTH_LDAP3_SERVER_URI = os.environ.get('AUTH_LDAP3_SERVER_URI', '') 49 | 50 | AUTH_LDAP3_SEARCH_USER_DN = os.environ.get('AUTH_LDAP3_SEARCH_USER_DN', '') 51 | 52 | AUTH_LDAP3_SEARCH_USER_PASSWORD = os.environ.get( 53 | 'AUTH_LDAP3_SEARCH_USER_PASSWORD', None) 54 | 55 | AUTH_LDAP3_SEARCH_BASE = os.environ.get('AUTH_LDAP3_SEARCH_BASE', '') 56 | 57 | AUTH_LDAP3_SEARCH_FILTER = os.environ.get('AUTH_LDAP3_SEARCH_FILTER', '') 58 | 59 | AUTH_LDAP3_ATTRIBUTES_MAPPING = { 60 | 'username': 'sAMAccountName', 61 | 'first_name': None, 62 | 'last_name': None, 63 | 'email': 'mail', 64 | 'is_active': None, 65 | 'is_superuser': None, 66 | 'is_staff': None 67 | } 68 | 69 | if AUTH_LDAP3_SEARCH_USER_PASSWORD is not None: 70 | AUTHENTICATION_BACKENDS = ( 71 | 'authentication.backend.ADBackend', 72 | 'django.contrib.auth.backends.ModelBackend', 73 | ) 74 | else: 75 | AUTHENTICATION_BACKENDS = ( 76 | 'django.contrib.auth.backends.ModelBackend', 77 | ) 78 | 79 | ROOT_URLCONF = 'pycd.urls' 80 | WSGI_APPLICATION = 'pycd.wsgi.application' 81 | 82 | if 'test' in sys.argv: 83 | TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' 84 | TEST_OUTPUT_VERBOSE = True 85 | TEST_OUTPUT_DESCRIPTIONS = True 86 | TEST_OUTPUT_DIR = 'reports' 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.sqlite3', 90 | 'NAME': 'db.sqlite3' 91 | } 92 | } 93 | else: 94 | DATABASES = { 95 | 'default': dj_database_url.config() 96 | } 97 | 98 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 102 | 103 | LANGUAGE_CODE = os.environ.get('LANGUAGE_CODE', 'en-us') 104 | TIME_ZONE = os.environ.get('TIME_ZONE', 'Asia/Novosibirsk') 105 | USE_I18N = True 106 | USE_L10N = True 107 | USE_TZ = True 108 | 109 | CDWS_API_HOSTNAME = os.environ.get('CDWS_API_HOSTNAME') 110 | 111 | if not CDWS_API_HOSTNAME: 112 | raise RuntimeError('Please set the environment variable CDWS_API_HOSTNAME') 113 | 114 | CDWS_API_PATH = 'api' 115 | CDWS_DEPLOY_DIR = os.environ.get('CDWS_DEPLOY_DIR', '/opt') 116 | CDWS_WORKING_DIR = os.environ.get('CDWS_WORKING_DIR', '/tmp/') 117 | 118 | 119 | TEMPLATE_CONTEXT_PROCESSORS = ( 120 | 'django.contrib.auth.context_processors.auth', 121 | 'django.core.context_processors.i18n', 122 | 'django.core.context_processors.tz', 123 | 'django.contrib.messages.context_processors.messages', 124 | 'common.context_processors.projects' 125 | ) 126 | 127 | REST_FRAMEWORK = { 128 | 'PAGINATE_BY': 100, 129 | 'PAGINATE_BY_PARAM': 'page_size', 130 | 'DEFAULT_RENDERER_CLASSES': ( 131 | 'rest_framework.renderers.JSONRenderer', 132 | 'rest_framework.renderers.BrowsableAPIRenderer' 133 | ), 134 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 135 | 'rest_framework.authentication.BasicAuthentication', 136 | 'rest_framework.authentication.SessionAuthentication', 137 | ), 138 | } 139 | 140 | LOG_LEVEL = 'DEBUG' if DEBUG else 'INFO' 141 | 142 | LOGGING = { 143 | 'version': 1, 144 | 'disable_existing_loggers': False, 145 | 'root': { 146 | 'level': LOG_LEVEL, 147 | 'handlers': ['console'] 148 | }, 149 | 'formatters': { 150 | 'standard': { 151 | 'format': '{ "level": "%(levelname)s", ' 152 | '"message": "%(message)s", ' 153 | '"lineno": "%(lineno)s", ' 154 | '"pathname": "%(pathname)s" }', 155 | } 156 | }, 157 | 'handlers': { 158 | 'null': { 159 | 'level': 'DEBUG', 160 | 'class': 'django.utils.log.NullHandler', 161 | }, 162 | 'console': { 163 | 'level': LOG_LEVEL, 164 | 'class': 'logging.StreamHandler', 165 | 'formatter': 'standard' 166 | } 167 | }, 168 | 'loggers': { 169 | 'django.request': { 170 | 'handlers': ['console'], 171 | 'level': 'DEBUG', 172 | 'propagate': True 173 | }, 174 | 'django.db.backends': { 175 | 'handlers': ['null'], 176 | 'level': 'DEBUG', 177 | 'propagate': False, 178 | }, 179 | } 180 | } 181 | 182 | CELERY_HOST = os.environ.get('CELERY_HOST', '') 183 | CELERY_ACCEPT_CONTENT = ['json'] 184 | CELERY_TASK_SERIALIZER = 'json' 185 | CELERY_RESULT_SERIALIZER = 'json' 186 | BROKER_URL = os.environ.get('BROKER_URL') 187 | BROKER_CONNECTION_MAX_RETRIES = None 188 | BROKER_HEARTBEAT = os.environ.get('BROKER_HEARTBEAT', 60) 189 | CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend' 190 | CELERY_USER = 'ubuntu' 191 | CELERY_TRACK_STARTED = True 192 | CELERY_CHORD_PROPAGATES = True 193 | CELERY_RESULT_ENGINE_OPTIONS = {'pool_recycle': 3600} 194 | 195 | # celery queues setup 196 | CELERY_DEFAULT_QUEUE = 'default' 197 | CELERY_DEFAULT_EXCHANGE_TYPE = 'direct' 198 | CELERY_DEFAULT_ROUTING_KEY = 'default' 199 | CELERY_QUEUES = ( 200 | Queue('default', Exchange('default'), routing_key='default'), 201 | Queue('launcher', Exchange('launcher'), routing_key='launcher') 202 | ) 203 | 204 | CELERY_ROUTES = { 205 | 'testreport.tasks.finalize_launch': { 206 | 'queue': 'launcher', 207 | 'routing_key': 'launcher', 208 | }, 209 | 'testreport.tasks.create_environment': { 210 | 'queue': 'launcher', 211 | 'routing_key': 'launcher', 212 | }, 213 | 'testreport.tasks.finalize_broken_launches': { 214 | 'queue': 'launcher', 215 | 'routing_key': 'launcher', 216 | }, 217 | 'common.tasks.launch_process': { 218 | 'queue': 'launcher', 219 | 'routing_key': 'launcher', 220 | }, 221 | } 222 | 223 | JIRA_INTEGRATION = os.environ.get('JIRA_INTEGRATION', False) 224 | # if JIRA_INTEGRATION = True, please fill constants below 225 | TIME_BEFORE_UPDATE_BUG_INFO = os.environ.get( 226 | 'TIME_BEFORE_UPDATE_BUG_INFO', 10800) 227 | BUG_TIME_EXPIRED = os.environ.get('BUG_TIME_EXPIRED', 1209600) 228 | BUG_STATE_EXPIRED = os.environ.get('BUG_STATE_EXPIRED') 229 | BUG_TRACKING_SYSTEM_HOST = os.environ.get('BUG_TRACKING_SYSTEM_HOST') 230 | BUG_TRACKING_SYSTEM_LOGIN = os.environ.get('BUG_TRACKING_SYSTEM_LOGIN') 231 | BUG_TRACKING_SYSTEM_PASSWORD = os.environ.get('BUG_TRACKING_SYSTEM_PASSWORD') 232 | BUG_TRACKING_SYSTEM_BUG_PATH = os.environ.get('BUG_TRACKING_SYSTEM_BUG_PATH') 233 | TRACKING_SYSTEM_SEARCH_PATH = os.environ.get('TRACKING_SYSTEM_SEARCH_PATH') 234 | TRACKING_SYSTEM_MAX_RESULTS = os.environ.get( 235 | 'TRACKING_SYSTEM_MAX_RESULTS', 1000) 236 | 237 | CORS_ORIGIN_ALLOW_ALL = True 238 | CORS_ALLOW_CREDENTIALS = True 239 | if not DEBUG: 240 | COOKIE_DOMAIN = os.environ.get('COOKIE_DOMAIN', '') 241 | SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN 242 | 243 | STORE_TESTRESULTS_IN_DAYS = os.environ.get('STORE_TESTRESULTS_IN_DAYS', 30) 244 | RUNDECK_URL = os.environ.get('RUNDECK_URL', '') 245 | 246 | STATIC_URL = os.environ.get('STATIC_URL', '/static/') 247 | STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 248 | os.environ.get('STATIC_ROOT', 'static')) 249 | STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' 250 | 251 | S3_ACCESS_KEY = os.environ.get('S3_ACCESS_KEY') 252 | S3_SECRET_KEY = os.environ.get('S3_SECRET_KEY') 253 | S3_HOST = os.environ.get('S3_HOST') 254 | S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME', 'xml-reports') 255 | S3_SECURE_CONNECTION = os.environ.get('S3_SECURE_CONNECTION', False) 256 | S3_MAX_RETRIES = os.environ.get('S3_MAX_RETRIES', 2) 257 | S3_COUNTDOWN = os.environ.get('S3_COUNTDOWN', 900) # in seconds 258 | 259 | LAST_COMMITS_SIZE = os.environ.get('LAST_COMMITS_SIZE', 100) 260 | -------------------------------------------------------------------------------- /pycd/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | 6 | from rest_framework import routers 7 | 8 | from cdws_api.views import ProjectViewSet 9 | from cdws_api.views import TestPlanViewSet 10 | from cdws_api.views import LaunchViewSet 11 | from cdws_api.views import TestResultViewSet 12 | from cdws_api.views import TestResultNegativeViewSet 13 | from cdws_api.views import LaunchItemViewSet 14 | from cdws_api.views import TaskResultViewSet 15 | from cdws_api.views import CommentViewSet 16 | from cdws_api.views import BugViewSet 17 | from cdws_api.views import StageViewSet 18 | from cdws_api.views import JenkinsViewSet 19 | from cdws_api.views import RundeckViewSet 20 | from cdws_api.views import MetricViewSet, MetricValueViewSet 21 | from cdws_api.views import ReportFileViewSet 22 | 23 | from authentication.views import LoginView 24 | from authentication.views import LogoutView 25 | from authentication.views import IsAuthorizedView 26 | from authentication.views import UpdateSettingsView 27 | 28 | from testreport.views import Base 29 | 30 | router = routers.DefaultRouter() 31 | router.register(r'projects', ProjectViewSet) 32 | router.register(r'testplans', TestPlanViewSet) 33 | router.register(r'launches', LaunchViewSet) 34 | router.register(r'testresults', TestResultViewSet) 35 | router.register(r'testresults_negative', TestResultNegativeViewSet) 36 | router.register(r'launch-items', LaunchItemViewSet) 37 | router.register(r'tasks', TaskResultViewSet) 38 | router.register(r'comments', CommentViewSet) 39 | router.register(r'bugs', BugViewSet) 40 | router.register(r'stages', StageViewSet) 41 | router.register(r'metrics', MetricViewSet) 42 | router.register(r'metricvalues', MetricValueViewSet) 43 | 44 | urlpatterns = patterns( 45 | '', 46 | url(r'^$', Base.as_view(), name='dashboard'), 47 | url(r'^{0}/'.format(settings.CDWS_API_PATH), include(router.urls)), 48 | url(r'^{0}/auth/login'.format(settings.CDWS_API_PATH), LoginView.as_view(), 49 | name='api-auth-login'), 50 | url(r'^{0}/auth/logout'.format(settings.CDWS_API_PATH), 51 | LogoutView.as_view(), name='api-auth-logout'), 52 | url(r'^{0}/auth/get'.format(settings.CDWS_API_PATH), 53 | IsAuthorizedView.as_view(), name='api-auth-get'), 54 | url(r'^{0}/auth/update'.format(settings.CDWS_API_PATH), 55 | UpdateSettingsView.as_view(), name='api-auth-update'), 56 | url(r'^admin/', include(admin.site.urls)), 57 | url(r'^{0}/external/jenkins/(?P[^/.]+)/'. 58 | format(settings.CDWS_API_PATH), JenkinsViewSet.as_view()), 59 | url(r'^{0}/external/rundeck/(?P[^/.]+)/'. 60 | format(settings.CDWS_API_PATH), RundeckViewSet.as_view()), 61 | url(r'^{0}/external/report-xunit/(?P[^/.]+)/' 62 | r'(?P[^/.]+)/(?P[^/.]+)'. 63 | format(settings.CDWS_API_PATH), ReportFileViewSet.as_view()), 64 | ) 65 | 66 | urlpatterns += staticfiles_urlpatterns() 67 | -------------------------------------------------------------------------------- /pycd/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for pycd project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | import os 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pycd.settings") 11 | 12 | from django.core.wsgi import get_wsgi_application # noqa 13 | from whitenoise.django import DjangoWhiteNoise # noqa 14 | 15 | application = get_wsgi_application() 16 | application = DjangoWhiteNoise(application) 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redis==2.10.6 2 | celery[redis]==3.1.18 3 | simplejson==3.6.5 4 | ldap3==2.2.2 5 | django==1.7.8 # 1.8.2 6 | djangorestframework==3.2.3 7 | djangorestframework-bulk==0.2.1 8 | django-filter==0.10.0 9 | django-contrib-comments==1.6.1 10 | django-cors-headers==1.1.0 11 | django-celery==3.1.16 12 | dj-database-url==0.3.0 13 | gunicorn==19.3.0 14 | psycopg2==2.7.4 15 | requests==2.7.0 16 | pytz 17 | psutil==3.2.2 18 | defusedxml==0.4.1 19 | djangorestframework-xml==1.2.0 20 | whitenoise==2.0.6 21 | boto -------------------------------------------------------------------------------- /run_coveralls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from subprocess import call 4 | 5 | 6 | if __name__ == '__main__': 7 | if 'TRAVIS' in os.environ: 8 | rc = call('coveralls') 9 | raise SystemExit(rc) 10 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.4.3 2 | -------------------------------------------------------------------------------- /stages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/stages/__init__.py -------------------------------------------------------------------------------- /stages/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from stages.models import Stage 3 | 4 | 5 | class StageAdmin(admin.ModelAdmin): 6 | fields = ('name', 'project', 'text', 'link', 'state', 'updated', 'weight') 7 | 8 | class Meta: 9 | model = Stage 10 | 11 | 12 | admin.site.register(Stage, StageAdmin) 13 | -------------------------------------------------------------------------------- /stages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('common', '0002_settings'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Stage', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('name', models.CharField(max_length=128)), 20 | ('text', models.TextField(default=b'', blank=True)), 21 | ('link', models.URLField(default=None, null=True, blank=True)), 22 | ('state', models.CharField(default=b'info', max_length=24, blank=True)), 23 | ('weight', models.IntegerField(default=0)), 24 | ('updated', models.DateTimeField(default=datetime.datetime(2015, 6, 11, 16, 34, 32, 887390))), 25 | ('project', models.ForeignKey(to='common.Project')), 26 | ], 27 | options={ 28 | }, 29 | bases=(models.Model,), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /stages/migrations/0002_auto_20150727_1145.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('stages', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='stage', 17 | name='state', 18 | field=models.CharField(max_length=24, blank=True, default='info'), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='stage', 23 | name='text', 24 | field=models.TextField(blank=True, default=''), 25 | preserve_default=True, 26 | ), 27 | migrations.AlterField( 28 | model_name='stage', 29 | name='updated', 30 | field=models.DateTimeField(default=datetime.datetime(2015, 7, 27, 11, 45, 50, 210078)), 31 | preserve_default=True, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /stages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/stages/migrations/__init__.py -------------------------------------------------------------------------------- /stages/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import datetime 3 | 4 | from common.models import Project 5 | 6 | 7 | class Stage(models.Model): 8 | name = models.CharField(max_length=128) 9 | project = models.ForeignKey(Project) 10 | text = models.TextField(default='', blank=True) 11 | link = models.URLField(default=None, blank=True, null=True) 12 | state = models.CharField(max_length=24, default='info', blank=True) 13 | weight = models.IntegerField(default=0) 14 | updated = models.DateTimeField(default=datetime.datetime.now()) 15 | 16 | def save(self, *args, **kwargs): 17 | self.updated = datetime.datetime.now() 18 | return super(Stage, self).save(*args, **kwargs) 19 | 20 | def __str__(self): 21 | return self.name 22 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | mock>=1.0 2 | coverage==3.7.1 3 | sqlalchemy 4 | unittest-xml-reporting 5 | requests-mock 6 | -------------------------------------------------------------------------------- /testreport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/testreport/__init__.py -------------------------------------------------------------------------------- /testreport/admin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib import admin 4 | 5 | from common.models import Project 6 | from common.models import Settings 7 | 8 | from testreport.models import Launch 9 | from testreport.models import TestPlan 10 | from testreport.models import TestResult 11 | from testreport.models import LaunchItem 12 | from testreport.models import Bug 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | admin.site.disable_action('delete_selected') 17 | 18 | 19 | class ProjectAdmin(admin.ModelAdmin): 20 | actions = ['delete_selected'] 21 | 22 | 23 | def force_delete(modeladmin, request, queryset): 24 | try: 25 | queryset.delete() 26 | except Exception as e: 27 | log.error(e) 28 | 29 | 30 | force_delete.short_description = 'Delete selected items w/o confirmation' 31 | 32 | 33 | class LaunchAdmin(admin.ModelAdmin): 34 | actions = [force_delete] 35 | 36 | 37 | class LaunchItemInline(admin.StackedInline): 38 | actions = ['delete_selected'] 39 | model = LaunchItem 40 | extra = 0 41 | 42 | 43 | class TestPlanAdmin(admin.ModelAdmin): 44 | inlines = [LaunchItemInline] 45 | actions = [force_delete] 46 | 47 | 48 | class TestResultAdmin(admin.ModelAdmin): 49 | actions = ['delete_selected'] 50 | 51 | 52 | class LaunchItemAdmin(admin.ModelAdmin): 53 | actions = ['delete_selected'] 54 | 55 | 56 | class BugAdmin(admin.ModelAdmin): 57 | actions = ['delete_selected'] 58 | 59 | 60 | admin.site.register(LaunchItem, LaunchItemAdmin) 61 | admin.site.register(Project, ProjectAdmin) 62 | admin.site.register(Settings, ProjectAdmin) 63 | admin.site.register(Launch, LaunchAdmin) 64 | admin.site.register(TestPlan, TestPlanAdmin) 65 | admin.site.register(TestResult, TestResultAdmin) 66 | admin.site.register(Bug, BugAdmin) 67 | -------------------------------------------------------------------------------- /testreport/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('common', '__first__'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Launch', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('counts_cache', models.TextField(blank=True)), 19 | ], 20 | options={ 21 | }, 22 | bases=(models.Model,), 23 | ), 24 | migrations.CreateModel( 25 | name='TestPlan', 26 | fields=[ 27 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 28 | ('name', models.CharField(max_length=256, verbose_name='Name')), 29 | ('project', models.ForeignKey(to='common.Project')), 30 | ], 31 | options={ 32 | }, 33 | bases=(models.Model,), 34 | ), 35 | migrations.CreateModel( 36 | name='TestResult', 37 | fields=[ 38 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 39 | ('name', models.CharField(max_length=128, verbose_name='Name')), 40 | ('suite', models.CharField(max_length=128, verbose_name='TestSuite')), 41 | ('state', models.IntegerField(default=3, verbose_name='State')), 42 | ('failure_reason', models.TextField(default=None, verbose_name='Failure Reason', blank=True)), 43 | ('duration', models.FloatField(default=0.0, verbose_name='Duration time')), 44 | ('launch', models.ForeignKey(to='testreport.Launch')), 45 | ], 46 | options={ 47 | }, 48 | bases=(models.Model,), 49 | ), 50 | migrations.AddField( 51 | model_name='launch', 52 | name='test_plan', 53 | field=models.ForeignKey(to='testreport.TestPlan'), 54 | preserve_default=True, 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /testreport/migrations/0002_auto_20141022_1358.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 10, 22, 13, 58, 55, 42534), auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='counts_cache', 24 | field=models.TextField(default=None, null=True, blank=True), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /testreport/migrations/0003_auto_20141023_1423.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0002_auto_20141022_1358'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 10, 23, 14, 23, 50, 549826), auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='testresult', 23 | name='suite', 24 | field=models.CharField(max_length=256, verbose_name='TestSuite'), 25 | preserve_default=True, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0004_auto_20141029_0900.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0003_auto_20141023_1423'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launch', 17 | name='started_by', 18 | field=models.URLField(default=None, null=True, blank=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='created', 24 | field=models.DateTimeField(default=datetime.datetime(2014, 10, 29, 9, 0, 21, 354221), auto_now_add=True), 25 | preserve_default=True, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0005_auto_20141117_0734.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0004_auto_20141029_0900'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 17, 7, 34, 6, 591200), auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0006_auto_20141117_0737.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0005_auto_20141117_0734'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 17, 7, 37, 7, 69970), auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0007_auto_20141117_1052.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0006_auto_20141117_0737'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launch', 17 | name='state', 18 | field=models.IntegerField(default=2, verbose_name='State'), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='created', 24 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 17, 10, 52, 1, 906983), verbose_name='Created', auto_now_add=True), 25 | preserve_default=True, 26 | ), 27 | migrations.AlterField( 28 | model_name='launch', 29 | name='started_by', 30 | field=models.URLField(default=None, null=True, verbose_name='Started by', blank=True), 31 | preserve_default=True, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /testreport/migrations/0008_auto_20141117_1348.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0007_auto_20141117_1052'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='LaunchItem', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('command', models.CharField(max_length=255)), 20 | ('test_plan', models.ForeignKey(to='testreport.TestPlan')), 21 | ], 22 | options={ 23 | }, 24 | bases=(models.Model,), 25 | ), 26 | migrations.AlterField( 27 | model_name='launch', 28 | name='created', 29 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 17, 13, 48, 34, 339949), verbose_name='Created', auto_now_add=True), 30 | preserve_default=True, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /testreport/migrations/0009_auto_20141117_1517.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0008_auto_20141117_1348'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launch', 17 | name='tasks', 18 | field=models.TextField(default=b'', verbose_name='Tasks'), 19 | preserve_default=True, 20 | ), 21 | migrations.AddField( 22 | model_name='launchitem', 23 | name='timeout', 24 | field=models.IntegerField(default=60), 25 | preserve_default=True, 26 | ), 27 | migrations.AlterField( 28 | model_name='launch', 29 | name='created', 30 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 17, 15, 17, 52, 556523), verbose_name='Created', auto_now_add=True), 31 | preserve_default=True, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /testreport/migrations/0010_auto_20141117_2134.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0009_auto_20141117_1517'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launchitem', 17 | name='type', 18 | field=models.IntegerField(default=0), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='created', 24 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 17, 21, 34, 27, 460244), verbose_name='Created', auto_now_add=True), 25 | preserve_default=True, 26 | ), 27 | migrations.AlterField( 28 | model_name='launchitem', 29 | name='command', 30 | field=models.TextField(), 31 | preserve_default=True, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /testreport/migrations/0011_auto_20141121_1306.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0010_auto_20141117_2134'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launchitem', 17 | name='name', 18 | field=models.CharField(default=None, max_length=128, null=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='created', 24 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 21, 13, 6, 45, 746581), verbose_name='Created', auto_now_add=True), 25 | preserve_default=True, 26 | ), 27 | migrations.AlterField( 28 | model_name='launchitem', 29 | name='timeout', 30 | field=models.IntegerField(default=300), 31 | preserve_default=True, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /testreport/migrations/0012_auto_20141121_1307.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0011_auto_20141121_1306'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 21, 13, 7, 44, 403138), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launchitem', 23 | name='name', 24 | field=models.CharField(default=None, max_length=128, null=True, blank=True), 25 | preserve_default=True, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0013_auto_20141126_1308.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0012_auto_20141121_1307'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launch', 17 | name='finished', 18 | field=models.DateTimeField(default=None, null=True, verbose_name='Finished', blank=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='created', 24 | field=models.DateTimeField(default=datetime.datetime(2014, 11, 26, 13, 8, 34, 43636), verbose_name='Created', auto_now_add=True), 25 | preserve_default=True, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0014_auto_20141208_0930.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0013_auto_20141126_1308'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 8, 9, 30, 12, 148944), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0015_auto_20141208_1330.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0014_auto_20141208_0930'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 8, 13, 30, 19, 585941), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0016_auto_20141209_0734.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0015_auto_20141208_1330'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 9, 7, 34, 3, 506661), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0017_auto_20141210_1646.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0016_auto_20141209_0734'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='launch', 17 | name='parameters', 18 | field=models.TextField(default=b'', verbose_name='Parameters'), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='created', 24 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 10, 16, 46, 59, 693179), verbose_name='Created', auto_now_add=True), 25 | preserve_default=True, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0018_auto_20141216_1834.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0017_auto_20141210_1646'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 16, 18, 34, 25, 110633), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='launch', 23 | name='parameters', 24 | field=models.TextField(default=b'{}', verbose_name='Parameters'), 25 | preserve_default=True, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0019_auto_20141217_2133.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0018_auto_20141216_1834'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 17, 21, 33, 28, 900950), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0020_auto_20141217_2134.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0019_auto_20141217_2133'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 17, 21, 34, 20, 705148), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0021_auto_20141217_2314.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0020_auto_20141217_2134'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 17, 23, 14, 17, 245838), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0022_auto_20141218_0830.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('testreport', '0021_auto_20141217_2314'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='launch', 17 | name='created', 18 | field=models.DateTimeField(default=datetime.datetime(2014, 12, 18, 8, 30, 24, 553426), verbose_name='Created', auto_now_add=True), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testreport/migrations/0023_auto_20141223_1300.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0022_auto_20141218_0830'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Bug', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('externalId', models.CharField(max_length=255)), 19 | ('name', models.CharField(default=b'', max_length=255, blank=True)), 20 | ('regexp', models.CharField(default=b'', max_length=255)), 21 | ('state', models.CharField(default=b'', max_length=16, blank=True)), 22 | ('updated', models.DateTimeField(auto_now=True)), 23 | ], 24 | options={ 25 | }, 26 | bases=(models.Model,), 27 | ), 28 | migrations.AlterField( 29 | model_name='launch', 30 | name='created', 31 | field=models.DateTimeField(auto_now_add=True, verbose_name='Created'), 32 | preserve_default=True, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /testreport/migrations/0024_auto_20150309_1013.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('testreport', '0023_auto_20141223_1300'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='launch', 18 | name='owner', 19 | field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), 20 | preserve_default=True, 21 | ), 22 | migrations.AddField( 23 | model_name='testplan', 24 | name='owner', 25 | field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), 26 | preserve_default=True, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /testreport/migrations/0025_auto_20150309_1149.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0024_auto_20150309_1013'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='launch', 16 | name='owner', 17 | ), 18 | migrations.AddField( 19 | model_name='testplan', 20 | name='hidden', 21 | field=models.BooleanField(default=True), 22 | preserve_default=True, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /testreport/migrations/0026_testresult_launch_item_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0025_auto_20150309_1149'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='testresult', 16 | name='launch_item_id', 17 | field=models.IntegerField(default=None, null=True, blank=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0027_auto_20150527_1204.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0026_testresult_launch_item_id'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='testplan', 16 | name='filter', 17 | field=models.TextField(default=b'', max_length=128, verbose_name='Started by filter', blank=True), 18 | preserve_default=True, 19 | ), 20 | migrations.AddField( 21 | model_name='testplan', 22 | name='main', 23 | field=models.BooleanField(default=False, verbose_name='Show in short statistic'), 24 | preserve_default=True, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /testreport/migrations/0028_auto_20150613_1727.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0027_auto_20150527_1204'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='bug', 16 | name='name', 17 | field=models.CharField(blank=True, max_length=255, default=''), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='bug', 22 | name='regexp', 23 | field=models.CharField(max_length=255, default=''), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='bug', 28 | name='state', 29 | field=models.CharField(blank=True, max_length=16, default=''), 30 | preserve_default=True, 31 | ), 32 | migrations.AlterField( 33 | model_name='launch', 34 | name='parameters', 35 | field=models.TextField(verbose_name='Parameters', default='{}'), 36 | preserve_default=True, 37 | ), 38 | migrations.AlterField( 39 | model_name='launch', 40 | name='tasks', 41 | field=models.TextField(verbose_name='Tasks', default=''), 42 | preserve_default=True, 43 | ), 44 | migrations.AlterField( 45 | model_name='testplan', 46 | name='filter', 47 | field=models.TextField(verbose_name='Started by filter', blank=True, max_length=128, default=''), 48 | preserve_default=True, 49 | ), 50 | migrations.AlterField( 51 | model_name='testresult', 52 | name='failure_reason', 53 | field=models.TextField(verbose_name='Failure Reason', blank=True, null=True, default=None), 54 | preserve_default=True, 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /testreport/migrations/0029_testplan_description.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0028_auto_20150613_1727'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='testplan', 16 | name='description', 17 | field=models.TextField(blank=True, default='', verbose_name='Description'), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0030_launch_duration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0029_testplan_description'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='launch', 16 | name='duration', 17 | field=models.FloatField(default=None, verbose_name='Duration time', null=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0031_extuser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('testreport', '0030_launch_duration'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ExtUser', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), 20 | ('default_project', models.IntegerField(default=None, null=True, blank=True)), 21 | ('launches_on_page', models.IntegerField(default=10)), 22 | ('testresults_on_page', models.IntegerField(default=25)), 23 | ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='settings')), 24 | ], 25 | options={ 26 | }, 27 | bases=(models.Model,), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /testreport/migrations/0032_auto_20151023_1055.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0031_extuser'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='extuser', 16 | name='default_project', 17 | field=models.IntegerField(verbose_name='User default project', blank=True, default=None, null=True), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='extuser', 22 | name='launches_on_page', 23 | field=models.IntegerField(verbose_name='Launches on page', default=10), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='extuser', 28 | name='testresults_on_page', 29 | field=models.IntegerField(verbose_name='Testresults on page', default=25), 30 | preserve_default=True, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /testreport/migrations/0033_auto_20151110_0925.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0032_auto_20151023_1055'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='testplan', 16 | name='variable_name', 17 | field=models.TextField(verbose_name='Environment variable name', blank=True, default='', max_length=128), 18 | preserve_default=True, 19 | ), 20 | migrations.AddField( 21 | model_name='testplan', 22 | name='variable_value_regexp', 23 | field=models.CharField(verbose_name='Regexp for variable value', blank=True, default='', max_length=255), 24 | preserve_default=True, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /testreport/migrations/0034_extuser_dashboards.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0033_auto_20151110_0925'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='extuser', 16 | name='dashboards', 17 | field=models.TextField(verbose_name='Dashboards', default='[]'), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0035_testplan_summary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0034_extuser_dashboards'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='testplan', 16 | name='summary', 17 | field=models.BooleanField(verbose_name='Use for total chart', default=False), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0036_auto_20151218_1144.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0035_testplan_summary'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='testplan', 16 | name='summary', 17 | ), 18 | migrations.AddField( 19 | model_name='testplan', 20 | name='show_in_summary', 21 | field=models.BooleanField(verbose_name='Consider in summary calculation', default=False), 22 | preserve_default=True, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /testreport/migrations/0037_auto_20151223_1643.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0036_auto_20151218_1144'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='bug', 16 | name='state', 17 | field=models.CharField(blank=True, max_length=32, default=''), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0038_testplan_show_in_twodays.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0037_auto_20151223_1643'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='testplan', 16 | name='show_in_twodays', 17 | field=models.BooleanField(verbose_name='Consider in statistic for last two days', default=False), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0039_auto_20160120_1606.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0038_testplan_show_in_twodays'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='testresult', 16 | name='name', 17 | field=models.CharField(db_index=True, max_length=128, verbose_name='Name'), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0040_extuser_result_preview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0039_auto_20160120_1606'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='extuser', 16 | name='result_preview', 17 | field=models.CharField(blank=True, default=None, max_length=128, verbose_name='Result preview', choices=[('head', 'Show test result head'), ('tail', 'Show test result tail')], null=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /testreport/migrations/0041_build.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0040_extuser_result_preview'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Build', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), 18 | ('version', models.CharField(null=True, blank=True, max_length=16, default=None)), 19 | ('hash', models.CharField(null=True, blank=True, max_length=64, default=None)), 20 | ('branch', models.CharField(null=True, blank=True, max_length=128, default=None)), 21 | ('launch', models.OneToOneField(related_name='build', to='testreport.Launch')), 22 | ], 23 | options={ 24 | }, 25 | bases=(models.Model,), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /testreport/migrations/0042_add_xml_parser_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | from django.core.exceptions import ObjectDoesNotExist 7 | 8 | 9 | def create_parser_user(apps, schema_editor): 10 | User = apps.get_model('auth', 'User') 11 | try: 12 | parser = User.objects.get(username='xml-parser') 13 | except ObjectDoesNotExist: 14 | parser = User( 15 | username='xml-parser', 16 | email='parser@xml.com', 17 | password='qweqwe', 18 | is_superuser=False, 19 | is_staff=True 20 | ) 21 | parser.save() 22 | 23 | 24 | class Migration(migrations.Migration): 25 | 26 | dependencies = [ 27 | ('testreport', '0041_build'), 28 | ] 29 | 30 | operations = [ 31 | migrations.RunPython(create_parser_user) 32 | ] 33 | -------------------------------------------------------------------------------- /testreport/migrations/0043_auto_20160413_1040.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('testreport', '0042_add_xml_parser_user'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='build', 16 | name='commit_author', 17 | field=models.CharField(null=True, default=None, blank=True, max_length=16), 18 | preserve_default=True, 19 | ), 20 | migrations.AddField( 21 | model_name='build', 22 | name='commit_message', 23 | field=models.CharField(null=True, default=None, blank=True, max_length=128), 24 | preserve_default=True, 25 | ), 26 | migrations.AddField( 27 | model_name='build', 28 | name='last_commits', 29 | field=models.TextField(null=True, default=None, blank=True), 30 | preserve_default=True, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /testreport/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2gis/badger-api/dfefb21846993cbab9cd6efbbcc9279fd977ab1f/testreport/migrations/__init__.py -------------------------------------------------------------------------------- /testreport/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext as _ 3 | from django.contrib.auth.models import User 4 | 5 | from common.models import Project 6 | 7 | from celery import states 8 | 9 | import logging 10 | import json 11 | 12 | from django.conf import settings 13 | import requests 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | TEST_STATES = (PASSED, FAILED, SKIPPED, BLOCKED) = (0, 1, 2, 3) 18 | LAUNCH_STATES = (INITIALIZED, IN_PROGRESS, FINISHED, STOPPED) = (0, 1, 2, 3) 19 | LAUNCH_TYPES = (ASYNC_CALL, INIT_SCRIPT, CONCLUSIVE) = (0, 1, 2) 20 | CELERY_FINISHED_STATES = (states.SUCCESS, states.FAILURE) 21 | 22 | RESULT_PREVIEW_CHOICES = ( 23 | ('head', 'Show test result head'), 24 | ('tail', 'Show test result tail') 25 | ) 26 | 27 | 28 | class ExtUser(models.Model): 29 | user = models.OneToOneField(User, related_name='settings') 30 | default_project = models.IntegerField(_('User default project'), 31 | blank=True, null=True, default=None) 32 | launches_on_page = models.IntegerField(_('Launches on page'), default=10) 33 | testresults_on_page = models.IntegerField( 34 | _('Testresults on page'), default=25) 35 | dashboards = models.TextField(_('Dashboards'), default='[]') 36 | result_preview = models.CharField(_('Result preview'), max_length=128, 37 | choices=RESULT_PREVIEW_CHOICES, 38 | blank=True, null=True, default=None) 39 | 40 | def get_dashboards(self): 41 | if self.dashboards == '""' or self.dashboards is None: 42 | self.dashboards = '[]' 43 | return json.loads(self.dashboards) 44 | 45 | def set_dashboards(self, dashboards): 46 | self.dashboards = json.dumps(dashboards) 47 | 48 | 49 | class TestPlan(models.Model): 50 | name = models.CharField(_('Name'), max_length=256) 51 | project = models.ForeignKey(Project) 52 | main = models.BooleanField(_('Show in short statistic'), 53 | blank=True, null=False, default=False) 54 | hidden = models.BooleanField(blank=False, null=False, default=True) 55 | owner = models.ForeignKey(User, default=1) 56 | filter = models.TextField(_('Started by filter'), default='', 57 | blank=True, null=False, max_length=128) 58 | description = models.TextField(_('Description'), default='', 59 | blank=True, null=False) 60 | variable_name = models.TextField(_('Environment variable name'), 61 | default='', blank=True, 62 | null=False, max_length=128) 63 | variable_value_regexp = models.CharField(_('Regexp for variable value'), 64 | max_length=255, default='', 65 | blank=True) 66 | show_in_summary = models.BooleanField( 67 | _('Consider in summary calculation'), 68 | blank=True, null=False, default=False) 69 | 70 | show_in_twodays = models.BooleanField( 71 | _('Consider in statistic for last two days'), 72 | blank=True, null=False, default=False) 73 | 74 | def __str__(self): 75 | return '{0} -> TestPlan: {1}'.format(self.project, self.name) 76 | 77 | 78 | class Launch(models.Model): 79 | test_plan = models.ForeignKey(TestPlan) 80 | counts_cache = models.TextField(blank=True, null=True, default=None) 81 | started_by = models.URLField(_('Started by'), blank=True, null=True, 82 | default=None) 83 | created = models.DateTimeField(_('Created'), auto_now_add=True) 84 | finished = models.DateTimeField(_('Finished'), default=None, blank=True, 85 | null=True) 86 | state = models.IntegerField(_('State'), default=FINISHED) 87 | tasks = models.TextField(_('Tasks'), default='') 88 | parameters = models.TextField(_('Parameters'), default='{}') 89 | duration = models.FloatField(_('Duration time'), null=True, default=None) 90 | 91 | def is_finished(self): 92 | return self.state == FINISHED 93 | 94 | @property 95 | def counts(self): 96 | if self.counts_cache is None or self.state == INITIALIZED: 97 | self.calculate_counts() 98 | return json.loads(self.counts_cache) 99 | 100 | def calculate_counts(self): 101 | data = { 102 | 'passed': len(self.passed), 103 | 'failed': len(self.failed), 104 | 'skipped': len(self.skipped), 105 | 'blocked': len(self.blocked), 106 | 'total': 0 107 | } 108 | for name, count in data.items(): 109 | if name != 'total': 110 | data['total'] += count 111 | self.counts_cache = json.dumps(data) 112 | self.save() 113 | 114 | @property 115 | def failed(self): 116 | return self.testresult_set.filter(state=FAILED) 117 | 118 | @property 119 | def skipped(self): 120 | return self.testresult_set.filter(state=SKIPPED) 121 | 122 | @property 123 | def passed(self): 124 | return self.testresult_set.filter(state=PASSED) 125 | 126 | @property 127 | def blocked(self): 128 | return self.testresult_set.filter(state=BLOCKED) 129 | 130 | def get_tasks(self): 131 | if self.tasks == '' or self.tasks is None: 132 | self.tasks = '{}' 133 | return json.loads(self.tasks) 134 | 135 | def set_tasks(self, tasks): 136 | self.tasks = json.dumps(tasks) 137 | 138 | def get_parameters(self): 139 | if self.parameters == '' or self.parameters is None: 140 | self.parameters = '{}' 141 | return json.loads(self.parameters) 142 | 143 | def set_parameters(self, parameters): 144 | self.parameters = json.dumps(parameters) 145 | 146 | def __str__(self): 147 | return '{0} -> Launch: {1}'.format(self.test_plan, self.pk) 148 | 149 | 150 | class Build(models.Model): 151 | launch = models.OneToOneField(Launch, related_name='build') 152 | version = models.CharField( 153 | max_length=16, default=None, null=True, blank=True) 154 | hash = models.CharField( 155 | max_length=64, default=None, null=True, blank=True) 156 | branch = models.CharField( 157 | max_length=128, default=None, null=True, blank=True) 158 | commit_author = models.CharField( 159 | max_length=16, default=None, null=True, blank=True) 160 | commit_message = models.CharField( 161 | max_length=128, default=None, null=True, blank=True) 162 | last_commits = models.TextField(default=None, null=True, blank=True) 163 | 164 | def get_last_commits(self): 165 | if self.last_commits == '""' or self.last_commits is None: 166 | self.last_commits = '[]' 167 | return json.loads(self.last_commits) 168 | 169 | def set_last_commits(self, last_commits): 170 | self.last_commits = json.dumps(last_commits) 171 | 172 | def __str__(self): 173 | return '{0} -> LaunchBuild: {1}/{2}/{3}'.format( 174 | self.launch, self.version, self.hash, self. branch) 175 | 176 | 177 | class TestResult(models.Model): 178 | launch = models.ForeignKey(Launch) 179 | name = models.CharField(_('Name'), max_length=128, db_index=True) 180 | suite = models.CharField(_('TestSuite'), max_length=256) 181 | state = models.IntegerField(_('State'), default=BLOCKED) 182 | failure_reason = models.TextField(_('Failure Reason'), default=None, 183 | blank=True, null=True) 184 | duration = models.FloatField(_('Duration time'), default=0.0) 185 | launch_item_id = models.IntegerField(blank=True, default=None, null=True) 186 | 187 | def __str__(self): 188 | return '{0} -> TestResult: {1}/{2}'.format( 189 | self.launch, self.suite, self.name) 190 | 191 | 192 | class LaunchItem(models.Model): 193 | test_plan = models.ForeignKey(TestPlan) 194 | name = models.CharField( 195 | max_length=128, default=None, null=True, blank=True) 196 | command = models.TextField() 197 | timeout = models.IntegerField(default=300) 198 | type = models.IntegerField(default=ASYNC_CALL) 199 | 200 | def __str__(self): 201 | return '{0} -> {1}'.format(self.test_plan.name, self.name) 202 | 203 | 204 | class Bug(models.Model): 205 | externalId = models.CharField(max_length=255, blank=False) 206 | name = models.CharField(max_length=255, default='', blank=True) 207 | regexp = models.CharField(max_length=255, default='', blank=False) 208 | state = models.CharField(max_length=32, default='', blank=True) 209 | updated = models.DateTimeField(auto_now=True) 210 | 211 | def get_state(self): 212 | return self.state 213 | 214 | def __str__(self): 215 | return ':'.join((self.externalId, self.name)) 216 | 217 | 218 | def get_issue_fields_from_bts(externalId): 219 | log.debug('Get fields for bug {}'.format(externalId)) 220 | res = _get_bug(externalId) 221 | if 'fields' in res: 222 | return res['fields'] 223 | return res 224 | 225 | 226 | def _get_bug(bug_id): 227 | response = requests.get( 228 | 'https://{}{}'.format( 229 | settings.BUG_TRACKING_SYSTEM_HOST, 230 | settings.BUG_TRACKING_SYSTEM_BUG_PATH.format(issue_id=bug_id)), 231 | auth=(settings.BUG_TRACKING_SYSTEM_LOGIN, 232 | settings.BUG_TRACKING_SYSTEM_PASSWORD), 233 | headers={'Content-Type': 'application/json'}) 234 | data = response.json() 235 | log.debug(data) 236 | return data 237 | -------------------------------------------------------------------------------- /testreport/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from testreport.models import Launch, FINISHED, STOPPED, CELERY_FINISHED_STATES 4 | from testreport.models import Bug 5 | from testreport.models import get_issue_fields_from_bts 6 | 7 | from cdws_api.xml_parser import xml_parser_func 8 | 9 | from common.storage import get_s3_connection, get_or_create_bucket 10 | from comments.models import Comment 11 | 12 | import celery 13 | 14 | import os 15 | import stat 16 | import json 17 | from django.contrib.auth.models import User 18 | from django.utils import timezone 19 | from django.conf import settings 20 | from datetime import timedelta, datetime 21 | from time import sleep 22 | 23 | 24 | import logging 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | @celery.task() 29 | def finalize_launch(launch_id, state=FINISHED, timeout=30, tries=5): 30 | log.info("Finalize launch {}".format(launch_id)) 31 | launch = Launch.objects.get(pk=launch_id) 32 | log.info("Current launch: {}".format(launch.__dict__)) 33 | launch.finished = datetime.now() 34 | launch.calculate_counts() 35 | launch.state = state 36 | log.info("Launch for update: {}".format(launch.__dict__)) 37 | launch.save(force_update=True) 38 | if state != STOPPED: 39 | for i in range(0, tries): 40 | log.info("Waiting for {} seconds, before next try".format(timeout)) 41 | sleep(timeout) 42 | launch = Launch.objects.get(pk=launch_id) 43 | if launch.state == state: 44 | break 45 | log.info("Launch state not finished, try to save again.") 46 | launch.finished = datetime.now() 47 | launch.state = state 48 | launch.save() 49 | log.info( 50 | "Updated launch: {}".format(Launch.objects.get(pk=launch_id).__dict__)) 51 | 52 | 53 | @celery.task() 54 | def create_environment(environment_vars, json_file): 55 | workspace_path = environment_vars['WORKSPACE'] 56 | # Create workspace directory 57 | if not os.path.exists(workspace_path): 58 | os.makedirs(workspace_path) 59 | os.chmod(workspace_path, 60 | stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH) 61 | 62 | # Write json file 63 | json_file_path = os.path.join(workspace_path, 'file.json') 64 | with open(json_file_path, 'w+') as f: 65 | f.write(json.dumps(json_file)) 66 | 67 | # Write environment file 68 | env_file_path = os.path.join(workspace_path, 'environments.sh') 69 | output = '' 70 | for key, value in iter(environment_vars.items()): 71 | output += 'export {key}="{value}"\n'.format(key=key, value=value) 72 | with open(env_file_path, 'w+') as f: 73 | f.write(output) 74 | 75 | 76 | @celery.task() 77 | def finalize_broken_launches(): 78 | log.debug("Finalize broke launches...") 79 | 80 | def is_finished(launch): 81 | log.debug("Check {} is finished".format(launch)) 82 | for k, v in iter(launch.get_tasks().items()): 83 | res = celery.result.AsyncResult(k) 84 | if res.state not in CELERY_FINISHED_STATES: 85 | return False 86 | return True 87 | 88 | def process(launch): 89 | if is_finished(launch): 90 | finalize_launch(launch.id) 91 | return launch 92 | 93 | return list(map(process, Launch.objects.filter(state__exact=0))) 94 | 95 | 96 | @celery.task() 97 | def cleanup_database(): 98 | days = timezone.now().date() - timedelta( 99 | days=settings.STORE_TESTRESULTS_IN_DAYS) 100 | 101 | list(map(lambda launch: launch.testresult_set.all().delete(), 102 | Launch.objects.filter(finished__lte=days))) 103 | 104 | 105 | @celery.task() 106 | def update_bugs(): 107 | if settings.JIRA_INTEGRATION: 108 | for bug in Bug.objects.all(): 109 | try: 110 | update_state(bug) 111 | except Exception as e: 112 | log.error('Unable to update bug {}: {}'. 113 | format(bug.externalId, e)) 114 | else: 115 | log.info('Jira integration is off. ' 116 | 'If you want to use this feature, turn it on.') 117 | 118 | 119 | def update_state(bug): 120 | log.debug('Starting bug "{}" update'.format(bug.externalId)) 121 | now = datetime.utcnow() 122 | td = now - datetime.replace(bug.updated, tzinfo=None) 123 | diff = (td.microseconds + 124 | (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 125 | 126 | if bug.state in settings.BUG_STATE_EXPIRED: 127 | old_state = bug.state 128 | new_state = \ 129 | get_issue_fields_from_bts(bug.externalId)['status']['name'] 130 | log.debug('Comparing bug state,' 131 | '"{0}" and "{1}"'.format(old_state, new_state)) 132 | if old_state == new_state and diff > float(settings.BUG_TIME_EXPIRED): 133 | log.debug( 134 | 'Bug "{}" expired, deleting it from DB'.format(bug.externalId)) 135 | bug.delete() 136 | elif old_state == new_state \ 137 | and diff < float(settings.BUG_TIME_EXPIRED): 138 | log.debug( 139 | 'Bug "{}" not updated, ' 140 | 'because {} seconds not expired'.format( 141 | bug.externalId, settings.BUG_TIME_EXPIRED)) 142 | else: 143 | bug.state = new_state 144 | bug.updated = now 145 | log.debug('Saving bug "{}"'.format(bug.externalId)) 146 | bug.save() 147 | if bug.state not in settings.BUG_STATE_EXPIRED \ 148 | and diff > float(settings.TIME_BEFORE_UPDATE_BUG_INFO): 149 | log.debug("%s > %s time to update bug state.", diff, 150 | settings.TIME_BEFORE_UPDATE_BUG_INFO) 151 | bug.updated = now 152 | bug.state = \ 153 | get_issue_fields_from_bts(bug.externalId)['status']['name'] 154 | log.debug('Saving bug "{}"'.format(bug.externalId)) 155 | bug.save() 156 | 157 | 158 | @celery.task(bind=True) 159 | def parse_xml(self, xunit_format, launch_id, params, s3_conn=False, 160 | s3_key_name=None, file_content=None): 161 | try: 162 | if s3_conn: 163 | s3_connection = get_s3_connection() 164 | log.debug('Trying to get file from {}'.format(settings.S3_HOST)) 165 | file_content = \ 166 | get_file_from_storage(s3_connection, s3_key_name).read() 167 | log.debug('Getting file is successful') 168 | 169 | log.debug('Start parsing xml {}'.format(s3_key_name)) 170 | xml_parser_func(format=xunit_format, 171 | file_content=file_content, 172 | launch_id=launch_id, 173 | params=params) 174 | log.debug('Xml parsed successful') 175 | except ConnectionRefusedError as e: 176 | log.error(e) 177 | comment = 'There are some problems with ' \ 178 | 'connection to {}: "{}". '.format(settings.S3_HOST, e) 179 | 180 | if parse_xml.request.retries < settings.S3_MAX_RETRIES: 181 | comment += 'Next try in {} min.'.\ 182 | format(int(settings.S3_COUNTDOWN / 60)) 183 | 184 | add_comment_to_launch(launch_id, comment) 185 | return self.retry(countdown=settings.S3_COUNTDOWN, 186 | throw=False, exc=e) 187 | 188 | comment += 'Please, try to send your xml later.' 189 | add_comment_to_launch(launch_id=launch_id, comment=comment) 190 | except Exception as e: 191 | log.error(e) 192 | 193 | comment = 'During xml parsing the ' \ 194 | 'following error is received: "{}"'.format(e) 195 | add_comment_to_launch(launch_id, comment) 196 | 197 | if s3_conn: 198 | finalize_launch(launch_id=launch_id, tries=0) 199 | delete_file_from_storage(s3_connection, s3_key_name) 200 | log.debug('Xml file "{}" deleted'.format(s3_key_name)) 201 | else: 202 | launch = Launch.objects.get(pk=launch_id) 203 | launch.calculate_counts() 204 | 205 | 206 | def delete_file_from_storage(s3_connection, file_name): 207 | bucket = get_or_create_bucket(s3_connection) 208 | bucket.delete_key(file_name) 209 | 210 | 211 | def get_file_from_storage(s3_connection, file_name): 212 | bucket = get_or_create_bucket(s3_connection) 213 | report = bucket.get_key(file_name) 214 | if report is None: 215 | raise Exception('Xml not found in bucket "{}"'. 216 | format(settings.S3_BUCKET_NAME)) 217 | return report 218 | 219 | 220 | def add_comment_to_launch(launch_id, comment): 221 | Comment.objects.create(comment=comment, 222 | object_pk=launch_id, 223 | content_type_id=17, 224 | user=User.objects.get(username='xml-parser')) 225 | -------------------------------------------------------------------------------- /testreport/templates/base.html: -------------------------------------------------------------------------------- 1 | CDWS API backend 2 | -------------------------------------------------------------------------------- /testreport/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from common.models import Project 4 | from common.tasks import launch_process 5 | 6 | from testreport.models import TestPlan 7 | from testreport.models import Launch 8 | from testreport.models import TestResult 9 | from testreport.models import FAILED 10 | from testreport.models import PASSED 11 | 12 | 13 | class ProjectTests(TestCase): 14 | def tearDown(self): 15 | Project.objects.all().delete() 16 | 17 | def test_creation(self): 18 | p = Project(name='Test Project 1') 19 | p.save() 20 | p1 = Project.objects.get(name__exact='Test Project 1') 21 | self.assertEqual(p, p1) 22 | 23 | 24 | class TestPlanTests(TestCase): 25 | project = None 26 | 27 | def setUp(self): 28 | self.project = Project(name='Test Project 1') 29 | self.project.save() 30 | 31 | def tearDown(self): 32 | Project.objects.all().delete() 33 | TestPlan.objects.all().delete() 34 | 35 | def test_creation(self): 36 | tp = TestPlan(name='TestPlan1', project=self.project) 37 | tp.save() 38 | tp1 = TestPlan.objects.get(name='TestPlan1') 39 | self.assertEqual(tp, tp1) 40 | 41 | def test_duplication(self): 42 | TestPlan.objects.get_or_create(name='TestPlan1', project=self.project) 43 | TestPlan.objects.get_or_create(name='TestPlan1', project=self.project) 44 | self.assertEqual(len(TestPlan.objects.all()), 1) 45 | 46 | 47 | class TestLaunch(TestCase): 48 | project = None 49 | tp = None 50 | 51 | def setUp(self): 52 | self.project = Project(name='Test Project 1') 53 | self.project.save() 54 | self.tp = TestPlan(name='Test Project 1', project=self.project) 55 | self.tp.save() 56 | 57 | def tearDown(self): 58 | Project.objects.all().delete() 59 | TestPlan.objects.all().delete() 60 | Launch.objects.all().delete() 61 | 62 | def test_creation(self): 63 | url = 'http://2gis.local' 64 | launch = Launch(test_plan=self.tp, started_by=url) 65 | launch.save() 66 | l1 = self.tp.launch_set.first() 67 | self.assertEqual(launch, l1) 68 | l1.started_by = url 69 | 70 | 71 | class TestResultTest(TestCase): 72 | project = None 73 | tp = None 74 | launch = None 75 | 76 | def setUp(self): 77 | self.project = Project(name='Test Project 1') 78 | self.project.save() 79 | self.tp = TestPlan(name='Test Plan 1', project=self.project) 80 | self.tp.save() 81 | self.launch = Launch(test_plan=self.tp) 82 | self.launch.save() 83 | 84 | def tearDown(self): 85 | Project.objects.all().delete() 86 | TestPlan.objects.all().delete() 87 | Launch.objects.all().delete() 88 | 89 | def test_creation(self): 90 | r = TestResult(launch=self.launch, name='TestCase1', suite='TestSute1', 91 | state=FAILED, 92 | failure_reason='Very clear message about failure', 93 | duration=1) 94 | r1 = TestResult(launch=self.launch, name='TestCase1', 95 | suite='TestSute2', 96 | state=PASSED, 97 | failure_reason='Very clear message about failure', 98 | duration=1) 99 | r.save() 100 | r1.save() 101 | self.assertEqual(len(self.launch.testresult_set.all()), 2) 102 | 103 | 104 | class TestLaunchProcessFunction(TestCase): 105 | def test_success(self): 106 | output = launch_process('echo "Hello world"') 107 | self.assertEqual(output['stdout'], b'Hello world\n') 108 | self.assertEqual(output['stderr'], b'') 109 | self.assertEqual(output['return_code'], 0) 110 | 111 | def test_failed(self): 112 | output = launch_process('echo "Error" 1>&2; exit 1') 113 | self.assertEqual(output['stdout'], b'') 114 | self.assertEqual(output['stderr'], b'Error\n') 115 | self.assertEqual(output['return_code'], 1) 116 | 117 | def test_env(self): 118 | test_dict = { 119 | 'HOME': '/tmp/', 120 | 'var1': 'VALUE', 121 | 'Test2': '0' 122 | } 123 | output = launch_process('echo "${HOME};${var1};${Test2}"', 124 | env=test_dict) 125 | self.assertDictEqual(output['env'], test_dict) 126 | self.assertEqual(output['stdout'], b'/tmp/;VALUE;0\n') 127 | -------------------------------------------------------------------------------- /testreport/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | import logging 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | 8 | class Base(TemplateView): 9 | template_name = 'base.html' 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py34 4 | 5 | [testenv] 6 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 7 | setenv = 8 | CDWS_API_HOSTNAME=localhost 9 | BROKER_URL=sqla+sqlite:///celerydb.sqlite 10 | CDWS_DEPLOY_DIR=/tmp 11 | CDWS_WORKING_DIR=/cdws 12 | DEBUG=False 13 | JIRA_INTEGRATION = True 14 | TIME_ZONE=UTC 15 | deps = -r{toxinidir}/requirements.txt 16 | -r{toxinidir}/test-requirements.txt 17 | coveralls 18 | flake8 19 | 20 | commands = 21 | flake8 22 | coverage erase 23 | coverage run --source='.' --rcfile=.coveragerc manage.py test --verbosity 2 24 | python {toxinidir}/run_coveralls.py 25 | 26 | [flake8] 27 | show-source = true 28 | exclude=*/migrations/*,.tox/* 29 | --------------------------------------------------------------------------------