Exemple #1
0
    def _ensure_hashes_merged(self, group, hash_list):
        # TODO(dcramer): there is a race condition with selecting/updating
        # in that another group could take ownership of the hash
        bad_hashes = GroupHash.objects.filter(
            project=group.project,
            hash__in=hash_list,
        ).exclude(
            group=group,
        )
        if not bad_hashes:
            return

        for hash in bad_hashes:
            if hash.group_id:
                merge_group.delay(
                    from_object_id=hash.group_id,
                    to_object_id=group.id,
                    transaction_id=uuid4().hex,
                )

        return GroupHash.objects.filter(
            project=group.project,
            hash__in=[h.hash for h in bad_hashes],
        ).update(
            group=group,
        )
Exemple #2
0
    def _ensure_hashes_merged(self, group, hash_list):
        # TODO(dcramer): there is a race condition with selecting/updating
        # in that another group could take ownership of the hash
        # XXX: This function is currently unused, and hasn't been updated to
        # take `GroupHash.state` into account.
        bad_hashes = GroupHash.objects.filter(
            id__in=[h.id for h in hash_list],
        ).exclude(
            group=group,
        )
        if not bad_hashes:
            return

        for hash in bad_hashes:
            if hash.group_id:
                merge_group.delay(
                    from_object_id=hash.group_id,
                    to_object_id=group.id,
                    transaction_id=uuid4().hex,
                )

        return GroupHash.objects.filter(
            project=group.project,
            hash__in=[h.hash for h in bad_hashes],
        ).update(
            group=group,
        )
Exemple #3
0
    def _ensure_hashes_merged(self, group, hash_list):
        # TODO(dcramer): there is a race condition with selecting/updating
        # in that another group could take ownership of the hash
        # XXX: This function is currently unused, and hasn't been updated to
        # take `GroupHash.state` into account.
        bad_hashes = GroupHash.objects.filter(
            id__in=[h.id for h in hash_list],
        ).exclude(
            group=group,
        )
        if not bad_hashes:
            return

        for hash in bad_hashes:
            if hash.group_id:
                merge_group.delay(
                    from_object_id=hash.group_id,
                    to_object_id=group.id,
                    transaction_id=uuid4().hex,
                )

        return GroupHash.objects.filter(
            project=group.project,
            hash__in=[h.hash for h in bad_hashes],
        ).update(
            group=group,
        )
