addons-server/apps/editors/helpers.py

566 строки
23 KiB
Python

from datetime import datetime
from django.conf import settings
from django.template import Context, loader
from django.utils.datastructures import SortedDict
import django_tables as tables
import jinja2
from jingo import register
from tower import ugettext as _, ugettext_lazy as _lazy, ungettext as ngettext
import amo
from amo.helpers import breadcrumbs, page_title, absolutify
from amo.urlresolvers import reverse
from amo.utils import send_mail as amo_send_mail
import commonware.log
from editors.models import (ViewPendingQueue, ViewFullReviewQueue,
ViewPreliminaryQueue)
from editors.sql_table import SQLTable
@register.function
def file_compare(file_obj, version):
# Compare this file to the one in the version with same platform
file_obj = version.files.filter(platform=file_obj.platform)
# If not there, just compare to all.
if not file_obj:
file_obj = version.files.filter(platform=amo.PLATFORM_ALL.id)
# At this point we've got no idea what Platform file to
# compare with, so just chose the first.
if not file_obj:
file_obj = version.files.all()
return file_obj[0]
@register.function
def file_review_status(addon, file):
if file.status not in [amo.STATUS_DISABLED, amo.STATUS_PUBLIC]:
if addon.status in [amo.STATUS_UNREVIEWED, amo.STATUS_LITE]:
return _(u'Pending Preliminary Review')
elif addon.status in [amo.STATUS_NOMINATED,
amo.STATUS_LITE_AND_NOMINATED,
amo.STATUS_PUBLIC]:
return _(u'Pending Full Review')
return amo.STATUS_CHOICES[file.status]
@register.function
@jinja2.contextfunction
def editor_page_title(context, title=None, addon=None):
"""Wrapper for editor page titles. Eerily similar to dev_page_title."""
if addon:
title = u'%s :: %s' % (title, addon.name)
else:
section = _lazy('Editor Tools')
title = u'%s :: %s' % (title, section) if title else section
return page_title(context, title)
@register.function
@jinja2.contextfunction
def editors_breadcrumbs(context, queue=None, queue_id=None, items=None):
"""
Wrapper function for ``breadcrumbs``. Prepends 'Editor Tools'
breadcrumbs.
**items**
list of [(url, label)] to be inserted after Add-on.
**addon**
Adds the Add-on name to the end of the trail. If items are
specified then the Add-on will be linked.
**add_default**
Prepends trail back to home when True. Default is False.
"""
crumbs = [(reverse('editors.home'), _('Editor Tools'))]
if queue_id:
queue_ids = {1: 'prelim', 3: 'nominated', 4: 'pending',
8: 'prelim', 9: 'nominated', 2: 'pending'}
queue = queue_ids.get(queue_id, 'queue')
if queue:
queues = {'queue': _("Queue"),
'pending': _("Pending Updates"),
'nominated': _("Full Reviews"),
'prelim': _("Preliminary Reviews"),
'moderated': _("Moderated Reviews")}
if items and not queue == 'queue':
url = reverse('editors.queue_%s' % queue)
else:
# The Addon is the end of the trail.
url = None
crumbs.append((url, queues[queue]))
if items:
crumbs.extend(items)
return breadcrumbs(context, crumbs, add_default=False)
class EditorQueueTable(SQLTable):
addon_name = tables.Column(verbose_name=_lazy(u'Addon'))
addon_type_id = tables.Column(verbose_name=_lazy(u'Type'))
waiting_time_min = tables.Column(verbose_name=_lazy(u'Waiting Time'))
flags = tables.Column(verbose_name=_lazy(u'Flags'), sortable=False)
applications = tables.Column(verbose_name=_lazy(u'Applications'),
sortable=False)
additional_info = tables.Column(
verbose_name=_lazy(u'Additional Information'), sortable=False)
def render_addon_name(self, row):
url = '%s?num=%s' % (reverse('editors.review',
args=[row.addon_slug]),
self.item_number)
self.item_number += 1
return u'<a href="%s">%s %s</a>' % (
url, jinja2.escape(row.addon_name),
jinja2.escape(row.latest_version))
def render_addon_type_id(self, row):
return amo.ADDON_TYPE[row.addon_type_id]
def render_additional_info(self, row):
info = []
if row.is_site_specific:
info.append(_lazy(u'Site Specific'))
if (len(row.file_platform_ids) == 1
and row.file_platform_ids != [amo.PLATFORM_ALL.id]):
k = row.file_platform_ids[0]
# L10n: first argument is the platform such as Linux, Mac OS X
info.append(_lazy(u'{0} only').format(amo.PLATFORMS[k].name))
if row.external_software:
info.append(_lazy(u'Requires External Software'))
if row.binary:
info.append(_lazy(u'Binary Components'))
return u', '.join([jinja2.escape(i) for i in info])
def render_applications(self, row):
# TODO(Kumar) show supported version ranges on hover (if still needed)
icon = u'<div class="app-icon ed-sprite-%s" title="%s"></div>'
return u''.join([icon % (amo.APPS_ALL[i].short, amo.APPS_ALL[i].pretty)
for i in row.application_ids])
def render_flags(self, row):
o = []
if row.admin_review:
o.append(u'<div class="app-icon ed-sprite-admin-review" '
u'title="%s"></div>' % _('Admin Review'))
if row.is_jetpack:
o.append(u'<div class="app-icon ed-sprite-jetpack" title="%s">'
u'</div>' % _('Jetpack Add-on'))
elif row.is_restartless:
# Only show restartless if it's not also a jetpack
o.append(u'<div class="app-icon ed-sprite-restartless" title="%s">'
u'</div>' % _('Bootstrapped Restartless Add-on'))
return ''.join(o)
def render_waiting_time_min(self, row):
if row.waiting_time_min == 0:
r = _lazy('moments ago')
elif row.waiting_time_hours == 0:
# L10n: first argument is number of minutes
r = ngettext(u'{0} minute', u'{0} minutes',
row.waiting_time_min).format(row.waiting_time_min)
elif row.waiting_time_days == 0:
# L10n: first argument is number of hours
r = ngettext(u'{0} hour', u'{0} hours',
row.waiting_time_hours).format(row.waiting_time_hours)
else:
# L10n: first argument is number of days
r = ngettext(u'{0} day', u'{0} days',
row.waiting_time_days).format(row.waiting_time_days)
return jinja2.escape(r)
def set_page(self, page):
self.item_number = page.start_index()
class Meta:
sortable = True
columns = ['addon_name', 'addon_type_id', 'waiting_time_min',
'flags', 'applications', 'additional_info']
class ViewPendingQueueTable(EditorQueueTable):
class Meta(EditorQueueTable.Meta):
model = ViewPendingQueue
class ViewFullReviewQueueTable(EditorQueueTable):
class Meta(EditorQueueTable.Meta):
model = ViewFullReviewQueue
class ViewPreliminaryQueueTable(EditorQueueTable):
class Meta(EditorQueueTable.Meta):
model = ViewPreliminaryQueue
log = commonware.log.getLogger('z.mailer')
NOMINATED_STATUSES = (amo.STATUS_NOMINATED, amo.STATUS_LITE_AND_NOMINATED)
PRELIMINARY_STATUSES = (amo.STATUS_UNREVIEWED, amo.STATUS_LITE)
PENDING_STATUSES = (amo.STATUS_BETA, amo.STATUS_DISABLED, amo.STATUS_LISTED,
amo.STATUS_NULL, amo.STATUS_PENDING, amo.STATUS_PUBLIC)
def send_mail(template, subject, emails, context):
template = loader.get_template(template)
amo_send_mail(subject, template.render(Context(context, autoescape=False)),
recipient_list=emails, from_email=settings.EDITORS_EMAIL,
use_blacklist=False)
class ReviewHelper:
"""
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.all_files = 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 self.addon.status in NOMINATED_STATUSES:
self.review_type = 'nominated'
self.handler = ReviewAddon(request, addon, version, 'nominated')
elif self.addon.status == amo.STATUS_UNREVIEWED:
self.review_type = 'preliminary'
self.handler = ReviewAddon(request, addon, version, 'preliminary')
elif self.addon.status == amo.STATUS_LITE:
self.review_type = 'preliminary'
self.handler = ReviewFiles(request, addon, version, 'preliminary')
else:
self.review_type = 'pending'
self.handler = ReviewFiles(request, addon, version, 'pending')
def get_actions(self):
labels, details = self._review_actions()
actions = SortedDict()
if (self.review_type != 'preliminary'):
actions['public'] = {'method': self.handler.process_public,
'minimal': False,
'label': _lazy('Push to public')}
actions['prelim'] = {'method': self.handler.process_preliminary,
'label': labels['prelim'],
'minimal': False}
actions['reject'] = {'method': self.handler.process_sandbox,
'label': _lazy('Reject'),
'minimal': False}
actions['info'] = {'method': self.handler.request_information,
'label': _lazy('Request more information'),
'minimal': True}
actions['super'] = {'method': self.handler.process_super_review,
'label': _lazy('Request super-review'),
'minimal': True}
actions['comment'] = {'method': self.handler.process_comment,
'label': _lazy('Comment'),
'minimal': True}
for k, v in actions.items():
v['details'] = details.get(k)
return actions
def _review_actions(self):
labels = {'prelim': _lazy('Grant preliminary review')}
details = {'prelim': _lazy('This will mark the files as '
'premliminary reviewed.'),
'info': _lazy('Use this form to request more information '
'from the author. They will receive an email '
'and be able to answer here. You will be '
'notified by email when they reply.'),
'super': _lazy('If you have concerns about this add-on\'s '
'security, copyright issues, or other '
'concerns that an administrator should look '
'into, enter your comments in the area '
'below. They will be sent to '
'administrators, not the author.'),
'reject': _lazy('This will reject the add-on and remove '
'it from the review queue.'),
'comment': _lazy('Make a comment on this version. The '
'author won\'t be able to see this.')}
if self.addon.status == amo.STATUS_LITE:
details['reject'] = _lazy('This will reject the files and remove '
'them from the review queue.')
if self.addon.status in (amo.STATUS_UNREVIEWED, amo.STATUS_NOMINATED):
details['prelim'] = _lazy('This will mark the add-on as '
'preliminarily reviewed. Future '
'versions will undergo '
'preliminary review.')
elif self.addon.status == amo.STATUS_LITE:
details['prelim'] = _lazy('This will mark the files as '
'preliminarily reviewed. Future '
'versions will undergo '
'preliminary review.')
elif self.addon.status == amo.STATUS_LITE_AND_NOMINATED:
labels['prelim'] = _lazy('Retain preliminary review')
details['prelim'] = _lazy('This will retain the add-on as '
'preliminarily reviewed. Future '
'versions will undergo preliminary '
'review.')
if self.review_type == 'pending':
details['public'] = _lazy('This will approve a sandboxed version '
'of a public add-on to appear on the '
'public side.')
details['reject'] = _lazy('This will reject a version of a public '
'add-on and remove it from the queue.')
else:
details['public'] = _lazy('This will mark the add-on and its most '
'recent version and files as public. '
'Future versions will go into the '
'sandbox until they are reviewed by an '
'editor.')
return labels, details
def process(self):
action = self.handler.data.get('action', '')
if not action:
raise NotImplementedError
return self.actions[action]['method']()
class ReviewBase:
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
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 addon has been reviewed."""
emails = [a.email for a in self.addon.authors.all()]
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('editors/emails/%s.ltxt' % template,
subject % (self.addon.name, self.version.version),
emails, Context(data))
def get_context_data(self):
return {'name': self.addon.name,
'number': self.version.version,
'reviewer': (self.request.user.get_profile().display_name),
'addon_url': absolutify(reverse('addons.detail',
args=[self.addon.slug])),
'comments': self.data['comments'],
'SITE_URL': settings.SITE_URL}
def request_information(self):
"""Send a request for information to the authors."""
emails = [a.email for a in self.addon.authors.all()]
self.log_action(amo.LOG.REQUEST_INFORMATION)
log.info(u'Sending request for information for %s to %s' %
(self.addon, emails))
send_mail('editors/emails/info.ltxt',
u'Mozilla Add-ons: %s %s' %
(self.addon.name, self.version.version),
emails, Context(self.get_context_data()))
def send_super_mail(self):
self.log_action(amo.LOG.REQUEST_SUPER_REVIEW)
log.info(u'Super review requested for %s' % (self.addon))
send_mail('editors/emails/super_review.ltxt',
u'Super review requested: %s' % (self.addon.name),
[settings.SENIOR_EDITORS_EMAIL],
Context(self.get_context_data()))
class ReviewAddon(ReviewBase):
def set_data(self, data):
self.data = data
self.files = self.version.files.all()
def process_public(self):
"""Set an addon to public."""
if self.review_type == 'preliminary':
raise AssertionError('Preliminary addons cannot be made public.')
# 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(),
copy_to_mirror=True)
self.set_addon(highest_status=amo.STATUS_PUBLIC,
status=amo.STATUS_PUBLIC)
self.log_action(amo.LOG.APPROVE_VERSION)
self.notify_email('%s_to_public' % self.review_type,
u'Mozilla Add-ons: %s %s Fully Reviewed')
log.info(u'Making %s public' % (self.addon))
log.info(u'Sending email for %s' % (self.addon))
def process_sandbox(self):
"""Set an addon back to sandbox."""
self.set_addon(status=amo.STATUS_NULL)
self.set_files(amo.STATUS_DISABLED, self.version.files.all(),
hide_disabled_file=True)
self.log_action(amo.LOG.REJECT_VERSION)
self.notify_email('%s_to_sandbox' % self.review_type,
u'Mozilla Add-ons: %s %s Reviewed')
log.info(u'Making %s disabled' % (self.addon))
log.info(u'Sending email for %s' % (self.addon))
def process_preliminary(self):
"""Set an addon to preliminary."""
changes = {'status': amo.STATUS_LITE}
if (self.addon.status in (amo.STATUS_PUBLIC,
amo.STATUS_LITE_AND_NOMINATED)):
changes['highest_status'] = amo.STATUS_LITE
template = '%s_to_preliminary' % self.review_type
if (self.review_type == 'preliminary' and
self.addon.status == amo.STATUS_LITE_AND_NOMINATED):
template = 'nominated_to_nominated'
self.set_addon(**changes)
self.set_files(amo.STATUS_LITE, self.version.files.all(),
copy_to_mirror=True)
self.log_action(amo.LOG.PRELIMINARY_VERSION)
self.notify_email(template,
u'Mozilla Add-ons: %s %s Preliminary Reviewed')
log.info(u'Making %s preliminary' % (self.addon))
log.info(u'Sending email for %s' % (self.addon))
def process_super_review(self):
"""Give an addon super review."""
self.addon.update(admin_review=True)
self.notify_email('author_super_review',
u'Mozilla Add-ons: %s %s flagged for Admin Review')
self.send_super_mail()
def process_comment(self):
self.log_action(amo.LOG.COMMENT_VERSION)
class ReviewFiles(ReviewBase):
def set_data(self, data):
self.data = data
self.files = data.get('addon_files', None)
def process_public(self):
"""Set an addons files to public."""
if self.review_type == 'preliminary':
raise AssertionError('Preliminary addons cannot be made public.')
self.set_files(amo.STATUS_PUBLIC, self.data['addon_files'],
copy_to_mirror=True)
self.log_action(amo.LOG.APPROVE_VERSION)
self.notify_email('%s_to_public' % self.review_type,
u'Mozilla Add-ons: %s %s Fully Reviewed')
log.info(u'Making %s files %s public' %
(self.addon,
', '.join([f.filename for f in self.data['addon_files']])))
log.info(u'Sending email for %s' % (self.addon))
def process_sandbox(self):
"""Set an addons files to sandbox."""
self.set_files(amo.STATUS_DISABLED, self.data['addon_files'],
hide_disabled_file=True)
self.log_action(amo.LOG.REJECT_VERSION)
self.notify_email('%s_to_sandbox' % self.review_type,
u'Mozilla Add-ons: %s %s Reviewed')
log.info(u'Making %s files %s disabled' %
(self.addon,
', '.join([f.filename for f in self.data['addon_files']])))
log.info(u'Sending email for %s' % (self.addon))
def process_preliminary(self):
"""Set an addons files to preliminary."""
self.set_files(amo.STATUS_LITE, self.data['addon_files'],
copy_to_mirror=True)
self.log_action(amo.LOG.PRELIMINARY_VERSION)
self.notify_email('%s_to_preliminary' % self.review_type,
u'Mozilla Add-ons: %s %s Preliminary Reviewed')
log.info(u'Making %s files %s preliminary' %
(self.addon,
', '.join([f.filename for f in self.data['addon_files']])))
log.info(u'Sending email for %s' % (self.addon))
def process_super_review(self):
"""Give an addon super review when preliminary."""
self.addon.update(admin_review=True)
if any(f.status for f in self.data['addon_files'] if f.status
in (amo.STATUS_PENDING, amo.STATUS_UNREVIEWED)):
self.log_action(amo.LOG.ESCALATE_VERSION)
self.notify_email('author_super_review',
u'Mozilla Add-ons: %s %s flagged for Admin Review')
self.send_super_mail()
def process_comment(self):
self.log_action(amo.LOG.COMMENT_VERSION)