Connecting SUSI iOS App to SUSI Smart Speaker

SUSI Smart Speaker is an Open Source speaker with many exciting features. The user needs an Android or iOS device to set up the speaker. You can refer this post for initial connection to SUSI Smart Speaker. In this post, we will see how a user can connect SUSI Smart Speaker to iOS devices (iPhone/iPad).

Implementation –

The first step is to detect whether an iOS device connects to SUSI.AI hotspot or not. For this, we match the currently connected wifi SSID with SUSI.AI hotspot SSID. If it matches, we show the connected device in Device Activity to proceed further with setups.

Choosing Room –

Room name is basically the location of your SUSI Smart Speaker in the home. You may have multiple SUSI Smart Speaker in different rooms, so the purpose of adding the room is to differentiate between them.

When the user clicks on Wi-Fi displayed cell, it starts the initial setups. We are using didSelectRowAt method of UITableViewDelegate to get which cell is selected. On clicking the displayed Wi-Fi cell, a popup is open with a Room Location Text field.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0, let speakerSSID = fetchSSIDInfo(), speakerSSID == ControllerConstants.DeviceActivity.susiSSID {
// Open a popup to select Rooms
presentRoomsPopup()
}
}

When the user clicks the Next button, we send the speaker room location to the local server of the speaker by the following API endpoint with room name as a parameter:

http://10.0.0.1:5000/speaker_config/

Refer this post for getting more detail about how choosing room work and how it is implemented in SUSI iOS.

Sharing Wi-Fi Credentials –

On successfully choosing the room, we present a popup that asks the user to enter the Wi-Fi credentials of previously connected Wi-Fi so that we can connect our Smart Speaker to the wifi which can provide internet connection to play music and set commands over the speaker.

We present a popup with a text field for entering wifi password.

When the user clicks the Next button, we share the wifi credentials to wifi by the following API endpoint:

http://10.0.0.1:5000/wifi_credentials/

With the following params-

  1. Wifissid – Connected Wi-Fi SSID
  2. Wifipassd – Connected Wi-Fi password

In this API endpoint, we are sharing wifi SSID and wifi password with Smart Speaker. If the credentials successfully accepted by speaker than we present a popup for user SUSI account password, otherwise we again present Enter Wifi Credentials popup.

Client.sharedInstance.sendWifiCredentials(params) { (success, message) in
DispatchQueue.main.async {
self.alertController.dismiss(animated: true, completion: nil)
if success {
self.presentUserPasswordPopup()
} else {
self.view.makeToast("", point: self.view.center, title: message, image: nil, completion: { didTap in
UIApplication.shared.endIgnoringInteractionEvents()
self.presentWifiCredentialsPopup()
})
}
}
}

 

Sharing SUSI Account Credentials –

In the method above we have seen that when SUSI Smart Speaker accept the wifi credentials, we proceed further with SUSI account credentials. We open a popup to Enter user’s SUSI account password:

When the user clicks the Next button, we use following API endpoint to share user’s SUSI account credentials to SUSI Smart Speaker:

http://10.0.0.1:5000/auth/

With the following params-

  1. email
  2. password

User email is already saved in the device so the user doesn’t have to type it again. If the user credentials successfully accepted by speaker then we proceed with configuration process otherwise we open up Enter Password popup again.

Client.sharedInstance.sendAuthCredentials(params) { (success, message) in
DispatchQueue.main.async {
self.alertController.dismiss(animated: true, completion: nil)
if success {
self.setConfiguration()
} else {
self.view.makeToast("", point: self.view.center, title: message, image: nil, completion: { didTap in
UIApplication.shared.endIgnoringInteractionEvents()
self.presentUserPasswordPopup()
})
}
}
}

 

Setting Configuration –

After successfully sharing SUSI account credentials, following API endpoint is using for setting configuration.

http://10.0.0.1:5000/config/

With the following params-

  1. sst
  2. tts
  3. hotword
  4. wake

The success of this API call makes successfully connection between user iOS Device and SUSI Smart Speaker.

Client.sharedInstance.setConfiguration(params) { (success, message) in
DispatchQueue.main.async {
if success {
// Successfully Configured
self.isSetupDone = true
self.view.makeToast(ControllerConstants.DeviceActivity.doneSetupDetailText)
} else {
self.view.makeToast("", point: self.view.center, title: message, image: nil, completion: { didTap in
UIApplication.shared.endIgnoringInteractionEvents()
})
}
}
}

