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

Building PSLab Desktop Apps using JavaScript, Python  and Electron

So before we get started let me quickly show you what we accomplished over a period of 2 and a half months building the Pocket Science Lab desktop app using the very technology I am going to talk about.

The PSLab desktop app was originally written using pyQt. The UI of the python stack looked really outdated and maintaining it was becoming a problem due to poor developer support. So, after a lot of discussion and experimentation, we finally decided to re-implement the whole app in electronJS. There are three major benefits of writing an app with Electron:

  • You can re-use your HTML, CSS knowledge to write the UI part in no time.
  • You can re-use your JavaScript knowledge to write the business logic of the app.
  • It works…

Why?

Before we even dive down to why use X and why use Y, let us try to answer a very basic question, The reason I had to invest a massive amount of time trying to experiment with different combinations and config parameters is because most of the blogs on the internet just wrote about “How to get started”, nobody talked about how to complete the build. And that is what I am going to cover. Anyone can figure out how to start out but  the real grind only begins when you get to the details of the app. But I hope to hit the escape velocity with my take on the subject.

The Tech Stack — Why we chose Electron for PSLab Desktop app

Now let’s talk about why we need React or Python in an Electron App.

1. React

It is perfectly fine to use everything minimal, plain old HTML, CSS and JS will do just fine for your UI, but for how long? React opens up so many possibilities and makes the code logic flow naturally through a very well designed UI language. The state management system is also brilliant and the most crucial thing that will definitely make your life easier is the life cycle methods offered by React class components. Making use of react also lets you take advantage of well known React oriented libraries like Material UI React, and Styled-Components. These things are game changers and can transform your UI to an absolute gem that your users will enjoy.

Oh, by the way, React UI is blazing fast…

But that is not the best part, we will not configure React on our own, but rather let the crowd favorite CRA do it for us. CRA (Create-React-App) will not only abstract out all the webpack config files, but will also make sure your react dependencies stay updated. This will help in keeping your code up to date in the long run.

2. Python

The task that is being performed by Python in our case was inevitable and could not be performed by JavaScript because PSLab only had Python libraries. We did not have the time to write everything again for JS so we figured a way to make use of Python in the electron app. If you can do everything with JS and have the necessary libraries to do so, then you don’t need this. For example, if you need to predict some value using a model you trained via TensorFlow, then this would be one such use case.

Bear in mind that this could be extended to any language as long as you get your hands on a library that can deal with the communication.

3. Electron

In short, it gives you the platform to make use of the above two. It becomes more interesting when we go into the details of how electron works. But for now, let us just say, it will provide you the space on which react will render the UI, it will provide the means for your UI to talk to the Python code, it will also provide APIs to talk to native system features like filesystem, notifications etc.

Resources:How to build an Electron app using create-react-app. No webpack configuration or “ejecting” necessary. Christian Sepulveda Christian Sepulveda: https://medium.com/free-code-camp/building-an-electron-application-with-create-react-app-97945861647c

Continue ReadingBuilding PSLab Desktop Apps using JavaScript, Python  and Electron

Two flavors of PSLab Android App to support Google Maps (in Play Store flavor) and Open Street maps (in Fdroid flavor)

What are the flavors of an App? And why are they needed in PSLab Android App?

While working on the PSLab Android Project, I ran into the need to create different variants of the app with different dependencies. In this blog, I have tried to explain the process of creating various flavors of the app in the easiest way possible. 

Android Allows Developers to create different variants of the same app with the same code base but having some functionalities different across the variants. These functionalities may include some special/pro features, some different dependencies, etc. Such variants are called flavors of the App. Most common flavors are Paid and Free version of the app.

In the PSLab Android Application, we needed to generate flavors, when we required to use Google Maps in the App. The app is also published on the Fdroid, which doesn’t allow dependencies of Google Maps. Hence 2 flavors of the app have been created, 

  1. Play Store Flavor (With Google Maps)
  2. F-Droid Flavor (With Open Street Maps)

Declaring Flavors in the build.gradle File

