Workflow of Admin Translations downloads as ZIP

This blog post emphasizes on the feature of translation archiving and download routes. The Open Event Project, popularly known as Eventyay is an event management solution which provides a robust platform to manage events, schedules multi-track sessions and supports many other features.

Prior to the actual implementation of this feature, a blueprint had to be registered in the flask app which handles the archiving and downloads logic for the translations. In addition, as this is only specific for administrators, the route had to be secured with access control. The access is controlled with the help of the is_admin decorator which checks if the user attempting to access the route is an admin. 

from flask import send_file, Blueprint
import shutil
import uuid
import tempfile
import os
from app.api.helpers.permissions import is_admin

admin_blueprint = Blueprint('admin_blueprint', __name__, url_prefix='/v1/admin/content/translations/all')
temp_dir = tempfile.gettempdir()
translations_dir = 'app/translations'

@admin_blueprint.route('/', methods=['GET'])
@is_admin
def download_translations():
    """Admin Translations Downloads"""
    uuid_literal = uuid.uuid4()
    zip_file = "translations{}".format(uuid_literal)
    zip_file_ext = zip_file+'.zip'
    shutil.make_archive(zip_file, "zip", translations_dir)
    shutil.move(zip_file_ext, temp_dir)
    path_to_zip = os.path.join(temp_dir, zip_file_ext)
    from .helpers.tasks import delete_translations
    delete_translations.apply_async(kwargs={'zip_file_path': path_to_zip}, countdown=600)
    return send_file(path_to_zip,  mimetype='application/zip',
                     as_attachment=True,
                     attachment_filename='translations.zip')

                                                         Code Snippet – Translations archiving

We utilize the shutil library to create archives of the specified directory which is the translations directory here. The directory file path which is to be archived must point to the folder with sub-directories of various language translations. We append a unique name generated by the UUID(Universally Unique Identifier) to the generated translations zip file. Also observe that the translations archive is saved to the temp folder. This is because the zip archive needs to be deleted after a certain amount of time. This time window mustn’t be too bounded as there might be multiple admins who might want to access these translations at the same time. Therefore, it is saved in the temp folder for 10 mins.

import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class extends Controller {
  isLoading = false;

  @action
  async translationsDownload() {
    this.set('isLoading', true);
    try {
      const result = this.loader.downloadFile('/admin/content/translations/all/');
      const anchor = document.createElement('a');
      anchor.style.display = 'none';
      anchor.href = URL.createObjectURL(new Blob([result], { type: 'octet/stream' }));
      anchor.download = 'Translations.zip';
      anchor.click();
      this.notify.success(this.l10n.t('Translations Zip generated successfully.'));
    } catch (e) {
      console.warn(e);
      this.notify.error(this.l10n.t('Unexpected error occurred.'));
    }
    this.set('isLoading', false);
  }
}

                                           Code Snippet – Frontend logic for anchor downloads

To make this work on all browsers, the frontend part handles the downloads via an action in the controller. The action translationsDownload is linked to the download button which uses the download method in the loader service to hit the specific route and fetch the resource intuitively. This is stored in a constant result.

After this, an HTML anchor is created explicitly and its attribute values are updated.

The href of the anchor is set to the result of the resource fetched using the loader service with a Blob.

 Then a click action is triggered to download this file. As the loader service returns a Promise, we use an async function as the action to resolve it. In cases of failures, the notify service raises the error related and the frontend triggers this notification.

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, Ember.js, Open Event, API

Continue ReadingWorkflow of Admin Translations downloads as ZIP

Gas sensor (MQ-135) support In PSLab Android application

Along with lots of sensors provided in the PSLab Android application, recently support for a new sensor – MQ-135 gas sensor has been added to the app. In this blog, I will discuss what is this gas sensor and how to use it with PSLab Android application

MQ-135 Gas sensor

The MQ-135 gas sensors are used in air quality control equipment and are suitable for detecting or measuring of NH3, NOx, Alcohol, Benzene, Smoke, CO2. 

The Pin layout of MQ-135 sensor

(Figure 1: MQ-135 pin layout)

How to Connect MQ-135 to PSLab Board

The following diagram shows how a user can connect MQ-135 sensor to a PSLab Board. 

(Figure 2: MQ-135 and PSLab connections)

As can be seen in the diagram above connect Voltage pin of MQ-135 sensor to one of the VDD pins on the PSLab board. Connect the Ground pin of MQ-135 sensor to one of GND pins on the PSLab board. And connect Analog Output pin to CH1 pin on the PSLab board. Once these connections are made user can connect PSLab board to their mobile phone and start reading data using Gas Sensor instrument in PSLab Android application

Gas Sensor Instrument in PSLab Android Application

To provide users an interface to read values collected by MQ-135 sensor connected to PSLAb board, a new instrument screen has been added to the PSLab Android application. The UI of the screen is shown below,

(Figure 3: Gas Sensor instrument UI)

As can be seen, the user is provided with a circular meter, a text box and a graph, all of which indicates the amount of different gases sensed by MQ-135 in PPM (parts per million) unit. The data is collected by very simple lines of codes. Since we are connecting Analog Output of MQ-135 to CH1 on PSLab board, we need to read the voltage at CH1 pin. Which would be in the range of 0 – input voltage (which is 3.3V in our case). To convert the voltage values to PPM, we map these output voltages to a range of 0 – 1024. This is done by following lines of code.

double volt = scienceLab.getVoltage("CH1", 1);
double ppmValue = (volt / 3.3) * 1024.0;

