Compare test suites results feature PRODX-36767

Update Django to 4.2 version

Change-Id: Ieed17cfac12518262503043ccee03473a3023221
diff --git a/testrail_bot/control/celery_tasks/tasks.py b/testrail_bot/control/celery_tasks/tasks.py
index 71b6783..6450c68 100644
--- a/testrail_bot/control/celery_tasks/tasks.py
+++ b/testrail_bot/control/celery_tasks/tasks.py
@@ -24,3 +24,17 @@
 @shared_task
 def update_plot_data():
     jenkins_pipeline.update_plot()
+
+
+@shared_task
+def get_test_passability_in_suite(diff_id: int, report_id: int):
+    try:
+        testrail_pipeline.get_test_passrate_in_suite(diff_id, report_id)
+    except BaseException as e:
+        print(f"Caught next exception: {e}")
+        traceback.print_exc()
+        from .. import models
+        r = models.SuitePassRate.objects.get(pk=report_id)
+        r.status = "Unexpected fail"
+        r.finished = True
+        r.save()
diff --git a/testrail_bot/control/celery_tasks/test_rail_api.py b/testrail_bot/control/celery_tasks/test_rail_api.py
index e5932bc..d77b511 100644
--- a/testrail_bot/control/celery_tasks/test_rail_api.py
+++ b/testrail_bot/control/celery_tasks/test_rail_api.py
@@ -66,7 +66,7 @@
                                 run_name: str = None,
                                 created_after: str = None,
                                 created_before: str = None,
-                                created_by: int = None) -> \
+                                created_by: int = None, **kwargs) -> \
         Iterator[List[dict]]:
     limit_step = 100
     suite_id = api.cases.get_case(case_id=case_id)["suite_id"]
diff --git a/testrail_bot/control/celery_tasks/testrail_pipeline.py b/testrail_bot/control/celery_tasks/testrail_pipeline.py
index 175fe74..f1e4bf0 100644
--- a/testrail_bot/control/celery_tasks/testrail_pipeline.py
+++ b/testrail_bot/control/celery_tasks/testrail_pipeline.py
@@ -1,8 +1,11 @@
+import datetime
 import difflib
-from typing import TextIO, List, Tuple, Optional, Iterator
+import json
+from typing import TextIO, List, Tuple, Optional, Iterator, Dict
 
 from datetime import datetime as dt
 from datetime import timedelta
+from itertools import islice
 from . import filters
 from .enums import StatusEnum
 from . import test_rail_api
@@ -242,7 +245,7 @@
                     "{defect}</a>".format(defect=sim_result["defects"])
         test_link = "<a href=https://mirantis.testrail.com/index.php?/tests/" \
                     "view/{test_id}>{test_id}</a>".format(
-                        test_id=sim_result["test_id"])
+            test_id=sim_result["test_id"])
         status_id = int(sim_result['status_id'])
         if status_id in [StatusEnum.retest, StatusEnum.failed,
                          StatusEnum.blocked]:
@@ -372,3 +375,99 @@
         f.write("Test processing finished")
         f.flush()
         finish_report(report)