In order to create flavors of the app, first, we need to declare flavors in the Gradle file. In PSLab Android app we are creating 2 flavors, which are declared in the build.gradle file as under

flavorDimensions 'default'
productFlavors {
   fdroid {
       dimension = 'default'
   }
   playstore {
       dimension = 'default'
   }
}

flavorDimensions is used to package flavors if there are many flavors for an App. Since we have only two flavors fdroid and playstore, hence we are using single dimension default for both the flavors. Once this has been added to the build.gradle file we need to sync the gradle. 

After the Sync is complete, if we open the Build Variants tab from the left corner of the Android Studio, it would look something like this: 

(Figure 1: Build Variant Window of Android Studio)

As can be seen in the screenshot above, once the gradle is successfully synced, Android Studio automatically creates debug and release build variants for each flavor and we can easily toggle between variants and build/ run / make apk for each variant. Congratulations! We have successfully finished the first step towards creating flavors of an app.

Directory Structure after creating Flavors

Apart from creating the build variants of different flavors, Android studio also creates src/<flavor name> folders for us. Now if we want to add new activities and classes to these flavors we can create java, res, values folders inside this folder. We can define separate Manifest file as well for each flavor individually. The directory structure of the PSLab Android project after creating required packages inside the automatically generated src/fdroid and src/playstore folders looks like below,  

(Figure 2: Directory structure after creating flavors)

Defining Flavor specific dependencies

We can have some dependencies for one app flavor and some for others. For example, in PSLab Android app, we need Google Maps dependencies only in playstore flavor and Open Street Maps dependencies only in fdroid flavor. We can easily define flavor specific dependencies by adding flavor name before Implementation command in gradle file. Like below,

// Map libraries
fdroidImplementation "org.osmdroid:osmdroid-android:$rootProject.osmVersion"
fdroidImplementation "org.osmdroid:osmdroid-mapsforge:$rootProject.mapsforgeVersion"
fdroidImplementation "org.osmdroid:osmdroid-geopackage:$rootProject.geoPackageVersion"
playstoreImplementation "com.google.android.gms:play-services-maps:$rootProject.googleMapsVersion"

Same Activity/Class with different Flavor 

Now the main purpose of creating flavors is to have some different functionalities between the flavors. For that we need the base app to call different class/activity from the src/<flavor name> folder depending on the selected flavor. We will discuss this in reference to PSLab Android app. 

So, for PSLab android app we want app to open Google Maps in Play Store flavor and Open Street Maps in froid flavor. For this we need to create a duplicate Activity. Which means we will have two separate implementation of same Activity MapsActivity.java , one in the F-Droid source folder and one in playstore source folder. So MapsActivity.java will only be declared once in the src/main/AndroidManifest.xml file, but there will be two different classes for this activity in each flavor folder. Now when the main app will call MapsActivity.class from any intent depending on the selected build variant either playstore version of MapsActivity will be launched or the F-Droid version. So after creating two instances of the MapsActivity.java the directory structure would look something like given in the screenshot below,

(Figure 3: Directory structure after creating MapsActivity)

As can be seen in the directory structure, now both Play Store and F-Droid folders have their own instances of MapsActivity.java , and now we can easily implement code for Open Street Maps and Google Maps in the respective MapsActivity.java and we have two versions of the app working flawlessly. 

References

Tags: GSoC ‘19, PSLab, Android, Flavors, GoogleMaps, OpenStreetMaps, Build Variants

Continue ReadingTwo flavors of PSLab Android App to support Google Maps (in Play Store flavor) and Open Street maps (in Fdroid flavor)

Implementing Stripe payment in Eventyay Attendee

In Eventyay Attendee, getting tickets for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Stripe?
  • Implementing Stripe Payment in Eventyay Attendee
  • Conclusion
  • Resources

WHY USING STRIPE?

There are many great APIs to be taken into consideration for making payments but we choose Stripe as one of our payment gateways because of simple implementations, detailed documentation, a good number of supported card type and good security support

IMPLEMENTING STRIPE PAYMENT IN EVENTYAY ATTENDEE