As provided in all the other instruments in PSLab Android application, Gas Sensor also has data logging and importing feature. User can record the data and store it as a CSV file and import previously recorded data into the PSLab application easily.

So in conclusion, now users can utilize and experiment with MQ-135 sensor effortlessly using PSLab Android application.

A working demo of this feature can be seen in the following video

https://drive.google.com/file/d/1-KxOaqE_Y5EYquMkebYpBOEc0d7GAdLS/view?usp=sharing

References:

Tags: PSLab, Android, GSoC 19, Sensors, Gas Sensor, MQ-135

Continue ReadingGas sensor (MQ-135) support In PSLab Android application

Implementing Event Invoice Forms

This blog post elaborates on the recent addition of user billing form in Eventyay which is an open source event management solution which allows users to buy & sell tickets, organize events & promote their brand, developed by FOSSASIA. As this project moves forward with the implementation of event invoices coming up,. In the past few weeks, I have collaborated with fellow developers in planning the integration of event invoice payments and this is a necessary step for the same due to its involvement in order invoice templates. This implementation focuses on event invoices billing ( the calculated amount an event organiser has to pay to the platform for their event’s revenue ).

This form includes basic details like contact details, tax ID, billing location and additional information (if any). The following is a specimen of this form :

Tax Form Implementation

First step of this form creation is to employ the account/billing/payment-info route for serving the relevant model data to the frontend.

// app/routes/account/billing/payment-info.js
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default class extends Route.extend(AuthenticatedRouteMixin) {
titleToken() {
  return this.l10n.t('Payment Info');
}
}

Since the field additions have been done in the user schema in the server side, the corresponding changes have to made in the ember user model as well.

// app/models/user.js
/**
  * Billing Contact Information
  */

billingContactName    : attr('string'),
billingPhone          : attr('string'),
billingCountry        : attr('string'),
company               : attr('string'),
billingAddress        : attr('string'),
billingCity           : attr('string'),
billingZipCode        : attr('string'),
billingTaxInfo        : attr('string'),
billingAdditionalInfo : attr('string'),
billingState          : attr('string'),

This form has a speciality. Instead of using the current user information directly, it uses an intermediate object and employs manipulation in current user record only when the submit button is clicked. This has been implemented in the following way : 

// app/components/user-payment-info-form.js
export default class extends Component.extend(FormMixin) {
didInsertElement() {
  super.didInsertElement(...arguments);
  this.set('userBillingInfo', pick(this.authManager.currentUser, ['billingContactName', 'billingCity', 'billingPhone', 'company', 'billingTaxInfo', 'billingCountry', 'billingState', 'billingAddress', 'billingZipCode', 'billingAdditionalInfo']));
}
@action
submit() {
  this.onValid(async() => {
    this.set('isLoading', true);
    try {
      this.authManager.currentUser.setProperties(this.userBillingInfo);
      await this.authManager.currentUser.save();
      this.notify.success(this.l10n.t('Your billing details has been updated'));
    } catch (error) {
      this.authManager.currentUser.rollbackAttributes();
      this.notify.error(this.l10n.t('An unexpected error occurred'));
    }
    this.set('isLoading', false);
  });
}
}

The usual form validations are employed as expected in this one too and works well in storing the invoice based information.

Resources

Related Work and Code Repository

Continue ReadingImplementing Event Invoice Forms

Play Your Favourite Music Files on your SUSI Smart Speaker

The SUSI smart speaker supports playing local music from any USB device connected to the smart speaker. To play your favourite music directly from files, just put them in a thumb drive and plug it into any one of the four USB ports on the smart speaker. SUSI can either play all songs from the USB device or songs from a specific artist, genre or album.

Working

The first thing that needs to be done is to automount the thumb drive in the smart speaker, for this the usbmount package is used. Further, after the mount is done local skills are created which are then used by the SUSI server to interpret voice commands related to offline music playback.

Breakdown

The code which enables the above functionality is : 

  1. Add dependency – https://github.com/fossasia/susi_installer/blob/1a2950b0eb1f88d4ecbd5b3c348d9b67ac2f4705/install.sh#L287
# usbmount is needed to automount usb drives on susibian(raspbian lite)
if [ $targetSystem = raspi ] ; then
DEBDEPS=”$DEBDEPS hostapd dnsmasq usbmount”
fi


USB mount is added to the dependency list if the installer is running on a Raspberry

  1. Enable offline skills – https://github.com/fossasia/susi_installer/blob/1a2950b0eb1f88d4ecbd5b3c348d9b67ac2f4705/install.sh#L881
mkdir -p $WORKDIR/susi_server_data/generic_skills/media_discovery
touch $WORKDIR/susi_server_data/generic_skills/media_discovery/custom_skill.txt
mkdir -p $WORKDIR/susi_server_data/settings
echo “local.mode = true” > $WORKDIR/susi_server_data/settings/customized_config.properties

Enable the server to work with offline skills stored on the device. The new skills related to offline music playback are stored in /susi_server_data/generic_skills/media_discovery in a file named custom_skill.txt.

  1. Creating on the fly skills – https://github.com/fossasia/susi_installer/blob/1a2950b0eb1f88d4ecbd5b3c348d9b67ac2f4705/install.sh#L758
echo “Preparing USB automount”
# systemd-udevd creates its own filesystem namespace, so mount is done, but it is not visible in the principal namespace.
sudo mkdir /etc/systemd/system/systemd-udevd.service.d/
echo -e “[Service]\nPrivateMounts=no” | sudo tee /etc/systemd/system/systemd-udevd.service.d/udev-service-override.conf

