From 6af4a2e0ffb5f5eae2c6da9dfd3ae9bcdb00c488 Mon Sep 17 00:00:00 2001 From: Achmad Fienan Rahardianto Date: Tue, 10 Feb 2026 10:07:35 +0700 Subject: [PATCH 1/7] Add markdown support for notes field in TestCase and TestRun - Add SimpleMDENotes widget with unique file upload ID to prevent conflicts when both text and notes editors are on the same page - Use SimpleMDENotes widget in TestCase and TestRun forms - Render notes with markdown2html filter in display templates - Render notes with markdown2HTML() in testplan JS expand area - Fix SimpleMDE autosave ID to use textarea id for uniqueness --- tcms/core/widgets.py | 114 +- tcms/static/js/index.js | 200 +-- tcms/testcases/forms.py | 266 +-- tcms/testcases/templates/testcases/get.html | 538 +++--- .../templates/testcases/mutable.html | 543 +++--- tcms/testplans/static/testplans/js/get.js | 1460 ++++++++--------- tcms/testruns/forms.py | 195 +-- tcms/testruns/templates/testruns/mutable.html | 524 +++--- 8 files changed, 1954 insertions(+), 1886 deletions(-) diff --git a/tcms/core/widgets.py b/tcms/core/widgets.py index 1a2a88dc29..ac0d4c2837 100644 --- a/tcms/core/widgets.py +++ b/tcms/core/widgets.py @@ -1,53 +1,61 @@ -# Copyright (c) 2018-2024 Kiwi TCMS project. All rights reserved. -# Author: Alexander Todorov - -""" -Custom widgets for Django -""" -from django import forms -from django.utils.dateparse import parse_duration -from django.utils.safestring import SafeString - - -class SimpleMDE(forms.Textarea): - """ - SimpleMDE widget for Django - """ - - file_upload_id = "simplemde-file-upload" - - def render(self, name, value, attrs=None, renderer=None): - # pylint: disable=objects-update-used - attrs.update( - { - "class": "js-simplemde-textarea", - "data-file-upload-id": self.file_upload_id, - } - ) - rendered_string = super().render(name, value, attrs, renderer) - rendered_string += SafeString( # nosec:B703:django_mark_safe - f""" - -""" - ) - return rendered_string - - class Media: - css = {"all": ["simplemde/dist/simplemde.min.css"]} - - -class DurationWidget(forms.Widget): - template_name = "widgets/duration.html" - - def format_value(self, value): - if not value: - return 0 - - duration = parse_duration(value) - return int(duration.total_seconds()) - - class Media: - css = {"all": ["bootstrap-duration-picker/dist/bootstrap-duration-picker.css"]} - js = [ - "bootstrap-duration-picker/dist/bootstrap-duration-picker.js", - ] +# Copyright (c) 2018-2024 Kiwi TCMS project. All rights reserved. +# Author: Alexander Todorov + +""" +Custom widgets for Django +""" +from django import forms +from django.utils.dateparse import parse_duration +from django.utils.safestring import SafeString + + +class SimpleMDE(forms.Textarea): + """ + SimpleMDE widget for Django + """ + + file_upload_id = "simplemde-file-upload" + + def render(self, name, value, attrs=None, renderer=None): + # pylint: disable=objects-update-used + attrs.update( + { + "class": "js-simplemde-textarea", + "data-file-upload-id": self.file_upload_id, + } + ) + rendered_string = super().render(name, value, attrs, renderer) + rendered_string += SafeString( # nosec:B703:django_mark_safe + f""" + +""" + ) + return rendered_string + + class Media: + css = {"all": ["simplemde/dist/simplemde.min.css"]} + + +class SimpleMDENotes(SimpleMDE): + """ + SimpleMDE widget for notes field with unique file upload ID + """ + + file_upload_id = "simplemde-notes-file-upload" + + +class DurationWidget(forms.Widget): + template_name = "widgets/duration.html" + + def format_value(self, value): + if not value: + return 0 + + duration = parse_duration(value) + return int(duration.total_seconds()) + + class Media: + css = {"all": ["bootstrap-duration-picker/dist/bootstrap-duration-picker.css"]} + js = [ + "bootstrap-duration-picker/dist/bootstrap-duration-picker.js", + ] diff --git a/tcms/static/js/index.js b/tcms/static/js/index.js index 4027f565d5..c713b5271b 100644 --- a/tcms/static/js/index.js +++ b/tcms/static/js/index.js @@ -1,98 +1,102 @@ -import { pageBugsGetReadyHandler } from '../../bugs/static/bugs/js/get' -import { pageBugsMutableReadyHandler } from '../../bugs/static/bugs/js/mutable' -import { pageBugsSearchReadyHandler } from '../../bugs/static/bugs/js/search' - -import { pageTestcasesGetReadyHandler } from '../../testcases/static/testcases/js/get' -import { pageTestcasesMutableReadyHandler } from '../../testcases/static/testcases/js/mutable' -import { pageTestcasesSearchReadyHandler } from '../../testcases/static/testcases/js/search' - -import { pageTestplansGetReadyHandler } from '../../testplans/static/testplans/js/get' -import { pageTestplansMutableReadyHandler } from '../../testplans/static/testplans/js/mutable' -import { pageTestplansSearchReadyHandler } from '../../testplans/static/testplans/js/search' - -import { pageTestrunsEnvironmentReadyHandler } from '../../testruns/static/testruns/js/environment' -import { pageTestrunsGetReadyHandler } from '../../testruns/static/testruns/js/get' -import { pageTestrunsMutableReadyHandler } from '../../testruns/static/testruns/js/mutable' -import { pageTestrunsSearchReadyHandler } from '../../testruns/static/testruns/js/search' - -import { pageManagementBuildAdminReadyHandler } from '../../management/static/management/js/build_admin' - -import { pageTelemetryReadyHandler } from '../../telemetry/static/telemetry/js/index' - -import { jsonRPC } from './jsonrpc' -import { initSimpleMDE } from './simplemde_security_override' - -function pageInitDBReadyHandler () { - $('.js-initialize-btn').click(function () { - $(this).button('loading') - }) -} - -const pageHandlers = { - 'page-bugs-get': pageBugsGetReadyHandler, - 'page-bugs-mutable': pageBugsMutableReadyHandler, - 'page-bugs-search': pageBugsSearchReadyHandler, - - 'page-init-db': pageInitDBReadyHandler, - - 'page-testcases-get': pageTestcasesGetReadyHandler, - 'page-testcases-mutable': pageTestcasesMutableReadyHandler, - 'page-testcases-search': pageTestcasesSearchReadyHandler, - - 'page-testplans-get': pageTestplansGetReadyHandler, - 'page-testplans-mutable': pageTestplansMutableReadyHandler, - 'page-testplans-search': pageTestplansSearchReadyHandler, - - 'page-testruns-environment': pageTestrunsEnvironmentReadyHandler, - 'page-testruns-get': pageTestrunsGetReadyHandler, - 'page-testruns-mutable': pageTestrunsMutableReadyHandler, - 'page-testruns-search': pageTestrunsSearchReadyHandler, - - 'page-telemetry-testing-breakdown': pageTelemetryReadyHandler, - 'page-telemetry-status-matrix': pageTelemetryReadyHandler, - 'page-telemetry-execution-dashboard': pageTelemetryReadyHandler, - 'page-telemetry-execution-trends': pageTelemetryReadyHandler, - 'page-telemetry-test-case-health': pageTelemetryReadyHandler -} - -$(() => { - const body = $('body') - const pageId = body.attr('id') - const readyFunc = pageHandlers[pageId] - if (readyFunc) { - readyFunc(pageId) - } - - // this page doesn't have a page id - if (body.hasClass('grp-change-form') && body.hasClass('management-build')) { - pageManagementBuildAdminReadyHandler() - } - - if ($('body').selectpicker) { - $('.selectpicker').selectpicker() - } - - if ($('body').bootstrapSwitch) { - $('.bootstrap-switch').bootstrapSwitch() - } - - if ($('body').tooltip) { - $('[data-toggle="tooltip"]').tooltip() - } - - $('.js-simplemde-textarea').each(function () { - const fileUploadId = $(this).data('file-upload-id') - const uploadField = $(`#${fileUploadId}`) - - // this value is only used in testcases/js/mutable.js - window.markdownEditor = initSimpleMDE(this, uploadField) - }) - - $('#logout_link').click(function () { - $('#logout_form').submit() - return false - }) - - // for debugging in browser - window.jsonRPC = jsonRPC -}) +import { pageBugsGetReadyHandler } from '../../bugs/static/bugs/js/get' +import { pageBugsMutableReadyHandler } from '../../bugs/static/bugs/js/mutable' +import { pageBugsSearchReadyHandler } from '../../bugs/static/bugs/js/search' + +import { pageTestcasesGetReadyHandler } from '../../testcases/static/testcases/js/get' +import { pageTestcasesMutableReadyHandler } from '../../testcases/static/testcases/js/mutable' +import { pageTestcasesSearchReadyHandler } from '../../testcases/static/testcases/js/search' + +import { pageTestplansGetReadyHandler } from '../../testplans/static/testplans/js/get' +import { pageTestplansMutableReadyHandler } from '../../testplans/static/testplans/js/mutable' +import { pageTestplansSearchReadyHandler } from '../../testplans/static/testplans/js/search' + +import { pageTestrunsEnvironmentReadyHandler } from '../../testruns/static/testruns/js/environment' +import { pageTestrunsGetReadyHandler } from '../../testruns/static/testruns/js/get' +import { pageTestrunsMutableReadyHandler } from '../../testruns/static/testruns/js/mutable' +import { pageTestrunsSearchReadyHandler } from '../../testruns/static/testruns/js/search' + +import { pageManagementBuildAdminReadyHandler } from '../../management/static/management/js/build_admin' + +import { pageTelemetryReadyHandler } from '../../telemetry/static/telemetry/js/index' + +import { jsonRPC } from './jsonrpc' +import { initSimpleMDE } from './simplemde_security_override' + +function pageInitDBReadyHandler () { + $('.js-initialize-btn').click(function () { + $(this).button('loading') + }) +} + +const pageHandlers = { + 'page-bugs-get': pageBugsGetReadyHandler, + 'page-bugs-mutable': pageBugsMutableReadyHandler, + 'page-bugs-search': pageBugsSearchReadyHandler, + + 'page-init-db': pageInitDBReadyHandler, + + 'page-testcases-get': pageTestcasesGetReadyHandler, + 'page-testcases-mutable': pageTestcasesMutableReadyHandler, + 'page-testcases-search': pageTestcasesSearchReadyHandler, + + 'page-testplans-get': pageTestplansGetReadyHandler, + 'page-testplans-mutable': pageTestplansMutableReadyHandler, + 'page-testplans-search': pageTestplansSearchReadyHandler, + + 'page-testruns-environment': pageTestrunsEnvironmentReadyHandler, + 'page-testruns-get': pageTestrunsGetReadyHandler, + 'page-testruns-mutable': pageTestrunsMutableReadyHandler, + 'page-testruns-search': pageTestrunsSearchReadyHandler, + + 'page-telemetry-testing-breakdown': pageTelemetryReadyHandler, + 'page-telemetry-status-matrix': pageTelemetryReadyHandler, + 'page-telemetry-execution-dashboard': pageTelemetryReadyHandler, + 'page-telemetry-execution-trends': pageTelemetryReadyHandler, + 'page-telemetry-test-case-health': pageTelemetryReadyHandler +} + +$(() => { + const body = $('body') + const pageId = body.attr('id') + const readyFunc = pageHandlers[pageId] + if (readyFunc) { + readyFunc(pageId) + } + + // this page doesn't have a page id + if (body.hasClass('grp-change-form') && body.hasClass('management-build')) { + pageManagementBuildAdminReadyHandler() + } + + if ($('body').selectpicker) { + $('.selectpicker').selectpicker() + } + + if ($('body').bootstrapSwitch) { + $('.bootstrap-switch').bootstrapSwitch() + } + + if ($('body').tooltip) { + $('[data-toggle="tooltip"]').tooltip() + } + + $('.js-simplemde-textarea').each(function () { + const fileUploadId = $(this).data('file-upload-id') + const uploadField = $(`#${fileUploadId}`) + + // Use textarea id/name for unique autosave ID to prevent content sharing + const textareaId = $(this).attr('id') || $(this).attr('name') || 'default' + const autoSaveId = window.location.toString() + '#' + textareaId + + // this value is only used in testcases/js/mutable.js + window.markdownEditor = initSimpleMDE(this, uploadField, autoSaveId) + }) + + $('#logout_link').click(function () { + $('#logout_form').submit() + return false + }) + + // for debugging in browser + window.jsonRPC = jsonRPC +}) diff --git a/tcms/testcases/forms.py b/tcms/testcases/forms.py index 482bc276ff..93a162cf9f 100644 --- a/tcms/testcases/forms.py +++ b/tcms/testcases/forms.py @@ -1,131 +1,135 @@ -# -*- coding: utf-8 -*- -from django import forms -from django.forms import inlineformset_factory - -from tcms.core.forms.fields import UserField -from tcms.core.widgets import DurationWidget, SimpleMDE -from tcms.management.models import Component, Priority, Product -from tcms.testcases.fields import MultipleEmailField -from tcms.testcases.models import ( - Category, - TestCase, - TestCaseEmailSettings, - TestCaseStatus, -) -from tcms.testplans.models import TestPlan - - -class TestCaseForm(forms.ModelForm): - class Meta: - model = TestCase - exclude = [ # pylint: disable=modelform-uses-exclude - "reviewer", - "tag", - "component", - "plan", - ] - - default_tester = UserField(required=False) - priority = forms.ModelChoiceField( - queryset=Priority.objects.filter(is_active=True), - empty_label=None, - ) - product = forms.ModelChoiceField( - queryset=Product.objects.all(), - empty_label=None, - ) - setup_duration = forms.DurationField( - widget=DurationWidget(), - required=False, - ) - testing_duration = forms.DurationField( - widget=DurationWidget(), - required=False, - ) - text = forms.CharField( - widget=SimpleMDE(), - required=False, - ) - - def populate(self, product_id=None): - if product_id: - self.fields["category"].queryset = Category.objects.filter( - product_id=product_id - ) - else: - self.fields["category"].queryset = Category.objects.all() - - -# only useful b/c we want to override the cc_list field -class CaseNotifyForm(forms.ModelForm): - class Meta: - model = TestCaseEmailSettings - fields = "__all__" - - cc_list = MultipleEmailField(required=False) - - -# note: these fields can't change during runtime ! -_email_settings_fields = [] # pylint: disable=invalid-name -for field in TestCaseEmailSettings._meta.fields: - _email_settings_fields.append(field.name) - - -# for usage in CreateView, UpdateView -CaseNotifyFormSet = inlineformset_factory( # pylint: disable=invalid-name - TestCase, - TestCaseEmailSettings, - form=CaseNotifyForm, - fields=_email_settings_fields, - can_delete=False, - can_order=False, -) - - -class SearchCaseForm(forms.ModelForm): - class Meta: - model = TestCase - fields = "__all__" - - # overriden initial values - product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) - category = forms.ModelChoiceField(queryset=Category.objects.none(), required=False) - component = forms.ModelChoiceField( - queryset=Component.objects.none(), required=False - ) - - # overriden widgets - priority = forms.ModelMultipleChoiceField( - queryset=Priority.objects.filter(is_active=True), - widget=forms.CheckboxSelectMultiple(), - required=False, - ) - case_status = forms.ModelMultipleChoiceField( - queryset=TestCaseStatus.objects.all(), - widget=forms.CheckboxSelectMultiple(), - required=False, - ) - - def populate(self, product_id=None): - if product_id: - self.fields["category"].queryset = Category.objects.filter( - product_id=product_id - ) - self.fields["component"].queryset = Component.objects.filter( - product_id=product_id - ) - - -class CloneCaseForm(forms.Form): # pylint: disable=must-inherit-from-model-form - case = forms.ModelMultipleChoiceField( - queryset=TestCase.objects.all(), - ) - plan = forms.ModelMultipleChoiceField( - queryset=TestPlan.objects.all(), - required=False, - ) - - def populate(self, case_ids): - self.fields["case"].queryset = TestCase.objects.filter(pk__in=case_ids) - plan_ids = self.fields["case"].queryset.values_list("plan", flat=True) - self.fields["plan"].queryset = TestPlan.objects.filter(pk__in=plan_ids) +# -*- coding: utf-8 -*- +from django import forms +from django.forms import inlineformset_factory + +from tcms.core.forms.fields import UserField +from tcms.core.widgets import DurationWidget, SimpleMDE, SimpleMDENotes +from tcms.management.models import Component, Priority, Product +from tcms.testcases.fields import MultipleEmailField +from tcms.testcases.models import ( + Category, + TestCase, + TestCaseEmailSettings, + TestCaseStatus, +) +from tcms.testplans.models import TestPlan + + +class TestCaseForm(forms.ModelForm): + class Meta: + model = TestCase + exclude = [ # pylint: disable=modelform-uses-exclude + "reviewer", + "tag", + "component", + "plan", + ] + + default_tester = UserField(required=False) + priority = forms.ModelChoiceField( + queryset=Priority.objects.filter(is_active=True), + empty_label=None, + ) + product = forms.ModelChoiceField( + queryset=Product.objects.all(), + empty_label=None, + ) + setup_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + testing_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + notes = forms.CharField( + widget=SimpleMDENotes(), + required=False, + ) + text = forms.CharField( + widget=SimpleMDE(), + required=False, + ) + + def populate(self, product_id=None): + if product_id: + self.fields["category"].queryset = Category.objects.filter( + product_id=product_id + ) + else: + self.fields["category"].queryset = Category.objects.all() + + +# only useful b/c we want to override the cc_list field +class CaseNotifyForm(forms.ModelForm): + class Meta: + model = TestCaseEmailSettings + fields = "__all__" + + cc_list = MultipleEmailField(required=False) + + +# note: these fields can't change during runtime ! +_email_settings_fields = [] # pylint: disable=invalid-name +for field in TestCaseEmailSettings._meta.fields: + _email_settings_fields.append(field.name) + + +# for usage in CreateView, UpdateView +CaseNotifyFormSet = inlineformset_factory( # pylint: disable=invalid-name + TestCase, + TestCaseEmailSettings, + form=CaseNotifyForm, + fields=_email_settings_fields, + can_delete=False, + can_order=False, +) + + +class SearchCaseForm(forms.ModelForm): + class Meta: + model = TestCase + fields = "__all__" + + # overriden initial values + product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) + category = forms.ModelChoiceField(queryset=Category.objects.none(), required=False) + component = forms.ModelChoiceField( + queryset=Component.objects.none(), required=False + ) + + # overriden widgets + priority = forms.ModelMultipleChoiceField( + queryset=Priority.objects.filter(is_active=True), + widget=forms.CheckboxSelectMultiple(), + required=False, + ) + case_status = forms.ModelMultipleChoiceField( + queryset=TestCaseStatus.objects.all(), + widget=forms.CheckboxSelectMultiple(), + required=False, + ) + + def populate(self, product_id=None): + if product_id: + self.fields["category"].queryset = Category.objects.filter( + product_id=product_id + ) + self.fields["component"].queryset = Component.objects.filter( + product_id=product_id + ) + + +class CloneCaseForm(forms.Form): # pylint: disable=must-inherit-from-model-form + case = forms.ModelMultipleChoiceField( + queryset=TestCase.objects.all(), + ) + plan = forms.ModelMultipleChoiceField( + queryset=TestPlan.objects.all(), + required=False, + ) + + def populate(self, case_ids): + self.fields["case"].queryset = TestCase.objects.filter(pk__in=case_ids) + plan_ids = self.fields["case"].queryset.values_list("plan", flat=True) + self.fields["plan"].queryset = TestPlan.objects.filter(pk__in=plan_ids) diff --git a/tcms/testcases/templates/testcases/get.html b/tcms/testcases/templates/testcases/get.html index 8665371a95..e1677626f1 100644 --- a/tcms/testcases/templates/testcases/get.html +++ b/tcms/testcases/templates/testcases/get.html @@ -1,247 +1,291 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} -{% load comments %} -{% load extra_filters %} - -{% block title %}TC-{{ object.pk }}: {{ object.summary }}{% endblock %} -{% block page_id %}page-testcases-get{% endblock %} -{% block body_class %}cards-pf{% endblock %} - -{% block contents %} -
- -

- TC-{{ object.pk }}: {{ object.summary }} -

- -
-
-
-

- {% trans 'Author' %}: - {{ object.author.username }} -

- -

- {% trans 'Default tester' %}: - {% if object.default_tester %} - {{ object.default_tester.username }} - {% else %} - - - {% endif %} -

- -

- {% trans 'Product' %}: - {{ object.category.product }} -

- -

- {% trans 'Category' %}: - {{ object.category }} -

- -

- {% trans 'Status' %}: - {{ object.case_status }} -

- -

- {% trans 'Priority' %}: - {{ object.priority }} -

- -

- {{ object.create_date }} -

- -

- {% trans 'Setup duration' %}: - {{ object.setup_duration|default:"-" }} -

- -

- {% trans 'Testing duration' %}: - {{ object.testing_duration|default:"-" }} -

- -

- {% trans 'Expected duration' %}: - {{ object.expected_duration }} -

- -

- {% trans 'Automated' %}: - {{ object.is_automated }} -

- -

- {% trans 'Script' %}: - {{ object.script|default:'-' }} -

- -

- {% trans 'Arguments' %}: - {{ object.arguments|default:'-' }} -

- -

- {% trans 'Requirements' %}: - {{ object.requirement|default:'-' }} -

- -

- {% trans 'Reference link' %}: - {% if object.extra_link %} - {{ object.extra_link }} - {% else %} - - - {% endif %} -

- -
-
-
- -
-
-
-
- {{ object.text|markdown2html }} -
- -

- {% trans 'Notes' %}: - {{ object.notes }} -

-
-
-
- -
- {% include "include/properties_card.html" %} -
-
- -
-
- {% include 'include/tc_executions.html' with show_bugs=True %} -
-
- -
- {% trans "Bugs" as bugs_heading %} - {% include "include/bugs_table.html" with heading=bugs_heading class="bugs" %} - -
-
-

- - {% trans 'Test plans' %} -

- -
- - - - - - - - - - - - - {% if perms.testcases.add_testcaseplan %} - - - - - - - {% endif %} - -
{% trans 'ID' %}{% trans 'Name' %}{% trans 'Author' %}{% trans 'Type' %}{% trans 'Product' %}
-
- -
-
- - - -
-
-
-
- -
- -
-
- {% include 'include/tags_card.html' with add_perm=perms.testcases.add_testcasetag %} -
- -
-
-

- - {% trans 'Components' %} -

