Beispiel #1
0
    def post(self, request, env_group_id):
        status = request.POST.get('status')
        if status is None:
            return JsonResponseBadRequest({
                'message': 'Environment group status is missing.'
            })
        if status not in ['0', '1']:
            return JsonResponseBadRequest({
                'message': f'Environment group status "{status}" is invalid.'
            })
        try:
            env = TCMSEnvGroup.objects.get(pk=int(env_group_id))
        except TCMSEnvGroup.DoesNotExist:
            return JsonResponseNotFound({
                'message': f'Environment group with id {env_group_id} does not exist.'
            })
        else:
            new_status = bool(int(status))
            if env.is_active != new_status:
                env.is_active = new_status
                env.save(update_fields=['is_active'])

                env.log_action(
                    who=request.user,
                    field='is_active',
                    original_value=env.is_active,
                    new_value=new_status)

            return JsonResponse({'env_group_id': env_group_id})
Beispiel #2
0
    def _update_sortkey(self):
        try:
            sortkey = int(self.new_value)
            if sortkey < 0 or sortkey > 32300:
                return JsonResponseBadRequest(
                    {'message': 'New sortkey is out of range [0, 32300].'})
        except ValueError:
            return JsonResponseBadRequest(
                {'message': 'New sortkey is not an integer.'})
        plan = plan_from_request_or_none(self.request, pk_enough=True)
        if plan is None:
            return JsonResponseBadRequest({'message': 'No plan record found.'})
        update_targets = self.get_update_targets()

        # ##
        # MySQL does not allow to exeucte UPDATE statement that contains
        # subquery querying from same table. In this case, OperationError will
        # be raised.
        offset = 0
        step_length = 500
        queryset_filter = TestCasePlan.objects.filter
        data = {self.target_field: sortkey}
        while 1:
            sub_cases = update_targets[offset:offset + step_length]
            case_pks = [case.pk for case in sub_cases]
            if len(case_pks) == 0:
Beispiel #3
0
def get(request):
    """Get links of specific instance of content type

    - target: The model name of the instance being searched
    - target_id: The ID of the instance

    Only accept GET request from client.
    """
    form = BasicValidationForm(request.GET)

    if form.is_valid():
        model_class = form.cleaned_data['target']
        target_id = form.cleaned_data['target_id']

        try:
            model_instance = model_class.objects.get(pk=target_id)
            links = LinkReference.get_from(model_instance)
        except Exception as err:
            return JsonResponseBadRequest({'message': str(err)})

        jd = []
        for link in links:
            jd.append({'name': link.name, 'url': link.url})
        return JsonResponse(jd, safe=False)

    else:
        return JsonResponseBadRequest({'message': form_errors_to_list(form)})
Beispiel #4
0
        def remove(self):
            if not self.request.user.has_perm('issuetracker.delete_issue'):
                return JsonResponseForbidden({'message': 'Permission denied.'})

            class RemoveIssueForm(forms.Form):
                issue_key = forms.CharField()
                case_run = forms.ModelMultipleChoiceField(
                    queryset=TestCaseRun.objects.all().only('pk'),
                    error_messages={
                        'required': 'Case run id is missed.',
                        'invalid_pk_value': 'Case run %(pk)s does not exist.',
                    },
                )

            form = RemoveIssueForm(request.GET)
            if not form.is_valid():
                return JsonResponseBadRequest(
                    {'message': form_error_messages_to_list(form)})

            issue_key = form.cleaned_data['issue_key']
            case_runs = form.cleaned_data['case_run']
            for case_run in case_runs:
                try:
                    case_run.remove_issue(issue_key)
                except Exception:
                    msg = 'Failed to remove issue {} from case run {}'.format(
                        issue_key, case_run.pk)
                    logger.exception(msg)
                    return JsonResponseBadRequest({'message': msg})

            return self.run_issues_info(case_runs)
