Newer
Older
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from os.path import join
import datetime
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

Sascha Narr
committed
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

Sascha Narr
committed
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
def get_poll_image_filename(instance, filename):
return _get_avatar_filename(instance, filename, 'images', 'polls')
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.'))

Sascha Narr
committed
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',
)
timeline_template = 'cosinnus_poll/v2/dashboard/timeline_item.html'
class Meta(BaseTaggableObjectModel.Meta):
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'
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)
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
@classmethod
def get_current(self, group, user):
qs = Poll.objects.filter(group=group)
if user:
qs = filter_tagged_object_queryset_for_user(qs, user)
def get_voters_pks(self):
""" Gets the pks of all Users that have voted for this 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})
class Option(ThumbnailableImageMixin, models.Model):
image_attr_name = 'image'
poll = models.ForeignKey(
Poll,
verbose_name=_('Poll'),
on_delete=models.CASCADE,
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)
verbose_name = _('Poll Option')
verbose_name_plural = _('Poll Options')
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')

Sascha Narr
committed
@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'),
)
voter = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('Voter'),
on_delete=models.CASCADE,
)
choice = models.PositiveSmallIntegerField(_('Vote'), blank=False, null=False,
default=VOTE_NO, choices=VOTE_CHOICES)
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):

Sascha Narr
committed
def get_label(self):
@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'))
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)
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

Sascha Narr
committed
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):
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()