# -*- coding: utf-8 -*- from __future__ import unicode_literals from builtins import str from builtins import object from os.path import join import datetime 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 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') @python_2_unicode_compatible class Poll(LikeableObjectMixin, BaseTaggableObjectModel): SORT_FIELDS_ALIASES = [ ('title', 'title'), ] STATE_VOTING_OPEN = 1 STATE_CLOSED = 2 STATE_ARCHIVED = 3 STATE_CHOICES = ( (STATE_VOTING_OPEN, _('Voting open')), (STATE_CLOSED, _('Voting closed')), (STATE_ARCHIVED, _('Poll archived')), ) state = models.PositiveIntegerField( _('State'), choices=STATE_CHOICES, default=STATE_VOTING_OPEN, ) 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.')) 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', ) __state = None # pre-save purpose objects = PollManager() timeline_template = 'cosinnus_poll/v2/dashboard/timeline_item.html' class Meta(BaseTaggableObjectModel.Meta): 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): if self.state == self.STATE_VOTING_OPEN: state_verbose = 'open' else: 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 if created: 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) 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? # send signal only for voters as audience! voter_ids = list(set(self.options.all().values_list('votes__voter__id', flat=True))) if self.creator.id in voter_ids: voter_ids.remove(self.creator.id) 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} return group_aware_reverse('cosinnus:poll:detail', kwargs=kwargs) def get_edit_url(self): kwargs = {'group': self.group, 'slug': self.slug} return group_aware_reverse('cosinnus:poll:edit', kwargs=kwargs) def get_delete_url(self): kwargs = {'group': self.group, 'slug': self.slug} return group_aware_reverse('cosinnus:poll:delete', 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))) 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 else: return self.save(update_fields=['winning_option']) @classmethod def get_current(self, group, user): """ Returns a queryset of the current polls """ qs = Poll.objects.filter(group=group) if user: qs = filter_tagged_object_queryset_for_user(qs, user) return current_poll_filter(qs) def get_voters_pks(self): """ Gets the pks of all Users that have voted for this poll. Returns an empty list if nobody has voted on the poll. """ return self.options.all().values_list('votes__voter__id', flat=True).distinct() def get_comment_post_url(self): return group_aware_reverse('cosinnus:poll:comment', kwargs={'group': self.group, 'poll_slug': self.slug}) @python_2_unicode_compatible class Option(ThumbnailableImageMixin, models.Model): image_attr_name = 'image' poll = models.ForeignKey( Poll, verbose_name=_('Poll'), on_delete=models.CASCADE, related_name='options', ) description = models.TextField(_('Description'), blank=False, null=False) 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) class Meta(object): ordering = ['poll', '-count'] verbose_name = _('Poll Option') verbose_name_plural = _('Poll Options') def __str__(self): 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')), ) option = models.ForeignKey( Option, verbose_name=_('Option'), on_delete=models.CASCADE, related_name='votes', ) voter = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('Voter'), on_delete=models.CASCADE, related_name='poll_votes', ) choice = models.PositiveSmallIntegerField(_('Vote'), blank=False, null=False, default=VOTE_NO, choices=VOTE_CHOICES) class Meta(object): unique_together = ('option', 'voter') verbose_name = pgettext_lazy('the subject', 'Vote') verbose_name_plural = pgettext_lazy('the subject', 'Votes') def __str__(self): return 'Vote for poll: "%(poll)s" with choice: %(choice)s' % { 'poll': self.option.poll.title, 'choice': self.choice, } def get_absolute_url(self): return self.option.poll.get_absolute_url() def get_label(self): 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')) 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 get_edit_url(self): return group_aware_reverse('cosinnus:poll:comment-update', kwargs={'group': self.poll.group, 'pk': self.pk}) def get_delete_url(self): return group_aware_reverse('cosinnus:poll:comment-delete', kwargs={'group': self.poll.group, 'pk': self.pk}) 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: kwargs['instance'].option.update_vote_count() except Option.DoesNotExist: pass @receiver(post_save, sender=Vote) def post_vote_save(sender, **kwargs): kwargs['instance'].option.update_vote_count() 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()