Exemple #4
0
    def put(self, request, project_id):
        """
        Bulk mutate a list of aggregates

        Bulk mutate various attributes on aggregates.

            {method} {path}?id=1&id=2&id=3
            {{
              "status": "resolved",
              "isBookmarked": true
            }}

        - For non-status updates, only queries by 'id' are accepted.
        - For status updates, the 'id' parameter may be omitted for a batch
        "update all" query.

        Attributes:

        - status: resolved, unresolved, muted
        - isBookmarked: true, false
        - merge: true, false

        If any ids are out of scope this operation will succeed without any data
        mutation.
        """
        project = Project.objects.get_from_cache(
            id=project_id,
        )

        assert_perm(project, request.user, request.auth)

        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project, id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]

            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        if not serializer.is_valid():
            return Response(status=400)

        result = serializer.object

        # validate that we've passed a selector for non-status bulk operations
        if not group_ids and result.get('isBookmarked') is not None:
            return Response(status=400)

        if group_ids:
            filters = Q(id__in=group_ids)
        else:
            filters = Q(project=project)

        if result.get('status') == 'resolved':
            now = timezone.now()

            happened = Group.objects.filter(filters).exclude(
                status=GroupStatus.RESOLVED,
            ).update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    create_or_update(
                        Activity,
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=request.user,
                    )
        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            happened = Group.objects.filter(filters).exclude(
                status=new_status,
            ).update(
                status=new_status,
            )
            if group_list and happened:
                for group in group_list:
                    group.status = new_status

        if result.get('isBookmarked'):
            for group in group_list:
                GroupBookmark.objects.get_or_create(
                    project=group.project,
                    group=group,
                    user=request.user,
                )
        elif result.get('isBookmarked') is False:
            GroupBookmark.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        if result.get('merge') and len(group_list) > 1:
            primary_group = sorted(group_list, key=lambda x: -x.times_seen)[0]
            for group in group_list:
                if group == primary_group:
                    continue
                merge_group.delay(
                    from_object_id=group.id,
                    to_object_id=primary_group.id,
                )

        if group_list:
            GroupMeta.objects.populate_cache(group_list)
            # TODO(dcramer): we need create a public API for 'sort_value'
            context = serialize(list(group_list), request.user)
            return Response(context)

        return Response(status=204)
    def put(self, request, project):
        """
        Bulk Mutate a List of Aggregates
        ````````````````````````````````

        Bulk mutate various attributes on aggregates.  The list of groups
        to modify is given through the `id` query parameter.  It is repeated
        for each group 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 groups to be mutated.  This
                        parameter shall be repeated for each group.  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 groups of the
                               specified status.  Valid values are
                               ``"resolved"``, ``"unresolved"`` and
                               ``"muted"``.
        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        :param string status: the new status for the groups.  Valid values
                              are ``"resolved"``, ``"unresolved"`` and
                              ``"muted"``.
        :param int snoozeDuration: the number of minutes to mute this issue.
        :param boolean isPublic: sets the group to public or private.
        :param boolean merge: allows to merge or unmerge different groups.
        :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
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project,
                                              id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]
            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        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': unicode(exc)}, status=400)

            # bulk mutations are limited to 1000 items
            # TODO(dcramer): it'd be nice to support more than this, but its
            # a bit too complicated right now
            query_kwargs['limit'] = 1000

            cursor_result = search.query(**query_kwargs)

            group_list = list(cursor_result)
            group_ids = [g.id for g in group_list]

        queryset = Group.objects.filter(id__in=group_ids, )

        if result.get('status') == 'resolvedInNextRelease':
            try:
                release = Release.objects.filter(
                    project=project, ).order_by('-date_added')[0]
            except IndexError:
                return Response(
                    '{"detail": "No release data present in the system to indicate form a basis for \'Next Release\'"}',
                    status=400)

            now = timezone.now()

            for group in group_list:
                try:
                    with transaction.atomic():
                        resolution, created = GroupResolution.objects.create(
                            group=group,
                            release=release,
                        ), True
                except IntegrityError:
                    resolution, created = GroupResolution.objects.get(
                        group=group, ), False

                if created:
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED_IN_RELEASE,
                        user=acting_user,
                        ident=resolution.id,
                        data={
                            # no version yet
                            'version': '',
                        })
                    activity.send_notification()

            queryset.update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            result.update({
                'status': 'resolved',
                'statusDetails': {
                    'inNextRelease': True,
                },
            })

        elif result.get('status') == 'resolved':
            now = timezone.now()

            happened = queryset.exclude(status=GroupStatus.RESOLVED, ).update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            GroupResolution.objects.filter(group__in=group_ids, ).delete()

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=acting_user,
                    )
                    activity.send_notification()

            result['statusDetails'] = {}

        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            happened = queryset.exclude(status=new_status, ).update(
                status=new_status, )

            GroupResolution.objects.filter(group__in=group_ids, ).delete()

            if new_status == GroupStatus.MUTED:
                snooze_duration = result.pop('snoozeDuration', None)
                if snooze_duration:
                    snooze_until = timezone.now() + timedelta(
                        minutes=snooze_duration, )
                    for group in group_list:
                        GroupSnooze.objects.create_or_update(group=group,
                                                             values={
                                                                 'until':
                                                                 snooze_until,
                                                             })
                        result['statusDetails'] = {
                            'snoozeUntil': snooze_until,
                        }
                else:
                    GroupSnooze.objects.filter(group__in=group_ids, ).delete()
                    snooze_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.MUTED:
                    activity_type = Activity.SET_MUTED
                    activity_data = {
                        'snoozeUntil': snooze_until,
                        'snoozeDuration': snooze_duration,
                    }

                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,
                    )
                    activity.send_notification()

        if result.get('hasSeen') and project.member_set.filter(
                user=request.user).exists():
            for group in group_list:
                instance, created = create_or_update(GroupSeen,
                                                     group=group,
                                                     user=request.user,
                                                     project=group.project,
                                                     values={
                                                         'last_seen':
                                                         timezone.now(),
                                                     })
        elif result.get('hasSeen') is False:
            GroupSeen.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isBookmarked'):
            for group in group_list:
                GroupBookmark.objects.get_or_create(
                    project=project,
                    group=group,
                    user=request.user,
                )
        elif result.get('isBookmarked') is False:
            GroupBookmark.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        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,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        if result.get('merge') and len(group_list) > 1:
            primary_group = sorted(group_list, key=lambda x: -x.times_seen)[0]
            children = []
            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,
                )

            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': str(primary_group.id),
                'children': [str(g.id) for g in children],
            }

        return Response(result)
Exemple #6
0
    def put(self, request, project):
        """
        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
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project, id__in=group_ids)
            # filter down group ids to only valid matches
            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)

            # bulk mutations are limited to 1000 items
            # TODO(dcramer): it'd be nice to support more than this, but its
            # a bit too complicated right now
            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,
        )

        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 = {
                    # no version yet
                    'version': '',
                }
                status_details = {
                    'inNextRelease': True,
                }
                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 = {
                    # no version yet
                    'version': release.version,
                }
                status_details = {
                    'inRelease': release.version,
                }
                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 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,
                    ).exclude(
                        status=GroupStatus.RESOLVED,
                    ).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,
                        )
                        # TODO(dcramer): we need a solution for activity rollups
                        # before sending notifications on bulk changes
                        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,
                                }
                            )
                            result['statusDetails'] = {
                                'ignoreCount': ignore_count,
                                'ignoreUntil': ignore_until,
                                'ignoreUserCount': ignore_user_count,
                                'ignoreUserWindow': ignore_user_window,
                                'ignoreWindow': ignore_window,
                            }
                    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,
                    )
                    # TODO(dcramer): we need a solution for activity rollups
                    # before sending notifications on bulk changes
                    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()

        # TODO(dcramer): we could make these more efficient by first
        # querying for rich rows are present (if N > 2), flipping the flag
        # on those rows, and then creating the missing rows
        if result.get('isSubscribed') in (True, False):
            is_subscribed = result['isSubscribed']
            for group in group_list:
                # NOTE: Subscribing without an initiating event (assignment,
                # commenting, etc.) clears out the previous subscription reason
                # to avoid showing confusing messaging as a result of this
                # action. It'd be jarring to go directly from "you are not
                # subscribed" to "you were subscribed due since you were
                # assigned" just by clicking the "subscribe" button (and you
                # may no longer be assigned to the issue anyway.)
                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,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        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)