Beispiel #5
0
    def update(self):
        has_perms = check_permission(self.request, self.ctype)
        if not has_perms:
            return JsonResponseForbidden({
                'message':
                "You don't have enough permission to update TestCases."
            })

        action = self.get_update_action()
        if action is not None:
            try:
                resp = action()
                self._sendmail()
            except ObjectDoesNotExist as err:
                return JsonResponseNotFound({'message': str(err)})
            except Exception:
                # TODO: besides this message to users, what happening should be
                # recorded in the system log.
                return JsonResponseBadRequest({
                    'message':
                    'Update failed. Please try again or request '
                    'support from your organization.'
                })
            else:
                if resp is None:
                    resp = JsonResponse({})
                return resp
        return JsonResponseBadRequest({'message': 'Not know what to update.'})
Beispiel #6
0
    def post(self, request, link_id):
        try:
            LinkReference.unlink(link_id)
        except Exception as err:
            return JsonResponseBadRequest({'message': str(err)})

        return JsonResponse({'message': 'Link has been removed successfully.'})
Beispiel #7
0
    def post(self, request):
        form = AddLinkReferenceForm(request.POST)
        if form.is_valid():
            name = form.cleaned_data['name']
            url = form.cleaned_data['url']
            target_id = form.cleaned_data['target_id']
            model_class = form.cleaned_data['target']

            model_instance = model_class.objects.get(pk=target_id)
            create_link(name=name, url=url, link_to=model_instance)

            return JsonResponse({
                'rc': 0,
                'response': 'ok',
                'data': {
                    'name': name,
                    'url': url
                }
            })

        else:
            return JsonResponseBadRequest({
                'rc': 1,
                'response': form.errors.as_text()
            })
Beispiel #8
0
    def post(self, request):
        property_name = request.POST.get('name')

        if not property_name:
            return JsonResponseBadRequest({
                'message': 'Property name is missing.'
            })

        if TCMSEnvProperty.objects.filter(name=property_name).exists():
            return JsonResponseBadRequest({
                'message': f'Environment property {property_name} '
                           f'already exists, please choose another name.'
            })

        new_property = TCMSEnvProperty.objects.create(name=property_name)

        return JsonResponse({'id': new_property.pk, 'name': new_property.name})
Beispiel #9
0
def treeview_add_child_plans(request: HttpRequest, plan_id: int):
    plan = TestPlan.objects.filter(pk=plan_id).only('pk').first()
    if plan is None:
        return JsonResponseNotFound(
            {'message': f'Plan {plan_id} does not exist.'})

    child_plan_ids: List[str] = request.POST.getlist('children')
    child_plans: List[TestPlan] = []

    ancestor_ids = plan.get_ancestor_ids()
    descendant_ids = plan.get_descendant_ids()

    for child_plan_id in child_plan_ids:
        if not child_plan_id.isdigit():
            return JsonResponseBadRequest(
                {'message': f'Child plan id {child_plan_id} is not a number.'})
        child_plan: TestPlan = (TestPlan.objects.filter(
            pk=int(child_plan_id)).only('pk').first())
        if child_plan is None:
            return JsonResponseBadRequest(
                {'message': f'Child plan {child_plan_id} does not exist.'})
        if child_plan.pk in ancestor_ids:
            return JsonResponseBadRequest({
                'message':
                f'Plan {child_plan_id} is an ancestor of '
                f'plan {plan_id} already.'
            })
        if child_plan.pk in descendant_ids:
            return JsonResponseBadRequest({
                'message':
                f'Plan {child_plan_id} is a descendant of '
                f'plan {plan_id} already.'
            })

        child_plans.append(child_plan)

    for child_plan in child_plans:
        child_plan.parent = plan
        child_plan.save(update_fields=['parent'])

    return JsonResponse({
        'parent_plan': plan.pk,
        'children_plans': [plan.pk for plan in child_plans]
    })
