384 строки
16 KiB
Python
384 строки
16 KiB
Python
from datetime import datetime
|
|
|
|
from django.conf import settings
|
|
from django.utils.datastructures import SortedDict
|
|
|
|
import commonware.log
|
|
from tower import ugettext_lazy as _lazy
|
|
|
|
import amo
|
|
from access import acl
|
|
from amo.helpers import absolutify
|
|
from amo.urlresolvers import reverse
|
|
from amo.utils import send_mail_jinja
|
|
from editors.models import EscalationQueue, ReviewerScore
|
|
from files.models import File
|
|
|
|
from .models import RereviewQueue
|
|
|
|
|
|
log = commonware.log.getLogger('z.mailer')
|
|
|
|
|
|
def send_mail(subject, template, context, emails, perm_setting=None, cc=None):
|
|
# Link to our newfangled "Account Settings" page.
|
|
manage_url = absolutify(reverse('account.settings')) + '#notifications'
|
|
send_mail_jinja(subject, template, context, recipient_list=emails,
|
|
from_email=settings.NOBODY_EMAIL, use_blacklist=False,
|
|
perm_setting=perm_setting, manage_url=manage_url,
|
|
headers={'Reply-To': settings.MKT_REVIEWERS_EMAIL}, cc=cc)
|
|
|
|
|
|
class ReviewBase(object):
|
|
|
|
def __init__(self, request, addon, version, review_type):
|
|
self.request = request
|
|
self.user = self.request.user
|
|
self.addon = addon
|
|
self.version = version
|
|
self.review_type = review_type
|
|
self.files = None
|
|
self.in_pending = self.addon.status == amo.STATUS_PENDING
|
|
self.in_rereview = RereviewQueue.objects.filter(
|
|
addon=self.addon).exists()
|
|
self.in_escalate = EscalationQueue.objects.filter(
|
|
addon=self.addon).exists()
|
|
|
|
def set_addon(self, **kw):
|
|
"""Alters addon and sets reviewed timestamp on version."""
|
|
self.addon.update(**kw)
|
|
self.version.update(reviewed=datetime.now())
|
|
|
|
def set_files(self, status, files, copy_to_mirror=False,
|
|
hide_disabled_file=False):
|
|
"""Change the files to be the new status
|
|
and copy, remove from the mirror as appropriate."""
|
|
for file in files:
|
|
file.datestatuschanged = datetime.now()
|
|
file.reviewed = datetime.now()
|
|
if copy_to_mirror:
|
|
file.copy_to_mirror()
|
|
if hide_disabled_file:
|
|
file.hide_disabled_file()
|
|
file.status = status
|
|
file.save()
|
|
|
|
def log_action(self, action):
|
|
details = {'comments': self.data['comments'],
|
|
'reviewtype': self.review_type}
|
|
if self.files:
|
|
details['files'] = [f.id for f in self.files]
|
|
|
|
amo.log(action, self.addon, self.version, user=self.user.get_profile(),
|
|
created=datetime.now(), details=details)
|
|
|
|
def notify_email(self, template, subject):
|
|
"""Notify the authors that their app has been reviewed."""
|
|
emails = list(self.addon.authors.values_list('email', flat=True))
|
|
cc_email = self.addon.mozilla_contact or None
|
|
data = self.data.copy()
|
|
data.update(self.get_context_data())
|
|
data['tested'] = ''
|
|
os, app = data.get('operating_systems'), data.get('applications')
|
|
if os and app:
|
|
data['tested'] = 'Tested on %s with %s' % (os, app)
|
|
elif os and not app:
|
|
data['tested'] = 'Tested on %s' % os
|
|
elif not os and app:
|
|
data['tested'] = 'Tested with %s' % app
|
|
send_mail(subject % self.addon.name,
|
|
'reviewers/emails/decisions/%s.txt' % template, data,
|
|
emails, perm_setting='app_reviewed', cc=cc_email)
|
|
|
|
def get_context_data(self):
|
|
return {'name': self.addon.name,
|
|
'reviewer': self.request.user.get_profile().name,
|
|
'detail_url': absolutify(
|
|
self.addon.get_url_path(add_prefix=False)),
|
|
'review_url': absolutify(reverse('reviewers.apps.review',
|
|
args=[self.addon.app_slug],
|
|
add_prefix=False)),
|
|
'status_url': absolutify(self.addon.get_dev_url('versions')),
|
|
'comments': self.data['comments'],
|
|
'MKT_SUPPORT_EMAIL': settings.MKT_SUPPORT_EMAIL,
|
|
'SITE_URL': settings.SITE_URL}
|
|
|
|
def request_information(self):
|
|
"""Send a request for information to the authors."""
|
|
emails = list(self.addon.authors.values_list('email', flat=True))
|
|
cc_email = self.addon.mozilla_contact or None
|
|
self.log_action(amo.LOG.REQUEST_INFORMATION)
|
|
self.version.update(has_info_request=True)
|
|
log.info(u'Sending request for information for %s to %s' %
|
|
(self.addon, emails))
|
|
send_mail(u'Submission Update: %s' % self.addon.name,
|
|
'reviewers/emails/decisions/info.txt',
|
|
self.get_context_data(), emails,
|
|
perm_setting='app_individual_contact', cc=cc_email)
|
|
|
|
def send_escalate_mail(self):
|
|
self.log_action(amo.LOG.ESCALATE_MANUAL)
|
|
log.info(u'Escalated review requested for %s' % self.addon)
|
|
send_mail(u'Escalated Review Requested: %s' % self.addon.name,
|
|
'reviewers/emails/super_review.txt',
|
|
self.get_context_data(), [settings.MKT_SENIOR_EDITORS_EMAIL])
|
|
|
|
|
|
class ReviewApp(ReviewBase):
|
|
|
|
def set_data(self, data):
|
|
self.data = data
|
|
self.files = self.version.files.all()
|
|
|
|
def process_public(self):
|
|
if self.addon.make_public == amo.PUBLIC_IMMEDIATELY:
|
|
self.process_public_immediately()
|
|
else:
|
|
self.process_public_waiting()
|
|
|
|
if self.in_escalate:
|
|
EscalationQueue.objects.filter(addon=self.addon).delete()
|
|
|
|
# Assign reviewer incentive scores.
|
|
event = ReviewerScore.get_event_by_type(self.addon)
|
|
ReviewerScore.award_points(self.request.amo_user, self.addon, event)
|
|
|
|
def process_public_waiting(self):
|
|
"""Make an app pending."""
|
|
self.set_files(amo.STATUS_PUBLIC_WAITING, self.version.files.all())
|
|
if self.addon.status != amo.STATUS_PUBLIC:
|
|
self.set_addon(status=amo.STATUS_PUBLIC_WAITING,
|
|
highest_status=amo.STATUS_PUBLIC_WAITING)
|
|
|
|
self.log_action(amo.LOG.APPROVE_VERSION_WAITING)
|
|
self.notify_email('pending_to_public_waiting',
|
|
u'App Approved but waiting: %s')
|
|
|
|
log.info(u'Making %s public but pending' % self.addon)
|
|
log.info(u'Sending email for %s' % self.addon)
|
|
|
|
def process_public_immediately(self):
|
|
"""Approve an app."""
|
|
# 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.version.files.all())
|
|
if self.addon.status != amo.STATUS_PUBLIC:
|
|
self.set_addon(status=amo.STATUS_PUBLIC,
|
|
highest_status=amo.STATUS_PUBLIC)
|
|
|
|
self.log_action(amo.LOG.APPROVE_VERSION)
|
|
self.notify_email('pending_to_public', u'App Approved: %s')
|
|
|
|
log.info(u'Making %s public' % self.addon)
|
|
log.info(u'Sending email for %s' % self.addon)
|
|
|
|
def process_sandbox(self):
|
|
"""Reject an app."""
|
|
self.set_files(amo.STATUS_DISABLED, self.version.files.all(),
|
|
hide_disabled_file=True)
|
|
# If this app is not packaged (packaged apps can have multiple
|
|
# versions) or if there aren't other versions with already reviewed
|
|
# files, reject the app also.
|
|
if (not self.addon.is_packaged or
|
|
not self.addon.versions.exclude(id=self.version.id)
|
|
.filter(files__status__in=amo.REVIEWED_STATUSES).exists()):
|
|
self.set_addon(status=amo.STATUS_REJECTED)
|
|
|
|
if self.in_escalate:
|
|
EscalationQueue.objects.filter(addon=self.addon).delete()
|
|
if self.in_rereview:
|
|
RereviewQueue.objects.filter(addon=self.addon).delete()
|
|
|
|
self.log_action(amo.LOG.REJECT_VERSION)
|
|
self.notify_email('pending_to_sandbox', u'Submission Update: %s')
|
|
|
|
log.info(u'Making %s disabled' % self.addon)
|
|
log.info(u'Sending email for %s' % self.addon)
|
|
|
|
def process_escalate(self):
|
|
"""Ask for escalation for an app."""
|
|
EscalationQueue.objects.get_or_create(addon=self.addon)
|
|
self.notify_email('author_super_review', u'Submission Update: %s')
|
|
|
|
self.send_escalate_mail()
|
|
|
|
def process_comment(self):
|
|
self.version.update(has_editor_comment=True)
|
|
self.log_action(amo.LOG.COMMENT_VERSION)
|
|
|
|
def process_clear_escalation(self):
|
|
"""Clear app from escalation queue."""
|
|
EscalationQueue.objects.filter(addon=self.addon).delete()
|
|
self.log_action(amo.LOG.ESCALATION_CLEARED)
|
|
log.info(u'Escalation cleared for app: %s' % self.addon)
|
|
|
|
def process_clear_rereview(self):
|
|
"""Clear app from re-review queue."""
|
|
RereviewQueue.objects.filter(addon=self.addon).delete()
|
|
self.log_action(amo.LOG.REREVIEW_CLEARED)
|
|
log.info(u'Re-review cleared for app: %s' % self.addon)
|
|
|
|
def process_disable(self):
|
|
"""Disables app."""
|
|
if not acl.action_allowed(self.request, 'Addons', 'Edit'):
|
|
return
|
|
|
|
# Disable disables all files, not just those in this version.
|
|
self.set_files(amo.STATUS_DISABLED,
|
|
File.objects.filter(version__addon=self.addon),
|
|
hide_disabled_file=True)
|
|
self.addon.update(status=amo.STATUS_DISABLED)
|
|
if self.in_escalate:
|
|
EscalationQueue.objects.filter(addon=self.addon).delete()
|
|
if self.in_rereview:
|
|
RereviewQueue.objects.filter(addon=self.addon).delete()
|
|
emails = list(self.addon.authors.values_list('email', flat=True))
|
|
cc_email = self.addon.mozilla_contact or None
|
|
send_mail(u'App disabled by reviewer: %s' % self.addon.name,
|
|
'reviewers/emails/decisions/disabled.txt',
|
|
self.get_context_data(), emails,
|
|
perm_setting='app_individual_contact', cc=cc_email)
|
|
self.log_action(amo.LOG.APP_DISABLED)
|
|
log.info(u'App %s has been disabled by a reviewer.' % self.addon)
|
|
|
|
|
|
class ReviewHelper(object):
|
|
"""
|
|
A class that builds enough to render the form back to the user and
|
|
process off to the correct handler.
|
|
"""
|
|
|
|
def __init__(self, request=None, addon=None, version=None):
|
|
self.handler = None
|
|
self.required = {}
|
|
self.addon = addon
|
|
self.version = version
|
|
self.all_files = version and version.files.all()
|
|
self.get_review_type(request, addon, version)
|
|
self.actions = self.get_actions()
|
|
|
|
def set_data(self, data):
|
|
self.handler.set_data(data)
|
|
|
|
def get_review_type(self, request, addon, version):
|
|
if EscalationQueue.objects.filter(addon=addon).exists():
|
|
queue = 'escalated'
|
|
elif RereviewQueue.objects.filter(addon=addon).exists():
|
|
queue = 'rereview'
|
|
else:
|
|
queue = 'pending'
|
|
self.review_type = queue
|
|
self.handler = ReviewApp(request, addon, version, queue)
|
|
|
|
def get_actions(self):
|
|
public = {
|
|
'method': self.handler.process_public,
|
|
'minimal': False,
|
|
'label': _lazy(u'Push to public'),
|
|
'details': _lazy(u'This will approve the sandboxed app so it '
|
|
u'appears on the public side.')}
|
|
reject = {
|
|
'method': self.handler.process_sandbox,
|
|
'label': _lazy(u'Reject'),
|
|
'minimal': False,
|
|
'details': _lazy(u'This will reject the app and remove it from '
|
|
u'the review queue.')}
|
|
info = {
|
|
'method': self.handler.request_information,
|
|
'label': _lazy(u'Request more information'),
|
|
'minimal': True,
|
|
'details': _lazy(u'This will send the author(s) an email '
|
|
u'requesting more information.')}
|
|
escalate = {
|
|
'method': self.handler.process_escalate,
|
|
'label': _lazy(u'Escalate'),
|
|
'minimal': True,
|
|
'details': _lazy(u'Flag this app for an admin to review.')}
|
|
comment = {
|
|
'method': self.handler.process_comment,
|
|
'label': _lazy(u'Comment'),
|
|
'minimal': True,
|
|
'details': _lazy(u'Make a comment on this app. The author won\'t '
|
|
u'be able to see this.')}
|
|
clear_escalation = {
|
|
'method': self.handler.process_clear_escalation,
|
|
'label': _lazy(u'Clear Escalation'),
|
|
'minimal': True,
|
|
'details': _lazy(u'Clear this app from the escalation queue. The '
|
|
u'author will get no email or see comments '
|
|
u'here.')}
|
|
clear_rereview = {
|
|
'method': self.handler.process_clear_rereview,
|
|
'label': _lazy(u'Clear Re-review'),
|
|
'minimal': True,
|
|
'details': _lazy(u'Clear this app from the re-review queue. The '
|
|
u'author will get no email or see comments '
|
|
u'here.')}
|
|
disable = {
|
|
'method': self.handler.process_disable,
|
|
'label': _lazy(u'Disable app'),
|
|
'minimal': True,
|
|
'details': _lazy(u'Disable the app, removing it from public '
|
|
u'results. Sends comments to author.')}
|
|
|
|
actions = SortedDict()
|
|
|
|
file_status = self.version.files.values_list('status', flat=True)
|
|
multiple_versions = (File.objects.exclude(version=self.version)
|
|
.filter(
|
|
version__addon=self.addon,
|
|
status__in=amo.REVIEWED_STATUSES)
|
|
.exists())
|
|
|
|
# Public.
|
|
if ((self.addon.is_packaged and amo.STATUS_PUBLIC not in file_status)
|
|
or (not self.addon.is_packaged and
|
|
self.addon.status != amo.STATUS_PUBLIC)):
|
|
actions['public'] = public
|
|
|
|
# Reject.
|
|
if self.addon.is_packaged:
|
|
# Packaged apps reject the file only, or the app itself if there's
|
|
# only a single version.
|
|
if (not multiple_versions and
|
|
self.addon.status not in [amo.STATUS_REJECTED,
|
|
amo.STATUS_DISABLED]):
|
|
actions['reject'] = reject
|
|
elif multiple_versions and amo.STATUS_DISABLED not in file_status:
|
|
actions['reject'] = reject
|
|
else:
|
|
# Hosted apps reject the app itself.
|
|
if self.addon.status not in [amo.STATUS_REJECTED,
|
|
amo.STATUS_DISABLED]:
|
|
actions['reject'] = reject
|
|
|
|
# Disable.
|
|
if (acl.action_allowed(self.handler.request, 'Addons', 'Edit') and (
|
|
self.addon.status != amo.STATUS_DISABLED or
|
|
amo.STATUS_DISABLED not in file_status)):
|
|
actions['disable'] = disable
|
|
|
|
# Clear escalation.
|
|
if self.handler.in_escalate:
|
|
actions['clear_escalation'] = clear_escalation
|
|
|
|
# Clear re-review.
|
|
if self.handler.in_rereview:
|
|
actions['clear_rereview'] = clear_rereview
|
|
|
|
# Escalate.
|
|
if not self.handler.in_escalate:
|
|
actions['escalate'] = escalate
|
|
|
|
# Request info and comment are always shown.
|
|
actions['info'] = info
|
|
actions['comment'] = comment
|
|
|
|
return actions
|
|
|
|
def process(self):
|
|
action = self.handler.data.get('action', '')
|
|
if not action:
|
|
raise NotImplementedError
|
|
return self.actions[action]['method']()
|