in kitsune/tidings/events.py [0:0]
def _users_watching_by_filter(self, object_id=None, exclude=None, **filters):
"""Return an iterable of (``User``/:class:`~tidings.models.EmailUser`,
[:class:`~tidings.models.Watch` objects]) tuples watching the event.
Of multiple Users/EmailUsers having the same email address, only one is
returned. Users are favored over EmailUsers so we are sure to be able
to, for example, include a link to a user profile in the mail.
The list of :class:`~tidings.models.Watch` objects includes both
those tied to the given User (if there is a registered user)
and to any anonymous Watch having the same email address. This
allows you to include all relevant unsubscribe URLs in a mail,
for example. It also lets you make decisions in the
:meth:`~tidings.events.EventUnion._mails()` method of
:class:`~tidings.events.EventUnion` based on the kinds of
watches found.
"Watching the event" means having a Watch whose ``event_type`` is
``self.event_type``, whose ``content_type`` is ``self.content_type`` or
``NULL``, whose ``object_id`` is ``object_id`` or ``NULL``, and whose
WatchFilter rows match as follows: each name/value pair given in
``filters`` must be matched by a related WatchFilter, or there must be
no related WatchFilter having that name. If you find yourself wanting
the lack of a particularly named WatchFilter to scuttle the match, use
a different event_type instead.
:arg exclude: A sequence of users or None. If a sequence of users is
passed in, each of those users will not be notified, though anonymous
notifications having the same email address may still be sent.
"""
# I don't think we can use the ORM here, as there's no way to get a
# second condition (name=whatever) into a left join. However, if we
# were willing to have 2 subqueries run for every watch row--select
# {are there any filters with name=x?} and select {is there a filter
# with name=x and value=y?}--we could do it with extra(). Then we could
# have EventUnion simply | the QuerySets together, which would avoid
# having to merge in Python.
def filter_conditions():
"""Return joins, WHERE conditions, and params to bind to them in
order to check a notification against all the given filters."""
# Not a one-liner. You're welcome. :-)
self._validate_filters(filters)
joins, wheres, join_params, where_params = [], [], [], []
for n, (k, v) in enumerate(iter(filters.items())):
joins.append(
"LEFT JOIN tidings_watchfilter f{n} "
"ON f{n}.watch_id=w.id "
"AND f{n}.name=%s".format(n=n)
)
join_params.append(k)
wheres.append("(f{n}.value=%s " "OR f{n}.value IS NULL)".format(n=n))
where_params.append(hash_to_unsigned(v))
return joins, wheres, join_params + where_params
# Apply watchfilter constraints:
joins, wheres, params = filter_conditions()
# Start off with event_type, which is always a constraint. These go in
# the `wheres` list to guarantee that the AND after the {wheres}
# substitution in the query is okay.
wheres.append("w.event_type=%s")
params.append(self.event_type)
# Constrain on other 1-to-1 attributes:
if self.content_type:
wheres.append("(w.content_type_id IS NULL " "OR w.content_type_id=%s)")
params.append(ContentType.objects.get_for_model(self.content_type).id)
if object_id:
wheres.append("(w.object_id IS NULL OR w.object_id=%s)")
params.append(object_id)
if exclude:
# Don't try excluding unsaved Users:1
if not all(e.id for e in exclude):
raise ValueError("Can't exclude an unsaved User.")
wheres.append("(u.id IS NULL OR u.id NOT IN (%s))" % ", ".join("%s" for e in exclude))
params.extend(e.id for e in exclude)
def get_fields(model):
if hasattr(model._meta, "_fields"):
# For django versions < 1.6
return model._meta._fields()
else:
# For django versions >= 1.6
return model._meta.fields
User = get_user_model()
model_to_fields = dict(
(m, [f.get_attname() for f in get_fields(m)]) for m in [User, Watch]
)
query_fields = ["u.{0}".format(field) for field in model_to_fields[User]]
query_fields.extend(["w.{0}".format(field) for field in model_to_fields[Watch]])
query = (
"SELECT {fields} "
"FROM tidings_watch w "
"LEFT JOIN {user_table} u ON u.id=w.user_id {joins} "
"WHERE {wheres} "
"AND (length(w.email)>0 OR length(u.email)>0) "
"AND w.is_active "
"ORDER BY u.email DESC, w.email DESC"
).format(
fields=", ".join(query_fields),
joins=" ".join(joins),
wheres=" AND ".join(wheres),
user_table=User._meta.db_table,
)
# IIRC, the DESC ordering was something to do with the placement of
# NULLs. Track this down and explain it.
# Put watch in a list just for consistency. Once the pairs go through
# _unique_by_email, watches will be in a list, and EventUnion uses the
# same function to union already-list-enclosed pairs from individual
# events.
return _unique_by_email(
(u, [w]) for u, w in multi_raw(query, params, [User, Watch], model_to_fields)
)