Skip to content
Snippets Groups Projects
models.py 13.7 KiB
Newer Older
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

sascha's avatar
 
sascha committed
from builtins import str
from builtins import object
from os.path import join
import datetime

sascha's avatar
sascha committed
from django.urls import reverse
from django.db import models
from django.db.models import Q
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils import dateformat
from django.utils.encoding import python_2_unicode_compatible, force_text
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import localtime, now
from django.utils.translation import ugettext_lazy as _, pgettext_lazy


from cosinnus_poll.conf import settings
from cosinnus_poll.managers import PollManager
from cosinnus.models import BaseTaggableObjectModel
from cosinnus.utils.permissions import filter_tagged_object_queryset_for_user,\
    check_object_read_access
from cosinnus.utils.urls import group_aware_reverse
from cosinnus_poll import cosinnus_notifications
from django.contrib.auth import get_user_model
from cosinnus.utils.files import _get_avatar_filename
Sascha Narr's avatar
Sascha Narr committed
from cosinnus.models.mixins.images import ThumbnailableImageMixin
from cosinnus.models.tagged import LikeableObjectMixin
from uuid import uuid1


def get_poll_image_filename(instance, filename):
    return _get_avatar_filename(instance, filename, 'images', 'polls')

Sascha Narr's avatar
Sascha Narr committed

@python_2_unicode_compatible
class Poll(LikeableObjectMixin, BaseTaggableObjectModel):

    SORT_FIELDS_ALIASES = [
        ('title', 'title'),
    ]

Sascha Narr's avatar
Sascha Narr committed
    STATE_VOTING_OPEN = 1
    STATE_CLOSED = 2
    STATE_ARCHIVED = 3

    STATE_CHOICES = (
        (STATE_VOTING_OPEN, _('Voting open')),
Sascha Narr's avatar
Sascha Narr committed
        (STATE_CLOSED, _('Voting closed')),
        (STATE_ARCHIVED, _('Poll archived')),
    )

    state = models.PositiveIntegerField(
        _('State'),
        choices=STATE_CHOICES,
        default=STATE_VOTING_OPEN,
    )
Sascha Narr's avatar
Sascha Narr committed
    description = models.TextField(_('Description'), blank=True, null=True)
    
    multiple_votes = models.BooleanField(_('Multiple options votable'), default=True,
         help_text=_('Does this poll allow users to vote on multiple options or just decide for one?'))
    can_vote_maybe = models.BooleanField(_('"Maybe" option enabled'), default=True,
         help_text=_('Is the maybe option enabled? Ignored and defaulting to False if ``multiple_votes==False``'))
    anyone_can_vote = models.BooleanField(_('Anyone can vote'), default=False,
         help_text=_('If true, anyone who can see this poll can vote on it. If false, only group members can.'))
    show_voters = models.BooleanField(_('Show voters'), default=False,
         help_text=_('If true, display a list of which user voted for each option.'))
Sascha Narr's avatar
Sascha Narr committed
    
    closed_date = models.DateTimeField(
        _('Start'), default=None, blank=True, null=True, editable=True)
    winning_option = models.ForeignKey(
        'Option',
        verbose_name=_('Winning Option'),
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='selected_name',
    )

sascha's avatar
sascha committed
    __state = None # pre-save purpose
    
    objects = PollManager()
Sascha Narr's avatar
Sascha Narr committed
    
sascha's avatar
sascha committed
    timeline_template = 'cosinnus_poll/v2/timeline_item.html'

    class Meta(BaseTaggableObjectModel.Meta):
Sascha Narr's avatar
Sascha Narr committed
        ordering = ['-created', '-closed_date']
        verbose_name = _('Poll')
        verbose_name_plural = _('Polls')
        
    def __init__(self, *args, **kwargs):
        super(Poll, self).__init__(*args, **kwargs)
        self.__state = self.state

    def __str__(self):
Sascha Narr's avatar
Sascha Narr committed
        if self.state == self.STATE_VOTING_OPEN:
            state_verbose = 'open'
Sascha Narr's avatar
Sascha Narr committed
            state_verbose = 'closed'
        readable = _('Poll: %(poll)s (%(state)s)') % {'poll': self.title, 'state': state_verbose}
        return readable
    
    def save(self, *args, **kwargs):
        created = bool(self.pk) == False
        super(Poll, self).save(*args, **kwargs)

        session_id = uuid1().int
            group_followers_except_creator_ids = [pk for pk in self.group.get_followed_user_ids() if not pk in [self.creator_id]]
            group_followers_except_creator = get_user_model().objects.filter(id__in=group_followers_except_creator_ids)
            cosinnus_notifications.followed_group_poll_created.send(sender=self, user=self.creator, obj=self, audience=group_followers_except_creator, session_id=session_id)
            cosinnus_notifications.poll_created.send(sender=self, user=self.creator, obj=self, audience=get_user_model().objects.filter(id__in=self.group.members).exclude(id=self.creator.pk), session_id=session_id, end_session=True)
Sascha Narr's avatar
Sascha Narr committed
        if not created and self.__state == Poll.STATE_VOTING_OPEN and self.state == Poll.STATE_CLOSED:
            # poll went from open to closed, so maybe send a notification for poll closed?
