Repository: Nitrate/Nitrate

Issue:
Split view function env_value
`env_value` has action class providing three actions `add`, `remove` and `change`. This issue is for splitting the view function `env_value` into three class-based views for those actions individually.

AC:
- [x] Each view should have permission set properly
- [x] Update the URL mappings.
- [x] Update the JavaScript accordingly.
- [x] Update and add tests accordingly.


Discussion:

Resolving patch:
diff --git a/src/tcms/testruns/forms.py b/src/tcms/testruns/forms.py
--- a/src/tcms/testruns/forms.py
+++ b/src/tcms/testruns/forms.py
@@ -4,7 +4,7 @@
 from django.contrib.auth.models import User
 
 from tcms.core.forms import UserField, DurationField
-from tcms.management.models import Product, Version, TestBuild, TCMSEnvGroup, TestTag
+from tcms.management.models import Product, Version, TestBuild, TCMSEnvGroup, TestTag, TCMSEnvValue
 from tcms.testplans.models import TestPlan
 from tcms.testcases.models import TestCase
 from .models import TestRun, TestCaseRunStatus
@@ -307,6 +307,47 @@ def populate(self, trs, product_id=None):
             )
 
 
+class RunAndEnvValueForm(forms.Form):
+    runs = forms.ModelMultipleChoiceField(
+        queryset=TestRun.objects.all(),
+        error_messages={
+            'required': 'Missing test run id to update the environment value',
+            'invalid_choice': 'Test run id %(value)s is not valid.'
+        }
+    )
+    env_value = forms.ModelChoiceField(
+        queryset=TCMSEnvValue.objects.all(),
+        error_messages={
+            'required': 'Missing environment value id.',
+            'invalid_choice': 'Invalid environment value id.'
+        }
+    )
+
+
+class ChangeRunEnvValueForm(forms.Form):
+    runs = forms.ModelMultipleChoiceField(
+        queryset=TestRun.objects.all(),
+        error_messages={
+            'required': 'Missing test run id to update the environment value',
+            'invalid_choice': 'Test run id %(value)s is not valid.'
+        }
+    )
+    old_env_value = forms.ModelChoiceField(
+        queryset=TCMSEnvValue.objects.all(),
+        error_messages={
+            'required': 'Missing old environment value id.',
+            'invalid_choice': 'Invalid old environment value id.'
+        }
+    )
+    new_env_value = forms.ModelChoiceField(
+        queryset=TCMSEnvValue.objects.all(),
+        error_messages={
+            'required': 'Missing new environment value id.',
+            'invalid_choice': 'Invalid new environment value id.'
+        }
+    )
+
+
 # ===========================================================================
 # Case run form
 # ===========================================================================
diff --git a/src/tcms/testruns/urls/runs_urls.py b/src/tcms/testruns/urls/runs_urls.py
--- a/src/tcms/testruns/urls/runs_urls.py
+++ b/src/tcms/testruns/urls/runs_urls.py
@@ -7,6 +7,12 @@
 urlpatterns = [
     path('', views.all, name='runs-all'),
     path('ajax/', views.ajax_search, name='runs-ajax-search'),
-    path('env_value/', views.env_value, name='runs-env-value'),
     path('clone/', views.clone, name='runs-clone'),
+
+    path('env_value/add/', views.AddEnvValueToRunView.as_view(),
+         name='runs-add-env-value'),
+    path('env_value/change/', views.ChangeRunEnvValueView.as_view(),
+         name='runs-change-env-value'),
+    path('env_value/delete/', views.DeleteRunEnvValueView.as_view(),
+         name='runs-delete-env-value'),
 ]
diff --git a/src/tcms/testruns/views.py b/src/tcms/testruns/views.py
--- a/src/tcms/testruns/views.py
+++ b/src/tcms/testruns/views.py
@@ -27,26 +27,26 @@
 from django.shortcuts import get_object_or_404
 from django.shortcuts import render
 from django.template.loader import get_template