- -
- - - - - - - - - {% if perms.testcases.add_testcasecomponent %} - - - - - - - {% endif %} -
{% trans 'Name' %}
-
- -
-
- - - -
-
-
-
- -
- {% include 'include/attachments.html' %} -
- -
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load comments %} +{% load extra_filters %} + +{% block title %}TC-{{ object.pk }}: {{ object.summary }}{% endblock %} +{% block page_id %}page-testcases-get{% endblock %} +{% block body_class %}cards-pf{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block contents %} +
+ +

+ TC-{{ object.pk }}: {{ object.summary }} +

+ + + + + +
+
+
+

+ {% trans 'Author' %}: + {{ object.author.username }} +

+ +

+ {% trans 'Default tester' %}: + {% if object.default_tester %} + {{ object.default_tester.username }} + {% else %} + - + {% endif %} +

+ +

+ {% trans 'Product' %}: + {{ object.category.product }} +

+ +

+ {% trans 'Category' %}: + {{ object.category }} +

+ +

+ {% trans 'Status' %}: + {{ object.case_status }} +

+ +

+ {% trans 'Priority' %}: + {{ object.priority }} +

+ +

+ {{ object.create_date }} +

+ +

+ {% trans 'Setup duration' %}: + {{ object.setup_duration|default:"-" }} +

+ +

+ {% trans 'Testing duration' %}: + {{ object.testing_duration|default:"-" }} +

+ +

+ {% trans 'Expected duration' %}: + {{ object.expected_duration }} +

+ +

+ {% trans 'Automated' %}: + {{ object.is_automated }} +

+ +

+ {% trans 'Script' %}: + {{ object.script|default:'-' }} +

+ +

+ {% trans 'Arguments' %}: + {{ object.arguments|default:'-' }} +

+ +

+ {% trans 'Requirements' %}: + {{ object.requirement|default:'-' }} +

+ +

+ {% trans 'Reference link' %}: + {% if object.extra_link %} + {{ object.extra_link }} + {% else %} + - + {% endif %} +

+ +
+
+
+ +
+
+
+
+ {{ object.text|markdown2html }} +
+ +

+ {% trans 'Notes' %}: + {{ object.notes|markdown2html }} +

+
+
+
+ +
+ {% include "include/properties_card.html" %} +
+
+ +
+
+ {% include 'include/tc_executions.html' with show_bugs=True %} +
+
+ +
+ {% trans "Bugs" as bugs_heading %} + {% include "include/bugs_table.html" with heading=bugs_heading class="bugs" %} + +
+
+

+ + {% trans 'Test plans' %} +

+ +
+ + + + + + + + + + + + + {% if perms.testcases.add_testcaseplan %} + + + + + + + {% endif %} + +
{% trans 'ID' %}{% trans 'Name' %}{% trans 'Author' %}{% trans 'Type' %}{% trans 'Product' %}
+
+ +
+
+ + + +
+
+
+
+ +
+ +
+
+ {% include 'include/tags_card.html' with add_perm=perms.testcases.add_testcasetag %} +
+ +
+
+

+ + {% trans 'Components' %} +

+ +
+ + + + + + + + + {% if perms.testcases.add_testcasecomponent %} + + + + + + + {% endif %} +
{% trans 'Name' %}
+
+ +
+
+ + + +
+
+
+
+ +
+ {% include 'include/attachments.html' %} +
+ +
+
+{% endblock %} diff --git a/tcms/testcases/templates/testcases/mutable.html b/tcms/testcases/templates/testcases/mutable.html index 8da89e7a9b..64c66b7f11 100644 --- a/tcms/testcases/templates/testcases/mutable.html +++ b/tcms/testcases/templates/testcases/mutable.html @@ -1,270 +1,273 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} - -{% block head %} - {{ form.media }} -{% endblock %} -{% block title %} - {% if object %} - {% trans "Edit TestCase" %} - {% else %} - {% trans "New Test Case" %} - {% endif %} -{% endblock %} - -{% block page_id %}page-testcases-mutable{% endblock %} - -{% block contents %} -
-
- {% csrf_token %} - -
- -
- - {% if test_plan %} -

TP-{{ test_plan.pk }}: {{ test_plan.name }}

- - {% endif %} - {{ form.summary.errors }} -
-
- -
- -
- - {{ form.default_tester.errors }} -
- -
- - + -
-
- - {{ form.product.errors }} -
- -
- - + -
-
- - {{ form.category.errors }} -
-
- -
- -
- - {{ form.case_status.errors }} -
- - -
- - {{ form.priority.errors }} -
- - -
- -
-
- -
- {% if not object %} -
- - + -
-
- -
- {% endif %} - - -
-
- {{ form.setup_duration }} -
-
- - -
-
- {{ form.testing_duration }} -
-
-
- -
-
-
{{ form.text }}
- {{ form.text.errors }} -
-
- -
- -
- - {{ form.script.errors }} -
- - -
- - {{ form.arguments.errors }} -
-
- -
- -
- - {{ form.requirement.errors }} -
- -
- - {{ form.extra_link.errors }} -
-
- -
- -
- - {{ form.notes.errors }} -
-
- - {% for notify_form in notify_formset %} -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- - {{ notify_form.cc_list.errors }} -

{% trans "Email addresses separated by comma. A notification email will be sent to each Email address within CC list." %}

-
-
- - {% for hidden_field in notify_form.hidden_fields %} - {{ hidden_field }} - {% endfor %} - {% endfor %} - - {{ notify_formset.management_form }} - -
-
- {% trans "Cancel" %} - -
-
-
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block head %} + {{ form.media }} +{% endblock %} +{% block title %} + {% if object %} + {% trans "Edit TestCase" %} + {% else %} + {% trans "New Test Case" %} + {% endif %} +{% endblock %} + +{% block page_id %}page-testcases-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + {% if request.GET.from_plan %} + + {% endif %} + +
+ +
+ + {% if test_plan %} +

TP-{{ test_plan.pk }}: {{ test_plan.name }}

+ + {% endif %} + {{ form.summary.errors }} +
+
+ +
+ +
+ + {{ form.default_tester.errors }} +
+ +
+ + + +
+
+ + {{ form.product.errors }} +
+ +
+ + + +
+
+ + {{ form.category.errors }} +
+
+ +
+ +
+ + {{ form.case_status.errors }} +
+ + +
+ + {{ form.priority.errors }} +
+ + +
+ +
+
+ +
+ {% if not object %} +
+ + + +
+
+ +
+ {% endif %} + + +
+
+ {{ form.setup_duration }} +
+
+ + +
+
+ {{ form.testing_duration }} +
+
+
+ +
+
+
{{ form.text }}
+ {{ form.text.errors }} +
+
+ +
+ +
+ + {{ form.script.errors }} +
+ + +
+ + {{ form.arguments.errors }} +
+
+ +
+ +
+ + {{ form.requirement.errors }} +
+ +
+ + {{ form.extra_link.errors }} +
+
+ +
+ +
+ {{ form.notes }} + {{ form.notes.errors }} +
+
+ + {% for notify_form in notify_formset %} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ + {{ notify_form.cc_list.errors }} +

{% trans "Email addresses separated by comma. A notification email will be sent to each Email address within CC list." %}