Exemple #7
0
    def put(self, request, project):
        """
        Bulk Mutate a List of Aggregates
        ````````````````````````````````

        Bulk mutate various attributes on aggregates.  The list of groups
        to modify is given through the `id` query parameter.  It is repeated
        for each group 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 groups to be mutated.  This
                        parameter shall be repeated for each group.  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 groups of the
                               specified status.  Valid values are
                               ``"resolved"``, ``"unresolved"`` and
                               ``"muted"``.
        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        :param string status: the new status for the groups.  Valid values
                              are ``"resolved"``, ``"unresolved"`` and
                              ``"muted"``.
        :param boolean isPublic: sets the group to public or private.
        :param boolean merge: allows to merge or unmerge different groups.
        :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
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project,
                                              id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]

            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        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

        # validate that we've passed a selector for non-status bulk operations
        if not group_ids and result.keys() != ['status']:
            return Response(
                '{"detail": "You must specify a list of IDs for this operation"}',
                status=400)

        if group_ids:
            filters = [Q(id__in=group_ids)]
        else:
            filters = [Q(project=project)]

        if request.GET.get('status'):
            try:
                status_filter = STATUS_CHOICES[request.GET['status']]
            except KeyError:
                return Response('{"detail": "Invalid status"}', status=400)
            filters.append(Q(status=status_filter))

        if result.get('status') == 'resolved':
            now = timezone.now()

            happened = Group.objects.filter(*filters).exclude(
                status=GroupStatus.RESOLVED, ).update(
                    status=GroupStatus.RESOLVED,
                    resolved_at=now,
                )

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=acting_user,
                    )
                    activity.send_notification()

        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            happened = Group.objects.filter(*filters).exclude(
                status=new_status, ).update(status=new_status, )
            if group_list and happened:
                if new_status == GroupStatus.UNRESOLVED:
                    activity_type = Activity.SET_UNRESOLVED
                elif new_status == GroupStatus.MUTED:
                    activity_type = Activity.SET_MUTED

                for group in group_list:
                    group.status = new_status
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=activity_type,
                        user=acting_user,
                    )
                    activity.send_notification()

        if result.get('hasSeen') and project.member_set.filter(
                user=request.user).exists():
            for group in group_list:
                instance, created = create_or_update(GroupSeen,
                                                     group=group,
                                                     user=request.user,
                                                     project=group.project,
                                                     values={
                                                         'last_seen':
                                                         timezone.now(),
                                                     })
        elif result.get('hasSeen') is False:
            GroupSeen.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isBookmarked'):
            for group in group_list:
                GroupBookmark.objects.get_or_create(
                    project=group.project,
                    group=group,
                    user=request.user,
                )
        elif result.get('isBookmarked') is False:
            GroupBookmark.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isPublic'):
            Group.objects.filter(id__in=group_ids, ).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:
            Group.objects.filter(id__in=group_ids, ).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,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        if result.get('merge') and len(group_list) > 1:
            primary_group = sorted(group_list, key=lambda x: -x.times_seen)[0]
            children = []
            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,
                )
            result['merge'] = {
                'parent': str(primary_group.id),
                'children': [str(g.id) for g in children],
            }

        return Response(result)
    def put(self, request, project):
        """
        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"``, ``"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 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
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project, id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]
            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        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)

            # bulk mutations are limited to 1000 items
            # TODO(dcramer): it'd be nice to support more than this, but its
            # a bit too complicated right now
            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,
        )

        if result.get('status') == 'resolvedInNextRelease':
            try:
                release = Release.objects.filter(
                    project=project,
                ).order_by('-date_added')[0]
            except IndexError:
                return Response('{"detail": "No release data present in the system to indicate form a basis for \'Next Release\'"}', status=400)

            now = timezone.now()

            for group in group_list:
                try:
                    with transaction.atomic():
                        resolution, created = GroupResolution.objects.create(
                            group=group,
                            release=release,
                        ), True
                except IntegrityError:
                    resolution, created = GroupResolution.objects.get(
                        group=group,
                    ), False

                if acting_user:
                    GroupSubscription.objects.subscribe(
                        user=acting_user,
                        group=group,
                        reason=GroupSubscriptionReason.status_change,
                    )

                if created:
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED_IN_RELEASE,
                        user=acting_user,
                        ident=resolution.id,
                        data={
                            # no version yet
                            'version': '',
                        }
                    )
                    # TODO(dcramer): we need a solution for activity rollups
                    # before sending notifications on bulk changes
                    if not is_bulk:
                        activity.send_notification()

            queryset.update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            result.update({
                'status': 'resolved',
                'statusDetails': {
                    'inNextRelease': True,
                },
            })

        elif result.get('status') == 'resolved':
            now = timezone.now()

            happened = queryset.exclude(
                status=GroupStatus.RESOLVED,
            ).update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            GroupResolution.objects.filter(
                group__in=group_ids,
            ).delete()

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    if acting_user:
                        GroupSubscription.objects.subscribe(
                            user=acting_user,
                            group=group,
                            reason=GroupSubscriptionReason.status_change,
                        )
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=acting_user,
                    )
                    # TODO(dcramer): we need a solution for activity rollups
                    # before sending notifications on bulk changes
                    if not is_bulk:
                        activity.send_notification()

            result['statusDetails'] = {}

        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            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 = (
                    result.pop('ignoreDuration', None)
                    or result.pop('snoozeDuration', None)
                )
                if ignore_duration:
                    ignore_until = timezone.now() + timedelta(
                        minutes=ignore_duration,
                    )
                    for group in group_list:
                        GroupSnooze.objects.create_or_update(
                            group=group,
                            values={
                                'until': ignore_until,
                            }
                        )
                        result['statusDetails'] = {
                            'ignoreUntil': ignore_until,
                        }
                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 = {
                        'ignoreUntil': ignore_until,
                        'ignoreDuration': ignore_duration,
                    }

                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,
                    )
                    # TODO(dcramer): we need a solution for activity rollups
                    # before sending notifications on bulk changes
                    if not is_bulk:
                        if acting_user:
                            GroupSubscription.objects.subscribe(
                                user=acting_user,
                                group=group,
                                reason=GroupSubscriptionReason.status_change,
                            )
                        activity.send_notification()

        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()

        # TODO(dcramer): we could make these more efficient by first
        # querying for rich rows are present (if N > 2), flipping the flag
        # on those rows, and then creating the missing rows
        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},
                )

        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,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        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)
