def confirm_auto_approved(self): """Confirm an auto-approval decision.""" channel = self.version.channel if channel == amo.RELEASE_CHANNEL_LISTED: # When doing an approval in listed channel, the version we care # about is always current_version and *not* self.version. # This allows reviewers to confirm approval of a public add-on even # when their latest version is disabled. version = self.addon.current_version else: # For unlisted, we just use self.version. version = self.version # The confirm auto-approval action should not show the comment box, # so override the text in case the reviewer switched between actions # and accidently submitted some comments from another action. self.data['comments'] = '' if channel == amo.RELEASE_CHANNEL_LISTED: version.autoapprovalsummary.update(confirmed=True) AddonApprovalsCounter.increment_for_addon(addon=self.addon) self.log_action(amo.LOG.CONFIRM_AUTO_APPROVED, version=version) # Assign reviewer incentive scores. if self.request: is_post_review = channel == amo.RELEASE_CHANNEL_LISTED ReviewerScore.award_points( self.request.user, self.addon, self.addon.status, version=version, post_review=is_post_review, content_review=self.content_review_only)
def confirm_auto_approved(self): """Confirm an auto-approval decision. We don't need to really store that information, what we care about is logging something for future reviewers to be aware of, and, if the version is listed, incrementing AddonApprovalsCounter, which also resets the last human review date to now, and log it so that it's displayed later in the review page.""" status = self.addon.status latest_version = self.version # The confirm auto-approval action should not show the comment box, # so override the text in case the reviewer switched between actions # and accidently submitted some comments from another action. self.data['comments'] = '' if self.content_review_only: # If we're only doing a content review, then don't increment # the counter, just record the date of the content approval # and log it. AddonApprovalsCounter.approve_content_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_CONTENT) else: if self.version.channel == amo.RELEASE_CHANNEL_LISTED: self.version.autoapprovalsummary.update(confirmed=True) AddonApprovalsCounter.increment_for_addon(addon=self.addon) self.log_action(amo.LOG.CONFIRM_AUTO_APPROVED) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points( self.request.user, self.addon, status, version=latest_version, post_review=True, content_review=self.content_review_only)
def approve_content(self): """Approve content of an add-on.""" channel = self.version.channel version = self.addon.current_version # Content review only action. assert self.content_review # Doesn't make sense for unlisted versions. assert channel == amo.RELEASE_CHANNEL_LISTED # Like confirm auto approval, the approve content action should not # show the comment box, so override the text in case the reviewer # switched between actions and accidently submitted some comments from # another action. self.data['comments'] = '' # When doing a content review, don't increment the approvals counter, # just record the date of the content approval and log it. AddonApprovalsCounter.approve_content_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_CONTENT, version=version) # Assign reviewer incentive scores. if self.human_review: is_post_review = channel == amo.RELEASE_CHANNEL_LISTED ReviewerScore.award_points(self.user, self.addon, self.addon.status, version=version, post_review=is_post_review, content_review=self.content_review)
def ratings_moderation_log_detail(request, id): log = get_object_or_404(ActivityLog.objects.moderation_events(), pk=id) review = None # I really cannot express the depth of the insanity incarnate in # our logging code... if len(log.arguments) > 1 and isinstance(log.arguments[1], Rating): review = log.arguments[1] is_admin = acl.action_allowed(request, amo.permissions.REVIEWS_ADMIN) can_undelete = review and review.deleted and ( is_admin or request.user.pk == log.user.pk) if request.method == 'POST': # A Form seems overkill for this. if request.POST['action'] == 'undelete': if not can_undelete: raise PermissionDenied ReviewerScore.award_moderation_points( log.user, review.addon, review.id, undo=True) review.undelete() return redirect('reviewers.ratings_moderation_log.detail', id) data = context(request, log=log, can_undelete=can_undelete) return render(request, 'reviewers/moderationlog_detail.html', data)
def process_sandbox(self): """Set an addon or a version back to sandbox.""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use reject_multiple_versions instead). assert not self.content_review_only # Hold onto the status before we change it. status = self.addon.status if self.set_addon_status: self.set_addon(status=amo.STATUS_NULL) self.set_files(amo.STATUS_DISABLED, self.files, hide_disabled_file=True) self.log_action(amo.LOG.REJECT_VERSION) template = u'%s_to_sandbox' % self.review_type subject = u'Mozilla Add-ons: %s %s didn\'t pass review' self.notify_email(template, subject) self.log_sandbox_message() log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points(self.request.user, self.addon, status, version=self.version)
def process_sandbox(self): """Set an addon or a version back to sandbox.""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use reject_multiple_versions instead). assert not self.content_review_only # Hold onto the status before we change it. status = self.addon.status if self.set_addon_status: self.set_addon(status=amo.STATUS_NULL) self.set_files(amo.STATUS_DISABLED, self.files, hide_disabled_file=True) self.log_action(amo.LOG.REJECT_VERSION) template = u'%s_to_sandbox' % self.review_type subject = u'Mozilla Add-ons: %s %s didn\'t pass review' self.notify_email(template, subject) self.log_sandbox_message() log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points( self.request.user, self.addon, status, version=self.version)
def delete(self, user_responsible=None): if user_responsible is None: user_responsible = self.user rating_was_moderated = False # Log deleting ratings to moderation log, # except if the author deletes it if user_responsible != self.user: # Remember moderation state rating_was_moderated = True from olympia.reviewers.models import ReviewerScore activity.log_create( amo.LOG.DELETE_RATING, self.addon, self, user=user_responsible, details=dict( body=unicode(self.body), addon_id=self.addon.pk, addon_title=unicode(self.addon.name), is_flagged=self.ratingflag_set.exists())) for flag in self.ratingflag_set.all(): flag.delete() log.info(u'Rating deleted: %s deleted id:%s by %s ("%s")', user_responsible.name, self.pk, self.user.name, unicode(self.body)) self.update(deleted=True) # Force refreshing of denormalized data (it wouldn't happen otherwise # because we're not dealing with a creation). self.update_denormalized_fields() if rating_was_moderated: ReviewerScore.award_moderation_points(user_responsible, self.addon, self.pk)
def delete(self, user_responsible=None): if user_responsible is None: user_responsible = self.user rating_was_moderated = False # Log deleting ratings to moderation log, # except if the author deletes it if user_responsible != self.user: # Remember moderation state rating_was_moderated = True from olympia.reviewers.models import ReviewerScore activity.log_create(amo.LOG.DELETE_RATING, self.addon, self, user=user_responsible, details=dict( body=six.text_type(self.body), addon_id=self.addon.pk, addon_title=six.text_type(self.addon.name), is_flagged=self.ratingflag_set.exists())) for flag in self.ratingflag_set.all(): flag.delete() log.info(u'Rating deleted: %s deleted id:%s by %s ("%s")', user_responsible.name, self.pk, self.user.name, six.text_type(self.body)) self.update(deleted=True) # Force refreshing of denormalized data (it wouldn't happen otherwise # because we're not dealing with a creation). self.update_denormalized_fields() if rating_was_moderated: ReviewerScore.award_moderation_points(user_responsible, self.addon, self.pk)
def award_all_points_for_action(self, action, content_review=False): for activity_log in ActivityLog.objects.filter(action=action.id): user = activity_log.user try: addon = activity_log.arguments[0] version = activity_log.arguments[1] except IndexError: log.error('ActivityLog %d is missing one or more arguments', activity_log.pk) continue # If there is already a score recorded in the database for this # event, with our special note, it means we already processed it # somehow (maybe we ran the script twice...), so ignore it. # Otherwise award the points! event = ReviewerScore.get_event( addon, amo.STATUS_PUBLIC, version=version, post_review=True, content_review=content_review) if not ReviewerScore.objects.filter( user=user, addon=addon, note_key=event, note=MANUAL_NOTE).exists(): ReviewerScore.award_points( user, addon, amo.STATUS_PUBLIC, version=version, post_review=True, content_review=content_review, extra_note=MANUAL_NOTE) else: log.error('Already awarded points for "%s" action on %s %s', action.short, addon, version) continue
def create_and_review_addon(self, user, weight, verdict, content_review): addon = addon_factory() AutoApprovalSummary.objects.create( version=addon.current_version, verdict=verdict, weight=weight) ReviewerScore.award_points( user, addon, addon.status, version=addon.versions.all()[0], post_review=True, content_review=content_review)
def award_all_points_for_action(self, action, content_review=False): for activity_log in ActivityLog.objects.filter(action=action.id): user = activity_log.user try: addon = activity_log.arguments[0] version = activity_log.arguments[1] except IndexError: log.error('ActivityLog %d is missing one or more arguments', activity_log.pk) continue # If there is already a score recorded in the database for this # event, with our special note, it means we already processed it # somehow (maybe we ran the script twice...), so ignore it. # Otherwise award the points! event = ReviewerScore.get_event(addon, amo.STATUS_PUBLIC, version=version, post_review=True, content_review=content_review) if not ReviewerScore.objects.filter( user=user, addon=addon, note_key=event, note=MANUAL_NOTE).exists(): ReviewerScore.award_points(user, addon, amo.STATUS_PUBLIC, version=version, post_review=True, content_review=content_review, extra_note=MANUAL_NOTE) else: log.error('Already awarded points for "%s" action on %s %s', action.short, addon, version) continue
def eventlog_detail(request, id): log = get_object_or_404(ActivityLog.objects.reviewer_events(), pk=id) review = None # I really cannot express the depth of the insanity incarnate in # our logging code... if len(log.arguments) > 1 and isinstance(log.arguments[1], Rating): review = log.arguments[1] is_admin = acl.action_allowed(request, amo.permissions.REVIEWS_ADMIN) can_undelete = review and review.deleted and (is_admin or request.user.pk == log.user.pk) if request.method == 'POST': # A Form seems overkill for this. if request.POST['action'] == 'undelete': if not can_undelete: raise PermissionDenied ReviewerScore.award_moderation_points(log.user, review.addon, review.id, undo=True) review.undelete() return redirect('reviewers.eventlog.detail', id) data = context(request, log=log, can_undelete=can_undelete) return render(request, 'reviewers/eventlog_detail.html', data)
def approve_latest_version(self): """Approve the add-on latest version (potentially setting the add-on to approved if it was awaiting its first review).""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use confirm_auto_approved instead). assert not self.content_review # Sign addon. self.sign_files() # Hold onto the status before we change it. status = self.addon.status # Save files first, because set_addon checks to make sure there # is at least one public file or it won't make the addon public. self.set_files(amo.STATUS_APPROVED, self.files) self.set_promoted() if self.set_addon_status: self.set_addon(status=amo.STATUS_APPROVED) if self.human_review: # No need for a human review anymore in this channel. self.clear_all_needs_human_review_flags_in_channel() # Clear pending rejection since we approved that version. VersionReviewerFlags.objects.filter( version=self.version, ).update(pending_rejection=None) # An approval took place so we can reset this. AddonReviewerFlags.objects.update_or_create( addon=self.addon, defaults={'auto_approval_disabled_until_next_approval': False}) # The counter can be incremented. AddonApprovalsCounter.increment_for_addon(addon=self.addon) # Assign reviewer incentive scores. ReviewerScore.award_points(self.user, self.addon, status, version=self.version) else: # Automatic approval, reset the counter. AddonApprovalsCounter.reset_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_VERSION) template = u'%s_to_approved' % self.review_type if self.review_type in ['extension_pending', 'theme_pending']: subject = u'Mozilla Add-ons: %s %s Updated' else: subject = u'Mozilla Add-ons: %s %s Approved' self.notify_email(template, subject) self.log_public_message() log.info(u'Sending email for %s' % (self.addon))
def reject_multiple_versions(self): """Reject a list of versions.""" # self.version and self.files won't point to the versions we want to # modify in this action, so set them to None before finding the right # versions. status = self.addon.status latest_version = self.version self.version = None self.files = None action_id = (amo.LOG.REJECT_CONTENT if self.content_review else amo.LOG.REJECT_VERSION) timestamp = datetime.now() log.info( u'Making %s versions %s disabled' % (self.addon, u', '.join(str(v.pk) for v in self.data['versions']))) for version in self.data['versions']: files = version.files.all() self.set_files(amo.STATUS_DISABLED, files, hide_disabled_file=True) self.log_action(action_id, version=version, files=files, timestamp=timestamp) if self.human_review: # Unset needs_human_review on rejected versions, we consider # that the reviewer looked at them before rejecting. if version.needs_human_review: version.update(needs_human_review=False) self.addon.update_status() # Assign reviewer incentive scores and send email, if it's an human # reviewer: if it's not, it's coming from some automation where we # don't need to notify the developer (we should already have done that # before) and don't need to award points. if self.human_review: channel = latest_version.channel # Send the email to the developer. We need to pass the latest # version of the add-on instead of one of the versions we rejected, # it will be used to generate a token allowing the developer to # reply, and that only works with the latest version. self.data['version_numbers'] = u', '.join( str(v.version) for v in self.data['versions']) if (self.addon.status != amo.STATUS_APPROVED and channel == amo.RELEASE_CHANNEL_LISTED): template = u'reject_multiple_versions_disabled_addon' subject = (u'Mozilla Add-ons: %s%s has been disabled on ' u'addons.mozilla.org') else: template = u'reject_multiple_versions' subject = u'Mozilla Add-ons: Versions disabled for %s%s' log.info(u'Sending email for %s' % (self.addon)) self.notify_email(template, subject, version=latest_version) ReviewerScore.award_points(self.user, self.addon, status, version=latest_version, post_review=True, content_review=self.content_review)
def process_public(self): """Set an add-on or a version to public.""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use confirm_auto_approved instead). assert not self.content_review_only # Sign addon. use_autograph = waffle.flag_is_active(self.request, 'activate-autograph-signing') for file_ in self.files: sign_file(file_, use_autograph=use_autograph) # Hold onto the status before we change it. status = self.addon.status # Save files first, because set_addon checks to make sure there # is at least one public file or it won't make the addon public. self.set_files(amo.STATUS_PUBLIC, self.files) if self.set_addon_status: self.set_addon(status=amo.STATUS_PUBLIC) # If we've approved a webextension, add a tag identifying them as such. if any(file_.is_webextension for file_ in self.files): Tag(tag_text='firefox57').save_tag(self.addon) # If we've approved a mozilla signed add-on, add the firefox57 tag if all(file_.is_mozilla_signed_extension for file_ in self.files): Tag(tag_text='firefox57').save_tag(self.addon) # Increment approvals counter if we have a request (it means it's a # human doing the review) otherwise reset it as it's an automatic # approval. if self.request: AddonApprovalsCounter.increment_for_addon(addon=self.addon) else: AddonApprovalsCounter.reset_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_VERSION) template = u'%s_to_public' % self.review_type if self.review_type == 'pending': subject = u'Mozilla Add-ons: %s %s Updated' else: subject = u'Mozilla Add-ons: %s %s Approved' self.notify_email(template, subject) self.log_public_message() log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points(self.request.user, self.addon, status, version=self.version)
def confirm_auto_approved(self): """Confirm an auto-approval decision.""" channel = self.version.channel if channel == amo.RELEASE_CHANNEL_LISTED: # When doing an approval in listed channel, the version we care # about is always current_version and *not* self.version. # This allows reviewers to confirm approval of a public add-on even # when their latest version is disabled. version = self.addon.current_version else: # For unlisted, we just use self.version. version = self.version # The confirm auto-approval action should not show the comment box, # so override the text in case the reviewer switched between actions # and accidently submitted some comments from another action. self.data['comments'] = '' self.log_action(amo.LOG.CONFIRM_AUTO_APPROVED, version=version) if self.human_review: # Mark the approval as confirmed (handle DoesNotExist, it may have # been auto-approved before we unified workflow for unlisted and # listed). try: version.autoapprovalsummary.update(confirmed=True) except AutoApprovalSummary.DoesNotExist: pass if channel == amo.RELEASE_CHANNEL_LISTED: # Clear needs_human_review flags on past versions in channel. self.unset_past_needs_human_review() AddonApprovalsCounter.increment_for_addon(addon=self.addon) else: # For now, for unlisted versions, only drop the # needs_human_review flag on the latest version. if self.version.needs_human_review: self.version.update(needs_human_review=False) # Clear the "needs_human_review_by_mad" and "pending_rejection" # flags too, if any, only for the specified version. VersionReviewerFlags.objects.filter(version=self.version).update( needs_human_review_by_mad=False, pending_rejection=None) # For other versions, we also clear pending_rejection (Note that # the action should only be accessible to admins if the current # version is pending rejection). VersionReviewerFlags.objects.filter( version__addon=self.addon).update(pending_rejection=None) is_post_review = channel == amo.RELEASE_CHANNEL_LISTED ReviewerScore.award_points(self.user, self.addon, self.addon.status, version=version, post_review=is_post_review, content_review=self.content_review)
def process_public(self): """Set an add-on or a version to public.""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use confirm_auto_approved instead). assert not self.content_review # Sign addon. self.sign_files() # Hold onto the status before we change it. status = self.addon.status # Save files first, because set_addon checks to make sure there # is at least one public file or it won't make the addon public. self.set_files(amo.STATUS_APPROVED, self.files) self.set_recommended() if self.set_addon_status: self.set_addon(status=amo.STATUS_APPROVED) # Clear needs_human_review flags on past listed versions. if self.human_review: self.unset_past_needs_human_review() # Clear the "needs_human_review" scanner flags too, if any, and # only for the specified version. VersionReviewerFlags.objects.filter(version=self.version).update( needs_human_review_by_mad=False) # Increment approvals counter if we have a request (it means it's a # human doing the review) otherwise reset it as it's an automatic # approval. if self.human_review: AddonApprovalsCounter.increment_for_addon(addon=self.addon) else: AddonApprovalsCounter.reset_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_VERSION) template = u'%s_to_approved' % self.review_type if self.review_type in ['extension_pending', 'theme_pending']: subject = u'Mozilla Add-ons: %s %s Updated' else: subject = u'Mozilla Add-ons: %s %s Approved' self.notify_email(template, subject) self.log_public_message() log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.human_review: ReviewerScore.award_points(self.user, self.addon, status, version=self.version)
def reviewers_score_bar(context, types=None, addon_type=None): user = context.get('user') return new_context(dict( request=context.get('request'), amo=amo, settings=settings, points=ReviewerScore.get_recent(user, addon_type=addon_type), total=ReviewerScore.get_total(user), **ReviewerScore.get_leaderboards(user, types=types, addon_type=addon_type)))
def process_public(self): """Set an add-on or a version to public.""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use confirm_auto_approved instead). assert not self.content_review_only # Sign addon. for file_ in self.files: sign_file(file_) # Hold onto the status before we change it. status = self.addon.status # Save files first, because set_addon checks to make sure there # is at least one public file or it won't make the addon public. self.set_files(amo.STATUS_PUBLIC, self.files) if self.set_addon_status: self.set_addon(status=amo.STATUS_PUBLIC) # If we've approved a webextension, add a tag identifying them as such. if any(file_.is_webextension for file_ in self.files): Tag(tag_text='firefox57').save_tag(self.addon) # If we've approved a mozilla signed add-on, add the firefox57 tag if all(file_.is_mozilla_signed_extension for file_ in self.files): Tag(tag_text='firefox57').save_tag(self.addon) # Increment approvals counter if we have a request (it means it's a # human doing the review) otherwise reset it as it's an automatic # approval. if self.request: AddonApprovalsCounter.increment_for_addon(addon=self.addon) else: AddonApprovalsCounter.reset_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_VERSION) template = u'%s_to_public' % self.review_type if self.review_type == 'pending': subject = u'Mozilla Add-ons: %s %s Updated' else: subject = u'Mozilla Add-ons: %s %s Approved' self.notify_email(template, subject) self.log_public_message() log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points( self.request.user, self.addon, status, version=self.version)
def reject_multiple_versions(self): """Reject a list of versions.""" # self.version and self.files won't point to the versions we want to # modify in this action, so set them to None before finding the right # versions. status = self.addon.status latest_version = self.version self.version = None self.files = None action_id = (amo.LOG.REJECT_CONTENT if self.content_review_only else amo.LOG.REJECT_VERSION) timestamp = datetime.now() for version in self.data['versions']: files = version.files.all() self.set_files(amo.STATUS_DISABLED, files, hide_disabled_file=True) self.log_action(action_id, version=version, files=files, timestamp=timestamp) self.addon.update_status() self.data['version_numbers'] = u', '.join( unicode(v.version) for v in self.data['versions']) # Send the email to the developer. We need to pass the latest version # of the add-on instead of one of the versions we rejected, it will be # used to generate a token allowing the developer to reply, and that # only works with the latest version. if self.addon.status != amo.STATUS_PUBLIC: template = u'reject_multiple_versions_disabled_addon' subject = (u'Mozilla Add-ons: %s%s has been disabled on ' u'addons.mozilla.org') else: template = u'reject_multiple_versions' subject = u'Mozilla Add-ons: Versions disabled for %s%s' self.notify_email(template, subject, version=latest_version) log.info(u'Making %s versions %s disabled' % (self.addon, u', '.join( unicode(v.pk) for v in self.data['versions']))) log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points(self.request.user, self.addon, status, version=latest_version, post_review=True, content_review=self.content_review_only)
def approve(self, user): from olympia.reviewers.models import ReviewerScore activity.log_create( amo.LOG.APPROVE_RATING, self.addon, self, user=user, details=dict( body=unicode(self.body), addon_id=self.addon.pk, addon_title=unicode(self.addon.name), is_flagged=self.ratingflag_set.exists())) for flag in self.ratingflag_set.all(): flag.delete() self.editorreview = False # We've already logged what we want to log, no need to pass # user_responsible=user. self.save() ReviewerScore.award_moderation_points(user, self.addon, self.pk)
def themes_commit(request): ThemeReviewFormset = formset_factory(forms.ThemeReviewForm) formset = ThemeReviewFormset(request.POST) scores = [] for form in formset: try: lock = ThemeLock.objects.filter( theme_id=form.data[form.prefix + '-theme'], reviewer=request.user) except MultiValueDictKeyError: # Address off-by-one error caused by management form. continue if lock and form.is_valid(): scores.append(form.save()) # Success message. points = sum(scores) success = ungettext( # L10n: {0} is the number of reviews. {1} is the points just earned. # L10n: {2} is the total number of points the reviewer has overall. '{0} theme review successfully processed (+{1} points, {2} total).', '{0} theme reviews successfully processed (+{1} points, {2} total).', len(scores)).format(len(scores), points, ReviewerScore.get_total(request.user)) amo.messages.success(request, success) if 'theme_redirect_url' in request.session: return redirect(request.session['theme_redirect_url']) else: return redirect(reverse('reviewers.themes.queue_themes'))
def save(self): action = self.cleaned_data['action'] comment = self.cleaned_data.get('comment') reject_reason = self.cleaned_data.get('reject_reason') theme = self.cleaned_data['theme'] is_rereview = ( theme.rereviewqueuetheme_set.exists() and theme.addon.status not in (amo.STATUS_PENDING, amo.STATUS_REVIEW_PENDING)) theme_lock = ThemeLock.objects.get(theme=self.cleaned_data['theme']) mail_and_log = True if action == amo.ACTION_APPROVE: if is_rereview: approve_rereview(theme) theme.addon.update(status=amo.STATUS_PUBLIC) theme.approve = datetime.datetime.now() theme.save() elif action in (amo.ACTION_REJECT, amo.ACTION_DUPLICATE): if is_rereview: reject_rereview(theme) else: theme.addon.update(status=amo.STATUS_REJECTED) elif action == amo.ACTION_FLAG: if is_rereview: mail_and_log = False else: theme.addon.update(status=amo.STATUS_REVIEW_PENDING) elif action == amo.ACTION_MOREINFO: if not is_rereview: theme.addon.update(status=amo.STATUS_REVIEW_PENDING) if mail_and_log: send_mail(self.cleaned_data, theme_lock) # Log. ActivityLog.create( amo.LOG.THEME_REVIEW, theme.addon, details={ 'theme': theme.addon.name.localized_string, 'action': action, 'reject_reason': reject_reason, 'comment': comment}, user=theme_lock.reviewer) log.info('%sTheme %s (%s) - %s' % ( '[Rereview] ' if is_rereview else '', theme.addon.name, theme.id, action)) score = 0 if action in (amo.ACTION_REJECT, amo.ACTION_DUPLICATE, amo.ACTION_APPROVE): score = ReviewerScore.award_points( theme_lock.reviewer, theme.addon, theme.addon.status) theme_lock.delete() return score
def reject_multiple_versions(self): """Reject a list of versions.""" # self.version and self.files won't point to the versions we want to # modify in this action, so set them to None before finding the right # versions. status = self.addon.status latest_version = self.version self.version = None self.files = None action_id = (amo.LOG.REJECT_CONTENT if self.content_review_only else amo.LOG.REJECT_VERSION) timestamp = datetime.now() for version in self.data['versions']: files = version.files.all() self.set_files(amo.STATUS_DISABLED, files, hide_disabled_file=True) self.log_action(action_id, version=version, files=files, timestamp=timestamp) self.addon.update_status() self.data['version_numbers'] = u', '.join( six.text_type(v.version) for v in self.data['versions']) # Send the email to the developer. We need to pass the latest version # of the add-on instead of one of the versions we rejected, it will be # used to generate a token allowing the developer to reply, and that # only works with the latest version. if self.addon.status != amo.STATUS_APPROVED: template = u'reject_multiple_versions_disabled_addon' subject = (u'Mozilla Add-ons: %s%s has been disabled on ' u'addons.mozilla.org') else: template = u'reject_multiple_versions' subject = u'Mozilla Add-ons: Versions disabled for %s%s' self.notify_email(template, subject, version=latest_version) log.info( u'Making %s versions %s disabled' % ( self.addon, u', '.join( six.text_type(v.pk) for v in self.data['versions']))) log.info(u'Sending email for %s' % (self.addon)) # Assign reviewer incentive scores. if self.request: ReviewerScore.award_points( self.request.user, self.addon, status, version=latest_version, post_review=True, content_review=self.content_review_only)
def reject_latest_version(self): """Reject the add-on latest version (potentially setting the add-on back to incomplete if it was awaiting its first review).""" # Safeguard to force implementation for unlisted add-ons to completely # override this method. assert self.version.channel == amo.RELEASE_CHANNEL_LISTED # Safeguard to make sure this action is not used for content review # (it should use reject_multiple_versions instead). assert not self.content_review # Hold onto the status before we change it. status = self.addon.status if self.set_addon_status: self.set_addon(status=amo.STATUS_NULL) self.set_files(amo.STATUS_DISABLED, self.files, hide_disabled_file=True) if self.human_review: # Clear needs human review flags, but only on the latest version: # it's the only version we can be certain that the reviewer looked # at. self.clear_specific_needs_human_review_flags(self.version) # Assign reviewer incentive scores. ReviewerScore.award_points(self.user, self.addon, status, version=self.version) self.log_action(amo.LOG.REJECT_VERSION) template = u'%s_to_rejected' % self.review_type subject = u'Mozilla Add-ons: %s %s didn\'t pass review' self.notify_email(template, subject) self.log_sandbox_message() log.info(u'Sending email for %s' % (self.addon))
def confirm_auto_approved(self): """Confirm an auto-approval decision.""" channel = self.version.channel if channel == amo.RELEASE_CHANNEL_LISTED: # When doing an approval in listed channel, the version we care # about is always current_version and *not* self.version. # This allows reviewers to confirm approval of a public add-on even # when their latest version is disabled. version = self.addon.current_version else: # For unlisted, we just use self.version. version = self.version # The confirm auto-approval action should not show the comment box, # so override the text in case the reviewer switched between actions # and accidently submitted some comments from another action. self.data['comments'] = '' if self.content_review_only: # If we're only doing a content review, then don't increment # the counter, just record the date of the content approval # and log it. AddonApprovalsCounter.approve_content_for_addon(addon=self.addon) self.log_action(amo.LOG.APPROVE_CONTENT, version=version) else: if channel == amo.RELEASE_CHANNEL_LISTED: version.autoapprovalsummary.update(confirmed=True) AddonApprovalsCounter.increment_for_addon(addon=self.addon) self.log_action(amo.LOG.CONFIRM_AUTO_APPROVED, version=version) # Assign reviewer incentive scores. if self.request: is_post_review = channel == amo.RELEASE_CHANNEL_LISTED ReviewerScore.award_points( self.request.user, self.addon, self.addon.status, version=version, post_review=is_post_review, content_review=self.content_review_only)
def generate_review_data(self): with freeze_time(self.last_week_begin): self.reviewer1 = user_factory(display_name='Volunteer A') self.reviewer2 = user_factory(display_name='Staff B') self.reviewer3 = user_factory(display_name=None) self.reviewer4 = user_factory(display_name='Staff Content D') self.reviewer5 = user_factory(display_name='Deleted') grant_permission(self.reviewer2, '', name='No Reviewer Incentives') grant_permission(self.reviewer4, '', name='No Reviewer Incentives') data = [ (self.reviewer1, 178, amo.AUTO_APPROVED, False), (self.reviewer1, 95, amo.AUTO_APPROVED, False), (self.reviewer1, 123, amo.NOT_AUTO_APPROVED, False), (self.reviewer1, 328, amo.AUTO_APPROVED, False), (self.reviewer1, 450, amo.AUTO_APPROVED, False), (self.reviewer1, 999, amo.NOT_AUTO_APPROVED, False), (self.reviewer1, 131, amo.AUTO_APPROVED, False), (self.reviewer1, 74, amo.NOT_AUTO_APPROVED, False), (self.reviewer1, 15, amo.AUTO_APPROVED, False), (self.reviewer2, 951, amo.NOT_AUTO_APPROVED, False), (self.reviewer2, 8421, amo.AUTO_APPROVED, False), (self.reviewer2, 281, amo.AUTO_APPROVED, False), (self.reviewer2, 54, amo.NOT_AUTO_APPROVED, False), (self.reviewer2, 91, amo.NOT_AUTO_APPROVED, False), (self.reviewer2, 192, amo.AUTO_APPROVED, False), (self.reviewer2, 222, amo.NOT_AUTO_APPROVED, False), (self.reviewer3, 178, amo.AUTO_APPROVED, True), (self.reviewer3, 95, amo.AUTO_APPROVED, True), (self.reviewer3, 123, amo.NOT_AUTO_APPROVED, True), (self.reviewer3, 328, amo.AUTO_APPROVED, True), (self.reviewer3, 450, amo.AUTO_APPROVED, True), (self.reviewer3, 999, amo.NOT_AUTO_APPROVED, True), (self.reviewer3, 131, amo.AUTO_APPROVED, True), (self.reviewer3, 74, amo.NOT_AUTO_APPROVED, True), (self.reviewer3, 15, amo.AUTO_APPROVED, True), (self.reviewer3, 48, amo.AUTO_APPROVED, True), (self.reviewer3, 87, amo.NOT_AUTO_APPROVED, True), (self.reviewer3, 265, amo.AUTO_APPROVED, True), (self.reviewer4, 951, amo.NOT_AUTO_APPROVED, True), (self.reviewer4, 8421, amo.AUTO_APPROVED, True), (self.reviewer4, 281, amo.AUTO_APPROVED, True), (self.reviewer4, 54, amo.NOT_AUTO_APPROVED, True), (self.reviewer4, 91, amo.NOT_AUTO_APPROVED, True), (self.reviewer4, 192, amo.AUTO_APPROVED, True), (self.reviewer4, 222, amo.NOT_AUTO_APPROVED, True), (self.reviewer4, 192, amo.AUTO_APPROVED, True), (self.reviewer4, 444, amo.NOT_AUTO_APPROVED, True), (self.reviewer4, 749, amo.AUTO_APPROVED, True), (self.reviewer5, 523, amo.NOT_AUTO_APPROVED, True), (self.reviewer5, 126, amo.AUTO_APPROVED, True), (self.reviewer5, 246, amo.AUTO_APPROVED, False), (self.reviewer5, 8740, amo.NOT_AUTO_APPROVED, True), (self.reviewer5, 346, amo.NOT_AUTO_APPROVED, False), (self.reviewer5, 985, amo.AUTO_APPROVED, False), (self.reviewer5, 123, amo.NOT_AUTO_APPROVED, True), (self.reviewer5, 93, amo.AUTO_APPROVED, True), (self.reviewer5, 22, amo.NOT_AUTO_APPROVED, False), (self.reviewer5, 165, amo.AUTO_APPROVED, True), ] for review_action in data: self.create_and_review_addon(review_action[0], review_action[1], review_action[2], review_action[3]) self.reviewer5.delete() mail.outbox = [] # Search plugin (submitted before auto-approval was implemented) search_plugin = addon_factory(type=4) ReviewerScore.award_points(self.reviewer3, search_plugin, amo.STATUS_APPROVED, version=search_plugin.versions.all()[0], post_review=False, content_review=True) # Dictionary (submitted before auto-approval was implemented) dictionary = addon_factory(type=3) ReviewerScore.award_points(self.reviewer3, dictionary, amo.STATUS_APPROVED, version=dictionary.versions.all()[0], post_review=False, content_review=True)
def reject_multiple_versions(self): """Reject a list of versions. Note: this is used in blocklist.utils.disable_addon_for_block for both listed and unlisted versions (human_review=False).""" # self.version and self.files won't point to the versions we want to # modify in this action, so set them to None before finding the right # versions. status = self.addon.status latest_version = self.version self.version = None self.files = None now = datetime.now() if self.data.get('delayed_rejection'): pending_rejection_deadline = now + timedelta( days=int(self.data['delayed_rejection_days'])) else: pending_rejection_deadline = None if pending_rejection_deadline: action_id = (amo.LOG.REJECT_CONTENT_DELAYED if self.content_review else amo.LOG.REJECT_VERSION_DELAYED) log.info('Marking %s versions %s for delayed rejection' % (self.addon, ', '.join( str(v.pk) for v in self.data['versions']))) else: action_id = (amo.LOG.REJECT_CONTENT if self.content_review else amo.LOG.REJECT_VERSION) log.info('Making %s versions %s disabled' % (self.addon, ', '.join( str(v.pk) for v in self.data['versions']))) for version in self.data['versions']: files = version.files.all() if not pending_rejection_deadline: self.set_files(amo.STATUS_DISABLED, files, hide_disabled_file=True) self.log_action(action_id, version=version, files=files, timestamp=now) if self.human_review: # Clear needs human review flags on rejected versions, we # consider that the reviewer looked at them before rejecting. self.clear_specific_needs_human_review_flags(version) # (Re)set pending_rejection. Could be reset to None if doing an # immediate rejection. VersionReviewerFlags.objects.update_or_create( version=version, defaults={'pending_rejection': pending_rejection_deadline}) if pending_rejection_deadline: # A delayed rejection implies the next version should be manually # reviewed and the developers should be notified again once the # deadline is close. AddonReviewerFlags.objects.update_or_create( addon=self.addon, defaults={ 'notified_about_expiring_delayed_rejections': False, 'auto_approval_disabled_until_next_approval': True }) # The reviewer should be automatically subscribed to any new # versions posted to the same channel. ReviewerSubscription.objects.get_or_create( user=self.user, addon=self.addon, channel=latest_version.channel) else: # An immediate one might require the add-on status to change. self.addon.update_status() # Assign reviewer incentive scores and send email, if it's an human # reviewer: if it's not, it's coming from some automation where we # don't need to notify the developer (we should already have done that # before) and don't need to award points. if self.human_review: channel = latest_version.channel # Send the email to the developer. We need to pass the latest # version of the add-on instead of one of the versions we rejected, # it will be used to generate a token allowing the developer to # reply, and that only works with the latest version. self.data['version_numbers'] = u', '.join( str(v.version) for v in self.data['versions']) if pending_rejection_deadline: template = 'reject_multiple_versions_with_delay' subject = ('Mozilla Add-ons: %s%s will be disabled on ' 'addons.mozilla.org') elif (self.addon.status != amo.STATUS_APPROVED and channel == amo.RELEASE_CHANNEL_LISTED): template = 'reject_multiple_versions_disabled_addon' subject = ('Mozilla Add-ons: %s%s has been disabled on ' 'addons.mozilla.org') else: template = 'reject_multiple_versions' subject = 'Mozilla Add-ons: Versions disabled for %s%s' log.info('Sending email for %s' % (self.addon)) self.notify_email(template, subject, version=latest_version) ReviewerScore.award_points(self.user, self.addon, status, version=latest_version, post_review=True, content_review=self.content_review)
def leaderboard(request): return render(request, 'reviewers/leaderboard.html', context( request, scores=ReviewerScore.all_users_by_score()))
def performance(request, user_id=False): user = request.user reviewers = _recent_reviewers() is_admin = acl.action_allowed(request, amo.permissions.REVIEWS_ADMIN) if is_admin and user_id: try: user = UserProfile.objects.get(pk=user_id) except UserProfile.DoesNotExist: pass # Use request.user from above. monthly_data = _performance_by_month(user.id) performance_total = _performance_total(monthly_data) # Incentive point breakdown. today = date.today() month_ago = today - timedelta(days=30) year_ago = today - timedelta(days=365) point_total = ReviewerScore.get_total(user) totals = ReviewerScore.get_breakdown(user) months = ReviewerScore.get_breakdown_since(user, month_ago) years = ReviewerScore.get_breakdown_since(user, year_ago) def _sum(iter, types, exclude=False): """Sum the `total` property for items in `iter` that have an `atype` that is included in `types` when `exclude` is False (default) or not in `types` when `exclude` is True.""" return sum(s.total for s in iter if (s.atype in types) == (not exclude)) breakdown = { 'month': { 'addons': _sum(months, amo.GROUP_TYPE_ADDON), 'themes': _sum(months, amo.GROUP_TYPE_THEME), 'other': _sum(months, amo.GROUP_TYPE_ADDON + amo.GROUP_TYPE_THEME, exclude=True) }, 'year': { 'addons': _sum(years, amo.GROUP_TYPE_ADDON), 'themes': _sum(years, amo.GROUP_TYPE_THEME), 'other': _sum(years, amo.GROUP_TYPE_ADDON + amo.GROUP_TYPE_THEME, exclude=True) }, 'total': { 'addons': _sum(totals, amo.GROUP_TYPE_ADDON), 'themes': _sum(totals, amo.GROUP_TYPE_THEME), 'other': _sum(totals, amo.GROUP_TYPE_ADDON + amo.GROUP_TYPE_THEME, exclude=True) } } data = context(request, monthly_data=json.dumps(monthly_data), performance_month=performance_total['month'], performance_year=performance_total['year'], breakdown=breakdown, point_total=point_total, reviewers=reviewers, current_user=user, is_admin=is_admin, is_user=(request.user.id == user.id)) return render(request, 'reviewers/performance.html', data)
def leaderboard(request): return render(request, 'reviewers/leaderboard.html', context(request, scores=ReviewerScore.all_users_by_score()))