+
+
+def get_cases(project_id: int, suite_id: int,
+              limit: int = 250,
+              max_limit: int = 1000,
+              filter: str = None,
+              created_after: int = None,
+              created_before: int = None,
+              ) -> Iterator[Dict]:
+    for offset in range(0, max_limit, limit):
+        cases = test_rail_api.api.cases.get_cases(
+            project_id=project_id,
+            suite_id=suite_id,
+            limit=limit,
+            offset=offset,
+            created_after=created_after,
+            created_before=created_before,
+            filter=filter).get("cases")
+
+        if not cases:
+            return
+        for case in cases:
+            yield case
+
+
+def get_test_passrate_in_suite(diff_id: int, report_id: int) -> Dict:
+    """
+    Returns a percentage of passed tests for each test in the suite
+
+    :param diff_id: ID of models.DiffOfSuitesPassRates object in the DB to
+    retrieve settings for comparing test suites
+    :param report_id: ID of models.SuitePassRate object which contains a
+    results of fetching passrates for specific test suite
+
+    :return: dict with
+    {"test_title": {
+        "rate": 40,
+        "case_id": 12345
+        },
+    "test_title2": {
+        "rate": 80,
+        "case_id": 12346
+        }
+    }
+    """
+
+    diff_obj: models.DiffOfSuitesPassRates = \
+        models.DiffOfSuitesPassRates.objects.get(pk=diff_id)
+    report: models.SuitePassRate = models.SuitePassRate.objects.get(
+        pk=report_id)
+    suite_id = report.suite_id
+
+    project_id = test_rail_api.get_project_id("Mirantis Cloud Platform")
+    report.suite_name = test_rail_api.get_suite_by_id(suite_id)["name"]
+
+    end_lookup_date = datetime.datetime.now()
+    start_lookup_date = end_lookup_date + timedelta(days=-15)
+    _filters = {
+        "created_by": 109,
+        "plan_name": "[MCP2.0]OSCORE",
+        "created_before": int(dt.timestamp(end_lookup_date)),
+        "created_after": int(dt.timestamp(start_lookup_date)),
+    }
+
+    passrate_by_cases = dict()
+    params = dict(project_id=project_id,
+                  suite_id=suite_id,
+                  filter=diff_obj.test_keyword,
+                  limit=200)
+    for _n, case in enumerate(get_cases(**params), start=1):
+        case_title = case['title']
+        case_id = case["id"]
+        report.status = f"Current case: {_n}"
+        report.save()
+        # Limit generator to the list with  the length defined in the
+        # DiffOfSuitesPassRates
+        last_case_results = list(islice(
+            test_rail_api.get_result_history_for_case(case_id, **_filters),
+            diff_obj.limit))
+
+        passrate_by_cases[case_title] = dict()
+        passrate_by_cases[case_title]['case_id'] = case_id
+        if last_case_results:
+            passed_tests = [x for x in last_case_results
+                            if x[-1]["status_id"] == StatusEnum.passed]
+            passrate_by_cases[case_title]['rate'] = \
+                len(passed_tests) * 100 / diff_obj.limit
+        else:
+            passrate_by_cases[case_title]['rate'] = "No result found"
+        report.passrate_by_tests = json.dumps(passrate_by_cases,
+                                              indent=4)
+        report.status = f"Current case: {_n}"
+        report.save()
+    report.finished = True
+    report.save()
+    return passrate_by_cases
diff --git a/testrail_bot/control/forms.py b/testrail_bot/control/forms.py
index 2bdffe3..a7f28bb 100644
--- a/testrail_bot/control/forms.py
+++ b/testrail_bot/control/forms.py
@@ -1,6 +1,6 @@
 from datetime import date
 from django import forms
-from .models import TestRailTestRun
+from .models import TestRailTestRun, DiffOfSuitesPassRates, SuitePassRate
 
 
 class TestRunForm(forms.ModelForm):
@@ -29,3 +29,22 @@
             "filter_func": "Leave blank if not used",
         }
 
+
+class DiffPassRatesForm(forms.ModelForm):
+    class Meta:
+        model = DiffOfSuitesPassRates
+        fields = ["limit", "test_keyword"]
+        labels = {
+            "test_keyword": "Pattern to search by tests",
+            "limit": "Count of tests to define the passrate. Don't recommend "
+                     "to use a number greater that 10"
+        }
+
+
+class SuitePassRateForm(forms.ModelForm):
+    class Meta:
+        model = SuitePassRate
+        fields = ["suite_id"]
+        labels = {
+            "suite_id": "Suite ID"
+        }
diff --git a/testrail_bot/control/migrations/0003_suitepassrate_diffofsuitespassrates.py b/testrail_bot/control/migrations/0003_suitepassrate_diffofsuitespassrates.py
new file mode 100644
index 0000000..6215a62
--- /dev/null
+++ b/testrail_bot/control/migrations/0003_suitepassrate_diffofsuitespassrates.py
@@ -0,0 +1,36 @@
+# Generated by Django 4.2.7 on 2023-11-20 17:46
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('control', '0002_testrailtestrun_checked_tests'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SuitePassRate',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('suite_id', models.CharField(choices=[('10651', '[MCP2.0_ROCKY]Tempest'), ('10635', '[MCP2.0_STEIN]Tempest'), ('10653', '[MCP2.0_TRAIN]Tempest'), ('10710', '[MCP2.0_USSURI]Tempest'), ('10888', '[MCP2.0_VICTORIA]Tempest'), ('11167', '[MCP2.0_WALLABY]Tempest'), ('11188', '[MCP2.0_XENA]Tempest'), ('11170', '[MCP2.0_YOGA]Tempest'), ('11192', '[MCP2.0_ANTELOPE]Tempest'), ('11193', '[MCP2.0_ANTELOPE]Stepler'), ('10886', '[MCP2.0_USSURI]Stepler'), ('10887', '[MCP2.0_VICTORIA]Stepler'), ('11171', '[MCP2.0_YOGA]Stepler')], max_length=20)),
+                ('suite_name', models.CharField(blank=True, max_length=100)),
+                ('passrate_by_tests', models.JSONField(blank=True, default='{}')),
+                ('status', models.TextField(blank=True, max_length=300)),
+                ('finished', models.BooleanField(blank=True, default=False)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DiffOfSuitesPassRates',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('started_at', models.DateTimeField(auto_created=True, auto_now=True)),
+                ('limit', models.IntegerField(blank=True, default=10)),
+                ('test_keyword', models.CharField(blank=True, default='', max_length=300)),
+                ('report1', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='report1', to='control.suitepassrate')),
+                ('report2', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='report2', to='control.suitepassrate')),
+            ],
+        ),
+    ]
diff --git a/testrail_bot/control/models.py b/testrail_bot/control/models.py
index f0e014f..0956b1a 100644
--- a/testrail_bot/control/models.py
+++ b/testrail_bot/control/models.py
@@ -78,3 +78,42 @@
 class ActionLog(models.Model):
     name = models.CharField(max_length=500)
     date = models.DateTimeField(null=True)
