Support/Notifications

< Support
Revision as of 20:40, 6 January 2011 by Erikrose (talk | contribs) (Added an API sketch. Scribble at will.)

Goals

  • Ref integ of email addresses
  • More customizable scoping

Use cases

Trigger a notification

  1. Notify anytime any Document changes.
  2. Notify anytime a Document in a certain locale changes.
  3. Notify when a particular Document changes.
  4. Delete an object and make its watches go away.
  5. New articles or changes to Documents that are untranslated into de and that are localizable and that are approved.
  6. Notify when an approval happens in German.
  7. Major edits to Documents in English
  8. Major edits to Documents
  9. Any activity tagged with "blue"
  10. Anything Fred does
  11. Tell what the user is subscribed to
  12. Notify any new thread in a given discussion forum
  13. Notify any post (new thread or reply) in a given discussion forum
  14. Notify any post in a discussion thread
  15. Notify any new thread in any KB forum in a given locale
  16. Notify any post in any KB forum in a given locale
  17. Notify any new thread in a given KB forum
  18. Notify any post in a given KB forum
  19. Notify any post in a given KB forum thread
  20. Notify on new question (confirmed)
  21. Notify on new answer to a given question
  22. Notify on answer marked as solution to a given question
  23. Notify on new flagged item for review

Deliver notification

Logged in user

(non-exclusive)

  • To email
  • To private message
  • (Future, other options?)

Anonymous user

  • To email

If a user registers, we should look for any anonymous watches with that email address and add them to the user, when the user is confirmed.

Evolution of table structure

Iteration 1: Event type determines the format of the path column(s).

path         path2*     type**                      email/whatever
15                      DocumentEvent               whatever@whatever.com
15.en-US                DocumentEvent
15.de.2837              DocumentEvent
15.en-US.30             DocumentSignificanceEvent
                        TagEvent

* optional. Choose the split per event type based on where orthogonality happens.
** An event type, which can span, frex, use cases 1-3 below. Determines the use of the spec fields.


Iteration 2: Get ref-integ on content type.

content_type    object_id    path      event_type                 email/?  (usecase)
15                           en-US     DocumentLocaleEvent                 2
15                                     DefaultEvent                        1
15              2837                   DefaultEvent                        3
15                           en-US.30  DocumentLocaleSignifEvent           7
15                           de.L.A    UntranslatedEvent                   5

Don't make path col unicode: save space.


Iteration 3: More normalization, yielding better queryability: orthogonality of path elements (not just hierarchy) is possible without ridiculously inefficient substring queries. I don't think the joins would be too bad, but benches would be nice. Maintains ref integ of content type and object.

watches:
id content_type object_id event_type        user/email/whatever
1  15           2345      UntranslatedEvent
watch_elements:
watch_id name        value (int for size/speed/ad hoc joins)
1        lang        CRC32(de)  # Postgres doesn't have a CRC32 function, so ad hoc joins on string keys would be a pain.
1        localizable 1
1        approved    1
on UntranslatedEvent for object_id=2345 and content_type=15:
    select distinct email from watches
    left join watch_elements lang on watches.id=lang.watch_id and name='LANG' and (value IS NULL or value='de')
    left join watch_elements localizable on watches.id=localizable.watch_id and name='LOCA' and (value IS NULL or value='1')
    left join watch_elements approved on watches.id=approved.watch_id and name='APPR' and (value IS NULL or value='1')
    where content_type=15 and (object_id=2345 or object_id IS NULL) and event_type=UntranslatedEvent 
    # It's an (n+1)-table join where n is the max length of the path (excluding content type and object ID). n of the joins, however, would be selecting among the same n rows: not a big deal.


Iteration 4: Move content_type and object_id into watch_elements? See what the benches say.

watches:
id event_type        user/email/whatever
1  UntranslatedEvent

watch_elements:
watch_id     name         value (int for size/speed/ad hoc joins)
1            lang         CRC32(de)  # Postgres doesn't support CRC32 in the DB, so ad hoc joins on string keys would be a drag.
1            localizable  1
1            approved     1
1            content_type 15
1            object_id    2345      

This will bring it up to an (n+1)-table join, n being the max length of a certain event type's path *including* content type and object ID.

Don't hamper work now because of Postgres. Assume we're on MySQL until Oracle shows up with a bill.

Features

  • Maybe blocking an addy someday till a mail is confirmed (to prevent spamming)?
  • Daily digests, supported by 2 tables:
digests:
user     event_type

digest_elements (values plugged into the template specified by event_type):
name      value
id        8634
hat_color red

API Sketch

class ObjectModifiedWatchType(object):
    """A notification which fires on the modification of a specific object"""

    code = 'modi'  # Key for the event_type column. Should be same size as a 32-bit int (but more memorable) if we store as a binary char(4). Migrations will be easy in case of collisions. All-lowercase codes are reserved for the notification app itself.

    @classmethod
    def fire(cls, obj):
        """Signal listener which sends any appropriate notifications"""

    @classmethod
    def create(cls, object):
        """Create (and save, and return) a watch which fires on the modification of the given object."""

    ...

objectModifiedSignal.register(ObjectCreatedEvent.listener)  # modulo spelling


class ContentTypeModifiedWatchType(...)
    code = 'modi'  # Can share a key with the above since the NULL in the object_id column and the non-NULL in the content_type_id column are sufficient to distinguish them.

class ContentTypeCreatedWatchType(...)
    code = 'crea'

class LocaleCreationWatchType(...):  # abstract because it doesn't provide a template. Might not really be worth making this abstract. We don't have a lot of locale-having things atm.
    """A notification which fires when an object (like a wiki Document) is created with a given locale.
    
    Works with anything that has a `locale` attr.
    
    """
    code = 'WLCr'  # convention: capital letters start a new word
    
    @classmethod
    def create(cls, locale, content_type=None):
        """Watch type for the creation of any object of a given locale.

        Specify a content_type to limit to a particular type.

        """
        watch = Watches.create(content_type=content_type, object_id=None, event_type=cls.code, user=[figure out how to represent this relationship to support anonymous also])
        
        # WatchElements implements a key/value store. We might use 4-char codes as the keys, again because they're more memorable than ints and because they have to be unique only within an event_type. Thus, none are reserved.
        WatchElements.create(watch=watch, name='locl', value=crc32(locale))

    @classmethod
    def fire(cls, obj):
        # TODO: Can probably use the ORM for this:
        watches = execute("""select distinct user (or whatever) from watches
            inner join watch_elements locale on watches.id=lang.watch_id and name='locl' and value={obj.locale}
            where content_type IS NULL or content_type={obj.content_type}
            AND event_type={cls.code}""")
        for w in watches:
            cls.notify(obj)  # builds and sends one mail, for example

    @classmethod
    def notify(cls, obj):
        # Send a mail or what have you.
        raise NotImplementError

def WikiLocaleCreationWatchType(LocaleCreationWatchType):
    @classmethod
    def notify(cls, obj):
        send_mail_in_a_task(some_template.render(obj.this, obj.that, ...))

objectCreatedSignal.register(WikiLocaleCreationWatch.fire)