After successful connection-

 

Resources –

  1. Apple’s Documentation of tableView(_:didSelectRowAt:) API
  2. Initial Setups for Connecting SUSI Smart Speaker with iPhone/iPad
  3. SUSI Linux Link: https://github.com/fossasia/susi_linux
  4. Adding Option to Choose Room for SUSI Smart Speaker in iOS App

Detecting Barometer sensor in PSLab Android App

The Pocket Science Lab Android app has Barometer Instrument implemented in it. Although the instrument  is currently working through the mobile sensors and not the PSLab i2c library as there were some issues in the i2c communication with PSLab device.

Thus as the barometer was completely working on through the mobile sensors, there was a major problem coming up. Majority of the mobiles don’t have the barometer sensor which was required, only a  few of the latest devices have the sensors in them.

This issues created problem as now anyone who would have used the barometer instrument would have made an impression that the App was itself not working.

Figure(1) : Showing the stagnant barometer Instrument

Thus this created a bad impression for both the app and it’s developers.

Solving the issue

To solve this major bug, it required to first detect the barometer instrument and then implementing an alert dialog-box showing that barometer sensor is not present in his device.

  • Detecting the Barometer Sensor[2]

The barometer sensor was detected using the sensorManager class of Java

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
sensor = sensorManager != null ? sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE) : null;

Thus using this code the barometer sensor is detected, if sensor was not present the  sensorManger would be null.

  • Implementing the alert-box[1]

Thus if the sensorManger variable was null it notified that the sensor was not present in           the device and corresponding to which an alert-box was implemeneted

if (sensor == null) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            if (!isFinishing()) {
                new AlertDialog.Builder(BarometerActivity.this)
                    .setTitle(R.string.barometer_alert_title)
                    .setMessage(R.string.barometer_alert_description)
                    .setCancelable(false)
                    .setPositiveButton("ok", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                        }
                    }).show();
            }
        }
    });

Thus as we can see in the code snippet the alert dialog-box will appear if  sensor is not present as shown in figure (2).

Figure(2) : Screenshot showing alert-box

Resources

  1. Android Alert-box Example, Mkyong.com:
    https://www.mkyong.com/android/android-alert-dialog-example/
  2. Creating a Barometer Application for Android, medium.com:
    https://medium.com/@ssaurel/creating-a-barometer-application-for-android-1c0a5c10b20e

Implementing Custom Rotary Knobs and Circular Positioning in the Multimeter in the PSLab Android App

