from textwrap import wrap
from django.db import models
from django.db.models import signals
from django.utils import simplejson as json
from django.contrib.auth.models import User
from django.core.mail import send_mail, send_mass_mail
from conference.models import Participant # TODO ... try to make this more generic ...
from misc.tools import render_wrapped
from django.conf import settings
class ParticipantComment(models.Model):
[docs]
Participant.new_msgs = new_msgs
def seen_msgs(self):
return self.participantcomment_set.filter(seen=True, settled=False)
[docs]
Participant.seen_msgs = seen_msgs
class ParticipantInvComment(models.Model): # comments by organizers about a specific participant (e.g. funding request),
# invisible to participant
def build_query_filter_from_spec(spec, field_mapping=None):
"""
[docs] Assemble a django "Q" query filter object from a specification that consists
of a possibly-nested list of query filter descriptions. These descriptions
themselves specify Django primitive query filters, along with boolean
"and", "or", and "not" operators. This format can be serialized and
deserialized, allowing django queries to be composed client-side and
sent across the wire using JSON.
Each filter description is a list. The first element of the list is always
the filter operator name. This name is one of either django's filter
operators, "eq" (a synonym for "exact"), or the boolean operators
"and", "or", and "not".
Primitive query filters have three elements:
[filteroperator, fieldname, queryarg]
"filteroperator" is a string name like "in", "range", "icontains", etc.
"fieldname" is the django field being queried. Any name that django
accepts is allowed, including references to fields in foreign keys
using the "__" syntax described in the django API reference.
"queryarg" is the argument you'd pass to the `filter()` method in
the Django database API.
"and" and "or" query filters are lists that begin with the appropriate
operator name, and include subfilters as additional list elements:
['or', [subfilter], ...]
['and', [subfilter], ...]
"not" query filters consist of exactly two elements:
['not', [subfilter]]
As a special case, the empty list "[]" or None return all elements.
If field_mapping is specified, the field name provided in the spec
is looked up in the field_mapping dictionary. If there's a match,
the result is subsitituted. Otherwise, the field name is used unchanged
to form the query. This feature allows client-side programs to use
"nice" names that can be mapped to more complex django names. If
you decide to use this feature, you'll probably want to do a similar
mapping on the field names being returned to the client.
This function returns a Q object that can be used anywhere you'd like
in the django query machinery.
This function raises ValueError in case the query is malformed, or
perhaps other errors from the underlying DB code.
Example queries:
['and', ['contains', 'name', 'Django'], ['range', 'apps', [1, 4]]]
['not', ['in', 'tags', ['colors', 'shapes', 'animals']]]
['or', ['eq', 'id', 2], ['icontains', 'city', 'Boston']]
"""
if spec == None or len(spec) == 0:
return Q()
cmd = spec[0]
if cmd == 'and' or cmd == 'or':
# ["or", [filter],[filter],[filter],...]
# ["and", [filter],[filter],[filter],...]
if len(spec) < 2:
raise ValueError,'"and" or "or" filters must have at least one subfilter'
resultq = None
for arg in spec[1:]:
q = build_query_filter_from_spec(arg)
if q != None:
if resultq == None:
resultq = q
else:
if cmd == 'and': resultq = resultq & q
else: resultq = resultq | q
elif cmd == 'not':
# ["not", [query]]
if len(spec) != 2:
raise ValueError,'"not" filters must have exactly one subfilter'
q = build_query_filter_from_spec(spec[1])
if q != None:
resultq = ~q
else:
# some other query, will be validated in the query machinery
# ["cmd", "fieldname", "arg"]
# provide an intuitive alias for exact field equality
if cmd == 'eq':
cmd = 'exact'
if len(spec) != 3:
raise ValueError,'primitive filters must have two arguments (fieldname and query arg)'
field_name = spec[1]
if field_mapping:
# see if the mapping contains an entry for the field_name
# (for example, if you're mapping an external database name
# to an internal django one). If not, use the existing name.
field_name = field_mapping.get(field_name, field_name)
kwname = str("%s__%s" % (field_name, cmd))
kwdict = {kwname : spec[2]}
resultq = Q(**kwdict)
return resultq
class StandardMessageManager(models.Manager):
[docs] def active(self):
qs = super(StandardMessageManager, self).get_query_set()
[docs] return qs.filter(models.Q(active=True) & models.Q(date__lte=datetime.date.today))
class StandardMessage(models.Model):
sender = models.CharField(max_length=100, default=settings.DEFAULT_SENDER)
[docs] slug = models.CharField(max_length=15)
subject = models.CharField(max_length=100)
template = models.TextField()
date = models.DateField(default=datetime.date(2010,1,1)) # automatically send (only) after this date
participants = models.ManyToManyField(Participant, blank=True, null=True)
active = models.BooleanField(default=False)
preview = models.BooleanField(default=True) # send immediately, or send to preview queue?
filters = models.TextField()
objects = StandardMessageManager()
def __unicode__(self):
return self.slug + ', ' + self.subject + ', (' + ('active' if self.active else 'not active') + ', ' + unicode(self.date) + ')'
def send_p(self, p):
text = render_wrapped(self.template, { 'p': p, 'common': settings.COMMON })
[docs] m = Message(sender=self.sender, subject=self.subject, text=text, participant=p, stdmsg=self)
if self.preview:
m.save()
else:
m.send_and_save()
self.participants.add(p) # automatically saved to db FIXME but does not work in admin!
def send_all(self):
if not self.active or datetime.date.today() < self.date: return
[docs] for p in self.relevant_ps():
self.send_p(p)
def show_ex(self):
q = build_query_filter_from_spec(json.loads(self.filters))
[docs] p_ids = [ p.id for p in self.participants.all() ] # TODO there may be a smarter way of doing this
plist = Participant.objects.active().filter(q).exclude(id__in=p_ids)
for p in plist:
print '----------------------------------------------------------------'
print p.get_full_name()
print '................................................................'
print self.sender
print self.subject
print render_wrapped(self.template, { 'p': p, 'common': settings.COMMON })
def relevant_ps(self):
q = build_query_filter_from_spec(json.loads(self.filters))
[docs] p_ids = [ p.id for p in self.participants.all() ] # TODO there may be a smarter way of doing this
return Participant.objects.active().filter(q).exclude(id__in=p_ids)
class Meta:
ordering = ['-date']
def on_save_participant(instance, **kwargs):
for stdm in StandardMessage.objects.active():
[docs] if not instance in stdm.relevant_ps(): continue
stdm.send_p(instance)
signals.post_save.connect(on_save_participant, sender=Participant)
def on_save_stdmessage(instance, **kwargs):
instance.send_all()
[docs]
signals.post_save.connect(on_save_stdmessage, sender=StandardMessage)
def sent_stdmsgs(self):
return self.message_set.exclude(stdmsg=None).filter(sent=True)
[docs]
Participant.sent_stdmsgs = sent_stdmsgs
class ParticipantOrgCommentManager(models.Manager):
[docs] use_for_related_fields = True
def sent(self):
qs = super(MessageManager, self).all()
[docs] return qs.filter(models.Q(sent=True))
def preview(self):
qs = super(MessageManager, self).all()
[docs] return qs.filter(models.Q(sent=False))
class Message(models.Model):
'''
[docs] messages to participants
sources:
- individual messages from edit_admin page
- bulk messages from StandardMessage class
as long as sent==False, they have a preview status (and can be edited & sent/discarded in maintenance mode)
'''
sender = models.CharField(max_length=100, default=settings.DEFAULT_SENDER)
subject = models.CharField(max_length=100)
text = models.TextField()
date = models.DateField(auto_now=True)
participant = models.ForeignKey(Participant)
sent = models.BooleanField(default=False)
stdmsg = models.ForeignKey(StandardMessage, null=True, blank=True) # does this come from a standard message?
def send_and_save(self):
send_mail(self.subject, self.text, self.sender, [ self.participant.user.email ])
[docs] self.sent = True
self.save()
# TODO: Ideally, disable changing "sent" messages
objects = MessageManager()
def __unicode__(self):
return u'%s, %s, %s' % (self.participant.get_full_name(), str(self.date), self.subject)
class Meta:
ordering = ['-date']