+
+
+class SuitePassRate(models.Model):
+    SUITE_CHOICES = [
+        ("10651", "[MCP2.0_ROCKY]Tempest"),
+        ("10635", "[MCP2.0_STEIN]Tempest"),
+        ("10653", "[MCP2.0_TRAIN]Tempest"),
+        ("10710", "[MCP2.0_USSURI]Tempest"),
+        ("10888", "[MCP2.0_VICTORIA]Tempest"),
+        ("11167", "[MCP2.0_WALLABY]Tempest"),
+        ("11188", "[MCP2.0_XENA]Tempest"),
+        ("11170", "[MCP2.0_YOGA]Tempest"),
+        ("11192", "[MCP2.0_ANTELOPE]Tempest"),
+
+        ("11193", "[MCP2.0_ANTELOPE]Stepler"),
+        ("10886", "[MCP2.0_USSURI]Stepler"),
+        ("10887", "[MCP2.0_VICTORIA]Stepler"),
+        ("11171", "[MCP2.0_YOGA]Stepler"),
+    ]
+    suite_id = models.CharField(max_length=20, choices=SUITE_CHOICES)
+    suite_name = models.CharField(max_length=100, blank=True)
+    passrate_by_tests = models.JSONField(default="{}", blank=True)
+    status = models.TextField(max_length=300, blank=True)
+    finished = models.BooleanField(default=False, blank=True)
+
+
+class DiffOfSuitesPassRates(models.Model):
+    limit = models.IntegerField(default=10, blank=True)
+    test_keyword = models.CharField(default="", max_length=300, blank=True)
+    report1 = models.ForeignKey(to=SuitePassRate,
+                                related_name="report1",
+                                on_delete=models.CASCADE,
+                                blank=True)
+    report2 = models.ForeignKey(to=SuitePassRate,
+                                related_name="report2",
+                                on_delete=models.CASCADE,
+                                blank=True)
+    started_at = models.DateTimeField(auto_created=True,
+                                      auto_now=True)
diff --git a/testrail_bot/control/template_tags/custom_tags.py b/testrail_bot/control/template_tags/custom_tags.py
new file mode 100644
index 0000000..92e203a
--- /dev/null
+++ b/testrail_bot/control/template_tags/custom_tags.py
@@ -0,0 +1,10 @@
+from django.template.defaulttags import register
+
+
+@register.filter
+def is_numberic(value):
+    try:
+        float(value)
+        return True
+    except ValueError:
+        return False
diff --git a/testrail_bot/control/templates/base.html b/testrail_bot/control/templates/base.html
index d2dd9a3..4bd5e45 100644
--- a/testrail_bot/control/templates/base.html
+++ b/testrail_bot/control/templates/base.html
@@ -25,6 +25,8 @@
             <li class="nav-item">
                 <a class="nav-link" href="{% url 'list_reports' %}">Reports</a></li>
             <li class="nav-item">
+                <a class="nav-link" href="{% url 'list_of_comparing_reports' %}">Compare Suites</a></li>
+            <li class="nav-item">
                 <a class="nav-link" href="{% url 'help' %}">Help</a></li>
             <li class="nav-item">
                 <a class="nav-link" href="{% url 'jenkins_plot' %}">Jenkins