In my previous blog [2I have discussed about how to implement  normal rotary knob using an open source library, this blog will be about the new user interface (UI) of multimeter in the PSLab Android app, how a custom rotary knob is implemented in it and how how the text views are positioned circular in them.

Implementation of Custom Rotary Knob

In the PSLab device  the rotary knob is implemented using the BeppiMenozzi Knob library[1] as by doing this we don’t have to manually create the extra class for the knob and we don’t have to write the code from scratch.

Figure 1: A basic rotary knob

Figure 1 shows a basic knob implemented using the BeppiMenozzi library whereas figure 2 shows the implementation of a custom knob using the basic knob.

Figure 2: A custom Knob

Steps of making a Custom-Knob using a simple Knob

  1. Implement the the basic knob using the steps given in my previous knobs explained in my previous blogs.
  2. Download the images of the knob which has to be implemented.
android:layout_weight="1"
android:rotation="15"
app:kDefaultState="2"
app:kIndicatorWidth="@dimen/multimeter_length_0"
app:kKnobCenterColor="@color/colorPrimaryDark"
app:kKnobColor="@color/white"
app:kKnobDrawable="@drawable/knob"
  1. Using the above code amend the knob as per the requirement. The advantage of using the beppiMonzi library is that the knob is fully amenable  , we can even define the minimum and maximum angle and many more stuffs can be done using the library.

                 

 

 

 

 

Figure 3: Showing the implementation of other custom knobs

The above figure shows the example of custom knobs implemented using the simple knob and by following the steps.

Implementation Circular positioning

One of the other major issues while making the new UI of the multimeter is the positioning of text-view around the circular knob. The issue was made overcome by implementing a circular positioning constraints in the text-views.

Steps of implementing circular positioning

  1. Use the constraint layout version 1.1.0 or above as the previous versions do not support the circular positioning feature.
  2. Add the circular constraint individually to every text-view.
app:layout_constraintCircle="@id/knobs"
app:layout_constraintCircleAngle="105"
app:layout_constraintCircleRadius="@dimen/multimeter_knobcircle_radius_1"

The above code snippets shows the addition od circular constraints added to a text-view. Using these constraint it decides positions the views relative to another views at a particular angle which thus makes up circular positioning.

Thus, this is how we can implement circular positioning in the views.

Resources

  1. BeppiMenozzi Knob Library
    https://github.com/BeppiMenozzi/Knob
  2. Rotary knob Blog
    https://docs.google.com/document/d/1IU_lpdt4sHI4euM543bBlHwYpv8vwwjxoB76sW1i5HA/edit?usp=sharing

Implementing Notification Action Buttons in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create access codes for his or her event.  Access codes can be used to password protect hidden tickets reserved for sponsors, members of the press and media. Notifications are an important part of the project. We show each registered user notifications based on their activity. This blog post goes over the implementation of the notification action buttons in the notification panel.

Notification Action Model

The model for Notification action is very simple. It has the following variables:

  1. Subject: The subject of the notification. E.g. ‘event’, ‘order’ etc.
  2. actionType: The action that can be taken by the user for that notification. E.g: ‘view’, ‘submit’.
  3. subjectId: The id of the subject. In case of an event, it will store the event id. Similarly for other cases.
  4. Link: The link to be applied to the button.

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';
import { belongsTo } from 'ember-data/relationships';

export default ModelBase.extend({
  subject    : attr('string'),
  actionType : attr('string'),
  subjectId  : attr('number'),
  link       : attr('string'),

  notification: belongsTo('notification')
});

Action Button Title

We make use of ember computed property to determine the action button title. The title of the button depends on the subject and the actionType defined in the notification-action model. The actionType can be one of ‘download’, ‘submit’ and ‘view’. If the action type is ‘download’ and the subject is ‘invoice’, then the button title will be “Download Invoice”. Similarly, for other cases, we do the same.

buttonTitle: computed('subject', 'actionType', function() {
    let action;
    const actionType = this.get('actionType');
    switch (actionType) {
      case 'download':
        action = 'Download';
        break;

      case 'submit':
        action = 'Submit';
        break;

      default:
        action = 'View';
    }

    let buttonSubject;
    const subject = this.get('subject');
    switch (subject) {
      case 'event-export':
        buttonSubject = ' Event';
        break;

      case 'event':
        buttonSubject = ' Event';
        break;

      case 'invoice':
        buttonSubject = ' Invoice';
        break;

      case 'order':
        buttonSubject = ' Order';
        break;

      case 'tickets-pdf':
        buttonSubject = ' Tickets';
        break;

      case 'event-role':
        buttonSubject = ' Invitation Link';
        break;

      case 'session':
        buttonSubject = ' Session';
        break;

      case 'call-for-speakers':
        if (this.get('actionType') === 'submit') {
          buttonSubject = ' Proposal';
        } else {
          buttonSubject = ' Call for Speakers';
        }
        break;

      default:
        // Nothing here.
    }

    return action + buttonSubject;
  })

Action Button Route

The route that the button will lead to depends on the subject of the action. If the link is provided in the notification action, we simply set it on the button otherwise we use the subject to derive the route name. For e.g., if the subject is an event, then the route will be “events.view”.

/**
   * The route name to which the action button will direct the user to.
   */
  buttonRoute: computed('subject', function() {
    const subject = this.get('subject');
    let routeName;
    switch (subject) {
      case 'event-export':
        routeName = 'events.view';
        break;

      case 'event':
        routeName = 'events.view';
        break;

      case 'invoice':
        routeName = 'orders.view';
        break;

      case 'order':
        routeName = 'orders.view';
        break;

      default:
      // Nothing here.
    }
    return routeName;
  })

Template

We simply check if the link exists or not. If it does then we simply use it otherwise we use the computed button route name.

{{#if action.link}}
     {{#link-to action.link tagName='button' class='ui blue button'}}
         {{t action.buttonTitle}}
         {{/link-to}}
{{else}}
    {{#link-to action.buttonRoute action.subjectId tagName='button' class='ui blue button'}}
         {{t action.buttonTitle}}
         {{/link-to}}
{{/if}}

References

Onsite Attendee in Open Event Server

The Open Event Server enables organizers to manage events from concerts to conferences and meetups. It offers features for events with several tracks and venues. The Event organizers may add orders on behalf of others and accept payments onsite. This blog post goes over the implementation of the onsite attendee feature in the Open Event Server.

Route

Normally we expect the payload for a POST request of order to contain already created attendees also. In this case we want to create the attendees internally inside the server. Hence we need some way to differentiate between the two types of orders. The most basic and easy to implement option is to use a query parameter to specify if the attendees are onsite or not. We use ?onsite=true in order to specify that the attendees are onsite and hence should be created internally.

In the POST request, we check if the query parameters contains the onsite param as true or not. If it is true then we create the attendees using a helper function. The helper function will be discussed in detail later in the article.

# Create on site attendees.
if request.args.get('onsite', False):
    create_onsite_attendees_for_order(data)
elif data.get('on_site_tickets'):
    del data['on_site_tickets']
require_relationship(['ticket_holders'], data)

 

OnsiteTicketSchema

In order to create attendees on the server, we need the information about each ticket bought and it’s quantity. This data is expected in the format declared in the OnsiteTicketSchema.

class OnSiteTicketSchema(SoftDeletionSchema):
    class Meta:
        type_ = 'on-site-ticket'
        inflect = dasherize

    id = fields.Str(load_only=True, required=True)
    quantity = fields.Str(load_only=True, required=True)

Creating onsite Attendees

Following are the few points which we need to focus on when creating onsite attendees:

  1. Validate if the ticket’s data is provided or not. We raise an error if the ticket data is not provided.
  2. Verify if the ticket is sold out or not. We raise an error if the ticket is sold out.
  3. In case an error is raised in any step then we delete the already created attendees. This is a very important point to keep in mind.

if not on_site_tickets:
        raise UnprocessableEntity({'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing')

ticket_sold_count = get_count(db.session.query(TicketHolder.id).
                                      filter_by(ticket_id=int(ticket.id), deleted_at=None))

        # Check if the ticket is already sold out or not.
        if ticket_sold_count + quantity > ticket.quantity:
            # delete the already created attendees.
            for holder in data['ticket_holders']:
                ticket_holder = db.session.query(TicketHolder).filter(id == int(holder)).one()
                db.session.delete(ticket_holder)
                try:
                    db.session.commit()
                except Exception as e:
                    logging.error('DB Exception! %s' % e)
                    db.session.rollback()

            raise ConflictException(
                {'pointer': '/data/attributes/on_site_tickets'},
                "Ticket with id: {} already sold out. You can buy at most {} tickets".format(ticket_id,
                                                                                             ticket.quantity -
                                                                                             ticket_sold_count)
            )

The complete method can be checked here.

References

 

 

Channel Communications Error of PSLab

The Pocket Science Lab multimeter has got three channels namely CH1,CH2 and CH3 with different ranges for measuring the voltages, but there was a channel communication error occuring at CH1 and CH2 pins of PSLab. This blogs will give a brief description of the channel communication error and how was it solved by me.

In the previous blog I have discussed about the channel communication of PSLab and how it works. In this blog i will be discussing about the channel communication error which was occuring while using CH1 and CH2 pins of PSLab android.

Communication between PSLab device and Android App

As discussed in the previous blog the communication between the PSLab and the android occurs with the help of the USBManger class of android.

One of the major function which makes it possible is the bulk transfer function of the android

amtWritten = mConnection.bulkTransfer(mWriteEndpoint, src, written, writeLength, timeoutMillis);

As shown in the above code, there is a timeout that some time required for this function to be executed, and otherwise this function will return a negative value which will mean that the communication is not successful.

Voltage Measuring Functions

The main function which gets called while pressing the button for measuring voltage is the getVoltage function which simultaneously calls the volmeterAutoRange function as well as the getAverage voltage function. The voltageAutoRnge function also calls the getAverage function inside of it.

public double getVoltage(String channelName, Integer sample) {
    this.voltmeterAutoRange(channelName);
    double Voltage = this.getAverageVoltage(channelName, sample);
    if (channelName.equals("CH3")) {
        return Voltage;
    } else {
        return 2 * Voltage;
    }
}

Calling both these functions simultaneously results in calling of the bulktranfer method

VoltmeterAutoRange function:-

private double voltmeterAutoRange(String channelName) {
    if (this.analogInputSources.get(channelName).gainPGA == 0)
        return 0;
    this.setGain(channelName, 0, true);
    double V = this.getAverageVoltage(channelName, null);
    return this.autoSelectRange(channelName, V);
}

The getAverage voltage function calls the getRawableVoltage function which thus calls the USBManger class functions of read and write, thus calling the bulkTranfer function.

Thus as the bulk transfer function is called simultaneously it caused problem in communication.

Solving the issue

The communication related  issues were finally solved when these bugs were spotted, the solution to this issue is that the voltageAutoRange function’s return value was never used in the codes and was thus not required.[2]The voltageAutoRange function was calling the getAverageVoltage function just to get a return value. Thus I formatted the function and now it looks like this-

private void voltmeterAutoRange(String channelName) {
    if (this.analogInputSources.get(channelName).gainPGA != 0) {
        this.setGain(channelName, 0, true);
    }
}

And thus finally the issue was solved and all things were working fine in channel communication.

Resources

Integrating System Roles API in Open Event Frontend

The Eventyay system supports different system roles and allows to set panel permissions for every role. The system supports two inbuilt roles namely Admin and Super Admin. The users having access to permissions panel can create new custom system roles and define set of panel permissions for them. Also the users are provided with the option of editing and deleting any system role except the two inbuilt system roles. The feature is implemented using custom-system-roles and panel-permissions API on the server.

Adding route for system-roles

The route for custom-system-system roles is defined which contains a model returning user permissions, system roles and the panel permissions. The model is defined as async so that the execution is paused while fetching the data from the store by adding the await expression.

async model() {
 return {
   userPermissions  : await this.get('store').findAll('user-permission'),
   systemRoles      : await this.get('store').findAll('custom-system-role'),
   panelPermissions : await this.get('store').findAll('panel-permission')
 };
},

The route created above gets all the data for user permissions, system-roles and panel permissions which is later used by the template for rendering of data.

Adding model for system-roles and panel-permissions

The model for system-roles is created which contains the ‘name’ attribute of type string and a relationship with panel permissions. Every system role can have multiple panel permissions, therefore a hasMany relationship is defined in the model.

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

 panelPermissions: hasMany('panelPermission')
});

Similarly, the model for panel-permissions is added to the models directory. The defined model contains ‘panelName’ as an attribute of type string and a bool value canAccess, defining if the panel is accessible by any role or not.

export default ModelBase.extend({
 panelName : attr('string'),
 canAccess : attr('boolean')
});

Defining controller for system-roles

The controller for system-roles is defined in the controllers/admin/permissions directory. The action for adding, updating and deleting system roles are defined in the controller. While adding the system roles, all the panels are fetched and checked which panel permissions are selected by the admin. A special property namely ‘isChecked’ is added to every panel permission checkbox which toggles on change. If the property is set true the corresponding panel is added to the panel permissions relationship of corresponding role. If no panel is selected, an error message to select atleast one panel is displayed.

deleteSystemRole(role) {
 this.set('isLoading', true);
 role.destroyRecord()
  ...
  // Notify success or failure
},
addSystemRole() {
 this.set('isLoading', true);
 let panels = this.get('panelPermissions');

 panels.forEach(panel => {
   if (panel.isChecked) {
     this.get('role.panelPermissions').addObject(panel);
   } else {
     this.get('role.panelPermissions').removeObject(panel);
   }
 });
 if (!this.get('role.panelPermissions').length) {
  // Notification to select atleast one panel
 } else {
   this.get('role').save()
    // Notify success or failure
 }
},
updatePermissions() {
 this.set('isLoading', true);
 this.get('model.userPermissions').save()
  ...
  // Notify success or failure
}

The actions defined above in the controller can be used in template by passing the appropriate parameters if required. The addSystemRole action makes a POST request to server for creating a new system role, the updatePermissions action makes a PATCH request for updating the existing system role and the deleteSystemRole action makes a delete request to the server for deleting the role.

Adding data to template for system-roles

The data obtained from the model defined in route is rendered in the template for system-roles. A loop for showing all system roles is added to the template with the name attribute containing the name of system role and another loop is added to display the panel permissions for the corresponding role.

{{#each model.systemRoles as |role|}}
 <tr>
   <td>{{role.name}}</td>
   <td>
     <div class="ui bulleted list">
       {{#each role.panelPermissions as |permission|}}
         <div class="item">{{concat permission.panelName ' panel'}}</div>
       {{/each}}
     </div>
   </td>
   <td>
    // Buttons for editing and deleting roles
   </td>
 </tr>
{{/each}}

A modal is to the component for creating and editing system roles. The data from this template is passed to the modal where the existing permissions are already checked and can be modified by the admins.

Resources

Integrating Event Roles API in Open Event Frontend

The Eventyay system supports different type of roles for an event like Attendee, organizer, co-organizer, track-organizer, moderator and the registrar. Every role has certain set of permissions such as Create, Read, Update, Delete. The Admin of the system is allowed to change the permissions for any role. The interface for updating the even role permissions was already available on the server but was not integrated on the frontend. The system is now integrated with the API and allows admin to change event role permission for any role.

Adding model for event role permissions

The model for event role permissions is added to the models directory. The model contains the attributes like canDelete, canUpdate, canCreate, canRead and the relationship with event role and the service.

export default ModelBase.extend({
 canDelete : attr('boolean'),
 canUpdate : attr('boolean'),
 canCreate : attr('boolean'),
 canRead   : attr('boolean'),

 role        : belongsTo('role'),
 service     : belongsTo('service'),
 serviceName : computed.alias('service.name')
});

The above defined model ensures that every permission belongs to a role and service. An alias is declared in the model using the computed property which is later used in the controller to sort the permissions according to service name in lexicographical order.

Adding route for event roles

The route for event role is created which contains model returning an object containing the list of roles, services and permissions. The model is defined as async so that the execution is paused while fetching the data from the store by adding the await expression.

export default Route.extend({
 titleToken() {
   return this.get('l10n').t('Event Roles');
 },
 async model() {
   return {
     roles       : ['Attendee', 'Co-organizer', 'Moderator', 'Organizer', 'Track Organizer', 'Registrar'],
     services    : await this.get('store').query('service', {}),
     permissions : await this.get('store').query('event-role-permission', { 'page[size]': 30 })
   };
 }
});

The route created above queries the data for roles, services and permissions which is later used by the template for rendering of the data obtained.

Adding controller for event roles

The controller for event roles is added to the controllers/admin/permissions directory. The computed property is used to sort the services obtained from model lexicographically and the permissions are sorted by the help of alias created in the model.

services: computed('model', function() {
 return this.get('model.services').sortBy('name');
}),
sortDefinition : ['serviceName'],
permissions    : computed.sort('model.permissions', 'sortDefinition'),
actions        : {
 updatePermissions() {
   this.set('isLoading', true);
   this.get('model.permissions').save()
     .then(() => {
       // Notify success and add Error handler
      }
   }
}

An action named updatePermissions is defined which is triggered when the admin updates and saves the permissions for any role where a PATCH request is made to the server in order to update the permissions.

Rendering data in the template

The data obtained from the model is manipulated in the controller and is rendered to the table in the event-roles template. Every role is fetched from the model and added to the template, all the permissions in sorted order are obtained from the controller and matched with the current role name. The relationship of permissions with role is used to check if its title is equal to the the current role. The permissions are updated accordingly, if the role title is equal to current role.

<tbody>
 {{#each model.roles as |role|}}
   <tr>
     <td>{{role}}</td>
     {{#each permissions as |permission|}}
       {{#if (eq permission.role.titleName role)}}
         <td>
           {{ui-checkbox label=(t 'Create') checked=permission.canCreate onChange=(action (mut permission.canCreate))}}
           <br>
           {{ui-checkbox label=(t 'Read') checked=permission.canRead onChange=(action (mut permission.canRead))}}
           <br>
           {{ui-checkbox label=(t 'Update') checked=permission.canUpdate onChange=(action (mut permission.canUpdate))}}
           <br>
           {{ui-checkbox label=(t 'Delete') checked=permission.canDelete onChange=(action (mut permission.canDelete))}}
         </td>
       {{/if}}
     {{/each}}
   </tr>
 {{/each}}
</tbody>

After rendering the data as shown above, the checkbox for permissions of different services for different roles are checked or unchecked depending upon the bool value of corresponding permission. The admin can update the permissions by checking or unchecking the checkbox and saving the changes made.

Resources

Adding Speakers Page in Open Event Frontend

Open Event Frontend earlier displayed all the speakers of an event on the main info page only, now a separate route for speakers is created and a separate page is added to display the speakers of an event. The design and layout of speakers page is kept similar to that on Open Event Web app. The info page only shows the featured speakers for an event and the complete list of speakers with additional information is present on speakers route.

Getting the event speakers data

The event data is obtained from the public model and a query is made for the speakers to get the required data. The speakers are fetched only for the sessions which are accepted, this is done by applying a filter while the query is made.

async model() {
 const eventDetails = this.modelFor('public');
 return {
   event    : eventDetails,
   speakers : await eventDetails.query('speakers', {
     filter: [
       {
         name : 'sessions',
         op   : 'any',
         val  : {
           name : 'state',
           op   : 'eq',
           val  : 'accepted'
         }
       }
     ]
   })
 };
}

Adding template for displaying speakers

A template is added to display three speakers in a row. The speakers data obtained from the model is looped through and details of every speaker is passed to the speaker-item component, which handles the design and layout for every item in the speakers list.

<div class="ui stackable grid container">
 {{#each model.speakers as |speaker|}}
   <div class="five wide column speaker-column">
     {{public/speaker-item speaker=speaker}}
   </div>
 {{/each}}
</div>

Adding component for speaker-item

A component for displaying the speaker-item is added to templates/component/public directory. The component contains of an accordion which displays the speaker details like biography, social links and the sessions that would be taken by him.

{{#ui-accordion}}
 <div class="title">
   <div class="ui">
     <img alt="speaker" class="ui medium rounded image" src="{{if speaker.photo.iconImageUrl speaker.image '/images/placeholders/avatar.png'}}">
    ...
    ... 
    ...
    // Speaker Details
   </div>
 </div>
{{/ui-accordion}}

The accordion with speaker image and other details appears for every speaker of an event.

Resources

Implementing the PDF download of Schedule in Open Event Web app

Open Event Web app now provides an option to its users to download the PDF of event schedule. Earlier it supported the download of list-view only, now it provides the support to download calendar-view as well. The problem incurred while downloading the calendar-view was that the view gets cropped due to limitations with the library used for PDF generation, thus only some parts of the calendar remained in the PDF. The problem is resolved by creating an image for every date in the schedule and adding the generated image to the PDF.

Selecting and adding the data for PDF generation

The data to be added to PDF depending on the filters and date-selectors applied is chosen from the DOM. Selection of data is done by looping through all the dates and adding only the ones which do not have ‘hide’ class added to them. The selected dates are first expanded such that their complete view is available while generating the image. The complete data is stored in a variable depending on if the complete schedule is requested for download or some filter is applied, which is later used for generating the image.

let fullScheduler = true;
let mapValue = '';

pdf = new jsPDF('l', 'pt', 'a1');
$('.calendar').each(function() {
 let hidePresent = $(this).attr('class').split(' ').indexOf('hide') <= 0;

 if (hidePresent) {
   $timeline = $(this);

  // Expanding the schedule for current date
  ...
  ...
 }
 fullScheduler = hidePresent && fullScheduler;
});

if(fullScheduler) {
 $timeline = $('.calendar').parent();
 mapValue = $timeline.children();
}

Adding the notification while generating the PDF

A loader with the notification is added to provide better user experience, as the PDF generation takes place at the time of request itself it may take some time depending on the size of the schedule. The notifications are added using ‘sweetalert’ library already added for Add to calendar notifications.

swal("Generating the PDF",{
 icon: "./images/loader.gif",
 buttons: false
});

Downloading the PDF

The selected dates are stored in an array named ‘schedArr’ whose data sequentially is passed for canvas generation. A new page is added to the PDF of size equal to canvas and the generated canvas is added to that page. With every new page added to the calendar a count is increased to keep a track if all the selected dates are added to PDF.

async.eachSeries(schedArr, function (child, callback) {
 html2canvas(child, {
   onrendered: function (canvas) {
     pdf.addPage(canvas.width, canvas.height);
     child.style.width = initialWidth[count] + 'px';
     pdf.addImage(canvas, 'png', 0, 0, canvas.width, canvas.height);
     currDate++;
     if(currDate === schedArr.length){
       pdf.deletePage(1);
       swal.close();
       pdf.save(scheduleDate + '.pdf');
     }
     count++;
     callback();
   }
 });
});

When the last page is added, the notification is closed and user is prompted to download pop-box.

Resources