+from django.utils.encoding import smart_str
 from django.views.decorators.csrf import csrf_protect
 from django.views.decorators.http import require_GET
 from django.views.decorators.http import require_POST
 from django.views.decorators.http import require_http_methods
-from django.views.generic import RedirectView
-from django.views.generic.base import TemplateView
-from django.views.generic.base import View
+from django.views.generic import RedirectView, FormView
+from django.views.generic.base import TemplateView, View
 
 from django_comments.models import Comment
-from tcms.core.responses import JsonResponseBadRequest, JsonResponseForbidden
+from tcms.core.responses import JsonResponseBadRequest
 
 from tcms.issuetracker.models import Issue
 from tcms.issuetracker.models import IssueTracker
 from tcms.issuetracker.services import find_service
 from tcms.core.tcms_router import connection
-from tcms.core.utils import clean_request
+from tcms.core.utils import clean_request, form_error_messags_to_list
 from tcms.core.utils import DataTableResult
 from tcms.core.utils import format_timedelta
 from tcms.core.views import prompt
-from tcms.management.models import Priority, TCMSEnvValue, TestTag, TCMSEnvGroup
+from tcms.management.models import Priority, TestTag, TCMSEnvGroup
 from tcms.search.forms import RunForm
 from tcms.search.query import SmartDjangoQuery
 from tcms.testcases.models import TestCase
@@ -55,7 +55,7 @@
 from tcms.testplans.models import TestPlan
 from tcms.testruns.data import stats_caseruns_status
 from tcms.testruns.data import TestCaseRunDataMixin
-from tcms.testruns.forms import MulitpleRunsCloneForm, PlanFilterRunForm
+from tcms.testruns.forms import MulitpleRunsCloneForm, PlanFilterRunForm, RunAndEnvValueForm, ChangeRunEnvValueForm
 from tcms.testruns.forms import NewRunForm, SearchRunForm, EditRunForm, RunCloneForm
 from tcms.testruns.helpers.serializer import TCR2File
 from tcms.testruns.models import TestRun, TestCaseRun, TestCaseRunStatus, TCMSEnvRunValueMap
@@ -1282,93 +1282,64 @@ def export(request, run_id):
     return response
 
 
-@require_GET
-def env_value(request):
-    """Run environment property edit function"""
-    trs = TestRun.objects.filter(run_id__in=request.GET.getlist('run_id'))
-
-    class RunEnvActions:
-        def __init__(self, requet, trs):
-            self.__all__ = ['add', 'remove', 'change']
-            self.request = request
-            self.trs = trs
-
-        def has_no_perm(self, perm):
-            if not self.request.user.has_perm('testruns.' + perm + '_tcmsenvrunvaluemap'):
-                return {'message': 'Permission denied - %s' % perm}
-
-            return False
-
-        def get_env_value(self, env_value_id):
-            return TCMSEnvValue.objects.get(id=env_value_id)
-
-        def add(self):
-            chk_perm = self.has_no_perm('add')
-
-            if chk_perm:
-                return JsonResponseForbidden(chk_perm)
-
-            env_value_id = int(request.GET.get('env_value_id'))
-            try:
-                value = self.get_env_value(env_value_id)
-            except ObjectDoesNotExist:
-                return JsonResponseBadRequest({
-                    'message': f'Environment value id {env_value_id} does not exist.'
-                })
+class AddEnvValueToRunView(PermissionRequiredMixin, FormView):
+    """Add env value to test runs"""
 
-            for tr in self.trs:
-                tr.add_env_value(env_value=value)
+    form_class = RunAndEnvValueForm
+    permission_required = 'testruns.add_tcmsenvrunvaluemap'
+    raise_exception = True
 
-            fragment = render(request, 'run/get_environment.html', context={
-                'test_run': self.trs[0],
-                'is_ajax': True
-            })
+    def form_valid(self, form):
+        for run in form.cleaned_data['runs']:
+            run.add_env_value(form.cleaned_data['env_value'])
 
