# coding=utf8 # -*- coding: utf8 -*- # vim: set fileencoding=utf8 : from __future__ import unicode_literals from django.conf import settings from django.db import models from django.db.models import Count, Max from django.db.models.signals import post_save from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now, timedelta @python_2_unicode_compatible class Participant(models.Model): """ The participant model holds a django.contrib.auth.models.User's id. This allows us to use rest_messaging without querying the main db. """ id = models.PositiveIntegerField(primary_key=True) def __str__(self): return "{0}".format(self.id) class ThreadManager(models.Manager): def get_threads_for_participant(self, participant_id): """ Gets all the threads in which the current participant is or was involved. The method does not exclude threads where the participant has left. """ return Thread.objects.\ filter(participants__id=participant_id).\ distinct() def get_threads_where_participant_is_active(self, participant_id): """ Gets all the threads in which the current participant is involved. The method excludes threads where the participant has left. """ participations = Participation.objects.\ filter(participant__id=participant_id).\ exclude(date_left__lte=now()).\ distinct().\ select_related('thread') return Thread.objects.\ filter(id__in=[p.thread.id for p in participations]).\ distinct() def get_active_threads_involving_all_participants(self, *participant_ids): """ Gets the threads where the specified participants are active and no one has left. """ query = Thread.objects.\ exclude(participation__date_left__lte=now()).\ annotate(count_participants=Count('participants')).\ filter(count_participants=len(participant_ids)) for participant_id in participant_ids: query = query.filter(participants__id=participant_id) return query.distinct() def get_or_create_thread(self, request, name=None, *participant_ids): """ When a Participant posts a message to other participants without specifying an existing Thread, we must 1. Create a new Thread if they have not yet opened the discussion. 2. If they have already opened the discussion and multiple Threads are not allowed for the same users, we must re-attach this message to the existing thread. 3. If they have already opened the discussion and multiple Threads are allowed, we simply create a new one. """ # we get the current participant # or create him if he does not exit participant_ids = list(participant_ids) if request.rest_messaging_participant.id not in participant_ids: participant_ids.append(request.rest_messaging_participant.id) # we need at least one other participant if len(participant_ids) < 2: raise Exception('At least two participants are required.') if getattr(settings, "REST_MESSAGING_THREAD_UNIQUE_FOR_ACTIVE_RECIPIENTS", True) is True: # if we limit the number of threads by active participants # we ensure a thread is not already running existing_threads = self.get_active_threads_involving_all_participants(*participant_ids) if len(list(existing_threads)) > 0: return existing_threads[0] # we have no existing Thread or multiple Thread instances are allowed thread = Thread.objects.create(name=name) # we add the participants thread.add_participants(request, *participant_ids) # we send a signal to say the thread with participants is created post_save.send(Thread, instance=thread, created=True, created_and_add_participants=True, request_participant_id=request.rest_messaging_participant.id) return thread @python_2_unicode_compatible class Thread(models.Model): """ A Thread groups messages using their recipients. """ name = models.CharField(max_length=255, null=True, blank=True) participants = models.ManyToManyField(Participant, through='Participation') objects = models.Manager() managers = ThreadManager() def __str__(self): return self.name if self.name else "Thread {0}".format(self.id) def is_participant(self, request, *args, **kwargs): """ We ensure request.user is a participant to the thread. """ participants = self.participants.all() try: current = Participant.objects.get(id=request.user.id) if current in participants: return current except: pass return False def add_participants(self, request, *participants_ids): """ Ensures the current user has the authorization to add the participants. By default, a user can add a participant if he himself is a participant. A callback can be added in the settings here. """ participants_ids_returned_by_callback = getattr(settings, 'REST_MESSAGING_ADD_PARTICIPANTS_CALLBACK', self._limit_participants)(request, *participants_ids) participations = [] ids = [] for participant_id in participants_ids_returned_by_callback: participations.append(Participation(participant_id=participant_id, thread=self)) ids.append(participant_id) Participation.objects.bulk_create(participations) post_save.send(Thread, instance=self, created=True, created_and_add_participants=True, request_participant_id=request.rest_messaging_participant.id) return ids def _limit_participants(self, request, *participants_ids): """ By default, we ensure we do not have more than 10 participants. """ participants_all = self.participants.all().values_list('id', flat=True) max = 10 - len(participants_all) lst = [] for index, participant_id in enumerate(participants_ids): if index >= max: break if participant_id not in participants_all: lst.append(participant_id) return lst def remove_participant(self, request, participant): removable_participants_ids = self.get_removable_participants_ids(request) if participant.id in removable_participants_ids: participation = Participation.objects.get(participant=participant, thread=self, date_left=None) participation.date_left = now() participation.save() post_save.send(Thread, instance=self, created=False, remove_participant=True, removed_participant=participant, request_participant_id=request.rest_messaging_participant.id) return participation else: raise Exception('The participant may not be removed.') def get_removable_participants_ids(self, request): removable_participants_ids = getattr(settings, 'REST_MESSAGING_REMOVE_PARTICIPANTS_CALLBACK', self._can_remove_oneself_only)(request, self) return removable_participants_ids def _can_remove_oneself_only(self, request, participant): """ By default, we ensure request.user can only remove oneself. """ try: return [self.is_participant(request).id] except: return [] class Participation(models.Model): """ Links Participant to threads. Tells us when a participant has joined a Thread and when he left. """ participant = models.ForeignKey(Participant) thread = models.ForeignKey(Thread) date_joined = models.DateTimeField(auto_now_add=True) date_left = models.DateTimeField(null=True, blank=True) date_last_check = models.DateTimeField(null=True, blank=True) # a timestamp to be set when a participant reads a thread class MessageManager(models.Manager): def return_daily_messages_count(self, sender): """ Returns the number of messages sent in the last 24 hours so we can ensure the user does not exceed his messaging limits """ h24 = now() - timedelta(days=1) return Message.objects.filter(sender=sender, sent_at__gte=h24).count() def check_who_read(self, messages): """ Check who read each message. """ # we get the corresponding Participation objects for m in messages: readers = [] for p in m.thread.participation_set.all(): if p.date_last_check is None: pass elif p.date_last_check > m.sent_at: # the message has been read readers.append(p.participant.id) setattr(m, "readers", readers) return messages def check_is_notification(self, participant_id, messages): """ Check if each message requires a notification for the specified participant. """ try: # we get the last check last_check = NotificationCheck.objects.filter(participant__id=participant_id).latest('id').date_check except Exception: # we have no notification check # all the messages are considered as new for m in messages: m.is_notification = True return messages for m in messages: if m.sent_at > last_check and m.sender.id != participant_id: setattr(m, "is_notification", True) else: setattr(m, "is_notification", False) return messages def get_lasts_messages_of_threads(self, participant_id, check_who_read=True, check_is_notification=True): """ Returns the last message in each thread """ # we get the last message for each thread # we must query the messages using two queries because only Postgres supports .order_by('thread', '-sent_at').distinct('thread') threads = Thread.managers.\ get_threads_where_participant_is_active(participant_id).\ annotate(last_message_id=Max('message__id')) messages = Message.objects.filter(id__in=[thread.last_message_id for thread in threads]).\ order_by('-id').\ distinct().\ select_related('thread', 'sender') if check_who_read is True: messages = messages.prefetch_related('thread__participation_set', 'thread__participation_set__participant') messages = self.check_who_read(messages) else: messages = messages.prefetch_related('thread__participants') if check_is_notification is True: messages = self.check_is_notification(participant_id, messages) return messages def get_all_messages_in_thread(self, participant_id, thread_id, check_who_read=True): """ Returns all the messages in a thread. """ try: messages = Message.objects.filter(thread__id=thread_id).\ order_by('-id').\ select_related('thread').\ prefetch_related('thread__participation_set', 'thread__participation_set__participant') except Exception: return Message.objects.none() messages = self.check_who_read(messages) return messages @python_2_unicode_compatible class Message(models.Model): """ A message between a User and another User or an AnonymousUser. """ body = models.TextField(null=False) sender = models.ForeignKey(Participant, null=False) thread = models.ForeignKey(Thread) sent_at = models.DateTimeField(auto_now_add=True, blank=True) objects = models.Manager() managers = MessageManager() def __str__(self): return "{0}: {1}".format(self.sender, "{0}".format(self.body[:15])) def save(self, *args, **kwargs): """ Checks if there is a daily limit to the number of messages that can be sent. """ max_messages = getattr(settings, 'REST_MESSAGING_DAILY_LIMIT_CALLBACK', lambda message_instance, *args, **kwargs: None)(self, *args, **kwargs) if max_messages is None or Message.managers.return_daily_messages_count(self.sender) < max_messages: super(Message, self).save(*args, **kwargs) else: # participant cannot write anymore today raise Exception('The daily messaging limit has been reached for this sender') @python_2_unicode_compatible class NotificationCheck(models.Model): """ A timestamp everytime a user checks his notifications """ participant = models.OneToOneField(Participant) date_check = models.DateTimeField() def __str__(self): return "{0}: {1}".format(self.participant, self.date_check)