addons-server/apps/editors/models.py

531 строка
19 KiB
Python

import copy
import datetime
import waffle
from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.db.models import Sum
from django.template import Context, loader
from django.utils.datastructures import SortedDict
import amo
import amo.models
from amo.helpers import absolutify
from amo.urlresolvers import reverse
from amo.utils import cache_ns_key, send_mail
from addons.models import Addon
from editors.sql_model import RawSQLModel
from translations.fields import TranslatedField
from users.models import UserProfile
from versions.models import version_uploaded
import commonware.log
user_log = commonware.log.getLogger('z.users')
class CannedResponse(amo.models.ModelBase):
name = TranslatedField()
response = TranslatedField(short=False)
sort_group = models.CharField(max_length=255)
type = models.PositiveIntegerField(
choices=amo.CANNED_RESPONSE_CHOICES.items(), db_index=True, default=0)
class Meta:
db_table = 'cannedresponses'
def __unicode__(self):
return unicode(self.name)
class AddonCannedResponseManager(amo.models.ManagerBase):
def get_query_set(self):
qs = super(AddonCannedResponseManager, self).get_query_set()
return qs.filter(type=amo.CANNED_RESPONSE_ADDON)
class AddonCannedResponse(CannedResponse):
objects = AddonCannedResponseManager()
class Meta:
proxy = True
class EventLog(models.Model):
type = models.CharField(max_length=60)
action = models.CharField(max_length=120)
field = models.CharField(max_length=60, blank=True)
user = models.ForeignKey(UserProfile)
changed_id = models.IntegerField()
added = models.CharField(max_length=765, blank=True)
removed = models.CharField(max_length=765, blank=True)
notes = models.TextField(blank=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = u'eventlog'
@staticmethod
def new_editors():
items = (EventLog.objects.values('added', 'created')
.filter(type='admin',
action='group_addmember',
changed_id=2)
.order_by('-created')[:5])
users = UserProfile.objects.filter(id__in=[i['added'] for i in items])
names = dict((u.id, u.display_name) for u in users)
return [dict(display_name=names[int(i['added'])], **i) for i in items]
class ViewQueue(RawSQLModel):
id = models.IntegerField()
addon_name = models.CharField(max_length=255)
addon_slug = models.CharField(max_length=30)
addon_status = models.IntegerField()
addon_type_id = models.IntegerField()
admin_review = models.BooleanField()
is_site_specific = models.BooleanField()
external_software = models.BooleanField()
binary = models.BooleanField()
binary_components = models.BooleanField()
premium_type = models.IntegerField()
_no_restart = models.CharField(max_length=255)
_jetpack_versions = models.CharField(max_length=255)
_latest_versions = models.CharField(max_length=255)
_latest_version_ids = models.CharField(max_length=255)
_file_platform_ids = models.CharField(max_length=255)
_file_platform_vers = models.CharField(max_length=255)
_info_request_vers = models.CharField(max_length=255)
_editor_comment_vers = models.CharField(max_length=255)
_application_ids = models.CharField(max_length=255)
waiting_time_days = models.IntegerField()
waiting_time_hours = models.IntegerField()
waiting_time_min = models.IntegerField()
is_version_specific = False
_latest_version_id = None
def base_query(self):
return {
'select': SortedDict([
('id', 'addons.id'),
('addon_name', 'tr.localized_string'),
('addon_status', 'addons.status'),
('addon_type_id', 'addons.addontype_id'),
('addon_slug', 'addons.slug'),
('admin_review', 'addons.adminreview'),
('is_site_specific', 'addons.sitespecific'),
('external_software', 'addons.externalsoftware'),
('binary', 'files.binary'),
('binary_components', 'files.binary_components'),
('premium_type', 'addons.premium_type'),
('_latest_version_ids', """GROUP_CONCAT(versions.id
ORDER BY versions.created DESC)"""),
('_latest_versions', """GROUP_CONCAT(versions.version
ORDER BY versions.created
DESC SEPARATOR '&&&&')"""),
('_editor_comment_vers', """GROUP_CONCAT(DISTINCT CONCAT(CONCAT(
versions.has_editor_comment, '-'),
versions.id))"""),
('_info_request_vers', """GROUP_CONCAT(DISTINCT CONCAT(CONCAT(
versions.has_info_request, '-'),
versions.id))"""),
('_file_platform_vers', """GROUP_CONCAT(DISTINCT CONCAT(CONCAT(
files.platform_id, '-'),
files.version_id))"""),
('_file_platform_ids', """GROUP_CONCAT(DISTINCT
files.platform_id)"""),
('_jetpack_versions', """GROUP_CONCAT(DISTINCT
files.jetpack_version)"""),
('_no_restart', """GROUP_CONCAT(DISTINCT files.no_restart)"""),
('_application_ids', """GROUP_CONCAT(DISTINCT
apps.application_id)"""),
]),
'from': [
'files',
'JOIN versions ON (files.version_id = versions.id)',
'JOIN addons ON (versions.addon_id = addons.id)',
"""JOIN files AS version_files ON (
version_files.version_id = versions.id)""",
"""LEFT JOIN applications_versions as apps
ON versions.id = apps.version_id""",
# Translations
"""JOIN translations AS tr ON (
tr.id = addons.name
AND tr.locale = addons.defaultlocale)"""
],
'where': [
'NOT addons.inactive', # disabled_by_user
'addons.addontype_id <> 11', # No webapps for AMO.
],
'group_by': 'id'}
@property
def latest_version(self):
return self._explode_concat(self._latest_versions, sep='&&&&',
cast=unicode)[0]
@property
def latest_version_id(self):
if not self._latest_version_id:
ids = self._explode_concat(self._latest_version_ids)
self._latest_version_id = ids[0]
return self._latest_version_id
@property
def is_restartless(self):
return any(self._explode_concat(self._no_restart))
@property
def is_jetpack(self):
return bool(self._jetpack_versions)
@property
def is_premium(self):
return self.premium_type in amo.ADDON_PREMIUMS
@property
def file_platform_vers(self):
return self._explode_concat(self._file_platform_vers, cast=str)
@property
def has_info_request(self):
return self.for_latest_version(self._info_request_vers)
@property
def has_editor_comment(self):
return self.for_latest_version(self._editor_comment_vers)
@property
def file_platform_ids(self):
return self._explode_concat(self._file_platform_ids)
@property
def application_ids(self):
return self._explode_concat(self._application_ids)
def for_latest_version(self, vals, cast=int, default=0):
split = self._explode_concat(vals, cast=str)
for s in split:
val, version_id = s.split('-')
if int(version_id) == self.latest_version_id:
return cast(val)
return default
class ViewFullReviewQueue(ViewQueue):
def base_query(self):
q = super(ViewFullReviewQueue, self).base_query()
q['select'].update({
'waiting_time_days':
'TIMESTAMPDIFF(DAY, MAX(versions.nomination), NOW())',
'waiting_time_hours':
'TIMESTAMPDIFF(HOUR, MAX(versions.nomination), NOW())',
'waiting_time_min':
'TIMESTAMPDIFF(MINUTE, MAX(versions.nomination), NOW())',
})
q['where'].extend(['files.status <> %s' % amo.STATUS_BETA,
'addons.status IN (%s, %s)' % (
amo.STATUS_NOMINATED,
amo.STATUS_LITE_AND_NOMINATED)])
return q
class VersionSpecificQueue(ViewQueue):
is_version_specific = True
def base_query(self):
q = copy.deepcopy(super(VersionSpecificQueue, self).base_query())
q['select'].update({
'waiting_time_days':
'TIMESTAMPDIFF(DAY, MAX(files.created), NOW())',
'waiting_time_hours':
'TIMESTAMPDIFF(HOUR, MAX(files.created), NOW())',
'waiting_time_min':
'TIMESTAMPDIFF(MINUTE, MAX(files.created), NOW())',
})
return q
class ViewPendingQueue(VersionSpecificQueue):
def base_query(self):
q = super(ViewPendingQueue, self).base_query()
q['where'].extend(['files.status = %s' % amo.STATUS_UNREVIEWED,
'addons.status = %s' % amo.STATUS_PUBLIC])
return q
class ViewPreliminaryQueue(VersionSpecificQueue):
def base_query(self):
q = super(ViewPreliminaryQueue, self).base_query()
q['where'].extend(['files.status = %s' % amo.STATUS_UNREVIEWED,
'addons.status IN (%s, %s)' % (
amo.STATUS_LITE,
amo.STATUS_UNREVIEWED)])
return q
class ViewFastTrackQueue(VersionSpecificQueue):
def base_query(self):
q = super(ViewFastTrackQueue, self).base_query()
# Fast track includes jetpack-based addons that do not require chrome.
q['where'].extend(['files.no_restart = 1',
'files.jetpack_version IS NOT NULL',
'files.requires_chrome = 0',
'files.status = %s' % amo.STATUS_UNREVIEWED,
'addons.status IN (%s, %s, %s, %s)' % (
amo.STATUS_LITE,
amo.STATUS_UNREVIEWED,
amo.STATUS_NOMINATED,
amo.STATUS_LITE_AND_NOMINATED)])
return q
class PerformanceGraph(ViewQueue):
id = models.IntegerField()
yearmonth = models.CharField(max_length=7)
approval_created = models.DateTimeField()
user_id = models.IntegerField()
total = models.IntegerField()
def base_query(self):
request_ver = amo.LOG.REQUEST_VERSION.id
review_ids = [str(r) for r in amo.LOG_REVIEW_QUEUE if r != request_ver]
return {
'select': SortedDict([
('yearmonth',
"DATE_FORMAT(`log_activity`.`created`, '%%Y-%%m')"),
('approval_created', '`log_activity`.`created`'),
('user_id', '`users`.`id`'),
('total', 'COUNT(*)')]),
'from': [
'log_activity',
'LEFT JOIN `users` ON (`users`.`id`=`log_activity`.`user_id`)'],
'where': ['log_activity.action in (%s)' % ','.join(review_ids)],
'group_by': 'yearmonth, user_id'
}
class EditorSubscription(amo.models.ModelBase):
user = models.ForeignKey(UserProfile)
addon = models.ForeignKey(Addon)
class Meta:
db_table = 'editor_subscriptions'
def send_notification(self, version):
user_log.info('Sending addon update notice to %s for %s' %
(self.user.email, self.addon.pk))
context = Context({
'name': self.addon.name,
'url': absolutify(reverse('addons.detail', args=[self.addon.pk])),
'number': version.version,
'review': absolutify(reverse('editors.review',
args=[self.addon.pk])),
'SITE_URL': settings.SITE_URL,
})
# Not being localised because we don't know the editors locale.
subject = 'Mozilla Add-ons: %s Updated' % self.addon.name
template = loader.get_template('editors/emails/notify_update.ltxt')
send_mail(subject, template.render(Context(context)),
recipient_list=[self.user.email],
from_email=settings.EDITORS_EMAIL,
use_blacklist=False)
def send_notifications(signal=None, sender=None, **kw):
if sender.is_beta:
return
subscribers = sender.addon.editorsubscription_set.all()
if not subscribers:
return
for subscriber in subscribers:
subscriber.send_notification(sender)
subscriber.delete()
version_uploaded.connect(send_notifications, dispatch_uid='send_notifications')
class ReviewerScore(amo.models.ModelBase):
user = models.ForeignKey(UserProfile, related_name='_reviewer_scores')
addon = models.ForeignKey(Addon, blank=True, null=True, related_name='+')
score = models.SmallIntegerField()
# For automated point rewards.
note_key = models.SmallIntegerField(choices=amo.REVIEWED_CHOICES.items(),
default=0)
# For manual point rewards with a note.
note = models.CharField(max_length=255)
class Meta:
db_table = 'reviewer_scores'
ordering = ('-created',)
@classmethod
def get_key(cls, key=None, invalidate=False):
namespace = 'riscore'
if not key: # Assuming we're invalidating the namespace.
cache_ns_key(namespace, invalidate)
return
else:
# Using cache_ns_key so each cache val is invalidated together.
ns_key = cache_ns_key(namespace, invalidate)
return '%s:%s' % (ns_key, key)
@classmethod
def get_event_by_type(cls, addon, review_type=None):
if addon.type == amo.ADDON_EXTENSION:
# Special case for addons depending on review_type.
if review_type == 'nominated':
return amo.REVIEWED_ADDON_FULL
elif review_type == 'preliminary':
return amo.REVIEWED_ADDON_PRELIM
else:
return amo.REVIEWED_ADDON_UPDATED
elif addon.type == amo.ADDON_DICT:
return amo.REVIEWED_DICT
elif addon.type in [amo.ADDON_LPAPP, amo.ADDON_LPADDON]:
return amo.REVIEWED_LP
elif addon.type == amo.ADDON_PERSONA:
return amo.REVIEWED_PERSONA
elif addon.type == amo.ADDON_SEARCH:
return amo.REVIEWED_SEARCH
elif addon.type == amo.ADDON_THEME:
return amo.REVIEWED_THEME
elif addon.type == amo.ADDON_WEBAPP:
return amo.REVIEWED_WEBAPP
else:
return None
@classmethod
def award_points(cls, user, addon, event):
"""Awards points to user based on an event.
`event` is one of the `REVIEWED_` keys in constants.
"""
if not waffle.switch_is_active('reviewer-incentive-points'):
return
score = amo.REVIEWED_SCORES.get(event)
if score:
cls.objects.create(user=user, addon=addon, score=score,
note_key=event)
cls.get_key(invalidate=True)
user_log.info(u'Awarding %s points to user %s for "%s" for addon'
'%s' % (score, user, amo.REVIEWED_CHOICES[event],
addon.id))
@classmethod
def get_total(cls, user):
"""Returns total points by user."""
key = cls.get_key('get_total:%s' % user.id)
val = cache.get(key)
if val is not None:
return val
val = (ReviewerScore.uncached.filter(user=user)
.aggregate(total=Sum('score'))
.values())[0]
if val is None:
val = 0
cache.set(key, val, 0)
return val
@classmethod
def get_recent(cls, user, limit=5):
"""Returns most recent ReviewerScore records."""
key = cls.get_key('get_recent:%s' % user.id)
val = cache.get(key)
if val is not None:
return val
val = list(ReviewerScore.uncached.filter(user=user)[:limit])
cache.set(key, val, 0)
return val
@classmethod
def get_leaderboards(cls, user, days=7):
"""Returns leaderboards with ranking for the past given days.
This will return a dict of 3 items::
{'leader_top': [...],
'leader_near: [...],
'user_rank': (int)}
If the user is not in the leaderboard, or if the user is in the top 5,
'leader_near' will be an empty list and 'leader_top' will contain 5
elements instead of the normal 3.
"""
key = cls.get_key('get_leaderboards:%s' % user.id)
val = cache.get(key)
if val is not None:
return val
week_ago = datetime.date.today() - datetime.timedelta(days=days)
# I'd normally use Django's ORM aggregation but bug 17144 scared me
# away.
leader_top = []
leader_near = []
in_leaderboard = (ReviewerScore.uncached.filter(created__gte=week_ago,
user=user)
.exists())
sql = """SELECT *, SUM(`reviewer_scores`.`score`) AS `total`
FROM `reviewer_scores`
WHERE `created` >= %s
GROUP BY `user_id`
ORDER BY `total` DESC"""
rank = 0
if not in_leaderboard:
sql += ' LIMIT 5' # Top 5 if not in leaderboard.
leader_top = list(ReviewerScore.uncached.raw(sql, [week_ago]))
else:
scores = list(ReviewerScore.uncached.raw(sql, [week_ago]))
for i, score in enumerate(scores, 1):
score.rank = i
if user.id == score.user_id:
rank = i
if rank <= 5: # User is in top 5, show top 5.
leader_top = scores[:5]
else:
leader_top = scores[:3]
leader_near = [scores[rank - 2], scores[rank - 1]]
try:
leader_near.append(scores[rank])
except IndexError:
pass # User is last on the leaderboard.
val = {
'leader_top': leader_top,
'leader_near': leader_near,
'user_rank': rank,
}
cache.set(key, val, 0)
return val
class EscalationQueue(amo.models.ModelBase):
addon = models.ForeignKey(Addon)
class Meta:
db_table = 'escalation_queue'