def publish(self, user=None, trivial=False, to_submitter_only=False): """Publishes this review. This will make the review public and update the timestamps of all contained comments. """ if not user: user = self.user self.public = True if self.is_reply(): reply_publishing.send(sender=self.__class__, user=user, reply=self) else: review_publishing.send(sender=self.__class__, user=user, review=self) self.save() self.comments.update(timestamp=self.timestamp) self.screenshot_comments.update(timestamp=self.timestamp) self.file_attachment_comments.update(timestamp=self.timestamp) self.general_comments.update(timestamp=self.timestamp) # Update the last_updated timestamp and the last review activity # timestamp on the review request. self.review_request.last_review_activity_timestamp = self.timestamp self.review_request.save( update_fields=['last_review_activity_timestamp', 'last_updated']) if self.is_reply(): reply_published.send(sender=self.__class__, user=user, reply=self, trivial=trivial) else: issue_counts = fetch_issue_counts(self.review_request, Q(pk=self.pk)) # Since we're publishing the review, all filed issues should be # open. assert issue_counts[BaseComment.RESOLVED] == 0 assert issue_counts[BaseComment.DROPPED] == 0 if self.ship_it: ship_it_value = 1 else: ship_it_value = 0 # Atomically update the issue count and Ship It count. CounterField.increment_many( self.review_request, { 'issue_open_count': issue_counts[BaseComment.OPEN], 'issue_dropped_count': 0, 'issue_resolved_count': 0, 'shipit_count': ship_it_value, }) review_published.send(sender=self.__class__, user=user, review=self, to_submitter_only=to_submitter_only)
def save(self, **kwargs): from reviewboard.reviews.models.review_request import ReviewRequest self.timestamp = timezone.now() super(BaseComment, self).save() try: # Update the review timestamp, but only if it's a draft. # Otherwise, resolving an issue will change the timestamp of # the review. review = self.get_review() if not review.public: review.timestamp = self.timestamp review.save() else: if not self.is_reply() and self._loaded_issue_status != self.issue_status: # The user has toggled the issue status of this comment, # so update the issue counts for the review request. old_field = ReviewRequest.ISSUE_COUNTER_FIELDS[self._loaded_issue_status] new_field = ReviewRequest.ISSUE_COUNTER_FIELDS[self.issue_status] CounterField.increment_many(self.get_review_request(), {old_field: -1, new_field: 1}) q = ReviewRequest.objects.filter(pk=review.review_request_id) q.update(last_review_activity_timestamp=self.timestamp) except ObjectDoesNotExist: pass
class LocalSiteProfile(models.Model): """User profile information specific to a LocalSite.""" user = models.ForeignKey(User, related_name='site_profiles') profile = models.ForeignKey(Profile, related_name='site_profiles') local_site = models.ForeignKey(LocalSite, null=True, blank=True, related_name='site_profiles') # A dictionary of permission that the user has granted. Any permission # missing is considered to be False. permissions = JSONField(null=True) # Counts for quickly knowing how many review requests are incoming # (both directly and total), outgoing (pending and total ever made), # and starred (public). direct_incoming_request_count = CounterField( _('direct incoming review request count'), initializer=lambda p: ( ReviewRequest.objects.to_user_directly( p.user, local_site=p.local_site).count() if p.user_id else 0)) total_incoming_request_count = CounterField( _('total incoming review request count'), initializer=lambda p: ( ReviewRequest.objects.to_user( p.user, local_site=p.local_site).count() if p.user_id else 0)) pending_outgoing_request_count = CounterField( _('pending outgoing review request count'), initializer=lambda p: ( ReviewRequest.objects.from_user( p.user, p.user, local_site=p.local_site).count() if p.user_id else 0)) total_outgoing_request_count = CounterField( _('total outgoing review request count'), initializer=lambda p: ( ReviewRequest.objects.from_user( p.user, p.user, None, local_site=p.local_site).count() if p.user_id else 0)) starred_public_request_count = CounterField( _('starred public review request count'), initializer=lambda p: ( p.profile.starred_review_requests.public( user=None, local_site=p.local_site).count() if p.pk else 0)) def __str__(self): """Return a string used for the admin site listing.""" return '%s (%s)' % (self.user.username, self.local_site) class Meta: db_table = 'accounts_localsiteprofile' unique_together = (('user', 'local_site'), ('profile', 'local_site')) verbose_name = _('Local Site Profile') verbose_name_plural = _('Local Site Profiles')
def publish(self, user=None): """Publishes this review. This will make the review public and update the timestamps of all contained comments. """ if not user: user = self.user self.public = True self.save() self.comments.update(timestamp=self.timestamp) self.screenshot_comments.update(timestamp=self.timestamp) self.file_attachment_comments.update(timestamp=self.timestamp) # Update the last_updated timestamp and the last review activity # timestamp on the review request. self.review_request.last_review_activity_timestamp = self.timestamp self.review_request.save( update_fields=['last_review_activity_timestamp']) if self.is_reply(): reply_published.send(sender=self.__class__, user=user, reply=self) else: issue_counts = fetch_issue_counts(self.review_request, Q(pk=self.pk)) # Since we're publishing the review, all filed issues should be # open. assert issue_counts[BaseComment.RESOLVED] == 0 assert issue_counts[BaseComment.DROPPED] == 0 if self.ship_it: ship_it_value = 1 else: ship_it_value = 0 # Atomically update the issue count and Ship It count. CounterField.increment_many( self.review_request, { 'issue_open_count': issue_counts[BaseComment.OPEN], 'issue_dropped_count': 0, 'issue_resolved_count': 0, 'shipit_count': ship_it_value, }) review_published.send(sender=self.__class__, user=user, review=self)
class CounterFieldInitializerFModel(models.Model): INIT_EXPR = F('my_int') + 1 my_int = models.IntegerField(default=42) counter = CounterField( initializer=lambda o: CounterFieldInitializerFModel.INIT_EXPR)
def save(self, **kwargs): """Save the comment. Args: **kwargs (dict): Keyword arguments passed to the method (unused). """ from reviewboard.reviews.models.review_request import ReviewRequest self.timestamp = timezone.now() super(BaseComment, self).save() try: # Update the review timestamp, but only if it's a draft. # Otherwise, resolving an issue will change the timestamp of # the review. review = self.get_review() if not review.public: review.timestamp = self.timestamp review.save() else: if (not self.is_reply() and self.issue_opened and self._loaded_issue_status != self.issue_status): # The user has toggled the issue status of this comment, # so update the issue counts for the review request. old_field = ReviewRequest.ISSUE_COUNTER_FIELDS[ self._loaded_issue_status] new_field = ReviewRequest.ISSUE_COUNTER_FIELDS[ self.issue_status] if old_field != new_field: CounterField.increment_many( self.get_review_request(), { old_field: -1, new_field: 1, }) q = ReviewRequest.objects.filter(pk=review.review_request_id) q.update(last_review_activity_timestamp=self.timestamp) except ObjectDoesNotExist: pass
class Group(models.Model): """A group of people who can be targetted for review. This is usually used to separate teams at a company or components of a project. Each group can have an e-mail address associated with it, sending all review requests and replies to that address. If that e-mail address is blank, e-mails are sent individually to each member of that group. """ name = models.SlugField(_("name"), max_length=64, blank=False) display_name = models.CharField(_("display name"), max_length=64) mailing_list = models.CharField( _("mailing list"), blank=True, max_length=254, help_text=_("The mailing list review requests and discussions " "are sent to.")) email_list_only = models.BooleanField( _('send e-mail only to the mailing list'), default=True, help_text=_('If a mailing list is specified and this option is ' 'checked, group members will not be individually ' 'included on e-mails, and only the mailing list ' 'will be used. This is highly recommended for ' 'large groups.')) users = models.ManyToManyField(User, blank=True, related_name="review_groups", verbose_name=_("users")) local_site = models.ForeignKey(LocalSite, blank=True, null=True, related_name='groups') is_default_group = models.BooleanField( _('add new users by default'), default=False, help_text=_('If a local site is set, this will automatically add ' 'users to this group when those users are added to the ' 'local site. If there is no local site, users will be ' 'automatically added to this group when they are ' 'registered.')) incoming_request_count = CounterField( _('incoming review request count'), initializer=_initialize_incoming_request_count) invite_only = models.BooleanField( _('invite only'), default=False, help_text=_('If checked, only the users listed below will be able ' 'to view review requests sent to this group.')) visible = models.BooleanField(default=True) extra_data = JSONField(null=True) objects = ReviewGroupManager() def is_accessible_by(self, user, request=None, silent=False): """Returns true if the user can access this group.""" if self.local_site and not self.local_site.is_accessible_by(user): if not silent: logging.warning( 'Group pk=%d (%s) is not accessible by user ' '%s because its local_site is not accessible ' 'by that user.', self.pk, self.name, user, request=request) return False if not self.invite_only or user.is_superuser: return True if user.is_authenticated() and self.users.filter(pk=user.pk).exists(): return True if not silent: logging.warning( 'Group pk=%d (%s) is not accessible by user %s ' 'because it is invite only, and the user is not a ' 'member.', self.pk, self.name, user, request=request) return False def is_mutable_by(self, user): """Returns whether or not the user can modify or delete the group. The group is mutable by the user if they are an administrator with proper permissions, or the group is part of a LocalSite and the user is in the admin list. """ return user.has_perm('reviews.change_group', self.local_site) def __str__(self): return self.name def get_absolute_url(self): if self.local_site_id: local_site_name = self.local_site.name else: local_site_name = None return local_site_reverse('group', local_site_name=local_site_name, kwargs={'name': self.name}) def clean(self): """Clean method for checking null unique_together constraints. Django has a bug where unique_together constraints for foreign keys aren't checked properly if one of the relations is null. This means that users who aren't using local sites could create multiple groups with the same name. """ super(Group, self).clean() if (self.local_site is None and Group.objects.filter( name=self.name).exclude(pk=self.pk).exists()): raise ValidationError(_('A group with this name already exists'), params={'field': 'name'}) class Meta: app_label = 'reviews' db_table = 'reviews_group' unique_together = (('name', 'local_site'), ) verbose_name = _('Review Group') verbose_name_plural = _('Review Groups') ordering = ['name']
class ReviewRequest(BaseReviewRequestDetails): """A review request. This is one of the primary models in Review Board. Most everything is associated with a review request. The ReviewRequest model contains detailed information on a review request. Some fields are user-modifiable, while some are used for internal state. """ PENDING_REVIEW = "P" SUBMITTED = "S" DISCARDED = "D" STATUSES = ( (PENDING_REVIEW, _('Pending Review')), (SUBMITTED, _('Submitted')), (DISCARDED, _('Discarded')), ) ISSUE_COUNTER_FIELDS = { BaseComment.OPEN: 'issue_open_count', BaseComment.RESOLVED: 'issue_resolved_count', BaseComment.DROPPED: 'issue_dropped_count', } summary = models.CharField( _("summary"), max_length=BaseReviewRequestDetails.MAX_SUMMARY_LENGTH) submitter = models.ForeignKey(User, verbose_name=_("submitter"), related_name="review_requests") time_added = models.DateTimeField(_("time added"), default=timezone.now) last_updated = ModificationTimestampField(_("last updated")) status = models.CharField(_("status"), max_length=1, choices=STATUSES, db_index=True) public = models.BooleanField(_("public"), default=False) changenum = models.PositiveIntegerField(_("change number"), blank=True, null=True, db_index=True) repository = models.ForeignKey(Repository, related_name="review_requests", verbose_name=_("repository"), null=True, blank=True) email_message_id = models.CharField(_("e-mail message ID"), max_length=255, blank=True, null=True) time_emailed = models.DateTimeField(_("time e-mailed"), null=True, default=None, blank=True) diffset_history = models.ForeignKey(DiffSetHistory, related_name="review_request", verbose_name=_('diff set history'), blank=True) target_groups = models.ManyToManyField(Group, related_name="review_requests", verbose_name=_("target groups"), blank=True) target_people = models.ManyToManyField( User, verbose_name=_("target people"), related_name="directed_review_requests", blank=True) screenshots = models.ManyToManyField(Screenshot, related_name="review_request", verbose_name=_("screenshots"), blank=True) inactive_screenshots = models.ManyToManyField( Screenshot, verbose_name=_("inactive screenshots"), help_text=_("A list of screenshots that used to be but are no " "longer associated with this review request."), related_name="inactive_review_request", blank=True) file_attachments = models.ManyToManyField( FileAttachment, related_name="review_request", verbose_name=_("file attachments"), blank=True) inactive_file_attachments = models.ManyToManyField( FileAttachment, verbose_name=_("inactive file attachments"), help_text=_("A list of file attachments that used to be but are no " "longer associated with this review request."), related_name="inactive_review_request", blank=True) file_attachment_histories = models.ManyToManyField( FileAttachmentHistory, related_name='review_request', verbose_name=_('file attachment histories'), blank=True) changedescs = models.ManyToManyField(ChangeDescription, verbose_name=_("change descriptions"), related_name="review_request", blank=True) depends_on = models.ManyToManyField('ReviewRequest', blank=True, null=True, verbose_name=_('Dependencies'), related_name='blocks') # Review-related information # The timestamp representing the last public activity of a review. # This includes publishing reviews and manipulating issues. last_review_activity_timestamp = models.DateTimeField( _("last review activity timestamp"), db_column='last_review_timestamp', null=True, default=None, blank=True) shipit_count = CounterField(_("ship-it count"), default=0) issue_open_count = CounterField(_('open issue count'), initializer=_initialize_issue_counts) issue_resolved_count = CounterField(_('resolved issue count'), initializer=_initialize_issue_counts) issue_dropped_count = CounterField(_('dropped issue count'), initializer=_initialize_issue_counts) local_site = models.ForeignKey(LocalSite, blank=True, null=True, related_name='review_requests') local_id = models.IntegerField('site-local ID', blank=True, null=True) # Set this up with the ReviewRequestManager objects = ReviewRequestManager() @staticmethod def status_to_string(status): """Return a string representation of a review request status. Args: status (unicode): A single-character string representing the status. Returns: unicode: A longer string representation of the status suitable for use in the API. """ if status == ReviewRequest.PENDING_REVIEW: return 'pending' elif status == ReviewRequest.SUBMITTED: return 'submitted' elif status == ReviewRequest.DISCARDED: return 'discarded' elif status is None: return 'all' else: raise ValueError('Invalid status "%s"' % status) @staticmethod def string_to_status(status): """Return a review request status from an API string. Args: status (unicode): A string from the API representing the status. Returns: unicode: A single-character string representing the status, suitable for storage in the ``status`` field. """ if status == 'pending': return ReviewRequest.PENDING_REVIEW elif status == 'submitted': return ReviewRequest.SUBMITTED elif status == 'discarded': return ReviewRequest.DISCARDED elif status == 'all': return None else: raise ValueError('Invalid status string "%s"' % status) def get_commit(self): if self.commit_id is not None: return self.commit_id elif self.changenum is not None: self.commit_id = six.text_type(self.changenum) # Update the state in the database, but don't save this # model, or we can end up with some state (if we haven't # properly loaded everything yet). This affects docs.db # generation, and may cause problems in the wild. ReviewRequest.objects.filter(pk=self.pk).update( commit_id=six.text_type(self.changenum)) return self.commit_id return None def set_commit(self, commit_id): try: self.changenum = int(commit_id) except (TypeError, ValueError): pass self.commit_id = commit_id commit = property(get_commit, set_commit) @property def approved(self): """Returns whether or not a review request is approved by reviewers. On a default installation, a review request is approved if it has at least one Ship It!, and doesn't have any open issues. Extensions may customize approval by providing their own ReviewRequestApprovalHook. """ if not hasattr(self, '_approved'): self._calculate_approval() return self._approved @property def approval_failure(self): """Returns the error indicating why a review request isn't approved. If ``approved`` is ``False``, this will provide the text describing why it wasn't approved. Extensions may customize approval by providing their own ReviewRequestApprovalHook. """ if not hasattr(self, '_approval_failure'): self._calculate_approval() return self._approval_failure def get_participants(self): """Returns a list of users who have discussed this review request.""" # See the comment in Review.get_participants for this list # comprehension. return [ u for review in self.reviews.all() for u in review.participants ] participants = property(get_participants) def get_new_reviews(self, user): """Returns all new reviews since last viewing this review request. This will factor in the time the user last visited the review request, and find any reviews that have been added or updated since. """ if user.is_authenticated(): # If this ReviewRequest was queried using with_counts=True, # then we should know the new review count and can use this to # decide whether we have anything at all to show. if hasattr(self, "new_review_count") and self.new_review_count > 0: query = self.visits.filter(user=user) try: visit = query[0] return self.reviews.filter( public=True, timestamp__gt=visit.timestamp).exclude(user=user) except IndexError: # This visit doesn't exist, so bail. pass return self.reviews.get_empty_query_set() def get_display_id(self): """Returns the ID that should be exposed to the user.""" if self.local_site_id: return self.local_id else: return self.id display_id = property(get_display_id) def get_public_reviews(self): """Returns all public top-level reviews for this review request.""" return self.reviews.filter(public=True, base_reply_to__isnull=True) def is_accessible_by(self, user, local_site=None, request=None, silent=False): """Returns whether or not the user can read this review request. This performs several checks to ensure that the user has access. This user has access if: * The review request is public or the user can modify it (either by being an owner or having special permissions). * The repository is public or the user has access to it (either by being explicitly on the allowed users list, or by being a member of a review group on that list). * The user is listed as a requested reviewer or the user has access to one or more groups listed as requested reviewers (either by being a member of an invite-only group, or the group being public). """ # Users always have access to their own review requests. if self.submitter == user: return True if not self.public and not self.is_mutable_by(user): if not silent: logging.warning( 'Review Request pk=%d (display_id=%d) is not ' 'accessible by user %s because it has not yet ' 'been published.', self.pk, self.display_id, user, request=request) return False if self.repository and not self.repository.is_accessible_by(user): if not silent: logging.warning( 'Review Request pk=%d (display_id=%d) is not ' 'accessible by user %s because its repository ' 'is not accessible by that user.', self.pk, self.display_id, user, request=request) return False if local_site and not local_site.is_accessible_by(user): if not silent: logging.warning( 'Review Request pk=%d (display_id=%d) is not ' 'accessible by user %s because its local_site ' 'is not accessible by that user.', self.pk, self.display_id, user, request=request) return False if (user.is_authenticated() and self.target_people.filter(pk=user.pk).count() > 0): return True groups = list(self.target_groups.all()) if not groups: return True # We specifically iterate over these instead of making it part # of the query in order to keep the logic in Group, and to allow # for future expansion (extensions, more advanced policy) # # We're looking for at least one group that the user has access # to. If they can access any of the groups, then they have access # to the review request. for group in groups: if group.is_accessible_by(user, silent=silent): return True if not silent: logging.warning( 'Review Request pk=%d (display_id=%d) is not ' 'accessible by user %s because they are not ' 'directly listed as a reviewer, and none of ' 'the target groups are accessible by that user.', self.pk, self.display_id, user, request=request) return False def is_mutable_by(self, user): """Returns whether the user can modify this review request.""" return (self.submitter == user or user.has_perm( 'reviews.can_edit_reviewrequest', self.local_site)) def is_status_mutable_by(self, user): """Returns whether the user can modify this review request's status.""" return (self.submitter == user or user.has_perm('reviews.can_change_status', self.local_site)) def is_deletable_by(self, user): """Returns whether the user can delete this review request.""" return user.has_perm('reviews.delete_reviewrequest') def get_draft(self, user=None): """Returns the draft of the review request. If a user is specified, than the draft will be returned only if owned by the user. Otherwise, None will be returned. """ if not user: return get_object_or_none(self.draft) elif user.is_authenticated(): return get_object_or_none(self.draft, review_request__submitter=user) return None def get_pending_review(self, user): """Returns the pending review owned by the specified user, if any. This will return an actual review, not a reply to a review. """ from reviewboard.reviews.models.review import Review return Review.objects.get_pending_review(self, user) def get_last_activity(self, diffsets=None, reviews=None): """Returns the last public activity information on the review request. This will return the last object updated, along with the timestamp of that object. It can be used to judge whether something on a review request has been made public more recently. """ timestamp = self.last_updated updated_object = self # Check if the diff was updated along with this. if not diffsets and self.repository_id: latest_diffset = self.get_latest_diffset() diffsets = [] if latest_diffset: diffsets.append(latest_diffset) if diffsets: for diffset in diffsets: if diffset.timestamp >= timestamp: timestamp = diffset.timestamp updated_object = diffset # Check for the latest review or reply. if not reviews: try: reviews = [self.reviews.filter(public=True).latest()] except ObjectDoesNotExist: reviews = [] for review in reviews: if review.public and review.timestamp >= timestamp: timestamp = review.timestamp updated_object = review return timestamp, updated_object def changeset_is_pending(self, commit_id): """Returns whether the associated changeset is pending commit. For repositories that support it, this will return whether the associated changeset is pending commit. This requires server-side knowledge of the change. """ cache_key = make_cache_key('commit-id-is-pending-%d-%s' % (self.pk, commit_id)) cached_values = cache.get(cache_key) if cached_values: return cached_values is_pending = False scmtool = self.repository.get_scmtool() if (scmtool.supports_pending_changesets and commit_id is not None): changeset = scmtool.get_changeset(commit_id, allow_empty=True) if changeset: is_pending = changeset.pending new_commit_id = six.text_type(changeset.changenum) if commit_id != new_commit_id: self.commit_id = new_commit_id self.save(update_fields=['commit_id']) commit_id = new_commit_id draft = self.get_draft() if draft: draft.commit_id = new_commit_id draft.save(update_fields=['commit_id']) # If the changeset is pending, we cache for only one minute to # speed things up a little bit when navigating through # different pages. If the changeset is no longer pending, cache # for the full default time. if is_pending: cache.set(cache_key, (is_pending, commit_id), 60) else: cache.set(cache_key, (is_pending, commit_id)) return is_pending, commit_id def get_absolute_url(self): if self.local_site: local_site_name = self.local_site.name else: local_site_name = None return local_site_reverse( 'review-request-detail', local_site_name=local_site_name, kwargs={'review_request_id': self.display_id}) def get_diffsets(self): """Returns a list of all diffsets on this review request. This will also fetch all associated FileDiffs, as well as a count of the number of files (stored in DiffSet.file_count). """ if not self.repository_id: return [] if not hasattr(self, '_diffsets'): self._diffsets = list( DiffSet.objects.filter( history__pk=self.diffset_history_id).annotate( file_count=Count('files')).prefetch_related('files')) return self._diffsets def get_latest_diffset(self): """Returns the latest diffset for this review request.""" try: return DiffSet.objects.filter( history=self.diffset_history_id).latest() except DiffSet.DoesNotExist: return None def get_close_description(self): """Returns a tuple (description, is_rich_text) for the close text. This is a helper which is used to gather the data which is rendered in the close description boxes on various pages. """ # We're fetching all entries instead of just public ones because # another query may have already prefetched the list of # changedescs. In this case, a new filter() would result in more # queries. # # Realistically, there will only ever be at most a single # non-public change description (the current draft), so we # wouldn't be saving much of anything with a filter. changedescs = list(self.changedescs.all()) latest_changedesc = None for changedesc in changedescs: if changedesc.public: latest_changedesc = changedesc break close_description = '' is_rich_text = False if latest_changedesc and 'status' in latest_changedesc.fields_changed: status = latest_changedesc.fields_changed['status']['new'][0] if status in (ReviewRequest.DISCARDED, ReviewRequest.SUBMITTED): close_description = latest_changedesc.text is_rich_text = latest_changedesc.rich_text return (close_description, is_rich_text) def get_blocks(self): """Returns the list of review request this one blocks. The returned value will be cached for future lookups. """ if not hasattr(self, '_blocks'): self._blocks = list(self.blocks.all()) return self._blocks def save(self, update_counts=False, old_submitter=None, **kwargs): if update_counts or self.id is None: self._update_counts(old_submitter) if self.status != self.PENDING_REVIEW: # If this is not a pending review request now, delete any # and all ReviewRequestVisit objects. self.visits.all().delete() super(ReviewRequest, self).save(**kwargs) def delete(self, **kwargs): from reviewboard.accounts.models import Profile, LocalSiteProfile profile, profile_is_new = \ Profile.objects.get_or_create(user=self.submitter) if profile_is_new: profile.save() local_site = self.local_site site_profile, site_profile_is_new = \ LocalSiteProfile.objects.get_or_create(user=self.submitter, profile=profile, local_site=local_site) site_profile.decrement_total_outgoing_request_count() if self.status == self.PENDING_REVIEW: site_profile.decrement_pending_outgoing_request_count() if self.public: self._decrement_reviewer_counts() super(ReviewRequest, self).delete(**kwargs) def can_publish(self): return not self.public or get_object_or_none(self.draft) is not None def close(self, type, user=None, description=None, rich_text=False): """Closes the review request. Args: type (unicode): How the close occurs. This should be one of :py:attr:`SUBMITTED` or :py:attr:`DISCARDED`. user (django.contrib.auth.models.User): The user who is closing the review request. description (unicode): An optional description that indicates why the review request was closed. rich_text (bool): Indicates whether or not that the description is rich text. """ if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError if type not in [self.SUBMITTED, self.DISCARDED]: raise AttributeError("%s is not a valid close type" % type) review_request_closing.send(sender=self.__class__, user=user, review_request=self, type=type, description=description, rich_text=rich_text) draft = get_object_or_none(self.draft) if self.status != type: if (draft is not None and not self.public and type == self.DISCARDED): # Copy over the draft information if this is a private discard. draft.copy_fields_to_request(self) # TODO: Use the user's default for rich_text. changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text or False, user=user or self.submitter) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, self.status, type) changedesc.save() self.changedescs.add(changedesc) if type == self.SUBMITTED: if not self.public: raise PublishError("The draft must be public first.") else: self.commit_id = None self.status = type self.save(update_counts=True) review_request_closed.send(sender=self.__class__, user=user, review_request=self, type=type, description=description, rich_text=rich_text) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.rich_text = rich_text changedesc.save() # Needed to renew last-update. self.save() # Delete the associated draft review request. if draft is not None: draft.delete() def reopen(self, user=None): """Reopens the review request for review.""" from reviewboard.reviews.models.review_request_draft import \ ReviewRequestDraft if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError old_status = self.status old_public = self.public if old_status != self.PENDING_REVIEW: # The reopening signal is only fired when actually making a status # change since the main consumers (extensions) probably only care # about changes. review_request_reopening.send(sender=self.__class__, user=user, review_request=self) changedesc = ChangeDescription(user=user or self.submitter) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, old_status, self.PENDING_REVIEW) if old_status == self.DISCARDED: # A draft is needed if reopening a discarded review request. self.public = False changedesc.save() draft = ReviewRequestDraft.create(self) draft.changedesc = changedesc draft.save() else: changedesc.public = True changedesc.save() self.changedescs.add(changedesc) self.status = self.PENDING_REVIEW self.save(update_counts=True) review_request_reopened.send(sender=self.__class__, user=user, review_request=self, old_status=old_status, old_public=old_public) def publish(self, user, trivial=False): """Publishes the current draft attached to this review request. The review request will be mark as public, and signals will be emitted for any listeners. """ if not self.is_mutable_by(user): raise PermissionError draft = get_object_or_none(self.draft) old_submitter = self.submitter review_request_publishing.send(sender=self.__class__, user=user, review_request_draft=draft) # Decrement the counts on everything. we lose them. # We'll increment the resulting set during ReviewRequest.save. # This should be done before the draft is published. # Once the draft is published, the target people # and groups will be updated with new values. # Decrement should not happen while publishing # a new request or a discarded request if self.public: self._decrement_reviewer_counts() if draft is not None: # This will in turn save the review request, so we'll be done. try: changes = draft.publish(self, send_notification=False, user=user) except Exception: # The draft failed to publish, for one reason or another. # Check if we need to re-increment those counters we # previously decremented. if self.public: self._increment_reviewer_counts() raise draft.delete() else: changes = None if not self.public and self.changedescs.count() == 0: # This is a brand new review request that we're publishing # for the first time. Set the creation timestamp to now. self.time_added = timezone.now() self.public = True self.save(update_counts=True, old_submitter=old_submitter) review_request_published.send(sender=self.__class__, user=user, review_request=self, trivial=trivial, changedesc=changes) def determine_user_for_changedesc(self, changedesc): """Determine the user associated with the change description. Args: changedesc (reviewboard.changedescs.models.ChangeDescription): The change description. Returns: django.contrib.auth.models.User: The user associated with the change description. """ if 'submitter' in changedesc.fields_changed: entry = changedesc.fields_changed['submitter']['old'][0] return User.objects.get(pk=entry[2]) user_pk = None changes = (self.changedescs.filter( pk__lt=changedesc.pk).order_by('-pk')) for changedesc in changes: if 'submitter' in changedesc.fields_changed: user_pk = changedesc.fields_changed['submitter']['new'][0][2] break if user_pk: return User.objects.get(pk=user_pk) return self.submitter def _update_counts(self, old_submitter): from reviewboard.accounts.models import Profile, LocalSiteProfile submitter_changed = (old_submitter is not None and old_submitter != self.submitter) profile, profile_is_new = \ Profile.objects.get_or_create(user=self.submitter) if profile_is_new: profile.save() local_site = self.local_site site_profile, site_profile_is_new = \ LocalSiteProfile.objects.get_or_create( user=self.submitter, profile=profile, local_site=local_site) if site_profile_is_new: site_profile.save() if self.id is None: # This hasn't been created yet. Bump up the outgoing request # count for the user. site_profile.increment_total_outgoing_request_count() old_status = None old_public = False else: # We need to see if the status has changed, so that means # finding out what's in the database. r = ReviewRequest.objects.get(pk=self.id) old_status = r.status old_public = r.public if submitter_changed: if not site_profile_is_new: site_profile.increment_total_outgoing_request_count() if self.status == self.PENDING_REVIEW: site_profile.increment_pending_outgoing_request_count() try: old_profile = LocalSiteProfile.objects.get( user=old_submitter, local_site=local_site) old_profile.decrement_total_outgoing_request_count() if old_status == self.PENDING_REVIEW: old_profile.decrement_pending_outgoing_request_count() except LocalSiteProfile.DoesNotExist: pass if self.status == self.PENDING_REVIEW: if old_status != self.status and not submitter_changed: site_profile.increment_pending_outgoing_request_count() if self.public and self.id is not None: self._increment_reviewer_counts() elif old_status == self.PENDING_REVIEW: if old_status != self.status and not submitter_changed: site_profile.decrement_pending_outgoing_request_count() if old_public: self._decrement_reviewer_counts() def _increment_reviewer_counts(self): from reviewboard.accounts.models import LocalSiteProfile groups = self.target_groups.all() people = self.target_people.all() Group.incoming_request_count.increment(groups) LocalSiteProfile.direct_incoming_request_count.increment( LocalSiteProfile.objects.filter(user__in=people, local_site=self.local_site)) LocalSiteProfile.total_incoming_request_count.increment( LocalSiteProfile.objects.filter( Q(local_site=self.local_site) & Q(Q(user__review_groups__in=groups) | Q(user__in=people)))) LocalSiteProfile.starred_public_request_count.increment( LocalSiteProfile.objects.filter( profile__starred_review_requests=self, local_site=self.local_site)) def _decrement_reviewer_counts(self): from reviewboard.accounts.models import LocalSiteProfile groups = self.target_groups.all() people = self.target_people.all() Group.incoming_request_count.decrement(groups) LocalSiteProfile.direct_incoming_request_count.decrement( LocalSiteProfile.objects.filter(user__in=people, local_site=self.local_site)) LocalSiteProfile.total_incoming_request_count.decrement( LocalSiteProfile.objects.filter( Q(local_site=self.local_site) & Q(Q(user__review_groups__in=groups) | Q(user__in=people)))) LocalSiteProfile.starred_public_request_count.decrement( LocalSiteProfile.objects.filter( profile__starred_review_requests=self, local_site=self.local_site)) def _calculate_approval(self): """Calculates the approval information for the review request.""" from reviewboard.extensions.hooks import ReviewRequestApprovalHook approved = True failure = None if self.shipit_count == 0: approved = False failure = 'The review request has not been marked "Ship It!"' elif self.issue_open_count > 0: approved = False failure = 'The review request has open issues.' for hook in ReviewRequestApprovalHook.hooks: try: result = hook.is_approved(self, approved, failure) if isinstance(result, tuple): approved, failure = result elif isinstance(result, bool): approved = result else: raise ValueError('%r returned an invalid value %r from ' 'is_approved' % (hook, result)) if approved: failure = None except Exception as e: extension = hook.extension logging.error( 'Error when running ReviewRequestApprovalHook.' 'is_approved function in extension: "%s": %s', extension.id, e, exc_info=1) self._approval_failure = failure self._approved = approved def get_review_request(self): """Returns this review request. This is provided so that consumers can be passed either a ReviewRequest or a ReviewRequestDraft and retrieve the actual ReviewRequest regardless of the object. """ return self class Meta: app_label = 'reviews' db_table = 'reviews_reviewrequest' ordering = ['-last_updated', 'submitter', 'summary'] unique_together = (('commit_id', 'repository'), ('changenum', 'repository'), ('local_site', 'local_id')) permissions = ( ("can_change_status", "Can change status"), ("can_submit_as_another_user", "Can submit as another user"), ("can_edit_reviewrequest", "Can edit review request"), ) verbose_name = _('Review Request') verbose_name_plural = _('Review Requests')
class ReviewRequest(BaseReviewRequestDetails): """A review request. This is one of the primary models in Review Board. Most everything is associated with a review request. The ReviewRequest model contains detailed information on a review request. Some fields are user-modifiable, while some are used for internal state. """ PENDING_REVIEW = "P" SUBMITTED = "S" DISCARDED = "D" STATUSES = ( (PENDING_REVIEW, _('Pending Review')), (SUBMITTED, _('Submitted')), (DISCARDED, _('Discarded')), ) ISSUE_COUNTER_FIELDS = { BaseComment.OPEN: 'issue_open_count', BaseComment.RESOLVED: 'issue_resolved_count', BaseComment.DROPPED: 'issue_dropped_count', } submitter = models.ForeignKey(User, verbose_name=_("submitter"), related_name="review_requests") time_added = models.DateTimeField(_("time added"), default=timezone.now) last_updated = ModificationTimestampField(_("last updated")) status = models.CharField(_("status"), max_length=1, choices=STATUSES, db_index=True) public = models.BooleanField(_("public"), default=False) changenum = models.PositiveIntegerField(_("change number"), blank=True, null=True, db_index=True) repository = models.ForeignKey(Repository, related_name="review_requests", verbose_name=_("repository"), null=True, blank=True) email_message_id = models.CharField(_("e-mail message ID"), max_length=255, blank=True, null=True) time_emailed = models.DateTimeField(_("time e-mailed"), null=True, default=None, blank=True) diffset_history = models.ForeignKey(DiffSetHistory, related_name="review_request", verbose_name=_('diff set history'), blank=True) target_groups = models.ManyToManyField(Group, related_name="review_requests", verbose_name=_("target groups"), blank=True) target_people = models.ManyToManyField( User, verbose_name=_("target people"), related_name="directed_review_requests", blank=True) screenshots = models.ManyToManyField(Screenshot, related_name="review_request", verbose_name=_("screenshots"), blank=True) inactive_screenshots = models.ManyToManyField( Screenshot, verbose_name=_("inactive screenshots"), help_text=_("A list of screenshots that used to be but are no " "longer associated with this review request."), related_name="inactive_review_request", blank=True) file_attachments = models.ManyToManyField( FileAttachment, related_name="review_request", verbose_name=_("file attachments"), blank=True) inactive_file_attachments = models.ManyToManyField( FileAttachment, verbose_name=_("inactive file attachments"), help_text=_("A list of file attachments that used to be but are no " "longer associated with this review request."), related_name="inactive_review_request", blank=True) changedescs = models.ManyToManyField(ChangeDescription, verbose_name=_("change descriptions"), related_name="review_request", blank=True) depends_on = models.ManyToManyField('ReviewRequest', blank=True, null=True, verbose_name=_('Dependencies'), related_name='blocks') # Review-related information # The timestamp representing the last public activity of a review. # This includes publishing reviews and manipulating issues. last_review_activity_timestamp = models.DateTimeField( _("last review activity timestamp"), db_column='last_review_timestamp', null=True, default=None, blank=True) shipit_count = CounterField(_("ship-it count"), default=0) issue_open_count = CounterField(_('open issue count'), initializer=_initialize_issue_counts) issue_resolved_count = CounterField(_('resolved issue count'), initializer=_initialize_issue_counts) issue_dropped_count = CounterField(_('dropped issue count'), initializer=_initialize_issue_counts) local_site = models.ForeignKey(LocalSite, blank=True, null=True) local_id = models.IntegerField('site-local ID', blank=True, null=True) # Set this up with the ReviewRequestManager objects = ReviewRequestManager() def get_commit(self): if self.commit_id is not None: return self.commit_id elif self.changenum is not None: self.commit_id = six.text_type(self.changenum) # Update the state in the database, but don't save this # model, or we can end up with some state (if we haven't # properly loaded everything yet). This affects docs.db # generation, and may cause problems in the wild. ReviewRequest.objects.filter(pk=self.pk).update( commit_id=six.text_type(self.changenum)) return self.commit_id return None def set_commit(self, commit_id): try: self.changenum = int(commit_id) except (TypeError, ValueError): pass self.commit_id = commit_id commit = property(get_commit, set_commit) @property def approved(self): """Returns whether or not a review request is approved by reviewers. On a default installation, a review request is approved if it has at least one Ship It!, and doesn't have any open issues. Extensions may customize approval by providing their own ReviewRequestApprovalHook. """ if not hasattr(self, '_approved'): self._calculate_approval() return self._approved @property def approval_failure(self): """Returns the error indicating why a review request isn't approved. If ``approved`` is ``False``, this will provide the text describing why it wasn't approved. Extensions may customize approval by providing their own ReviewRequestApprovalHook. """ if not hasattr(self, '_approval_failure'): self._calculate_approval() return self._approval_failure def get_participants(self): """Returns a list of users who have discussed this review request.""" # See the comment in Review.get_participants for this list # comprehension. return [ u for review in self.reviews.all() for u in review.participants ] participants = property(get_participants) def get_new_reviews(self, user): """Returns all new reviews since last viewing this review request. This will factor in the time the user last visited the review request, and find any reviews that have been added or updated since. """ if user.is_authenticated(): # If this ReviewRequest was queried using with_counts=True, # then we should know the new review count and can use this to # decide whether we have anything at all to show. if hasattr(self, "new_review_count") and self.new_review_count > 0: query = self.visits.filter(user=user) try: visit = query[0] return self.reviews.filter( public=True, timestamp__gt=visit.timestamp).exclude(user=user) except IndexError: # This visit doesn't exist, so bail. pass return self.reviews.get_empty_query_set() def get_display_id(self): """Returns the ID that should be exposed to the user.""" if self.local_site_id: return self.local_id else: return self.id display_id = property(get_display_id) def get_public_reviews(self): """Returns all public top-level reviews for this review request.""" return self.reviews.filter(public=True, base_reply_to__isnull=True) def is_accessible_by(self, user, local_site=None): """Returns whether or not the user can read this review request. This performs several checks to ensure that the user has access. This user has access if: * The review request is public or the user can modify it (either by being an owner or having special permissions). * The repository is public or the user has access to it (either by being explicitly on the allowed users list, or by being a member of a review group on that list). * The user is listed as a requested reviewer or the user has access to one or more groups listed as requested reviewers (either by being a member of an invite-only group, or the group being public). """ # Users always have access to their own review requests. if self.submitter == user: return True if not self.public and not self.is_mutable_by(user): return False if self.repository and not self.repository.is_accessible_by(user): return False if local_site and not local_site.is_accessible_by(user): return False if (user.is_authenticated() and self.target_people.filter(pk=user.pk).count() > 0): return True groups = list(self.target_groups.all()) if not groups: return True # We specifically iterate over these instead of making it part # of the query in order to keep the logic in Group, and to allow # for future expansion (extensions, more advanced policy) # # We're looking for at least one group that the user has access # to. If they can access any of the groups, then they have access # to the review request. for group in groups: if group.is_accessible_by(user): return True return False def is_mutable_by(self, user): """Returns whether the user can modify this review request.""" return (self.submitter == user or user.has_perm( 'reviews.can_edit_reviewrequest', self.local_site)) def is_status_mutable_by(self, user): """Returns whether the user can modify this review request's status.""" return (self.submitter == user or user.has_perm('reviews.can_change_status', self.local_site)) def is_deletable_by(self, user): """Returns whether the user can delete this review request.""" return user.has_perm('reviews.delete_reviewrequest') def get_draft(self, user=None): """Returns the draft of the review request. If a user is specified, than the draft will be returned only if owned by the user. Otherwise, None will be returned. """ if not user: return get_object_or_none(self.draft) elif user.is_authenticated(): return get_object_or_none(self.draft, review_request__submitter=user) return None def get_pending_review(self, user): """Returns the pending review owned by the specified user, if any. This will return an actual review, not a reply to a review. """ from reviewboard.reviews.models.review import Review return Review.objects.get_pending_review(self, user) def get_last_activity(self, diffsets=None, reviews=None): """Returns the last public activity information on the review request. This will return the last object updated, along with the timestamp of that object. It can be used to judge whether something on a review request has been made public more recently. """ timestamp = self.last_updated updated_object = self # Check if the diff was updated along with this. if not diffsets and self.repository_id: latest_diffset = self.get_latest_diffset() diffsets = [] if latest_diffset: diffsets.append(latest_diffset) if diffsets: for diffset in diffsets: if diffset.timestamp >= timestamp: timestamp = diffset.timestamp updated_object = diffset # Check for the latest review or reply. if not reviews: try: reviews = [self.reviews.filter(public=True).latest()] except ObjectDoesNotExist: reviews = [] for review in reviews: if review.public and review.timestamp >= timestamp: timestamp = review.timestamp updated_object = review return timestamp, updated_object def changeset_is_pending(self): """Returns whether the associated changeset is pending commit. For repositories that support it, this will return whether the associated changeset is pending commit. This requires server-side knowledge of the change. """ changeset = None commit_id = self.commit if (self.repository.get_scmtool().supports_pending_changesets and commit_id is not None): changeset = self.repository.get_scmtool().get_changeset( commit_id, allow_empty=True) return changeset and changeset.pending def get_absolute_url(self): if self.local_site: local_site_name = self.local_site.name else: local_site_name = None return local_site_reverse( 'review-request-detail', local_site_name=local_site_name, kwargs={'review_request_id': self.display_id}) def get_diffsets(self): """Returns a list of all diffsets on this review request. This will also fetch all associated FileDiffs, as well as a count of the number of files (stored in DiffSet.file_count). """ if not self.repository_id: return [] if not hasattr(self, '_diffsets'): self._diffsets = list( DiffSet.objects.filter( history__pk=self.diffset_history_id).annotate( file_count=Count('files')).prefetch_related('files')) return self._diffsets def get_all_diff_filenames(self): """Returns a set of filenames from files in all diffsets.""" q = FileDiff.objects.filter( diffset__history__id=self.diffset_history_id) return set(q.values_list('source_file', 'dest_file')) def get_latest_diffset(self): """Returns the latest diffset for this review request.""" try: return DiffSet.objects.filter( history=self.diffset_history_id).latest() except DiffSet.DoesNotExist: return None def get_blocks(self): """Returns the list of review request this one blocks. The returned value will be cached for future lookups. """ if not hasattr(self, '_blocks'): self._blocks = list(self.blocks.all()) return self._blocks def save(self, update_counts=False, **kwargs): if update_counts or self.id is None: self._update_counts() if self.status != self.PENDING_REVIEW: # If this is not a pending review request now, delete any # and all ReviewRequestVisit objects. self.visits.all().delete() super(ReviewRequest, self).save(**kwargs) def delete(self, **kwargs): from reviewboard.accounts.models import Profile, LocalSiteProfile profile, profile_is_new = \ Profile.objects.get_or_create(user=self.submitter) if profile_is_new: profile.save() local_site = self.local_site site_profile, site_profile_is_new = \ LocalSiteProfile.objects.get_or_create(user=self.submitter, profile=profile, local_site=local_site) site_profile.decrement_total_outgoing_request_count() if self.status == self.PENDING_REVIEW: site_profile.decrement_pending_outgoing_request_count() if self.public: people = self.target_people.all() groups = self.target_groups.all() Group.incoming_request_count.decrement(groups) LocalSiteProfile.direct_incoming_request_count.decrement( LocalSiteProfile.objects.filter(user__in=people, local_site=local_site)) LocalSiteProfile.total_incoming_request_count.decrement( LocalSiteProfile.objects.filter( Q(local_site=local_site) & Q(Q(user__review_groups__in=groups) | Q(user__in=people)))) LocalSiteProfile.starred_public_request_count.decrement( LocalSiteProfile.objects.filter( profile__starred_review_requests=self, local_site=local_site)) super(ReviewRequest, self).delete(**kwargs) def can_publish(self): return not self.public or get_object_or_none(self.draft) is not None def close(self, type, user=None, description=None, rich_text=False): """Closes the review request. The type must be one of SUBMITTED or DISCARDED. """ if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError if type not in [self.SUBMITTED, self.DISCARDED]: raise AttributeError("%s is not a valid close type" % type) if self.status != type: changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text) changedesc.record_field_change('status', self.status, type) changedesc.save() self.changedescs.add(changedesc) if type == self.SUBMITTED: self.public = True self.status = type self.save(update_counts=True) review_request_closed.send(sender=self.__class__, user=user, review_request=self, type=type) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.save() # Needed to renew last-update. self.save() try: draft = self.draft.get() except ObjectDoesNotExist: pass else: draft.delete() def reopen(self, user=None): """Reopens the review request for review.""" from reviewboard.reviews.models.review_request_draft import \ ReviewRequestDraft if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError if self.status != self.PENDING_REVIEW: changedesc = ChangeDescription() changedesc.record_field_change('status', self.status, self.PENDING_REVIEW) if self.status == self.DISCARDED: # A draft is needed if reopening a discarded review request. self.public = False changedesc.save() draft = ReviewRequestDraft.create(self) draft.changedesc = changedesc draft.save() else: changedesc.public = True changedesc.save() self.changedescs.add(changedesc) self.status = self.PENDING_REVIEW self.save(update_counts=True) review_request_reopened.send(sender=self.__class__, user=user, review_request=self) def publish(self, user): """Publishes the current draft attached to this review request. The review request will be mark as public, and signals will be emitted for any listeners. """ from reviewboard.accounts.models import LocalSiteProfile if not self.is_mutable_by(user): raise PermissionError # Decrement the counts on everything. we lose them. # We'll increment the resulting set during ReviewRequest.save. # This should be done before the draft is published. # Once the draft is published, the target people # and groups will be updated with new values. # Decrement should not happen while publishing # a new request or a discarded request if self.public: Group.incoming_request_count.decrement(self.target_groups.all()) LocalSiteProfile.direct_incoming_request_count.decrement( LocalSiteProfile.objects.filter( user__in=self.target_people.all(), local_site=self.local_site)) LocalSiteProfile.total_incoming_request_count.decrement( LocalSiteProfile.objects.filter( Q(local_site=self.local_site) & Q( Q(user__review_groups__in=self.target_groups.all()) | Q(user__in=self.target_people.all())))) LocalSiteProfile.starred_public_request_count.decrement( LocalSiteProfile.objects.filter( profile__starred_review_requests=self, local_site=self.local_site)) draft = get_object_or_none(self.draft) if draft is not None: # This will in turn save the review request, so we'll be done. changes = draft.publish(self, send_notification=False) draft.delete() else: changes = None if not self.public and self.changedescs.count() == 0: # This is a brand new review request that we're publishing # for the first time. Set the creation timestamp to now. self.time_added = timezone.now() self.public = True self.save(update_counts=True) review_request_published.send(sender=self.__class__, user=user, review_request=self, changedesc=changes) def _update_counts(self): from reviewboard.accounts.models import Profile, LocalSiteProfile profile, profile_is_new = \ Profile.objects.get_or_create(user=self.submitter) if profile_is_new: profile.save() local_site = self.local_site site_profile, site_profile_is_new = \ LocalSiteProfile.objects.get_or_create( user=self.submitter, profile=profile, local_site=local_site) if site_profile_is_new: site_profile.save() if self.id is None: # This hasn't been created yet. Bump up the outgoing request # count for the user. site_profile.increment_total_outgoing_request_count() old_status = None old_public = False else: # We need to see if the status has changed, so that means # finding out what's in the database. r = ReviewRequest.objects.get(pk=self.id) old_status = r.status old_public = r.public if self.status == self.PENDING_REVIEW: if old_status != self.status: site_profile.increment_pending_outgoing_request_count() if self.public and self.id is not None: groups = self.target_groups.all() people = self.target_people.all() Group.incoming_request_count.increment(groups) LocalSiteProfile.direct_incoming_request_count.increment( LocalSiteProfile.objects.filter(user__in=people, local_site=local_site)) LocalSiteProfile.total_incoming_request_count.increment( LocalSiteProfile.objects.filter( Q(local_site=local_site) & Q( Q(user__review_groups__in=groups) | Q(user__in=people)))) LocalSiteProfile.starred_public_request_count.increment( LocalSiteProfile.objects.filter( profile__starred_review_requests=self, local_site=local_site)) else: if old_status != self.status: site_profile.decrement_pending_outgoing_request_count() if old_public: groups = self.target_groups.all() people = self.target_people.all() Group.incoming_request_count.decrement(groups) LocalSiteProfile.direct_incoming_request_count.decrement( LocalSiteProfile.objects.filter(user__in=people, local_site=local_site)) LocalSiteProfile.total_incoming_request_count.decrement( LocalSiteProfile.objects.filter( Q(local_site=local_site) & Q( Q(user__review_groups__in=groups) | Q(user__in=people)))) LocalSiteProfile.starred_public_request_count.decrement( LocalSiteProfile.objects.filter( profile__starred_review_requests=self, local_site=local_site)) def _calculate_approval(self): """Calculates the approval information for the review request.""" from reviewboard.extensions.hooks import ReviewRequestApprovalHook approved = True failure = None if self.shipit_count == 0: approved = False failure = 'The review request has not been marked "Ship It!"' elif self.issue_open_count > 0: approved = False failure = 'The review request has open issues.' for hook in ReviewRequestApprovalHook.hooks: result = hook.is_approved(self, approved, failure) if isinstance(result, tuple): approved, failure = result elif isinstance(result, bool): approved = result else: raise ValueError('%r returned an invalid value %r from ' 'is_approved' % (hook, result)) if approved: failure = None self._approval_failure = failure self._approved = approved def get_review_request(self): """Returns this review request. This is provided so that consumers can be passed either a ReviewRequest or a ReviewRequestDraft and retrieve the actual ReviewRequest regardless of the object. """ return self class Meta: app_label = 'reviews' ordering = ['-last_updated', 'submitter', 'summary'] unique_together = (('commit_id', 'repository'), ('changenum', 'repository'), ('local_site', 'local_id')) permissions = ( ("can_change_status", "Can change status"), ("can_submit_as_another_user", "Can submit as another user"), ("can_edit_reviewrequest", "Can edit review request"), )
class Group(models.Model): """A group of people who can be targetted for review. This is usually used to separate teams at a company or components of a project. Each group can have an e-mail address associated with it, sending all review requests and replies to that address. If that e-mail address is blank, e-mails are sent individually to each member of that group. """ name = models.SlugField(_("name"), max_length=64, blank=False) display_name = models.CharField(_("display name"), max_length=64) mailing_list = models.CharField( _("mailing list"), blank=True, max_length=254, help_text=_("The mailing list review requests and discussions " "are sent to.")) users = models.ManyToManyField(User, blank=True, related_name="review_groups", verbose_name=_("users")) local_site = models.ForeignKey(LocalSite, blank=True, null=True) incoming_request_count = CounterField( _('incoming review request count'), initializer=_initialize_incoming_request_count) invite_only = models.BooleanField(_('invite only'), default=False) visible = models.BooleanField(default=True) extra_data = JSONField(null=True) objects = ReviewGroupManager() def is_accessible_by(self, user): """Returns true if the user can access this group.""" if self.local_site and not self.local_site.is_accessible_by(user): return False return (not self.invite_only or user.is_superuser or (user.is_authenticated() and self.users.filter(pk=user.pk).count() > 0)) def is_mutable_by(self, user): """Returns whether or not the user can modify or delete the group. The group is mutable by the user if they are an administrator with proper permissions, or the group is part of a LocalSite and the user is in the admin list. """ return user.has_perm('reviews.change_group', self.local_site) def __str__(self): return self.name def get_absolute_url(self): if self.local_site_id: local_site_name = self.local_site.name else: local_site_name = None return local_site_reverse('group', local_site_name=local_site_name, kwargs={'name': self.name}) def clean(self): """Clean method for checking null unique_together constraints. Django has a bug where unique_together constraints for foreign keys aren't checked properly if one of the relations is null. This means that users who aren't using local sites could create multiple groups with the same name. """ super(Group, self).clean() if (self.local_site is None and Group.objects.filter( name=self.name).exclude(pk=self.pk).exists()): raise ValidationError(_('A group with this name already exists'), params={'field': 'name'}) class Meta: app_label = 'reviews' unique_together = (('name', 'local_site'), ) verbose_name = _("review group") ordering = ['name']
class CounterFieldTestModel(models.Model): counter = CounterField(initializer=lambda o: 5)
class CounterFieldNoInitializerModel(models.Model): counter = CounterField()
class CounterFieldInitializerModel(models.Model): counter = CounterField(initializer=lambda o: 42)
class CounterFieldMixin(models.Model): mixin_counter = CounterField(initializer=lambda o: 0) class Meta: abstract = True
class CounterFieldModelWithMixin(CounterFieldMixin, models.Model): counter = CounterField(initializer=lambda o: 1)