+
+
+ + {% for hidden_field in notify_form.hidden_fields %} + {{ hidden_field }} + {% endfor %} + {% endfor %} + + {{ notify_formset.management_form }} + +
+
+ {% trans "Cancel" %} + +
+
+
+
+{% endblock %} diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index db4e15b0f0..f7b3ce6220 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -1,730 +1,730 @@ -import { jsonRPC } from '../../../../static/js/jsonrpc' -import { tagsCard } from '../../../../static/js/tags' -import { - animate, - advancedSearchAndAddTestCases, - bindDeleteCommentButton, changeDropdownSelectedItem, - markdown2HTML, renderCommentsForObject, renderCommentHTML, - treeViewBind, quickSearchAndAddTestCase, - findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData, - showOrHideMultipleRows -} from '../../../../static/js/utils' -import { initSimpleMDE } from '../../../../static/js/simplemde_security_override' - -const expandedTestCaseIds = [] -const fadeAnimationTime = 500 - -const allTestCases = {} -const autocompleteCache = {} - -const confirmedStatuses = [] - -export function pageTestplansGetReadyHandler () { - const testPlanDataElement = $('#test_plan_pk') - const testPlanId = testPlanDataElement.data('testplan-pk') - - const permissions = { - 'perm-change-testcase': testPlanDataElement.data('perm-change-testcase') === 'True', - 'perm-remove-testcase': testPlanDataElement.data('perm-remove-testcase') === 'True', - 'perm-add-testcase': testPlanDataElement.data('perm-add-testcase') === 'True', - 'perm-add-comment': testPlanDataElement.data('perm-add-comment') === 'True', - 'perm-delete-comment': testPlanDataElement.data('perm-delete-comment') === 'True' - } - - // bind everything in tags table - const permRemoveTag = testPlanDataElement.data('perm-remove-tag') === 'True' - tagsCard('TestPlan', testPlanId, { plan: testPlanId }, permRemoveTag) - - jsonRPC('TestCaseStatus.filter', { is_confirmed: true }, function (statuses) { - // save for later use - for (let i = 0; i < statuses.length; i++) { - confirmedStatuses.push(statuses[i].id) - } - - jsonRPC('TestCase.sortkeys', { plan: testPlanId }, function (sortkeys) { - jsonRPC('TestCase.filter', { plan: testPlanId }, function (data) { - for (let i = 0; i < data.length; i++) { - const testCase = data[i] - - testCase.sortkey = sortkeys[testCase.id] - allTestCases[testCase.id] = testCase - } - sortTestCases(Object.values(allTestCases), testPlanId, permissions, 'sortkey') - - // drag & reorder needs the initial order of test cases and - // they may not be fully loaded when sortable() is initialized! - toolbarEvents(testPlanId, permissions) - }) - }) - }) - - adjustTestPlanFamilyTree() - collapseDocumentText() - quickSearchAndAddTestCase(testPlanId, addTestCaseToPlan, autocompleteCache) - $('#btn-search-cases').click(function () { - return advancedSearchAndAddTestCases( - testPlanId, 'TestPlan.add_case', $(this).attr('href'), - $('#test_plan_pk').data('trans-error-adding-cases') - ) - }) -} - -function addTestCaseToPlan (planId) { - const caseName = $('#search-testcase')[0].value - const testCase = autocompleteCache[caseName] - - // test case is already present so don't add it - if (allTestCases[testCase.id]) { - $('#search-testcase').val('') - return false - } - - jsonRPC('TestPlan.add_case', [planId, testCase.id], function (result) { - // IMPORTANT: the API result includes a 'sortkey' field value! - window.location.reload(true) - - // TODO: remove the page reload above and add the new case to the list - // NB: pay attention to drawTestCases() & treeViewBind() - // NB: also add to allTestCases !!! - - $('#search-testcase').val('') - }) -} - -function collapseDocumentText () { - // for some reason .height() reports a much higher value than - // reality and the 59% reduction seems to work nicely - const infoCardHeight = 0.59 * $('#testplan-info').height() - - if ($('#testplan-text').height() > infoCardHeight) { - $('#testplan-text-collapse-btn').removeClass('hidden') - - $('#testplan-text').css('min-height', infoCardHeight) - $('#testplan-text').css('height', infoCardHeight) - $('#testplan-text').css('overflow', 'hidden') - - $('#testplan-text').on('hidden.bs.collapse', function () { - $('#testplan-text').removeClass('collapse').css({ - height: infoCardHeight - }) - }) - } -} - -function adjustTestPlanFamilyTree () { - treeViewBind('#test-plan-family-tree') - - // remove the > arrows from elements which don't have children - $('#test-plan-family-tree').find('.list-group-item-container').each(function (index, element) { - if (!element.innerHTML.trim()) { - const span = $(element).siblings('.list-group-item-header').find('.list-view-pf-left span') - - span.removeClass('fa-angle-right') - // this is the exact same width so rows are still aligned - span.attr('style', 'width:9px') - } - }) - - // expand all parent elements so that the current one is visible - $('#test-plan-family-tree').find('.list-group-item.active').each(function (index, element) { - $(element).parents('.list-group-item-container').each(function (idx, container) { - $(container).toggleClass('hidden') - $(container).siblings('.list-group-item-header').find('.fa-angle-right').toggleClass('fa-angle-down') - }) - }) -} - -function drawTestCases (testCases, testPlanId, permissions) { - const container = $('#testcases-list') - const noCasesTemplate = $('#no_test_cases') - const testCaseRowDocumentFragment = $('#test_case_row')[0].content - - if (testCases.length > 0) { - testCases.forEach(function (element) { - container.append(getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), element, permissions)) - }) - attachEvents(testPlanId, permissions) - } else { - container.append(noCasesTemplate[0].innerHTML) - } - - $('.test-cases-count').html(testCases.length) -} - -function redrawSingleRow (testCaseId, testPlanId, permissions) { - const testCaseRowDocumentFragment = $('#test_case_row')[0].content - const newRow = getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), allTestCases[testCaseId], permissions) - - // remove from expanded list b/c the comment section may have changed - expandedTestCaseIds.splice(expandedTestCaseIds.indexOf(testCaseId), 1) - - // replace the element in the dom - $(`[data-testcase-pk=${testCaseId}]`).replaceWith(newRow) - attachEvents(testPlanId, permissions) -} - -function getTestCaseRowContent (rowContent, testCase, permissions) { - const row = $(rowContent) - - row[0].firstElementChild.dataset.testcasePk = testCase.id - row.find('.js-test-case-link').html(`TC-${testCase.id}: ${testCase.summary}`).attr('href', `/case/${testCase.id}/`) - // todo: TestCaseStatus here isn't translated b/c TestCase.filter uses a - // custom serializer which needs to be refactored as well - row.find('.js-test-case-status').html(`${testCase.case_status__name}`) - row.find('.js-test-case-priority').html(`${testCase.priority__value}`) - row.find('.js-test-case-category').html(`${testCase.category__name}`) - row.find('.js-test-case-author').html(`${testCase.author__username}`) - row.find('.js-test-case-tester').html(`${testCase.default_tester__username || '-'}`) - row.find('.js-test-case-reviewer').html(`${testCase.reviewer__username || '-'}`) - - // set the links in the kebab menu - if (permissions['perm-change-testcase']) { - row.find('.js-test-case-menu-edit')[0].href = `/case/${testCase.id}/edit/` - } - - if (permissions['perm-add-testcase']) { - row.find('.js-test-case-menu-clone')[0].href = `/cases/clone/?c=${testCase.id}` - } - - // apply visual separation between confirmed and not confirmed - - if (!isTestCaseConfirmed(testCase.case_status)) { - row.find('.list-group-item-header').addClass('bg-danger') - - // add customizable icon as part of #1932 - row.find('.js-test-case-status-icon').addClass('fa-times') - - row.find('.js-test-case-tester-div').toggleClass('hidden') - row.find('.js-test-case-reviewer-div').toggleClass('hidden') - } else { - row.find('.js-test-case-status-icon').addClass('fa-check-square') - } - - // handle automated icon - const automationIndicationElement = row.find('.js-test-case-automated') - let automatedClassToRemove = 'fa-cog' - - if (testCase.is_automated) { - automatedClassToRemove = 'fa-hand-paper-o' - } - - automationIndicationElement.parent().attr( - 'title', - automationIndicationElement.data(testCase.is_automated.toString()) - ) - automationIndicationElement.removeClass(automatedClassToRemove) - - // produce unique IDs for comments textarea and file upload fields - row.find('textarea')[0].id = `comment-for-testcase-${testCase.id}` - row.find('input[type="file"]')[0].id = `file-upload-for-testcase-${testCase.id}` - - return row -} - -function getTestCaseExpandArea (row, testCase, permissions) { - markdown2HTML(testCase.text, row.find('.js-test-case-expand-text')) - if (testCase.notes.trim().length > 0) { - row.find('.js-test-case-expand-notes').html(testCase.notes) - } - - // draw the attachments - const uniqueDivCustomId = `js-tc-id-${testCase.id}-attachments` - // set unique identifier so we know where to draw fetched data - row.find('.js-test-case-expand-attachments').parent()[0].id = uniqueDivCustomId - - jsonRPC('TestCase.list_attachments', [testCase.id], function (data) { - // cannot use instance of row in the callback - const ulElement = $(`#${uniqueDivCustomId} .js-test-case-expand-attachments`) - - if (data.length === 0) { - ulElement.children().removeClass('hidden') - return - } - - const liElementFragment = $('#attachments-list-item')[0].content - - for (let i = 0; i < data.length; i++) { - // should create new element for every attachment - const liElement = liElementFragment.cloneNode(true) - const attachmentLink = $(liElement).find('a')[0] - - attachmentLink.href = data[i].url - attachmentLink.innerText = data[i].url.split('/').slice(-1)[0] - ulElement.append(liElement) - } - }) - - // load components - const componentTemplate = row.find('.js-testcase-expand-components').find('template')[0].content - jsonRPC('Component.filter', { cases: testCase.id }, function (result) { - result.forEach(function (element) { - const newComponent = componentTemplate.cloneNode(true) - $(newComponent).find('span').html(element.name) - row.find('.js-testcase-expand-components').append(newComponent) - }) - }) - - // load tags - const tagTemplate = row.find('.js-testcase-expand-tags').find('template')[0].content - jsonRPC('Tag.filter', { case: testCase.id }, function (result) { - const uniqueTags = [] - - result.forEach(function (element) { - if (uniqueTags.indexOf(element.name) === -1) { - uniqueTags.push(element.name) - - const newTag = tagTemplate.cloneNode(true) - $(newTag).find('span').html(element.name) - row.find('.js-testcase-expand-tags').append(newTag) - } - }) - }) - - // render previous comments - renderCommentsForObject( - testCase.id, - 'TestCase.comments', - 'TestCase.remove_comment', - !isTestCaseConfirmed(testCase.case_status) && permissions['perm-delete-comment'], - row.find('.comments') - ) - - // render comments form - const commentFormTextArea = row.find('.js-comment-form-textarea') - if (!isTestCaseConfirmed(testCase.case_status) && permissions['perm-add-comment']) { - const textArea = row.find('textarea')[0] - const fileUpload = row.find('input[type="file"]') - const editor = initSimpleMDE(textArea, $(fileUpload), textArea.id) - - row.find('.js-post-comment').click(function (event) { - event.preventDefault() - const input = editor.value().trim() - - if (input) { - jsonRPC('TestCase.add_comment', [testCase.id, input], comment => { - editor.value('') - - // show the newly added comment and bind its delete button - row.find('.comments').append( - renderCommentHTML( - 1 + row.find('.js-comment-container').length, - comment, - $('template#comment-template')[0], - function (parentNode) { - bindDeleteCommentButton( - testCase.id, - 'TestCase.remove_comment', - permissions['perm-delete-comment'], // b/c we already know it's unconfirmed - parentNode) - }) - ) - }) - } - }) - } else { - commentFormTextArea.hide() - } -} - -function attachEvents (testPlanId, permissions) { - treeViewBind('#testcases-list') - - if (permissions['perm-change-testcase']) { - // update default tester - $('.js-test-case-menu-tester').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - - const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) - if (!emailOrUsername) { - return false - } - - updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { default_tester: emailOrUsername }, - testPlanId, permissions) - - return false - }) - - $('.js-test-case-menu-priority').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - - updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { priority: ev.target.dataset.id }, - testPlanId, permissions) - return false - }) - - $('.js-test-case-menu-status').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const testCaseId = getCaseIdFromEvent(ev) - updateTestCasesViaAPI([testCaseId], { case_status: ev.target.dataset.id }, - testPlanId, permissions) - return false - }) - } - - if (permissions['perm-remove-testcase']) { - // delete testcase from the plan - $('.js-test-case-menu-delete').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const testCaseId = getCaseIdFromEvent(ev) - - jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { - delete allTestCases[testCaseId] - - // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change - $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { - $(this).remove() - }) - - const testCasesCountEl = $('.test-cases-count') - const count = parseInt(testCasesCountEl[0].innerText) - testCasesCountEl.html(count - 1) - }) - - return false - }) - } - - // get details and draw expand area only on expand - $('.js-testcase-row').click(function (ev) { - // don't trigger row expansion when kebab menu is clicked - if ($(ev.target).is('button, a, input, .fa-ellipsis-v')) { - return - } - - const testCaseId = getCaseIdFromEvent(ev) - - // tc was expanded once, dom is ready - if (expandedTestCaseIds.indexOf(testCaseId) > -1) { - return - } - - const tcRow = $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`) - expandedTestCaseIds.push(testCaseId) - getTestCaseExpandArea(tcRow, allTestCases[testCaseId], permissions) - }) - - const inputs = $('.js-testcase-row').find('input') - inputs.click(function (ev) { - // stop trigerring row.click() - ev.stopPropagation() - const checkbox = $('.js-checkbox-toolbar')[0] - - inputs.each(function (index, tc) { - checkbox.checked = tc.checked - - if (!checkbox.checked) { - return false - } - }) - }) - - function getCaseIdFromEvent (ev) { - return $(ev.target).closest('.js-testcase-row').data('testcase-pk') - } -} - -function updateTestCasesViaAPI (testCaseIds, updateQuery, testPlanId, permissions) { - testCaseIds.forEach(function (caseId) { - jsonRPC('TestCase.update', [caseId, updateQuery], function (updatedTC) { - const testCaseRow = $(`.js-testcase-row[data-testcase-pk=${caseId}]`) - - // update internal data - const sortkey = allTestCases[caseId].sortkey - allTestCases[caseId] = updatedTC - // note: updatedTC doesn't have sortkey information - allTestCases[caseId].sortkey = sortkey - - animate(testCaseRow, function () { - redrawSingleRow(caseId, testPlanId, permissions) - }) - }) - }) -} - -function toolbarEvents (testPlanId, permissions) { - $('.js-checkbox-toolbar').click(function (ev) { - const isChecked = ev.target.checked - const testCaseRows = $('.js-testcase-row').find('input') - - testCaseRows.each(function (index, tc) { - tc.checked = isChecked - }) - }) - - $('.js-toolbar-filter-options li').click(function (ev) { - return changeDropdownSelectedItem( - '.js-toolbar-filter-options', - '#input-filter-button', - ev.target, - $('#toolbar-filter') - ) - }) - - $('#toolbar-filter').on('keyup', function () { - const filterValue = $(this).val().toLowerCase() - const filterBy = $('.js-toolbar-filter-options .selected')[0].dataset.filterType - - filterTestCasesByProperty( - testPlanId, - Object.values(allTestCases), - filterBy, - filterValue - ) - }) - - $('.js-toolbar-sort-options li').click(function (ev) { - changeDropdownSelectedItem('.js-toolbar-sort-options', '#sort-button', ev.target) - - sortTestCases(Object.values(allTestCases), testPlanId, permissions) - return false - }) - - // handle asc desc icon - $('.js-toolbar-sorting-order > span').click(function (ev) { - const icon = $(this) - - icon.siblings('.hidden').removeClass('hidden') - icon.addClass('hidden') - - sortTestCases(Object.values(allTestCases), testPlanId, permissions) - }) - - // always initialize the sortable list however you can only - // move items using the handle icon on the left which becomes - // visible only when the manual sorting button is clicked - sortable('#testcases-list', { - handle: '.handle', - itemSerializer: (serializedItem, sortableContainer) => { - return parseInt(serializedItem.node.getAttribute('data-testcase-pk')) - } - }) - - // IMPORTANT: this is not empty b/c sortable() is initialized *after* - // all of the test cases have been rendered !!! - const initialOrder = sortable('#testcases-list', 'serialize')[0].items - - $('.js-toolbar-manual-sort').click(function (event) { - $(this).blur() - $('.js-toolbar-manual-sort').find('span').toggleClass(['fa-sort', 'fa-check-square']) - $('.js-testcase-sort-handler, .js-testcase-expand-arrow, .js-testcase-checkbox').toggleClass('hidden') - - const currentOrder = sortable('#testcases-list', 'serialize')[0].items - - // rows have been rearranged and the results must be committed to the DB - if (currentOrder.join() !== initialOrder.join()) { - currentOrder.forEach(function (tcPk, index) { - jsonRPC('TestPlan.update_case_order', [testPlanId, tcPk, index * 10], function (result) {}) - }) - } - }) - - $('.js-toolbar-priority').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - updateTestCasesViaAPI(selectedCases, { priority: ev.target.dataset.id }, - testPlanId, permissions) - - return false - }) - - $('.js-toolbar-status').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - updateTestCasesViaAPI(selectedCases, { case_status: ev.target.dataset.id }, - testPlanId, permissions) - return false - }) - - $('#default-tester-button').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) - - if (!emailOrUsername) { - return false - } - - updateTestCasesViaAPI(selectedCases, { default_tester: emailOrUsername }, - testPlanId, permissions) - - return false - }) - - $('#bulk-reviewer-button').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) - - if (!emailOrUsername) { - return false - } - - updateTestCasesViaAPI(selectedCases, { reviewer: emailOrUsername }, - testPlanId, permissions) - - return false - }) - - $('#delete_button').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - const areYouSureText = $('#test_plan_pk').data('trans-are-you-sure') - if (confirm(areYouSureText)) { - for (let i = 0; i < selectedCases.length; i++) { - const testCaseId = selectedCases[i] - jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { - delete allTestCases[testCaseId] - - // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change - $(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { - $(this).remove() - }) - }) - } - - const testCasesCountEl = $('.test-cases-count') - const count = parseInt(testCasesCountEl[0].innerText) - testCasesCountEl.html(count - selectedCases.length) - } - - return false - }) - - $('#bulk-clone-button').click(function () { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - window.location.assign(`/cases/clone?c=${selectedCases.join('&c=')}`) - }) - - $('#testplan-toolbar-newrun').click(function () { - $(this).parents('.dropdown').toggleClass('open') - const selectedTestCases = getSelectedTestCases() - - if (!selectedTestCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - for (let i = 0; i < selectedTestCases.length; i++) { - const status = allTestCases[selectedTestCases[i]].case_status - if (!isTestCaseConfirmed(status)) { - alert($('#test_plan_pk').data('trans-cannot-create-testrun')) - return false - } - } - - const newTestRunUrl = $('#test_plan_pk').data('new-testrun-url') - window.location.assign(`${newTestRunUrl}?c=${selectedTestCases.join('&c=')}`) - return false - }) -} - -function isTestCaseConfirmed (status) { - return confirmedStatuses.indexOf(Number(status)) > -1 -} - -function sortTestCases (testCases, testPlanId, permissions, defaultSortBy = undefined) { - const sortBy = defaultSortBy || $('.js-toolbar-sort-options .selected')[0].dataset.filterType - const sortOrder = $('.js-toolbar-sorting-order > span:not(.hidden)').data('order') - - $('#testcases-list').html('') - - testCases.sort(function (tc1, tc2) { - const value1 = tc1[sortBy] || '' - const value2 = tc2[sortBy] || '' - - if (Number.isInteger(value1) && Number.isInteger(value2)) { - return (value1 - value2) * sortOrder - } - - return value1.toString().localeCompare(value2.toString()) * sortOrder - }) - - // put the new order in the DOM - drawTestCases(testCases, testPlanId, permissions) -} - -// todo check selectedCheckboxes function in testrun/get.js -function getSelectedTestCases () { - const inputs = $('.js-testcase-row input:checked') - const tcIds = [] - - inputs.each(function (index, el) { - const elJq = $(el) - - if (elJq.is(':hidden')) { - return - } - - const id = elJq.closest('.js-testcase-row').data('testcase-pk') - tcIds.push(id) - }) - - return tcIds -} - -function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { - // no input => show all rows - if (filterValue.trim().length === 0) { - $('.js-testcase-row').show() - $('.test-cases-count').text(testCases.length) - return - } - - $('.js-testcase-row').hide() - - if (filterBy === 'component' || filterBy === 'tag') { - const query = { plan: planId } - query[`${filterBy}__name__icontains`] = filterValue - - jsonRPC('TestCase.filter', query, function (filtered) { - // hide again if a previous async request showed something else - $('.js-testcase-row').hide() - - const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered, '[data-testcase-pk={0}]') - showOrHideMultipleRows('.js-testcase-row', rows) - $('.test-cases-count').text(rows.show.length) - }) - } else { - const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue, '[data-testcase-pk={0}]') - showOrHideMultipleRows('.js-testcase-row', rows) - $('.test-cases-count').text(rows.show.length) - } -} +import { jsonRPC } from '../../../../static/js/jsonrpc' +import { tagsCard } from '../../../../static/js/tags' +import { + animate, + advancedSearchAndAddTestCases, + bindDeleteCommentButton, changeDropdownSelectedItem, + markdown2HTML, renderCommentsForObject, renderCommentHTML, + treeViewBind, quickSearchAndAddTestCase, + findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData, + showOrHideMultipleRows +} from '../../../../static/js/utils' +import { initSimpleMDE } from '../../../../static/js/simplemde_security_override' + +const expandedTestCaseIds = [] +const fadeAnimationTime = 500 + +const allTestCases = {} +const autocompleteCache = {} + +const confirmedStatuses = [] + +export function pageTestplansGetReadyHandler () { + const testPlanDataElement = $('#test_plan_pk') + const testPlanId = testPlanDataElement.data('testplan-pk') + + const permissions = { + 'perm-change-testcase': testPlanDataElement.data('perm-change-testcase') === 'True', + 'perm-remove-testcase': testPlanDataElement.data('perm-remove-testcase') === 'True', + 'perm-add-testcase': testPlanDataElement.data('perm-add-testcase') === 'True', + 'perm-add-comment': testPlanDataElement.data('perm-add-comment') === 'True', + 'perm-delete-comment': testPlanDataElement.data('perm-delete-comment') === 'True' + } + + // bind everything in tags table + const permRemoveTag = testPlanDataElement.data('perm-remove-tag') === 'True' + tagsCard('TestPlan', testPlanId, { plan: testPlanId }, permRemoveTag) + + jsonRPC('TestCaseStatus.filter', { is_confirmed: true }, function (statuses) { + // save for later use + for (let i = 0; i < statuses.length; i++) { + confirmedStatuses.push(statuses[i].id) + } + + jsonRPC('TestCase.sortkeys', { plan: testPlanId }, function (sortkeys) { + jsonRPC('TestCase.filter', { plan: testPlanId }, function (data) { + for (let i = 0; i < data.length; i++) { + const testCase = data[i] + + testCase.sortkey = sortkeys[testCase.id] + allTestCases[testCase.id] = testCase + } + sortTestCases(Object.values(allTestCases), testPlanId, permissions, 'sortkey') + + // drag & reorder needs the initial order of test cases and + // they may not be fully loaded when sortable() is initialized! + toolbarEvents(testPlanId, permissions) + }) + }) + }) + + adjustTestPlanFamilyTree() + collapseDocumentText() + quickSearchAndAddTestCase(testPlanId, addTestCaseToPlan, autocompleteCache) + $('#btn-search-cases').click(function () { + return advancedSearchAndAddTestCases( + testPlanId, 'TestPlan.add_case', $(this).attr('href'), + $('#test_plan_pk').data('trans-error-adding-cases') + ) + }) +} + +function addTestCaseToPlan (planId) { + const caseName = $('#search-testcase')[0].value + const testCase = autocompleteCache[caseName] + + // test case is already present so don't add it + if (allTestCases[testCase.id]) { + $('#search-testcase').val('') + return false + } + + jsonRPC('TestPlan.add_case', [planId, testCase.id], function (result) { + // IMPORTANT: the API result includes a 'sortkey' field value! + window.location.reload(true) + + // TODO: remove the page reload above and add the new case to the list + // NB: pay attention to drawTestCases() & treeViewBind() + // NB: also add to allTestCases !!! + + $('#search-testcase').val('') + }) +} + +function collapseDocumentText () { + // for some reason .height() reports a much higher value than + // reality and the 59% reduction seems to work nicely + const infoCardHeight = 0.59 * $('#testplan-info').height() + + if ($('#testplan-text').height() > infoCardHeight) { + $('#testplan-text-collapse-btn').removeClass('hidden') + + $('#testplan-text').css('min-height', infoCardHeight) + $('#testplan-text').css('height', infoCardHeight) + $('#testplan-text').css('overflow', 'hidden') + + $('#testplan-text').on('hidden.bs.collapse', function () { + $('#testplan-text').removeClass('collapse').css({ + height: infoCardHeight + }) + }) + } +} + +function adjustTestPlanFamilyTree () { + treeViewBind('#test-plan-family-tree') + + // remove the > arrows from elements which don't have children + $('#test-plan-family-tree').find('.list-group-item-container').each(function (index, element) { + if (!element.innerHTML.trim()) { + const span = $(element).siblings('.list-group-item-header').find('.list-view-pf-left span') + + span.removeClass('fa-angle-right') + // this is the exact same width so rows are still aligned + span.attr('style', 'width:9px') + } + }) + + // expand all parent elements so that the current one is visible + $('#test-plan-family-tree').find('.list-group-item.active').each(function (index, element) { + $(element).parents('.list-group-item-container').each(function (idx, container) { + $(container).toggleClass('hidden') + $(container).siblings('.list-group-item-header').find('.fa-angle-right').toggleClass('fa-angle-down') + }) + }) +} + +function drawTestCases (testCases, testPlanId, permissions) { + const container = $('#testcases-list') + const noCasesTemplate = $('#no_test_cases') + const testCaseRowDocumentFragment = $('#test_case_row')[0].content + + if (testCases.length > 0) { + testCases.forEach(function (element) { + container.append(getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), element, permissions, testPlanId)) + }) + attachEvents(testPlanId, permissions) + } else { + container.append(noCasesTemplate[0].innerHTML) + } + + $('.test-cases-count').html(testCases.length) +} + +function redrawSingleRow (testCaseId, testPlanId, permissions) { + const testCaseRowDocumentFragment = $('#test_case_row')[0].content + const newRow = getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), allTestCases[testCaseId], permissions, testPlanId) + + // remove from expanded list b/c the comment section may have changed + expandedTestCaseIds.splice(expandedTestCaseIds.indexOf(testCaseId), 1) + + // replace the element in the dom + $(`[data-testcase-pk=${testCaseId}]`).replaceWith(newRow) + attachEvents(testPlanId, permissions) +} + +function getTestCaseRowContent (rowContent, testCase, permissions, testPlanId) { + const row = $(rowContent) + + row[0].firstElementChild.dataset.testcasePk = testCase.id + row.find('.js-test-case-link').html(`TC-${testCase.id}: ${testCase.summary}`).attr('href', `/case/${testCase.id}/?from_plan=${testPlanId}`) + // todo: TestCaseStatus here isn't translated b/c TestCase.filter uses a + // custom serializer which needs to be refactored as well + row.find('.js-test-case-status').html(`${testCase.case_status__name}`) + row.find('.js-test-case-priority').html(`${testCase.priority__value}`) + row.find('.js-test-case-category').html(`${testCase.category__name}`) + row.find('.js-test-case-author').html(`${testCase.author__username}`) + row.find('.js-test-case-tester').html(`${testCase.default_tester__username || '-'}`) + row.find('.js-test-case-reviewer').html(`${testCase.reviewer__username || '-'}`) + + // set the links in the kebab menu + if (permissions['perm-change-testcase']) { + row.find('.js-test-case-menu-edit')[0].href = `/case/${testCase.id}/edit/?from_plan=${testPlanId}` + } + + if (permissions['perm-add-testcase']) { + row.find('.js-test-case-menu-clone')[0].href = `/cases/clone/?c=${testCase.id}` + } + + // apply visual separation between confirmed and not confirmed + + if (!isTestCaseConfirmed(testCase.case_status)) { + row.find('.list-group-item-header').addClass('bg-danger') + + // add customizable icon as part of #1932 + row.find('.js-test-case-status-icon').addClass('fa-times') + + row.find('.js-test-case-tester-div').toggleClass('hidden') + row.find('.js-test-case-reviewer-div').toggleClass('hidden') + } else { + row.find('.js-test-case-status-icon').addClass('fa-check-square') + } + + // handle automated icon + const automationIndicationElement = row.find('.js-test-case-automated') + let automatedClassToRemove = 'fa-cog' + + if (testCase.is_automated) { + automatedClassToRemove = 'fa-hand-paper-o' + } + + automationIndicationElement.parent().attr( + 'title', + automationIndicationElement.data(testCase.is_automated.toString()) + ) + automationIndicationElement.removeClass(automatedClassToRemove) + + // produce unique IDs for comments textarea and file upload fields + row.find('textarea')[0].id = `comment-for-testcase-${testCase.id}` + row.find('input[type="file"]')[0].id = `file-upload-for-testcase-${testCase.id}` + + return row +} + +function getTestCaseExpandArea (row, testCase, permissions) { + markdown2HTML(testCase.text, row.find('.js-test-case-expand-text')) + if (testCase.notes.trim().length > 0) { + markdown2HTML(testCase.notes, row.find('.js-test-case-expand-notes')) + } + + // draw the attachments + const uniqueDivCustomId = `js-tc-id-${testCase.id}-attachments` + // set unique identifier so we know where to draw fetched data + row.find('.js-test-case-expand-attachments').parent()[0].id = uniqueDivCustomId + + jsonRPC('TestCase.list_attachments', [testCase.id], function (data) { + // cannot use instance of row in the callback + const ulElement = $(`#${uniqueDivCustomId} .js-test-case-expand-attachments`) + + if (data.length === 0) { + ulElement.children().removeClass('hidden') + return + } + + const liElementFragment = $('#attachments-list-item')[0].content + + for (let i = 0; i < data.length; i++) { + // should create new element for every attachment + const liElement = liElementFragment.cloneNode(true) + const attachmentLink = $(liElement).find('a')[0] + + attachmentLink.href = data[i].url + attachmentLink.innerText = data[i].url.split('/').slice(-1)[0] + ulElement.append(liElement) + } + }) + + // load components + const componentTemplate = row.find('.js-testcase-expand-components').find('template')[0].content + jsonRPC('Component.filter', { cases: testCase.id }, function (result) { + result.forEach(function (element) { + const newComponent = componentTemplate.cloneNode(true) + $(newComponent).find('span').html(element.name) + row.find('.js-testcase-expand-components').append(newComponent) + }) + }) + + // load tags + const tagTemplate = row.find('.js-testcase-expand-tags').find('template')[0].content + jsonRPC('Tag.filter', { case: testCase.id }, function (result) { + const uniqueTags = [] + + result.forEach(function (element) { + if (uniqueTags.indexOf(element.name) === -1) { + uniqueTags.push(element.name) + + const newTag = tagTemplate.cloneNode(true) + $(newTag).find('span').html(element.name) + row.find('.js-testcase-expand-tags').append(newTag) + } + }) + }) + + // render previous comments + renderCommentsForObject( + testCase.id, + 'TestCase.comments', + 'TestCase.remove_comment', + !isTestCaseConfirmed(testCase.case_status) && permissions['perm-delete-comment'], + row.find('.comments') + ) + + // render comments form + const commentFormTextArea = row.find('.js-comment-form-textarea') + if (!isTestCaseConfirmed(testCase.case_status) && permissions['perm-add-comment']) { + const textArea = row.find('textarea')[0] + const fileUpload = row.find('input[type="file"]') + const editor = initSimpleMDE(textArea, $(fileUpload), textArea.id) + + row.find('.js-post-comment').click(function (event) { + event.preventDefault() + const input = editor.value().trim() + + if (input) { + jsonRPC('TestCase.add_comment', [testCase.id, input], comment => { + editor.value('') + + // show the newly added comment and bind its delete button + row.find('.comments').append( + renderCommentHTML( + 1 + row.find('.js-comment-container').length, + comment, + $('template#comment-template')[0], + function (parentNode) { + bindDeleteCommentButton( + testCase.id, + 'TestCase.remove_comment', + permissions['perm-delete-comment'], // b/c we already know it's unconfirmed + parentNode) + }) + ) + }) + } + }) + } else { + commentFormTextArea.hide() + } +} + +function attachEvents (testPlanId, permissions) { + treeViewBind('#testcases-list') + + if (permissions['perm-change-testcase']) { + // update default tester + $('.js-test-case-menu-tester').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + + const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) + if (!emailOrUsername) { + return false + } + + updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { default_tester: emailOrUsername }, + testPlanId, permissions) + + return false + }) + + $('.js-test-case-menu-priority').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + + updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { priority: ev.target.dataset.id }, + testPlanId, permissions) + return false + }) + + $('.js-test-case-menu-status').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const testCaseId = getCaseIdFromEvent(ev) + updateTestCasesViaAPI([testCaseId], { case_status: ev.target.dataset.id }, + testPlanId, permissions) + return false + }) + } + + if (permissions['perm-remove-testcase']) { + // delete testcase from the plan + $('.js-test-case-menu-delete').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const testCaseId = getCaseIdFromEvent(ev) + + jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { + delete allTestCases[testCaseId] + + // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change + $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { + $(this).remove() + }) + + const testCasesCountEl = $('.test-cases-count') + const count = parseInt(testCasesCountEl[0].innerText) + testCasesCountEl.html(count - 1) + }) + + return false + }) + } + + // get details and draw expand area only on expand + $('.js-testcase-row').click(function (ev) { + // don't trigger row expansion when kebab menu is clicked + if ($(ev.target).is('button, a, input, .fa-ellipsis-v')) { + return + } + + const testCaseId = getCaseIdFromEvent(ev) + + // tc was expanded once, dom is ready + if (expandedTestCaseIds.indexOf(testCaseId) > -1) { + return + } + + const tcRow = $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`) + expandedTestCaseIds.push(testCaseId) + getTestCaseExpandArea(tcRow, allTestCases[testCaseId], permissions) + }) + + const inputs = $('.js-testcase-row').find('input') + inputs.click(function (ev) { + // stop trigerring row.click() + ev.stopPropagation() + const checkbox = $('.js-checkbox-toolbar')[0] + + inputs.each(function (index, tc) { + checkbox.checked = tc.checked + + if (!checkbox.checked) { + return false + } + }) + }) + + function getCaseIdFromEvent (ev) { + return $(ev.target).closest('.js-testcase-row').data('testcase-pk') + } +} + +function updateTestCasesViaAPI (testCaseIds, updateQuery, testPlanId, permissions) { + testCaseIds.forEach(function (caseId) { + jsonRPC('TestCase.update', [caseId, updateQuery], function (updatedTC) { + const testCaseRow = $(`.js-testcase-row[data-testcase-pk=${caseId}]`) + + // update internal data + const sortkey = allTestCases[caseId].sortkey + allTestCases[caseId] = updatedTC + // note: updatedTC doesn't have sortkey information + allTestCases[caseId].sortkey = sortkey + + animate(testCaseRow, function () { + redrawSingleRow(caseId, testPlanId, permissions) + }) + }) + }) +} + +function toolbarEvents (testPlanId, permissions) { + $('.js-checkbox-toolbar').click(function (ev) { + const isChecked = ev.target.checked + const testCaseRows = $('.js-testcase-row').find('input') + + testCaseRows.each(function (index, tc) { + tc.checked = isChecked + }) + }) + + $('.js-toolbar-filter-options li').click(function (ev) { + return changeDropdownSelectedItem( + '.js-toolbar-filter-options', + '#input-filter-button', + ev.target, + $('#toolbar-filter') + ) + }) + + $('#toolbar-filter').on('keyup', function () { + const filterValue = $(this).val().toLowerCase() + const filterBy = $('.js-toolbar-filter-options .selected')[0].dataset.filterType + + filterTestCasesByProperty( + testPlanId, + Object.values(allTestCases), + filterBy, + filterValue + ) + }) + + $('.js-toolbar-sort-options li').click(function (ev) { + changeDropdownSelectedItem('.js-toolbar-sort-options', '#sort-button', ev.target) + + sortTestCases(Object.values(allTestCases), testPlanId, permissions) + return false + }) + + // handle asc desc icon + $('.js-toolbar-sorting-order > span').click(function (ev) { + const icon = $(this) + + icon.siblings('.hidden').removeClass('hidden') + icon.addClass('hidden') + + sortTestCases(Object.values(allTestCases), testPlanId, permissions) + }) + + // always initialize the sortable list however you can only + // move items using the handle icon on the left which becomes + // visible only when the manual sorting button is clicked + sortable('#testcases-list', { + handle: '.handle', + itemSerializer: (serializedItem, sortableContainer) => { + return parseInt(serializedItem.node.getAttribute('data-testcase-pk')) + } + }) + + // IMPORTANT: this is not empty b/c sortable() is initialized *after* + // all of the test cases have been rendered !!! + const initialOrder = sortable('#testcases-list', 'serialize')[0].items + + $('.js-toolbar-manual-sort').click(function (event) { + $(this).blur() + $('.js-toolbar-manual-sort').find('span').toggleClass(['fa-sort', 'fa-check-square']) + $('.js-testcase-sort-handler, .js-testcase-expand-arrow, .js-testcase-checkbox').toggleClass('hidden') + + const currentOrder = sortable('#testcases-list', 'serialize')[0].items + + // rows have been rearranged and the results must be committed to the DB + if (currentOrder.join() !== initialOrder.join()) { + currentOrder.forEach(function (tcPk, index) { + jsonRPC('TestPlan.update_case_order', [testPlanId, tcPk, index * 10], function (result) {}) + }) + } + }) + + $('.js-toolbar-priority').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + updateTestCasesViaAPI(selectedCases, { priority: ev.target.dataset.id }, + testPlanId, permissions) + + return false + }) + + $('.js-toolbar-status').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + updateTestCasesViaAPI(selectedCases, { case_status: ev.target.dataset.id }, + testPlanId, permissions) + return false + }) + + $('#default-tester-button').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) + + if (!emailOrUsername) { + return false + } + + updateTestCasesViaAPI(selectedCases, { default_tester: emailOrUsername }, + testPlanId, permissions) + + return false + }) + + $('#bulk-reviewer-button').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) + + if (!emailOrUsername) { + return false + } + + updateTestCasesViaAPI(selectedCases, { reviewer: emailOrUsername }, + testPlanId, permissions) + + return false + }) + + $('#delete_button').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + const areYouSureText = $('#test_plan_pk').data('trans-are-you-sure') + if (confirm(areYouSureText)) { + for (let i = 0; i < selectedCases.length; i++) { + const testCaseId = selectedCases[i] + jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { + delete allTestCases[testCaseId] + + // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change + $(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { + $(this).remove() + }) + }) + } + + const testCasesCountEl = $('.test-cases-count') + const count = parseInt(testCasesCountEl[0].innerText) + testCasesCountEl.html(count - selectedCases.length) + } + + return false + }) + + $('#bulk-clone-button').click(function () { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + window.location.assign(`/cases/clone?c=${selectedCases.join('&c=')}`) + }) + + $('#testplan-toolbar-newrun').click(function () { + $(this).parents('.dropdown').toggleClass('open') + const selectedTestCases = getSelectedTestCases() + + if (!selectedTestCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + for (let i = 0; i < selectedTestCases.length; i++) { + const status = allTestCases[selectedTestCases[i]].case_status + if (!isTestCaseConfirmed(status)) { + alert($('#test_plan_pk').data('trans-cannot-create-testrun')) + return false + } + } + + const newTestRunUrl = $('#test_plan_pk').data('new-testrun-url') + window.location.assign(`${newTestRunUrl}?c=${selectedTestCases.join('&c=')}`) + return false + }) +} + +function isTestCaseConfirmed (status) { + return confirmedStatuses.indexOf(Number(status)) > -1 +} + +function sortTestCases (testCases, testPlanId, permissions, defaultSortBy = undefined) { + const sortBy = defaultSortBy || $('.js-toolbar-sort-options .selected')[0].dataset.filterType + const sortOrder = $('.js-toolbar-sorting-order > span:not(.hidden)').data('order') + + $('#testcases-list').html('') + + testCases.sort(function (tc1, tc2) { + const value1 = tc1[sortBy] || '' + const value2 = tc2[sortBy] || '' + + if (Number.isInteger(value1) && Number.isInteger(value2)) { + return (value1 - value2) * sortOrder + } + + return value1.toString().localeCompare(value2.toString()) * sortOrder + }) + + // put the new order in the DOM + drawTestCases(testCases, testPlanId, permissions) +} + +// todo check selectedCheckboxes function in testrun/get.js +function getSelectedTestCases () { + const inputs = $('.js-testcase-row input:checked') + const tcIds = [] + + inputs.each(function (index, el) { + const elJq = $(el) + + if (elJq.is(':hidden')) { + return + } + + const id = elJq.closest('.js-testcase-row').data('testcase-pk') + tcIds.push(id) + }) + + return tcIds +} + +function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { + // no input => show all rows + if (filterValue.trim().length === 0) { + $('.js-testcase-row').show() + $('.test-cases-count').text(testCases.length) + return + } + + $('.js-testcase-row').hide() + + if (filterBy === 'component' || filterBy === 'tag') { + const query = { plan: planId } + query[`${filterBy}__name__icontains`] = filterValue + + jsonRPC('TestCase.filter', query, function (filtered) { + // hide again if a previous async request showed something else + $('.js-testcase-row').hide() + + const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered, '[data-testcase-pk={0}]') + showOrHideMultipleRows('.js-testcase-row', rows) + $('.test-cases-count').text(rows.show.length) + }) + } else { + const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue, '[data-testcase-pk={0}]') + showOrHideMultipleRows('.js-testcase-row', rows) + $('.test-cases-count').text(rows.show.length) + } +} diff --git a/tcms/testruns/forms.py b/tcms/testruns/forms.py index 741dee150d..e8cfe6d192 100644 --- a/tcms/testruns/forms.py +++ b/tcms/testruns/forms.py @@ -1,95 +1,100 @@ -# -*- coding: utf-8 -*- -from django import forms -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ - -from tcms.core.forms.fields import UserField -from tcms.management.models import Build, Product, Version -from tcms.rpc.api.forms import DateTimeField -from tcms.testcases.models import TestCase -from tcms.testruns.models import Environment, TestRun - -User = get_user_model() # pylint: disable=invalid-name - - -class NewRunForm(forms.ModelForm): - class Meta: - model = TestRun - exclude = ("tag", "cc") # pylint: disable=modelform-uses-exclude - - manager = UserField() - default_tester = UserField(required=False) - start_date = DateTimeField(required=False) - stop_date = DateTimeField(required=False) - planned_start = DateTimeField(required=False) - planned_stop = DateTimeField(required=False) - - case = forms.ModelMultipleChoiceField( - queryset=TestCase.objects.none(), - required=False, - ) - - product = forms.ModelChoiceField( - queryset=Product.objects.all(), - required=False, - ) - - matrix_type = forms.ChoiceField( - choices=( - ("full", _("Full")), - ("pairwise", _("Pairwise")), - ), - required=False, - ) - - environment = forms.ModelMultipleChoiceField( - queryset=Environment.objects.all(), - required=False, - ) - - def populate(self, plan_id): - if plan_id: - # plan is ModelChoiceField which contains all the plans - # as we need only the plan for current run we filter the queryset - self.fields["plan"].queryset = self.fields["plan"].queryset.filter( - pk=plan_id - ) - self.fields["product"].queryset = Product.objects.filter( - pk=self.fields["plan"].queryset.first().product_id, - ) - self.fields["build"].queryset = Build.objects.filter( - version_id=self.fields["plan"].queryset.first().product_version_id, - is_active=True, - ) - else: - # these are dynamically filtered via JavaScript - self.fields["plan"].queryset = self.fields["plan"].queryset.none() - self.fields["build"].queryset = Build.objects.none() - - self.fields["case"].queryset = TestCase.objects.filter( - case_status__is_confirmed=True - ).all() - - -class SearchRunForm(forms.ModelForm): - class Meta: - model = TestRun - fields = "__all__" - - # overriden widget - manager = UserField() - default_tester = UserField() - - # extra fields - product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) - version = forms.ModelChoiceField(queryset=Version.objects.none(), required=False) - running = forms.IntegerField(required=False) - - def populate(self, product_id=None): - if product_id: - self.fields["version"].queryset = Version.objects.filter( - product__pk=product_id - ) - self.fields["build"].queryset = Build.objects.filter( - version__product=product_id - ) +# -*- coding: utf-8 -*- +from django import forms +from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ + +from tcms.core.forms.fields import UserField +from tcms.core.widgets import SimpleMDENotes +from tcms.management.models import Build, Product, Version +from tcms.rpc.api.forms import DateTimeField +from tcms.testcases.models import TestCase +from tcms.testruns.models import Environment, TestRun + +User = get_user_model() # pylint: disable=invalid-name + + +class NewRunForm(forms.ModelForm): + class Meta: + model = TestRun + exclude = ("tag", "cc") # pylint: disable=modelform-uses-exclude + + manager = UserField() + default_tester = UserField(required=False) + start_date = DateTimeField(required=False) + stop_date = DateTimeField(required=False) + planned_start = DateTimeField(required=False) + planned_stop = DateTimeField(required=False) + notes = forms.CharField( + widget=SimpleMDENotes(), + required=False, + ) + + case = forms.ModelMultipleChoiceField( + queryset=TestCase.objects.none(), + required=False, + ) + + product = forms.ModelChoiceField( + queryset=Product.objects.all(), + required=False, + ) + + matrix_type = forms.ChoiceField( + choices=( + ("full", _("Full")), + ("pairwise", _("Pairwise")), + ), + required=False, + ) + + environment = forms.ModelMultipleChoiceField( + queryset=Environment.objects.all(), + required=False, + ) + + def populate(self, plan_id): + if plan_id: + # plan is ModelChoiceField which contains all the plans + # as we need only the plan for current run we filter the queryset + self.fields["plan"].queryset = self.fields["plan"].queryset.filter( + pk=plan_id + ) + self.fields["product"].queryset = Product.objects.filter( + pk=self.fields["plan"].queryset.first().product_id, + ) + self.fields["build"].queryset = Build.objects.filter( + version_id=self.fields["plan"].queryset.first().product_version_id, + is_active=True, + ) + else: + # these are dynamically filtered via JavaScript + self.fields["plan"].queryset = self.fields["plan"].queryset.none() + self.fields["build"].queryset = Build.objects.none() + + self.fields["case"].queryset = TestCase.objects.filter( + case_status__is_confirmed=True + ).all() + + +class SearchRunForm(forms.ModelForm): + class Meta: + model = TestRun + fields = "__all__" + + # overriden widget + manager = UserField() + default_tester = UserField() + + # extra fields + product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) + version = forms.ModelChoiceField(queryset=Version.objects.none(), required=False) + running = forms.IntegerField(required=False) + + def populate(self, product_id=None): + if product_id: + self.fields["version"].queryset = Version.objects.filter( + product__pk=product_id + ) + self.fields["build"].queryset = Build.objects.filter( + version__product=product_id + ) diff --git a/tcms/testruns/templates/testruns/mutable.html b/tcms/testruns/templates/testruns/mutable.html index d215b3c919..15044b4aff 100644 --- a/tcms/testruns/templates/testruns/mutable.html +++ b/tcms/testruns/templates/testruns/mutable.html @@ -1,262 +1,262 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} - -{% block title %} - {% if object %} - {% trans "Edit TestRun" %} - {% elif is_cloning %} - {% trans "Clone TestRun" %} - {% else %} - {% trans "New Test Run" %} - {% endif %} -{% endblock %} - -{% block page_id %}page-testruns-mutable{% endblock %} - -{% block contents %} -
-
- {% csrf_token %} - -
- -
- - {{ form.summary.errors }} -
- - -
- - {{ form.manager.errors }} -
- - -
- - {{ form.default_tester.errors }} -
-
- -
-
- - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.product.errors }} -
- -
- - - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.plan.errors }} -
- -
- - - + -
- -
- - - {{ form.build.errors }} -
-
- -
- -
-
- - - - -
- {{ form.planned_start.errors }} -
- - -
-
- - - - - -
- {{ form.planned_stop.errors }} -
- - - - {% if object and object.stop_date %} - -
-
- - - - - -
- {{ form.stop_date.errors }} -
- {% else %} - - {% endif %} -
- - {% if test_cases %} -
- -
- - -