# readonly mount for external USB drives
sudo sed -i -e ‘/^MOUNTOPTIONS/ s/sync/ro/’ /etc/usbmount/usbmount.conf
sudo cp $INSTALLERDIR/raspi/media_daemon/01_create_skill /etc/usbmount/mount.d/
sudo cp $INSTALLERDIR/raspi/media_daemon/01_remove_auto_skill /etc/usbmount/umount.d/


First an override rule is added, which changes `PrivateMounts` rule’s value to `no` in /lib/systemd/system/systemd-udevd.service.  PrivateMounts if set to yes, the processes of this unit will be run in their own private file system (mount) namespace with all mount propagation from the processes towards the host’s main file system namespace turned off. This means any file system mount points established or removed by the unit’s processes will be private to them and not be visible to the host. To learn more about mount namespaces read –
http://man7.org/linux/man-pages/man7/mount_namespaces.7.html 

Next, whenever a device is mounted the 01_create_skill file is executed which contains the following instruction:

python3 /home/pi/SUSI.AI/susi_installer/raspi/media_daemon/auto_skills.py “$UM_MOUNTPOINT”

This calls the auto_skills.py file with the mount point of the storage device.

The auto_skills.py file is used to generate audio skills for the USB drive. It scans for all the files in the USB thumb drive and creates relevant skills.

Whenever the thumb drive is removed it calls out the 01_remove_auto_skill script which has the following instruction –


echo -n > /home/pi/SUSI.AI/susi_server_data/generic_skills/media_discovery/custom_skill.txt


This cleans out the custom_skills.txt file i.e. all the offline skills that were created for music playback are removed and the server no longer responds those skills.

USAGE

Play all music on the USB device

Usage: SUSI, Play Audio

This will play all audio from the USB device connected to the speaker. SUSI Smart speaker currently supports the following audio formats:

  • MP3
  • FLAC
  • OGG
  • WAV

Play All Songs From an Artist

Usage : SUSI, play <artist_name> from USB

Example : SUSI, play Linkin Park from USB

This will play and queue all songs from the given artist if found on the USB device.

Play a Specific Music Genre

Usage : SUSI, play <Genre> from USB

Example : SUSI, play Hard Rock from USB

This will play and queue songs from the USB device that matches the given genre.

Play an Album

Usage : SUSI, play <album_name> from USB

Example : SUSI, play Hybrid Thoery from USB

This will play and queue songs from a specific Album Name.

Note: The above three skills depend on the metadata of the file. The file should have relevant metadata for these skills to work.

Playback Control

Usage : SUSI, <control_keyword>

Example : SUSI, pause or SUSI, resume

Available Music Playback Control keywords

  • Pause : Pause the currently playing music
  • Resume : Resume the currently playing music if paused
  • Restart : Restart the currently playing Music
  • Next : Go to the next song in the current playlist
  • Previous : Plays the previous song in the current playlist
  • Shuffle : Shuffles all songs in the current playlist and play again

Note : Playlist is made for offline Music skills such as play audio or play album from USB.

Tags

SUSI Smart Speaker, SUSI.AI, FOSSASIA, GSoC19

Resources

Continue ReadingPlay Your Favourite Music Files on your SUSI Smart Speaker

Control Your Susi Smart Speaker

The SUSI Smart Speaker is an AI assistant device which runs SUS.AI. To learn to set up your own smart speaker, head up to SUSI Installer. One of the new features of the smart speaker is the ability to control it via a webpage, the smarts speaker now allows the user to control various playback features such play/pause music directly via their mobile phones or laptops which are in the same network. The web page is served via the sound server running locally on the Raspberry Pi. The soundserver provides various methods of the vlcplayer as endpoints. The webpage uses these endpoints to control the smart speaker. Also, an external application such as an android/ios app can use these endpoints(or the webpage) to control the music playback on the device.

Making the Front-end

The front end is served via the flask server on ‘ / ’ endpoint and on the port 7070. Currently, the Front End contains the volume control slider and various buttons to control the audio playback of the device. The responses are sent to the server via javascript. Bootstrap is used for the CSS framework and Fontawesome is used for various icon support. Since the smart speaker should be able to run offline, CDN links for Bootstrap and Fontawesome are not used and the required files are served via the flask server on /static. 

Adding required frameworks:

    <link href=”{{ url_for(‘static’, filename=’bootstrap.min.css’) }}” rel=”stylesheet”>
    <script type=”text/javascript” src=”{{
      url_for(‘static’,filename=’fontawesome.min.js’)
    }}”></script>

Web Page front-end

<div class=”form-signin”>
      <img class=”mb-4″ src=”{{ url_for(‘static’,       filename=’SUSI.AI_Icon_2017a.svg’) }}” alt=”” width=”256″       height=”256″>      {SUSI.AI Icon}
      <h1 class=”h3 mb-3 font-weight-normal”>Smart Speaker Control</h1>
        <div class=”form-group”>
          <fieldset class=”the-fieldset”>
              <legend class=”text-left w-auto”>Volume Control</legend>
              <span class=”font-weight-bold”>0</span>
              <i class=”fas fa-volume-down”></i>
              <input id=”vol-control” class=”slider” type=”range” min=”0″                  max=”100″ value=”100″ step=”1″ oninput=”SetVolume(this.value)”               onchange=”SetVolume(this.value)”></input>              {Volume Control Slider}
              <i class=”fas fa-volume-up”></i>
              <span class=”font-weight-bold”>100</span>
          </fieldset>
          <fieldset class=”the-fieldset”>
              <legend class=”text-left w-auto”>Playback Control</legend>
          <button onclick=”control(‘pause’)”                   class=”btn btn-outline-primary m-2″>
          Pause
          <i class=”fas fa-pause”></i>
          </button>                  {pause control button}
            {similar to the pause button other required buttons are added}
          </fieldset>
          <button onclick=”window.location.href = ‘/set_password’;”            class=”btn btn-warning m-2″>
            Set or Change Password
            <i class=”fa fa-key”></i>
          </button>
        </div>
    </div>

