'Bulk Mutate a List of Issues
Bulk mutate various attributes on issues.  The list of issues
to modify is given through the `id` query parameter.  It is repeated
for each issue that should be modified.
- For non-status updates, the `id` query parameter is required.
- For status updates, the `id` query parameter may be omitted
for a batch "update all" query.
- An optional `status` query parameter may be used to restrict
mutations to only events with the given status.
The following attributes can be modified and are supplied as
JSON object in the body:
If any ids are out of scope this operation will succeed without
any data mutation.
:qparam int id: a list of IDs of the issues to be mutated.  This
parameter shall be repeated for each issue.  It
is optional only if a status is mutated in which
case an implicit `update all` is assumed.
:qparam string status: optionally limits the query to issues of the
specified status.  Valid values are
``"resolved"``, ``"unresolved"`` and
``"ignored"``.
:pparam string organization_slug: the slug of the organization the
issues belong to.
:pparam string project_slug: the slug of the project the issues
belong to.
:param string status: the new status for the issues.  Valid values
are ``"resolved"``, ``resolvedInNextRelease``,
``"unresolved"``, and ``"ignored"``.
:param int ignoreDuration: the number of minutes to ignore this issue.
:param boolean isPublic: sets the issue to public or private.
:param boolean merge: allows to merge or unmerge different issues.
:param string assignedTo: the username of the user that should be
assigned to this issue.
:param boolean hasSeen: in case this API call is invoked with a user
context this allows changing of the flag
that indicates if the user has seen the
event.
:param boolean isBookmarked: in case this API call is invoked with a
user context this allows changing of
the bookmark flag.
:auth: required'

@attach_scenarios([bulk_update_issues_scenario])
def put(self, request, project):

    group_ids = request.GET.getlist('id')
    if group_ids:
        group_list = Group.objects.filter(project=project, id__in=group_ids)
        group_ids = [g.id for g in group_list]
        if (not group_ids):
            return Response(status=204)
    else:
        group_list = None
    serializer = GroupValidator(data=request.DATA, partial=True, context={'project': project})
    if (not serializer.is_valid()):
        return Response(serializer.errors, status=400)
    result = dict(serializer.object)
    acting_user = (request.user if request.user.is_authenticated() else None)
    if (not group_ids):
        try:
            query_kwargs = self._build_query_params_from_request(request, project)
        except ValidationError as exc:
            return Response({'detail': six.text_type(exc)}, status=400)
        query_kwargs['limit'] = 1000
        cursor_result = search.query(**query_kwargs)
        group_list = list(cursor_result)
        group_ids = [g.id for g in group_list]
    is_bulk = (len(group_ids) > 1)
    queryset = Group.objects.filter(id__in=group_ids)
    discard = result.get('discard')
    if discard:
        if (not features.has('projects:custom-filters', project, actor=request.user)):
            return Response({'detail': ['You    do    not    have    that    feature    enabled']}, status=400)
        group_list = list(queryset)
        groups_to_delete = []
        for group in group_list:
            with transaction.atomic():
                try:
                    tombstone = GroupTombstone.objects.create(previous_group_id=group.id, actor_id=(acting_user.id if acting_user else None), **{name: getattr(group, name) for name in TOMBSTONE_FIELDS_FROM_GROUP})
                except IntegrityError:
                    pass
                else:
                    groups_to_delete.append(group)
                    GroupHash.objects.filter(group=group).update(group=None, group_tombstone_id=tombstone.id)
        self._delete_groups(request, project, groups_to_delete)
        return Response(status=204)
    statusDetails = result.pop('statusDetails', result)
    status = result.get('status')
    if (status in ('resolved', 'resolvedInNextRelease')):
        if ((status == 'resolvedInNextRelease') or statusDetails.get('inNextRelease')):
            release = Release.objects.filter(projects=project, organization_id=project.organization_id).order_by('-date_added')[0]
            activity_type = Activity.SET_RESOLVED_IN_RELEASE
            activity_data = {'version': ''}
            status_details = {'inNextRelease': True, 'actor': serialize(extract_lazy_object(request.user), request.user)}
            res_type = GroupResolution.Type.in_next_release
            res_status = GroupResolution.Status.pending
        elif statusDetails.get('inRelease'):
            release = statusDetails['inRelease']
            activity_type = Activity.SET_RESOLVED_IN_RELEASE
            activity_data = {'version': release.version}
            status_details = {'inRelease': release.version, 'actor': serialize(extract_lazy_object(request.user), request.user)}
            res_type = GroupResolution.Type.in_release
            res_status = GroupResolution.Status.resolved
        else:
            release = None
            activity_type = Activity.SET_RESOLVED
            activity_data = {}
            status_details = {}
        now = timezone.now()
        for group in group_list:
            with transaction.atomic():
                if release:
                    resolution_params = {'release': release, 'type': res_type, 'status': res_status, 'actor_id': (request.user.id if request.user.is_authenticated() else None)}
                    (resolution, created) = GroupResolution.objects.get_or_create(group=group, defaults=resolution_params)
                    if (not created):
                        resolution.update(datetime=timezone.now(), **resolution_params)
                else:
                    resolution = None
                affected = Group.objects.filter(id=group.id).update(status=GroupStatus.RESOLVED, resolved_at=now)
                if (not resolution):
                    created = affected
                group.status = GroupStatus.RESOLVED
                group.resolved_at = now
                self._subscribe_and_assign_issue(acting_user, group, result)
                if created:
                    activity = Activity.objects.create(project=group.project, group=group, type=activity_type, user=acting_user, ident=(resolution.id if resolution else None), data=activity_data)
                    if (not is_bulk):
                        activity.send_notification()
            issue_resolved_in_release.send(group=group, project=project, sender=acting_user)
        result.update({'status': 'resolved', 'statusDetails': status_details})
    elif status:
        new_status = STATUS_CHOICES[result['status']]
        with transaction.atomic():
            happened = queryset.exclude(status=new_status).update(status=new_status)
            GroupResolution.objects.filter(group__in=group_ids).delete()
            if (new_status == GroupStatus.IGNORED):
                ignore_duration = ((statusDetails.pop('ignoreDuration', None) or statusDetails.pop('snoozeDuration', None)) or None)
                ignore_count = (statusDetails.pop('ignoreCount', None) or None)
                ignore_window = (statusDetails.pop('ignoreWindow', None) or None)
                ignore_user_count = (statusDetails.pop('ignoreUserCount', None) or None)
                ignore_user_window = (statusDetails.pop('ignoreUserWindow', None) or None)
                if (ignore_duration or ignore_count or ignore_user_count):
                    if ignore_duration:
                        ignore_until = (timezone.now() + timedelta(minutes=ignore_duration))
                    else:
                        ignore_until = None
                    for group in group_list:
                        state = {}
                        if (ignore_count and (not ignore_window)):
                            state['times_seen'] = group.times_seen
                        if (ignore_user_count and (not ignore_user_window)):
                            state['users_seen'] = group.count_users_seen()
                        GroupSnooze.objects.create_or_update(group=group, values={'until': ignore_until, 'count': ignore_count, 'window': ignore_window, 'user_count': ignore_user_count, 'user_window': ignore_user_window, 'state': state, 'actor_id': (request.user.id if request.user.is_authenticated() else None)})
                        result['statusDetails'] = {'ignoreCount': ignore_count, 'ignoreUntil': ignore_until, 'ignoreUserCount': ignore_user_count, 'ignoreUserWindow': ignore_user_window, 'ignoreWindow': ignore_window, 'actor': serialize(extract_lazy_object(request.user), request.user)}
                else:
                    GroupSnooze.objects.filter(group__in=group_ids).delete()
                    ignore_until = None
                    result['statusDetails'] = {}
            else:
                result['statusDetails'] = {}
        if (group_list and happened):
            if (new_status == GroupStatus.UNRESOLVED):
                activity_type = Activity.SET_UNRESOLVED
                activity_data = {}
            elif (new_status == GroupStatus.IGNORED):
                activity_type = Activity.SET_IGNORED
                activity_data = {'ignoreCount': ignore_count, 'ignoreDuration': ignore_duration, 'ignoreUntil': ignore_until, 'ignoreUserCount': ignore_user_count, 'ignoreUserWindow': ignore_user_window, 'ignoreWindow': ignore_window}
            for group in group_list:
                group.status = new_status
                activity = Activity.objects.create(project=group.project, group=group, type=activity_type, user=acting_user, data=activity_data)
                if (not is_bulk):
                    if acting_user:
                        GroupSubscription.objects.subscribe(user=acting_user, group=group, reason=GroupSubscriptionReason.status_change)
                    activity.send_notification()
    if ('assignedTo' in result):
        if result['assignedTo']:
            for group in group_list:
                GroupAssignee.objects.assign(group, result['assignedTo'], acting_user)
                if (('isSubscribed' not in result) or (result['assignedTo'] != request.user)):
                    GroupSubscription.objects.subscribe(group=group, user=result['assignedTo'], reason=GroupSubscriptionReason.assigned)
            result['assignedTo'] = serialize(result['assignedTo'])
        else:
            for group in group_list:
                GroupAssignee.objects.deassign(group, acting_user)
    if (result.get('hasSeen') and project.member_set.filter(user=acting_user).exists()):
        for group in group_list:
            (instance, created) = create_or_update(GroupSeen, group=group, user=acting_user, project=group.project, values={'last_seen': timezone.now()})
    elif (result.get('hasSeen') is False):
        GroupSeen.objects.filter(group__in=group_ids, user=acting_user).delete()
    if result.get('isBookmarked'):
        for group in group_list:
            GroupBookmark.objects.get_or_create(project=project, group=group, user=acting_user)
            GroupSubscription.objects.subscribe(user=acting_user, group=group, reason=GroupSubscriptionReason.bookmark)
    elif (result.get('isBookmarked') is False):
        GroupBookmark.objects.filter(group__in=group_ids, user=acting_user).delete()
    if (result.get('isSubscribed') in (True, False)):
        is_subscribed = result['isSubscribed']
        for group in group_list:
            GroupSubscription.objects.create_or_update(user=acting_user, group=group, project=project, values={'is_active': is_subscribed, 'reason': GroupSubscriptionReason.unknown})
        result['subscriptionDetails'] = {'reason': SUBSCRIPTION_REASON_MAP.get(GroupSubscriptionReason.unknown, 'unknown')}
    if result.get('isPublic'):
        queryset.update(is_public=True)
        for group in group_list:
            if group.is_public:
                continue
            group.is_public = True
            Activity.objects.create(project=group.project, group=group, type=Activity.SET_PUBLIC, user=acting_user)
    elif (result.get('isPublic') is False):
        queryset.update(is_public=False)
        for group in group_list:
            if (not group.is_public):
                continue
            group.is_public = False
            Activity.objects.create(project=group.project, group=group, type=Activity.SET_PRIVATE, user=acting_user)
    if (result.get('merge') and (len(group_list) > 1)):
        primary_group = sorted(group_list, key=(lambda x: (- x.times_seen)))[0]
        children = []
        transaction_id = uuid4().hex
        for group in group_list:
            if (group == primary_group):
                continue
            children.append(group)
            group.update(status=GroupStatus.PENDING_MERGE)
            merge_group.delay(from_object_id=group.id, to_object_id=primary_group.id, transaction_id=transaction_id)
        Activity.objects.create(project=primary_group.project, group=primary_group, type=Activity.MERGE, user=acting_user, data={'issues': [{'id': c.id} for c in children]})
        result['merge'] = {'parent': six.text_type(primary_group.id), 'children': [six.text_type(g.id) for g in children]}
    return Response(result)