- - {% trans 'This is a tech-preview feature!' %} -

- - {{ form.environment.errors }} -
- -
- - - -
- -
- - -

- - - {% trans 'more information' %} - -

-
-
- {% endif %} - -
- -
- -
-
- -
-
- -
-
- - {% if test_cases %} - -
-
- {% trans "Selected TestCase(s):" %} - {% if disabled_cases %} - -{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! -See test plan for more details!{% endblocktrans %} - - {% endif %} -
- - - - - - - - - - - - - - {% for test_case in test_cases %} - - - - - - - - - {% endfor %} - -
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
- - - TC-{{ test_case.pk }}: {{ test_case.summary }} - - - {{ test_case.author.username }} - {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
-
- {% endif %} -
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} + {% if object %} + {% trans "Edit TestRun" %} + {% elif is_cloning %} + {% trans "Clone TestRun" %} + {% else %} + {% trans "New Test Run" %} + {% endif %} +{% endblock %} + +{% block page_id %}page-testruns-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + +
+ +
+ + {{ form.summary.errors }} +
+ + +
+ + {{ form.manager.errors }} +
+ + +
+ + {{ form.default_tester.errors }} +
+
+ +
+
+ + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.product.errors }} +
+ +
+ + + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.plan.errors }} +
+ +
+ + + + +
+ +
+ + + {{ form.build.errors }} +
+
+ +
+ +
+
+ + + + +
+ {{ form.planned_start.errors }} +
+ + +
+
+ + + + + +
+ {{ form.planned_stop.errors }} +
+ + + + {% if object and object.stop_date %} + +
+
+ + + + + +
+ {{ form.stop_date.errors }} +
+ {% else %} + + {% endif %} +
+ + {% if test_cases %} +
+ +
+ + +

+ + {% trans 'This is a tech-preview feature!' %} +

+ + {{ form.environment.errors }} +
+ +
+ + + +
+ +
+ + +

+ + + {% trans 'more information' %} + +

