├── .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 [](https://travis-ci.org/2gis/badger-api) [](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 [](https://travis-ci.org/2gis/badger-api) [](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 |
Your username and password didn't match. Please try again.
6 | {% endif %}
7 |
29 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/common/templates/logout.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block container %}
3 |
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 |
--------------------------------------------------------------------------------