Implementing Accepting and Rejecting Proposals in Open Event Frontend

This blog post will illustrate how to add buttons to accept and reject proposal and making them functional. Sessions tab in event dashboard communicates with the following APIs of Open Event Server.

  • GET                    /v1/sessions
  • PATCH              /v1/sessions
What is meant by accepting or rejecting a session in open event?

Sessions are part of event which include one or many speakers. Speakers can propose one or many sessions for a event. Now it is duty of organizer to accept some proposals and reject others. Open event provides two options to accept or reject a proposal i.e. with email or without email.

For this we need to send a key value pair which includes whether we want to send email or not along with other parameters which include current state of session and other important properties. A typical request to alter state of session looks like this.

{
  "data": {
    "attributes": {
      "title": "Micropython Session",
      "level": 1,
      "starts-at": "2017-06-01T10:00:00.500127+00:00",
      "ends-at": "2017-06-01T11:00:00.500127+00:00",
      "created-at": "2017-05-01T01:24:47.500127+00:00",
      "is-mail-sent": false,
      "send-email": true,
    },
    "type": "session",
    "id": "1"
  }
}
Implementing in frontend

We start by providing two buttons for a pending session. One to accept the session and other to reject the session.

On clicking either accept or reject button we get two options to choose i.e. with email and without email. Depending on what organizer chooses a action is fired from the template and sent to controller. Template code for these buttons looks something like this.

class=“ui vertical compact basic buttons”> {{#unless (eq record.state ‘accepted’)}} {{#ui-dropdown class=‘ui icon bottom right pointing dropdown button’}} class=“green checkmark icon”>

class=“menu”>

class=“item” {{action acceptProposal record true}}>{{t ‘With email’}}

 


class=“item” {{action acceptProposal record false}}>{{t ‘Without email’}}

 