Beispiel #10
0
def comment_case_runs(request):
    """
    Add comment to one or more caseruns at a time.
    """
    data = request.POST.copy()
    comment = data.get('comment', None)
    if not comment:
        return JsonResponseBadRequest({'message': 'Comments needed'})
    run_ids = [int(item) for item in data.getlist('run')]
    if not run_ids:
        return JsonResponseBadRequest({'message': 'No runs selected.'})
    case_run_ids = (TestCaseRun.objects.filter(pk__in=run_ids).values_list(
        'pk', flat=True))
    if not case_run_ids:
        return JsonResponseBadRequest({'message': 'No caserun found.'})
    tcms.comments.models.add_comment(request.user, 'testruns.testcaserun',
                                     case_run_ids, comment,
                                     request.META.get('REMOTE_ADDR'))
    return JsonResponse({})
Beispiel #11
0
def post(request, template_name='comments/comments.html'):
    """Post a comment"""
    data = request.POST.copy()
    try:
        target, _ = post_comment(data, request.user,
                                 request.META.get('REMOTE_ADDR'))
    except InvalidCommentPostRequest as e:
        msg = f'Fail to add comment to object {e.target}'
        log.exception(msg)
        return JsonResponseBadRequest({'message': msg})
    return JsonResponse({})
Beispiel #12
0
    def post(self, request):
        group_name = request.POST.get('name')

        # Get the group name of environment from javascript
        if not group_name:
            return JsonResponseBadRequest({
                'message': 'Environment group name is required.'
            })

        if TCMSEnvGroup.objects.filter(name=group_name).exists():
            return JsonResponseBadRequest({
                'message': f'Environment group name "{group_name}" already'
                           f' exists, please choose another name.'
            })

        env = TCMSEnvGroup.objects.create(
            name=group_name,
            manager_id=request.user.id,
            modified_by_id=None)
        env.log_action(who=request.user,
                       new_value=f'Initial env group {env.name}')
        return JsonResponse({'env_group_id': env.id})
Beispiel #13
0
    def post(self, request, env_property_id):
        env_property = TCMSEnvProperty.objects.filter(pk=env_property_id).first()
        if env_property is None:
            return JsonResponseBadRequest({
                'message': f'Environment property with id {env_property_id} '
                           f'does not exist.'
            })

        new_name = request.POST.get('name', env_property.name)
        env_property.name = new_name
        env_property.save(update_fields=['name'])

        return JsonResponse({'id': env_property_id, 'name': new_name})
Beispiel #14
0
        def add(self):
            # TODO: make a migration for the permission
            if not self.request.user.has_perm('issuetracker.add_issue'):
                return JsonResponseForbidden({'message': 'Permission denied.'})

            form = CaseRunIssueForm(request.GET)

            if not form.is_valid():
                return JsonResponseBadRequest(
                    {'message': form_error_messages_to_list(form)})

            service = find_service(form.cleaned_data['tracker'])
            issue_key = form.cleaned_data['issue_key']
            link_et = form.cleaned_data['link_external_tracker']
            case_runs = form.cleaned_data['case_run']

            # FIXME: maybe, make sense to validate in the form.
            if not all(case_run.run_id == self.run.pk
                       for case_run in case_runs):
                return JsonResponseBadRequest({
                    'message':
                    f'Not all case runs belong to run {self.run.pk}.'
                })

            try:
                for case_run in case_runs:
                    service.add_issue(issue_key,
                                      case_run.case,
                                      case_run=case_run,
                                      add_case_to_issue=link_et)
            except ValidationError as e:
                logger.exception(
                    'Failed to add issue to case run %s. Error reported: %s',
                    form.case_run.pk, str(e))
                return JsonResponseBadRequest({'message': str(e)})

            return self.run_issues_info(case_runs)
