Adding a Last Modified At column in Open Event Server

This blog article will illustrate how, with the help of SQLAlchemy, a last modified at column, with complete functionality can be added to the Open Event Server database. To illustrate the process, the blog article will discuss adding the column to the sessions api. Since last modified at is a time field, and will need to be updated each time user successfully updates the session, the logic to implement will be a slightly more complex than a mere addition of a column to the table.

The first obvious step will comprise of adding the column to the database table. To achieve the same, the column will have to be added to the model for the sessions table, as well as the schema.

In app/api/schema/sessions.py:

...
class SessionSchema(Schema):
   """
   Api schema for Session Model
   """
   ...
   last_modified_at = fields.DateTime(dump_only=True)
   ...

And in app/models/sessions.py:

import pytz
...

class Session(db.Model):
   """Session model class"""
   __tablename__ = 'sessions'
   __versioned__ = {
       'exclude': []
   }
   ...
   last_modified_at = db.Column(db.DateTime(timezone=True),   
   default=datetime.datetime.utcnow)
   def init(self, ..., last_modified_at=None))
     #inside init method
     ...
     self.last_modified_at = datetime.datetime.now(pytz.utc)
     ...

NOTE: The users for the open event organiser server will be operating in multiple time zones and hence it is important for all the times to be in sync, hence the open event database maintains all the time in UTC timezone (python’s pytz module takes care of converting user’s local time into UTC time while storing, thus unifying the timezones.) From this, it directly follows that the time needs to be timezone aware hence timezone=true is passed, while defining the column.

Next, while initialising an object of this class, the last modified time is the time of creation, and hence

datetime.now(pytz.utc) is set as the initial value which basically stores the current time in UTC timezone format.

Finally, the logic for updating the last modified at column every time any other value changes for a session record needs to be implemented. SQLAlchemy provides an inbuilt support for detecting update and insert events which have been used to achieve the goal. To quote the official SQLAlchemy Docs,  “SQLAlchemy includes an event API which publishes a wide variety of hooks into the internals of both SQLAlchemy Core and ORM.

@event.listens_for(Session, 'after_update')
def receive_after_update(mapper, connection, target):
  target.last_modified_at = datetime.datetime.now(pytz.utc)

The listens_for() decorator is used to register the event according to the arguments passed to it. In our case, it will register any event on the Session API (sessions table), whenever it updates.

The corresponding function defined below the decorator, receive_after_update(mapper, connection, target) is then called, and session model (table) is the the registered target with the event. It sets the value of the last_modified_at to the current time in the UTC timezone as expected.

Lastly, since the changes have been made to the database schema, the migration file needs to be generated, and the database will be upgraded to alter the structure.

The sequence of steps to be followed on the CLI will be

> python manage.py db migrate
> python manage.py db upgrade

Resources

Implementing Email Notifications in Open Event Frontend

In Open Event Frontend, we have a ‘settings/email-preferences’ route where we give the user an access to change the email-notifications that he wants to subscribe for a specific event. For now, we are having three notifications for an event which the user can toggle to on and off. To achieve this, we did the following things:

First, we create a model for email-notifications so as to have a skeleton of the data that we are going to receive from the JSON API.

export default ModelBase.extend({

  /**
   * Attributes
   */

  nextEvent           : attr('boolean'),
  newPaper            : attr('boolean'),
  sessionAcceptReject : attr('boolean'),
  sessionSchedule     : attr('boolean'),
  afterTicketPurchase : attr('boolean'),

  /**
   * Relationships
   */
  user  : belongsTo('user'),
  event : belongsTo('event')
});

Thus, above code shows the attributes which we are going to receive via our JSON API and we will render the data accordingly on the page. We have established the relationship of the email-notifications with user and event so that in future wherever needed, we can query the records from either side. The client side has checkboxes to show the data to the user. Following is the format of the checkboxes:

<div class="row">
        <div class="column eight wide">
          {{t 'New Paper is Submitted to your Event'}}
        </div>
        <div class="ui column eight wide right aligned">
          {{ui-checkbox class='toggle' checked=preference.newPaper onChange=(pipe-action (action (mut preference.newPaper)) (action 'savePreference' preference))}}
        </div>
      </div>
      <div class="row">
        <div class="column eight wide">
          {{t 'Change in Schedule of Sessions in your Event'}}
        </div>
        <div class="ui column eight wide right aligned">
          {{ui-checkbox class='toggle' checked=preference.sessionSchedule onChange=(pipe-action (action (mut preference.sessionSchedule)) (action 'savePreference' preference))}}
        </div>
      </div>

The states of the checkboxes are determined by the data that we receive from the API. For example, if for a record, the API returns:

nextEvent           : true,
newPaper            : false,
sessionAcceptReject : true,
sessionSchedule     : false,
afterTicketPurchase : false,

Then, the respective states will be shown by the checkbox and the user can toggle the states to change the email-preferences as they want.

Thus to get the data sent by the server to the client, we return it as a model and query it as:

model() {
    return this.get('authManager.currentUser').query('emailNotifications', { include: 'event' });
}

As we can see, as mentioned earlier, we kept the relationships so that we can query the email-notifications specific to the particular user or specific to particular event. Here, we are showing a user’s email-notifications and hence we queried it with the user relationship.
The authManager loads the currentUser and queries the email-notifications for a particular use. We also want the event details to show the email-preferences, hence we include the event model to be fetched in the query also.

We also let the user change the preferences of the email-notifications so that he can customise the notifications and keep the ones he wants to receive. We implement the updating of email-preferences API as follows:

Whenever a user toggles the checkbox, we are having an action as called ‘savePreference’, which handles the updation of the preferences.

 

{{ui-checkbox class='toggle' checked=preference.newPaper onChange=(pipe-action (action (mut preference.newPaper)) (action 'savePreference' preference))}}

savePreference(emailPreference) {
      emailPreference.save()
        .then(() => {
          this.get('notify').success(this.l10n.t('Email notifications updated successfully'));
        })
        .catch(() => {
          emailPreference.rollbackAttributes();
          this.get('notify').error(this.l10n.t('An unexpected error occurred.'));
        });
    }

We are passing the parameter(the whole preference object to the action), and then just performing a ‘save’ method on it which will send a PATCH request to the server to update the data.

Thus, in this way, the user can change the email-notification preferences in the Open Event Frontend.

Resources:
Ember data Official guide
Blog on Models and Ember data by Daniel Lavigne: Learning Ember.js Part 4: Models

Source code: https://github.com/fossasia/open-event-frontend/pull/537/files