Sascha Narr's avatar
Sascha Narr committed
            # send signal only for voters as audience!
            voter_ids = list(set(self.options.all().values_list('votes__voter__id', flat=True)))
Sascha Narr's avatar
Sascha Narr committed
            if self.creator.id in voter_ids:
                voter_ids.remove(self.creator.id)
Sascha Narr's avatar
Sascha Narr committed
            voters = get_user_model().objects.filter(id__in=voter_ids)
            cosinnus_notifications.poll_completed.send(sender=self, user=self.creator, obj=self, audience=voters, session_id=session_id)
            # message following users
            followers_except_creator = [pk for pk in self.get_followed_user_ids() if not pk in [self.creator_id]]
            cosinnus_notifications.following_poll_completed.send(sender=self, user=self.creator, obj=self, audience=get_user_model().objects.filter(id__in=followers_except_creator), session_id=session_id, end_session=True)
            
        self.__state = self.state

    def get_absolute_url(self):
        kwargs = {'group': self.group, 'slug': self.slug}
Sascha Narr's avatar
Sascha Narr committed
        return group_aware_reverse('cosinnus:poll:detail', kwargs=kwargs)
    
    def get_options_hash(self):
        """ Returns a hashable string containing all suggestions with their time.
            Useful to compare equality of suggestions for two doodles. """
        return ','.join(list(self.options.all().values_list('description', flat=True)))
    
Sascha Narr's avatar
Sascha Narr committed
    def set_winning_option(self, winning_option=None):
        if winning_option is None:
            # No option selected or remove selection
            self.winning_option = None
        elif winning_option.poll.pk == self.pk:
            # Make sure to not assign a option belonging to another poll.
            self.option = winning_option
Sascha Narr's avatar
Sascha Narr committed
        self.save(update_fields=['winning_option'])

    @classmethod
    def get_current(self, group, user):
Sascha Narr's avatar
Sascha Narr committed
        """ Returns a queryset of the current polls """
        qs = Poll.objects.filter(group=group)
        if user:
            qs = filter_tagged_object_queryset_for_user(qs, user)
Sascha Narr's avatar
Sascha Narr committed
        return current_poll_filter(qs)
    
    def get_voters_pks(self):
        """ Gets the pks of all Users that have voted for this poll.
Sascha Narr's avatar
Sascha Narr committed
            Returns an empty list if nobody has voted on the poll. """
Sascha Narr's avatar
Sascha Narr committed
        return self.options.all().values_list('votes__voter__id', flat=True).distinct()


@python_2_unicode_compatible
Sascha Narr's avatar
Sascha Narr committed
class Option(ThumbnailableImageMixin, models.Model):
    
    image_attr_name = 'image'
Sascha Narr's avatar
Sascha Narr committed
    
    poll = models.ForeignKey(
        Poll,
        verbose_name=_('Poll'),
        on_delete=models.CASCADE,
Sascha Narr's avatar
Sascha Narr committed
        related_name='options',
Sascha Narr's avatar
Sascha Narr committed
    
    description = models.TextField(_('Description'), blank=False, null=False)
Sascha Narr's avatar
Sascha Narr committed
    image = models.ImageField(
        _('Image'),
        upload_to=get_poll_image_filename,
        blank=True,
        null=True)

    count = models.PositiveIntegerField(
        pgettext_lazy('the subject', 'Votes'), default=0, editable=False)

sascha's avatar
 
sascha committed
    class Meta(object):
        ordering = ['poll', '-count']
Sascha Narr's avatar
Sascha Narr committed
        verbose_name = _('Poll Option')
        verbose_name_plural = _('Poll Options')

    def __str__(self):
Sascha Narr's avatar
Sascha Narr committed
        return 'Poll Option for Poll id: %s' % str(getattr(self, 'poll_id', None))

    def get_absolute_url(self):
        return self.poll.get_absolute_url()

    def update_vote_count(self, count=None):
        self.count = self.votes.count()
        self.save(update_fields=['count'])

    @cached_property
    def sorted_votes(self):
        return self.votes.order_by('voter__first_name', 'voter__last_name')
    
    @cached_property
    def sorted_votes_by_choice(self):
        return self.votes.order_by('-choice')

@python_2_unicode_compatible
class Vote(models.Model):
    
    VOTE_YES = 2
    VOTE_MAYBE = 1
    VOTE_NO = 0
    
    VOTE_CHOICES = (
        (VOTE_YES, _('Yes')),
        (VOTE_MAYBE, _('Maybe')),
        (VOTE_NO, _('No')),     
    )
    
Sascha Narr's avatar
Sascha Narr committed
    option = models.ForeignKey(
        Option,
        verbose_name=_('Option'),
        on_delete=models.CASCADE,
Sascha Narr's avatar
Sascha Narr committed
        related_name='votes',
    )

    voter = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_('Voter'),
        on_delete=models.CASCADE,
Sascha Narr's avatar
Sascha Narr committed
        related_name='poll_votes',
    )
    
    choice = models.PositiveSmallIntegerField(_('Vote'), blank=False, null=False,
        default=VOTE_NO, choices=VOTE_CHOICES)
    