-            if isinstance(fragment.content, bytes):
-                s = fragment.content.decode('utf-8')
-            else:
-                s = fragment.content
+        fragment = render(self.request, 'run/get_environment.html', context={
+            'test_run': form.cleaned_data['runs'][0],
+            'is_ajax': True
+        })
+        return JsonResponse({'fragment': smart_str(fragment.content)})
 
-            return JsonResponse({'fragment': s})
+    def form_invalid(self, form):
+        return JsonResponseBadRequest({
+            'message': form_error_messags_to_list(form.errors)
+        })
 
-        def remove(self):
-            chk_perm = self.has_no_perm('delete')
-            if chk_perm:
-                return JsonResponseForbidden(chk_perm)
 
-            try:
-                for tr in self.trs:
-                    tr.remove_env_value(env_value=self.get_env_value(
-                        request.GET.get('env_value_id')
-                    ))
-            except Exception:
-                pass
+class DeleteRunEnvValueView(PermissionRequiredMixin, FormView):
+    """Delete a test run's env value"""
 
-            return JsonResponse({})
+    form_class = RunAndEnvValueForm
+    permission_required = 'testruns.delete_tcmsenvrunvaluemap'
+    raise_exception = True
 
-        def change(self):
-            chk_perm = self.has_no_perm('change')
-            if chk_perm:
-                return JsonResponseForbidden(chk_perm)
+    def form_valid(self, form):
+        for run in form.cleaned_data['runs']:
+            run.remove_env_value(form.cleaned_data['env_value'])
+        return JsonResponse({})
 
-            for tr in self.trs:
-                tr.remove_env_value(env_value=self.get_env_value(
-                    request.GET.get('old_env_value_id')
-                ))
+    def form_invalid(self, form):
+        return JsonResponseBadRequest({
+            'message': form_error_messags_to_list(form.errors)
+        })
 
-                tr.add_env_value(env_value=self.get_env_value(
-                    request.GET.get('new_env_value_id')
-                ))
 
-            return JsonResponse({})
+class ChangeRunEnvValueView(PermissionRequiredMixin, FormView):
+    """Change a test run's env value"""
 
-    run_env_actions = RunEnvActions(request, trs)
+    form_class = ChangeRunEnvValueForm
+    permission_required = 'testruns.change_tcmsenvrunvaluemap'
+    raise_exception = True
 
-    if request.GET.get('a') not in run_env_actions.__all__:
-        return JsonResponseBadRequest({'message': 'Unrecognizable actions'})
+    def form_valid(self, form):
+        for run in form.cleaned_data['runs']:
+            run.remove_env_value(form.cleaned_data['old_env_value'])
+            run.add_env_value(form.cleaned_data['new_env_value'])
+        return JsonResponse({})
 
-    func = getattr(run_env_actions, request.GET['a'])
-    return func()
+    def form_invalid(self, form):
+        return JsonResponseBadRequest({
+            'message': form_error_messags_to_list(form.errors)
+        })
 
 
 def caseruns(request, templ='report/caseruns.html'):


Test patch:
diff --git a/src/tests/testruns/test_views.py b/src/tests/testruns/test_views.py
--- a/src/tests/testruns/test_views.py
+++ b/src/tests/testruns/test_views.py
@@ -949,83 +949,83 @@ def setUpTestData(cls):
         cls.test_run.add_env_value(cls.value_linux)
         cls.test_run_1.add_env_value(cls.value_linux)
 
-        cls.env_value_url = reverse('runs-env-value')
         user_should_have_perm(cls.tester, 'testruns.add_tcmsenvrunvaluemap')
         user_should_have_perm(cls.tester, 'testruns.delete_tcmsenvrunvaluemap')
 
-    def test_refuse_if_action_is_unknown(self):
+    def test_add_env_value(self):
         self.login_tester()
 
-        response = self.client.get(self.env_value_url, {
-            'env_value_id': self.value_bsd,
-            'run_id': self.test_run.pk
+        self.client.post(reverse('runs-add-env-value'), {
+            'env_value': self.value_bsd.pk,
+            'runs': self.test_run.pk
         })
 
-        self.assertJsonResponse(
-            response,
-            {'message': 'Unrecognizable actions'},
-            status_code=HTTPStatus.BAD_REQUEST
+        self.assertTrue(
+            TCMSEnvRunValueMap.objects.filter(
+                run=self.test_run, value=self.value_bsd
+            ).exists()
         )
 
-    def test_add_env_value(self):
+    def test_add_env_value_to_runs(self):
         self.login_tester()
 
-        self.client.get(self.env_value_url, {
-            'a': 'add',
-            'env_value_id': self.value_bsd.pk,
-            'run_id': self.test_run.pk
+        self.client.post(reverse('runs-add-env-value'), {
+            'env_value': self.value_bsd.pk,
+            'runs': [self.test_run.pk, self.test_run_1.pk]
         })
 
-        rel = TCMSEnvRunValueMap.objects.filter(run=self.test_run,
-                                                value=self.value_bsd)
-        self.assertTrue(rel.exists())
+        self.assertTrue(
+            TCMSEnvRunValueMap.objects.filter(
+                run=self.test_run, value=self.value_bsd
+            ).exists()
+        )
 
-    def test_add_env_value_to_runs(self):
-        self.login_tester()
+        self.assertTrue(
+            TCMSEnvRunValueMap.objects.filter(
+                run=self.test_run_1, value=self.value_bsd
+            ).exists()
+        )
 
-        self.client.get(self.env_value_url, {
-            'a': 'add',
-            'env_value_id': self.value_bsd.pk,
-            'run_id': [self.test_run.pk, self.test_run_1.pk]
+    def test_no_perm_to_delete(self):
+        resp = self.client.post(reverse('runs-add-env-value'), {
+            'env_value': self.value_linux.pk,
+            'runs': self.test_run.pk,
         })
-
-        rel = TCMSEnvRunValueMap.objects.filter(run=self.test_run,
-                                                value=self.value_bsd)
-        self.assertTrue(rel.exists())
-
-        rel = TCMSEnvRunValueMap.objects.filter(run=self.test_run_1,
-                                                value=self.value_bsd)
-        self.assertTrue(rel.exists())
+        self.assert403(resp)
 
     def test_delete_env_value(self):
         self.login_tester()
 
-        self.client.get(self.env_value_url, {
-            'a': 'remove',
-            'env_value_id': self.value_linux.pk,
-            'run_id': self.test_run.pk,
+        self.client.post(reverse('runs-add-env-value'), {
+            'env_value': self.value_linux.pk,
+            'runs': self.test_run.pk,
         })
 
-        rel = TCMSEnvRunValueMap.objects.filter(run=self.test_run,
-                                                value=self.value_linux)
-        self.assertFalse(rel.exists())
+        self.assertTrue(
+            TCMSEnvRunValueMap.objects.filter(
+                run=self.test_run, value=self.value_linux
+            ).exists()
+        )
 
     def test_delete_env_value_from_runs(self):
         self.login_tester()
 
-        self.client.get(self.env_value_url, {
-            'a': 'remove',
-            'env_value_id': self.value_linux.pk,
-            'run_id': [self.test_run.pk, self.test_run_1.pk],
+        self.client.post(reverse('runs-add-env-value'), {
+            'env_value': self.value_linux.pk,
+            'runs': [self.test_run.pk, self.test_run_1.pk],
         })
 
-        rel = TCMSEnvRunValueMap.objects.filter(run=self.test_run,
-                                                value=self.value_linux)
-        self.assertFalse(rel.exists())
+        self.assertTrue(
+            TCMSEnvRunValueMap.objects.filter(
+                run=self.test_run, value=self.value_linux
+            ).exists()
+        )
 
-        rel = TCMSEnvRunValueMap.objects.filter(run=self.test_run_1,
-                                                value=self.value_linux)
-        self.assertFalse(rel.exists())
+        self.assertTrue(
+            TCMSEnvRunValueMap.objects.filter(
+                run=self.test_run_1, value=self.value_linux
+            ).exists()
+        )
 
 
 class TestExportTestRunCases(BaseCaseRun):