Files
zulip/zephyr/models.py
Reid Barton 7b7f6390f9 Add a notion of "public" stream
A public stream is one for which any user can view all messages sent
to the stream, regardless of whether the user was subscribed when
those messages were sent.

For now, to avoid a database schema change and to facilitate testing,
public streams are all streams on the customer29.invalid or
humbughq.com realms.

(imported from commit 7a71fd788d585a6f5b3e494e771ec85b632bb36e)
2013-01-16 14:19:05 -05:00

344 lines
13 KiB
Python

from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
import hashlib
from zephyr.lib.cache import cache_with_key
from zephyr.lib.initial_password import initial_api_key
import os
from django.db import transaction, IntegrityError
from zephyr.lib import bugdown
from zephyr.lib.avatar import gravatar_hash
from django.utils import timezone
from django.contrib.sessions.models import Session
from django.utils.html import escape
from zephyr.lib.timestamp import datetime_to_timestamp
MAX_SUBJECT_LENGTH = 60
MAX_MESSAGE_LENGTH = 10000
@cache_with_key(lambda self: 'display_recipient_dict:%d' % (self.id,))
def get_display_recipient(recipient):
"""
recipient: an instance of Recipient.
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.
"""
if recipient.type == Recipient.STREAM:
stream = Stream.objects.get(id=recipient.type_id)
return stream.name
# 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()
.order_by('user__email'))
return [{'email': user_profile.user.email,
'full_name': user_profile.full_name,
'short_name': user_profile.short_name} for user_profile in user_profile_list]
class Realm(models.Model):
domain = models.CharField(max_length=40, db_index=True, unique=True)
def __repr__(self):
return "<Realm: %s %s>" % (self.domain, self.id)
def __str__(self):
return self.__repr__()
class UserProfile(models.Model):
user = models.OneToOneField(User)
full_name = models.CharField(max_length=100)
short_name = models.CharField(max_length=100)
pointer = models.IntegerField()
last_pointer_updater = models.CharField(max_length=64)
realm = models.ForeignKey(Realm)
api_key = models.CharField(max_length=32)
enable_desktop_notifications = models.BooleanField(default=True)
def __repr__(self):
return "<UserProfile: %s %s>" % (self.user.email, self.realm)
def __str__(self):
return self.__repr__()
@classmethod
def create(cls, user, realm, full_name, short_name):
"""When creating a new user, make a profile for him or her."""
if not cls.objects.filter(user=user):
profile = cls(user=user, pointer=-1, realm=realm,
full_name=full_name, short_name=short_name)
profile.api_key = initial_api_key(user.email)
profile.save()
# Auto-sub to the ability to receive personals.
recipient = Recipient.objects.create(type_id=profile.id, type=Recipient.PERSONAL)
Subscription.objects.create(user_profile=profile, recipient=recipient)
return profile
class PreregistrationUser(models.Model):
email = models.EmailField()
referred_by = models.ForeignKey(UserProfile, null=True)
streams = models.ManyToManyField('Stream', null=True)
invited_at = models.DateTimeField(auto_now=True)
# 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
status = models.IntegerField(default=0)
class Stream(models.Model):
name = models.CharField(max_length=30, db_index=True)
realm = models.ForeignKey(Realm, db_index=True)
def __repr__(self):
return "<Stream: %s>" % (self.name,)
def __str__(self):
return self.__repr__()
def is_public(self):
return self.realm.domain in ["humbughq.com"]
class Meta:
unique_together = ("name", "realm")
@classmethod
def create(cls, name, realm):
stream = cls(name=name, realm=realm)
stream.save()
recipient = Recipient.objects.create(type_id=stream.id,
type=Recipient.STREAM)
return (stream, recipient)
class Recipient(models.Model):
type_id = models.IntegerField(db_index=True)
type = models.PositiveSmallIntegerField(db_index=True)
# Valid types are {personal, stream, huddle}
PERSONAL = 1
STREAM = 2
HUDDLE = 3
class Meta:
unique_together = ("type", "type_id")
# N.B. If we used Django's choice=... we would get this for free (kinda)
_type_names = {
PERSONAL: 'personal',
STREAM: 'stream',
HUDDLE: 'huddle' }
def type_name(self):
# Raises KeyError if invalid
return self._type_names[self.type]
def __repr__(self):
display_recipient = get_display_recipient(self)
return "<Recipient: %s (%d, %s)>" % (display_recipient, self.type_id, self.type)
class Client(models.Model):
name = models.CharField(max_length=30, db_index=True, unique=True)
@cache_with_key(lambda name: 'get_client:%s' % (hashlib.sha1(name).hexdigest(),))
@transaction.commit_on_success
def get_client(name):
try:
(client, _) = Client.objects.get_or_create(name=name)
except IntegrityError:
# 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 :(
transaction.commit()
return Client.objects.get(name=name)
return client
def linebreak(string):
return string.replace('\n\n', '<p/>').replace('\n', '<br/>')
class Message(models.Model):
sender = models.ForeignKey(UserProfile)
recipient = models.ForeignKey(Recipient)
subject = models.CharField(max_length=MAX_SUBJECT_LENGTH, db_index=True)
content = models.TextField()
pub_date = models.DateTimeField('date published', db_index=True)
sending_client = models.ForeignKey(Client)
def __repr__(self):
display_recipient = get_display_recipient(self.recipient)
return "<Message: %s / %s / %r>" % (display_recipient, self.subject, self.sender)
def __str__(self):
return self.__repr__()
@cache_with_key(lambda self, apply_markdown: 'message_dict:%d:%d' % (self.id, apply_markdown))
def to_dict(self, apply_markdown):
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
recip = {'email': self.sender.user.email,
'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()
obj = dict(
id = self.id,
sender_email = self.sender.user.email,
sender_full_name = self.sender.full_name,
sender_short_name = self.sender.short_name,
type = display_type,
display_recipient = display_recipient,
recipient_id = self.recipient.id,
subject = self.subject,
timestamp = datetime_to_timestamp(self.pub_date),
gravatar_hash = gravatar_hash(self.sender.user.email))
if apply_markdown:
if (self.sender.realm.domain != "customer1.invalid" or
self.sending_client.name in ('API', 'github_bot')):
obj['content'] = bugdown.convert(self.content)
else:
obj['content'] = linebreak(escape(self.content))
obj['content_type'] = 'text/html'
else:
obj['content'] = self.content
obj['content_type'] = 'text/x-markdown'
return obj
def to_log_dict(self):
return dict(
id = self.id,
sender_email = self.sender.user.email,
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(),
recipient = get_display_recipient(self.recipient),
subject = self.subject,
content = self.content,
timestamp = datetime_to_timestamp(self.pub_date))
@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()
class UserMessage(models.Model):
user_profile = models.ForeignKey(UserProfile)
message = models.ForeignKey(Message)
# 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()
class Meta:
unique_together = ("user_profile", "message")
def __repr__(self):
display_recipient = get_display_recipient(self.message.recipient)
return "<UserMessage: %s / %s>" % (display_recipient, self.user_profile.user.email)
class Subscription(models.Model):
user_profile = models.ForeignKey(UserProfile)
recipient = models.ForeignKey(Recipient)
active = models.BooleanField(default=True)
class Meta:
unique_together = ("user_profile", "recipient")
def __repr__(self):
return "<Subscription: %r -> %r>" % (self.user_profile, self.recipient)
def __str__(self):
return self.__repr__()
class Huddle(models.Model):
# TODO: We should consider whether using
# CommaSeparatedIntegerField would be better.
huddle_hash = models.CharField(max_length=40, db_index=True, unique=True)
def get_huddle_hash(id_list):
id_list = sorted(set(id_list))
hash_key = ",".join(str(x) for x in id_list)
return hashlib.sha1(hash_key).hexdigest()
def get_huddle(id_list):
huddle_hash = get_huddle_hash(id_list)
(huddle, created) = Huddle.objects.get_or_create(huddle_hash=huddle_hash)
if created:
recipient = Recipient.objects.create(type_id=huddle.id,
type=Recipient.HUDDLE)
# Add subscriptions
for uid in id_list:
Subscription.objects.create(recipient = recipient,
user_profile = UserProfile.objects.get(id=uid))
return huddle
# This function is used only by tests.
# We have faster implementations within the app itself.
def filter_by_subscriptions(messages, user):
user_profile = UserProfile.objects.get(user=user)
user_messages = []
subscriptions = [sub.recipient for sub in
Subscription.objects.filter(user_profile=user_profile, active=True)]
for message in messages:
# If you are subscribed to the personal or stream, or if you
# sent the personal, you can see the message.
if (message.recipient in subscriptions) or \
(message.recipient.type == Recipient.PERSONAL and
message.sender == user_profile):
user_messages.append(message)
return user_messages
def clear_database():
for model in [Message, Stream, UserProfile, User, Recipient,
Realm, Subscription, Huddle, UserMessage, Client,
DefaultStream]:
model.objects.all().delete()
Session.objects.all().delete()
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")
class DefaultStream(models.Model):
realm = models.ForeignKey(Realm)
stream = models.ForeignKey(Stream)
class Meta:
unique_together = ("realm", "stream")
# 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.
class StreamColor(models.Model):
subscription = models.ForeignKey(Subscription)
color = models.CharField(max_length=10)