Beispiel #15
0
def remove(request, link_id):
    """Remove a specific link with ID ``link_id``"""

    from django.forms import IntegerField
    from django.forms import ValidationError

    field = IntegerField(min_value=1)
    try:
        value = field.clean(link_id)
    except ValidationError as err:
        return JsonResponseBadRequest({
            'rc': 1,
            'response': '\n'.join(err.messages)
        })

    try:
        LinkReference.unlink(value)
    except Exception as err:
        return JsonResponseBadRequest({'rc': 1, 'response': str(err)})

    return JsonResponse({
        'rc': 0,
        'response': 'Link has been removed successfully.'
    })
Beispiel #16
0
    def post(self, request):
        property_ids = request.POST.getlist('id')

        if not property_ids:
            return JsonResponseBadRequest({
                'message': 'Missing environment property ids. Nothing changed.'
            })

        invalid_ids = list(filter(lambda item: not item.isdigit(), property_ids))
        if invalid_ids:
            if len(invalid_ids) > 1:
                msg = f'Environment property ids {", ".join(invalid_ids)} are not number.'
            else:
                msg = f'Environment property id {invalid_ids[0]} is not a number.'
            return JsonResponseBadRequest({'message': msg})

        new_status = request.POST.get('status')
        if new_status is None:
            return JsonResponseBadRequest({'message': 'Missing status.'})
        if new_status not in ['0', '1']:
            return JsonResponseBadRequest(
                {'message': f'Invalid status {new_status}.'})

        property_ids = list(map(int, property_ids))
        env_properties = TCMSEnvProperty.objects.filter(pk__in=property_ids)

        for env_property in env_properties:
            env_property.is_active = bool(int(new_status))
            env_property.save()

        # FIXME: why delete such properties?
        if not env_property.is_active:
            TCMSEnvGroupPropertyMap.objects.filter(
                property__id__in=property_ids).delete()

        return JsonResponse({'property_ids': property_ids})
Beispiel #17
0
    def _update_case_status(self):
        exists = TestCaseStatus.objects.filter(pk=self.new_value).exists()
        if not exists:
            raise ObjectDoesNotExist('The status you choose does not exist.')
        self.get_update_targets().update(
            **{str(self.target_field): self.new_value})

        # ###
        # Case is moved between Cases and Reviewing Cases tabs accoding to the
        # change of status. Meanwhile, the number of cases with each status
        # should be updated also.

        try:
            plan = plan_from_request_or_none(self.request)
        except Http404:
            return JsonResponseBadRequest({'message': 'No plan record found.'})
        else:
            if plan is None:
                return JsonResponseBadRequest(
                    {'message': 'No plan record found.'})

        confirm_status_name = 'CONFIRMED'
        plan.run_case = plan.case.filter(case_status__name=confirm_status_name)
        plan.review_case = plan.case.exclude(
            case_status__name=confirm_status_name)
        run_case_count = plan.run_case.count()
        case_count = plan.case.count()
        # FIXME: why not calculate review_case_count or run_case_count by using
        # substraction, which saves one SQL query.
        review_case_count = plan.review_case.count()

        return http.JsonResponse({
            'run_case_count': run_case_count,
            'case_count': case_count,
            'review_case_count': review_case_count,
        })
Beispiel #18
0
    def post(self, request):
        comments = django_comments.get_model().objects.filter(
            pk__in=request.POST.getlist('comment_id'),
            site__pk=settings.SITE_ID,
            is_removed=False,
            user_id=request.user.id)

        if not comments:
            return JsonResponseBadRequest(
                {'message': 'No incoming comment id exists.'})

        # Flag the comment as deleted instead of actually deleting it.
        for comment in comments:
            perform_delete(request, comment)

        return JsonResponse({})