Step 1: Setup dependency in the build.gradle

// Stripe
implementation 'com.stripe:stripe-android:10.3.0'

Step 2: Set up UI to take card information

The information needed for making payments are Card Number, CVC, Expiration Date, which can be made with simple UI (EditText, Spinner,…). Stripe support getting information with CardInputWidget but we made a custom UI for that. Here is the UI we created.

Step 3: Create a card and validate information

Stripe has an object called Card, which takes card number, expiration date and CVC number as parameter to detect the card type and validate the card information with function .validateCard()

PAYMENT_MODE_STRIPE -> {
   card = Card.create(rootView.cardNumber.text.toString(), attendeeViewModel.monthSelectedPosition,
       rootView.year.selectedItem.toString().toInt(), rootView.cvc.text.toString())

   if (!card.validateCard()) {
       rootView.snackbar(getString(R.string.invalid_card_data_message))
       false
   } else {
       true
   }
}

Step 4: Send the token to the server

If card information is valid, we can create a token from the Card and then send it to the server. The token will act as the identifier of the card in order for the server to charge the payment and create tickets for the user. 

private fun sendToken(card: Card) {
   Stripe(requireContext())
       .createToken(card, BuildConfig.STRIPE_API_KEY, object : TokenCallback {
           override fun onSuccess(token: Token) {
               val charge = Charge(attendeeViewModel.getId().toInt(), token.id, null)
               attendeeViewModel.chargeOrder(charge)
           }
           override fun onError(error: Exception) {
               rootView.snackbar(error.localizedMessage.toString())
           }
       })
}

Step 5: So the rest is already handled by the server. Android application will then just receive the response from the server to see if the order is charged successfully or not.

CONCLUSION

With Stripe, user can easily make payments to get tickets for events. Stripe is a great payment gateway as it is really easy to implement in Android. Hopefully, this blog post will help you create a great shopping cart app or any kind of application that requires fast, simple and easy payments.

RESOURCES

Eventyay Attendee Pull Request on Stripe: https://github.com/fossasia/open-event-attendee-android/pull/1863

Documentation from Stripe for Android: https://stripe.com/docs/mobile/android


Continue ReadingImplementing Stripe payment in Eventyay Attendee

Integrating Redux In Settings

Settings page in SUSI.AI earlier implemented using Flux needed to be migrated to Redux, Redux integration eases handling of global state management. With Redux being integrated with Settings, the code could be split into smaller manageable components. Earlier implementation involved passing data to other Settings Tab and using Settings component as a state source. This resulted in Settings component getting more lines of code(~1300 LOC). After using Redux, only ~280 LOC is present in settings component file, and separate files for each settings tab view. 

Code Integration

Once user login, the App component fetches User Settings in ComponentDidMount.

componentDidMount = () => {
   const { accessToken, actions } = this.props;

   window.addEventListener('offline', this.onUserOffline);
   window.addEventListener('online', this.onUserOnline);

   actions.getApiKeys();
   if (accessToken) {
     actions.getAdmin();
     actions.getUserSettings().catch(e => {
       console.log(e);
     });
   }
 };

src/App.js

The action getUserSettings fetch users settings from apis.getUserSettings and populates the store settings using SETTINGS_GET_USER_SETTINGS reducer function.

Default States:

The store consists of all the settings required for persisting data.

 const defaultState = {
 theme: 'light',
 server: 'https://api.susi.ai',
 enterAsSend: true,
 micInput: true,
 speechOutput: true,
 speechOutputAlways: false,
 speechRate: 1,
 speechPitch: 1,
 ttsLanguage: 'en-US',
 userName: '',
 prefLanguage: 'en-US',
 timeZone: 'UTC-02',
 customThemeValue: {
   header: '#4285f4',
   pane: '#f3f2f4',
   body: '#fff',
   composer: '#f3f2f4',
   textarea: '#fff',
   button: '#4285f4',
 },
 localStorage: true,
 countryCode: 'US',
 countryDialCode: '+1',
 phoneNo: '',
 checked: false,
 serverUrl: 'https://api.susi.ai',
 backgroundImage: '',
 messageBackgroundImage: '',
 avatarType: 'default',
 devices: {},
};