+
+
+ {% endif %} + +
+ +
+ {{ form.notes }} +
+
+ +
+
+ +
+
+ + {% if test_cases %} + +
+
+ {% trans "Selected TestCase(s):" %} + {% if disabled_cases %} + +{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! +See test plan for more details!{% endblocktrans %} + + {% endif %} +
+ + + + + + + + + + + + + + {% for test_case in test_cases %} + + + + + + + + + {% endfor %} + +
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
+ + + TC-{{ test_case.pk }}: {{ test_case.summary }} + + + {{ test_case.author.username }} + {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
+
+ {% endif %} +
+
+{% endblock %} From c99eccd2835dc33a57641c0adfa8294077098841 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:52:02 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcms/core/widgets.py | 122 +- tcms/static/js/index.js | 204 +-- tcms/testcases/forms.py | 270 +-- tcms/testcases/templates/testcases/get.html | 582 +++---- .../templates/testcases/mutable.html | 546 +++--- tcms/testplans/static/testplans/js/get.js | 1460 ++++++++--------- tcms/testruns/forms.py | 200 +-- tcms/testruns/templates/testruns/mutable.html | 524 +++--- 8 files changed, 1954 insertions(+), 1954 deletions(-) diff --git a/tcms/core/widgets.py b/tcms/core/widgets.py index ac0d4c2837..4a19710856 100644 --- a/tcms/core/widgets.py +++ b/tcms/core/widgets.py @@ -1,61 +1,61 @@ -# Copyright (c) 2018-2024 Kiwi TCMS project. All rights reserved. -# Author: Alexander Todorov - -""" -Custom widgets for Django -""" -from django import forms -from django.utils.dateparse import parse_duration -from django.utils.safestring import SafeString - - -class SimpleMDE(forms.Textarea): - """ - SimpleMDE widget for Django - """ - - file_upload_id = "simplemde-file-upload" - - def render(self, name, value, attrs=None, renderer=None): - # pylint: disable=objects-update-used - attrs.update( - { - "class": "js-simplemde-textarea", - "data-file-upload-id": self.file_upload_id, - } - ) - rendered_string = super().render(name, value, attrs, renderer) - rendered_string += SafeString( # nosec:B703:django_mark_safe - f""" - -""" - ) - return rendered_string - - class Media: - css = {"all": ["simplemde/dist/simplemde.min.css"]} - - -class SimpleMDENotes(SimpleMDE): - """ - SimpleMDE widget for notes field with unique file upload ID - """ - - file_upload_id = "simplemde-notes-file-upload" - - -class DurationWidget(forms.Widget): - template_name = "widgets/duration.html" - - def format_value(self, value): - if not value: - return 0 - - duration = parse_duration(value) - return int(duration.total_seconds()) - - class Media: - css = {"all": ["bootstrap-duration-picker/dist/bootstrap-duration-picker.css"]} - js = [ - "bootstrap-duration-picker/dist/bootstrap-duration-picker.js", - ] +# Copyright (c) 2018-2024 Kiwi TCMS project. All rights reserved. +# Author: Alexander Todorov + +""" +Custom widgets for Django +""" +from django import forms +from django.utils.dateparse import parse_duration +from django.utils.safestring import SafeString + + +class SimpleMDE(forms.Textarea): + """ + SimpleMDE widget for Django + """ + + file_upload_id = "simplemde-file-upload" + + def render(self, name, value, attrs=None, renderer=None): + # pylint: disable=objects-update-used + attrs.update( + { + "class": "js-simplemde-textarea", + "data-file-upload-id": self.file_upload_id, + } + ) + rendered_string = super().render(name, value, attrs, renderer) + rendered_string += SafeString( # nosec:B703:django_mark_safe + f""" + +""" + ) + return rendered_string + + class Media: + css = {"all": ["simplemde/dist/simplemde.min.css"]} + + +class SimpleMDENotes(SimpleMDE): + """ + SimpleMDE widget for notes field with unique file upload ID + """ + + file_upload_id = "simplemde-notes-file-upload" + + +class DurationWidget(forms.Widget): + template_name = "widgets/duration.html" + + def format_value(self, value): + if not value: + return 0 + + duration = parse_duration(value) + return int(duration.total_seconds()) + + class Media: + css = {"all": ["bootstrap-duration-picker/dist/bootstrap-duration-picker.css"]} + js = [ + "bootstrap-duration-picker/dist/bootstrap-duration-picker.js", + ] diff --git a/tcms/static/js/index.js b/tcms/static/js/index.js index c713b5271b..55f08ba255 100644 --- a/tcms/static/js/index.js +++ b/tcms/static/js/index.js @@ -1,102 +1,102 @@ -import { pageBugsGetReadyHandler } from '../../bugs/static/bugs/js/get' -import { pageBugsMutableReadyHandler } from '../../bugs/static/bugs/js/mutable' -import { pageBugsSearchReadyHandler } from '../../bugs/static/bugs/js/search' - -import { pageTestcasesGetReadyHandler } from '../../testcases/static/testcases/js/get' -import { pageTestcasesMutableReadyHandler } from '../../testcases/static/testcases/js/mutable' -import { pageTestcasesSearchReadyHandler } from '../../testcases/static/testcases/js/search' - -import { pageTestplansGetReadyHandler } from '../../testplans/static/testplans/js/get' -import { pageTestplansMutableReadyHandler } from '../../testplans/static/testplans/js/mutable' -import { pageTestplansSearchReadyHandler } from '../../testplans/static/testplans/js/search' - -import { pageTestrunsEnvironmentReadyHandler } from '../../testruns/static/testruns/js/environment' -import { pageTestrunsGetReadyHandler } from '../../testruns/static/testruns/js/get' -import { pageTestrunsMutableReadyHandler } from '../../testruns/static/testruns/js/mutable' -import { pageTestrunsSearchReadyHandler } from '../../testruns/static/testruns/js/search' - -import { pageManagementBuildAdminReadyHandler } from '../../management/static/management/js/build_admin' - -import { pageTelemetryReadyHandler } from '../../telemetry/static/telemetry/js/index' - -import { jsonRPC } from './jsonrpc' -import { initSimpleMDE } from './simplemde_security_override' - -function pageInitDBReadyHandler () { - $('.js-initialize-btn').click(function () { - $(this).button('loading') - }) -} - -const pageHandlers = { - 'page-bugs-get': pageBugsGetReadyHandler, - 'page-bugs-mutable': pageBugsMutableReadyHandler, - 'page-bugs-search': pageBugsSearchReadyHandler, - - 'page-init-db': pageInitDBReadyHandler, - - 'page-testcases-get': pageTestcasesGetReadyHandler, - 'page-testcases-mutable': pageTestcasesMutableReadyHandler, - 'page-testcases-search': pageTestcasesSearchReadyHandler, - - 'page-testplans-get': pageTestplansGetReadyHandler, - 'page-testplans-mutable': pageTestplansMutableReadyHandler, - 'page-testplans-search': pageTestplansSearchReadyHandler, - - 'page-testruns-environment': pageTestrunsEnvironmentReadyHandler, - 'page-testruns-get': pageTestrunsGetReadyHandler, - 'page-testruns-mutable': pageTestrunsMutableReadyHandler, - 'page-testruns-search': pageTestrunsSearchReadyHandler, - - 'page-telemetry-testing-breakdown': pageTelemetryReadyHandler, - 'page-telemetry-status-matrix': pageTelemetryReadyHandler, - 'page-telemetry-execution-dashboard': pageTelemetryReadyHandler, - 'page-telemetry-execution-trends': pageTelemetryReadyHandler, - 'page-telemetry-test-case-health': pageTelemetryReadyHandler -} - -$(() => { - const body = $('body') - const pageId = body.attr('id') - const readyFunc = pageHandlers[pageId] - if (readyFunc) { - readyFunc(pageId) - } - - // this page doesn't have a page id - if (body.hasClass('grp-change-form') && body.hasClass('management-build')) { - pageManagementBuildAdminReadyHandler() - } - - if ($('body').selectpicker) { - $('.selectpicker').selectpicker() - } - - if ($('body').bootstrapSwitch) { - $('.bootstrap-switch').bootstrapSwitch() - } - - if ($('body').tooltip) { - $('[data-toggle="tooltip"]').tooltip() - } - - $('.js-simplemde-textarea').each(function () { - const fileUploadId = $(this).data('file-upload-id') - const uploadField = $(`#${fileUploadId}`) - - // Use textarea id/name for unique autosave ID to prevent content sharing - const textareaId = $(this).attr('id') || $(this).attr('name') || 'default' - const autoSaveId = window.location.toString() + '#' + textareaId - - // this value is only used in testcases/js/mutable.js - window.markdownEditor = initSimpleMDE(this, uploadField, autoSaveId) - }) - - $('#logout_link').click(function () { - $('#logout_form').submit() - return false - }) - - // for debugging in browser - window.jsonRPC = jsonRPC -}) +import { pageBugsGetReadyHandler } from '../../bugs/static/bugs/js/get' +import { pageBugsMutableReadyHandler } from '../../bugs/static/bugs/js/mutable' +import { pageBugsSearchReadyHandler } from '../../bugs/static/bugs/js/search' + +import { pageTestcasesGetReadyHandler } from '../../testcases/static/testcases/js/get' +import { pageTestcasesMutableReadyHandler } from '../../testcases/static/testcases/js/mutable' +import { pageTestcasesSearchReadyHandler } from '../../testcases/static/testcases/js/search' + +import { pageTestplansGetReadyHandler } from '../../testplans/static/testplans/js/get' +import { pageTestplansMutableReadyHandler } from '../../testplans/static/testplans/js/mutable' +import { pageTestplansSearchReadyHandler } from '../../testplans/static/testplans/js/search' + +import { pageTestrunsEnvironmentReadyHandler } from '../../testruns/static/testruns/js/environment' +import { pageTestrunsGetReadyHandler } from '../../testruns/static/testruns/js/get' +import { pageTestrunsMutableReadyHandler } from '../../testruns/static/testruns/js/mutable' +import { pageTestrunsSearchReadyHandler } from '../../testruns/static/testruns/js/search' + +import { pageManagementBuildAdminReadyHandler } from '../../management/static/management/js/build_admin' + +import { pageTelemetryReadyHandler } from '../../telemetry/static/telemetry/js/index' + +import { jsonRPC } from './jsonrpc' +import { initSimpleMDE } from './simplemde_security_override' + +function pageInitDBReadyHandler () { + $('.js-initialize-btn').click(function () { + $(this).button('loading') + }) +} + +const pageHandlers = { + 'page-bugs-get': pageBugsGetReadyHandler, + 'page-bugs-mutable': pageBugsMutableReadyHandler, + 'page-bugs-search': pageBugsSearchReadyHandler, + + 'page-init-db': pageInitDBReadyHandler, + + 'page-testcases-get': pageTestcasesGetReadyHandler, + 'page-testcases-mutable': pageTestcasesMutableReadyHandler, + 'page-testcases-search': pageTestcasesSearchReadyHandler, + + 'page-testplans-get': pageTestplansGetReadyHandler, + 'page-testplans-mutable': pageTestplansMutableReadyHandler, + 'page-testplans-search': pageTestplansSearchReadyHandler, + + 'page-testruns-environment': pageTestrunsEnvironmentReadyHandler, + 'page-testruns-get': pageTestrunsGetReadyHandler, + 'page-testruns-mutable': pageTestrunsMutableReadyHandler, + 'page-testruns-search': pageTestrunsSearchReadyHandler, + + 'page-telemetry-testing-breakdown': pageTelemetryReadyHandler, + 'page-telemetry-status-matrix': pageTelemetryReadyHandler, + 'page-telemetry-execution-dashboard': pageTelemetryReadyHandler, + 'page-telemetry-execution-trends': pageTelemetryReadyHandler, + 'page-telemetry-test-case-health': pageTelemetryReadyHandler +} + +$(() => { + const body = $('body') + const pageId = body.attr('id') + const readyFunc = pageHandlers[pageId] + if (readyFunc) { + readyFunc(pageId) + } + + // this page doesn't have a page id + if (body.hasClass('grp-change-form') && body.hasClass('management-build')) { + pageManagementBuildAdminReadyHandler() + } + + if ($('body').selectpicker) { + $('.selectpicker').selectpicker() + } + + if ($('body').bootstrapSwitch) { + $('.bootstrap-switch').bootstrapSwitch() + } + + if ($('body').tooltip) { + $('[data-toggle="tooltip"]').tooltip() + } + + $('.js-simplemde-textarea').each(function () { + const fileUploadId = $(this).data('file-upload-id') + const uploadField = $(`#${fileUploadId}`) + + // Use textarea id/name for unique autosave ID to prevent content sharing + const textareaId = $(this).attr('id') || $(this).attr('name') || 'default' + const autoSaveId = window.location.toString() + '#' + textareaId + + // this value is only used in testcases/js/mutable.js + window.markdownEditor = initSimpleMDE(this, uploadField, autoSaveId) + }) + + $('#logout_link').click(function () { + $('#logout_form').submit() + return false + }) + + // for debugging in browser + window.jsonRPC = jsonRPC +}) diff --git a/tcms/testcases/forms.py b/tcms/testcases/forms.py index 93a162cf9f..54e4ce0a6a 100644 --- a/tcms/testcases/forms.py +++ b/tcms/testcases/forms.py @@ -1,135 +1,135 @@ -# -*- coding: utf-8 -*- -from django import forms -from django.forms import inlineformset_factory - -from tcms.core.forms.fields import UserField -from tcms.core.widgets import DurationWidget, SimpleMDE, SimpleMDENotes -from tcms.management.models import Component, Priority, Product -from tcms.testcases.fields import MultipleEmailField -from tcms.testcases.models import ( - Category, - TestCase, - TestCaseEmailSettings, - TestCaseStatus, -) -from tcms.testplans.models import TestPlan - - -class TestCaseForm(forms.ModelForm): - class Meta: - model = TestCase - exclude = [ # pylint: disable=modelform-uses-exclude - "reviewer", - "tag", - "component", - "plan", - ] - - default_tester = UserField(required=False) - priority = forms.ModelChoiceField( - queryset=Priority.objects.filter(is_active=True), - empty_label=None, - ) - product = forms.ModelChoiceField( - queryset=Product.objects.all(), - empty_label=None, - ) - setup_duration = forms.DurationField( - widget=DurationWidget(), - required=False, - ) - testing_duration = forms.DurationField( - widget=DurationWidget(), - required=False, - ) - notes = forms.CharField( - widget=SimpleMDENotes(), - required=False, - ) - text = forms.CharField( - widget=SimpleMDE(), - required=False, - ) - - def populate(self, product_id=None): - if product_id: - self.fields["category"].queryset = Category.objects.filter( - product_id=product_id - ) - else: - self.fields["category"].queryset = Category.objects.all() - - -# only useful b/c we want to override the cc_list field -class CaseNotifyForm(forms.ModelForm): - class Meta: - model = TestCaseEmailSettings - fields = "__all__" - - cc_list = MultipleEmailField(required=False) - - -# note: these fields can't change during runtime ! -_email_settings_fields = [] # pylint: disable=invalid-name -for field in TestCaseEmailSettings._meta.fields: - _email_settings_fields.append(field.name) - - -# for usage in CreateView, UpdateView -CaseNotifyFormSet = inlineformset_factory( # pylint: disable=invalid-name - TestCase, - TestCaseEmailSettings, - form=CaseNotifyForm, - fields=_email_settings_fields, - can_delete=False, - can_order=False, -) - - -class SearchCaseForm(forms.ModelForm): - class Meta: - model = TestCase - fields = "__all__" - - # overriden initial values - product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) - category = forms.ModelChoiceField(queryset=Category.objects.none(), required=False) - component = forms.ModelChoiceField( - queryset=Component.objects.none(), required=False - ) - - # overriden widgets - priority = forms.ModelMultipleChoiceField( - queryset=Priority.objects.filter(is_active=True), - widget=forms.CheckboxSelectMultiple(), - required=False, - ) - case_status = forms.ModelMultipleChoiceField( - queryset=TestCaseStatus.objects.all(), - widget=forms.CheckboxSelectMultiple(), - required=False, - ) - - def populate(self, product_id=None): - if product_id: - self.fields["category"].queryset = Category.objects.filter( - product_id=product_id - ) - self.fields["component"].queryset = Component.objects.filter( - product_id=product_id - ) - - -class CloneCaseForm(forms.Form): # pylint: disable=must-inherit-from-model-form - case = forms.ModelMultipleChoiceField( - queryset=TestCase.objects.all(), - ) - plan = forms.ModelMultipleChoiceField( - queryset=TestPlan.objects.all(), - required=False, - ) - - def populate(self, case_ids): - self.fields["case"].queryset = TestCase.objects.filter(pk__in=case_ids) - plan_ids = self.fields["case"].queryset.values_list("plan", flat=True) - self.fields["plan"].queryset = TestPlan.objects.filter(pk__in=plan_ids) +# -*- coding: utf-8 -*- +from django import forms +from django.forms import inlineformset_factory + +from tcms.core.forms.fields import UserField +from tcms.core.widgets import DurationWidget, SimpleMDE, SimpleMDENotes +from tcms.management.models import Component, Priority, Product +from tcms.testcases.fields import MultipleEmailField +from tcms.testcases.models import ( + Category, + TestCase, + TestCaseEmailSettings, + TestCaseStatus, +) +from tcms.testplans.models import TestPlan + + +class TestCaseForm(forms.ModelForm): + class Meta: + model = TestCase + exclude = [ # pylint: disable=modelform-uses-exclude + "reviewer", + "tag", + "component", + "plan", + ] + + default_tester = UserField(required=False) + priority = forms.ModelChoiceField( + queryset=Priority.objects.filter(is_active=True), + empty_label=None, + ) + product = forms.ModelChoiceField( + queryset=Product.objects.all(), + empty_label=None, + ) + setup_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + testing_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + notes = forms.CharField( + widget=SimpleMDENotes(), + required=False, + ) + text = forms.CharField( + widget=SimpleMDE(), + required=False, + ) + + def populate(self, product_id=None): + if product_id: + self.fields["category"].queryset = Category.objects.filter( + product_id=product_id + ) + else: + self.fields["category"].queryset = Category.objects.all() + + +# only useful b/c we want to override the cc_list field +class CaseNotifyForm(forms.ModelForm): + class Meta: + model = TestCaseEmailSettings + fields = "__all__" + + cc_list = MultipleEmailField(required=False) + + +# note: these fields can't change during runtime ! +_email_settings_fields = [] # pylint: disable=invalid-name +for field in TestCaseEmailSettings._meta.fields: + _email_settings_fields.append(field.name) + + +# for usage in CreateView, UpdateView +CaseNotifyFormSet = inlineformset_factory( # pylint: disable=invalid-name + TestCase, + TestCaseEmailSettings, + form=CaseNotifyForm, + fields=_email_settings_fields, + can_delete=False, + can_order=False, +) + + +class SearchCaseForm(forms.ModelForm): + class Meta: + model = TestCase + fields = "__all__" + + # overriden initial values + product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) + category = forms.ModelChoiceField(queryset=Category.objects.none(), required=False) + component = forms.ModelChoiceField( + queryset=Component.objects.none(), required=False + ) + + # overriden widgets + priority = forms.ModelMultipleChoiceField( + queryset=Priority.objects.filter(is_active=True), + widget=forms.CheckboxSelectMultiple(), + required=False, + ) + case_status = forms.ModelMultipleChoiceField( + queryset=TestCaseStatus.objects.all(), + widget=forms.CheckboxSelectMultiple(), + required=False, + ) + + def populate(self, product_id=None): + if product_id: + self.fields["category"].queryset = Category.objects.filter( + product_id=product_id + ) + self.fields["component"].queryset = Component.objects.filter( + product_id=product_id + ) + + +class CloneCaseForm(forms.Form): # pylint: disable=must-inherit-from-model-form + case = forms.ModelMultipleChoiceField( + queryset=TestCase.objects.all(), + ) + plan = forms.ModelMultipleChoiceField( + queryset=TestPlan.objects.all(), + required=False, + ) + + def populate(self, case_ids): + self.fields["case"].queryset = TestCase.objects.filter(pk__in=case_ids) + plan_ids = self.fields["case"].queryset.values_list("plan", flat=True) + self.fields["plan"].queryset = TestPlan.objects.filter(pk__in=plan_ids) diff --git a/tcms/testcases/templates/testcases/get.html b/tcms/testcases/templates/testcases/get.html index e1677626f1..8f35b356ae 100644 --- a/tcms/testcases/templates/testcases/get.html +++ b/tcms/testcases/templates/testcases/get.html @@ -1,291 +1,291 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} -{% load comments %} -{% load extra_filters %} - -{% block title %}TC-{{ object.pk }}: {{ object.summary }}{% endblock %} -{% block page_id %}page-testcases-get{% endblock %} -{% block body_class %}cards-pf{% endblock %} - -{% block breadcrumbs %} - -{% endblock %} - -{% block contents %} -
- -

- TC-{{ object.pk }}: {{ object.summary }} -

- - - - - -
-
-
-

- {% trans 'Author' %}: - {{ object.author.username }} -

- -

- {% trans 'Default tester' %}: - {% if object.default_tester %} - {{ object.default_tester.username }} - {% else %} - - - {% endif %} -

- -

- {% trans 'Product' %}: - {{ object.category.product }} -

- -

- {% trans 'Category' %}: - {{ object.category }} -

- -

- {% trans 'Status' %}: - {{ object.case_status }} -

- -

- {% trans 'Priority' %}: - {{ object.priority }} -

- -

- {{ object.create_date }} -

- -

- {% trans 'Setup duration' %}: - {{ object.setup_duration|default:"-" }} -

- -

- {% trans 'Testing duration' %}: - {{ object.testing_duration|default:"-" }} -

- -

- {% trans 'Expected duration' %}: - {{ object.expected_duration }} -

- -

- {% trans 'Automated' %}: - {{ object.is_automated }} -

- -

- {% trans 'Script' %}: - {{ object.script|default:'-' }} -

- -

- {% trans 'Arguments' %}: - {{ object.arguments|default:'-' }} -

- -

- {% trans 'Requirements' %}: - {{ object.requirement|default:'-' }} -

- -

- {% trans 'Reference link' %}: - {% if object.extra_link %} - {{ object.extra_link }} - {% else %} - - - {% endif %} -

- -
-
-
- -
-
-
-
- {{ object.text|markdown2html }} -
- -

- {% trans 'Notes' %}: - {{ object.notes|markdown2html }} -

-
-
-
- -
- {% include "include/properties_card.html" %} -
-
- -
-
- {% include 'include/tc_executions.html' with show_bugs=True %} -
-
- -
- {% trans "Bugs" as bugs_heading %} - {% include "include/bugs_table.html" with heading=bugs_heading class="bugs" %} - -
-
-

- - {% trans 'Test plans' %} -

- -
- - - - - - - - - - - - - {% if perms.testcases.add_testcaseplan %} - - - - - - - {% endif %} - -
{% trans 'ID' %}{% trans 'Name' %}{% trans 'Author' %}{% trans 'Type' %}{% trans 'Product' %}
-
- -
-
- - - -
-
-
-
- -
- -
-
- {% include 'include/tags_card.html' with add_perm=perms.testcases.add_testcasetag %} -
- -
-
-

- - {% trans 'Components' %} -

- -
- - - - - - - - - {% if perms.testcases.add_testcasecomponent %} - - - - - - - {% endif %} -
{% trans 'Name' %}
-
- -
-
- - - -
-
-
-
- -
- {% include 'include/attachments.html' %} -
- -
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load comments %} +{% load extra_filters %} + +{% block title %}TC-{{ object.pk }}: {{ object.summary }}{% endblock %} +{% block page_id %}page-testcases-get{% endblock %} +{% block body_class %}cards-pf{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block contents %} +
+ +

+ TC-{{ object.pk }}: {{ object.summary }} +

+ + + + + +
+
+
+

+ {% trans 'Author' %}: + {{ object.author.username }} +

+ +

+ {% trans 'Default tester' %}: + {% if object.default_tester %} + {{ object.default_tester.username }} + {% else %} + - + {% endif %} +

+ +

+ {% trans 'Product' %}: + {{ object.category.product }} +

+ +

+ {% trans 'Category' %}: + {{ object.category }} +

+ +

+ {% trans 'Status' %}: + {{ object.case_status }} +

+ +

+ {% trans 'Priority' %}: + {{ object.priority }} +

+ +

+ {{ object.create_date }} +

+ +

+ {% trans 'Setup duration' %}: + {{ object.setup_duration|default:"-" }} +

+ +

+ {% trans 'Testing duration' %}: + {{ object.testing_duration|default:"-" }} +

+ +

+ {% trans 'Expected duration' %}: + {{ object.expected_duration }} +

+ +

+ {% trans 'Automated' %}: + {{ object.is_automated }} +

+ +

+ {% trans 'Script' %}: + {{ object.script|default:'-' }} +

+ +

+ {% trans 'Arguments' %}: + {{ object.arguments|default:'-' }} +

+ +

+ {% trans 'Requirements' %}: + {{ object.requirement|default:'-' }} +

+ +

+ {% trans 'Reference link' %}: + {% if object.extra_link %} + {{ object.extra_link }} + {% else %} + - + {% endif %} +

+ +
+
+
+ +
+
+
+
+ {{ object.text|markdown2html }} +
+ +

+ {% trans 'Notes' %}: + {{ object.notes|markdown2html }} +

+
+
+
+ +
+ {% include "include/properties_card.html" %} +
+
+ +
+
+ {% include 'include/tc_executions.html' with show_bugs=True %} +
+
+ +
+ {% trans "Bugs" as bugs_heading %} + {% include "include/bugs_table.html" with heading=bugs_heading class="bugs" %} + +
+
+

+ + {% trans 'Test plans' %} +

+ +
+ + + + + + + + + + + + + {% if perms.testcases.add_testcaseplan %} + + + + + + + {% endif %} + +
{% trans 'ID' %}{% trans 'Name' %}{% trans 'Author' %}{% trans 'Type' %}{% trans 'Product' %}
+
+ +
+
+ + + +
+
+
+
+ +
+ +
+
+ {% include 'include/tags_card.html' with add_perm=perms.testcases.add_testcasetag %} +
+ +
+
+

+ + {% trans 'Components' %} +

+ +
+ + + + + + + + + {% if perms.testcases.add_testcasecomponent %} + + + + + + + {% endif %} +
{% trans 'Name' %}
+
+ +
+
+ + + +
+
+
+
+ +
+ {% include 'include/attachments.html' %} +
+ +
+
+{% endblock %} diff --git a/tcms/testcases/templates/testcases/mutable.html b/tcms/testcases/templates/testcases/mutable.html index 64c66b7f11..ad95304f00 100644 --- a/tcms/testcases/templates/testcases/mutable.html +++ b/tcms/testcases/templates/testcases/mutable.html @@ -1,273 +1,273 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} - -{% block head %} - {{ form.media }} -{% endblock %} -{% block title %} - {% if object %} - {% trans "Edit TestCase" %} - {% else %} - {% trans "New Test Case" %} - {% endif %} -{% endblock %} - -{% block page_id %}page-testcases-mutable{% endblock %} - -{% block contents %} -
-
- {% csrf_token %} - {% if request.GET.from_plan %} - - {% endif %} - -
- -
- - {% if test_plan %} -

TP-{{ test_plan.pk }}: {{ test_plan.name }}

- - {% endif %} - {{ form.summary.errors }} -
-
- -
- -
- - {{ form.default_tester.errors }} -
- -
- - + -
-
- - {{ form.product.errors }} -
- -
- - + -
-
- - {{ form.category.errors }} -
-
- -
- -
- - {{ form.case_status.errors }} -
- - -
- - {{ form.priority.errors }} -
- - -
- -
-
- -
- {% if not object %} -
- - + -
-
- -
- {% endif %} - - -
-
- {{ form.setup_duration }} -
-
- - -
-
- {{ form.testing_duration }} -
-
-
- -
-
-
{{ form.text }}
- {{ form.text.errors }} -
-
- -
- -
- - {{ form.script.errors }} -
- - -
- - {{ form.arguments.errors }} -
-
- -
- -
- - {{ form.requirement.errors }} -
- -
- - {{ form.extra_link.errors }} -
-
- -
- -
- {{ form.notes }} - {{ form.notes.errors }} -
-
- - {% for notify_form in notify_formset %} -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- - {{ notify_form.cc_list.errors }} -

{% trans "Email addresses separated by comma. A notification email will be sent to each Email address within CC list." %}

-
-
- - {% for hidden_field in notify_form.hidden_fields %} - {{ hidden_field }} - {% endfor %} - {% endfor %} - - {{ notify_formset.management_form }} - -
-
- {% trans "Cancel" %} - -
-
-
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block head %} + {{ form.media }} +{% endblock %} +{% block title %} + {% if object %} + {% trans "Edit TestCase" %} + {% else %} + {% trans "New Test Case" %} + {% endif %} +{% endblock %} + +{% block page_id %}page-testcases-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + {% if request.GET.from_plan %} + + {% endif %} + +
+ +
+ + {% if test_plan %} +

TP-{{ test_plan.pk }}: {{ test_plan.name }}

+ + {% endif %} + {{ form.summary.errors }} +
+
+ +
+ +
+ + {{ form.default_tester.errors }} +
+ +
+ + + +
+
+ + {{ form.product.errors }} +
+ +
+ + + +
+
+ + {{ form.category.errors }} +
+
+ +
+ +
+ + {{ form.case_status.errors }} +
+ + +
+ + {{ form.priority.errors }} +
+ + +
+ +
+
+ +
+ {% if not object %} +
+ + + +
+
+ +
+ {% endif %} + + +
+
+ {{ form.setup_duration }} +
+
+ + +
+
+ {{ form.testing_duration }} +
+
+
+ +
+
+
{{ form.text }}
+ {{ form.text.errors }} +
+
+ +
+ +
+ + {{ form.script.errors }} +
+ + +
+ + {{ form.arguments.errors }} +
+
+ +
+ +
+ + {{ form.requirement.errors }} +
+ +
+ + {{ form.extra_link.errors }} +
+
+ +
+ +
+ {{ form.notes }} + {{ form.notes.errors }} +
+
+ + {% for notify_form in notify_formset %} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ + {{ notify_form.cc_list.errors }} +

{% trans "Email addresses separated by comma. A notification email will be sent to each Email address within CC list." %}