Beispiel #19
0
def add(request):
    """Add new link to a specific target

    The target should be a valid model within TCMS, which are documented in
    ``LINKREF_TARGET``.

    Incoming request should be a POST request, and contains following
    arguments:

    - target: To which the new link will link to. The avialable target names
      are documented in the ``LINKREF_TARGET``.
    - target_id: the ID used to construct the concrete target instance, to
      which the new link will be linked.
    - name: a short description to this new link, and accept 64 characters at
      most.
    - url: the actual URL.
    """

    form = AddLinkReferenceForm(request.POST)
    if form.is_valid():
        name = form.cleaned_data['name']
        url = form.cleaned_data['url']
        target_id = form.cleaned_data['target_id']
        model_class = form.cleaned_data['target']

        model_instance = model_class.objects.get(pk=target_id)
        create_link(name=name, url=url, link_to=model_instance)

        return JsonResponse({
            'rc': 0,
            'response': 'ok',
            'data': {
                'name': name,
                'url': url
            }
        })

    else:
        return JsonResponseBadRequest({
            'rc': 1,
            'response': form.errors.as_text()
        })
Beispiel #20
0
    def post(self, request, plan_id):
        # Current we should rewrite all of cases belong to the plan.
        # Because the cases sortkey in database is chaos,
        # Most of them are None.

        if 'case' not in request.POST:
            return JsonResponseBadRequest(
                {'message': 'At least one case is required to re-order.'})

        plan = get_object_or_404(TestPlan, pk=int(plan_id))

        case_ids = [int(id) for id in request.POST.getlist('case')]
        cases = TestCase.objects.filter(pk__in=case_ids).only('pk')

        for case in cases:
            new_sort_key = (case_ids.index(case.pk) + 1) * 10
            TestCasePlan.objects.filter(plan=plan,
                                        case=case).update(sortkey=new_sort_key)

        return JsonResponse({})
Beispiel #21
0
    def post(self, request, plan_id):
        plan = get_object_or_404(TestPlan.objects.only('pk'), pk=int(plan_id))

        if 'case' not in request.POST:
            return JsonResponseBadRequest(
                {'message': 'At least one case is required to delete.'})

        cases = get_selected_testcases(request).only('pk')

        # Log Action
        plan_log = TCMSLog(model=plan)
        for case in cases:
            plan_log.make(
                who=request.user,
                new_value=f'Remove case {case.pk} from plan {plan.pk}')
            case.log_action(who=request.user,
                            new_value=f'Remove from plan {plan.pk}')
            plan.delete_case(case=case)

        return JsonResponse({})
Beispiel #22
0
 def form_invalid(self, form):
     return JsonResponseBadRequest(
         {'message': form_error_messages_to_list(form.errors)})
Beispiel #23
0
def update_case_run_status(request):
    """Update Case Run status."""
    now = datetime.datetime.now()

    data = request.POST.copy()
    ctype = data.get("content_type")
    vtype = data.get('value_type', 'str')
    object_pk_str = data.get("object_pk")
    field = data.get('field')
    value = data.get('value')

    object_pk = [int(a) for a in object_pk_str.split(',')]

    if not field or not value or not object_pk or not ctype:
        return JsonResponseBadRequest({
            'message':
            'Following fields are required - '
            'content_type, object_pk, field and value.'
        })

    # Convert the value type
    # FIXME: Django bug here: update() keywords must be strings
    field = str(field)

    value, error = get_value_by_type(value, vtype)
    if error:
        return JsonResponseBadRequest({'message': error})
    has_perms = check_permission(request, ctype)
    if not has_perms:
        return JsonResponseForbidden({'message': 'Permission Denied.'})

    model = utils.get_model(ctype)
    targets = model._default_manager.filter(pk__in=object_pk)

    if not targets:
        return JsonResponseBadRequest({'message': 'No record found'})
    if not hasattr(targets[0], field):
        return JsonResponseBadRequest(
            {'message': f'{ctype} has no field {field}'})

    if hasattr(targets[0], 'log_action'):
        for t in targets:
            try:
                t.log_action(who=request.user,
                             field=field,
                             original_value=getattr(t, field),
                             new_value=TestCaseRunStatus.id_to_name(value))
            except (AttributeError, User.DoesNotExist):
                pass
    objects_update(targets, **{field: value})

    if hasattr(model, 'mail_scene'):
        from tcms.core.mailto import mailto

        mail_context = model.mail_scene(
            objects=targets,
            field=field,
            value=value,
            ctype=ctype,
            object_pk=object_pk,
        )
        if mail_context:
            mail_context['context']['user'] = request.user
            mailto(**mail_context)

    # Special hacking for updating test case run status
    if ctype == 'testruns.testcaserun' and field == 'case_run_status':
        for t in targets:
            field = 'close_date'
            t.log_action(who=request.user,
                         field=field,
                         original_value=getattr(t, field) or '',
                         new_value=now)
            if t.tested_by != request.user:
                field = 'tested_by'
                t.log_action(who=request.user,
                             field=field,
                             original_value=getattr(t, field) or '',
                             new_value=request.user)

            field = 'assignee'
            try:
                assignee = t.assginee
                if assignee != request.user:
                    t.log_action(who=request.user,
                                 field=field,
                                 original_value=getattr(t, field) or '',
                                 new_value=request.user)
                    # t.assignee = request.user
                t.save()
            except (AttributeError, User.DoesNotExist):
                pass
        targets.update(close_date=now, tested_by=request.user)

    return JsonResponse({})
