Add initial testRail bot implementation
Related-PROD: PRODX-5842
Change-Id: Id2ac9b2275ced80a95019d30ae9e0f7a967f07ec
diff --git a/.gitignore b/.gitignore
index 21daff0..8d00c83 100755
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,6 @@
.i*
.v*
oscore-e717344565a0.json
-.*
-__*
*.json
*.log
-
+venv/
\ No newline at end of file
diff --git a/testrail_bot/.env b/testrail_bot/.env
new file mode 100644
index 0000000..2f9dcb1
--- /dev/null
+++ b/testrail_bot/.env
@@ -0,0 +1,12 @@
+DEBUG=0
+SECRET_KEY=y3y+y(^(m*4=$g&a4mu^0u#2zcs4(fekxbwa(&3w@n_zqgmuiz
+DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
+SQL_ENGINE=django.db.backends.postgresql
+SQL_DATABASE=dev
+SQL_USER=dev
+SQL_PASSWORD=dev
+SQL_HOST=db
+SQL_PORT=5432
+DATABASE=postgres
+TESTRAIL_EMAIL=mosqa-eng@mirantis.com
+TESTRAIL_PASSWORD=mosqa-eng496F
diff --git a/testrail_bot/Dockerfile b/testrail_bot/Dockerfile
new file mode 100644
index 0000000..508de46
--- /dev/null
+++ b/testrail_bot/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.8.3-alpine
+ENV PYTHONUNBUFFERED 1
+COPY . /testrail_bot/
+WORKDIR /testrail_bot
+
+RUN apk update
+RUN apk add postgresql-dev gcc python3-dev musl-dev linux-headers
+
+RUN pip install -r requirements.txt
diff --git a/testrail_bot/README.md b/testrail_bot/README.md
new file mode 100644
index 0000000..ded468d
--- /dev/null
+++ b/testrail_bot/README.md
@@ -0,0 +1,8 @@
+# TestRail bot
+
+## Download repository
+`git clone "https://gerrit.mcp.mirantis.com/mcp/osccore-qa-testing-tools"`
+
+## Start docker-compose
+`cd osccore-qa-testing-tools/testrail_bot`
+`docker-compose up`
diff --git a/testrail_bot/__init__.py b/testrail_bot/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/__init__.py
diff --git a/testrail_bot/control/__init__.py b/testrail_bot/control/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/control/__init__.py
diff --git a/testrail_bot/control/admin.py b/testrail_bot/control/admin.py
new file mode 100644
index 0000000..df2bfb3
--- /dev/null
+++ b/testrail_bot/control/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from .models import TestRun, Report
+
+admin.site.register(TestRun)
+admin.site.register(Report)
diff --git a/testrail_bot/control/apps.py b/testrail_bot/control/apps.py
new file mode 100644
index 0000000..a5f7085
--- /dev/null
+++ b/testrail_bot/control/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ControlConfig(AppConfig):
+ name = 'control'
diff --git a/testrail_bot/control/celery_tasks/__init__.py b/testrail_bot/control/celery_tasks/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/__init__.py
diff --git a/testrail_bot/control/celery_tasks/api.py b/testrail_bot/control/celery_tasks/api.py
new file mode 100644
index 0000000..2954903
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/api.py
@@ -0,0 +1,64 @@
+from testrail_api import TestRailAPI, StatusCodeError
+
+from django.conf import settings
+
+from .enums import StatusEnum
+
+api = TestRailAPI(
+ "https://mirantis.testrail.com/",
+ settings.TESTRAIL_EMAIL,
+ settings.TESTRAIL_PASSWORD)
+
+
+def get_project_id(project_name):
+ project = list(filter(
+ lambda x: x["name"] == project_name,
+ api.projects.get_projects()))
+ if project:
+ return project[0]["id"]
+ else:
+ return None
+
+
+def get_plans(project_id, plan_name, **kwargs):
+ plans = api.plans.get_plans(project_id, **kwargs)
+ return [x["id"] for x in filter(
+ lambda x: plan_name in x["name"], plans)]
+
+
+def get_entries(plan_id):
+ return api.plans.get_plan(plan_id)["entries"]
+
+
+def get_run_id(entries, run_name):
+ entries = list(filter(
+ lambda x: x["name"] == run_name,
+ entries))
+ if not entries:
+ return None
+ return entries[0]["runs"][0]["id"]
+
+
+def get_result_for_case(run_id, case_id):
+ try:
+ results = api.results.get_results_for_case(run_id, case_id)
+ except StatusCodeError:
+ return None
+ return results
+
+
+def get_failed_tests(last_run_id):
+ return api.tests.get_tests(
+ last_run_id, status_id=StatusEnum.failed)
+
+
+def add_result(test_id, update_dict):
+ api.results.add_result(test_id, **update_dict)
+
+
+def get_user_id(user_name):
+ users = api.users.get_users()
+ users = list(filter(lambda x: x["name"] == user_name, users))
+ if not users:
+ return None
+ return users[0]["id"]
diff --git a/testrail_bot/control/celery_tasks/enums.py b/testrail_bot/control/celery_tasks/enums.py
new file mode 100644
index 0000000..f7b7251
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/enums.py
@@ -0,0 +1,13 @@
+class StatusEnum:
+ passed = "1"
+ blocked = "2"
+ untested = "3"
+ retest = "4"
+ failed = "5"
+ skipped = "6"
+ in_progress = "7"
+ product_failed = "8"
+ test_failed = "9"
+ wont_fix = "10"
+ mixes_success = "11"
+ wont_test = "12"
diff --git a/testrail_bot/control/celery_tasks/filters.py b/testrail_bot/control/celery_tasks/filters.py
new file mode 100644
index 0000000..f552dfc
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/filters.py
@@ -0,0 +1,16 @@
+import re
+
+
+def filter_ip(data):
+ ip_addr_regex = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b')
+ return re.sub(ip_addr_regex, "x.x.x.x", data)
+
+
+def filter_uuid(data):
+ uuid4hex = re.compile(
+ r'[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I)
+ return re.sub(uuid4hex, "xxxx", data)
+
+
+def last_traceback_filter(data):
+ return data[data.rfind("Traceback"):]
diff --git a/testrail_bot/control/celery_tasks/tasks.py b/testrail_bot/control/celery_tasks/tasks.py
new file mode 100644
index 0000000..8566653
--- /dev/null
+++ b/testrail_bot/control/celery_tasks/tasks.py
@@ -0,0 +1,126 @@
+from __future__ import absolute_import, unicode_literals
+
+import difflib
+
+from celery import shared_task
+
+from .. import models
+
+from . import filters
+from . import api
+from .enums import StatusEnum
+
+
+def finish_report(report):
+ report.finished = True
+ report.save()
+
+
+def apply_filters(data, test_run):
+ if test_run.filter_last_traceback:
+ data = filters.last_traceback_filter(data)
+
+ if test_run.ip_filter:
+ data = filters.filter_ip(data)
+
+ if test_run.uuid_filter:
+ data = filters.filter_uuid(data)
+
+ if test_run.filter_func:
+ import rpdb;rpdb.set_trace()
+ exec(test_run.filter_func)
+ data = locals()["custom_filter"](data)
+ return data
+
+
+def process_plan(plan_id, case_id, last_comment, run):
+ run_id = api.get_run_id(api.get_entries(plan_id), run.run_name)
+ if not run_id:
+ return None, -1.0
+
+ results = api.get_result_for_case(run_id, case_id)
+
+ if not results:
+ return None, -2.0
+
+ status_code = str(results[0]["status_id"])
+ if status_code not in [StatusEnum.test_failed, StatusEnum.product_failed]:
+ return None, -3.0
+
+ comment = apply_filters(results[-1]["comment"], run)
+
+ ratio = difflib.SequenceMatcher(
+ lambda symbol: symbol in [" ", ",", "\n"],
+ last_comment, comment, autojunk=False).ratio()
+ if ratio > 0.9:
+ return results[0], ratio
+ return None, ratio
+
+
+@shared_task
+def process_run(run_id, report_id, path):
+ report = models.Report.objects.get(pk=report_id)
+ with open(path, "w") as f:
+ test_run = models.TestRun.objects.get(pk=run_id)
+ f.write("Start processing {}\n".format(test_run.run_name))
+ f.flush()
+
+ project_id = api.get_project_id(test_run.project_name)
+ if not project_id:
+ f.write("Incorrect Project {}. Stopping processing\n".format(
+ test_run.project_name))
+ f.flush()
+ finish_report(report)
+ return
+
+ created_by_id = api.get_user_id(test_run.created_by)
+ kw = {"limit": 100}
+ if created_by_id:
+ kw["created_by"] = created_by_id
+ plans = api.get_plans(project_id, test_run.plan_name, **kw)
+
+ last_plan = plans[0]
+ last_run_id = api.get_run_id(
+ api.get_entries(last_plan), test_run.run_name)
+ if not last_run_id:
+ f.write("No {} in {} plan\n".format(
+ test_run.run_name, last_plan))
+ f.flush()
+ finish_report(report)
+ return
+
+ failed_tests = api.get_failed_tests(last_run_id)
+ for test in failed_tests:
+ case_id = test["case_id"]
+
+ f.write("Processing test with id {}\n".format(test["id"]))
+ f.flush()
+
+ last_result = api.get_result_for_case(last_run_id, case_id)[0]
+
+ last_comment = apply_filters(
+ last_result["comment"], test_run)
+
+ for plan_id in plans[1:]:
+ sim_result, ratio = process_plan(
+ plan_id, case_id, last_comment, test_run)
+ if sim_result:
+ update_dict = {
+ "status_id": sim_result["status_id"],
+ "comment": "Marked by TestRailBot because "
+ "of similarity with test {} {}%".format(
+ sim_result["test_id"], round(100.0 * ratio, 2)),
+ "defects": sim_result["defects"]
+ }
+ f.write("Found similarity: {}\n".format(update_dict))
+ f.flush()
+ api.add_result(test["id"], update_dict)
+ break
+ else:
+ f.write(
+ "Similarity not found due to similarity:{}%\n".format(
+ round(100.0 * ratio, 2)))
+ f.flush()
+ f.write("Test processing finished")
+ f.flush()
+ finish_report(report)
diff --git a/testrail_bot/control/forms.py b/testrail_bot/control/forms.py
new file mode 100644
index 0000000..9375f42
--- /dev/null
+++ b/testrail_bot/control/forms.py
@@ -0,0 +1,22 @@
+from django.forms import ModelForm
+from .models import TestRun
+
+
+class TestRunForm(ModelForm):
+ class Meta:
+ model = TestRun
+ fields = "__all__"
+ labels = {
+ "project_name": "Name of the project",
+ "plan_name": "Name of the Test Plan without date",
+ "run_name": "Name of the run",
+ "created_by": "Test Run created by",
+ "filter_func": "Custom filter function",
+ "ip_filter": "Change ip to x.x.x.x",
+ "uuid_filter": "Change uuid to xxxx",
+ "filter_last_traceback": "Use only last traceback to "
+ "compare comments"
+ }
+ help_texts = {
+ "filter_func": "Leave blank if not used",
+ }
diff --git a/testrail_bot/control/migrations/0001_initial.py b/testrail_bot/control/migrations/0001_initial.py
new file mode 100644
index 0000000..774da4a
--- /dev/null
+++ b/testrail_bot/control/migrations/0001_initial.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-06-30 13:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TestRun',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('project_name', models.CharField(max_length=300)),
+ ('plan_name', models.CharField(max_length=300)),
+ ('run_name', models.CharField(max_length=300)),
+ ('filter_func', models.TextField()),
+ ],
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0002_auto_20200701_0735.py b/testrail_bot/control/migrations/0002_auto_20200701_0735.py
new file mode 100644
index 0000000..b1070b9
--- /dev/null
+++ b/testrail_bot/control/migrations/0002_auto_20200701_0735.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2020-07-01 07:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='testrun',
+ name='ip_filter',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='testrun',
+ name='uuid_filter',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0003_report.py b/testrail_bot/control/migrations/0003_report.py
new file mode 100644
index 0000000..55b5d7b
--- /dev/null
+++ b/testrail_bot/control/migrations/0003_report.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2020-07-01 08:09
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0002_auto_20200701_0735'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Report',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('path', models.FileField(upload_to='')),
+ ('report_name', models.CharField(max_length=300)),
+ ('test_run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='control.TestRun')),
+ ],
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0004_auto_20200701_1342.py b/testrail_bot/control/migrations/0004_auto_20200701_1342.py
new file mode 100644
index 0000000..5928c8e
--- /dev/null
+++ b/testrail_bot/control/migrations/0004_auto_20200701_1342.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-07-01 13:42
+
+import django.core.files.storage
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0003_report'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='report',
+ name='path',
+ field=models.FileField(storage=django.core.files.storage.FileSystemStorage(location='media/reports'), upload_to=''),
+ ),
+ migrations.AlterField(
+ model_name='testrun',
+ name='filter_func',
+ field=models.TextField(null=True),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0005_auto_20200701_1344.py b/testrail_bot/control/migrations/0005_auto_20200701_1344.py
new file mode 100644
index 0000000..46b38e8
--- /dev/null
+++ b/testrail_bot/control/migrations/0005_auto_20200701_1344.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-01 13:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0004_auto_20200701_1342'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='testrun',
+ name='filter_func',
+ field=models.TextField(default='', null=True),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0006_auto_20200701_1347.py b/testrail_bot/control/migrations/0006_auto_20200701_1347.py
new file mode 100644
index 0000000..9e40f0e
--- /dev/null
+++ b/testrail_bot/control/migrations/0006_auto_20200701_1347.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-01 13:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0005_auto_20200701_1344'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='testrun',
+ name='filter_func',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0007_auto_20200702_1416.py b/testrail_bot/control/migrations/0007_auto_20200702_1416.py
new file mode 100644
index 0000000..7940406
--- /dev/null
+++ b/testrail_bot/control/migrations/0007_auto_20200702_1416.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.7 on 2020-07-02 14:16
+
+import django.core.files.storage
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0006_auto_20200701_1347'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='report',
+ name='finished',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='report',
+ name='path',
+ field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(location='media/reports'), upload_to=''),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0008_testrun_created_by.py b/testrail_bot/control/migrations/0008_testrun_created_by.py
new file mode 100644
index 0000000..eb0ec87
--- /dev/null
+++ b/testrail_bot/control/migrations/0008_testrun_created_by.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-03 08:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0007_auto_20200702_1416'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='testrun',
+ name='created_by',
+ field=models.CharField(default='', max_length=300),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0009_testrun_filter_last_traceback.py b/testrail_bot/control/migrations/0009_testrun_filter_last_traceback.py
new file mode 100644
index 0000000..49aa60d
--- /dev/null
+++ b/testrail_bot/control/migrations/0009_testrun_filter_last_traceback.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2020-07-03 11:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0008_testrun_created_by'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='testrun',
+ name='filter_last_traceback',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/0010_auto_20200709_0831.py b/testrail_bot/control/migrations/0010_auto_20200709_0831.py
new file mode 100644
index 0000000..826aac8
--- /dev/null
+++ b/testrail_bot/control/migrations/0010_auto_20200709_0831.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.7 on 2020-07-09 08:31
+
+import django.core.files.storage
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('control', '0009_testrun_filter_last_traceback'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='report',
+ name='path',
+ field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(), upload_to=''),
+ ),
+ ]
diff --git a/testrail_bot/control/migrations/__init__.py b/testrail_bot/control/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testrail_bot/control/migrations/__init__.py
diff --git a/testrail_bot/control/models.py b/testrail_bot/control/models.py
new file mode 100644
index 0000000..6a13c8e
--- /dev/null
+++ b/testrail_bot/control/models.py
@@ -0,0 +1,23 @@
+from django.core.files.storage import FileSystemStorage
+from django.db import models
+
+
+class TestRun(models.Model):
+ project_name = models.CharField(max_length=300)
+ plan_name = models.CharField(max_length=300)
+ run_name = models.CharField(max_length=300)
+ created_by = models.CharField(max_length=300, default="")
+ filter_func = models.TextField(null=True, blank=True)
+ ip_filter = models.BooleanField(default=False)
+ uuid_filter = models.BooleanField(default=False)
+ filter_last_traceback = models.BooleanField(default=False)
+
+
+fs = FileSystemStorage()
+
+
+class Report(models.Model):
+ path = models.FileField(storage=fs, null=True, blank=True)
+ test_run = models.ForeignKey(TestRun, on_delete=models.CASCADE)
+ report_name = models.CharField(max_length=300)
+ finished = models.BooleanField(default=False)
diff --git a/testrail_bot/control/templates/base.html b/testrail_bot/control/templates/base.html
new file mode 100644
index 0000000..e21330b
--- /dev/null
+++ b/testrail_bot/control/templates/base.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+{% load bootstrap3 %}
+{% bootstrap_css %}
+{% bootstrap_javascript %}
+
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>TestRail bot configuration</title>
+ <style>
+ body { padding-top: 70px; }
+ </style>
+ {% block head %}
+ {% endblock %}
+</head>
+<body>
+<nav class="navbar navbar-inverse navbar-fixed-top">
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <a class="navbar-brand" href="{% url 'index' %}">TestRail bot</a>
+ </div>
+ <ul class="nav navbar-nav">
+ <li><a href="{% url 'index' %}">Test Runs</a></li>
+ <li><a href="{% url 'create_run' %}">Create New Test Run</a></li>
+ <li><a href="{% url 'list_reports' %}">Reports</a></li>
+ <li><a href="{% url 'help' %}">Help</a></li>
+
+ </ul>
+ </div>
+</nav>
+{% block section %}{% endblock %}
+</body>
+</html>
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/create_run.html b/testrail_bot/control/templates/control/create_run.html
new file mode 100644
index 0000000..9d6ddb1
--- /dev/null
+++ b/testrail_bot/control/templates/control/create_run.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+{% load bootstrap3 %}
+{% block section %}
+<div>
+ <form action="{% url 'create_run' %}" method="post" class="form">
+ {% csrf_token %}
+ {% bootstrap_form_errors form type='non_fields' %}
+ <div class="col-xs-8">{% bootstrap_field form.project_name size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.plan_name size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.run_name size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.created_by size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.filter_func size='sm' %}</div>
+ <div class="col-md-5">
+ {% bootstrap_field form.ip_filter size='sm'%}
+ </div>
+ <div class="col-md-5">
+ {% bootstrap_field form.uuid_filter size='sm'%}
+ </div>
+ <div class="col-md-5">
+ {% bootstrap_field form.filter_last_traceback size='sm'%}
+ </div>
+ {% buttons %}
+ <div class="btn-toolbar col-xs-7">
+ <button type="submit" class="btn btn-primary">
+ {% bootstrap_icon "star" %} Save
+ </button>
+ </div>
+ {% endbuttons %}
+ </form>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/help.html b/testrail_bot/control/templates/control/help.html
new file mode 100644
index 0000000..d4dbf69
--- /dev/null
+++ b/testrail_bot/control/templates/control/help.html
@@ -0,0 +1,71 @@
+{% extends "base.html" %}
+{% load static %}
+{% block section %}
+<h1 class="display-1 text-center">How to use</h1>
+<div class="col-xs-8">
+ <h3 class="display-3 text-center">Create Run</h3>
+ <ul>
+ <li>
+ Open <a href="{% url 'create_run' %}"><code>Create New Test Run</code></a> page.
+ <img style="width:100%" src="{% static 'control/help_1.png' %}"/>
+ </li>
+ <li>
+ Fill input forms:
+ <ol>
+ <li>Enter Project name i.e. <code>Mirantis Cloud Platform</code></li>
+ <li>Enter Plan name without date i.e. <code>[MCP2.0]OSCORE</code></li>
+ <li>Enter full name of the run i.e. <code>ussuri-core-ceph-ceph-dvr <[MCP2.0_USSURI]Tempest></code></li>
+ <li>Fill created by column i.e. <code>os-qa-bot</code></li>
+ <li>(Optional) Enter custom filtration function.
+ If not used it should be empty. If used, the function definition should be like:
+ <pre>
+def custom_filter(data):
+ ...
+ return modified_data</pre>
+ </li>
+ <li>Select <code>change ip to x.x.x.x</code> to replace all ip addresses to x.x.x.x</li>
+ <li>Select <code>change uuid to xxxx</code> to change all uuids to xxxx</li>
+ <li>Select <code>Use only last traceback to compare comments</code> to use data located after last <code>Traceback:</code></li>
+ <li>Click save to save record and run it later</li>
+ </ol>
+ </li>
+
+ </ul>
+</div>
+<div class="col-xs-8">
+ <h3 class="display-3 text-center">Change Run</h3>
+ <ul>
+ <li>Open <a href="{% url 'index' %}"><code>Test Runs</code></a> page</li>
+ <li>
+ Select run that you want to change from list of available runs.
+ <img style="width:100%" src="{% static 'control/help_2.png' %}"/>
+ </li>
+ <li>
+ Change fields like it's described in Create Run How To.
+ </li>
+ <li>Click <code>Save</code></li>
+ </ul>
+</div>
+<div class="col-xs-8">
+ <h3 class="display-3 text-center">Submit Run</h3>
+ <ul>
+ <li>Open <a href="{% url 'index' %}"><code>Test Runs</code></a> page</li>
+ <li>
+ Select run that you want to change from list of available runs.
+ <img style="width:100%" src="{% static 'control/help_2.png' %}"/>
+ </li>
+ <li>Click <code>Submit</code></li>
+ <li>You will be redirected to report for this run, where you can view how test run is being processed.</li>
+ </ul>
+</div>
+<div class="col-xs-8">
+ <h3 class="display-3 text-center">Generate Report</h3>
+ <ul>
+ <li>Open <a href="{% url 'list_reports' %}"><code>Reports</code></a> page</li>
+ <li>
+ Select report that you want to view.
+ <img style="width:100%" src="{% static 'control/help_3.png' %}"/>
+ </li>
+ </ul>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/index.html b/testrail_bot/control/templates/control/index.html
new file mode 100644
index 0000000..4722575
--- /dev/null
+++ b/testrail_bot/control/templates/control/index.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% block section %}
+<p>Test Runs:</p>
+<div class="list-group">
+ {% for run in runs %}
+ <a href="{% url 'single_run' run.id %}" class="list-group-item list-group-item-success">
+ Run {{run.run_name}}
+ </a>
+ {% endfor %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/report.html b/testrail_bot/control/templates/control/report.html
new file mode 100644
index 0000000..2adc34b
--- /dev/null
+++ b/testrail_bot/control/templates/control/report.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+{% block section %}
+{% load bootstrap3 %}
+{% bootstrap_javascript jquery=True %}
+<script>
+function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+};
+const csrftoken = getCookie('csrftoken');
+
+function send(){
+ $.ajax({
+ type: "post",
+ url: "{% url 'single_report' report_id %}",
+ dataType: "json",
+ headers: {'X-CSRFToken': csrftoken},
+ success:function(data)
+ {
+ tag = document.getElementById("data");
+ tag.innerHTML = data["data"];
+ //Send another request in 10 seconds.
+ if (data["finished"] != true){
+ setTimeout(function(){
+ send();
+ }, 3000);
+ }
+ }
+ });
+}
+send();</script>
+<pre id="data">{{report}}</pre>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/reports.html b/testrail_bot/control/templates/control/reports.html
new file mode 100644
index 0000000..4f402a6
--- /dev/null
+++ b/testrail_bot/control/templates/control/reports.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% block section %}
+<p>Reports:</p>
+<div class="list-group">
+ {% for report in reports %}
+ <a href="{% url 'single_report' report.id %}" class="list-group-item list-group-item-success">
+ {{report.report_name}}
+ </a>
+ {% endfor %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/templates/control/update_run.html b/testrail_bot/control/templates/control/update_run.html
new file mode 100644
index 0000000..1893ee5
--- /dev/null
+++ b/testrail_bot/control/templates/control/update_run.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% load bootstrap3 %}
+{% block section %}
+<div>
+ <form action="{% url 'single_run' run_id %}" method="post" class="form">
+ {% csrf_token %}
+ {% bootstrap_form_errors form type='non_fields' %}
+ <div class="col-xs-8">{% bootstrap_field form.project_name size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.plan_name size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.run_name size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.created_by size='sm' %}</div>
+ <div class="col-xs-8">{% bootstrap_field form.filter_func size='sm' %}</div>
+ <div class="col-md-5">
+ {% bootstrap_field form.ip_filter size='sm'%}
+ </div>
+ <div class="col-md-5">
+ {% bootstrap_field form.uuid_filter size='sm'%}
+ </div>
+ <div class="col-md-5">
+ {% bootstrap_field form.filter_last_traceback size='sm'%}
+ </div>
+ {% buttons %}
+ <div class="btn-toolbar col-xs-7">
+ <button type="submit" class="btn btn-primary">
+ {% bootstrap_icon "star" %} Save
+ </button>
+ <button type="submit" formaction="{% url 'submit_run' run_id %}" class="btn btn-primary">
+ <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-chevron-double-right" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" d="M3.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L9.293 8 3.646 2.354a.5.5 0 0 1 0-.708z"/>
+ <path fill-rule="evenodd" d="M7.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L13.293 8 7.646 2.354a.5.5 0 0 1 0-.708z"/>
+ </svg>
+ Submit
+ </button>
+ </div>
+ {% endbuttons %}
+ </form>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/testrail_bot/control/tests.py b/testrail_bot/control/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/testrail_bot/control/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/testrail_bot/control/urls.py b/testrail_bot/control/urls.py
new file mode 100644
index 0000000..d530635
--- /dev/null
+++ b/testrail_bot/control/urls.py
@@ -0,0 +1,14 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("", views.redirect_to_index, name="redirect"),
+ path("runs/", views.create_run, name="create_run"),
+ path("runs/<int:run_id>/", views.single_run, name="single_run"),
+ path("runs/<int:run_id>/submit/", views.submit_run, name="submit_run"),
+ path("reports/", views.list_reports, name="list_reports"),
+ path("reports/<int:report_id>/", views.single_report, name="single_report"),
+ path('index/', views.index, name='index'),
+ path("help/", views.show_help, name="help"),
+]
diff --git a/testrail_bot/control/views.py b/testrail_bot/control/views.py
new file mode 100644
index 0000000..ee582eb
--- /dev/null
+++ b/testrail_bot/control/views.py
@@ -0,0 +1,83 @@
+from datetime import date
+import json
+import os
+
+from django.shortcuts import render, redirect, HttpResponse
+
+from . import models
+from . import forms
+from .celery_tasks.tasks import process_run
+
+
+def index(request):
+ runs = models.TestRun.objects.all()
+ return render(request, "control/index.html", {"runs": runs})
+
+
+def redirect_to_index(request):
+ return redirect("index")
+
+
+def single_run(request, run_id):
+ run = models.TestRun.objects.get(pk=run_id)
+ if request.method == "POST":
+ form = forms.TestRunForm(request.POST, instance=run)
+ if form.is_valid():
+ form.save()
+ return redirect("single_run", run_id)
+ else:
+ form = forms.TestRunForm(instance=run)
+
+ return render(request, "control/update_run.html",
+ {"form": form, "run_id": run_id})
+
+
+def create_run(request):
+ if request.method == "POST":
+ form = forms.TestRunForm(request.POST)
+ if form.is_valid():
+ obj = form.save()
+ return redirect("single_run", obj.id)
+ else:
+ form = forms.TestRunForm()
+
+ return render(request, "control/create_run.html", {"form": form})
+
+
+def list_reports(request):
+ reports = models.Report.objects.all()
+ return render(request, "control/reports.html", {"reports": reports})
+
+
+def single_report(request, report_id):
+ report = models.Report.objects.get(pk=report_id)
+ data = report.path.read().decode("utf-8")
+ if request.method == "POST" and request.is_ajax():
+ return HttpResponse(
+ json.dumps({"data": data, "finished": report.finished}),
+ content_type="application/json")
+ return render(request, "control/report.html",
+ {"report_id": report.id, "report": data})
+
+
+def submit_run(request, run_id):
+ run = models.TestRun.objects.get(pk=run_id)
+ report_name = "{}-{}".format(run.run_name, date.isoformat(date.today()))
+ path = os.path.join(models.fs.location, report_name)
+ with open(path, "w"):
+ pass
+
+ report = models.Report(
+ test_run=run,
+ report_name=report_name,
+ path=path)
+ report.save()
+ process_run.delay(run_id, report.id, path)
+
+ return render(
+ request, "control/report.html",
+ {"report": report.path.read().decode("utf-8"), "report_id": report.id})
+
+
+def show_help(request):
+ return render(request, "control/help.html")
diff --git a/testrail_bot/docker-compose.yml b/testrail_bot/docker-compose.yml
new file mode 100644
index 0000000..88dce9e
--- /dev/null
+++ b/testrail_bot/docker-compose.yml
@@ -0,0 +1,69 @@
+version: '3'
+
+services:
+ redis:
+ image: redis:6.0.5-alpine
+ networks:
+ - tr_bot
+ db:
+ image: postgres:12.0-alpine
+ volumes:
+ - postgres_data:/var/lib/postgresql/data/
+ environment:
+ - POSTGRES_USER=dev
+ - POSTGRES_PASSWORD=dev
+ - POSTGRES_DB=dev
+ networks:
+ - tr_bot
+ worker:
+ build: .
+ command: celery -A testrail_bot worker -l info
+ volumes:
+ - media_volume:/mediafiles
+ networks:
+ - tr_bot
+ depends_on:
+ - redis
+ - db
+ env_file:
+ - .env
+ web:
+ build: .
+ command: ./start_webapp.sh
+ volumes:
+ - .:/testrail_bot
+ - static_volume:/staticfiles
+ - media_volume:/mediafiles
+ networks:
+ - tr_bot
+ env_file:
+ - .env
+ expose:
+ - 8000
+ depends_on:
+ - db
+ nginx:
+ build: ./nginx
+ volumes:
+ - static_volume:/staticfiles
+ - media_volume:/mediafiles
+ ports:
+ - "80:80"
+ depends_on:
+ - web
+ networks:
+ - tr_bot
+
+
+networks:
+ tr_bot:
+ driver: bridge
+ ipam:
+ driver: default
+ config:
+ - subnet: 192.168.201.0/24
+
+volumes:
+ postgres_data:
+ static_volume:
+ media_volume:
\ No newline at end of file
diff --git a/testrail_bot/manage.py b/testrail_bot/manage.py
new file mode 100755
index 0000000..ff64df6
--- /dev/null
+++ b/testrail_bot/manage.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/testrail_bot/media/images/control/help_1.png b/testrail_bot/media/images/control/help_1.png
new file mode 100644
index 0000000..c6ad4f1
--- /dev/null
+++ b/testrail_bot/media/images/control/help_1.png
Binary files differ
diff --git a/testrail_bot/media/images/control/help_2.png b/testrail_bot/media/images/control/help_2.png
new file mode 100644
index 0000000..c29c3bb
--- /dev/null
+++ b/testrail_bot/media/images/control/help_2.png
Binary files differ
diff --git a/testrail_bot/media/images/control/help_3.png b/testrail_bot/media/images/control/help_3.png
new file mode 100644
index 0000000..1a1124f
--- /dev/null
+++ b/testrail_bot/media/images/control/help_3.png
Binary files differ
diff --git a/testrail_bot/nginx/Dockerfile b/testrail_bot/nginx/Dockerfile
new file mode 100644
index 0000000..8e5916e
--- /dev/null
+++ b/testrail_bot/nginx/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:1.19.0-alpine
+
+RUN rm /etc/nginx/conf.d/default.conf
+COPY nginx.conf /etc/nginx/conf.d
\ No newline at end of file
diff --git a/testrail_bot/nginx/nginx.conf b/testrail_bot/nginx/nginx.conf
new file mode 100644
index 0000000..5201782
--- /dev/null
+++ b/testrail_bot/nginx/nginx.conf
@@ -0,0 +1,21 @@
+upstream tr_bor {
+ server web:8000;
+
+}
+
+server {
+
+ listen 80;
+
+ location / {
+ proxy_pass http://web:8000;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_redirect off;
+ }
+
+ location /staticfiles/ {
+ alias /staticfiles/;
+ }
+
+}
\ No newline at end of file
diff --git a/testrail_bot/requirements.txt b/testrail_bot/requirements.txt
new file mode 100644
index 0000000..eebc681
--- /dev/null
+++ b/testrail_bot/requirements.txt
@@ -0,0 +1,7 @@
+celery==4.4.6
+Django==3.0.7
+django-bootstrap3==14.0.0
+psycopg2-binary==2.8.5
+redis==3.5.3
+testrail-api==1.7.0
+uWSGI==2.0.19.1
diff --git a/testrail_bot/start_webapp.sh b/testrail_bot/start_webapp.sh
new file mode 100755
index 0000000..81fbd83
--- /dev/null
+++ b/testrail_bot/start_webapp.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+if [ "$DATABASE" = "postgres" ]
+then
+ echo "Waiting for postgres..."
+
+ while ! nc -z $SQL_HOST $SQL_PORT; do
+ sleep 0.1
+ done
+
+ echo "PostgreSQL started"
+fi
+
+python manage.py migrate --noinput
+python manage.py collectstatic --noinput
+uwsgi --http :8000 --module testrail_bot.wsgi
diff --git a/testrail_bot/testrail_bot/__init__.py b/testrail_bot/testrail_bot/__init__.py
new file mode 100644
index 0000000..070e835
--- /dev/null
+++ b/testrail_bot/testrail_bot/__init__.py
@@ -0,0 +1,7 @@
+from __future__ import absolute_import, unicode_literals
+
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery import app as celery_app
+
+__all__ = ('celery_app',)
diff --git a/testrail_bot/testrail_bot/asgi.py b/testrail_bot/testrail_bot/asgi.py
new file mode 100644
index 0000000..be76c4c
--- /dev/null
+++ b/testrail_bot/testrail_bot/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for testrail_bot project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+
+application = get_asgi_application()
diff --git a/testrail_bot/testrail_bot/celery.py b/testrail_bot/testrail_bot/celery.py
new file mode 100644
index 0000000..9e4cd7b
--- /dev/null
+++ b/testrail_bot/testrail_bot/celery.py
@@ -0,0 +1,18 @@
+from __future__ import absolute_import, unicode_literals
+
+import os
+
+from celery import Celery
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+
+app = Celery('testrail_bot')
+
+app.config_from_object('django.conf:settings', namespace='CELERY')
+
+app.autodiscover_tasks()
+
+
+@app.task(bind=True)
+def debug_task(self):
+ print('Request: {0!r}'.format(self.request))
diff --git a/testrail_bot/testrail_bot/settings.py b/testrail_bot/testrail_bot/settings.py
new file mode 100644
index 0000000..b5b964a
--- /dev/null
+++ b/testrail_bot/testrail_bot/settings.py
@@ -0,0 +1,146 @@
+"""
+Django settings for testrail_bot project.
+
+Generated by 'django-admin startproject' using Django 3.0.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.0/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ.get("SECRET_KEY", default="123")
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = int(os.environ.get("DEBUG", default=0))
+
+ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", default="*").split(" ")
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'control.apps.ControlConfig',
+ 'bootstrap3',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'testrail_bot.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'testrail_bot.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
+ "NAME": os.environ.get("SQL_DATABASE", os.path.join(BASE_DIR, "db.sqlite3")),
+ "USER": os.environ.get("SQL_USER", "user"),
+ "PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
+ "HOST": os.environ.get("SQL_HOST", "localhost"),
+ "PORT": os.environ.get("SQL_PORT", "5432"),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.0/howto/static-files/
+
+STATIC_URL = '/staticfiles/'
+STATIC_ROOT = '/staticfiles'
+
+STATICFILES_DIRS = (
+ os.path.join(BASE_DIR, "media/images"),
+)
+
+MEDIA_URL = "/mediafiles/"
+MEDIA_ROOT = "/mediafiles"
+
+
+# Celery configs
+CELERY_BROKER_URL = 'redis://redis:6379'
+CELERY_RESULT_BACKEND = 'redis://redis:6379'
+CELERY_ACCEPT_CONTENT = ['application/json']
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_RESULT_SERIALIZER = 'json'
+
+# TestRail configs
+TESTRAIL_EMAIL = os.environ.get("TESTRAIL_EMAIL", default="123")
+TESTRAIL_PASSWORD = os.environ.get("TESTRAIL_PASSWORD", default="123")
diff --git a/testrail_bot/testrail_bot/urls.py b/testrail_bot/testrail_bot/urls.py
new file mode 100644
index 0000000..d8b6e77
--- /dev/null
+++ b/testrail_bot/testrail_bot/urls.py
@@ -0,0 +1,23 @@
+"""testrail_bot URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import include, path
+
+
+urlpatterns = [
+ path("", include("control.urls")),
+ path('admin/', admin.site.urls),
+]
diff --git a/testrail_bot/testrail_bot/wsgi.py b/testrail_bot/testrail_bot/wsgi.py
new file mode 100644
index 0000000..5e7bafa
--- /dev/null
+++ b/testrail_bot/testrail_bot/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for testrail_bot project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testrail_bot.settings')
+
+application = get_wsgi_application()