addons-server/apps/zadmin/models.py

316 строки
11 KiB
Python

from decimal import Decimal
import json
from django.conf import settings
from django.db import models
from django.utils.functional import memoize
import redisutils
import amo
import amo.models
from amo.urlresolvers import reverse
from applications.models import Application, AppVersion
from files.models import File
_config_cache = {}
class Config(models.Model):
"""Sitewide settings."""
key = models.CharField(max_length=255, primary_key=True)
value = models.TextField()
class Meta:
db_table = u'config'
@property
def json(self):
try:
return json.loads(self.value)
except TypeError, ValueError:
return {}
def get_config(conf):
try:
c = Config.objects.get(key=conf)
return c.value
except Config.DoesNotExist:
return
get_config = memoize(get_config, _config_cache, 1)
def set_config(conf, value):
cf, created = Config.objects.get_or_create(key=conf)
cf.value = value
cf.save()
_config_cache.clear()
class ValidationJob(amo.models.ModelBase):
application = models.ForeignKey(Application)
curr_max_version = models.ForeignKey(AppVersion,
related_name='validation_current_set')
target_version = models.ForeignKey(AppVersion,
related_name='validation_target_set')
finish_email = models.EmailField(null=True)
completed = models.DateTimeField(null=True, db_index=True)
creator = models.ForeignKey('users.UserProfile', null=True)
def result_passing(self):
return self.result_set.exclude(completed=None).filter(errors=0,
task_error=None)
def result_completed(self):
return self.result_set.exclude(completed=None)
def result_errors(self):
return self.result_set.exclude(task_error=None)
def result_failing(self):
return self.result_set.exclude(completed=None).filter(errors__gt=0)
@property
def preview_success_mail_link(self):
return self._preview_link(EmailPreviewTopic(self, 'success'))
@property
def preview_failure_mail_link(self):
return self._preview_link(EmailPreviewTopic(self, 'failures'))
def _preview_link(self, topic):
qs = topic.filter()
if qs.count():
return reverse('zadmin.email_preview_csv', args=[topic.topic])
def preview_success_mail(self, *args, **kwargs):
EmailPreviewTopic(self, 'success').send_mail(*args, **kwargs)
def preview_failure_mail(self, *args, **kwargs):
EmailPreviewTopic(self, 'failures').send_mail(*args, **kwargs)
def get_success_preview_emails(self):
return EmailPreviewTopic(self, 'success').filter()
def get_failure_preview_emails(self):
return EmailPreviewTopic(self, 'failures').filter()
def is_complete(self, as_int=False):
completed = self.completed is not None
if as_int:
return 1 if completed else 0
else:
return completed
@property
def stats(self):
if not hasattr(self, '_stats'):
self._stats = self._count_stats()
return self._stats
def _count_stats(self):
total = self.result_set.count()
completed = self.result_completed().count()
passing = self.result_passing().count()
errors = self.result_errors().count()
failing = self.result_failing().count()
return {
'job_id': self.pk,
'total': total,
'completed': completed,
'completed_timestamp': str(self.completed or ''),
'passing': passing,
'failing': failing,
'errors': errors,
'percent_complete': (Decimal(completed) / Decimal(total)
* Decimal(100)
if (total and completed) else 0),
}
class Meta:
db_table = 'validation_job'
class ValidationResult(amo.models.ModelBase):
"""Result of a single validation task based on the addon file.
This is different than FileValidation because it allows multiple
validation results per file.
"""
validation_job = models.ForeignKey(ValidationJob,
related_name='result_set')
file = models.ForeignKey(File, related_name='validation_results')
valid = models.BooleanField(default=False)
errors = models.IntegerField(default=0, null=True)
warnings = models.IntegerField(default=0, null=True)
notices = models.IntegerField(default=0, null=True)
validation = models.TextField(null=True)
task_error = models.TextField(null=True)
completed = models.DateTimeField(null=True, db_index=True)
class Meta:
db_table = 'validation_result'
def apply_validation(self, validation):
self.validation = validation
results = json.loads(validation)
compat = results['compatibility_summary']
# TODO(Kumar) these numbers should not be combined. See bug 657936.
self.errors = results['errors'] + compat['errors']
self.warnings = results['warnings'] + compat['warnings']
self.notices = results['notices'] + compat['notices']
self.valid = self.errors == 0
class EmailPreviewTopic(object):
"""Store emails in a given topic so an admin can preview before
re-sending.
A topic is a unique string identifier that groups together preview emails.
If you pass in an object (a Model instance) you will get a poor man's
foreign key as your topic.
For example, EmailPreviewTopic(addon) will link all preview emails to
the ID of that addon object.
"""
def __init__(self, object=None, suffix='', topic=None):
if not topic:
assert object, 'object keyword is required when topic is empty'
topic = '%s-%s-%s' % (object.__class__._meta.db_table, object.pk,
suffix)
self.topic = topic
def filter(self, *args, **kw):
kw['topic'] = self.topic
return EmailPreview.objects.filter(**kw)
def send_mail(self, subject, body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=tuple([])):
return EmailPreview.objects.create(
topic=self.topic,
subject=subject, body=body,
recipient_list=u','.join(recipient_list),
from_email=from_email)
class EmailPreview(amo.models.ModelBase):
"""A log of emails for previewing purposes.
This is only for development and the data might get deleted at any time.
"""
topic = models.CharField(max_length=255, db_index=True)
recipient_list = models.TextField() # comma separated list of emails
from_email = models.EmailField()
subject = models.CharField(max_length=255)
body = models.TextField()
class Meta:
db_table = 'email_preview'
class ValidationJobTally(object):
"""Redis key/vals for a tally of validation job results.
The key/val pairs look like this::
# message keys that were found by this validation job:
validation.job_id:1234:msg_keys = set([
'path.to.javascript_data_urls',
'path.to.navigator_language'
])
# translation of message keys to actual messages:
validation.msg_key:<msg_key>:message =
'javascript:/data: URIs may be incompatible with Firefox 6'
validation.msg_key:<msg_key>:long_message =
'A more detailed message....'
# type of message (error, warning, or notice)
validation.msg_key:<msg_key>:type = 'error'
# count of addons affected per message key, per job
validation.job_id:1234.msg_key:<msg_key>:addons_affected = 120
"""
def __init__(self, job_id):
self.job_id = job_id
self.kv = redisutils.connections['master']
def get_messages(self):
for msg_key in self.kv.smembers('validation.job_id:%s' % self.job_id):
yield ValidationMsgTally(self.job_id, msg_key)
def save_message(self, msg):
msg_key = '.'.join(msg['id'])
self.kv.sadd('validation.job_id:%s' % self.job_id,
msg_key)
self.kv.set('validation.msg_key:%s:message' % msg_key,
msg['message'])
if type(msg['description']) == list:
des = []
for _m in msg['description']:
if type(_m) == list:
for x in _m:
des.append(x)
else:
des.append(_m)
des = '; '.join(des)
else:
des = msg['description']
self.kv.set('validation.msg_key:%s:long_message' % msg_key,
des)
if msg.get('compatibility_type'):
effective_type = msg['compatibility_type']
else:
effective_type = msg['type']
self.kv.set('validation.msg_key:%s:type' % msg_key,
effective_type)
self.kv.incr('validation.job_id:%s.msg_key:%s:addons_affected'
% (self.job_id, msg_key))
class ValidationMsgTally(object):
"""Redis key/vals for a tally of validation messages.
"""
def __init__(self, job_id, msg_key):
self.job_id = job_id
self.msg_key = msg_key
self.kv = redisutils.connections['master']
def __getattr__(self, key):
if key in ('message', 'long_message', 'type'):
val = self.kv.get('validation.msg_key:%s:%s'
% (self.msg_key, key))
elif key in ('addons_affected',):
val = self.kv.get('validation.job_id:%s.msg_key:%s:%s'
% (self.job_id, self.msg_key, key))
elif key in ('key',):
val = self.msg_key
else:
raise ValueError('Unknown field: %s' % key)
return val or ''
class SiteEvent(models.Model):
"""Information records about downtime, releases, and other pertinent
events on the site."""
SITE_EVENT_CHOICES = amo.SITE_EVENT_CHOICES.items()
start = models.DateField(db_index=True,
help_text='The time at which the event began.')
end = models.DateField(db_index=True, null=True, blank=True,
help_text='If the event was a range, the time at which it ended.')
event_type = models.PositiveIntegerField(choices=SITE_EVENT_CHOICES,
db_index=True, default=0)
description = models.CharField(max_length=255, blank=True, null=True)
# An outbound link to an explanatory blog post or bug.
more_info_url = models.URLField(max_length=255, blank=True, null=True,
verify_exists=False)