Sending Response to Server

Since this is a control webpage, on sending of a response, the webpage should not reload. To accomplish this all the buttons point to a javascript function which then sends out an HTTP POST request to the server. For this purpose XMLHttpRequest Object is used. The XMLHttpRequest object is used to exchange data with a web server behind the scenes. 


Here the SetVolume function is used to send a request to the /volume endpoint which is used to control the volume of the device. The control function is used to send a post request to audio control endpoints such as /pause /stop /shuffle etc.

      function control(action){
        console.log(action)
        var http = new XMLHttpRequest();
        var url = ‘/’+action;
        http.open(‘POST’, url, true);
        http.send();
      }
      function SetVolume(val){
        console.log(val)
        var http = new XMLHttpRequest();
        var url = ‘/volume?val=’+val;
        http.open(‘POST’, url, true);
        http.send();
      }

Features

The endpoints on the server provide the different audio control features via the vlc player. The endpoints used are listed below –

Play

The play functionality currently is only used directly via the busy state. It currently supports playback via youtube URL or MRLs. 

To play using an MRL(Media Resource Locator) the request URL should have an argument called MRL with the needed MRL value. This also supports multiple semicolon ‘ ; ‘ separated MRLs in a single request. 

Example Request URL: http://127.0.0.1:7070/play?mrl=/home/user/Desktop/song1.mp3;/home/user/Desktop/song2.mp3

@app.route(‘/play’, methods=[‘POST’, ‘PUT’])
def play_route():
    if ‘ytb’ in request.args:
        vlcplayer.playytb(request.args.get(‘ytb’))
        return do_return(‘Ok’, 200)
    elif ‘mrl’ in request.args:
        vlcplayer.play(request.args.get(‘mrl’))
        return do_return(‘Ok’, 200)
    else:
        return do_return(‘Unknown play mode’, 400)

Stop, Next, Previous, Pause and Resume

Stop, next and previous use the inbuilt methods of the MediaListPlayer class and are implemented in the same way. The request type must be POST and the request URL doesn’t require any arguments.

@app.route(‘/stop’, methods=[‘POST’, ‘PUT’])
def stop_route():
    vlcplayer.stop()
    return do_return(‘Ok’, 200)

Pause and Resume are also implemented in the same way but both of these use the same method pause of the MediaListPlayer class as that method acts as a toggle.

Shuffle

The shuffle endpoint shuffles the currently playing song list. It uses the shuffle method of the random library to shuffle the list containing MRLs of all the songs and then initiates a new MediaListPlayer object for playback. The Request URL doesn’t need any arguments.

    def shuffle(self):
        if self.is_playing():
            self.list_player.stop()
            random.shuffle(self.mrl)
            media_list = self.instance.media_list_new(self.mrl)
            self.list_player.set_media_list(media_list)
            self.list_player.play()
            self.softvolume(100, self.player)

Restart

The restart endpoint is used to restart the currently playing audio. It does so by going back in the playlist and playing the current audio again. The Request URL doesn’t need any arguments.

@app.route(‘/restart’, methods=[‘POST’, ‘PUT’])
def restart_route():
    vlcplayer.restart()
    return do_return(‘Ok’, 200)

Volume

The Volume endpoint is used to set the volume of the device, The volume control slider uses this endpoint. A single argument val is needed in the URL of the POST request. Val can have a value ranging from 0 to 100, where 0 means mute and 100 means full volume.

@app.route(‘/volume’, methods=[‘POST’, ‘PUT’])
def volume_route():
    try:
        vlcplayer.volume(request.args.get(‘val’))
        return do_return(‘Ok’, 200)
    except Exception as e:
        logger.error(e)
        return do_return(‘Volume adjustment error’ + e, 400)

Resources

Javascript XML HTTP Requests – https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest

Flask HTML Templates –
https://pythonhow.com/html-templates-in-flask/

Tags

SUSI Smart Speaker, SUSI.AI, FOSSASIA, GSoC19

Continue ReadingControl Your Susi Smart Speaker

Rating session in Open Event Frontend

This blog post will showcase an option which can be used by organizers to rate a session in Open Event Frontend. Let’s start by understanding why this feature is important for organizers.

Consider a situation where an event can have hundreds of session submissions. It’ll be hard for organizers/co-organizers to keep track of the session they have already evaluated and which session is better than other. Here, session rating comes to the rescue. After evaluating a particular session, the organizer/co-organizer can simply rate the session out of 5 stars. We have a column Average Rating and No. of ratings which can be used to pick up the top rated sessions and so the organizer/co-organizers need not worry to keep track of evaluated sessions.

We start by adding the three columns – Rating, Average Rating and No. of ratings to session controller along with actions createRating and updateRating to create and update session rating respectively.

Code snippet to add the three mentioned columns to session controller –

