How Errors from Server Side are Handled On Open Event Frontend

This blog article will illustrate how the various error or status codes are handled  in  Open Event Frontend, and how the appropriate response is generated corresponding to those error codes. Open Event Frontend, relies on Open Event Server for all server operations. Open Event Server exposes  a well documented JSON:API Spec Compliant REST API. The clients using the api primarily interact with it using GET, POST , PATCH and DELETE requests. And thus for each request the API returns corresponding data as response along with it’s status code.

For instance whenever the app opens, for the landing page, all the events are fetched by making a GET request to the end point v1/events. If the request is successful and events data is returned, the status code is 200 which stands for OK in the http standard set by IANA.

Fig 1: Screenshot of google chrome developer consoles’ networking tab while making a request.

Since Open Event server is compliant with JSON:API Spec, to quote it’s official documentation, “Error objects MUST be returned as an array keyed by errors in the top level of a JSON API document.” Thus whenever there is an error, or the request is unsuccessful due to a variety of reasons, the server has a predefined format to convey the information to the front end.

The process is illustrated by the reset password form on open event frontend. When a user forgets his password, he/she has the option to reset it, using his email address. Thus the form just takes in the email address of the user and makes a POST request to the reset-password API endpoint of the server.

  • Once the request is made there are 3 possibilities (check references for error code significance):
    The request is successful and a status code of 200 is returned.
  • The email address user entered doesn’t exists and no record is found in the database. 422 status code should be returned.
  • The server is down, or the request is invalid (something unexpected has occurred). In all such scenarios error code 404 should be returned.