Beispiel #24
0
def info(request):
    """Return misc information"""
    class Objects:
        __all__ = [
            'builds', 'categories', 'components', 'envs', 'env_groups',
            'env_properties', 'env_values', 'tags', 'users', 'versions'
        ]

        def __init__(self, request, product_ids=None):
            self.request = request
            self.product_ids = product_ids
            self.internal_parameters = ('info_type', 'field', 'format')

        def builds(self):
            from tcms.management.models import TestBuild

            query = {
                'product_ids': self.product_ids,
                'is_active': self.request.GET.get('is_active')
            }
            return TestBuild.list(query)

        def categories(self):
            from tcms.testcases.models import TestCaseCategory
            return TestCaseCategory.objects.filter(
                product__in=self.product_ids)

        def components(self):
            from tcms.management.models import Component
            return Component.objects.filter(product__in=self.product_ids)

        def envs(self):
            from tcms.management.models import TestEnvironment
            return TestEnvironment.objects.filter(product__in=self.product_ids)

        def env_groups(self):
            from tcms.management.models import TCMSEnvGroup

            return TCMSEnvGroup.objects.all()

        def env_properties(self):
            from tcms.management.models import TCMSEnvGroup, TCMSEnvProperty

            if self.request.GET.get('env_group_id'):
                env_group = TCMSEnvGroup.objects.get(
                    id=self.request.GET['env_group_id'])
                return env_group.property.all()
            else:
                return TCMSEnvProperty.objects.all()

        def env_values(self):
            from tcms.management.models import TCMSEnvValue

            return TCMSEnvValue.objects.filter(
                property__id=self.request.GET.get('env_property_id'))

        def tags(self):
            query = strip_parameters(request.GET, self.internal_parameters)
            tags = TestTag.objects
            # Generate the string combination, because we are using
            # case sensitive table
            if query.get('name__startswith'):
                seq = get_string_combinations(query['name__startswith'])
                criteria = reduce(operator.or_,
                                  (models.Q(name__startswith=item)
                                   for item in seq))
                tags = tags.filter(criteria)
                del query['name__startswith']

            tags = tags.filter(**query).distinct()
            return tags

        def users(self):
            from django.contrib.auth.models import User

            query = strip_parameters(self.request.GET,
                                     self.internal_parameters)
            return User.objects.filter(**query)

        def versions(self):
            from tcms.management.models import Version
            return Version.objects.filter(product__in=self.product_ids)

    product_ids = []
    for s in request.GET.getlist('product_id'):
        if s.isdigit():
            product_ids.append(int(s))
        else:
            return JsonResponseBadRequest({
                'message':
                f'Invalid product id {s}. It must be a positive integer.'
            })

    objects = Objects(request=request, product_ids=product_ids)
    obj = getattr(objects, request.GET['info_type'], None)

    if obj:
        if request.GET.get('format') == 'ulli':
            field = request.GET.get('field', 'name')
            response_str = '<ul>'
            for o in obj():
                response_str += '<li>' + getattr(o, field, None) + '</li>'
            response_str += '</ul>'
            return HttpResponse(response_str)

        return JsonResponse(json.loads(
            serializers.serialize(request.GET.get('format', 'json'),
                                  obj(),
                                  fields=('name', 'value'))),
                            safe=False)

    return JsonResponseBadRequest({'message': 'Unrecognizable infotype'})