diff --git a/testrail_bot/control/templates/control/compare_suites.html b/testrail_bot/control/templates/control/compare_suites.html
new file mode 100644
index 0000000..f5905fa
--- /dev/null
+++ b/testrail_bot/control/templates/control/compare_suites.html
@@ -0,0 +1,100 @@
+{% extends "base.html" %}
+{% load bootstrap5 %}
+{% load custom_tags %}
+{% block section %}
+
+{% if is_finished != None %}
+    {% if is_finished %}
+        <i>Generating completed</i>
+    {% else %}
+        <meta http-equiv="refresh" content="10">
+        <i>Generating in progress...</i>
+    {% endif %}
+{% endif %}
+
+<div class="p-2 border">
+  <h3>Compare Suites</h3>
+  <div class="input-group inline">
+    {% bootstrap_form_errors diff_form type='non_fields' %}
+    {% bootstrap_form_errors report1_form type='non_fields' %}
+    {% bootstrap_form_errors report2_form type='non_fields' %}
+    <form action="{% url 'submit_suites' %}" method="post" class="form">
+    {% csrf_token %}
+      <div class="col-xs-8 d-inline-flex">
+        {% bootstrap_field report1_form.suite_id size='sm' %}</div>
+      <div class="col-xs-8 d-inline-flex">
+        {% bootstrap_field report2_form.suite_id size='sm' %}</div>
+    <div class="col-xs-8">
+        {% bootstrap_field diff_form.test_keyword  size='sm' %}</div>
+    <div class="col-xs-8">
+        {% bootstrap_field diff_form.limit  size='sm' %}</div>
+
+    <input type="submit" class="btn btn-primary m-1"
+           value="Compare results" />
+    </form>
+  </div>
+
+  {% if is_finished or diff_table %}
+    Total: {{ diff_table | length  }}
+    <table id="diff_table" class="p-4 table table-sm border">
+      <tbody>
+      <tr>
+          <th></th>
+          <th>{% if report1.finished %} Completed
+              {% else %} in progress...{% endif %}
+          </th>
+          <th>
+              {% if report2.finished %} Completed
+              {% else %} in progress...{% endif %}
+          </th>
+      </tr>
+      <tr>
+          <th></th>
+          <th>{{ report1.suite_name}}
+          </th>
+          <th>
+              {{ report2.suite_name}}
+          </th>
+      </tr>
+      <tr>
+          <th></th>
+          <th>
+              {{ report1.status}}
+          </th>
+          <th>
+              {{ report2.status}}
+          </th>
+      </tr>
+      {% for test_name, probabilities in diff_table.items %}
+      <tr>
+        <th scope="row">{{ test_name }}</th>
+            {% for prob in probabilities %}
+                {% if prob %}
+                  <td>
+                <a href="https://mirantis.testrail.com/index.php?/cases/results/{{ prob.case_id }}"
+                class="text-decoration-none
+                  {% if prob.rate|is_numberic %}
+                    {% if prob.rate > 90 %}
+                      badge bg-success
+                    {% elif prob.rate > 10 %}
+                      badge bg-warning
+                    {% else %}
+                      badge bg-danger
+                    {% endif %}
+                  {% endif%}
+">
+                   {{ prob.rate }}</a>
+                  </td>
+                {% else %}
+                    <td> Not found </td>
+                {% endif %}
+            {% endfor %}
+      </tr>
+      {% endfor %}
+      </tbody>
+    </table>
+  {% endif %}
+<!--    for test purposes only -->
+<!--    <pre class="text-wrap"> {{data | safe}}</pre>-->
+</div>
+{% endblock %}
diff --git a/testrail_bot/control/templates/control/list_comparing_suites.html b/testrail_bot/control/templates/control/list_comparing_suites.html
new file mode 100644
index 0000000..8d90718
--- /dev/null
+++ b/testrail_bot/control/templates/control/list_comparing_suites.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+{% load bootstrap5 %}
+{% block section %}
+    <div class="p-2 border">
+        <input type="button"
+               onclick="location.href='{% url 'compare_suites' %}'"
+               class="btn btn-primary m-1"
+               value="Start new comparing" />
+
+        {% for report in reports %}
+        <a href="{% url 'report_comparing_suites' report.id  %}"
+            class="list-group-item
+                {% if report.report1.finished and report.report2.finished  %}
+                    list-group-item-success
+                {% else %}
+                    list-group-item-warning
+                {% endif %}
+           ">
+            <b>{{report.report1.suite_name}}</b> vs.
+            <b>{{report.report2.suite_name}}</b>
+
+            {% if report.test_keyword %}
+              [{{ report.test_keyword }}]
+            {% endif %}
+            started at {{ report.started_at |date:'Y-m-d H:i:s' }}
+            </a>
+        {% endfor %}
+    </div>
+{% endblock %}
diff --git a/testrail_bot/control/urls.py b/testrail_bot/control/urls.py
index fd8bebc..b5292f3 100644
--- a/testrail_bot/control/urls.py
+++ b/testrail_bot/control/urls.py
@@ -12,6 +12,15 @@
     path("reports/<int:report_id>/", views.single_report, name="single_report"),
     path('index/', views.index, name='index'),
     path("help/", views.show_help, name="help"),