reducers/settings.js

Reducer action:

SETTINGS_GET_USER_SETTINGS sets the settings store from API payload. The customThemeValue is an object, which is populated once the settings are fetched and the string is split into array.

[actionTypes.SETTINGS_GET_USER_SETTINGS](state, { error, payload }) {
     const { settings, devices = {} } = payload;
     if (error || !settings) {
       return state;
     }

     const {
       theme = defaultState.theme,
       server,
       .
       avatarType,
     } = settings;
     let { customThemeValue } = settings;
     const themeArray = customThemeValue
       ? customThemeValue.split(',').map(value => `#${value}`)
       : defaultState.customThemeValue;
     return {
       ...state,
       devices,
       server,
       serverUrl,
       theme,
       enterAsSend: enterAsSend === 'true',
       micInput: micInput === 'true',
       speechOutput: speechOutput === 'true',
       speechOutputAlways: speechOutputAlways === 'true',
       speechRate: Number(speechRate),
       speechPitch: Number(speechPitch),
       ttsLanguage,
       userName,
       prefLanguage,
       timeZone,
       countryCode,
       countryDialCode,
       phoneNo,
       checked: checked === 'true',
       backgroundImage,
       messageBackgroundImage,
       avatarType,
       customThemeValue: {
         header: themeArray[0],
         pane: themeArray[1],
         body: themeArray[2],
         composer: themeArray[3],
         textarea: themeArray[4],
         button: themeArray[5],
       },
     };
   },

reducers/settings.js

Updating Settings

Reduce function:

The payload contains the updated state, the reducer returns a new state with payload appended.

setUserSettings: createAction(
   actionTypes.SETTINGS_SET_USER_SETTINGS,
   returnArgumentsFn,
 )

reducers/settings.js

Action:

The setUserSettings action updates the settings store, once the user presses the Save Button in the Settings Tab.

function mapStateToProps(store) {
 return {
   enterAsSend: store.settings.enterAsSend,
 };
}

actions/settings.js

Redux integration in the component

Redux is used in ChatAppTab as: The local state of ChatAppTab is initialized with redux store state. Using mapStateToProps we can access enterAsSend object from the redux store.

The connect function connects the React component with Redux store and injects props and actions into our component using HOC pattern. It injects data from the store and functions for dispatching actions to store.

Injecting props in local components:

function mapStateToProps(store) {
 return {
   enterAsSend: store.settings.enterAsSend,
 };
}

components/Settings/ChatAppTab.react.js

Once a user makes a change in the tab and saves it, we need to dispatch an action to modify our prop enterAsSend inside the redux store. 

function mapDispatchToProps(dispatch) {
 return {
   actions: bindActionCreators({ ...settingActions, ...uiActions }, dispatch),
 };
}

components/Settings/ChatAppTab.react.j

handleSubmit = () => {
   const { actions } = this.props;
   const { enterAsSend } = this.state;
   this.setState({ loading: true });
   setUserSettings({ enterAsSend })
     .then(data => {
       if (data.accepted) {
         actions.openSnackBar({
           snackBarMessage: 'Settings updated',
         });
         actions.setUserSettings({ enterAsSend });
         this.setState({ loading: false });
       } else {
         actions.openSnackBar({
           snackBarMessage: 'Failed to save Settings',
         });
         this.setState({ loading: false });
       }
     })
     .catch(error => {
       actions.openSnackBar({
         snackBarMessage: 'Failed to save Settings',
       });
     });
 };

components/Settings/ChatAppTab.react.js

handleSubmit is invoked when user presses the Save Button, action.setUserSettings is dispatched once API returns accepted: true response.

To conclude, the removal of flux and integration of redux gave the advantage of making the code more modular and separate the logic into smaller files. In the future, more settings can be easily added using the same data flow pattern.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Redux, SUSI Chat, Settings

Continue ReadingIntegrating Redux In Settings