+
+
+ + {% for hidden_field in notify_form.hidden_fields %} + {{ hidden_field }} + {% endfor %} + {% endfor %} + + {{ notify_formset.management_form }} + +
+
+ {% trans "Cancel" %} + +
+
+
+
+{% endblock %} diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index f7b3ce6220..ccb83b7e7f 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -1,730 +1,730 @@ -import { jsonRPC } from '../../../../static/js/jsonrpc' -import { tagsCard } from '../../../../static/js/tags' -import { - animate, - advancedSearchAndAddTestCases, - bindDeleteCommentButton, changeDropdownSelectedItem, - markdown2HTML, renderCommentsForObject, renderCommentHTML, - treeViewBind, quickSearchAndAddTestCase, - findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData, - showOrHideMultipleRows -} from '../../../../static/js/utils' -import { initSimpleMDE } from '../../../../static/js/simplemde_security_override' - -const expandedTestCaseIds = [] -const fadeAnimationTime = 500 - -const allTestCases = {} -const autocompleteCache = {} - -const confirmedStatuses = [] - -export function pageTestplansGetReadyHandler () { - const testPlanDataElement = $('#test_plan_pk') - const testPlanId = testPlanDataElement.data('testplan-pk') - - const permissions = { - 'perm-change-testcase': testPlanDataElement.data('perm-change-testcase') === 'True', - 'perm-remove-testcase': testPlanDataElement.data('perm-remove-testcase') === 'True', - 'perm-add-testcase': testPlanDataElement.data('perm-add-testcase') === 'True', - 'perm-add-comment': testPlanDataElement.data('perm-add-comment') === 'True', - 'perm-delete-comment': testPlanDataElement.data('perm-delete-comment') === 'True' - } - - // bind everything in tags table - const permRemoveTag = testPlanDataElement.data('perm-remove-tag') === 'True' - tagsCard('TestPlan', testPlanId, { plan: testPlanId }, permRemoveTag) - - jsonRPC('TestCaseStatus.filter', { is_confirmed: true }, function (statuses) { - // save for later use - for (let i = 0; i < statuses.length; i++) { - confirmedStatuses.push(statuses[i].id) - } - - jsonRPC('TestCase.sortkeys', { plan: testPlanId }, function (sortkeys) { - jsonRPC('TestCase.filter', { plan: testPlanId }, function (data) { - for (let i = 0; i < data.length; i++) { - const testCase = data[i] - - testCase.sortkey = sortkeys[testCase.id] - allTestCases[testCase.id] = testCase - } - sortTestCases(Object.values(allTestCases), testPlanId, permissions, 'sortkey') - - // drag & reorder needs the initial order of test cases and - // they may not be fully loaded when sortable() is initialized! - toolbarEvents(testPlanId, permissions) - }) - }) - }) - - adjustTestPlanFamilyTree() - collapseDocumentText() - quickSearchAndAddTestCase(testPlanId, addTestCaseToPlan, autocompleteCache) - $('#btn-search-cases').click(function () { - return advancedSearchAndAddTestCases( - testPlanId, 'TestPlan.add_case', $(this).attr('href'), - $('#test_plan_pk').data('trans-error-adding-cases') - ) - }) -} - -function addTestCaseToPlan (planId) { - const caseName = $('#search-testcase')[0].value - const testCase = autocompleteCache[caseName] - - // test case is already present so don't add it - if (allTestCases[testCase.id]) { - $('#search-testcase').val('') - return false - } - - jsonRPC('TestPlan.add_case', [planId, testCase.id], function (result) { - // IMPORTANT: the API result includes a 'sortkey' field value! - window.location.reload(true) - - // TODO: remove the page reload above and add the new case to the list - // NB: pay attention to drawTestCases() & treeViewBind() - // NB: also add to allTestCases !!! - - $('#search-testcase').val('') - }) -} - -function collapseDocumentText () { - // for some reason .height() reports a much higher value than - // reality and the 59% reduction seems to work nicely - const infoCardHeight = 0.59 * $('#testplan-info').height() - - if ($('#testplan-text').height() > infoCardHeight) { - $('#testplan-text-collapse-btn').removeClass('hidden') - - $('#testplan-text').css('min-height', infoCardHeight) - $('#testplan-text').css('height', infoCardHeight) - $('#testplan-text').css('overflow', 'hidden') - - $('#testplan-text').on('hidden.bs.collapse', function () { - $('#testplan-text').removeClass('collapse').css({ - height: infoCardHeight - }) - }) - } -} - -function adjustTestPlanFamilyTree () { - treeViewBind('#test-plan-family-tree') - - // remove the > arrows from elements which don't have children - $('#test-plan-family-tree').find('.list-group-item-container').each(function (index, element) { - if (!element.innerHTML.trim()) { - const span = $(element).siblings('.list-group-item-header').find('.list-view-pf-left span') - - span.removeClass('fa-angle-right') - // this is the exact same width so rows are still aligned - span.attr('style', 'width:9px') - } - }) - - // expand all parent elements so that the current one is visible - $('#test-plan-family-tree').find('.list-group-item.active').each(function (index, element) { - $(element).parents('.list-group-item-container').each(function (idx, container) { - $(container).toggleClass('hidden') - $(container).siblings('.list-group-item-header').find('.fa-angle-right').toggleClass('fa-angle-down') - }) - }) -} - -function drawTestCases (testCases, testPlanId, permissions) { - const container = $('#testcases-list') - const noCasesTemplate = $('#no_test_cases') - const testCaseRowDocumentFragment = $('#test_case_row')[0].content - - if (testCases.length > 0) { - testCases.forEach(function (element) { - container.append(getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), element, permissions, testPlanId)) - }) - attachEvents(testPlanId, permissions) - } else { - container.append(noCasesTemplate[0].innerHTML) - } - - $('.test-cases-count').html(testCases.length) -} - -function redrawSingleRow (testCaseId, testPlanId, permissions) { - const testCaseRowDocumentFragment = $('#test_case_row')[0].content - const newRow = getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), allTestCases[testCaseId], permissions, testPlanId) - - // remove from expanded list b/c the comment section may have changed - expandedTestCaseIds.splice(expandedTestCaseIds.indexOf(testCaseId), 1) - - // replace the element in the dom - $(`[data-testcase-pk=${testCaseId}]`).replaceWith(newRow) - attachEvents(testPlanId, permissions) -} - -function getTestCaseRowContent (rowContent, testCase, permissions, testPlanId) { - const row = $(rowContent) - - row[0].firstElementChild.dataset.testcasePk = testCase.id - row.find('.js-test-case-link').html(`TC-${testCase.id}: ${testCase.summary}`).attr('href', `/case/${testCase.id}/?from_plan=${testPlanId}`) - // todo: TestCaseStatus here isn't translated b/c TestCase.filter uses a - // custom serializer which needs to be refactored as well - row.find('.js-test-case-status').html(`${testCase.case_status__name}`) - row.find('.js-test-case-priority').html(`${testCase.priority__value}`) - row.find('.js-test-case-category').html(`${testCase.category__name}`) - row.find('.js-test-case-author').html(`${testCase.author__username}`) - row.find('.js-test-case-tester').html(`${testCase.default_tester__username || '-'}`) - row.find('.js-test-case-reviewer').html(`${testCase.reviewer__username || '-'}`) - - // set the links in the kebab menu - if (permissions['perm-change-testcase']) { - row.find('.js-test-case-menu-edit')[0].href = `/case/${testCase.id}/edit/?from_plan=${testPlanId}` - } - - if (permissions['perm-add-testcase']) { - row.find('.js-test-case-menu-clone')[0].href = `/cases/clone/?c=${testCase.id}` - } - - // apply visual separation between confirmed and not confirmed - - if (!isTestCaseConfirmed(testCase.case_status)) { - row.find('.list-group-item-header').addClass('bg-danger') - - // add customizable icon as part of #1932 - row.find('.js-test-case-status-icon').addClass('fa-times') - - row.find('.js-test-case-tester-div').toggleClass('hidden') - row.find('.js-test-case-reviewer-div').toggleClass('hidden') - } else { - row.find('.js-test-case-status-icon').addClass('fa-check-square') - } - - // handle automated icon - const automationIndicationElement = row.find('.js-test-case-automated') - let automatedClassToRemove = 'fa-cog' - - if (testCase.is_automated) { - automatedClassToRemove = 'fa-hand-paper-o' - } - - automationIndicationElement.parent().attr( - 'title', - automationIndicationElement.data(testCase.is_automated.toString()) - ) - automationIndicationElement.removeClass(automatedClassToRemove) - - // produce unique IDs for comments textarea and file upload fields - row.find('textarea')[0].id = `comment-for-testcase-${testCase.id}` - row.find('input[type="file"]')[0].id = `file-upload-for-testcase-${testCase.id}` - - return row -} - -function getTestCaseExpandArea (row, testCase, permissions) { - markdown2HTML(testCase.text, row.find('.js-test-case-expand-text')) - if (testCase.notes.trim().length > 0) { - markdown2HTML(testCase.notes, row.find('.js-test-case-expand-notes')) - } - - // draw the attachments - const uniqueDivCustomId = `js-tc-id-${testCase.id}-attachments` - // set unique identifier so we know where to draw fetched data - row.find('.js-test-case-expand-attachments').parent()[0].id = uniqueDivCustomId - - jsonRPC('TestCase.list_attachments', [testCase.id], function (data) { - // cannot use instance of row in the callback - const ulElement = $(`#${uniqueDivCustomId} .js-test-case-expand-attachments`) - - if (data.length === 0) { - ulElement.children().removeClass('hidden') - return - } - - const liElementFragment = $('#attachments-list-item')[0].content - - for (let i = 0; i < data.length; i++) { - // should create new element for every attachment - const liElement = liElementFragment.cloneNode(true) - const attachmentLink = $(liElement).find('a')[0] - - attachmentLink.href = data[i].url - attachmentLink.innerText = data[i].url.split('/').slice(-1)[0] - ulElement.append(liElement) - } - }) - - // load components - const componentTemplate = row.find('.js-testcase-expand-components').find('template')[0].content - jsonRPC('Component.filter', { cases: testCase.id }, function (result) { - result.forEach(function (element) { - const newComponent = componentTemplate.cloneNode(true) - $(newComponent).find('span').html(element.name) - row.find('.js-testcase-expand-components').append(newComponent) - }) - }) - - // load tags - const tagTemplate = row.find('.js-testcase-expand-tags').find('template')[0].content - jsonRPC('Tag.filter', { case: testCase.id }, function (result) { - const uniqueTags = [] - - result.forEach(function (element) { - if (uniqueTags.indexOf(element.name) === -1) { - uniqueTags.push(element.name) - - const newTag = tagTemplate.cloneNode(true) - $(newTag).find('span').html(element.name) - row.find('.js-testcase-expand-tags').append(newTag) - } - }) - }) - - // render previous comments - renderCommentsForObject( - testCase.id, - 'TestCase.comments', - 'TestCase.remove_comment', - !isTestCaseConfirmed(testCase.case_status) && permissions['perm-delete-comment'], - row.find('.comments') - ) - - // render comments form - const commentFormTextArea = row.find('.js-comment-form-textarea') - if (!isTestCaseConfirmed(testCase.case_status) && permissions['perm-add-comment']) { - const textArea = row.find('textarea')[0] - const fileUpload = row.find('input[type="file"]') - const editor = initSimpleMDE(textArea, $(fileUpload), textArea.id) - - row.find('.js-post-comment').click(function (event) { - event.preventDefault() - const input = editor.value().trim() - - if (input) { - jsonRPC('TestCase.add_comment', [testCase.id, input], comment => { - editor.value('') - - // show the newly added comment and bind its delete button - row.find('.comments').append( - renderCommentHTML( - 1 + row.find('.js-comment-container').length, - comment, - $('template#comment-template')[0], - function (parentNode) { - bindDeleteCommentButton( - testCase.id, - 'TestCase.remove_comment', - permissions['perm-delete-comment'], // b/c we already know it's unconfirmed - parentNode) - }) - ) - }) - } - }) - } else { - commentFormTextArea.hide() - } -} - -function attachEvents (testPlanId, permissions) { - treeViewBind('#testcases-list') - - if (permissions['perm-change-testcase']) { - // update default tester - $('.js-test-case-menu-tester').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - - const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) - if (!emailOrUsername) { - return false - } - - updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { default_tester: emailOrUsername }, - testPlanId, permissions) - - return false - }) - - $('.js-test-case-menu-priority').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - - updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { priority: ev.target.dataset.id }, - testPlanId, permissions) - return false - }) - - $('.js-test-case-menu-status').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const testCaseId = getCaseIdFromEvent(ev) - updateTestCasesViaAPI([testCaseId], { case_status: ev.target.dataset.id }, - testPlanId, permissions) - return false - }) - } - - if (permissions['perm-remove-testcase']) { - // delete testcase from the plan - $('.js-test-case-menu-delete').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const testCaseId = getCaseIdFromEvent(ev) - - jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { - delete allTestCases[testCaseId] - - // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change - $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { - $(this).remove() - }) - - const testCasesCountEl = $('.test-cases-count') - const count = parseInt(testCasesCountEl[0].innerText) - testCasesCountEl.html(count - 1) - }) - - return false - }) - } - - // get details and draw expand area only on expand - $('.js-testcase-row').click(function (ev) { - // don't trigger row expansion when kebab menu is clicked - if ($(ev.target).is('button, a, input, .fa-ellipsis-v')) { - return - } - - const testCaseId = getCaseIdFromEvent(ev) - - // tc was expanded once, dom is ready - if (expandedTestCaseIds.indexOf(testCaseId) > -1) { - return - } - - const tcRow = $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`) - expandedTestCaseIds.push(testCaseId) - getTestCaseExpandArea(tcRow, allTestCases[testCaseId], permissions) - }) - - const inputs = $('.js-testcase-row').find('input') - inputs.click(function (ev) { - // stop trigerring row.click() - ev.stopPropagation() - const checkbox = $('.js-checkbox-toolbar')[0] - - inputs.each(function (index, tc) { - checkbox.checked = tc.checked - - if (!checkbox.checked) { - return false - } - }) - }) - - function getCaseIdFromEvent (ev) { - return $(ev.target).closest('.js-testcase-row').data('testcase-pk') - } -} - -function updateTestCasesViaAPI (testCaseIds, updateQuery, testPlanId, permissions) { - testCaseIds.forEach(function (caseId) { - jsonRPC('TestCase.update', [caseId, updateQuery], function (updatedTC) { - const testCaseRow = $(`.js-testcase-row[data-testcase-pk=${caseId}]`) - - // update internal data - const sortkey = allTestCases[caseId].sortkey - allTestCases[caseId] = updatedTC - // note: updatedTC doesn't have sortkey information - allTestCases[caseId].sortkey = sortkey - - animate(testCaseRow, function () { - redrawSingleRow(caseId, testPlanId, permissions) - }) - }) - }) -} - -function toolbarEvents (testPlanId, permissions) { - $('.js-checkbox-toolbar').click(function (ev) { - const isChecked = ev.target.checked - const testCaseRows = $('.js-testcase-row').find('input') - - testCaseRows.each(function (index, tc) { - tc.checked = isChecked - }) - }) - - $('.js-toolbar-filter-options li').click(function (ev) { - return changeDropdownSelectedItem( - '.js-toolbar-filter-options', - '#input-filter-button', - ev.target, - $('#toolbar-filter') - ) - }) - - $('#toolbar-filter').on('keyup', function () { - const filterValue = $(this).val().toLowerCase() - const filterBy = $('.js-toolbar-filter-options .selected')[0].dataset.filterType - - filterTestCasesByProperty( - testPlanId, - Object.values(allTestCases), - filterBy, - filterValue - ) - }) - - $('.js-toolbar-sort-options li').click(function (ev) { - changeDropdownSelectedItem('.js-toolbar-sort-options', '#sort-button', ev.target) - - sortTestCases(Object.values(allTestCases), testPlanId, permissions) - return false - }) - - // handle asc desc icon - $('.js-toolbar-sorting-order > span').click(function (ev) { - const icon = $(this) - - icon.siblings('.hidden').removeClass('hidden') - icon.addClass('hidden') - - sortTestCases(Object.values(allTestCases), testPlanId, permissions) - }) - - // always initialize the sortable list however you can only - // move items using the handle icon on the left which becomes - // visible only when the manual sorting button is clicked - sortable('#testcases-list', { - handle: '.handle', - itemSerializer: (serializedItem, sortableContainer) => { - return parseInt(serializedItem.node.getAttribute('data-testcase-pk')) - } - }) - - // IMPORTANT: this is not empty b/c sortable() is initialized *after* - // all of the test cases have been rendered !!! - const initialOrder = sortable('#testcases-list', 'serialize')[0].items - - $('.js-toolbar-manual-sort').click(function (event) { - $(this).blur() - $('.js-toolbar-manual-sort').find('span').toggleClass(['fa-sort', 'fa-check-square']) - $('.js-testcase-sort-handler, .js-testcase-expand-arrow, .js-testcase-checkbox').toggleClass('hidden') - - const currentOrder = sortable('#testcases-list', 'serialize')[0].items - - // rows have been rearranged and the results must be committed to the DB - if (currentOrder.join() !== initialOrder.join()) { - currentOrder.forEach(function (tcPk, index) { - jsonRPC('TestPlan.update_case_order', [testPlanId, tcPk, index * 10], function (result) {}) - }) - } - }) - - $('.js-toolbar-priority').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - updateTestCasesViaAPI(selectedCases, { priority: ev.target.dataset.id }, - testPlanId, permissions) - - return false - }) - - $('.js-toolbar-status').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - updateTestCasesViaAPI(selectedCases, { case_status: ev.target.dataset.id }, - testPlanId, permissions) - return false - }) - - $('#default-tester-button').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) - - if (!emailOrUsername) { - return false - } - - updateTestCasesViaAPI(selectedCases, { default_tester: emailOrUsername }, - testPlanId, permissions) - - return false - }) - - $('#bulk-reviewer-button').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) - - if (!emailOrUsername) { - return false - } - - updateTestCasesViaAPI(selectedCases, { reviewer: emailOrUsername }, - testPlanId, permissions) - - return false - }) - - $('#delete_button').click(function (ev) { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - const areYouSureText = $('#test_plan_pk').data('trans-are-you-sure') - if (confirm(areYouSureText)) { - for (let i = 0; i < selectedCases.length; i++) { - const testCaseId = selectedCases[i] - jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { - delete allTestCases[testCaseId] - - // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change - $(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { - $(this).remove() - }) - }) - } - - const testCasesCountEl = $('.test-cases-count') - const count = parseInt(testCasesCountEl[0].innerText) - testCasesCountEl.html(count - selectedCases.length) - } - - return false - }) - - $('#bulk-clone-button').click(function () { - $(this).parents('.dropdown').toggleClass('open') - const selectedCases = getSelectedTestCases() - - if (!selectedCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - window.location.assign(`/cases/clone?c=${selectedCases.join('&c=')}`) - }) - - $('#testplan-toolbar-newrun').click(function () { - $(this).parents('.dropdown').toggleClass('open') - const selectedTestCases = getSelectedTestCases() - - if (!selectedTestCases.length) { - alert($('#test_plan_pk').data('trans-no-testcases-selected')) - return false - } - - for (let i = 0; i < selectedTestCases.length; i++) { - const status = allTestCases[selectedTestCases[i]].case_status - if (!isTestCaseConfirmed(status)) { - alert($('#test_plan_pk').data('trans-cannot-create-testrun')) - return false - } - } - - const newTestRunUrl = $('#test_plan_pk').data('new-testrun-url') - window.location.assign(`${newTestRunUrl}?c=${selectedTestCases.join('&c=')}`) - return false - }) -} - -function isTestCaseConfirmed (status) { - return confirmedStatuses.indexOf(Number(status)) > -1 -} - -function sortTestCases (testCases, testPlanId, permissions, defaultSortBy = undefined) { - const sortBy = defaultSortBy || $('.js-toolbar-sort-options .selected')[0].dataset.filterType - const sortOrder = $('.js-toolbar-sorting-order > span:not(.hidden)').data('order') - - $('#testcases-list').html('') - - testCases.sort(function (tc1, tc2) { - const value1 = tc1[sortBy] || '' - const value2 = tc2[sortBy] || '' - - if (Number.isInteger(value1) && Number.isInteger(value2)) { - return (value1 - value2) * sortOrder - } - - return value1.toString().localeCompare(value2.toString()) * sortOrder - }) - - // put the new order in the DOM - drawTestCases(testCases, testPlanId, permissions) -} - -// todo check selectedCheckboxes function in testrun/get.js -function getSelectedTestCases () { - const inputs = $('.js-testcase-row input:checked') - const tcIds = [] - - inputs.each(function (index, el) { - const elJq = $(el) - - if (elJq.is(':hidden')) { - return - } - - const id = elJq.closest('.js-testcase-row').data('testcase-pk') - tcIds.push(id) - }) - - return tcIds -} - -function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { - // no input => show all rows - if (filterValue.trim().length === 0) { - $('.js-testcase-row').show() - $('.test-cases-count').text(testCases.length) - return - } - - $('.js-testcase-row').hide() - - if (filterBy === 'component' || filterBy === 'tag') { - const query = { plan: planId } - query[`${filterBy}__name__icontains`] = filterValue - - jsonRPC('TestCase.filter', query, function (filtered) { - // hide again if a previous async request showed something else - $('.js-testcase-row').hide() - - const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered, '[data-testcase-pk={0}]') - showOrHideMultipleRows('.js-testcase-row', rows) - $('.test-cases-count').text(rows.show.length) - }) - } else { - const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue, '[data-testcase-pk={0}]') - showOrHideMultipleRows('.js-testcase-row', rows) - $('.test-cases-count').text(rows.show.length) - } -} +import { jsonRPC } from '../../../../static/js/jsonrpc' +import { tagsCard } from '../../../../static/js/tags' +import { + animate, + advancedSearchAndAddTestCases, + bindDeleteCommentButton, changeDropdownSelectedItem, + markdown2HTML, renderCommentsForObject, renderCommentHTML, + treeViewBind, quickSearchAndAddTestCase, + findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData, + showOrHideMultipleRows +} from '../../../../static/js/utils' +import { initSimpleMDE } from '../../../../static/js/simplemde_security_override' + +const expandedTestCaseIds = [] +const fadeAnimationTime = 500 + +const allTestCases = {} +const autocompleteCache = {} + +const confirmedStatuses = [] + +export function pageTestplansGetReadyHandler () { + const testPlanDataElement = $('#test_plan_pk') + const testPlanId = testPlanDataElement.data('testplan-pk') + + const permissions = { + 'perm-change-testcase': testPlanDataElement.data('perm-change-testcase') === 'True', + 'perm-remove-testcase': testPlanDataElement.data('perm-remove-testcase') === 'True', + 'perm-add-testcase': testPlanDataElement.data('perm-add-testcase') === 'True', + 'perm-add-comment': testPlanDataElement.data('perm-add-comment') === 'True', + 'perm-delete-comment': testPlanDataElement.data('perm-delete-comment') === 'True' + } + + // bind everything in tags table + const permRemoveTag = testPlanDataElement.data('perm-remove-tag') === 'True' + tagsCard('TestPlan', testPlanId, { plan: testPlanId }, permRemoveTag) + + jsonRPC('TestCaseStatus.filter', { is_confirmed: true }, function (statuses) { + // save for later use + for (let i = 0; i < statuses.length; i++) { + confirmedStatuses.push(statuses[i].id) + } + + jsonRPC('TestCase.sortkeys', { plan: testPlanId }, function (sortkeys) { + jsonRPC('TestCase.filter', { plan: testPlanId }, function (data) { + for (let i = 0; i < data.length; i++) { + const testCase = data[i] + + testCase.sortkey = sortkeys[testCase.id] + allTestCases[testCase.id] = testCase + } + sortTestCases(Object.values(allTestCases), testPlanId, permissions, 'sortkey') + + // drag & reorder needs the initial order of test cases and + // they may not be fully loaded when sortable() is initialized! + toolbarEvents(testPlanId, permissions) + }) + }) + }) + + adjustTestPlanFamilyTree() + collapseDocumentText() + quickSearchAndAddTestCase(testPlanId, addTestCaseToPlan, autocompleteCache) + $('#btn-search-cases').click(function () { + return advancedSearchAndAddTestCases( + testPlanId, 'TestPlan.add_case', $(this).attr('href'), + $('#test_plan_pk').data('trans-error-adding-cases') + ) + }) +} + +function addTestCaseToPlan (planId) { + const caseName = $('#search-testcase')[0].value + const testCase = autocompleteCache[caseName] + + // test case is already present so don't add it + if (allTestCases[testCase.id]) { + $('#search-testcase').val('') + return false + } + + jsonRPC('TestPlan.add_case', [planId, testCase.id], function (result) { + // IMPORTANT: the API result includes a 'sortkey' field value! + window.location.reload(true) + + // TODO: remove the page reload above and add the new case to the list + // NB: pay attention to drawTestCases() & treeViewBind() + // NB: also add to allTestCases !!! + + $('#search-testcase').val('') + }) +} + +function collapseDocumentText () { + // for some reason .height() reports a much higher value than + // reality and the 59% reduction seems to work nicely + const infoCardHeight = 0.59 * $('#testplan-info').height() + + if ($('#testplan-text').height() > infoCardHeight) { + $('#testplan-text-collapse-btn').removeClass('hidden') + + $('#testplan-text').css('min-height', infoCardHeight) + $('#testplan-text').css('height', infoCardHeight) + $('#testplan-text').css('overflow', 'hidden') + + $('#testplan-text').on('hidden.bs.collapse', function () { + $('#testplan-text').removeClass('collapse').css({ + height: infoCardHeight + }) + }) + } +} + +function adjustTestPlanFamilyTree () { + treeViewBind('#test-plan-family-tree') + + // remove the > arrows from elements which don't have children + $('#test-plan-family-tree').find('.list-group-item-container').each(function (index, element) { + if (!element.innerHTML.trim()) { + const span = $(element).siblings('.list-group-item-header').find('.list-view-pf-left span') + + span.removeClass('fa-angle-right') + // this is the exact same width so rows are still aligned + span.attr('style', 'width:9px') + } + }) + + // expand all parent elements so that the current one is visible + $('#test-plan-family-tree').find('.list-group-item.active').each(function (index, element) { + $(element).parents('.list-group-item-container').each(function (idx, container) { + $(container).toggleClass('hidden') + $(container).siblings('.list-group-item-header').find('.fa-angle-right').toggleClass('fa-angle-down') + }) + }) +} + +function drawTestCases (testCases, testPlanId, permissions) { + const container = $('#testcases-list') + const noCasesTemplate = $('#no_test_cases') + const testCaseRowDocumentFragment = $('#test_case_row')[0].content + + if (testCases.length > 0) { + testCases.forEach(function (element) { + container.append(getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), element, permissions, testPlanId)) + }) + attachEvents(testPlanId, permissions) + } else { + container.append(noCasesTemplate[0].innerHTML) + } + + $('.test-cases-count').html(testCases.length) +} + +function redrawSingleRow (testCaseId, testPlanId, permissions) { + const testCaseRowDocumentFragment = $('#test_case_row')[0].content + const newRow = getTestCaseRowContent(testCaseRowDocumentFragment.cloneNode(true), allTestCases[testCaseId], permissions, testPlanId) + + // remove from expanded list b/c the comment section may have changed + expandedTestCaseIds.splice(expandedTestCaseIds.indexOf(testCaseId), 1) + + // replace the element in the dom + $(`[data-testcase-pk=${testCaseId}]`).replaceWith(newRow) + attachEvents(testPlanId, permissions) +} + +function getTestCaseRowContent (rowContent, testCase, permissions, testPlanId) { + const row = $(rowContent) + + row[0].firstElementChild.dataset.testcasePk = testCase.id + row.find('.js-test-case-link').html(`TC-${testCase.id}: ${testCase.summary}`).attr('href', `/case/${testCase.id}/?from_plan=${testPlanId}`) + // todo: TestCaseStatus here isn't translated b/c TestCase.filter uses a + // custom serializer which needs to be refactored as well + row.find('.js-test-case-status').html(`${testCase.case_status__name}`) + row.find('.js-test-case-priority').html(`${testCase.priority__value}`) + row.find('.js-test-case-category').html(`${testCase.category__name}`) + row.find('.js-test-case-author').html(`${testCase.author__username}`) + row.find('.js-test-case-tester').html(`${testCase.default_tester__username || '-'}`) + row.find('.js-test-case-reviewer').html(`${testCase.reviewer__username || '-'}`) + + // set the links in the kebab menu + if (permissions['perm-change-testcase']) { + row.find('.js-test-case-menu-edit')[0].href = `/case/${testCase.id}/edit/?from_plan=${testPlanId}` + } + + if (permissions['perm-add-testcase']) { + row.find('.js-test-case-menu-clone')[0].href = `/cases/clone/?c=${testCase.id}` + } + + // apply visual separation between confirmed and not confirmed + + if (!isTestCaseConfirmed(testCase.case_status)) { + row.find('.list-group-item-header').addClass('bg-danger') + + // add customizable icon as part of #1932 + row.find('.js-test-case-status-icon').addClass('fa-times') + + row.find('.js-test-case-tester-div').toggleClass('hidden') + row.find('.js-test-case-reviewer-div').toggleClass('hidden') + } else { + row.find('.js-test-case-status-icon').addClass('fa-check-square') + } + + // handle automated icon + const automationIndicationElement = row.find('.js-test-case-automated') + let automatedClassToRemove = 'fa-cog' + + if (testCase.is_automated) { + automatedClassToRemove = 'fa-hand-paper-o' + } + + automationIndicationElement.parent().attr( + 'title', + automationIndicationElement.data(testCase.is_automated.toString()) + ) + automationIndicationElement.removeClass(automatedClassToRemove) + + // produce unique IDs for comments textarea and file upload fields + row.find('textarea')[0].id = `comment-for-testcase-${testCase.id}` + row.find('input[type="file"]')[0].id = `file-upload-for-testcase-${testCase.id}` + + return row +} + +function getTestCaseExpandArea (row, testCase, permissions) { + markdown2HTML(testCase.text, row.find('.js-test-case-expand-text')) + if (testCase.notes.trim().length > 0) { + markdown2HTML(testCase.notes, row.find('.js-test-case-expand-notes')) + } + + // draw the attachments + const uniqueDivCustomId = `js-tc-id-${testCase.id}-attachments` + // set unique identifier so we know where to draw fetched data + row.find('.js-test-case-expand-attachments').parent()[0].id = uniqueDivCustomId + + jsonRPC('TestCase.list_attachments', [testCase.id], function (data) { + // cannot use instance of row in the callback + const ulElement = $(`#${uniqueDivCustomId} .js-test-case-expand-attachments`) + + if (data.length === 0) { + ulElement.children().removeClass('hidden') + return + } + + const liElementFragment = $('#attachments-list-item')[0].content + + for (let i = 0; i < data.length; i++) { + // should create new element for every attachment + const liElement = liElementFragment.cloneNode(true) + const attachmentLink = $(liElement).find('a')[0] + + attachmentLink.href = data[i].url + attachmentLink.innerText = data[i].url.split('/').slice(-1)[0] + ulElement.append(liElement) + } + }) + + // load components + const componentTemplate = row.find('.js-testcase-expand-components').find('template')[0].content + jsonRPC('Component.filter', { cases: testCase.id }, function (result) { + result.forEach(function (element) { + const newComponent = componentTemplate.cloneNode(true) + $(newComponent).find('span').html(element.name) + row.find('.js-testcase-expand-components').append(newComponent) + }) + }) + + // load tags + const tagTemplate = row.find('.js-testcase-expand-tags').find('template')[0].content + jsonRPC('Tag.filter', { case: testCase.id }, function (result) { + const uniqueTags = [] + + result.forEach(function (element) { + if (uniqueTags.indexOf(element.name) === -1) { + uniqueTags.push(element.name) + + const newTag = tagTemplate.cloneNode(true) + $(newTag).find('span').html(element.name) + row.find('.js-testcase-expand-tags').append(newTag) + } + }) + }) + + // render previous comments + renderCommentsForObject( + testCase.id, + 'TestCase.comments', + 'TestCase.remove_comment', + !isTestCaseConfirmed(testCase.case_status) && permissions['perm-delete-comment'], + row.find('.comments') + ) + + // render comments form + const commentFormTextArea = row.find('.js-comment-form-textarea') + if (!isTestCaseConfirmed(testCase.case_status) && permissions['perm-add-comment']) { + const textArea = row.find('textarea')[0] + const fileUpload = row.find('input[type="file"]') + const editor = initSimpleMDE(textArea, $(fileUpload), textArea.id) + + row.find('.js-post-comment').click(function (event) { + event.preventDefault() + const input = editor.value().trim() + + if (input) { + jsonRPC('TestCase.add_comment', [testCase.id, input], comment => { + editor.value('') + + // show the newly added comment and bind its delete button + row.find('.comments').append( + renderCommentHTML( + 1 + row.find('.js-comment-container').length, + comment, + $('template#comment-template')[0], + function (parentNode) { + bindDeleteCommentButton( + testCase.id, + 'TestCase.remove_comment', + permissions['perm-delete-comment'], // b/c we already know it's unconfirmed + parentNode) + }) + ) + }) + } + }) + } else { + commentFormTextArea.hide() + } +} + +function attachEvents (testPlanId, permissions) { + treeViewBind('#testcases-list') + + if (permissions['perm-change-testcase']) { + // update default tester + $('.js-test-case-menu-tester').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + + const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) + if (!emailOrUsername) { + return false + } + + updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { default_tester: emailOrUsername }, + testPlanId, permissions) + + return false + }) + + $('.js-test-case-menu-priority').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + + updateTestCasesViaAPI([getCaseIdFromEvent(ev)], { priority: ev.target.dataset.id }, + testPlanId, permissions) + return false + }) + + $('.js-test-case-menu-status').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const testCaseId = getCaseIdFromEvent(ev) + updateTestCasesViaAPI([testCaseId], { case_status: ev.target.dataset.id }, + testPlanId, permissions) + return false + }) + } + + if (permissions['perm-remove-testcase']) { + // delete testcase from the plan + $('.js-test-case-menu-delete').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const testCaseId = getCaseIdFromEvent(ev) + + jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { + delete allTestCases[testCaseId] + + // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change + $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { + $(this).remove() + }) + + const testCasesCountEl = $('.test-cases-count') + const count = parseInt(testCasesCountEl[0].innerText) + testCasesCountEl.html(count - 1) + }) + + return false + }) + } + + // get details and draw expand area only on expand + $('.js-testcase-row').click(function (ev) { + // don't trigger row expansion when kebab menu is clicked + if ($(ev.target).is('button, a, input, .fa-ellipsis-v')) { + return + } + + const testCaseId = getCaseIdFromEvent(ev) + + // tc was expanded once, dom is ready + if (expandedTestCaseIds.indexOf(testCaseId) > -1) { + return + } + + const tcRow = $(ev.target).closest(`[data-testcase-pk=${testCaseId}]`) + expandedTestCaseIds.push(testCaseId) + getTestCaseExpandArea(tcRow, allTestCases[testCaseId], permissions) + }) + + const inputs = $('.js-testcase-row').find('input') + inputs.click(function (ev) { + // stop trigerring row.click() + ev.stopPropagation() + const checkbox = $('.js-checkbox-toolbar')[0] + + inputs.each(function (index, tc) { + checkbox.checked = tc.checked + + if (!checkbox.checked) { + return false + } + }) + }) + + function getCaseIdFromEvent (ev) { + return $(ev.target).closest('.js-testcase-row').data('testcase-pk') + } +} + +function updateTestCasesViaAPI (testCaseIds, updateQuery, testPlanId, permissions) { + testCaseIds.forEach(function (caseId) { + jsonRPC('TestCase.update', [caseId, updateQuery], function (updatedTC) { + const testCaseRow = $(`.js-testcase-row[data-testcase-pk=${caseId}]`) + + // update internal data + const sortkey = allTestCases[caseId].sortkey + allTestCases[caseId] = updatedTC + // note: updatedTC doesn't have sortkey information + allTestCases[caseId].sortkey = sortkey + + animate(testCaseRow, function () { + redrawSingleRow(caseId, testPlanId, permissions) + }) + }) + }) +} + +function toolbarEvents (testPlanId, permissions) { + $('.js-checkbox-toolbar').click(function (ev) { + const isChecked = ev.target.checked + const testCaseRows = $('.js-testcase-row').find('input') + + testCaseRows.each(function (index, tc) { + tc.checked = isChecked + }) + }) + + $('.js-toolbar-filter-options li').click(function (ev) { + return changeDropdownSelectedItem( + '.js-toolbar-filter-options', + '#input-filter-button', + ev.target, + $('#toolbar-filter') + ) + }) + + $('#toolbar-filter').on('keyup', function () { + const filterValue = $(this).val().toLowerCase() + const filterBy = $('.js-toolbar-filter-options .selected')[0].dataset.filterType + + filterTestCasesByProperty( + testPlanId, + Object.values(allTestCases), + filterBy, + filterValue + ) + }) + + $('.js-toolbar-sort-options li').click(function (ev) { + changeDropdownSelectedItem('.js-toolbar-sort-options', '#sort-button', ev.target) + + sortTestCases(Object.values(allTestCases), testPlanId, permissions) + return false + }) + + // handle asc desc icon + $('.js-toolbar-sorting-order > span').click(function (ev) { + const icon = $(this) + + icon.siblings('.hidden').removeClass('hidden') + icon.addClass('hidden') + + sortTestCases(Object.values(allTestCases), testPlanId, permissions) + }) + + // always initialize the sortable list however you can only + // move items using the handle icon on the left which becomes + // visible only when the manual sorting button is clicked + sortable('#testcases-list', { + handle: '.handle', + itemSerializer: (serializedItem, sortableContainer) => { + return parseInt(serializedItem.node.getAttribute('data-testcase-pk')) + } + }) + + // IMPORTANT: this is not empty b/c sortable() is initialized *after* + // all of the test cases have been rendered !!! + const initialOrder = sortable('#testcases-list', 'serialize')[0].items + + $('.js-toolbar-manual-sort').click(function (event) { + $(this).blur() + $('.js-toolbar-manual-sort').find('span').toggleClass(['fa-sort', 'fa-check-square']) + $('.js-testcase-sort-handler, .js-testcase-expand-arrow, .js-testcase-checkbox').toggleClass('hidden') + + const currentOrder = sortable('#testcases-list', 'serialize')[0].items + + // rows have been rearranged and the results must be committed to the DB + if (currentOrder.join() !== initialOrder.join()) { + currentOrder.forEach(function (tcPk, index) { + jsonRPC('TestPlan.update_case_order', [testPlanId, tcPk, index * 10], function (result) {}) + }) + } + }) + + $('.js-toolbar-priority').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + updateTestCasesViaAPI(selectedCases, { priority: ev.target.dataset.id }, + testPlanId, permissions) + + return false + }) + + $('.js-toolbar-status').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + updateTestCasesViaAPI(selectedCases, { case_status: ev.target.dataset.id }, + testPlanId, permissions) + return false + }) + + $('#default-tester-button').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) + + if (!emailOrUsername) { + return false + } + + updateTestCasesViaAPI(selectedCases, { default_tester: emailOrUsername }, + testPlanId, permissions) + + return false + }) + + $('#bulk-reviewer-button').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + const emailOrUsername = window.prompt($('#test_plan_pk').data('trans-username-email-prompt')) + + if (!emailOrUsername) { + return false + } + + updateTestCasesViaAPI(selectedCases, { reviewer: emailOrUsername }, + testPlanId, permissions) + + return false + }) + + $('#delete_button').click(function (ev) { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + const areYouSureText = $('#test_plan_pk').data('trans-are-you-sure') + if (confirm(areYouSureText)) { + for (let i = 0; i < selectedCases.length; i++) { + const testCaseId = selectedCases[i] + jsonRPC('TestPlan.remove_case', [testPlanId, testCaseId], function () { + delete allTestCases[testCaseId] + + // fadeOut the row then remove it from the dom, if we remove it directly the user may not see the change + $(`[data-testcase-pk=${testCaseId}]`).fadeOut(fadeAnimationTime, function () { + $(this).remove() + }) + }) + } + + const testCasesCountEl = $('.test-cases-count') + const count = parseInt(testCasesCountEl[0].innerText) + testCasesCountEl.html(count - selectedCases.length) + } + + return false + }) + + $('#bulk-clone-button').click(function () { + $(this).parents('.dropdown').toggleClass('open') + const selectedCases = getSelectedTestCases() + + if (!selectedCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + window.location.assign(`/cases/clone?c=${selectedCases.join('&c=')}`) + }) + + $('#testplan-toolbar-newrun').click(function () { + $(this).parents('.dropdown').toggleClass('open') + const selectedTestCases = getSelectedTestCases() + + if (!selectedTestCases.length) { + alert($('#test_plan_pk').data('trans-no-testcases-selected')) + return false + } + + for (let i = 0; i < selectedTestCases.length; i++) { + const status = allTestCases[selectedTestCases[i]].case_status + if (!isTestCaseConfirmed(status)) { + alert($('#test_plan_pk').data('trans-cannot-create-testrun')) + return false + } + } + + const newTestRunUrl = $('#test_plan_pk').data('new-testrun-url') + window.location.assign(`${newTestRunUrl}?c=${selectedTestCases.join('&c=')}`) + return false + }) +} + +function isTestCaseConfirmed (status) { + return confirmedStatuses.indexOf(Number(status)) > -1 +} + +function sortTestCases (testCases, testPlanId, permissions, defaultSortBy = undefined) { + const sortBy = defaultSortBy || $('.js-toolbar-sort-options .selected')[0].dataset.filterType + const sortOrder = $('.js-toolbar-sorting-order > span:not(.hidden)').data('order') + + $('#testcases-list').html('') + + testCases.sort(function (tc1, tc2) { + const value1 = tc1[sortBy] || '' + const value2 = tc2[sortBy] || '' + + if (Number.isInteger(value1) && Number.isInteger(value2)) { + return (value1 - value2) * sortOrder + } + + return value1.toString().localeCompare(value2.toString()) * sortOrder + }) + + // put the new order in the DOM + drawTestCases(testCases, testPlanId, permissions) +} + +// todo check selectedCheckboxes function in testrun/get.js +function getSelectedTestCases () { + const inputs = $('.js-testcase-row input:checked') + const tcIds = [] + + inputs.each(function (index, el) { + const elJq = $(el) + + if (elJq.is(':hidden')) { + return + } + + const id = elJq.closest('.js-testcase-row').data('testcase-pk') + tcIds.push(id) + }) + + return tcIds +} + +function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { + // no input => show all rows + if (filterValue.trim().length === 0) { + $('.js-testcase-row').show() + $('.test-cases-count').text(testCases.length) + return + } + + $('.js-testcase-row').hide() + + if (filterBy === 'component' || filterBy === 'tag') { + const query = { plan: planId } + query[`${filterBy}__name__icontains`] = filterValue + + jsonRPC('TestCase.filter', query, function (filtered) { + // hide again if a previous async request showed something else + $('.js-testcase-row').hide() + + const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered, '[data-testcase-pk={0}]') + showOrHideMultipleRows('.js-testcase-row', rows) + $('.test-cases-count').text(rows.show.length) + }) + } else { + const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue, '[data-testcase-pk={0}]') + showOrHideMultipleRows('.js-testcase-row', rows) + $('.test-cases-count').text(rows.show.length) + } +} diff --git a/tcms/testruns/forms.py b/tcms/testruns/forms.py index e8cfe6d192..ef4f71a1e0 100644 --- a/tcms/testruns/forms.py +++ b/tcms/testruns/forms.py @@ -1,100 +1,100 @@ -# -*- coding: utf-8 -*- -from django import forms -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ - -from tcms.core.forms.fields import UserField -from tcms.core.widgets import SimpleMDENotes -from tcms.management.models import Build, Product, Version -from tcms.rpc.api.forms import DateTimeField -from tcms.testcases.models import TestCase -from tcms.testruns.models import Environment, TestRun - -User = get_user_model() # pylint: disable=invalid-name - - -class NewRunForm(forms.ModelForm): - class Meta: - model = TestRun - exclude = ("tag", "cc") # pylint: disable=modelform-uses-exclude - - manager = UserField() - default_tester = UserField(required=False) - start_date = DateTimeField(required=False) - stop_date = DateTimeField(required=False) - planned_start = DateTimeField(required=False) - planned_stop = DateTimeField(required=False) - notes = forms.CharField( - widget=SimpleMDENotes(), - required=False, - ) - - case = forms.ModelMultipleChoiceField( - queryset=TestCase.objects.none(), - required=False, - ) - - product = forms.ModelChoiceField( - queryset=Product.objects.all(), - required=False, - ) - - matrix_type = forms.ChoiceField( - choices=( - ("full", _("Full")), - ("pairwise", _("Pairwise")), - ), - required=False, - ) - - environment = forms.ModelMultipleChoiceField( - queryset=Environment.objects.all(), - required=False, - ) - - def populate(self, plan_id): - if plan_id: - # plan is ModelChoiceField which contains all the plans - # as we need only the plan for current run we filter the queryset - self.fields["plan"].queryset = self.fields["plan"].queryset.filter( - pk=plan_id - ) - self.fields["product"].queryset = Product.objects.filter( - pk=self.fields["plan"].queryset.first().product_id, - ) - self.fields["build"].queryset = Build.objects.filter( - version_id=self.fields["plan"].queryset.first().product_version_id, - is_active=True, - ) - else: - # these are dynamically filtered via JavaScript - self.fields["plan"].queryset = self.fields["plan"].queryset.none() - self.fields["build"].queryset = Build.objects.none() - - self.fields["case"].queryset = TestCase.objects.filter( - case_status__is_confirmed=True - ).all() - - -class SearchRunForm(forms.ModelForm): - class Meta: - model = TestRun - fields = "__all__" - - # overriden widget - manager = UserField() - default_tester = UserField() - - # extra fields - product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) - version = forms.ModelChoiceField(queryset=Version.objects.none(), required=False) - running = forms.IntegerField(required=False) - - def populate(self, product_id=None): - if product_id: - self.fields["version"].queryset = Version.objects.filter( - product__pk=product_id - ) - self.fields["build"].queryset = Build.objects.filter( - version__product=product_id - ) +# -*- coding: utf-8 -*- +from django import forms +from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ + +from tcms.core.forms.fields import UserField +from tcms.core.widgets import SimpleMDENotes +from tcms.management.models import Build, Product, Version +from tcms.rpc.api.forms import DateTimeField +from tcms.testcases.models import TestCase +from tcms.testruns.models import Environment, TestRun + +User = get_user_model() # pylint: disable=invalid-name + + +class NewRunForm(forms.ModelForm): + class Meta: + model = TestRun + exclude = ("tag", "cc") # pylint: disable=modelform-uses-exclude + + manager = UserField() + default_tester = UserField(required=False) + start_date = DateTimeField(required=False) + stop_date = DateTimeField(required=False) + planned_start = DateTimeField(required=False) + planned_stop = DateTimeField(required=False) + notes = forms.CharField( + widget=SimpleMDENotes(), + required=False, + ) + + case = forms.ModelMultipleChoiceField( + queryset=TestCase.objects.none(), + required=False, + ) + + product = forms.ModelChoiceField( + queryset=Product.objects.all(), + required=False, + ) + + matrix_type = forms.ChoiceField( + choices=( + ("full", _("Full")), + ("pairwise", _("Pairwise")), + ), + required=False, + ) + + environment = forms.ModelMultipleChoiceField( + queryset=Environment.objects.all(), + required=False, + ) + + def populate(self, plan_id): + if plan_id: + # plan is ModelChoiceField which contains all the plans + # as we need only the plan for current run we filter the queryset + self.fields["plan"].queryset = self.fields["plan"].queryset.filter( + pk=plan_id + ) + self.fields["product"].queryset = Product.objects.filter( + pk=self.fields["plan"].queryset.first().product_id, + ) + self.fields["build"].queryset = Build.objects.filter( + version_id=self.fields["plan"].queryset.first().product_version_id, + is_active=True, + ) + else: + # these are dynamically filtered via JavaScript + self.fields["plan"].queryset = self.fields["plan"].queryset.none() + self.fields["build"].queryset = Build.objects.none() + + self.fields["case"].queryset = TestCase.objects.filter( + case_status__is_confirmed=True + ).all() + + +class SearchRunForm(forms.ModelForm): + class Meta: + model = TestRun + fields = "__all__" + + # overriden widget + manager = UserField() + default_tester = UserField() + + # extra fields + product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) + version = forms.ModelChoiceField(queryset=Version.objects.none(), required=False) + running = forms.IntegerField(required=False) + + def populate(self, product_id=None): + if product_id: + self.fields["version"].queryset = Version.objects.filter( + product__pk=product_id + ) + self.fields["build"].queryset = Build.objects.filter( + version__product=product_id + ) diff --git a/tcms/testruns/templates/testruns/mutable.html b/tcms/testruns/templates/testruns/mutable.html index 15044b4aff..3be804ab5a 100644 --- a/tcms/testruns/templates/testruns/mutable.html +++ b/tcms/testruns/templates/testruns/mutable.html @@ -1,262 +1,262 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} - -{% block title %} - {% if object %} - {% trans "Edit TestRun" %} - {% elif is_cloning %} - {% trans "Clone TestRun" %} - {% else %} - {% trans "New Test Run" %} - {% endif %} -{% endblock %} - -{% block page_id %}page-testruns-mutable{% endblock %} - -{% block contents %} -
-
- {% csrf_token %} - -
- -
- - {{ form.summary.errors }} -
- - -
- - {{ form.manager.errors }} -
- - -
- - {{ form.default_tester.errors }} -
-
- -
-
- - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.product.errors }} -
- -
- - - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.plan.errors }} -
- -
- - - + -
- -
- - - {{ form.build.errors }} -
-
- -
- -
-
- - - - -
- {{ form.planned_start.errors }} -
- - -
-
- - - - - -
- {{ form.planned_stop.errors }} -
- - - - {% if object and object.stop_date %} - -
-
- - - - - -
- {{ form.stop_date.errors }} -
- {% else %} - - {% endif %} -
- - {% if test_cases %} -
- -
- - -

- - {% trans 'This is a tech-preview feature!' %} -

- - {{ form.environment.errors }} -
- -
- - - -
- -
- - -

- - - {% trans 'more information' %} - -

-
-
- {% endif %} - -
- -
- {{ form.notes }} -
-
- -
-
- -
-
- - {% if test_cases %} - -
-
- {% trans "Selected TestCase(s):" %} - {% if disabled_cases %} - -{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! -See test plan for more details!{% endblocktrans %} - - {% endif %} -
- - - - - - - - - - - - - - {% for test_case in test_cases %} - - - - - - - - - {% endfor %} - -
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
- - - TC-{{ test_case.pk }}: {{ test_case.summary }} - - - {{ test_case.author.username }} - {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
-
- {% endif %} -
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} + {% if object %} + {% trans "Edit TestRun" %} + {% elif is_cloning %} + {% trans "Clone TestRun" %} + {% else %} + {% trans "New Test Run" %} + {% endif %} +{% endblock %} + +{% block page_id %}page-testruns-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + +
+ +
+ + {{ form.summary.errors }} +
+ + +
+ + {{ form.manager.errors }} +
+ + +
+ + {{ form.default_tester.errors }} +
+
+ +
+
+ + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.product.errors }} +
+ +
+ + + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.plan.errors }} +
+ +
+ + + + +
+ +
+ + + {{ form.build.errors }} +
+
+ +
+ +
+
+ + + + +
+ {{ form.planned_start.errors }} +
+ + +
+
+ + + + + +
+ {{ form.planned_stop.errors }} +
+ + + + {% if object and object.stop_date %} + +
+
+ + + + + +
+ {{ form.stop_date.errors }} +
+ {% else %} + + {% endif %} +
+ + {% if test_cases %} +
+ +
+ + +

+ + {% trans 'This is a tech-preview feature!' %} +

+ + {{ form.environment.errors }} +
+ +
+ + + +
+ +
+ + +

+ + + {% trans 'more information' %} + +

+
+
+ {% endif %} + +
+ +
+ {{ form.notes }} +
+
+ +
+
+ +
+
+ + {% if test_cases %} + +
+
+ {% trans "Selected TestCase(s):" %} + {% if disabled_cases %} + +{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! +See test plan for more details!{% endblocktrans %} + + {% endif %} +
+ + + + + + + + + + + + + + {% for test_case in test_cases %} + + + + + + + + + {% endfor %} + +
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
+ + + TC-{{ test_case.pk }}: {{ test_case.summary }} + + + {{ test_case.author.username }} + {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
+
+ {% endif %} +
+
+{% endblock %} From c8548a1e12c373aab1bc1d38b20061b2dee12646 Mon Sep 17 00:00:00 2001 From: Achmad Fienan Rahardianto Date: Sat, 14 Feb 2026 14:02:20 +0700 Subject: [PATCH 3/7] code update based on PR feedback --- tcms/static/js/index.js | 6 +- tcms/static/js/simplemde_security_override.js | 15 + tcms/testcases/static/testcases/js/mutable.js | 5 +- tcms/testcases/templates/testcases/get.html | 15 +- tcms/testplans/static/testplans/js/get.js | 2 +- tcms/testruns/templates/testruns/mutable.html | 529 +++++++++--------- 6 files changed, 294 insertions(+), 278 deletions(-) diff --git a/tcms/static/js/index.js b/tcms/static/js/index.js index 55f08ba255..a962d89a08 100644 --- a/tcms/static/js/index.js +++ b/tcms/static/js/index.js @@ -80,6 +80,8 @@ $(() => { $('[data-toggle="tooltip"]').tooltip() } + window.markdownEditors = {} + $('.js-simplemde-textarea').each(function () { const fileUploadId = $(this).data('file-upload-id') const uploadField = $(`#${fileUploadId}`) @@ -88,8 +90,8 @@ $(() => { const textareaId = $(this).attr('id') || $(this).attr('name') || 'default' const autoSaveId = window.location.toString() + '#' + textareaId - // this value is only used in testcases/js/mutable.js - window.markdownEditor = initSimpleMDE(this, uploadField, autoSaveId) + const editor = initSimpleMDE(this, uploadField, autoSaveId) + window.markdownEditors[textareaId] = editor }) $('#logout_link').click(function () { diff --git a/tcms/static/js/simplemde_security_override.js b/tcms/static/js/simplemde_security_override.js index 5fd69e2012..fdf7999b37 100644 --- a/tcms/static/js/simplemde_security_override.js +++ b/tcms/static/js/simplemde_security_override.js @@ -24,6 +24,9 @@ export function initSimpleMDE (textArea, fileUploadElement, autoSaveId = window. return null } + // Capture the server-rendered value before SimpleMDE replaces it + const serverValue = textArea.value + const simpleMDE = new SimpleMDE({ element: textArea, autoDownloadFontAwesome: false, @@ -72,6 +75,18 @@ export function initSimpleMDE (textArea, fileUploadElement, autoSaveId = window. } }) + // On edit pages the server value must take precedence over stale autosave + // data. When the textarea had content from the server and autosave replaced + // it with something different, restore the server value. + if (serverValue && simpleMDE.value() !== serverValue) { + simpleMDE.value(serverValue) + } + + // Remove legacy shared autosave key (before per-textarea unique IDs) + // to prevent cross-field content leaking + const legacyKey = 'smde_' + window.location.toString() + try { localStorage.removeItem(legacyKey) } catch (e) { /* ignore */ } + fileUploadElement.change(function () { const attachment = this.files[0] const reader = new FileReader() diff --git a/tcms/testcases/static/testcases/js/mutable.js b/tcms/testcases/static/testcases/js/mutable.js index c37d9c9e32..c84485928f 100644 --- a/tcms/testcases/static/testcases/js/mutable.js +++ b/tcms/testcases/static/testcases/js/mutable.js @@ -2,7 +2,10 @@ import { updateCategorySelectFromProduct } from '../../../../static/js/utils' export function pageTestcasesMutableReadyHandler () { $('#id_template').change(function () { - window.markdownEditor.codemirror.setValue($(this).val()) + const editor = window.markdownEditors && window.markdownEditors['id_text'] + if (editor) { + editor.codemirror.setValue($(this).val()) + } }) $('#add_id_template').click(function () { diff --git a/tcms/testcases/templates/testcases/get.html b/tcms/testcases/templates/testcases/get.html index 8f35b356ae..2864b62eb5 100644 --- a/tcms/testcases/templates/testcases/get.html +++ b/tcms/testcases/templates/testcases/get.html @@ -8,15 +8,6 @@ {% block page_id %}page-testcases-get{% endblock %} {% block body_class %}cards-pf{% endblock %} -{% block breadcrumbs %} - -{% endblock %} {% block contents %}
@@ -173,10 +164,10 @@

{{ object.text|markdown2html }}

-

- {% trans 'Notes' %}: +

{{ object.notes|markdown2html }} -

+
+ diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index ccb83b7e7f..be9b01d0e0 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -179,7 +179,7 @@ function getTestCaseRowContent (rowContent, testCase, permissions, testPlanId) { // set the links in the kebab menu if (permissions['perm-change-testcase']) { - row.find('.js-test-case-menu-edit')[0].href = `/case/${testCase.id}/edit/?from_plan=${testPlanId}` + row.find('.js-test-case-menu-edit')[0].href = `/case/${testCase.id}/edit/` } if (permissions['perm-add-testcase']) { diff --git a/tcms/testruns/templates/testruns/mutable.html b/tcms/testruns/templates/testruns/mutable.html index 3be804ab5a..6555bf074b 100644 --- a/tcms/testruns/templates/testruns/mutable.html +++ b/tcms/testruns/templates/testruns/mutable.html @@ -1,262 +1,267 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} - -{% block title %} - {% if object %} - {% trans "Edit TestRun" %} - {% elif is_cloning %} - {% trans "Clone TestRun" %} - {% else %} - {% trans "New Test Run" %} - {% endif %} -{% endblock %} - -{% block page_id %}page-testruns-mutable{% endblock %} - -{% block contents %} -
-
- {% csrf_token %} - -
- -
- - {{ form.summary.errors }} -
- - -
- - {{ form.manager.errors }} -
- - -
- - {{ form.default_tester.errors }} -
-
- -
-
- - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.product.errors }} -
- -
- - - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.plan.errors }} -
- -
- - - + -
- -
- - - {{ form.build.errors }} -
-
- -
- -
-
- - - - -
- {{ form.planned_start.errors }} -
- - -
-
- - - - - -
- {{ form.planned_stop.errors }} -
- - - - {% if object and object.stop_date %} - -
-
- - - - - -
- {{ form.stop_date.errors }} -
- {% else %} - - {% endif %} -
- - {% if test_cases %} -
- -
- - -

- - {% trans 'This is a tech-preview feature!' %} -

- - {{ form.environment.errors }} -
- -
- - - -
- -
- - -

- - - {% trans 'more information' %} - -

-
-
- {% endif %} - -
- -
- {{ form.notes }} -
-
- -
-
- -
-
- - {% if test_cases %} - -
-
- {% trans "Selected TestCase(s):" %} - {% if disabled_cases %} - -{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! -See test plan for more details!{% endblocktrans %} - - {% endif %} -
- - - - - - - - - - - - - - {% for test_case in test_cases %} - - - - - - - - - {% endfor %} - -
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
- - - TC-{{ test_case.pk }}: {{ test_case.summary }} - - - {{ test_case.author.username }} - {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
-
- {% endif %} -
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block head %} + {{ form.media}} +{% endblock %} + +{% block title %} + {% if object %} + {% trans "Edit TestRun" %} + {% elif is_cloning %} + {% trans "Clone TestRun" %} + {% else %} + {% trans "New Test Run" %} + {% endif %} +{% endblock %} + +{% block page_id %}page-testruns-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + +
+ +
+ + {{ form.summary.errors }} +
+ + +
+ + {{ form.manager.errors }} +
+ + +
+ + {{ form.default_tester.errors }} +
+
+ +
+
+ + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.product.errors }} +
+ +
+ + + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.plan.errors }} +
+ +
+ + + + +
+ +
+ + + {{ form.build.errors }} +
+
+ +
+ +
+
+ + + + +
+ {{ form.planned_start.errors }} +
+ + +
+
+ + + + + +
+ {{ form.planned_stop.errors }} +
+ + + + {% if object and object.stop_date %} + +
+
+ + + + + +
+ {{ form.stop_date.errors }} +
+ {% else %} + + {% endif %} +
+ + {% if test_cases %} +
+ +
+ + +

+ + {% trans 'This is a tech-preview feature!' %} +

+ + {{ form.environment.errors }} +
+ +
+ + + +
+ +
+ + +

+ + + {% trans 'more information' %} + +

+
+
+ {% endif %} + +
+ +
+ {{ form.notes }} + {{ form.notes.errors }} +
+
+ +
+
+ +
+
+ + {% if test_cases %} + +
+
+ {% trans "Selected TestCase(s):" %} + {% if disabled_cases %} + +{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! +See test plan for more details!{% endblocktrans %} + + {% endif %} +
+ + + + + + + + + + + + + + {% for test_case in test_cases %} + + + + + + + + + {% endfor %} + +
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
+ + + TC-{{ test_case.pk }}: {{ test_case.summary }} + + + {{ test_case.author.username }} + {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
+
+ {% endif %} +
+
+{% endblock %} From e29a9ce57ea63cb34538c6fedbcf81f86fea4b67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 07:34:40 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcms/testcases/templates/testcases/get.html | 2 +- tcms/testruns/templates/testruns/mutable.html | 534 +++++++++--------- 2 files changed, 268 insertions(+), 268 deletions(-) diff --git a/tcms/testcases/templates/testcases/get.html b/tcms/testcases/templates/testcases/get.html index 2864b62eb5..fca36da05e 100644 --- a/tcms/testcases/templates/testcases/get.html +++ b/tcms/testcases/templates/testcases/get.html @@ -167,7 +167,7 @@

{{ object.notes|markdown2html }}
- + diff --git a/tcms/testruns/templates/testruns/mutable.html b/tcms/testruns/templates/testruns/mutable.html index 6555bf074b..18d40f10bb 100644 --- a/tcms/testruns/templates/testruns/mutable.html +++ b/tcms/testruns/templates/testruns/mutable.html @@ -1,267 +1,267 @@ -{% extends "base.html" %} -{% load i18n %} -{% load static %} - -{% block head %} - {{ form.media}} -{% endblock %} - -{% block title %} - {% if object %} - {% trans "Edit TestRun" %} - {% elif is_cloning %} - {% trans "Clone TestRun" %} - {% else %} - {% trans "New Test Run" %} - {% endif %} -{% endblock %} - -{% block page_id %}page-testruns-mutable{% endblock %} - -{% block contents %} -
-
- {% csrf_token %} - -
- -
- - {{ form.summary.errors }} -
- - -
- - {{ form.manager.errors }} -
- - -
- - {{ form.default_tester.errors }} -
-
- -
-
- - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.product.errors }} -
- -
- - - {% if not plan_id %} - + - {% endif %} -
- -
- - - {{ form.plan.errors }} -
- -
- - - + -
- -
- - - {{ form.build.errors }} -
-
- -
- -
-
- - - - -
- {{ form.planned_start.errors }} -
- - -
-
- - - - - -
- {{ form.planned_stop.errors }} -
- - - - {% if object and object.stop_date %} - -
-
- - - - - -
- {{ form.stop_date.errors }} -
- {% else %} - - {% endif %} -
- - {% if test_cases %} -
- -
- - -

- - {% trans 'This is a tech-preview feature!' %} -

- - {{ form.environment.errors }} -
- -
- - - -
- -
- - -

- - - {% trans 'more information' %} - -

-
-
- {% endif %} - -
- -
- {{ form.notes }} - {{ form.notes.errors }} -
-
- -
-
- -
-
- - {% if test_cases %} - -
-
- {% trans "Selected TestCase(s):" %} - {% if disabled_cases %} - -{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! -See test plan for more details!{% endblocktrans %} - - {% endif %} -
- - - - - - - - - - - - - - {% for test_case in test_cases %} - - - - - - - - - {% endfor %} - -
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
- - - TC-{{ test_case.pk }}: {{ test_case.summary }} - - - {{ test_case.author.username }} - {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
-
- {% endif %} -
-
-{% endblock %} +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block head %} + {{ form.media}} +{% endblock %} + +{% block title %} + {% if object %} + {% trans "Edit TestRun" %} + {% elif is_cloning %} + {% trans "Clone TestRun" %} + {% else %} + {% trans "New Test Run" %} + {% endif %} +{% endblock %} + +{% block page_id %}page-testruns-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + +
+ +
+ + {{ form.summary.errors }} +
+ + +
+ + {{ form.manager.errors }} +
+ + +
+ + {{ form.default_tester.errors }} +
+
+ +
+
+ + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.product.errors }} +
+ +
+ + + {% if not plan_id %} + + + {% endif %} +
+ +
+ + + {{ form.plan.errors }} +
+ +
+ + + + +
+ +
+ + + {{ form.build.errors }} +
+
+ +
+ +
+
+ + + + +
+ {{ form.planned_start.errors }} +
+ + +
+
+ + + + + +
+ {{ form.planned_stop.errors }} +
+ + + + {% if object and object.stop_date %} + +
+
+ + + + + +
+ {{ form.stop_date.errors }} +
+ {% else %} + + {% endif %} +
+ + {% if test_cases %} +
+ +
+ + +

+ + {% trans 'This is a tech-preview feature!' %} +

+ + {{ form.environment.errors }} +
+ +
+ + + +
+ +
+ + +

+ + + {% trans 'more information' %} + +

+
+
+ {% endif %} + +
+ +
+ {{ form.notes }} + {{ form.notes.errors }} +
+
+ +
+
+ +
+
+ + {% if test_cases %} + +
+
+ {% trans "Selected TestCase(s):" %} + {% if disabled_cases %} + +{% blocktrans with count=disabled_cases %}{{ count }} of the pre-selected test cases is not CONFIRMED and will not be cloned! +See test plan for more details!{% endblocktrans %} + + {% endif %} +
+ + + + + + + + + + + + + + {% for test_case in test_cases %} + + + + + + + + + {% endfor %} + +
{% trans "Summary" %}{% trans "Author" %}{% trans "Created on" %}{% trans "Status" %}{% trans "Category" %}{% trans "Priority" %}
+ + + TC-{{ test_case.pk }}: {{ test_case.summary }} + + + {{ test_case.author.username }} + {{ test_case.create_date }}{{ test_case.case_status }}{{ test_case.category }}{{ test_case.priority }}
+
+ {% endif %} +
+
+{% endblock %} From 46d464fb199e3b5dd20e96d058ac5384e1a474c6 Mon Sep 17 00:00:00 2001 From: Achmad Fienan Rahardianto Date: Sat, 14 Feb 2026 14:38:32 +0700 Subject: [PATCH 5/7] missed 1 removal of unrelated implementation --- tcms/testplans/static/testplans/js/get.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index be9b01d0e0..6b509d0425 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -167,7 +167,7 @@ function getTestCaseRowContent (rowContent, testCase, permissions, testPlanId) { const row = $(rowContent) row[0].firstElementChild.dataset.testcasePk = testCase.id - row.find('.js-test-case-link').html(`TC-${testCase.id}: ${testCase.summary}`).attr('href', `/case/${testCase.id}/?from_plan=${testPlanId}`) + row.find('.js-test-case-link').html(`TC-${testCase.id}: ${testCase.summary}`).attr('href', `/case/${testCase.id}/`) // todo: TestCaseStatus here isn't translated b/c TestCase.filter uses a // custom serializer which needs to be refactored as well row.find('.js-test-case-status').html(`${testCase.case_status__name}`) From b899ef67114ad24220ae234d7384354bf87e6477 Mon Sep 17 00:00:00 2001 From: Achmad Fienan Rahardianto Date: Thu, 5 Mar 2026 08:21:53 +0700 Subject: [PATCH 6/7] Render test case notes as markdown in TestRun detail view --- tcms/testruns/static/testruns/js/get.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcms/testruns/static/testruns/js/get.js b/tcms/testruns/static/testruns/js/get.js index aead2c3ec6..3b6a0242b8 100644 --- a/tcms/testruns/static/testruns/js/get.js +++ b/tcms/testruns/static/testruns/js/get.js @@ -475,7 +475,7 @@ function getExpandArea (testExecution) { }], (data) => { data.forEach((entry) => { markdown2HTML(entry.text, container.find('.test-execution-text')[0]) - container.find('.test-execution-notes').append(entry.notes) + markdown2HTML(entry.notes, container.find('.test-execution-notes')) }) }) From 02a3682699551047f4de59fdc87fb58c51d24af4 Mon Sep 17 00:00:00 2001 From: Achmad Fienan Rahardianto Date: Thu, 5 Mar 2026 08:23:07 +0700 Subject: [PATCH 7/7] Remove stale autosave override and revert unrelated signature change --- tcms/static/js/simplemde_security_override.js | 7 ------- tcms/testplans/static/testplans/js/get.js | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/tcms/static/js/simplemde_security_override.js b/tcms/static/js/simplemde_security_override.js index fdf7999b37..96e09eb538 100644 --- a/tcms/static/js/simplemde_security_override.js +++ b/tcms/static/js/simplemde_security_override.js @@ -75,13 +75,6 @@ export function initSimpleMDE (textArea, fileUploadElement, autoSaveId = window. } }) - // On edit pages the server value must take precedence over stale autosave - // data. When the textarea had content from the server and autosave replaced - // it with something different, restore the server value. - if (serverValue && simpleMDE.value() !== serverValue) { - simpleMDE.value(serverValue) - } - // Remove legacy shared autosave key (before per-textarea unique IDs) // to prevent cross-field content leaking const legacyKey = 'smde_' + window.location.toString() diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index 6b509d0425..07902281c7 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -163,7 +163,7 @@ function redrawSingleRow (testCaseId, testPlanId, permissions) { attachEvents(testPlanId, permissions) } -function getTestCaseRowContent (rowContent, testCase, permissions, testPlanId) { +function getTestCaseRowContent (rowContent, testCase, permissions) { const row = $(rowContent) row[0].firstElementChild.dataset.testcasePk = testCase.id