Exemple #9
0
    def put(self, request, project):
        """
        Bulk mutate a list of aggregates

        Bulk mutate various attributes on aggregates.

            {method} {path}?id=1&id=2&id=3
            {{
              "status": "resolved",
              "isBookmarked": true
            }}

        - For non-status updates, the 'id' parameter is required.
        - For status updates, the 'id' parameter may be omitted for a batch
          "update all" query.
        - An optional 'status' parameter may be used to restrict mutations to
          only events with the given status.

        For example, to resolve all aggregates (project-wide):

            {method} {path}
            {{
                "status": "resolved"
            }}

        Attributes:

        - status: resolved, unresolved, muted
        - hasSeen: true, false
        - isBookmarked: true, false
        - isPublic: true, false
        - merge: true, false

        If any ids are out of scope this operation will succeed without any data
        mutation.
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project,
                                              id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]

            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)

        result = serializer.object

        # validate that we've passed a selector for non-status bulk operations
        if not group_ids and result.keys() != ['status']:
            return Response(
                '{"detail": "You must specify a list of IDs for this operation"}',
                status=400)

        if group_ids:
            filters = [Q(id__in=group_ids)]
        else:
            filters = [Q(project=project)]

        if request.GET.get('status'):
            try:
                status_filter = STATUS_CHOICES[request.GET['status']]
            except KeyError:
                return Response('{"detail": "Invalid status"}', status=400)
            filters.append(Q(status=status_filter))

        if result.get('status') == 'resolved':
            now = timezone.now()

            happened = Group.objects.filter(*filters).exclude(
                status=GroupStatus.RESOLVED, ).update(
                    status=GroupStatus.RESOLVED,
                    resolved_at=now,
                )

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=request.user,
                    )
                    activity.send_notification()

        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            happened = Group.objects.filter(*filters).exclude(
                status=new_status, ).update(status=new_status, )
            if group_list and happened:
                if new_status == GroupStatus.UNRESOLVED:
                    activity_type = Activity.SET_UNRESOLVED
                elif new_status == GroupStatus.MUTED:
                    activity_type = Activity.SET_MUTED

                for group in group_list:
                    group.status = new_status
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=activity_type,
                        user=request.user,
                    )
                    activity.send_notification()

        if result.get('hasSeen'):
            for group in group_list:
                instance, created = create_or_update(GroupSeen,
                                                     group=group,
                                                     user=request.user,
                                                     project=group.project,
                                                     values={
                                                         'last_seen':
                                                         timezone.now(),
                                                     })
        elif result.get('hasSeen') is False:
            GroupSeen.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isBookmarked'):
            for group in group_list:
                GroupBookmark.objects.get_or_create(
                    project=group.project,
                    group=group,
                    user=request.user,
                )
        elif result.get('isBookmarked') is False:
            GroupBookmark.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isPublic'):
            Group.objects.filter(id__in=group_ids, ).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=request.user,
                )
        elif result.get('isPublic') is False:
            Group.objects.filter(id__in=group_ids, ).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=request.user,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        if result.get('merge') and len(group_list) > 1:
            primary_group = sorted(group_list, key=lambda x: -x.times_seen)[0]
            for group in group_list:
                if group == primary_group:
                    continue
                merge_group.delay(
                    from_object_id=group.id,
                    to_object_id=primary_group.id,
                )

        return Response(dict(result))
Exemple #10
0
    def put(self, request, project):
        """
        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 actor id (or username) of the user or team 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
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(
                project=project, id__in=group_ids)
            # filter down group ids to only valid matches
            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:
                # bulk mutations are limited to 1000 items
                # TODO(dcramer): it'd be nice to support more than this, but its
                # a bit too complicated right now
                cursor_result, _ = self._search(request, project, {
                    'limit': 1000,
                    'paginator_options': {'max_limit': 1000},
                })
            except ValidationError as exc:
                return Response({'detail': six.text_type(exc)}, status=400)

            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:discard-groups', 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:
                        # in this case, a tombstone has already been created
                        # for a group, so no hash updates are necessary
                        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,
                ).extra(select={
                    'sort': 'COALESCE(date_released, date_added)',
                }).order_by('-sort')[0]
                activity_type = Activity.SET_RESOLVED_IN_RELEASE
                activity_data = {
                    # no version yet
                    '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 = {
                    # no version yet
                    '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,
                        )
                        # TODO(dcramer): we need a solution for activity rollups
                        # before sending notifications on bulk changes
                        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,
                    )
                    # TODO(dcramer): we need a solution for activity rollups
                    # before sending notifications on bulk changes
                    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:
            assigned_actor = result['assignedTo']
            if assigned_actor:
                for group in group_list:
                    resolved_actor = assigned_actor.resolve()

                    GroupAssignee.objects.assign(group, resolved_actor, acting_user)
                result['assignedTo'] = serialize(
                    assigned_actor.resolve(), acting_user, ActorSerializer())
            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()

        # TODO(dcramer): we could make these more efficient by first
        # querying for rich rows are present (if N > 2), flipping the flag
        # on those rows, and then creating the missing rows
        if result.get('isSubscribed') in (True, False):
            is_subscribed = result['isSubscribed']
            for group in group_list:
                # NOTE: Subscribing without an initiating event (assignment,
                # commenting, etc.) clears out the previous subscription reason
                # to avoid showing confusing messaging as a result of this
                # action. It'd be jarring to go directly from "you are not
                # subscribed" to "you were subscribed due since you were
                # assigned" just by clicking the "subscribe" button (and you
                # may no longer be assigned to the issue anyway.)
                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 'isPublic' in result:
            # We always want to delete an existing share, because triggering
            # an isPublic=True even when it's already public, should trigger
            # regenerating.
            for group in group_list:
                if GroupShare.objects.filter(group=group).delete():
                    result['shareId'] = None
                    Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_PRIVATE,
                        user=acting_user,
                    )

        if result.get('isPublic'):
            for group in group_list:
                share, created = GroupShare.objects.get_or_create(
                    project=group.project,
                    group=group,
                    user=acting_user,
                )
                if created:
                    result['shareId'] = share.uuid
                    Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_PUBLIC,
                        user=acting_user,
                    )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        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)
    def put(self, request, project):
        """
        Bulk mutate a list of aggregates

        Bulk mutate various attributes on aggregates.

            {method} {path}?id=1&id=2&id=3
            {{
              "status": "resolved",
              "isBookmarked": true
            }}

        - For non-status updates, the 'id' parameter is required.
        - For status updates, the 'id' parameter may be omitted for a batch
          "update all" query.
        - An optional 'status' parameter may be used to restrict mutations to
          only events with the given status.

        For example, to resolve all aggregates (project-wide):

            {method} {path}
            {{
                "status": "resolved"
            }}

        Attributes:

        - status: resolved, unresolved, muted
        - hasSeen: true, false
        - isBookmarked: true, false
        - isPublic: true, false
        - merge: true, false

        If any ids are out of scope this operation will succeed without any data
        mutation.
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project, id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]

            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)

        result = serializer.object

        # validate that we've passed a selector for non-status bulk operations
        if not group_ids and result.keys() != ['status']:
            return Response('{"detail": "You must specify a list of IDs for this operation"}', status=400)

        if group_ids:
            filters = [Q(id__in=group_ids)]
        else:
            filters = [Q(project=project)]

        if request.GET.get('status'):
            try:
                status_filter = STATUS_CHOICES[request.GET['status']]
            except KeyError:
                return Response('{"detail": "Invalid status"}', status=400)
            filters.append(Q(status=status_filter))

        if result.get('status') == 'resolved':
            now = timezone.now()

            happened = Group.objects.filter(*filters).exclude(
                status=GroupStatus.RESOLVED,
            ).update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=request.user,
                    )
                    activity.send_notification()

        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            happened = Group.objects.filter(*filters).exclude(
                status=new_status,
            ).update(
                status=new_status,
            )
            if group_list and happened:
                if new_status == GroupStatus.UNRESOLVED:
                    activity_type = Activity.SET_UNRESOLVED
                elif new_status == GroupStatus.MUTED:
                    activity_type = Activity.SET_MUTED

                for group in group_list:
                    group.status = new_status
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=activity_type,
                        user=request.user,
                    )
                    activity.send_notification()

        if result.get('hasSeen'):
            for group in group_list:
                instance, created = create_or_update(
                    GroupSeen,
                    group=group,
                    user=request.user,
                    project=group.project,
                    values={
                        'last_seen': timezone.now(),
                    }
                )
        elif result.get('hasSeen') is False:
            GroupSeen.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isBookmarked'):
            for group in group_list:
                GroupBookmark.objects.get_or_create(
                    project=group.project,
                    group=group,
                    user=request.user,
                )
        elif result.get('isBookmarked') is False:
            GroupBookmark.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isPublic'):
            Group.objects.filter(
                id__in=group_ids,
            ).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=request.user,
                )
        elif result.get('isPublic') is False:
            Group.objects.filter(
                id__in=group_ids,
            ).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=request.user,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        if result.get('merge') and len(group_list) > 1:
            primary_group = sorted(group_list, key=lambda x: -x.times_seen)[0]
            for group in group_list:
                if group == primary_group:
                    continue
                merge_group.delay(
                    from_object_id=group.id,
                    to_object_id=primary_group.id,
                )

        return Response(dict(result))
    def put(self, request, project):
        """
        Bulk Mutate a List of Aggregates
        ````````````````````````````````

        Bulk mutate various attributes on aggregates.  The list of groups
        to modify is given through the `id` query parameter.  It is repeated
        for each group 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 groups to be mutated.  This
                        parameter shall be repeated for each group.  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 groups of the
                               specified status.  Valid values are
                               ``"resolved"``, ``"unresolved"`` and
                               ``"muted"``.
        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        :param string status: the new status for the groups.  Valid values
                              are ``"resolved"``, ``"unresolved"`` and
                              ``"muted"``.
        :param int snoozeDuration: the number of minutes to mute this issue.
        :param boolean isPublic: sets the group to public or private.
        :param boolean merge: allows to merge or unmerge different groups.
        :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
        """
        group_ids = request.GET.getlist('id')
        if group_ids:
            group_list = Group.objects.filter(project=project, id__in=group_ids)
            # filter down group ids to only valid matches
            group_ids = [g.id for g in group_list]

            if not group_ids:
                return Response(status=204)
        else:
            group_list = None

        serializer = GroupSerializer(data=request.DATA, partial=True)
        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

        # validate that we've passed a selector for non-status bulk operations
        if not group_ids and result.keys() != ['status']:
            return Response('{"detail": "You must specify a list of IDs for this operation"}', status=400)

        if group_ids:
            filters = [Q(id__in=group_ids)]
        else:
            filters = [Q(project=project)]

        if request.GET.get('status'):
            try:
                status_filter = STATUS_CHOICES[request.GET['status']]
            except KeyError:
                return Response('{"detail": "Invalid status"}', status=400)
            filters.append(Q(status=status_filter))

        if result.get('status') == 'resolvedInNextRelease':
            try:
                release = Release.objects.filter(
                    project=project,
                ).order_by('-date_added')[0]
            except IndexError:
                return Response('{"detail": "No release data present in the system to indicate form a basis for \'Next Release\'"}', status=400)

            now = timezone.now()

            for group in group_list:
                try:
                    with transaction.atomic():
                        resolution, created = GroupResolution.objects.create(
                            group=group,
                            release=release,
                        ), True
                except IntegrityError:
                    resolution, created = GroupResolution.objects.get(
                        group=group,
                    ), False

                Group.objects.filter(
                    id=group.id,
                ).update(
                    status=GroupStatus.RESOLVED,
                    resolved_at=now,
                )

                if created:
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED_IN_RELEASE,
                        user=acting_user,
                        ident=resolution.id,
                        data={
                            # no version yet
                            'version': '',
                        }
                    )
                    activity.send_notification()

            result.update({
                'status': 'resolved',
                'statusDetails': {
                    'inNextRelease': True,
                },
            })

        elif result.get('status') == 'resolved':
            now = timezone.now()

            happened = Group.objects.filter(*filters).exclude(
                status=GroupStatus.RESOLVED,
            ).update(
                status=GroupStatus.RESOLVED,
                resolved_at=now,
            )

            GroupResolution.objects.filter(
                group__in=Group.objects.filter(*filters),
            ).delete()

            if group_list and happened:
                for group in group_list:
                    group.status = GroupStatus.RESOLVED
                    group.resolved_at = now
                    activity = Activity.objects.create(
                        project=group.project,
                        group=group,
                        type=Activity.SET_RESOLVED,
                        user=acting_user,
                    )
                    activity.send_notification()

            result['statusDetails'] = {}

        elif result.get('status'):
            new_status = STATUS_CHOICES[result['status']]

            happened = Group.objects.filter(*filters).exclude(
                status=new_status,
            ).update(
                status=new_status,
            )

            GroupResolution.objects.filter(
                group__in=Group.objects.filter(*filters),
            ).delete()

            if new_status == GroupStatus.MUTED:
                snooze_duration = result.pop('snoozeDuration', None)
                if snooze_duration:
                    snooze_until = timezone.now() + timedelta(
                        minutes=snooze_duration,
                    )
                    for group in group_list:
                        GroupSnooze.objects.create_or_update(
                            group=group,
                            values={
                                'until': snooze_until,
                            }
                        )
                        result['statusDetails'] = {
                            'snoozeUntil': snooze_until,
                        }
                else:
                    GroupSnooze.objects.filter(
                        group__in=group_list,
                    ).delete()
                    snooze_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.MUTED:
                    activity_type = Activity.SET_MUTED
                    activity_data = {
                        'snoozeUntil': snooze_until,
                        'snoozeDuration': snooze_duration,
                    }

                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,
                    )
                    activity.send_notification()

        if result.get('hasSeen') and project.member_set.filter(user=request.user).exists():
            for group in group_list:
                instance, created = create_or_update(
                    GroupSeen,
                    group=group,
                    user=request.user,
                    project=group.project,
                    values={
                        'last_seen': timezone.now(),
                    }
                )
        elif result.get('hasSeen') is False:
            GroupSeen.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isBookmarked'):
            for group in group_list:
                GroupBookmark.objects.get_or_create(
                    project=group.project,
                    group=group,
                    user=request.user,
                )
        elif result.get('isBookmarked') is False:
            GroupBookmark.objects.filter(
                group__in=group_ids,
                user=request.user,
            ).delete()

        if result.get('isPublic'):
            Group.objects.filter(
                id__in=group_ids,
            ).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:
            Group.objects.filter(
                id__in=group_ids,
            ).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,
                )

        # XXX(dcramer): this feels a bit shady like it should be its own
        # endpoint
        if result.get('merge') and len(group_list) > 1:
            primary_group = sorted(group_list, key=lambda x: -x.times_seen)[0]
            children = []
            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,
                )
            result['merge'] = {
                'parent': str(primary_group.id),
                'children': [str(g.id) for g in children],
            }

        return Response(result)