2013-04-23 18:51:17 +02:00
|
|
|
from __future__ import absolute_import
|
|
|
|
|
2012-08-28 18:44:51 +02:00
|
|
|
from django.db import models
|
2012-09-19 19:39:34 +02:00
|
|
|
from django.conf import settings
|
2013-04-01 16:57:50 +02:00
|
|
|
from django.contrib.auth.models import AbstractBaseUser, UserManager
|
2013-03-15 21:23:34 +01:00
|
|
|
from zephyr.lib.cache import cache_with_key, update_user_profile_cache, \
|
2013-04-05 00:13:03 +02:00
|
|
|
user_profile_by_id_cache_key, user_profile_by_email_cache_key, \
|
|
|
|
update_user_presence_cache
|
2013-03-20 15:31:27 +01:00
|
|
|
from zephyr.lib.utils import make_safe_digest
|
2012-09-27 19:58:42 +02:00
|
|
|
import os
|
2012-10-23 23:29:56 +02:00
|
|
|
from django.db import transaction, IntegrityError
|
2012-10-15 22:03:50 +02:00
|
|
|
from zephyr.lib import bugdown
|
2013-06-10 21:35:48 +02:00
|
|
|
from zephyr.lib.avatar import gravatar_hash, avatar_url
|
2012-10-20 18:02:58 +02:00
|
|
|
from django.utils import timezone
|
2012-10-29 19:43:00 +01:00
|
|
|
from django.contrib.sessions.models import Session
|
2012-12-10 21:20:20 +01:00
|
|
|
from django.utils.html import escape
|
2013-01-10 22:49:07 +01:00
|
|
|
from zephyr.lib.timestamp import datetime_to_timestamp
|
2013-03-15 21:17:32 +01:00
|
|
|
from django.db.models.signals import post_save
|
2013-06-19 21:41:29 +02:00
|
|
|
import zlib
|
2012-09-21 16:10:36 +02:00
|
|
|
|
2013-03-12 17:51:55 +01:00
|
|
|
from bitfield import BitField
|
2013-06-18 23:55:55 +02:00
|
|
|
import ujson
|
2013-03-12 17:51:55 +01:00
|
|
|
|
2012-12-07 01:05:14 +01:00
|
|
|
MAX_SUBJECT_LENGTH = 60
|
2012-12-11 17:12:53 +01:00
|
|
|
MAX_MESSAGE_LENGTH = 10000
|
2012-12-07 01:05:14 +01:00
|
|
|
|
2013-04-25 20:42:28 +02:00
|
|
|
# Doing 1000 memcached requests to get_display_recipient is quite slow,
|
|
|
|
# so add a local cache as well as the memcached cache.
|
|
|
|
recipient_cache = {}
|
|
|
|
def get_display_recipient(recipient):
|
|
|
|
if settings.TEST_SUITE:
|
|
|
|
# The test suite expects all caching to be turned off
|
|
|
|
return get_display_recipient_memcached(recipient)
|
|
|
|
if recipient.id not in recipient_cache:
|
|
|
|
recipient_cache[recipient.id] = get_display_recipient_memcached(recipient)
|
|
|
|
return recipient_cache[recipient.id]
|
|
|
|
|
2013-03-26 19:09:45 +01:00
|
|
|
@cache_with_key(lambda self: 'display_recipient_dict:%d' % (self.id,),
|
|
|
|
timeout=3600*24*7)
|
2013-04-25 20:42:28 +02:00
|
|
|
def get_display_recipient_memcached(recipient):
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
"""
|
2012-10-30 21:47:43 +01:00
|
|
|
recipient: an instance of Recipient.
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
|
2012-12-03 19:49:12 +01:00
|
|
|
returns: an appropriate object describing the recipient. For a
|
|
|
|
stream this will be the stream name as a string. For a huddle or
|
|
|
|
personal, it will be an array of dicts about each recipient.
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
"""
|
2012-10-10 22:57:21 +02:00
|
|
|
if recipient.type == Recipient.STREAM:
|
2012-10-10 22:53:24 +02:00
|
|
|
stream = Stream.objects.get(id=recipient.type_id)
|
|
|
|
return stream.name
|
2012-09-27 19:58:42 +02:00
|
|
|
|
2012-12-03 19:49:12 +01:00
|
|
|
# We don't really care what the ordering is, just that it's deterministic.
|
|
|
|
user_profile_list = (UserProfile.objects.filter(subscription__recipient=recipient)
|
|
|
|
.select_related()
|
2013-03-28 20:43:34 +01:00
|
|
|
.order_by('email'))
|
|
|
|
return [{'email': user_profile.email,
|
2013-05-02 18:56:42 +02:00
|
|
|
'domain': user_profile.realm.domain,
|
2012-09-27 19:58:42 +02:00
|
|
|
'full_name': user_profile.full_name,
|
|
|
|
'short_name': user_profile.short_name} for user_profile in user_profile_list]
|
|
|
|
|
2012-09-05 21:49:56 +02:00
|
|
|
class Realm(models.Model):
|
2012-10-22 19:23:11 +02:00
|
|
|
domain = models.CharField(max_length=40, db_index=True, unique=True)
|
2013-02-06 16:59:31 +01:00
|
|
|
restricted_to_domain = models.BooleanField(default=True)
|
2012-09-05 21:49:56 +02:00
|
|
|
|
|
|
|
def __repr__(self):
|
2013-03-26 19:46:47 +01:00
|
|
|
return (u"<Realm: %s %s>" % (self.domain, self.id)).encode("utf-8")
|
2012-09-05 21:49:56 +02:00
|
|
|
def __str__(self):
|
|
|
|
return self.__repr__()
|
|
|
|
|
2013-03-08 19:53:00 +01:00
|
|
|
class UserProfile(AbstractBaseUser):
|
2013-04-04 20:38:22 +02:00
|
|
|
# Fields from models.AbstractUser minus last_name and first_name,
|
|
|
|
# which we don't use; email is modified to make it indexed and unique.
|
|
|
|
email = models.EmailField(blank=False, db_index=True, unique=True)
|
2013-03-08 19:53:00 +01:00
|
|
|
is_staff = models.BooleanField(default=False)
|
|
|
|
is_active = models.BooleanField(default=True)
|
2013-05-03 00:25:43 +02:00
|
|
|
is_bot = models.BooleanField(default=False)
|
2013-03-08 19:53:00 +01:00
|
|
|
date_joined = models.DateTimeField(default=timezone.now)
|
2013-05-03 00:25:43 +02:00
|
|
|
bot_owner = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
|
|
|
|
|
2013-03-08 19:53:00 +01:00
|
|
|
USERNAME_FIELD = 'email'
|
|
|
|
|
|
|
|
# Our custom site-specific fields
|
2012-09-11 19:20:01 +02:00
|
|
|
full_name = models.CharField(max_length=100)
|
|
|
|
short_name = models.CharField(max_length=100)
|
2012-08-28 18:44:51 +02:00
|
|
|
pointer = models.IntegerField()
|
2012-10-17 17:42:40 +02:00
|
|
|
last_pointer_updater = models.CharField(max_length=64)
|
2012-09-05 21:49:56 +02:00
|
|
|
realm = models.ForeignKey(Realm)
|
2012-10-01 21:36:44 +02:00
|
|
|
api_key = models.CharField(max_length=32)
|
2012-11-23 21:23:41 +01:00
|
|
|
enable_desktop_notifications = models.BooleanField(default=True)
|
2013-05-03 21:49:01 +02:00
|
|
|
enable_sounds = models.BooleanField(default=True)
|
2013-02-27 23:18:38 +01:00
|
|
|
enter_sends = models.NullBooleanField(default=False)
|
2013-05-07 23:15:48 +02:00
|
|
|
enable_offline_email_notifications = models.BooleanField(default=True)
|
2013-05-23 20:33:57 +02:00
|
|
|
last_reminder = models.DateTimeField(default=timezone.now, null=True)
|
2013-05-30 22:37:34 +02:00
|
|
|
rate_limits = models.CharField(default="", max_length=100) # comma-separated list of range:max pairs
|
2012-08-28 18:44:51 +02:00
|
|
|
|
2013-05-17 21:28:51 +02:00
|
|
|
# Hours to wait before sending another email to a user
|
|
|
|
EMAIL_REMINDER_WAITPERIOD = 24
|
|
|
|
|
2013-06-07 21:51:57 +02:00
|
|
|
AVATAR_FROM_GRAVATAR = 'G'
|
|
|
|
AVATAR_FROM_USER = 'U'
|
|
|
|
AVATAR_FROM_SYSTEM = 'S'
|
|
|
|
AVATAR_SOURCES = (
|
|
|
|
(AVATAR_FROM_GRAVATAR, 'Hosted by Gravatar'),
|
|
|
|
(AVATAR_FROM_USER, 'Uploaded by user'),
|
|
|
|
(AVATAR_FROM_SYSTEM, 'System generated'),
|
|
|
|
)
|
|
|
|
avatar_source = models.CharField(default=AVATAR_FROM_GRAVATAR, choices=AVATAR_SOURCES, max_length=1)
|
|
|
|
|
2013-04-04 22:30:28 +02:00
|
|
|
TUTORIAL_WAITING = 'W'
|
|
|
|
TUTORIAL_STARTED = 'S'
|
|
|
|
TUTORIAL_FINISHED = 'F'
|
|
|
|
TUTORIAL_STATES = ((TUTORIAL_WAITING, "Waiting"),
|
|
|
|
(TUTORIAL_STARTED, "Started"),
|
|
|
|
(TUTORIAL_FINISHED, "Finished"))
|
|
|
|
|
|
|
|
tutorial_status = models.CharField(default=TUTORIAL_WAITING, choices=TUTORIAL_STATES, max_length=1)
|
2013-05-08 16:12:19 +02:00
|
|
|
# Contains serialized JSON of the form:
|
|
|
|
# [("step 1", true), ("step 2", false)]
|
|
|
|
# where the second element of each tuple is if the step has been
|
|
|
|
# completed.
|
2013-06-18 23:55:55 +02:00
|
|
|
onboarding_steps = models.TextField(default=ujson.dumps([]))
|
2013-04-04 22:30:28 +02:00
|
|
|
|
|
|
|
def tutorial_stream_name(self):
|
2013-04-23 22:04:28 +02:00
|
|
|
# If you change this, you need to change the corresponding
|
|
|
|
# client-computed version of it in tutorial.js
|
|
|
|
long_name = "tutorial-%s" % (self.email.split('@')[0],)
|
|
|
|
short_name = long_name[:Stream.MAX_NAME_LENGTH]
|
|
|
|
return short_name
|
2013-04-04 22:30:28 +02:00
|
|
|
|
2013-03-08 19:53:00 +01:00
|
|
|
objects = UserManager()
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
|
2013-03-08 19:53:00 +01:00
|
|
|
def __repr__(self):
|
2013-03-28 20:43:34 +01:00
|
|
|
return (u"<UserProfile: %s %s>" % (self.email, self.realm)).encode("utf-8")
|
2013-03-08 19:53:00 +01:00
|
|
|
def __str__(self):
|
|
|
|
return self.__repr__()
|
|
|
|
|
2013-03-15 21:17:32 +01:00
|
|
|
# Make sure we flush the UserProfile object from our memcached
|
|
|
|
# whenever we save it.
|
|
|
|
post_save.connect(update_user_profile_cache, sender=UserProfile)
|
|
|
|
|
2012-09-28 22:47:05 +02:00
|
|
|
class PreregistrationUser(models.Model):
|
2012-12-11 23:42:32 +01:00
|
|
|
email = models.EmailField()
|
|
|
|
referred_by = models.ForeignKey(UserProfile, null=True)
|
|
|
|
streams = models.ManyToManyField('Stream', null=True)
|
|
|
|
invited_at = models.DateTimeField(auto_now=True)
|
|
|
|
|
2012-10-29 19:08:18 +01:00
|
|
|
# status: whether an object has been confirmed.
|
|
|
|
# if confirmed, set to confirmation.settings.STATUS_ACTIVE
|
|
|
|
status = models.IntegerField(default=0)
|
|
|
|
|
|
|
|
class MitUser(models.Model):
|
|
|
|
email = models.EmailField(unique=True)
|
|
|
|
# status: whether an object has been confirmed.
|
|
|
|
# if confirmed, set to confirmation.settings.STATUS_ACTIVE
|
2012-09-28 22:47:05 +02:00
|
|
|
status = models.IntegerField(default=0)
|
|
|
|
|
2012-10-10 22:53:24 +02:00
|
|
|
class Stream(models.Model):
|
2013-04-23 22:04:28 +02:00
|
|
|
# If you change this, you also need to change the
|
|
|
|
# corresponding value in tutorial.js
|
2013-03-20 01:16:41 +01:00
|
|
|
MAX_NAME_LENGTH = 30
|
|
|
|
name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True)
|
2012-09-14 23:28:38 +02:00
|
|
|
realm = models.ForeignKey(Realm, db_index=True)
|
2013-01-23 20:36:32 +01:00
|
|
|
invite_only = models.NullBooleanField(default=False)
|
2012-08-28 18:44:51 +02:00
|
|
|
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
def __repr__(self):
|
2013-03-26 19:46:47 +01:00
|
|
|
return (u"<Stream: %s>" % (self.name,)).encode("utf-8")
|
2012-09-07 17:04:41 +02:00
|
|
|
def __str__(self):
|
|
|
|
return self.__repr__()
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
|
2013-01-15 21:10:50 +01:00
|
|
|
def is_public(self):
|
2013-05-06 21:33:36 +02:00
|
|
|
# For every realm except for legacy realms on prod (aka those
|
|
|
|
# older than realm id 68 with some exceptions), we enable
|
|
|
|
# historical messages for all streams that are not invite-only.
|
|
|
|
return ((not settings.DEPLOYED or self.realm.domain in
|
|
|
|
["humbughq.com"] or self.realm.id > 68)
|
2013-04-30 16:52:13 +02:00
|
|
|
and not self.invite_only)
|
2013-01-15 21:10:50 +01:00
|
|
|
|
2012-11-07 22:33:38 +01:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("name", "realm")
|
|
|
|
|
2012-09-19 18:54:57 +02:00
|
|
|
@classmethod
|
|
|
|
def create(cls, name, realm):
|
2012-10-10 22:53:24 +02:00
|
|
|
stream = cls(name=name, realm=realm)
|
|
|
|
stream.save()
|
2012-09-07 19:24:54 +02:00
|
|
|
|
2012-10-19 23:40:44 +02:00
|
|
|
recipient = Recipient.objects.create(type_id=stream.id,
|
|
|
|
type=Recipient.STREAM)
|
2012-10-10 22:53:24 +02:00
|
|
|
return (stream, recipient)
|
2012-09-07 19:24:54 +02:00
|
|
|
|
2013-03-18 18:57:34 +01:00
|
|
|
def valid_stream_name(name):
|
|
|
|
return name != ""
|
|
|
|
|
2012-08-28 21:27:42 +02:00
|
|
|
class Recipient(models.Model):
|
2012-09-14 23:28:38 +02:00
|
|
|
type_id = models.IntegerField(db_index=True)
|
|
|
|
type = models.PositiveSmallIntegerField(db_index=True)
|
2012-10-10 22:58:51 +02:00
|
|
|
# Valid types are {personal, stream, huddle}
|
2012-09-07 20:14:13 +02:00
|
|
|
PERSONAL = 1
|
2012-10-10 22:57:21 +02:00
|
|
|
STREAM = 2
|
2012-09-07 20:14:13 +02:00
|
|
|
HUDDLE = 3
|
|
|
|
|
2012-11-07 22:33:38 +01:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("type", "type_id")
|
|
|
|
|
2012-11-02 21:08:29 +01:00
|
|
|
# N.B. If we used Django's choice=... we would get this for free (kinda)
|
|
|
|
_type_names = {
|
|
|
|
PERSONAL: 'personal',
|
|
|
|
STREAM: 'stream',
|
|
|
|
HUDDLE: 'huddle' }
|
|
|
|
|
2012-09-07 20:14:13 +02:00
|
|
|
def type_name(self):
|
2012-11-02 21:08:29 +01:00
|
|
|
# Raises KeyError if invalid
|
|
|
|
return self._type_names[self.type]
|
2012-08-28 21:27:42 +02:00
|
|
|
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
def __repr__(self):
|
|
|
|
display_recipient = get_display_recipient(self)
|
2013-03-26 19:46:47 +01:00
|
|
|
return (u"<Recipient: %s (%d, %s)>" % (display_recipient, self.type_id, self.type)).encode("utf-8")
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
|
2012-10-19 21:30:42 +02:00
|
|
|
class Client(models.Model):
|
2012-10-22 19:23:11 +02:00
|
|
|
name = models.CharField(max_length=30, db_index=True, unique=True)
|
2012-10-19 21:30:42 +02:00
|
|
|
|
2013-03-26 17:47:52 +01:00
|
|
|
def get_client_cache_key(name):
|
|
|
|
return 'get_client:%s' % (make_safe_digest(name),)
|
|
|
|
|
2013-03-26 19:09:45 +01:00
|
|
|
@cache_with_key(get_client_cache_key, timeout=3600*24*7)
|
2012-10-23 23:29:56 +02:00
|
|
|
@transaction.commit_on_success
|
2012-10-19 21:30:42 +02:00
|
|
|
def get_client(name):
|
2012-10-23 23:29:56 +02:00
|
|
|
try:
|
|
|
|
(client, _) = Client.objects.get_or_create(name=name)
|
|
|
|
except IntegrityError:
|
2012-11-08 00:11:31 +01:00
|
|
|
# If we're racing with other threads trying to create this
|
|
|
|
# client, get_or_create will throw IntegrityError (because our
|
|
|
|
# database is enforcing the no-duplicate-objects constraint);
|
|
|
|
# in this case one should just re-fetch the object. This race
|
|
|
|
# actually happens with populate_db.
|
|
|
|
#
|
|
|
|
# Much of the rest of our code that writes to the database
|
|
|
|
# doesn't handle this duplicate object on race issue correctly :(
|
2012-10-23 23:29:56 +02:00
|
|
|
transaction.commit()
|
|
|
|
return Client.objects.get(name=name)
|
2012-10-19 21:30:42 +02:00
|
|
|
return client
|
|
|
|
|
2013-03-18 16:40:00 +01:00
|
|
|
def get_stream_cache_key(stream_name, realm):
|
|
|
|
if isinstance(realm, Realm):
|
|
|
|
realm_id = realm.id
|
|
|
|
else:
|
|
|
|
realm_id = realm
|
2013-03-20 15:31:27 +01:00
|
|
|
return "stream_by_realm_and_name:%s:%s" % (
|
|
|
|
realm_id, make_safe_digest(stream_name.strip().lower()))
|
2013-03-18 16:40:00 +01:00
|
|
|
|
2013-03-19 13:05:19 +01:00
|
|
|
# get_stream_backend takes either a realm id or a realm
|
2013-03-26 19:09:45 +01:00
|
|
|
@cache_with_key(get_stream_cache_key, timeout=3600*24*7)
|
2013-03-19 13:05:19 +01:00
|
|
|
def get_stream_backend(stream_name, realm):
|
2013-01-17 22:16:39 +01:00
|
|
|
if isinstance(realm, Realm):
|
|
|
|
realm_id = realm.id
|
|
|
|
else:
|
|
|
|
realm_id = realm
|
2013-03-19 13:05:19 +01:00
|
|
|
return Stream.objects.select_related("realm").get(
|
|
|
|
name__iexact=stream_name.strip(), realm_id=realm_id)
|
|
|
|
|
|
|
|
# get_stream takes either a realm id or a realm
|
|
|
|
def get_stream(stream_name, realm):
|
2013-01-17 22:16:39 +01:00
|
|
|
try:
|
2013-03-19 13:05:19 +01:00
|
|
|
return get_stream_backend(stream_name, realm)
|
2013-01-17 22:16:39 +01:00
|
|
|
except Stream.DoesNotExist:
|
|
|
|
return None
|
|
|
|
|
2013-03-26 17:10:44 +01:00
|
|
|
def get_recipient_cache_key(type, type_id):
|
|
|
|
return "get_recipient:%s:%s" % (type, type_id,)
|
|
|
|
|
2013-03-26 19:09:45 +01:00
|
|
|
@cache_with_key(get_recipient_cache_key, timeout=3600*24*7)
|
2013-03-18 16:54:58 +01:00
|
|
|
def get_recipient(type, type_id):
|
|
|
|
return Recipient.objects.get(type_id=type_id, type=type)
|
|
|
|
|
2013-02-02 00:56:37 +01:00
|
|
|
# NB: This function is currently unused, but may come in handy.
|
2012-12-10 21:39:28 +01:00
|
|
|
def linebreak(string):
|
|
|
|
return string.replace('\n\n', '<p/>').replace('\n', '<br/>')
|
|
|
|
|
2013-06-18 20:02:45 +02:00
|
|
|
def extract_message_dict(message_str):
|
2013-06-19 21:41:29 +02:00
|
|
|
return ujson.loads(zlib.decompress(message_str))
|
2013-06-18 20:02:45 +02:00
|
|
|
|
|
|
|
def stringify_message_dict(message_dict):
|
2013-06-19 21:41:29 +02:00
|
|
|
return zlib.compress(ujson.dumps(message_dict))
|
2013-06-18 20:02:45 +02:00
|
|
|
|
2013-05-02 18:51:09 +02:00
|
|
|
def to_dict_cache_key_id(message_id, apply_markdown, rendered_content=None):
|
|
|
|
return 'message_dict:%d:%d' % (message_id, apply_markdown)
|
|
|
|
|
2013-04-22 16:29:57 +02:00
|
|
|
def to_dict_cache_key(message, apply_markdown, rendered_content=None):
|
2013-05-02 18:51:09 +02:00
|
|
|
return to_dict_cache_key_id(message.id, apply_markdown, rendered_content=None)
|
2013-04-22 16:29:57 +02:00
|
|
|
|
2012-10-03 21:05:48 +02:00
|
|
|
class Message(models.Model):
|
2012-08-28 18:44:51 +02:00
|
|
|
sender = models.ForeignKey(UserProfile)
|
2012-09-04 23:20:21 +02:00
|
|
|
recipient = models.ForeignKey(Recipient)
|
2012-12-07 23:21:26 +01:00
|
|
|
subject = models.CharField(max_length=MAX_SUBJECT_LENGTH, db_index=True)
|
2012-09-14 19:16:01 +02:00
|
|
|
content = models.TextField()
|
2013-03-18 22:51:08 +01:00
|
|
|
rendered_content = models.TextField(null=True)
|
|
|
|
rendered_content_version = models.IntegerField(null=True)
|
2012-12-07 23:21:26 +01:00
|
|
|
pub_date = models.DateTimeField('date published', db_index=True)
|
2012-10-19 21:30:42 +02:00
|
|
|
sending_client = models.ForeignKey(Client)
|
2013-05-21 17:48:46 +02:00
|
|
|
last_edit_time = models.DateTimeField(null=True)
|
|
|
|
edit_history = models.TextField(null=True)
|
2012-08-28 18:44:51 +02:00
|
|
|
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
def __repr__(self):
|
|
|
|
display_recipient = get_display_recipient(self.recipient)
|
2013-03-26 19:46:47 +01:00
|
|
|
return (u"<Message: %s / %s / %r>" % (display_recipient, self.subject, self.sender)).encode("utf-8")
|
2012-09-07 17:04:41 +02:00
|
|
|
def __str__(self):
|
|
|
|
return self.__repr__()
|
Give our models meaningful reprs.
>>> from zephyr.models import UserProfile, Recipient, Zephyr, ZephyrClass
>>> for klass in [UserProfile, Recipient, Zephyr, ZephyrClass]:
... print klass.objects.all()[:2]
...
[<UserProfile: othello>, <UserProfile: iago>]
[<Recipient: Verona (1, class)>, <Recipient: Denmark (2, class)>]
[<Zephyr: Scotland / Scotland3 / <UserProfile: prospero>>, <Zephyr: Venice / Venice3 / <UserProfile: iago>>]
[<ZephyrClass: Verona>, <ZephyrClass: Denmark>]
(imported from commit 9998ffe40800213a5425990d6e85f5c5a43a5355)
2012-08-29 16:15:06 +02:00
|
|
|
|
2013-03-08 20:40:39 +01:00
|
|
|
def to_dict(self, apply_markdown, rendered_content=None):
|
2013-06-18 20:02:45 +02:00
|
|
|
return extract_message_dict(self.to_dict_json(apply_markdown, rendered_content))
|
|
|
|
|
|
|
|
@cache_with_key(to_dict_cache_key, timeout=3600*24)
|
|
|
|
def to_dict_json(self, apply_markdown, rendered_content=None):
|
|
|
|
return stringify_message_dict(self.to_dict_uncached(apply_markdown, rendered_content))
|
2013-04-25 20:41:54 +02:00
|
|
|
|
|
|
|
def to_dict_uncached(self, apply_markdown, rendered_content=None):
|
2012-12-03 19:49:12 +01:00
|
|
|
display_recipient = get_display_recipient(self.recipient)
|
|
|
|
if self.recipient.type == Recipient.STREAM:
|
|
|
|
display_type = "stream"
|
|
|
|
elif self.recipient.type in (Recipient.HUDDLE, Recipient.PERSONAL):
|
|
|
|
display_type = "private"
|
|
|
|
if len(display_recipient) == 1:
|
|
|
|
# add the sender in if this isn't a message between
|
|
|
|
# someone and his self, preserving ordering
|
2013-03-28 20:43:34 +01:00
|
|
|
recip = {'email': self.sender.email,
|
2013-05-02 18:56:42 +02:00
|
|
|
'domain': self.sender.realm.domain,
|
2012-12-03 19:49:12 +01:00
|
|
|
'full_name': self.sender.full_name,
|
|
|
|
'short_name': self.sender.short_name};
|
|
|
|
if recip['email'] < display_recipient[0]['email']:
|
|
|
|
display_recipient = [recip, display_recipient[0]]
|
|
|
|
elif recip['email'] > display_recipient[0]['email']:
|
|
|
|
display_recipient = [display_recipient[0], recip]
|
|
|
|
else:
|
|
|
|
display_type = self.recipient.type_name()
|
|
|
|
|
2012-10-24 20:16:26 +02:00
|
|
|
obj = dict(
|
|
|
|
id = self.id,
|
2013-03-28 20:43:34 +01:00
|
|
|
sender_email = self.sender.email,
|
2012-10-24 20:16:26 +02:00
|
|
|
sender_full_name = self.sender.full_name,
|
|
|
|
sender_short_name = self.sender.short_name,
|
2013-05-02 18:56:42 +02:00
|
|
|
sender_domain = self.sender.realm.domain,
|
2012-12-03 19:49:12 +01:00
|
|
|
type = display_type,
|
|
|
|
display_recipient = display_recipient,
|
2012-10-24 20:16:26 +02:00
|
|
|
recipient_id = self.recipient.id,
|
|
|
|
subject = self.subject,
|
2012-12-11 23:08:17 +01:00
|
|
|
timestamp = datetime_to_timestamp(self.pub_date),
|
2013-06-10 21:35:48 +02:00
|
|
|
gravatar_hash = gravatar_hash(self.sender.email), # Deprecated June 2013
|
|
|
|
avatar_url = avatar_url(self.sender),
|
2013-02-21 20:05:18 +01:00
|
|
|
client = self.sending_client.name)
|
2012-10-24 20:16:26 +02:00
|
|
|
|
2013-05-21 17:48:46 +02:00
|
|
|
if self.last_edit_time != None:
|
|
|
|
obj['last_edit_timestamp'] = datetime_to_timestamp(self.last_edit_time)
|
2013-06-18 23:55:55 +02:00
|
|
|
obj['edit_history'] = ujson.loads(self.edit_history)
|
2013-03-18 22:51:08 +01:00
|
|
|
if apply_markdown and self.rendered_content_version is not None:
|
|
|
|
obj['content'] = self.rendered_content
|
|
|
|
obj['content_type'] = 'text/html'
|
|
|
|
elif apply_markdown:
|
2013-03-08 20:34:10 +01:00
|
|
|
if rendered_content is None:
|
2013-06-05 17:45:57 +02:00
|
|
|
rendered_content = bugdown.convert(self.content, self.sender.realm.domain)
|
2013-03-08 20:40:39 +01:00
|
|
|
if rendered_content is None:
|
|
|
|
rendered_content = '<p>[Humbug note: Sorry, we could not understand the formatting of your message]</p>'
|
2013-03-21 21:22:37 +01:00
|
|
|
|
|
|
|
# Update the database cache of the rendered content
|
|
|
|
self.rendered_content = rendered_content
|
|
|
|
self.rendered_content_version = bugdown.version
|
2013-03-21 21:29:28 +01:00
|
|
|
self.save(update_fields=["rendered_content", "rendered_content_version"])
|
2013-03-08 20:34:10 +01:00
|
|
|
obj['content'] = rendered_content
|
2012-10-24 20:51:45 +02:00
|
|
|
obj['content_type'] = 'text/html'
|
2012-10-04 00:13:03 +02:00
|
|
|
else:
|
2012-10-24 20:16:26 +02:00
|
|
|
obj['content'] = self.content
|
2012-10-24 20:51:45 +02:00
|
|
|
obj['content_type'] = 'text/x-markdown'
|
2012-10-24 20:16:26 +02:00
|
|
|
|
|
|
|
return obj
|
2012-08-30 19:56:15 +02:00
|
|
|
|
2012-09-27 19:58:42 +02:00
|
|
|
def to_log_dict(self):
|
2012-10-24 20:16:26 +02:00
|
|
|
return dict(
|
|
|
|
id = self.id,
|
2013-03-28 20:43:34 +01:00
|
|
|
sender_email = self.sender.email,
|
2013-05-02 18:56:42 +02:00
|
|
|
sender_domain = self.sender.realm.domain,
|
2012-10-24 20:16:26 +02:00
|
|
|
sender_full_name = self.sender.full_name,
|
|
|
|
sender_short_name = self.sender.short_name,
|
|
|
|
sending_client = self.sending_client.name,
|
|
|
|
type = self.recipient.type_name(),
|
2012-12-03 19:49:12 +01:00
|
|
|
recipient = get_display_recipient(self.recipient),
|
2012-10-24 20:16:26 +02:00
|
|
|
subject = self.subject,
|
|
|
|
content = self.content,
|
2012-12-11 23:08:17 +01:00
|
|
|
timestamp = datetime_to_timestamp(self.pub_date))
|
2012-09-27 19:58:42 +02:00
|
|
|
|
2012-11-28 00:50:33 +01:00
|
|
|
@classmethod
|
|
|
|
def remove_unreachable(cls):
|
|
|
|
"""Remove all Messages that are not referred to by any UserMessage."""
|
|
|
|
cls.objects.exclude(id__in = UserMessage.objects.values('message_id')).delete()
|
|
|
|
|
2012-09-07 17:04:41 +02:00
|
|
|
class UserMessage(models.Model):
|
|
|
|
user_profile = models.ForeignKey(UserProfile)
|
2012-10-03 21:05:48 +02:00
|
|
|
message = models.ForeignKey(Message)
|
2012-09-07 17:04:41 +02:00
|
|
|
# We're not using the archived field for now, but create it anyway
|
|
|
|
# since this table will be an unpleasant one to do schema changes
|
|
|
|
# on later
|
|
|
|
archived = models.BooleanField()
|
2013-05-31 16:15:27 +02:00
|
|
|
flags = BitField(flags=['read', 'starred', 'collapsed', 'mentioned', 'wildcard_mentioned'], default=0)
|
2012-09-07 17:04:41 +02:00
|
|
|
|
2012-11-08 21:08:13 +01:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("user_profile", "message")
|
|
|
|
|
2012-09-07 17:04:41 +02:00
|
|
|
def __repr__(self):
|
|
|
|
display_recipient = get_display_recipient(self.message.recipient)
|
2013-03-28 20:43:34 +01:00
|
|
|
return (u"<UserMessage: %s / %s (%s)>" % (display_recipient, self.user_profile.email, self.flags_dict())).encode("utf-8")
|
2012-09-07 17:04:41 +02:00
|
|
|
|
2013-06-19 21:05:22 +02:00
|
|
|
def flags_list(self):
|
|
|
|
return [flag for flag in self.flags.keys() if getattr(self.flags, flag).is_set]
|
2013-03-11 15:47:29 +01:00
|
|
|
|
2013-06-19 21:05:22 +02:00
|
|
|
def flags_dict(self):
|
|
|
|
return dict(flags = [self.flags_list()])
|
2013-03-11 15:47:29 +01:00
|
|
|
|
2012-08-29 17:50:36 +02:00
|
|
|
class Subscription(models.Model):
|
2012-10-22 20:15:25 +02:00
|
|
|
user_profile = models.ForeignKey(UserProfile)
|
2012-09-05 21:55:40 +02:00
|
|
|
recipient = models.ForeignKey(Recipient)
|
2012-08-30 18:03:58 +02:00
|
|
|
active = models.BooleanField(default=True)
|
2013-01-15 22:31:46 +01:00
|
|
|
in_home_view = models.NullBooleanField(default=True)
|
2012-08-29 17:50:36 +02:00
|
|
|
|
2013-03-29 20:57:02 +01:00
|
|
|
DEFAULT_STREAM_COLOR = "#c2c2c2"
|
|
|
|
color = models.CharField(max_length=10, default=DEFAULT_STREAM_COLOR)
|
2013-04-08 17:29:33 +02:00
|
|
|
notifications = models.BooleanField(default=False)
|
2013-03-29 20:57:02 +01:00
|
|
|
|
2012-11-07 22:33:38 +01:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("user_profile", "recipient")
|
|
|
|
|
2012-08-29 17:50:36 +02:00
|
|
|
def __repr__(self):
|
2013-03-26 19:46:47 +01:00
|
|
|
return (u"<Subscription: %r -> %s>" % (self.user_profile, self.recipient)).encode("utf-8")
|
2012-09-07 17:04:41 +02:00
|
|
|
def __str__(self):
|
|
|
|
return self.__repr__()
|
2012-08-28 22:56:21 +02:00
|
|
|
|
2013-03-26 19:09:45 +01:00
|
|
|
@cache_with_key(user_profile_by_id_cache_key, timeout=3600*24*7)
|
2013-03-26 18:51:55 +01:00
|
|
|
def get_user_profile_by_id(uid):
|
|
|
|
return UserProfile.objects.select_related().get(id=uid)
|
|
|
|
|
2013-03-28 20:20:31 +01:00
|
|
|
@cache_with_key(user_profile_by_email_cache_key, timeout=3600*24*7)
|
|
|
|
def get_user_profile_by_email(email):
|
2013-03-28 20:43:34 +01:00
|
|
|
return UserProfile.objects.select_related().get(email__iexact=email)
|
2013-03-28 20:20:31 +01:00
|
|
|
|
2013-04-08 18:27:07 +02:00
|
|
|
def get_prereg_user_by_email(email):
|
|
|
|
# A user can be invited many times, so only return the result of the latest
|
|
|
|
# invite.
|
|
|
|
return PreregistrationUser.objects.filter(email__iexact=email).latest("invited_at")
|
|
|
|
|
2012-09-04 23:20:21 +02:00
|
|
|
class Huddle(models.Model):
|
2012-09-07 20:14:13 +02:00
|
|
|
# TODO: We should consider whether using
|
|
|
|
# CommaSeparatedIntegerField would be better.
|
2012-10-22 19:23:11 +02:00
|
|
|
huddle_hash = models.CharField(max_length=40, db_index=True, unique=True)
|
2012-09-04 23:20:21 +02:00
|
|
|
|
2012-10-20 18:02:58 +02:00
|
|
|
def get_huddle_hash(id_list):
|
2012-09-05 17:38:09 +02:00
|
|
|
id_list = sorted(set(id_list))
|
2012-09-05 17:41:53 +02:00
|
|
|
hash_key = ",".join(str(x) for x in id_list)
|
2013-03-20 15:31:27 +01:00
|
|
|
return make_safe_digest(hash_key)
|
2012-10-20 18:02:58 +02:00
|
|
|
|
2013-03-26 18:17:55 +01:00
|
|
|
def huddle_hash_cache_key(huddle_hash):
|
|
|
|
return "huddle_by_hash:%s" % (huddle_hash,)
|
|
|
|
|
2012-10-20 18:02:58 +02:00
|
|
|
def get_huddle(id_list):
|
|
|
|
huddle_hash = get_huddle_hash(id_list)
|
2013-03-26 18:17:55 +01:00
|
|
|
return get_huddle_backend(huddle_hash, id_list)
|
|
|
|
|
2013-03-26 19:09:45 +01:00
|
|
|
@cache_with_key(lambda huddle_hash, id_list: huddle_hash_cache_key(huddle_hash), timeout=3600*24*7)
|
2013-03-26 18:17:55 +01:00
|
|
|
def get_huddle_backend(huddle_hash, id_list):
|
2012-10-19 23:40:44 +02:00
|
|
|
(huddle, created) = Huddle.objects.get_or_create(huddle_hash=huddle_hash)
|
|
|
|
if created:
|
2013-03-26 18:51:55 +01:00
|
|
|
with transaction.commit_on_success():
|
|
|
|
recipient = Recipient.objects.create(type_id=huddle.id,
|
|
|
|
type=Recipient.HUDDLE)
|
|
|
|
subs_to_create = [Subscription(recipient=recipient,
|
|
|
|
user_profile=get_user_profile_by_id(user_profile_id))
|
|
|
|
for user_profile_id in id_list]
|
|
|
|
Subscription.objects.bulk_create(subs_to_create)
|
2012-10-19 23:40:44 +02:00
|
|
|
return huddle
|
2012-09-04 23:20:21 +02:00
|
|
|
|
2013-05-31 20:04:18 +02:00
|
|
|
def get_realm(domain):
|
|
|
|
try:
|
|
|
|
return Realm.objects.get(domain__iexact=domain.strip())
|
|
|
|
except Realm.DoesNotExist:
|
|
|
|
return None
|
|
|
|
|
2012-10-31 18:48:11 +01:00
|
|
|
# This function is used only by tests.
|
|
|
|
# We have faster implementations within the app itself.
|
2013-03-29 19:37:21 +01:00
|
|
|
def filter_by_subscriptions(messages, user_profile):
|
2012-10-03 21:29:38 +02:00
|
|
|
user_messages = []
|
2012-09-07 19:53:24 +02:00
|
|
|
subscriptions = [sub.recipient for sub in
|
2012-10-22 20:15:25 +02:00
|
|
|
Subscription.objects.filter(user_profile=user_profile, active=True)]
|
2012-10-03 21:29:38 +02:00
|
|
|
for message in messages:
|
2012-10-10 22:58:51 +02:00
|
|
|
# If you are subscribed to the personal or stream, or if you
|
2012-10-03 21:29:38 +02:00
|
|
|
# sent the personal, you can see the message.
|
|
|
|
if (message.recipient in subscriptions) or \
|
|
|
|
(message.recipient.type == Recipient.PERSONAL and
|
2012-10-22 20:15:25 +02:00
|
|
|
message.sender == user_profile):
|
2012-10-03 21:29:38 +02:00
|
|
|
user_messages.append(message)
|
2012-08-28 22:56:21 +02:00
|
|
|
|
2012-10-03 21:29:38 +02:00
|
|
|
return user_messages
|
2012-10-29 19:43:00 +01:00
|
|
|
|
|
|
|
def clear_database():
|
2013-04-01 16:57:50 +02:00
|
|
|
for model in [Message, Stream, UserProfile, Recipient,
|
2012-11-27 18:26:51 +01:00
|
|
|
Realm, Subscription, Huddle, UserMessage, Client,
|
|
|
|
DefaultStream]:
|
2012-10-29 19:43:00 +01:00
|
|
|
model.objects.all().delete()
|
|
|
|
Session.objects.all().delete()
|
2012-11-08 23:02:16 +01:00
|
|
|
|
|
|
|
class UserActivity(models.Model):
|
|
|
|
user_profile = models.ForeignKey(UserProfile)
|
|
|
|
client = models.ForeignKey(Client)
|
|
|
|
query = models.CharField(max_length=50, db_index=True)
|
|
|
|
|
|
|
|
count = models.IntegerField()
|
|
|
|
last_visit = models.DateTimeField('last visit')
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
unique_together = ("user_profile", "client", "query")
|
2012-11-27 18:26:51 +01:00
|
|
|
|
2013-02-08 23:44:15 +01:00
|
|
|
class UserPresence(models.Model):
|
|
|
|
user_profile = models.ForeignKey(UserProfile)
|
|
|
|
client = models.ForeignKey(Client)
|
|
|
|
|
|
|
|
# Valid statuses
|
|
|
|
ACTIVE = 1
|
|
|
|
IDLE = 2
|
|
|
|
|
|
|
|
timestamp = models.DateTimeField('presence changed')
|
|
|
|
status = models.PositiveSmallIntegerField(default=ACTIVE)
|
|
|
|
|
2013-04-03 22:00:02 +02:00
|
|
|
def to_dict(self):
|
|
|
|
if self.status == UserPresence.ACTIVE:
|
|
|
|
presence_val = 'active'
|
|
|
|
elif self.status == UserPresence.IDLE:
|
|
|
|
presence_val = 'idle'
|
|
|
|
|
2013-04-05 00:13:03 +02:00
|
|
|
return {'client' : self.client.name,
|
|
|
|
'status' : presence_val,
|
2013-04-03 22:00:02 +02:00
|
|
|
'timestamp': datetime_to_timestamp(self.timestamp)}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def status_from_string(status):
|
|
|
|
if status == 'active':
|
|
|
|
status_val = UserPresence.ACTIVE
|
|
|
|
elif status == 'idle':
|
|
|
|
status_val = UserPresence.IDLE
|
|
|
|
else:
|
|
|
|
status_val = None
|
|
|
|
|
|
|
|
return status_val
|
|
|
|
|
2013-02-08 23:44:15 +01:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("user_profile", "client")
|
|
|
|
|
2013-04-05 00:13:03 +02:00
|
|
|
# Flush the cached user status_dict whenever a user's presence
|
|
|
|
# changes
|
|
|
|
post_save.connect(update_user_presence_cache, sender=UserPresence)
|
|
|
|
|
2012-11-27 18:26:51 +01:00
|
|
|
class DefaultStream(models.Model):
|
|
|
|
realm = models.ForeignKey(Realm)
|
|
|
|
stream = models.ForeignKey(Stream)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
unique_together = ("realm", "stream")
|
2012-12-01 04:35:59 +01:00
|
|
|
|
2012-12-05 22:23:45 +01:00
|
|
|
# FIXME: The foreign key relationship here is backwards.
|
|
|
|
#
|
|
|
|
# We can't easily get a list of streams and their associated colors (if any) in
|
|
|
|
# a single query. See zephyr.views.gather_subscriptions for an example.
|
|
|
|
#
|
|
|
|
# We should change things around so that is possible. Probably this should
|
|
|
|
# just be a column on Subscription.
|
2012-12-01 04:35:59 +01:00
|
|
|
class StreamColor(models.Model):
|
2013-01-28 23:06:35 +01:00
|
|
|
DEFAULT_STREAM_COLOR = "#c2c2c2"
|
|
|
|
|
2012-12-01 04:35:59 +01:00
|
|
|
subscription = models.ForeignKey(Subscription)
|
|
|
|
color = models.CharField(max_length=10)
|