class NotificationCallbackTimer(BaseDataObject): """ Registers a callback to occur after a timestamp. This can be used by the application tier to evaluate if conditions are met to trigger a notification message to be sent. A callback can be periodic, in which case, when one callback is completed another is schedule as <now>+periodicity_mind. It will reuse the same context as when the first was created. IMPORTANT: Do not register a lot of high frequency callbacks as each reiteration will take up another row in the database to store the re-registration of the callback and the results. There is a system limit - which is configurable - on the minimum minutes, for example no less than 60 minutes (hourly job) IMPORTANT: the class that is registered to be called back *must* implement the NotificationCallbackTimerHandler interface """ @property def id(self): """ Alias the timer name as the id, since all data objects have names """ return self.name # timer name, must be unique! name = StringField() # earliest to callback at callback_at = DateTimeField() # the entry point "e.g. myapp.module.NotificationAsyncCallbackHandler" class_name = StringField() # is active is_active = BooleanField() # any specific context that should be passed into the callback context = DictField() # if this is a recurring timer entry, what is the periodicity # in minutes periodicity_min = IntegerField() # when the callback was called executed_at = DateTimeField() # any unhandled messages associated with the callback err_msg = StringField() # any stats the the callback handler returned results = DictField() # timestamps created = DateTimeField() modified = DateTimeField()
def load_from_data_object(self, notification_timer): """ Hydrate ourselves from a passed in notification_timer """ self.name = notification_timer.name # pylint: disable=attribute-defined-outside-init self.callback_at = notification_timer.callback_at self.class_name = notification_timer.class_name self.context = DictField.to_json(notification_timer.context) self.is_active = notification_timer.is_active self.periodicity_min = notification_timer.periodicity_min self.executed_at = notification_timer.executed_at self.err_msg = notification_timer.err_msg self.results = DictField.to_json(notification_timer.results)
class UserNotification(BaseDataObject): """ Maps a NotificationMessage to a User NOTE: We will have to figure out a way to model cursor behavior paging for large collections NOTE: If we can say that broadcast-type messages, e.g. course-wide, don't need to persist read_at state nor any personalization, then we could maybe do away with excessive fan-outs """ # unconstrained pointer to edx-platform auth_user table user_id = IntegerField() # the message itself msg = RelatedObjectField(NotificationMessage) # time the user read the notification read_at = DateTimeField() # dict containing any user specific context (e.g. personalization) for the notification user_context = DictField() # creation timestamp created = DateTimeField()
def load_from_data_object(self, msg_type): """ Hydrate ourselves from a passed in user_msg """ self.name = msg_type.name # pylint: disable=attribute-defined-outside-init self.renderer = msg_type.renderer self.renderer_context = DictField.to_json(msg_type.renderer_context)
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return NotificationCallbackTimer( name=self.name, callback_at=self.callback_at, class_name=self.class_name, context=DictField.from_json( self.context), # special case, dict<-->JSON string is_active=self.is_active, periodicity_min=self.periodicity_min, # pylint: disable=no-member executed_at=self.executed_at, err_msg=self.err_msg, created=self.created, modified=self.modified, results=DictField.from_json(self.results))
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return NotificationCallbackTimer( name=self.name, callback_at=self.callback_at, class_name=self.class_name, context=DictField.from_json(self.context), # special case, dict<-->JSON string is_active=self.is_active, periodicity_min=self.periodicity_min, # pylint: disable=no-member executed_at=self.executed_at, err_msg=self.err_msg, created=self.created, modified=self.modified, results=DictField.from_json(self.results) )
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return NotificationType(name=self.name, renderer=self.renderer, renderer_context=DictField.from_json( self.renderer_context))
class DataObjectWithTypedFields(BaseDataObject): """ More sophisticated DataObject """ test_int_field = IntegerField() test_dict_field = DictField() test_class_field = RelatedObjectField(NotificationMessage) test_enum_field = EnumField(allowed_values=['foo'])
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return NotificationType( name=self.name, renderer=self.renderer, renderer_context=DictField.from_json(self.renderer_context) )
def load_from_data_object(self, user_msg): """ Hydrate ourselves from a passed in user_msg """ self.id = user_msg.id # pylint: disable=attribute-defined-outside-init self.user_id = user_msg.user_id self.msg = SQLNotificationMessage.from_data_object(user_msg.msg) self.read_at = user_msg.read_at self.user_context = DictField.to_json(user_msg.user_context)
def load_from_data_object(self, msg): """ Hydrate ourselves from a data object, note that we do not set the created/modified timestamps as that is auto-generated """ msg.validate() self.id = msg.id # pylint: disable=attribute-defined-outside-init self.namespace = msg.namespace self.msg_type = SQLNotificationType.from_data_object(msg.msg_type) self.from_user_id = msg.from_user_id self.deliver_no_earlier_than = msg.deliver_no_earlier_than self.expires_at = msg.expires_at self.expires_secs_after_read = msg.expires_secs_after_read self.payload = DictField.to_json(msg.payload) self.resolve_links = DictField.to_json(msg.resolve_links) self.object_id = msg.object_id
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Return a Notification Message data object """ msg = NotificationMessage( id=self.id, namespace=self.namespace, msg_type=self.msg_type.to_data_object(), from_user_id=self.from_user_id, deliver_no_earlier_than=self.deliver_no_earlier_than, expires_at=self.expires_at, expires_secs_after_read=self.expires_secs_after_read, payload=DictField.from_json(self.payload), # special case, dict<-->JSON string created=self.created, resolve_links=DictField.from_json(self.resolve_links), # special case, dict<-->JSON string object_id=self.object_id ) return msg
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return UserNotification( id=self.id, user_id=self.user_id, msg=self.msg.to_data_object(), # pylint: disable=no-member read_at=self.read_at, user_context=DictField.from_json(self.user_context), created=self.created)
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return UserNotification( id=self.id, user_id=self.user_id, msg=self.msg.to_data_object(), # pylint: disable=no-member read_at=self.read_at, user_context=DictField.from_json(self.user_context), created=self.created )
class NotificationType(BaseDataObject): """ The Data Object representing the NotificationType """ # the name (including namespace) of the notification, e.g. open-edx.lms.forums.reply-to-post name = StringField() # default delivery channel for this type # None = no default default_channel = RelatedObjectField(NotificationChannel) # renderer class - as a string - that will handle the rendering of # this type renderer = StringField() # any context information to pass into the renderer renderer_context = DictField()
class NotificationTypeUserChannelPreference(BaseDataObject): """ Specifies a User preference as to how he/she would like notifications of a certain type delivered """ # unconstrained identifier that is provided by some identity service (e.g. auth_user Django Auth) user_id = IntegerField() # the NotificationType this preference is for notification_type = RelatedObjectField(NotificationType) # the Channel that this NotificationType should route to channel = RelatedObjectField(NotificationChannel) # dict containing any user specific context for this channel, for example a mobile # for SMS # message, or email address channel_context = DictField()
class NotificationMessage(BaseDataObject): """ The basic Notification Message """ _exclude_fields_for_equality = ['created', 'modified'] # to account for slight clock skews # instance of NotificationMessageType, None = unloaded msg_type = RelatedObjectField(NotificationType) # namespace is an optional scoping field. This could # be used to indicate - for instance - a course_id. Note, # that we can filter on this property when # getting notifications namespace = StringField() # unconstained ID to some user identity service (e.g. auth_user in Django) from_user_id = IntegerField() # dict containing key/value pairs which comprise the notification data payload payload = DictField() # DateTime, the earliest that this notification should be delivered # for example, this could be used for a delayed notification. # # None = ASAP deliver_no_earlier_than = DateTimeField() # DateTime, when this notification is no longer considered valid even if it has not been read # # None = never expires_at = DateTimeField() # Duration in seconds, when this notification should be purged after being marked as read. # # None = never expires_secs_after_read = IntegerField() priority = EnumField( allowed_values=[ const.NOTIFICATION_PRIORITY_NONE, const.NOTIFICATION_PRIORITY_LOW, const.NOTIFICATION_PRIORITY_MEDIUM, const.NOTIFICATION_PRIORITY_HIGH, const.NOTIFICATION_PRIORITY_URGENT, ], default=const.NOTIFICATION_PRIORITY_NONE ) # timestamps created = DateTimeField() # links to resolve by the NotificationChannel when dispatching. resolve_links = DictField() # generic id regarding the object that this notification msg is about # this can be used for lookups object_id = StringField() @property def _click_link_keyname(self): """ Hide this constant so that other's don't need to know how internal schemas """ return '_click_link' @property def _channel_payloads_keyname(self): """ The name of the dictionary key in the Payload field """ return '__channel_payloads' @property def _default_payload_keyname(self): """ The name of the dictionary key in the Payload field """ return '__channel_payloads' def validate(self): """ Validator for this DataObject I'd like to consolidate this to be optional args on the fields and have introspection to make sure everything is OK, but since Fields are descriptors, that might make things a bit more difficult. Basically we need a way to look at the descriptor not the value the descriptor reveals """ if not self.msg_type: raise ValidationError("Missing required property: msg_type") def set_click_link(self, click_link): """ If we have a "click link" associate with the notification, this will store it in the system defined meta-field in the payload which the Backbone presentation tier knows about. IMPORTANT: If click links are generated through the LinkResolvers in the NotificationChannels then it will be overwritten. This happens when self.resolve_links != None """ if not self.payload: self.payload = {} self.payload[self._click_link_keyname] = click_link def get_click_link(self): """ Return the click link associated with this message, if it was set. IMPORTANT: If click links are generated through the LinkResolvers in the NotificationChannels then it will be overwritten. This happens when self.resolve_links != None """ if not self.payload: return None return self.payload[self._click_link_keyname] def add_resolve_link_params(self, link_name, params): """ Helper method to set resolve_links field when the message gets published to a channel and we need to add meta-links to the message, e.g. to - say - link to a webpage """ if not self.resolve_links: self.resolve_links = {} if link_name in self.resolve_links: # the link_name already exists, so we should update # the parameters associated with it self.resolve_links[link_name].update(params) else: # new link name, so let's add the whole thing self.resolve_links.update({ link_name: params }) def add_click_link_params(self, params): """ Helper method to set a system defined '_click_link' payload value, which can be handled by the front-end Backbone application to signify a click through link """ self.add_resolve_link_params(self._click_link_keyname, params) def get_click_link_params(self): """ Helper method to get all click links, so that calling applications need to know that we store that under a key named '_click_link' """ return self.resolve_links.get(self._click_link_keyname) @property def has_multi_payloads(self): """ Returns true/false if the Message is setup for multi payloads """ return self.payload and self._channel_payloads_keyname in self.payload def add_payload(self, payload_dict, channel_name=None): """ Adds a payload that is targeted to the specific channel """ sub_key = self._channel_payloads_keyname default_key = self._default_payload_keyname # use, old style schema? if not channel_name and not self.has_multi_payloads: self.payload = payload_dict elif channel_name and not self.has_multi_payloads: # convert to support multi-payloads existing_payload = copy.deepcopy(self.payload) self.payload[sub_key] = {} payloads = self.payload[sub_key] payloads[channel_name] = payload_dict payloads[default_key] = existing_payload if existing_payload else {} elif channel_name: self.payload[sub_key][channel_name] = payload_dict else: self.payload[sub_key][default_key] = payload_dict def get_payload(self, channel_name=None): """ Returns a payload for the specific channel """ if not self.has_multi_payloads: return self.payload sub_key = self._channel_payloads_keyname payloads = self.payload[sub_key] if channel_name in payloads: return self.payload[sub_key][channel_name] default_key = self._default_payload_keyname if default_key in payloads: return payloads[default_key] return {} def get_message_for_channel(self, channel_name=None): """ Returns a copy of self with the correct payload channel """ # simple case if not self.has_multi_payloads: return self clone_msg = copy.deepcopy(self) # return a NotificationMessage with all other # channel payloads removed clone_msg.payload = self.get_payload(channel_name) return clone_msg
def to_representation(self, obj): """ to json format """ return DictField.to_json(obj)
def to_internal_value(self, data): """ from json format """ return DictField.from_json(data)
def to_native(self, obj): """ to json format """ return DictField.to_json(obj)
def from_native(self, data): """ from json format """ return DictField.from_json(data)