sascha's avatar
 
sascha committed
    class Meta(object):
Sascha Narr's avatar
Sascha Narr committed
        unique_together = ('option', 'voter')
        verbose_name = pgettext_lazy('the subject', 'Vote')
        verbose_name_plural = pgettext_lazy('the subject', 'Votes')

    def __str__(self):
Sascha Narr's avatar
Sascha Narr committed
        return 'Vote for poll: "%(poll)s" with choice: %(choice)s' % {
            'poll': self.option.poll.title,
            'choice': self.choice,
        }

    def get_absolute_url(self):
Sascha Narr's avatar
Sascha Narr committed
        return self.option.poll.get_absolute_url()
Sascha Narr's avatar
Sascha Narr committed
        return force_text(dict(self.VOTE_CHOICES)[self.choice])


@python_2_unicode_compatible
class Comment(models.Model):
    creator = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Creator'), on_delete=models.PROTECT, related_name='poll_comments')
    created_on = models.DateTimeField(_('Created'), default=now, editable=False)
    last_modified = models.DateTimeField(_('Last modified'), auto_now=True, editable=False)
    poll = models.ForeignKey(Poll, related_name='comments', on_delete=models.CASCADE)
    text = models.TextField(_('Text'))

sascha's avatar
 
sascha committed
    class Meta(object):
        ordering = ['created_on']
        verbose_name = _('Comment')
        verbose_name_plural = _('Comments')

    def __str__(self):
        return 'Comment on “%(poll)s” by %(creator)s' % {
            'poll': self.poll.title,
            'creator': self.creator.get_full_name(),
        }

    def get_absolute_url(self):
        if self.pk:
            return '%s#comment-%d' % (self.poll.get_absolute_url(), self.pk)
        return self.poll.get_absolute_url()
    
    def is_user_following(self, user):
        """ Delegates to parent object """
        return self.poll.is_user_following(user)
    
    def save(self, *args, **kwargs):
        created = bool(self.pk) == False
        super(Comment, self).save(*args, **kwargs)
        session_id = uuid1().int
        if created:
            # comment was created, message poll creator
            if not self.poll.creator == self.creator:
                cosinnus_notifications.poll_comment_posted.send(sender=self, user=self.creator, obj=self, audience=[self.poll.creator], session_id=session_id)
                
            # message all followers of the poll
            followers_except_creator = [pk for pk in self.poll.get_followed_user_ids() if not pk in [self.creator_id, self.poll.creator_id]]
            cosinnus_notifications.following_poll_comment_posted.send(sender=self, user=self.creator, obj=self, audience=get_user_model().objects.filter(id__in=followers_except_creator), session_id=session_id)
                
            # message votees (except comment creator and poll creator) if voting is still open
            votees_except_creator = [pk for pk in self.poll.get_voters_pks() if not pk in [self.creator_id, self.poll.creator_id]]
            if votees_except_creator and self.poll.state == Poll.STATE_VOTING_OPEN:
                cosinnus_notifications.voted_poll_comment_posted.send(sender=self, user=self.creator, obj=self, audience=get_user_model().objects.filter(id__in=votees_except_creator), session_id=session_id)
            # message all taggees (except comment creator)
            if self.poll.media_tag and self.poll.media_tag.persons:
                tagged_users_without_self = self.poll.media_tag.persons.exclude(id=self.creator.id)
                if len(tagged_users_without_self) > 0:
                    cosinnus_notifications.tagged_poll_comment_posted.send(sender=self, user=self.creator, obj=self, audience=list(tagged_users_without_self), session_id=session_id)
    
            # end notification session        
            cosinnus_notifications.tagged_poll_comment_posted.send(sender=self, user=self.creator, obj=self, audience=[], session_id=session_id, end_session=True)
    
    
    @property
    def group(self):
        """ Needed by the notifications system """
        return self.poll.group

    def grant_extra_read_permissions(self, user):
        """ Comments inherit their visibility from their commented on parent """
        return check_object_read_access(self.poll, user)

@receiver(post_delete, sender=Vote)
def post_vote_delete(sender, **kwargs):
    try:
Sascha Narr's avatar
Sascha Narr committed
        kwargs['instance'].option.update_vote_count()
    except Option.DoesNotExist:
        pass


@receiver(post_save, sender=Vote)
def post_vote_save(sender, **kwargs):
Sascha Narr's avatar
Sascha Narr committed
    kwargs['instance'].option.update_vote_count()
Sascha Narr's avatar
Sascha Narr committed
def current_poll_filter(queryset):
    """ Filters a queryset of polls for polls are open or closed (but not archived). """
    return queryset.exclude(state=Poll.STATE_ARCHIVED)

def past_poll_filter(queryset):
    """ Filters a queryset of polls for polls that began before today, 
    or have an end date before today. """
    return queryset.filter(state=Poll.STATE_ARCHIVED)


import django
if django.VERSION[:2] < (1, 7):
    from cosinnus_poll import cosinnus_app
    cosinnus_app.register()