this.get('loader')
         .post('auth/reset-password', payload)
         .then(() => {
           this.set('successMessage', this.l10n.t('Please go to the link sent to your     

           email to reset your password'));
         })
         .catch(reason => {
           if (reason && reason.hasOwnProperty('errors') && reason.errors[0].status

               === 422) {
             this.set('errorMessage', this.l10n.t('No account is registered with this

                      email address.'));
           } else {
             this.set('errorMessage', this.l10n.t('An unexpected error occurred.'));
           }
         })
         .finally(()=> {
           this.set('isLoading', false);
         }
         );
Figure 2 : The reset password UI

Thus as mentioned in the JSON:API docs, the errors property is expected to contain the status code and error message(optional) , which ember handles via the the catch block. The catch block is executed whenever the response from the request is not successful. The contents of the response are present in the reason property. If the status of the error is 422, the corresponding message is stored inside the errorMessage property of the component which is further used to display the alert by rendering an error block on the forgot password form.

In case there is no error, the errorMessage is undefined, and the error block is not rendered at all. In case of any other unexpected error, the standard text is displayed by initialising the errorMessage property to it.

Resources

How the Form Mixin Enhances Validations in Open Event Frontend

This blog article will illustrate how the various validations come together in  Open Event Frontend, in a standard format, strongly reducing the code redundancy in declaring validations. Open Event Frontend, offers high flexibility in terms of validation options, and all are stored in a convenient object notation format as follows:

getValidationRules() {
return {
  inline : true,
  delay  : false,
  on     : 'blur',
  fields : {
    identification: {
      identifier : 'email',
      rules      : [
        {
          type   : 'empty',
          prompt : this.l10n.t('Please enter your email ID')
        },
        {
          type   : 'email',
          prompt : this.l10n.t('Please enter a valid email ID')
        }
      ]
    },
    password: {
      identifier : 'password',
      rules      : [
        {
          type   : 'empty',
          prompt : this.l10n.t('Please enter your password')
        }
      ]
    }
  }
};
}

Thus the validations of a form are stored as objects, where the  identifier attribute determines which field to apply the validation conditions to. The rules array contains all the rules to be applied to the determined object. Within rules, the type represents the kind of validation, whereas the prompt attribute determines what message shall be displayed in case there is a violation of the validation rule.

 

NOTE: In case an identifier is not specified, then the name of the rule object is itself considered as the identifier.

These validations are in turn implemented by the FormMixin. The various relevant sections of the mixin will be discussed in detail. Please check references for complete source code of the mixin.

getForm() {
return this.get($form);
}

The getForm function returns the calling component’s entire form object on which the validations are to be applied.

onValid(callback) {
this.getForm().form('validate form');
if (this.getForm().form('is valid')) {
  callback();
}
}

The onValid function serves as the boundary level function, and is used to specify what should happen when the validation is successful. It expects a function object as i’s argument. It makes use of the getForm() function to retrieve the form and using semantic UI, determines if all the validations have been successful. In case they have, it calls the callback method passed down to it as the argument.

Next transitioning into the actual implementation, certain behavioral traits of the validations are controlled by the two global variables autoScrollToErrors and autoScrollSpeed. The former is a boolean, which is set to true if the application is designed in such a way that once the user presses the submit button and a validation fails, the browser scrolls to the first field whose validation failed with a speed specified by the latter.

Next, comes the heart of the mixin. Since it needs to be continuously determined if a field’s validation has failed, the entire logic of checking is placed inside a debounce call which is supported by ember to continuously execute a function with a gap of a certain specified time (400 ms in this case). Since the validity can be checked only after all the fields have rendered, the debounce call is placed inside a didRender call. The didRender call ensures that any logic inside it is executed only after all the elements of the relevant component have been rendered and are a part of the DOM.

didRender() {
. . .
debounce(this, () => {
  const defaultFormRules = {
    onFailure: formErrors => {
      if (this.autoScrollToErrors) {
        // Scroll to the first error message
        if (formErrors.length > 0) {
          $('html,body').animate({
            scrollTop: this.$(`div:contains('${formErrors[0]}')`).offset().top
          }, this.autoScrollSpeed);
        }
      }
    }
  };
}, 400); . . .}

 

onFailure is an ES6 syntax function, which takes all the form errors as its arguments. And in case the autoScrollToErrors global variable is set to true, it checks if there are any errors or the length of the formErrors array is more than 0. In case there are errors, it makes a call to animate callback, and scrolls to the very first field which has an error with the speed defined by the previously discussed global autoScrollSpeed . The fact that the very first field is selected for scrolling off to, is ensured by the index 0 specified (formErrors[0]). Thus role of the mixin is to continuously check for any validations and in case there are, scroll to them.

Resources

How App Social Links are specified in Open Event Frontend

This blog article will illustrate how the various social links are specified in the the footer of Open Event Frontend, using the settings API. Open Event Frontend, offers high flexibility to the admins regarding the settings of the App, and hence the media links are not hard coded, and can be changed easily via the admin settings panel.

The primary end point of Open Event API with which we are concerned with for fetching the settings  for the app is

GET /v1/settings

The model for settings has the following fields which concern the social links.

 googleUrl              : attr('string'),
 githubUrl              : attr('string'),
 twitterUrl             : attr('string')

Next we define them as segmented URL(s) so that they can make use of the link input widget.

segmentedTwitterUrl    : computedSegmentedLink.bind(this)('twitterUrl'),
 segmentedGoogleUrl     : computedSegmentedLink.bind(this)('googleUrl'),
 segmentedGithubUrl     : computedSegmentedLink.bind(this)('githubUrl'),

Now it is required for us to fetch the data from the API, by making the corresponding call to the API. Since the footer is present in every single page of the app, it is necessary that we make the call from the application route itself. Hence we add the following to the application route modal.

socialLinks: this.get('store').queryRecord('setting', {
})

Next we need to iterate over these social links, and add them to the footer as per their availability.So we will do so by first passing the model to the footer component, and then iterating over it in footer.hbs

{{footer-main socialLinks=model.socialLinks footerPages=footerPages}}


And thus we have passed the socialLinks portion of the model, under the alias socialLinks.Next, we iterate over them and each time check, if the link exists before rendering it.

<div class="three wide column">
     <div class="ui inverted link list">
       <strong class="item">{{t 'Connect with us'}}</strong>
       {{#if socialLinks.supportUrl}}
         <a class="item" href="{{socialLinks.supportUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="info icon"></i> {{t 'Support'}}
         </a>
       {{/if}}
       {{#if socialLinks.facebookUrl}}
         <a class="item" href="{{socialLinks.facebookUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="facebook f icon"></i> {{t 'Facebook'}}
         </a>
       {{/if}}
       {{#if socialLinks.youtubeUrl}}
         <a class="item" href="{{socialLinks.youtubeUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="youtube icon"></i> {{t 'Youtube'}}
         </a>
       {{/if}}
       {{#if socialLinks.googleUrl}}
         <a class="item" href="{{socialLinks.googleUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="google plus icon"></i> {{t 'Google +'}}
         </a>
       {{/if}}
     </div>
   </div>

Thus all the links in the app are easily manageable, from the admin settings menu, without the need of hard coding them. This approach also, makes it easy to preserve the configuration in a central location.

Resources

Keeping Order of tickets in Event Wizard in Sync with API on Open Event Frontend

This blog article will illustrate how the various tickets are stored and displayed in order the event organiser decides  on  Open Event Frontend and also, how they are kept in sync with the backend.

First we will take a look at how the user is able to control the order of the tickets using the ticket widget.

{{#each tickets as |ticket index|}}
  {{widgets/forms/ticket-input ticket=ticket
  timezone=data.event.timezone
  canMoveUp=(not-eq index 0)
  canMoveDown=(not-eq ticket.position (dec
  data.event.tickets.length))
  moveTicketUp=(action 'moveTicket' ticket 'up')
  moveTicketDown=(action 'moveTicket' ticket 'down')
  removeTicket=(confirm 'Are you sure you  wish to delete this 
  ticket ?' (action 'removeTicket' ticket))}}
{{/each}}

The canMoveUp and canMoveDown are dynamic properties and are dependent upon the current positions of the tickets in the tickets array.  These properties define whether the up or down arraow or both should be visible alongside the ticket to trigger the moveTicket action.

There is an attribute called position in the ticket model which is responsible for storing the position of the ticket on the backend. Hence it is necessary that the list of the ticket available should always be ordered by position. However, it should be kept in mind, that even if the position attribute of the tickers is changed, it will not actually change the indices of the ticket records in the array fetched from the API. And since we want the ticker order in sync with the backend, i.e. user shouldn’t have to refresh to see the changes in ticket order, we are going to return the tickets via a computed function which sorts them in the required order.

tickets: computed('data.event.tickets.@each.isDeleted', 'data.event.tickets.@each.position', function() {
   return this.get('data.event.tickets').sortBy('position').filterBy('isDeleted', false);
 })

The sortBy method ensures that the tickets are always ordered and this computed property thus watches the position of each of the tickets to look out for any changes. Now we can finally define the moveTicket action to enable modification of position for tickets.

moveTicket(ticket, direction) {
     const index = ticket.get('position');
     const otherTicket = this.get('data.event.tickets').find(otherTicket => otherTicket.get('position') === (direction === 'up' ? (index - 1) : (index + 1)));
     otherTicket.set('position', index);
     ticket.set('position', direction === 'up' ? (index - 1) : (index + 1));
   }

The moveTicket action takes two arguments, ticket and direction. It temporarily stores the position of the current ticket and the position of the ticket which needs to be swapped with the current ticket.Based on the direction the positions are swapped. Since the position of each of the tickets is being watched by the tickets computed array, the change in order becomes apparent immediately.

Now when the User will trigger the save request, the positions of each of the tickets will be updated via a PATCH or POST (if the ticket is new) request.

Also, the positions of all the tickets maybe affected while adding a new ticket or deleting an existing one. In case of a new ticket, the position of the new ticket should be initialised while creating it and it should be below all the other tickets.

addTicket(type, position) {
     const salesStartDateTime = moment();
     const salesEndDateTime = this.get('data.event.startsAt');
     this.get('data.event.tickets').pushObject(this.store.createRecord('ticket', {
       type,
       position,
       salesStartsAt : salesStartDateTime,
       salesEndsAt   : salesEndDateTime
     }));
   }

Deleting a ticket requires updating positions of all the tickets below the deleted ticket. All of the positions need to be shifted one place up.

removeTicket(deleteTicket) {
     const index = deleteTicket.get('position');
     this.get('data.event.tickets').forEach(ticket => {
       if (ticket.get('position') > index) {
         ticket.set('position', ticket.get('position') - 1);
       }
     });
     deleteTicket.deleteRecord();
   }

The tickets whose position is to be updated are filtered by comparison of their position from the position of the deleted ticket.

Resources

Implementing Roles API on Open Event Frontend to Create Roles Using an External Modal

This blog article will illustrate how the roles are created via the external model  on the admin permissions page in Open Event Frontend, using the roles API. Our discussion primarily will involve the admin/permissions/index route to illustrate the process.The primary end point of Open Event API with which we are concerned with for fetching the permissions  for a user is

POST /v1/roles

First we need to create a model for the user-permissions, which will have the fields corresponding to the api, so we proceed with the ember CLI command:

ember g model role

Next we define the model according to the requirements. The model needs to extend the base model class, and has only two fields one for the title and one for the actual name of the role.

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';

export default ModelBase.extend({
 name           : attr('string'),
 titleName      : attr('string')
 });

Next we need to modify the existing modal to incorporate the API and creation of roles in it. It is very important to note here that using createRecord as the model will result in a major flaw. If createRecord is used and the user tries to create multiple roles, other than the first POST request all the subsequent requests will be PATCH requests and will keep on modifying the same role. To avoid this, a new record needs to be created every time the user clicks on Add Role.  We slightly modify the modal component call to pass in the name and titleName to it.

{{modals/add-system-role-modal  isOpen=isAddSystemRoleModalOpen
                                isLoading=isLoading
                                name=name
                                titleName=titleName
                                addSystemRole=(action 'addSystemRole')}}

Upon entering the details of the roles and successful validation of the form, if the user clicks the Add Role button of the modal, the action addSystemRole will be triggered. We will write the entire logic for the same in the respective controller of the route.

addSystemRole() {
     this.set('isLoading', true);
     this.get('store').createRecord('role', {
       name      : this.get('name'),
       titleName : this.get('titleName')
     }).save()
       .then(() => {
         this.set('isLoading', false);
         this.notify.success(this.l10n.t('User permissions have 
         been saved successfully.'));
         this.set('isAddSystemRoleModalOpen', false);
         this.setProperties({
           name          : null,
           roleTitleName : null
         });
       })
       .catch(()=> {
         this.set('isLoading', false);
         this.notify.error(this.l10n.t('An unexpected error has occurred.
         User permissions not saved.'));
       });
   },

At first the isLoading property is made true.This adds the semantic UI class loading to the the form,  and so the form goes in the loading state, Next, a record is created of the type role  and it’s properties are made equal to the corresponding values entered by the user.

Then save() is called, which subsequently makes a POST request to the server. If the request is successful the modal is closed by setting the isAddSystemRoleModalOpen property to false. Also, the fields of the modal are cleared for a  better user experience in case multiple roles need to be added one after the other.

In cases when  there is an error during the processing of the request the catch() block executes. And the modal is not closed. Neither are the fields cleared.

Resources

Implementing Admin Statistics Mail and Session API on Open Event Frontend

This blog article will illustrate how the admin-statistics-mail and admin-statistics-session API  are implemented on the admin dashboard page in Open Event Frontend.Our discussion primarily will involve the admin/index route to illustrate the process.The primary end points of Open Event API with which we are concerned with for fetching the admin statistics  for the dashboard are

GET /v1/admin/statistics/mails
GET /v1/admin/statistics/sessions

First we need to create the corresponding models according to the type of the response returned by the server , which in this case will be admin-statistics-event and admin-statistics-sessions, so we proceed with the ember CLI commands:

ember g model admin-statistics-mail
ember g model admin-statistics-session

Next we define the model according to the requirements. The model needs to extend the base model class, and all the fields will be number since the all the data obtained via these models from the API will be numerical statistics

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';

export default ModelBase.extend({
 oneDay     : attr('number'),
 threeDays  : attr('number'),
 sevenDays  : attr('number'),
 thirtyDays : attr('number')
});

And the model for sessions will be the following. It too will consist all the attributes of type number since it represents statistics

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';

export default ModelBase.extend({
 confirmed : attr('number'),
 accepted  : attr('number'),
 submitted : attr('number'),
 draft     : attr('number'),
 rejected  : attr('number'),
 pending   : attr('number')
});

Now we need to load the data from the api using the models, so will send a get request to the api to fetch the current permissions. This can be easily achieved via a store query in the model hook of the admin/index route.However this cannot be a normal get request. Because the the urls for the end point are /v1/admin/statistics/mails & /v1/admin/statistics/sessions but there are no relationships between statistics and various sub routes, which is what ember’s default behaviour would expect.

Hence we need to override the generated default request url using custom adapters and use buildUrl method to customize the request urls.

import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
 buildURL(modelName, id, snapshot, requestType, query) {
   let url = this._super(modelName, id, snapshot, requestType, query);
   url = url.replace('admin-statistics-session', 'admin/statistics/session');
   return url;
 }
});

The buildURL method replaces the the default  URL for admin-statistics-session  with admin/statistics/session otherwise the the default request would have been

GET v1/admin-statistics-session

Similarly it must be done for the mail statistics too. These will ensure that the correct request is sent to the server. Now all that remains is making the requests in the model hooks and adjusting the template slightly for the new model.

model() {
   return RSVP.hash({
         mails: this.get('store').queryRecord('admin-statistics-mail', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     }),
     sessions: this.get('store').queryRecord('admin-statistics-session', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
   });
 }


queryRecord is used instead of query because only a single record is expected to be returned by the API.

Resources

Tags :

Open event, Open event frontend, ember JS, ember service, semantic UI, ember-data, ember adapters,  tickets, Open Event API, Ember models

Implementing User Permissions API on Open Event Frontend to View and Update User Permission Settings

This blog article will illustrate how the user permissions  are displayed and updated on the admin permissions page in Open Event Frontend, using the user permissions API. Our discussion primarily will involve the admin/permissions/index route to illustrate the process.

The primary end point of Open Event API with which we are concerned with for fetching the permissions  for a user is

GET /v1/user-permissions

First we need to create a model for the user-permissions, which will have the fields corresponding to the api, so we proceed with the ember CLI command:

ember g model user-permission

Next we define the model according to the requirements. The model needs to extend the base model class, and other than the name and description all the fields will be boolean since the user permissions frontend primarily consists of checkboxes to grant and revoke permissions. Hence the model will be of the following format.

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';

export default ModelBase.extend({
 name           : attr('string'),
 description    : attr('string'),
 unverifiedUser : attr('boolean'),
 anonymousUser  : attr('boolean')
});

Now we need to load the data from the api using this model, so will send a get request to the api to fetch the current permissions. This can be easily achieved via a store query in the model hook of the admin/permissions/system-roles route. It is important to note here, that findAll is preferred over an empty query. To quote the source of this information,

The reason findAll is preferred over query when no filtering is done is, query will always make a server request. findAll on the other hand, will not make a server request if findAll has already been used once somewhere before. It’ll re-use the data already available whenever possible.

model() {
   return this.get('store').findAll('user-permission');
 }

The user permissions form is not a separate component and is directly embedded in the route template hence, there is no need to explicitly pass the model, it will be available in the route template by default. And can be used as following:

{{#each model as |userPermission|}}
<tr>
  <td>
    {{userPermission.name}}
    <div class="muted text">
      {{userPermission.description}}
    </div>
  </td>
  <td>
     {{ui-checkbox label=(t 'Unverified User') checked=userPermission.unverifiedUser onChange=(action (mut userPermission.unverifiedUser))}}
  </td>
  <td>
    {{ui-checkbox label=(t 'Anonymous User') checked=userPermission.anonymousUser onChange=(action (mut userPermission.anonymousUser))}}
  </td>
</tr>
{{/each}}

In the template after mutating the model’s values according to whether the checkboxes are checked or not, the only thing left is triggering the update action in the controller which will be triggered with the default submit action of the form.

updatePermissions() {
     this.set('isLoading', true);
     this.get('model').save()
       .then(() => {
         this.set('isLoading', false);
         this.notify.success(this.l10n.t('User permissions have been saved successfully.'));
       })
       .catch(()=> {
         this.set('isLoading', false);
         this.notify.error(this.l10n.t('An unexpected error has occurred. User permissions not saved.'));
       });
   }

The controller action first sets the isLoading property to true. This adds the semantic UI class loading to the the form,  and so the form goes in the loading state, to let the user know the request is being processed. Then the save()  call occurs and this makes a PATCH request to the API to update the values stored inside the database. And if the PATCH request is successful, the .then() clause executes, which in addition to setting the isLoading as false, notifies the user that the settings have been saved  successfully using the notify service.

However, in case there is an unexpected error and the PATCH request fails, the .catch() executes. After setting isLoading to false, it notifies the user of the error via an error notification.

Resources

 

 

Stubbed Routing Inbuilt Service used in Open Event Frontend

In Open Event Frontend, we have used services like ‘auth-manager’, ‘l10n’, ‘loader’, ‘sanitizer’, etc to ease our work with the help of predefined-functions in those services. However, while dealing with an issue in the project, there was a need to use ‘Routing’ as a service.

In the issue, we wanted to generate an access link dynamically from the access code entered by the user. The format of the access link was as follows:

“base_url + event_id + access_code”

So, for the above URL, we needed to have ‘event_id’ and ‘access_code’.

The ‘access_code’ can be readily accessed from the user’s input itself, whereas to get the event_id, we used the ‘Routing’ service in Ember.

Generally to use a service in Ember, it has to be written first,then registered, injected and then used.

‘Routing’ service in Ember is an inbuilt service unlike the ones listed at the beginning.

There is no need to write it. It can be simply registered, injected and used.

this.register('service:routing', routingStub);
this.inject.service('routing', { as: 'routing' });

where ‘register’ and ‘inject’ are the methods on Ember objects.

The integration tests in Open Event Frontend are written such that the services can be used without injecting, but the tests will fail. To pass those tests, we had to register and inject the service in the required component.

The Routing service could thus be registered and injected into the specific component( injection in the component’s integration test ) only but for future needs, this service might be needed in any other component too. For this purpose, this service was registered and injected in ‘component-helper.js’.

const routingStub = Service.extend({
  router: {
    router: {
      state: {
        params: {
          'events.view': {
            event_id: 1
          }
        }
      }
    },
    generate() {
      return 'http://dummy-url.com';
    }
  }
});


export default function(path, name, testCase = null) {
  moduleForComponent(path, name, {
    integration: true,

    beforeEach() {
      this.register('service:routing', routingStub);
      this.inject.service('routing', { as: 'routing' });
      this.register('service:l10n', L10n);
      this.inject.service('l10n', { as: 'l10n' });
      this.application = startApp();
      l10nTestHelper(this);
      run(() => fragmentTransformInitializer.initialize(getOwner(this)));
    }
  }
}

Stubbing a Service: This is a process of faking an app of importing a service when no path is available to import. Stubbing of a service is mainly done when one needs to deal with the testing of the app. In our case, the same is done. We have stubbed the ‘Routing’ service in order to deal with the testing part. It can be seen from the above code that we have generated a ‘routingStub’ which fakes the app while registering the service in the ‘beforeModel’. The next line of code shows the ‘injection’ of service into the app.

Now we are just left with one task i.e to pass ‘routing’ from our integration tests to the component.

test('it renders', function(assert) {
  this.render(hbs`{{forms/events/view/create-access-code routing=routing}}`);
  assert.ok(this.$().html().trim().includes('Save'));
});

Above code shows the same.

Thus we can stub the services in Ember when any component depends on them.

Resources:

Official Ember guide: https://guides.emberjs.com/v2.1.0/testing/testing-components

Blog by Todd Jordan: http://presentationtier.com/stubbing-services-in-emberjs-integration-tests/

Source codehttps://github.com/sumedh123/open-event-frontend/blob/0b193ca679ce3b51f65e19ee0d03ac6a679258de/tests/helpers/component-helper.js

Implementing Settings API on Open Event Frontend to View and Update Admin Settings

This blog article will illustrate how the admin settings are displayed and updated on the admin settings page in Open Event Frontend, using the settings API. It will also illustrate the use of the notification service to display corresponding notifications on whether the update operation is successful or not. Our discussion primarily will involve the admin/settings/index route to illustrate the process, all other admin settings route work exactly the same way.

The primary end point of Open Event API with which we are concerned with for fetching tickets for an event is

GET /v1/settings

Since there are multiple  routes under admin/settings  including admin/settings/index, and they all will share the same setting model, it is efficient to make the call for Event on the settings route, rather than repeating it for each sub route, so the model for settings route is:

model() {
 return this.store.queryRecord(setting, {});
}

It is important to note that, we need not specify the model for index route or in fact for any of the sub routes of settings.  This is because it is the default behaviour of ember that if the model for a route is not found, it will automatically look for it in the parent  route.  

And hence all that is needed to be done to make the model available in the system settings form  is to pass it while calling the form component.

<div class="ui basic {{if isLoading 'loading' ''}} segment">
 {{forms/admin/settings/system-form save='updateSettings' settings=model}}
</div>

Thus the model properties will be available in the form via settings alias. Next, we need to bind the value property  of the input fields to the corresponding model properties.  Here is a sample snippet on so as to how to achieve that, for the full code please refer to the codebase or the resources below.

<div class="field">
 {{ui-radio label=(t 'Development') current=settings.appEnvironment name='environment' value='development' onChange=(action (mut settings.appEnvironment))}}
</div>
<div class="field">
 {{ui-radio label=(t 'Staging') current=settings.appEnvironment name='environment' value='staging'}}
</div>
<div class="field">
 {{ui-radio label=(t 'Production') current=settings.appEnvironment name='environment' value='production'}}
</div>
<div class="field">
 <label>
   {{t 'App Name'}}
 </label>
 {{input type='text' name='app_name' value=settings.appName}}
</div>
<div class="field">
 <label>
   {{t 'Tagline'}}
 </label>
 {{input type='text' name='tag_line' value=settings.tagline}}
</div>

In the example above, appName, tagLine and appEnvironment are binded to the actual properties in the model. After the required changes have been done, the user next submits the form which triggers the submit action. If the validation is successful, the action updateSettings residing in the controller of the route is triggered, this is where the primary operations happen.

updateSettings() {
 this.set('isLoading', true);
 let settings = this.get('model');
 settings.save()
   .then(() => {
     this.set('isLoading', false);
     this.notify.success(this.l10n.t('Settings have been saved successfully.'));
   })
   .catch(()=> {
     this.set('isLoading', false);
     this.notify.error(this.l10n.t('An unexpected error has occured. Settings not saved.'));
   });
}

The controller action first sets the isLoading property to true. This adds the semantic UI class loading to the segment containing the form, and it and so the form goes in the loading state, to let the user know the requests is being processed. Then the save()  call occurs and this makes a PATCH request to the API to update the values stored inside the database. And if the PATCH request is successful, the .then() clause executes, which in addition to setting the isLoading as false.

However, in case there is an unexpected error and the PATCH request fails, the .catch() executes. After setting isLoading to false, it notifies the user of the error via an error notification.

Resources

Implementing Tickets API on Open Event Frontend to Display Tickets

This blog article will illustrate how the tickets are displayed on the public event page in Open Event Frontend, using the tickets API. It will also illustrate the use of the add on, ember-data-has-query, and what role it will play in fetching data from various APIs. Our discussion primarily will involve the public/index route. The primary end point of Open Event API with which we are concerned with for fetching tickets for an event is

GET /v1/events/{event_identifier}/tickets

Since there are multiple  routes under public  including public/index, and they share some common event data, it is efficient to make the call for Event on the public route, rather than repeating it for each sub route, so the model for public route is:

model(params) {
return this.store.findRecord('event', params.event_id, { include: 'social-links' });
}

This modal takes care of fetching all the event data, but as we can see, the tickets are not included in the include parameter. The primary reason for this is the fact that the tickets data is not required on each of the public routes, rather it is required for the index route only. However the tickets have a has-many relationship to events, and it is not possible to make a call for them without calling in the entire event data again. This is where a really useful addon, ember-data-has-many-query comes in.

To quote the official documentation,

Ember Data‘s DS.Store supports querying top-level records using the query function.However, DS.hasMany and DS.belongsTo cannot be queried in the same way.This addon provides a way to query has-many and belongs-to relationships

So we can now proceed with the model for public/index route.

model() {
const eventDetails = this._super(...arguments);
return RSVP.hash({
  event   : eventDetails,
  tickets : eventDetails.query('tickets', {
    filter: [
      {
        and: [
          {
            name : 'sales-starts-at',
            op   : 'le',
            val  : moment().toISOString()
          },
          {
            name : 'sales-ends-at',
            op   : 'ge',
            val  : moment().toISOString()
          }
        ]
      }
    ]
  }),

We make use of this._super(…arguments) to use the event data fetched in the model of public route, eliminating the need for a separate API call for the same. Next, the ember-has-many-query add on allows us to query the tickets of the event, and we apply the filters restricting the tickets to only those, whose sale is live.
After the tickets are fetched they are passed onto the ticket list component to display them. We also need to take care of the cases, where there might be no tickets in case the event organiser is using an external ticket URL for ticketing, which can be easily handled via the is-ticketing-enabled property of events. And in case they are not enabled we don’t render the ticket-list component rather a button linked to the external ticket URL is rendered.  In case where ticketing is enabled the various properties which need to be computed such as the total price of tickets based on user input are handled by the ticket-list component itself.

{{#if model.event.isTicketingEnabled}}
  {{public/ticket-list tickets=model.tickets}}
{{else}}
<div class="ui grid">
  <div class="ui row">
      <a href="{{ticketUrl}}" class="ui right labeled blue icon button">
        <i class="ticket icon"></i>
        {{t 'Order tickets'}}
      </a>
  </div>
  <div class="ui row muted text">
      {{t 'You will be taken to '}} {{ticketUrl}} {{t ' to complete the purchase of tickets'}}
  </div>
</div>
{{/if}}

This is the most efficient way to fetch tickets, and also ensures that only the relevant data is passed to the concerned ticket-list component, without making any extra API calls, and it is made possible by the ember-data-has-many-query add on, with very minor changes required in the adapter and the event model. All that is required to do is make the adapter and the event model extend the RestAdapterMixin and ModelMixin provided by the add on, respectively.

Resources