-    path("update_jenkins_plot", views.update_jenkins_plot, name="update_jenkins"),
-    path("jenkins_plot", views.jenkins_plot, name="jenkins_plot")
+    path("update_jenkins_plot",
+         views.update_jenkins_plot,
+         name="update_jenkins"),
+    path("jenkins_plot", views.jenkins_plot, name="jenkins_plot"),
+    path("compare_suites/new/", views.compare_suites, name="compare_suites"),
+    path("compare_suites/",
+         views.list_of_comparing_reports,
+         name="list_of_comparing_reports"),
+    path("compare_suites/submit/", views.submit_suites, name="submit_suites"),
+    path("compare_suites/<int:report_id>/", views.report_comparing_suites,
+         name="report_comparing_suites"),
 ]
diff --git a/testrail_bot/control/utils.py b/testrail_bot/control/utils.py
new file mode 100644
index 0000000..2dabafe
--- /dev/null
+++ b/testrail_bot/control/utils.py
@@ -0,0 +1,57 @@
+from parse import parse
+from typing import Dict, List
+
+
+def parse_title(test_name):
+    # Sometimes id can be without the closing ] symbol
+    if "[" in test_name and "]" not in test_name:
+        test_name += "]"
+    token_count = test_name.split(".").__len__()
+
+    if test_name.startswith("=="):
+        return test_name
+
+    if test_name.startswith(".setUp") or test_name.startswith(".tearDown"):
+        fmt = "{test_title}(" + "{}." * (token_count - 2) + "{class_name})"
+        r = parse(fmt, test_name)
+        return f"{r['class_name']}.{r['test_title']}".strip()
+    try:
+        fmt = "{}." * (token_count - 2) + "{class_name}.{test_title}[{id}]"
+        r = parse(fmt, test_name)
+        return f"{r['test_title']}[{r['id']}]"
+    except TypeError:
+        # return file_name.test_class.test_name in other complicated cases
+        return '.'.join(test_name.split(".")[:3])
+
+
+def short_names_for_dict(_dict):
+    __dict = {}
+    for _k in _dict.keys():
+        __k = parse_title(_k)
+        # Replace only those keys which are absent in the dict or empty
+        # (defined as "No result found")
+        if __dict.get(__k) == "No result found" or not __dict.get(__k):
+            __dict[__k] = _dict[_k]
+    return __dict
+
+
+def get_dict_diff(dict1: dict,
+                  dict2: dict,
+                  compare_by_key=None) -> Dict[str, List]:
+    all_keys = sorted(set(list(dict1.keys()) + list(dict2.keys())))
+
+    result = dict()
+    for k in all_keys:
+        if compare_by_key:
+            if dict1.get(k, {}).get(compare_by_key) == dict2.get(k, {}).get(
+                    compare_by_key):
+                continue
+        else:
+            if dict1.get(k) == dict2.get(k):
+                continue
+        result[k] = [dict1.get(k), dict2.get(k)]
+    return result
+
+
+if __name__ == "__main__":
+    pass
diff --git a/testrail_bot/control/views.py b/testrail_bot/control/views.py
index c9fe8f3..f7b0a9d 100644
--- a/testrail_bot/control/views.py
+++ b/testrail_bot/control/views.py
@@ -7,7 +7,9 @@
 
 from . import models
 from . import forms
-from .celery_tasks.tasks import process_run, update_plot_data
+from .celery_tasks.tasks import process_run, update_plot_data, \
+    get_test_passability_in_suite
+from .utils import short_names_for_dict, get_dict_diff
 
 
 def index(request):