@computed()
 get columns() {
   return [
     {
       ...
     },
     {
       name            : 'Rating',
       valuePath       : 'id',
       extraValuePaths : ['rating', 'feedbacks'],
       cellComponent   : 'ui-table/cell/events/view/sessions/cell-rating',
       options         : {
         ratedSessions: this.ratedSessions
       },
       actions: {
         updateRating : this.updateRating.bind(this),
         addRating    : this.addRating.bind(this)
       }
     },
     {
       name            : 'Avg Rating',
       valuePath       : 'averageRating',
       isSortable      : true,
       headerComponent : 'tables/headers/sort'
     },
     {
       name            : 'No. of ratings',
       valuePath       : 'feedbacks.length',
       isSortable      : true,
       headerComponent : 'tables/headers/sort'
     },
     {
       ...
     }
   ];
 }

The code snippet to add the two actions createRating and updateRating to the session controller –

@action
 async updateRating(rating, feedback) {
   try {
     this.set('isLoading', true);
     if (rating) {
       feedback.set('rating', rating);
       await feedback.save();
     } else {
       await feedback.destroyRecord();
     }
     this.notify.success(this.l10n.t('Session feedback has been updated 
                                      successfully.'));
   } catch (error) {
     this.notify.error(this.l10n.t(error.message));
   }
   this.send('refreshRoute');
   this.set('isLoading', false);
 }

The action updateRating in the above code snippet takes rating and the feedback to be updated as parameters. If rating param is 0, the feedback is simply destroyed because it means that user removes his/her feedback from the session.

@action
 async addRating(rating, session_id) {
   try {
     let session =  this.store.peekRecord('session', session_id, { 
                                          backgroundReload: false });
     this.set('isLoading', true);
     let feedback = await this.store.createRecord('feedback', {
       rating,
       session,
       comment : '',
       user    : this.authManager.currentUser
     });
     await feedback.save();
     this.notify.success(this.l10n.t('Session feedback has been created 
                                      successfully.'));
   } catch (error) {
     this.notify.error(this.l10n.t(error.message));
   }
   this.send('refreshRoute');
   this.set('isLoading', false);
 }

And the action addRating takes rating and the id of the session being rated as an input and then create a new feedback record with the info.

Now the main challenge was to retrieve rating corresponding to a session and display it on the frontend. I tackled this by fetching all the feedback related to any session which is itself related to the event. Then I mapped the feedback to the id of the session they were related to.

Code snippet fetching the feedback and mapping them to session id –

let queryObject = {
     include : 'session',
     filter  : [
       {
         name : 'session',
         op   : 'has',
         val  : {
           name : 'event',
           op   : 'has',
           val  : {
             name : 'identifier',
             op   : 'eq',
             val  : store.id
           }
         }
       }
     ]
   };
let feedbacks = await this.authManager.currentUser.query('feedbacks',queryObject);
@mapBy('model.feedbacks', 'session.id') ratedSessions;

Once I got the mapped data as ratedSessions all I needed to do was to check if the id of the session being rendered is in the array ratedSessions or not. If the id was present in the array, it clearly depicts that the session was rated by the user and we can simply display the block of rating to the user.

Code snippet which executed the logic explained above – 

{{#if (includes props.options.ratedSessions record)}}
 {{#each extraRecords.feedbacks as |feedback|}}
   {{#if (eq feedback.user.email authManager.currentUser.email)}}
     {{ui-rating
       initialRating=feedback.rating
       rating=feedback.rating
       maxRating=5
       onRate=(pipe-action (action (mut feedback.rating)) (action 
               props.actions.updateRating feedback.rating feedback))
       clearable=true}}
   {{/if}}
 {{/each}}
{{else}}
 {{ui-rating
   initialRating=0
   rating=extraRecords.rating
   maxRating=5
   onRate=(pipe-action (action (mut extraRecords.rating)) (action 
           props.actions.addRating extraRecords.rating record))
   clearable=true}}
{{/if}}

The helper includes simply tells us if the first parameter passed contains the second parameter or not and pipe-action helps us to perform multiple action on a single click.

I used ui-rating module of semantic UI for implementing this feature. Whenever user added/updated the rating, at first the feedback.rating is mutated and then the relevant action is called. To ensure that duplicate entries of feedback doesn’t exist in the db I added unique constraint for the table feedback on user_id and session_id columns.

Resources:

Related work and code repo:

Continue ReadingRating session in Open Event Frontend

Implement Order Confirmation Feature in Eventyay

This post elaborates on the details of an endpoint which can be used to explicatively used to resend order confirmations. In the current implementation of the open event project, if the order has been confirmed, the ticket holders and buyers get an email each regarding their order confirmation. But in case that email has been accidentally deleted by any of the attendees, the event organizer / owner should have the power to resend the confirmations.

The first step to the implementation was to create the appropriate endpoint for the server to be pinged. I utilized the existing blueprint being used for serving tickets on eventyay frontend project and created a new endpoint on the route : orders/resend-email [POST]

# app/api/auth.py
@ticket_blueprint.route('/orders/resend-email', methods=['POST'])
@limiter.limit(
  '5/minute', key_func=lambda: request.json['data']['user'], error_message='Limit for this action exceeded'
)
@limiter.limit(
  '60/minute', key_func=get_remote_address, error_message='Limit for this action exceeded'
)
def resend_emails():
  """
  Sends confirmation email for pending and completed orders on organizer request
  :param order_identifier:
  :return: JSON response if the email was succesfully sent
  """
  order_identifier = request.json['data']['order']
  order = safe_query(db, Order, 'identifier', order_identifier, 'identifier')
  if (has_access('is_coorganizer', event_id=order.event_id)):
      if order.status == 'completed' or order.status == 'placed':
          # fetch tickets attachment
          order_identifier = order.identifier
          key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
          ticket_path = 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
          key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
          invoice_path = 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'

          # send email.
          send_email_to_attendees(order=order, purchaser_id=current_user.id, attachments=[ticket_path, invoice_path])
          return jsonify(status=True, message="Verification emails for order : {} has been sent succesfully".
                          format(order_identifier))
      else:
          return UnprocessableEntityError({'source': 'data/order'},
                                          "Only placed and completed orders have confirmation").respond()
  else:
      return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()

I utilized exiting send_email_to_attendees for the email purpose but for security reasons, the endpoint was limited to make sure that an organizer can request only 5 order confrimations to be resent each minute (implemented using flask limiter).

This was all for server implementation, to implement this on the front end, I just created a new action named as resendConfirmation implemented as given.

// app/controllers/events/view/tickets/orders/list.js
async resendConfirmation(order) {
    let payload = {};
    try {
      payload = {
        'data': {
          'order' : order.identifier,
          'user'  : this.authManager.currentUser.email
        }
      };
      await this.loader.post('orders/resend-email', payload);
      this.notify.success(this.l10n.t('Email confirmation has been sent to attendees successfully'));
    } catch (error) {
      if (error.status === 429) {
        this.notify.error(this.l10n.t('Only 5 resend actions are allowed in a minute'));
      }
      if (error.errors[0].detail) {
        this.notify.error(this.l10n.t(error.errors[0].detail));
      }
    }
  }

Using a simple post request, this was implemented on the frontend for sending the confirmation, but the additional work to be done was to handle the new error (429 status). The server throws the error but loader service hasn’t been configured yet to handle this error appropriately.

// app/services/loader.js
  if (!response.ok) {
    const defaultMessage = httpStatus[response.status];
    if (parsedResponse) {
      throw parsedResponse;
    }
    if (response.status === 429) {
      throw { status: 429, message: ‘TOO MANY REQUESTS’ };
    }
    throw new Error(
      getErrorMessage(
        response.statusText,
        defaultMessage
          ? `${response.status} – ${defaultMessage}`
          : `Could not make ${fetchOptions.type} request to ${fetchOptions.url}`
      )
    );
  }

The loader service has been modified in the following manner to accommodate the new error been thrown so that a more user friendly error could be shown on the controller level.

This was the whole mechanism which has been implemented for this particular problem. 

Resources

Related Work and Code Repository

Continue ReadingImplement Order Confirmation Feature in Eventyay

Implementation of Organizer Invoicing in Open Event

This blog post emphasizes on the workflow of event invoicing for organizers in Open Event. The Open Event Project, popularly known as Eventyay is an event management solution which provides a robust platform to manage events, schedules multi-track sessions and supports many other features.

Organizer invoicing in layman terms is the fee paid by an organizer for hosting an event on the platform. This is calculated every month and respective emails/notifications are sent to the user. An event invoice in the form a PDF is generated monthly based on the sales generated. The organizer can pay the invoice via a credit/debit card through PayPal.

This feature was divided into a set of sub-features namely:

  1. Integration of ember tables for event invoices
  2. Implementing the review route & corresponding logic
  3. Creating a compatible paypal component to work with invoice workflow
  4. Creation of a payment completion page on successful invoice payments

Navigating to Account > Billing Info > Invoices, we are presented with a table of all the invoices which are due, paid & upcoming. You can review your due invoices and pay them. Adding to that, you can also view any past invoices which were paid or the upcoming ones which would have a draft status with the information about the current sales. 

                                                         Event Invoice Overview – Organizers

For an invoice which is due, an organizer will be able to navigate to the review route by clicking on the Review Payment Action. In this route, details pertaining to Admin billing details, User billing details, total number of tickets sold and total invoice amount will be depicted. The Organizer can have a look at the total invoice amount here.

                                              Review Route – Invoice Payment

The Invoice details and Billing Info components were built using ui segments. 

After reviewing the information, the organizer can click on pay via PayPal to initiate the transaction process via PayPal. The challenge here was the lack of proper API routing(sandbox/live) present in the system. To overcome this, the env variable in the PayPal component was given the value of the current environment enabled.

def send_monthly_event_invoice():
    from app import current_app as app
    with app.app_context():
        events = Event.query.filter_by(deleted_at=None, state='published').all()
        for event in events:
            # calculate net & gross revenues
            user = event.owner
            admin_info = get_settings()
            currency = event.payment_currency
            ticket_fee_object = db.session.query(TicketFees).filter_by(currency=currency).one()
            ticket_fee_percentage = ticket_fee_object.service_fee
            ticket_fee_maximum = ticket_fee_object.maximum_fee
            orders = Order.query.filter_by(event=event).all()
            gross_revenue = event.calc_monthly_revenue()
            ticket_fees = event.tickets_sold * (ticket_fee_percentage / 100)
            if ticket_fees > ticket_fee_maximum:
                ticket_fees = ticket_fee_maximum
            net_revenue = gross_revenue - ticket_fees
            payment_details = {
                'tickets_sold': event.tickets_sold,
                'gross_revenue': gross_revenue,
                'net_revenue': net_revenue,
                'amount_payable': ticket_fees
            }
            # save invoice as pdf
            pdf = create_save_pdf(render_template('pdf/event_invoice.html', orders=orders, user=user,
                                  admin_info=admin_info, currency=currency, event=event,
                                  ticket_fee_object=ticket_fee_object, payment_details=payment_details,
                                  net_revenue=net_revenue), UPLOAD_PATHS['pdf']['event_invoice'],
                                  dir_path='/static/uploads/pdf/event_invoices/', identifier=event.identifier)
            # save event_invoice info to DB

            event_invoice = EventInvoice(amount=net_revenue, invoice_pdf_url=pdf, event_id=event.id)
            save_to_db(event_invoice)

                                Invoice generation and calculation logic as a cron job

The event invoice PDFs along with the amount calculation was done on the server side by taking the product of the number of tickets multiplied by eventyay fees. The invoice generation, amount calculation and the task of marking invoices as due were implemented as cron jobs which are scheduled every month from the time the event was created.

def event_invoices_mark_due():
    from app import current_app as app
    with app.app_context():
        db.session.query(EventInvoice).\
                    filter(EventInvoice.status == 'upcoming',
                           EventInvoice.event.ends_at >= datetime.datetime.now(),
                           (EventInvoice.created_at + datetime.timedelta(days=30) <=
                            datetime.datetime.now())).\
                    update({'status': 'due'})

        db.session.commit()

                                            Cron job to mark invoices as due

As soon as the payment succeeds, the organizer is routed to the paid route where the information alluding to the PayPal ID, Amount can be found. The organizer can also download their invoice using the route action similar to tickets and order invoices generation on Eventyay.

                                           Organizer View – Paid Invoice Route

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, Ember.js, Open Event, API

Continue ReadingImplementation of Organizer Invoicing in Open Event

Implementation of Event Invoice view using Ember Tables

This blog post emphasizes the power of ember tables and how it was leveraged to implement the event invoice view for eventyay. Event Invoices can be defined as the fee given by the organizer for hosting the event on the platform. 

Eventyay is the outstanding Open Event management solution using standardized event formats developed at FOSSASIA. Porting from v1 to v2, event invoices are an integral part in the process. 

Initially, throughout the whole project, plain HTML tables were utilized to render data pertaining to sales, tickets info etc. This in turn made the task of rendering data a cumbersome one. To implement clean & ubiquitous tables with in-built search & pagination functionalities, the ember addon ember-table has been used in Eventyay v2.

To integrate ember tables, the HTML tables had to be replaced with the ember-table component which was created in the Open Event Project. 

To utilize this component, column names for upcoming, paid and due invoices are required. These are stored in Plain Old Javascript Objects (POJOs) in the controller logic passed to the appropriate ember-table component.

In the template logic, we check for the params i.e invoice status in this case and render the ember table through a component. Certain parameters such as the searchQuery, metaData, filterOptions etc. were to be passed in for total control of the table.

@computed()
  get columns() {
    let columns = [];
    if (this.model.params.invoice_status === 'upcoming') {
      columns = [
        {
          name      : 'Invoice ID',
          valuePath : 'identifier'
        },
        {
          name          : 'Event Name',
          valuePath     : 'event',
          cellComponent : 'ui-table/cell/events/cell-event-invoice'
        },
        {
          name      : 'Date Issued',
          valuePath : 'createdAt'
        },
        {
          name            : 'Outstanding Amount',
          valuePath       : 'amount',
          extraValuePaths : ['event'],
          cellComponent   : 'ui-table/cell/events/cell-amount'
        },
        {
          name      : 'View Invoice',
          valuePath : 'invoicePdfUrl'
        }
      ];
    } else if (this.model.params.invoice_status === 'paid') {
      columns = [
        {
          name      : 'Invoice ID',
          valuePath : 'identifier'
        },
        {
          name          : 'Event Name',
          valuePath     : 'event',
          cellComponent : 'ui-table/cell/events/cell-event-invoice'
        },
        {
          name      : 'Date Issued',
          valuePath : 'createdAt'
        },
        {
          name            : 'Amount',
          valuePath       : 'amount',
          extraValuePaths : ['event'],
          cellComponent   : 'ui-table/cell/events/cell-amount'
        },
        {
          name      : 'Date Paid',
          valuePath : 'completedAt'
        },
        {
          name      : 'View Invoice',
          valuePath : 'invoicePdfUrl'
        },
        {
          name            : 'Action',
          valuePath       : 'identifier',
          extraValuePaths : ['status'],
          cellComponent   : 'ui-table/cell/events/cell-action'
        }

      ];
    } else if (this.model.params.invoice_status === 'due') {
      columns =   [
        {
          name      : 'Invoice ID',
          valuePath : 'identifier'
        },
        {
          name          : 'Event Name',
          valuePath     : 'event',
          cellComponent : 'ui-table/cell/events/cell-event-invoice'

        },
        {
          name      : 'Date Issued',
          valuePath : 'createdAt'
        },
        {
          name            : 'Amount Due',
          valuePath       : 'amount',
          extraValuePaths : ['event'],
          cellComponent   : 'ui-table/cell/events/cell-amount'
        },
        {
          name      : 'View Invoice',
          valuePath : 'invoicePdfUrl'
        },
        {
          name            : 'Action',
          valuePath       : 'identifier',
          extraValuePaths : ['status'],
          cellComponent   : 'ui-table/cell/events/cell-action'
        }

      ];
    } else if (this.model.params.invoice_status === 'all') {
      columns = [
        {
          name      : 'Invoice ID',
          valuePath : 'identifier'
        },
        {
          name          : 'Event Name',
          valuePath     : 'event',
          cellComponent : 'ui-table/cell/events/cell-event-invoice'
        },
        {
          name            : 'Amount',
          valuePath       : 'amount',
          extraValuePaths : ['event'],
          cellComponent   : 'ui-table/cell/events/cell-amount'
        },
        {
          name      : 'Status',
          valuePath : 'status'
        },
        {
          name            : 'Action',
          valuePath       : 'identifier',
          extraValuePaths : ['status'],
          cellComponent   : 'ui-table/cell/events/cell-action'
        }

      ];
    }
    return columns;
  }
}

In the route logic, the queryObject is defined to specify the default page size, page numbers and the filter applied to query the event invoice model. A sorting filter is additionally applied to pre-sort the entries before rendering it. We return the data and the params under the model hook which is then used inside the template.

For the implementation of filters, we specify the type of filters we use by storing them in a filterOptions object. For paid and due invoices, we query the event invoice model, checking if the invoice_status is paid or due. The challenge for upcoming invoices was that we won’t be able to name every other invoice as an upcoming one. Therefore, the solution which was utilized here was to query only those events which whose createdAt(date created) attribute was less than 30 days and those which weren’t deleted using Soft Deletion.

These invoices were rendered using the tabbed navigation component complying with the existing code practices.

  async model(params) {
    this.set('params', params);
    const searchField = 'name';
    let filterOptions = [];
    if (params.invoice_status === 'paid' || params.invoice_status === 'due') {
      filterOptions = [
        {
          name : 'status',
          op   : 'eq',
          val  : params.invoice_status
        }
      ];
    } else if (params.invoice_status === 'upcoming') {
      filterOptions = [
        {
          and: [
            {
              name : 'deleted-at',
              op   : 'eq',
              val  : null
            },
            {
              name : 'created-at',
              op   : 'ge',
              val  : moment().subtract(30, 'days').toISOString()
            }
          ]
        }
      ];
    }


    filterOptions = this.applySearchFilters(filterOptions, params, searchField);

    let queryString = {
      include        : 'event',
      filter         : filterOptions,
      'page[size]'   : params.per_page || 10,
      'page[number]' : params.page || 1
    };

    queryString = this.applySortFilters(queryString, params);
    return {
      eventInvoices: (await this.store.query('event-invoice', queryString)).toArray(),
      params

    };

  }

Model Hook for ember invoice tables

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, Ember Tables, SQLAlchemy, Open Event, Python

Continue ReadingImplementation of Event Invoice view using Ember Tables

Enhancing Network Requests by Chaining or Zipping with RxJava

In Eventyay Attendee, making HTTP requests to fetch data from the API is one of the most basic techniques used. RxJava comes in as a great method to help us making asynchronous requests and optimize the code a lot. This blog post will deliver some advanced RxJava used in Eventyay Attendee.

  • Why using RxJava?
  • Advanced RxJava Technique – Chaining network calls with RxJava
  • Advanced RxJava Technique – Merging network calls with RxJava
  • Conclusions
  • Resources

WHY USING RXJAVA?

There are many reasons why RxJava is a great API in Android Development. RxJava is an elegant solution to control data flow in programming, where developers can cache data, get data, update the UI after getting the data, handle asynchronous tasks. RxJava also works really well with MVVM architectural pattern.

CHAINING NETWORK CALLS WITH RXJAVA

Chaining RxJava is a technique using flatMap() operator of Rxjava. It will use the result from one network call in order to make the next network call. 

In Eventyay Attendee, this technique is used when we want to update the user profile image. First, we need to upload the new profile image to the server in order to get the image URL, and then we use that URL to update the user profile

compositeDisposable += authService.uploadImage(UploadImage(encodedImage)).flatMap {
   authService.updateUser(user.copy(avatarUrl = it.url))
}.withDefaultSchedulers()
   .doOnSubscribe {
       mutableProgress.value = true
   }
   .doFinally {
       mutableProgress.value = false
   }
   .subscribe({
       mutableMessage.value = resource.getString(R.string.user_update_success_message)
       Timber.d("User updated")
   }) {
       mutableMessage.value = resource.getString(R.string.user_update_error_message)
       Timber.e(it, "Error updating user!")
   }

In conclusion, zipping RxJava helps to make HTTP requests more continuous and reduce unnecessary codes. 

ZIPPING NETWORK CALLS WITH RXJAVA

Zipping RxJava is a technique using zip() operator of Rxjava. It will wait for items from two or more Observables to arrive and then merge them together for emitting. This technique would be useful when two observables emit the same type of data.

In Eventyay Attendee, this technique is used when fetching similar events by merging events in the same location and merging events in the same event type.

var similarEventsFlowable = eventService.getEventsByLocationPaged(location, requestedPage, 3)
if (topicId != -1L) {
   similarEventsFlowable = similarEventsFlowable
       .zipWith(eventService.getSimilarEventsPaged(topicId, requestedPage, 3),
           BiFunction { firstList: List<Event>, secondList: List<Event> ->
               val similarList = mutableSetOf<Event>()
               similarList.addAll(firstList + secondList)
               similarList.toList()
           })
}

compositeDisposable += similarEventsFlowable
   .take(1)
   .withDefaultSchedulers()
   .subscribe({ response ->
       ...
   }, { error ->
       ...
   })

In conclusion, zipping RxJava helps running all the tasks in parallel and return all of the results in a single callback.

CONCLUSION

Even though RxJava is pretty hard to understand and master, it is a really powerful tool in Android Development and MVVM models. These techniques above are really simple to implement and they could improve the app by r

RESOURCES

Eventyay Attendee Source Code: 

https://github.com/fossasia/open-event-attendee-android/pull/2010

https://github.com/fossasia/open-event-attendee-android/pull/2117

RxJava Documentation: http://reactivex.io/documentation

Continue ReadingEnhancing Network Requests by Chaining or Zipping with RxJava