      </div>
    {{/ui-dropdown}}
  {{/unless}}
  {{#unless (eq record.state 'rejected')}}
    {{#ui-dropdown class='ui icon bottom right pointing dropdown button'}}
      <i class="red remove icon"></i>

class=“menu”>

class=“item” {{action rejectProposal record true}}>{{t ‘With email’}}

 


class=“item” {{action rejectProposal record false}}>{{t ‘Without email’}}

 

      </div>
    {{/ui-dropdown}}
  {{/unless}}
</div>

We can see that for with email button we trigger accept proposal button with two parameters record and true. Record contains the instance of session and true signifies that we are sending email. Similar is the case with without email button. Controller for these actions looks something like this.

acceptProposal(session, sendEmail) {
      session.set('sendEmail', sendEmail);
      session.set('state', 'accepted');
      session.set('isMailSent', sendEmail);
      this.set('isLoading', true);
      session.save()
        .then(() => {
          sendEmail ? this.notify.success(this.get('l10n').t('Session has been accepted and speaker has been notified via email.'))
            : this.notify.success(this.get('l10n').t('Session has been accepted'));
        })
        .catch(() => {
          this.notify.error(this.get('l10n').t('An unexpected error has occurred.'));
        })
        .finally(() => {
          this.set('isLoading', false);
        });
    },
    rejectProposal(session, sendEmail) {
      session.set('sendEmail', sendEmail);
      session.set('state', 'rejected');
      session.set('isMailSent', sendEmail);
      this.set('isLoading', true);
      session.save()
        .then(() => {
          sendEmail ? this.notify.success(this.get('l10n').t('Session has been rejected and speaker has been notified via email.'))
            : this.notify.success(this.get('l10n').t('Session has been rejected'));
        })
        .catch(() => {
          this.notify.error(this.get('l10n').t('An unexpected error has occurred.'));
        })
        .finally(() => {
          this.set('isLoading', false);
        });
    }

For accepting with email we set sendEmail field to true and send the query to server. Similarly for reject proposal action we follow same procedure.

Conclusion

Implementing buttons like these, and defining proper actions like these we are able to change the state of session with options to send email or not.

Resources

Continue Reading

Open Event Server – Export Sessions as PDF File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the sessions in a very detailed view in the event management dashboard. He can see the statuses of all the sessions. The possible statuses are pending, accepted, confirmed and rejected. He/she can take actions such as accepting/rejecting the sessions.

If the organizer wants to download the list of all the sessions as a PDF file, he or she can do it very easily by simply clicking on the Export As PDF button in the top right-hand corner.

Let us see how this is done on the server.

Server side – generating the Sessions PDF file

Here we will be using the pisa package which is used to convert from HTML to PDF. It is a html2pdf converter which uses ReportLab Toolkit, the HTML5lib and pyPdf. It supports HTML5 and CSS 2.1 (and some of CSS 3). It is completely written in pure Python so it is platform independent.

from xhtml2pdf import pisa

We have a utility method create_save_pdf which creates and saves PDFs from HTML. It takes the following arguments:

  • pdf_data – This contains the HTML template which has to be converted to PDF.
  • key – This contains the file name
  • dir_path – This contains the directory

It returns the newly formed PDF file. The code is as follows:

def create_save_pdf(pdf_data, key, dir_path='/static/uploads/pdf/temp/'):
   filedir = current_app.config.get('BASE_DIR') + dir_path

   if not os.path.isdir(filedir):
       os.makedirs(filedir)

   filename = get_file_name() + '.pdf'
   dest = filedir + filename

   file = open(dest, "wb")
   pisa.CreatePDF(io.BytesIO(pdf_data.encode('utf-8')), file)
   file.close()

   uploaded_file = UploadedFile(dest, filename)
   upload_path = key.format(identifier=get_file_name())
   new_file = upload(uploaded_file, upload_path)
   # Removing old file created
   os.remove(dest)

   return new_file

The HTML file is formed using the render_template method of flask. This method takes the HTML template and its required variables as the arguments. In our case, we pass in ‘pdf/sessions_pdf.html’(template) and sessions. Here, sessions is the list of sessions to be included in the PDF file. In the template, we loop through each item of sessions and check if it is deleted or not. If it not deleted then we print its title, state, list of its speakers, track, created at and has an email been sent or not. All these fields form a row in the table. Hence, each session is a row in our PDF file.

The various columns are as follows:

<thead>
<tr>
   <th>
       {{ ("Title") }}
   </th>
   <th>
       {{ ("State") }}
   </th>
   <th>
       {{ ("Speakers") }}
   </th>
   <th>
       {{ ("Track") }}
   </th>
   <th>
       {{ ("Created At") }}
   </th>
   <th>
       {{ ("Email Sent") }}
   </th>
</tr>
</thead>

A snippet of the code which handles iterating over the sessions list and forming a row is as follows:

{% for session in sessions %}
   {% if not session.deleted_at %}
       <tr class="padded" style="text-align:center; margin-top: 5px">
           <td>
               {% if session.title %}
                   {{ session.title }}
               {% else %}
                   {{ "-" }}
               {% endif %}
           </td>
           <td>
               {% if session.state %}
                   {{ session.state }}
               {% else %}
                   {{ "-" }}
               {% endif %}
           </td>
           <td>
               {% if session.speakers %}
                   {% for speaker in session.speakers %}
                       {{ speaker.name }}<br>
                   {% endfor %}
               {% else %}
                   {{ "-" }}
               {% endif %}
           </td>
          ….. And so on
       </tr>
   {% endif %}
{% endfor %}

The full template can be found here.

Obtaining the Sessions PDF file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/sessions/pdf

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the sessions of the event as a PDF file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

Resources

Continue Reading

Open Event Server – Export Sessions as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the sessions in a very detailed view in the event management dashboard. He can see the statuses of all the sessions. The possible statuses are pending, accepted, confirmed and rejected. He/she can take actions such as accepting/rejecting the sessions.

If the organizer wants to download the list of all the sessions as a CSV file, he or she can do it very easily by simply clicking on the Export As CSV button in the top right-hand corner.

Let us see how this is done on the server.

Server side – generating the Sessions CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_sessions_csv which takes the sessions to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_sessions_csv(sessions):
   headers = ['Session Title', 'Session Speakers',
              'Session Track', 'Session Abstract', 'Created At', 'Email Sent']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each session in sessions and form a row for that session by separating the values of each of the columns by a comma. Here, every row is one session.
  • As a session can contain multiple speakers we iterate over each speaker for that particular session and append each speaker to a string. ‘;’ is used as a delimiter. This string is then added to the row.
  • The newly formed row is added to the rows list.
for session in sessions:
   if not session.deleted_at:
       column = [session.title + ' (' + session.state + ')' if session.title else '']
       if session.speakers:
           in_session = ''
           for speaker in session.speakers:
               if speaker.name:
                   in_session += (speaker.name + '; ')
           column.append(in_session[:-2])
       else:
           column.append('')
       column.append(session.track.name if session.track and session.track.name else '')
       column.append(strip_tags(session.short_abstract) if session.short_abstract else '')
       column.append(session.created_at if session.created_at else '')
       column.append('Yes' if session.is_mail_sent else 'No')
       rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
writer = csv.writer(temp_file)
from app.api.helpers.csv_jobs_util import export_sessions_csv
content = export_sessions_csv(sessions)
for row in content:
   writer.writerow(row)

Obtaining the Sessions CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/sessions/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the sessions of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

Resources

Continue Reading

Persisting Cookies over SUSI.AI Subdomains

In this blog post, we are going to see, how the cookies are persisted for all the subdomains of SUSI.AI. By this implementation, the session for the user is maintained over all the SUSI.AI websites.

The cookies are persisted over these SUSI.AI websites –

All the web clients are developed using ReactJs framework and for the manipulation of cookies in React, a npm package – universal-cookie is used. Firstly, we will see how the cookies are set/created during login, followed by removal of cookies during logout.

Creating cookies

  • Firstly, we need to import the universal-cookie package into the component and create an instance of it.
import Cookies from 'universal-cookie';
const cookies = new Cookies();

Now, we can set a new cookie using this instance of cookies.

  • The following snippet sets the cookies after the login is done.

// AJAX call for login

let email = this.state.email.trim();
$.ajax({
  url: loginEndpoint,
  dataType: 'jsonp',
  jsonp: 'callback',
  crossDomain: true,
  success: function(response) {
  if (response.accepted) {
    cookies.set('serverUrl', BASE_URL, {
      path: '/',
      domain: '.susi.ai',
    });
    let accessToken = response.access_token;
    let state = this.state;
    let time = response.valid_seconds;
    this.handleOnSubmit(email, accessToken, time);
  }.bind(this),
  error: function(errorThrown) {
    .
    .
    .
  }.bind(this)    
});

handleOnSubmit = (email, loggedIn, showAdmin, time) => {
  let state = this.state;
  if (state.success) {
    cookies.set('loggedIn', loggedIn, {
      path: '/',
      maxAge: time,
      domain: '.susi.ai',
    });
    cookies.set('emailId', this.state.email, {
      path: '/',
      maxAge: time,
      domain: '.susi.ai',
    });
    this.props.history.push('/', { showLogin: false });
    window.location.reload();
  } else {
    this.setState({
      error: true,
      accessToken: '',
      success: false,
    });
  }
};

 

  • The cookies.set is a function provided by the package, that takes in three (3) parameters –
    • Cookie name
    • Cookie vallue
    • Options – an object containing the cookies properties
  • In the above example, say setting the loggedIn cookie, that contains the access token. We set the cookie name as loggedIn, the cookie value equal to the access token value received from the server response.
  • Apart from that, we have set 3 properties of the cookies, by passing an optional options parameter to the set function.
    • path – It indicates a URL path that must exist in the requested URL in order to send the Cookie header.
    • domainIt specifies allowed hosts to receive the cookie. If unspecified, it defaults to the host of the current document location, excluding subdomains.
    • maxAgeIt specifies a time duration after which the cookie gets expired.

Deleting cookies

  • It is mainly used, when a user wants to logout. It is used in the Logout component of the client’s codebase.
  • An approach to delete/remove a cookie is to set the expiry date of the cookie as Thu, 01 Jan 1970 00:00:01 GMT, which results in the removal of the cookie after a page refresh.
  • Following is the code snippet of how the cookies are removed to log-out a user of the website.

.
.
.
let deleteCookie = function(name, options = {}) {
  let cookieString = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
  if (options.domain) {
    cookieString = `${cookieString}domain=${options.domain};`;
  }
  if (options.path) {
    cookieString = `${cookieString}path=${options.path};`;
  }
  document.cookie = cookieString;
};
.
.
.
.
deleteCookie('loggedIn', { domain: '.susi.ai', path: '/' });
deleteCookie('serverUrl', { domain: '.susi.ai', path: '/' });
deleteCookie('emailId', { domain: '.susi.ai', path: '/' });
deleteCookie('showAdmin', { domain: '.susi.ai', path: '/' });
deleteCookie('username', { domain: '.susi.ai', path: '/' });

 

  • The deleteCookie function takes in 2 params –
    • Cookie name
    • options – an object containing the cookies properties
  • The options parameter needs to be passed while deleting the cookie, as it defines the scope for which the cookie has to be deleted.
  • The function creates a string and appends to it the expiry date, path, domain to the cookie name, if provided.
  • Finally, it sets the cookie by assigning the string to the document object.

Resources

Continue Reading

Adding Sessions and Events Statistics in the Admin Dashboard in Open Event Frontend

This blog article will illustrate how the admin statistics API for events and sessions is integrated and how the values of different types of sessions and events is added to the admin dashboard in   Open Event Frontend.

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

GET /v1/admin/statistics/events

GET /v1/admin/statistics/events

import Route from '@ember/routing/route';

export default Route.extend({
 async model() {
   return {
     events: await this.get('store').queryRecord('admin-statistics-event', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
sessions: await this.get('store').queryRecord('admin-statistics-session', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
   };
 }
});

The route file helps to fetch the total count of each type of session and event through the queries written in the code. queryRecord is used instead of query because a single record is expected to be returned by the API. The view route is /admin.

The model needs to extend the base model class and all the attributes of the model will be number since the all the data obtained via these models from the API will be numerical statistics.

For Events:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';export default ModelBase.extend({
draft     : attr('number'),
published : attr('number'),
past      : attr('number')
});

For Sessions:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';export default ModelBase.extend({
confirmed : attr('number'),
accepted  : attr('number'),
submitted : attr('number'),
draft     : attr('number'),
rejected  : attr('number'),
pending   : attr('number')
});

Once we retrieve the values of the attributes from the queries written in the route file we display the values of pending, rejected, accepted sessions and published, draft, past events.

class="label"> {{t 'Accepted'}}
class="value">
class="ui teal label"> {{model.sessions.accepted}}
</div> </div>
class="ui small statistic">
class="label"> {{t 'Draft'}}
class="value">
class="ui yellow label"> {{model.sessions.pending}}
</div> </div>
class="ui small statistic">
class="label"> {{t 'Rejected'}}
class="value">
class="ui red label"> {{model.sessions.rejected}}

Resources

Continue Reading

Creating the View Route for Sessions in Open Event Frontend

This blog article will illustrate how the creation of the view route for sessions is done and how the sessions API is integrated with it on Open Event Frontend, which allows for the sessions and their associated data to be displayed. Also, it illustrates how the template for my-sessions is modified to make it possible to reuse it for the view route with desired changes.

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

GET /v1/sessions/{session_identifier}

For displaying the complete session information on the view route, the session type,  speakers and session track are also required. All of these extra attributes have a relationship with a given session and hence can be fetched in a single query. Thus the model for the route is defined as follows:

model(params) {
return this.store.findRecord(‘session’, params.session_id, {
include: ‘session-type,speakers,track’
});

The view route is located at app/routes/my-sessions/view and the parent route, app/routes/my-sessions has another sub route within it called list. The list route shows upcoming and past sessions to the user based on the params passed to it. Thus a navigation is required to alternate between those two routes. However, this navigation should not be present in the view route. Thus the template my-sessions.hbs is modified as follows:

{{#if (and (not-includes session.currentRouteName ‘my-sessions.view’))}}
<h1 class=”ui header”>{{t ‘My Sessions’}}</h1>

{{#tabbed-navigation}}
{{#link-to ‘my-sessions.list’ ‘upcoming’ class=’item’}}
{{t ‘Upcomming Sessions’}}
{{/link-to}}
{{#link-to ‘my-sessions.list’ ‘past’ class=’item’}}
{{t ‘Past Sessions’}}
{{/link-to}}
{{/tabbed-navigation}}

</div>
{{outlet}}
</div>
{{else}}
{{outlet}}
{{/if}}

The session.currentRouteName property allows conditional rendering of the navigation component.

Finally the template for the view route is created:

   

{{#if (eq model.state ‘accepted’)}}

{{t ‘Accepted’}}

{{else if (eq model.state ‘pending’)}}

{{t ‘Pending’}}

{{else if (eq model.state ‘rejected’)}}

{{t ‘Rejected’}}

{{/if}}
</div>

{{t ‘From ‘}}{{moment-format model.startAt ‘ddd, MMM DD HH:mm A’}}{{t ‘ to ‘}}{{moment-format model.endsAt ‘ddd, MMM DD HH:mm A’}}

</div>

{{#if model.shortAbstract}}
<p> <i>{{model.shortAbstract}}</i> </p>
{{/if}}
{{#if model.sessionType.name}}
<h3 class=”ui left aligned header”>Session Type</h3>
<p>{{model.sessionType.name}}</p>
{{/if}}
{{#if model.track.name}}
<h3 class=”ui left aligned header”>Track</h3>
<p>{{model.track.name}}</p>
{{/if}}
{{#if model.slidesUrl}}
<h3 class=”ui left aligned header”>Slide</h3>
<a href=”{{model.slidesUrl}}”>{{t ‘Download’}}</a>
{{/if}}
</div>

Based on the state property of the session, the label displaying the status is appropriately coloured wherein green, yellow and red colours denote accepted, pending and rejected sessions respectively. Since most of the fields of the session model are optional in nature, all of the them are subjected to conditional checks for existence.

Resources

 

Continue Reading
Close Menu
%d bloggers like this: