def _users_watching_by_filter()

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)
        )