@@ -55,7 +57,8 @@
 def single_report(request, report_id):
     report = models.TestRailReport.objects.get(pk=report_id)
     data = report.path.read().decode("utf-8")
-    if request.method == "POST" and request.is_ajax():
+    if request.method == "POST" \
+            and request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
         return HttpResponse(
             json.dumps({"data": data, "finished": report.finished}),
             content_type="application/json")
@@ -141,3 +144,80 @@
         request, "control/jenkins_plot.html",
         {"update_date": update_date, "update_started": update_started,
          "job_names": enumerate(job_names, 1)})
+
+
+def submit_suites(request):
+    form = forms.DiffPassRatesForm(request.POST)
+    if not form.is_valid():
+        print(f"{form.errors=}")
+        return
+    report1 = models.SuitePassRate(suite_id=request.POST["first-suite_id"])
+    report1.save()
+    report2 = models.SuitePassRate(suite_id=request.POST["second-suite_id"])
+    report2.save()
+
+    diff_model = models.DiffOfSuitesPassRates(
+        report1=report1,
+        report2=report2,
+        limit=form.cleaned_data["limit"],
+        test_keyword=form.cleaned_data["test_keyword"]
+    )
+    diff_model.save()
+    get_test_passability_in_suite.delay(diff_model.id, report1.id)
+    get_test_passability_in_suite.delay(diff_model.id, report2.id)
+
+    return redirect("report_comparing_suites", diff_model.id)
+
+
+def compare_suites(request):
+    if request.method == "POST":
+        return submit_suites(request)
+
+    diff_form = forms.DiffPassRatesForm()
+    report1_form = forms.SuitePassRateForm(prefix='first')
+    report2_form = forms.SuitePassRateForm(prefix='second')
+
+    return render(
+        request, "control/compare_suites.html",
+        {
+         "diff_form": diff_form,
+         "report1_form": report1_form,
+         "report2_form": report2_form,
+         "finished": None
+        })
+
+
+def list_of_comparing_reports(request):
+    list_of_reports = models.DiffOfSuitesPassRates.objects.all()
+    return render(
+        request, "control/list_comparing_suites.html",
+        {
+            "reports": list_of_reports
+        })
+
+
+def report_comparing_suites(request, report_id):
+    report = models.DiffOfSuitesPassRates.objects.get(pk=report_id)
+    passrate1 = short_names_for_dict(json.loads(
+        report.report1.passrate_by_tests))
+    passrate2 = short_names_for_dict(json.loads(
+        report.report2.passrate_by_tests))
+
+    diff_table = get_dict_diff(dict1=passrate1,
+                               dict2=passrate2,
+                               compare_by_key="rate")
+    diff_form = forms.DiffPassRatesForm(instance=report)
+    report1_form = forms.SuitePassRateForm(instance=report.report1,
+                                           prefix="first")
+    report2_form = forms.SuitePassRateForm(instance=report.report2,
+                                           prefix="second")
+
+    return render(
+        request, "control/compare_suites.html",
+        {"diff_form": diff_form,
+         "report1_form": report1_form,
+         "report2_form": report2_form,
+         "report1": report.report1,
+         "report2": report.report2,
+         "is_finished": report.report1.finished and report.report2.finished,
+         "diff_table": diff_table})
diff --git a/testrail_bot/requirements.txt b/testrail_bot/requirements.txt
index 41fe5c8..d0c566a 100644
--- a/testrail_bot/requirements.txt
+++ b/testrail_bot/requirements.txt
@@ -1,5 +1,5 @@
 celery==4.4.6
-Django==3.0.7
+Django==4.2.7
 django-bootstrap-v5==1.0.11
 psycopg2-binary==2.8.5
 redis==3.5.3
@@ -8,4 +8,6 @@
 python-jenkins==1.7.0
 matplotlib==3.3.2
 retry==0.9.2
-
+psycopg2==2.9.9
+parse==1.19.1
+rpdb==0.1.6
diff --git a/testrail_bot/testrail_bot/settings.py b/testrail_bot/testrail_bot/settings.py
index a40279d..0dfc606 100644
--- a/testrail_bot/testrail_bot/settings.py
+++ b/testrail_bot/testrail_bot/settings.py
@@ -65,6 +65,9 @@
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
             ],
+            'libraries': {
+                'custom_tags': 'control.template_tags.custom_tags'
+                }
         },
     },
 ]