Beispiel #25
0
def manage_case_run_issues(request, run_id):
    """Process the issues for case runs."""
    class CaseRunIssueActions:
        __all__ = ['add', 'remove']

        def __init__(self, request, run):
            self.request = request
            self.run = run

        def add(self):
            # TODO: make a migration for the permission
            if not self.request.user.has_perm('issuetracker.add_issue'):
                return JsonResponseForbidden({'message': 'Permission denied.'})

            form = CaseRunIssueForm(request.GET)

            if not form.is_valid():
                return JsonResponseBadRequest(
                    {'message': form_error_messages_to_list(form)})

            service = find_service(form.cleaned_data['tracker'])
            issue_key = form.cleaned_data['issue_key']
            link_et = form.cleaned_data['link_external_tracker']
            case_runs = form.cleaned_data['case_run']

            # FIXME: maybe, make sense to validate in the form.
            if not all(case_run.run_id == self.run.pk
                       for case_run in case_runs):
                return JsonResponseBadRequest({
                    'message':
                    f'Not all case runs belong to run {self.run.pk}.'
                })

            try:
                for case_run in case_runs:
                    service.add_issue(issue_key,
                                      case_run.case,
                                      case_run=case_run,
                                      add_case_to_issue=link_et)
            except ValidationError as e:
                logger.exception(
                    'Failed to add issue to case run %s. Error reported: %s',
                    form.case_run.pk, str(e))
                return JsonResponseBadRequest({'message': str(e)})

            return self.run_issues_info(case_runs)

        def remove(self):
            if not self.request.user.has_perm('issuetracker.delete_issue'):
                return JsonResponseForbidden({'message': 'Permission denied.'})

            class RemoveIssueForm(forms.Form):
                issue_key = forms.CharField()
                case_run = forms.ModelMultipleChoiceField(
                    queryset=TestCaseRun.objects.all().only('pk'),
                    error_messages={
                        'required': 'Case run id is missed.',
                        'invalid_pk_value': 'Case run %(pk)s does not exist.',
                    },
                )

            form = RemoveIssueForm(request.GET)
            if not form.is_valid():
                return JsonResponseBadRequest(
                    {'message': form_error_messages_to_list(form)})

            issue_key = form.cleaned_data['issue_key']
            case_runs = form.cleaned_data['case_run']
            for case_run in case_runs:
                try:
                    case_run.remove_issue(issue_key)
                except Exception:
                    msg = 'Failed to remove issue {} from case run {}'.format(
                        issue_key, case_run.pk)
                    logger.exception(msg)
                    return JsonResponseBadRequest({'message': msg})

            return self.run_issues_info(case_runs)

        def run_issues_info(self, case_runs):
            """Return a JSON response including run's issues info"""
            return JsonResponse({
                # The total number of issues this run has
                'run_issues_count':
                self.run.get_issues_count(),

                # The number of issues each of case run has
                'caserun_issues_count':
                Issue.count_by_case_run(list(map(attrgetter('pk'), case_runs)))
            })

    try:
        run = get_object_or_404(TestRun, pk=run_id)
    except Http404:
        return JsonResponseNotFound(
            {'message': f'Test run {run_id} does not exist.'})

    crba = CaseRunIssueActions(request=request, run=run)

    if not request.GET.get('a') in crba.__all__:
        return JsonResponseBadRequest({'message': 'Unrecognizable actions'})

    func = getattr(crba, request.GET['a'])
    return func()