# -*- coding: utf-8 -*- from uuid import uuid4 from hashlib import md5 from django.db import models from django.conf import settings from django.utils import timezone from django.core.cache import cache from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.translation import ugettext_lazy as _ from html2text import html2text from haystack.query import SearchQuerySet from taggit.managers import TaggableManager from hitcounter.models import HitCounterModelMixin from .utils import blocks from .utils.etiquetador import etiquetador User = get_user_model() class NotSpamManager(models.Manager): """Only return objects which are not marked as spam.""" def get_query_set(self): return super(NotSpamManager, self).get_query_set().exclude(spam=True) class EmailAddressValidation(models.Model): address = models.EmailField(unique=True) user = models.ForeignKey(User, null=True, related_name='emails_not_validated') validation_key = models.CharField(max_length=32, null=True, default=lambda: uuid4().hex) created = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'address') class EmailAddress(models.Model): user = models.ForeignKey(User, null=True, related_name='emails', on_delete=models.SET_NULL) address = models.EmailField(unique=True) real_name = models.CharField(max_length=64, blank=True, db_index=True) md5 = models.CharField(max_length=32, null=True) class Meta: ordering = ('id', ) def save(self, *args, **kwargs): self.md5 = md5(self.address).hexdigest() super(EmailAddress, self).save(*args, **kwargs) def get_full_name(self): if self.user and self.user.get_full_name(): return self.user.get_full_name() else: return self.real_name def get_full_name_or_anonymous(self): return self.get_full_name() or _('Anonymous') def __unicode__(self): return '"%s" <%s>' % (self.get_full_name(), self.address) class MailingList(models.Model): name = models.CharField(max_length=80) email = models.EmailField() description = models.TextField() logo = models.FileField(upload_to='list_logo') #TODO last_imported_index = models.IntegerField(default=0) def get_absolute_url(self): return u'{}?list={}'.format(reverse('thread_list'), self.name) def __unicode__(self): return self.name class MailingListMembership(models.Model): user = models.ForeignKey(User) mailinglist = models.ForeignKey(MailingList) def __unicode__(self): return '%s on %s' % (self.user.email, self.mailinglist.name) class Keyword(models.Model): keyword = models.CharField(max_length='128') weight = models.IntegerField(default=0) thread = models.ForeignKey('Thread') class Meta: ordering = ('?', ) # random order def __unicode__(self): return self.keyword class Thread(models.Model, HitCounterModelMixin): subject_token = models.CharField(max_length=512) mailinglist = models.ForeignKey(MailingList, verbose_name=_(u"Mailing List"), help_text=_(u"The Mailing List where is the thread")) latest_message = models.OneToOneField('Message', null=True, related_name='+', verbose_name=_(u"Latest message"), help_text=_(u"Latest message posted")) score = models.IntegerField(default=0, verbose_name=_(u"Score"), help_text=_(u"Thread score")) spam = models.BooleanField(default=False) all_objects = models.Manager() objects = NotSpamManager() tags = TaggableManager() class Meta: verbose_name = _(u"Thread") verbose_name_plural = _(u"Threads") unique_together = ('subject_token', 'mailinglist') @models.permalink def get_absolute_url(self): return ('thread_view', [self.mailinglist, self.subject_token]) def update_keywords(self): blocks = MessageBlock.objects.filter(message__thread__pk=self.pk, is_reply=False) self.tags.clear() text = u'\n'.join(map(unicode, blocks)) tags = etiquetador(html2text(text)) for tag, weight in tags: keyword, created = Keyword.objects.get_or_create(thread=self, keyword=tag) if created or keyword.weight != weight: keyword.weight = weight keyword.save() if weight >= 3: self.tags.add(tag) # removing old tags not used anylonger if tags: qs = Keyword.objects.filter(thread=self) qs = qs.exclude(keyword__in=zip(*tags)[0]) qs.delete() def get_related(self): query_string = u' '.join(self.tags.names()) if query_string: query_set = SearchQuerySet().exclude(django_id=self.pk) return query_set.filter(content=query_string, type='thread') return tuple() def save(self, *args, **kwargs): super(Thread, self).save(*args, **kwargs) self.update_keywords() def __unicode__(self): return '%s - %s (%s)' % (self.id, self.subject_token, self.message_set.count()) def update_score(self): """Update the relevance score for this thread. The score is calculated with the following variables: * vote_weight: 100 - (minus) 1 for each 3 days since voted with minimum of 5. * replies_weight: 300 - (minus) 1 for each 3 days since replied with minimum of 5. * page_view_weight: 10. * vote_score: sum(vote_weight) * replies_score: sum(replies_weight) * page_view_score: sum(page_view_weight) * score = (vote_score + replies_score + page_view_score) // 10 with minimum of 0 and maximum of 5000 """ if not self.subject_token: return # Save this pseudo now to avoid calling the # function N times in the loops below now = timezone.now() days_ago = lambda date: (now - date).days get_score = lambda weight, created: \ max(weight - (days_ago(created) // 3), 5) vote_score = 0 replies_score = 0 for msg in self.message_set.all(): # Calculate replies_score replies_score += get_score(300, msg.received_time) # Calculate vote_score for vote in msg.vote_set.all(): vote_score += get_score(100, vote.created) # Calculate page_view_score page_view_score = self.hits * 10 self.score = (page_view_score + vote_score + replies_score) // 10 self.save() class Vote(models.Model): user = models.ForeignKey(User) message = models.ForeignKey('Message') created = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'message') def __unicode__(self): return 'Vote on %s by %s' % (self.Message.id, self.user.username) class Message(models.Model): from_address = models.ForeignKey(EmailAddress, db_index=True) thread = models.ForeignKey(Thread, null=True, db_index=True) # RFC 2822 recommends to use 78 chars + CRLF (so 80 chars) for # the max_length of a subject but most of implementations # goes for 256. We use 512 just in case. subject = models.CharField(max_length=512, db_index=True, verbose_name=_(u"Subject"), help_text=_(u"Please enter a message subject")) subject_clean = models.CharField(max_length=512, db_index=True) body = models.TextField(default='', verbose_name=_(u"Message body"), help_text=_(u"Please enter a message body")) received_time = models.DateTimeField(db_index=True) message_id = models.CharField(max_length=512) spam = models.BooleanField(default=False) all_objects = models.Manager() objects = NotSpamManager() class Meta: verbose_name = _(u"Message") verbose_name_plural = _(u"Messages") unique_together = ('thread', 'message_id') ordering = ('received_time', ) def __unicode__(self): return '(%s) %s: %s' % (self.id, self.from_address.get_full_name(), self.subject_clean) def update_blocks(self): # delete all blocks for that message self.blocks.all().delete() for i, block in enumerate(blocks.EmailBlockParser(self)): MessageBlock.from_emailblock(block, self, i) @property def mailinglist(self): if not self.thread: return None return self.thread.mailinglist def vote_list(self): """Return a list of user that voted in this message.""" return [vote.user for vote in self.vote_set.iterator()] def votes_count(self): return len(self.vote_list()) def vote(self, user): Vote.objects.create( message=self, user=user ) def unvote(self, user): Vote.objects.get( message=self, user=user ).delete() @property def url(self): """Shortcut to get thread url""" return reverse('thread_view', args=[self.mailinglist.name, self.thread.subject_token]) @property def description(self): """Alias to self.body""" return self.body @property def title(self): """Alias to self.subject_clean""" return self.subject_clean @property def modified(self): """Alias to self.modified""" return self.received_time @property def tag(self): if not self.thread: return None return self.mailinglist.name @property def author(self): return self.from_address.get_full_name() @property def author_url(self): return self.from_address.user.get_absolute_url() @property def icon_name(self): return u'envelope' @property def type(self): return u'thread' class MessageBlock(models.Model): message = models.ForeignKey(Message, related_name='blocks') text = models.TextField() is_reply = models.BooleanField() order = models.IntegerField() def __unicode__(self): return self.text class Meta: ordering = ('order', ) @classmethod def from_emailblock(klass, emailblock, message, order): obj = klass.objects.create(text=emailblock.text, is_reply=emailblock.is_reply, message=message, order=order) return obj class MessageMetadata(models.Model): Message = models.ForeignKey(Message) # Same problem here than on subjects. Read comment above # on Message.subject name = models.CharField(max_length=512) value = models.TextField() def __unicode__(self): return 'Email Message Id: %s - %s: %s' % (self.Message.id, self.name, self.value)