def notify(cls, user_or_email_, object_id=None, **filters): """Start notifying the given user or email address when this event occurs and meets the criteria given in `filters`. Return the created (or the existing matching) Watch so you can call activate() on it if you're so inclined. Implementations in subclasses may take different arguments; see the docstring of is_notifying(). Send an activation email if an anonymous watch is created and settings.CONFIRM_ANONYMOUS_WATCHES = True. If the activation request fails, raise a ActivationRequestFailed exception. Calling notify() twice for an anonymous user will send the email each time. """ # A test-for-existence-then-create race condition exists here, but it # doesn't matter: de-duplication on fire() and deletion of all matches # on stop_notifying() nullify its effects. try: # Pick 1 if >1 are returned: watch = cls._watches_belonging_to_user( user_or_email_, object_id=object_id, **filters)[0:1].get() except Watch.DoesNotExist: create_kwargs = {} if cls.content_type: create_kwargs['content_type'] = \ ContentType.objects.get_for_model(cls.content_type) create_kwargs['email' if isinstance(user_or_email_, basestring) else 'user'] = user_or_email_ secret = ''.join(random.choice(letters) for x in xrange(10)) # Registered users don't need to confirm, but anonymous users do. is_active = ('user' in create_kwargs or not settings.CONFIRM_ANONYMOUS_WATCHES) if object_id: create_kwargs['object_id'] = object_id watch = Watch.objects.create( secret=secret, is_active=is_active, event_type=cls.event_type, **create_kwargs) for k, v in filters.iteritems(): WatchFilter.objects.create(watch=watch, name=k, value=hash_to_unsigned(v)) # Send email for inactive watches. if not watch.is_active: email = watch.user.email if watch.user else watch.email message = cls._activation_email(watch, email) try: message.send() except SMTPException, e: watch.delete() raise ActivationRequestFailed(e.recipients)
def _watches_belonging_to_user(cls, user_or_email, object_id=None, **filters): """Return a QuerySet of watches having the given user or email, having (only) the given filters, and having the event_type and content_type attrs of the class. Matched Watches may be either confirmed and unconfirmed. They may include duplicates if the get-then-create race condition in notify() allowed them to be created. If you pass an email, it will be matched against only the email addresses of anonymous watches. At the moment, the only integration point planned between anonymous and registered watches is the claiming of anonymous watches of the same email address on user registration confirmation. If you pass the AnonymousUser, this will return an empty QuerySet. """ # If we have trouble distinguishing subsets and such, we could store a # number_of_filters on the Watch. cls._validate_filters(filters) if isinstance(user_or_email, basestring): user_condition = Q(email=user_or_email) elif not user_or_email.is_anonymous(): user_condition = Q(user=user_or_email) else: return Watch.objects.none() # Filter by stuff in the Watch row: watches = Watch.uncached.filter( user_condition, Q(content_type=ContentType.objects.get_for_model(cls.content_type)) if cls.content_type else Q(), Q(object_id=object_id) if object_id else Q(), event_type=cls.event_type).extra(where=[ '(SELECT count(*) FROM notifications_watchfilter WHERE ' 'notifications_watchfilter.watch_id=' 'notifications_watch.id)=%s' ], params=[len(filters)]) # Optimization: If the subselect ends up being slow, store the number # of filters in each Watch row or try a GROUP BY. # Apply 1-to-many filters: for k, v in filters.iteritems(): watches = watches.filter(filters__name=k, filters__value=hash_to_unsigned(v)) return watches
def _watches_belonging_to_user(cls, user_or_email, object_id=None, **filters): """Return a QuerySet of watches having the given user or email, having (only) the given filters, and having the event_type and content_type attrs of the class. Matched Watches may be either confirmed and unconfirmed. They may include duplicates if the get-then-create race condition in notify() allowed them to be created. If you pass an email, it will be matched against only the email addresses of anonymous watches. At the moment, the only integration point planned between anonymous and registered watches is the claiming of anonymous watches of the same email address on user registration confirmation. If you pass the AnonymousUser, this will return an empty QuerySet. """ # If we have trouble distinguishing subsets and such, we could store a # number_of_filters on the Watch. cls._validate_filters(filters) if isinstance(user_or_email, basestring): user_condition = Q(email=user_or_email) elif not user_or_email.is_anonymous(): user_condition = Q(user=user_or_email) else: return Watch.objects.none() # Filter by stuff in the Watch row: watches = Watch.uncached.filter( user_condition, Q(content_type=ContentType.objects.get_for_model(cls.content_type)) if cls.content_type else Q(), Q(object_id=object_id) if object_id else Q(), event_type=cls.event_type).extra( where=['(SELECT count(*) FROM notifications_watchfilter WHERE ' 'notifications_watchfilter.watch_id=' 'notifications_watch.id)=%s'], params=[len(filters)]) # Optimization: If the subselect ends up being slow, store the number # of filters in each Watch row or try a GROUP BY. # Apply 1-to-many filters: for k, v in filters.iteritems(): watches = watches.filter(filters__name=k, filters__value=hash_to_unsigned(v)) return watches
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(filters.iteritems()): joins.append('LEFT JOIN notifications_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
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(filters.iteritems()): joins.append( 'LEFT JOIN notifications_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