Open Event Server – Export Event as a Pentabarf XML 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 makes his event public, others can view it and buy tickets if interested.

To make event promotion easier, we also provide the event organizer to export his event as a Pentabarf XML file. Pentabarf XML is used to store events/conferences in a format which most of the scheduling applications can read and add that particular event/conference to the user’s schedule.

Server side – generating the Pentabarf XML file

Here we will be using the pentabarf package for Python for parsing and creating the file.

from pentabarf.Conference import Conference
from pentabarf.Day import Day
from pentabarf.Event import Event
from pentabarf.Person import Person
from pentabarf.Room import Room
  • We define a class PentabarfExporter which has a static method export(event_id).
  • Query the event using the event_id passed and start forming the event in the required format:
event = EventModel.query.get(event_id)
diff = (event.ends_at - event.starts_at)

conference = Conference(title=event.name, start=event.starts_at, end=event.ends_at,
                       days=diff.days if diff.days > 0 else 1,
                       day_change="00:00", timeslot_duration="00:15",
                       venue=event.location_name)
dates = (db.session.query(cast(Session.starts_at, DATE))
        .filter_by(event_id=event_id)
        .filter_by(state='accepted')
        .filter(Session.deleted_at.is_(None))
        .order_by(asc(Session.starts_at)).distinct().all())
  • We have queried for the dates of the event and saved it in dates.
  • We will now iterate over each date and query the microlocations who have a session on that particular date.
for date in dates:
   date = date[0]
   day = Day(date=date)
   microlocation_ids = list(db.session.query(Session.microlocation_id)
                            .filter(func.date(Session.starts_at) == date)
                            .filter_by(state='accepted')
                            .filter(Session.deleted_at.is_(None))
                            .order_by(asc(Session.microlocation_id)).distinct())
  • For each microlocation thus obtained, we will query for accepted sessions to be held at those microlocations.
  • We will also initialize a Room for each microlocation.
for microlocation_id in microlocation_ids:
   microlocation_id = microlocation_id[0]
   microlocation = Microlocation.query.get(microlocation_id)
   sessions = Session.query.filter_by(microlocation_id=microlocation_id) \
       .filter(func.date(Session.starts_at) == date) \
       .filter_by(state='accepted') \
       .filter(Session.deleted_at.is_(None)) \
       .order_by(asc(Session.starts_at)).all()

   room = Room(name=microlocation.name)
  • We will now iterate over the aabove-obtained sessions and instantiate an Event for each session.
  • Then we will iterate over all the speakers of that session and instantiate a Person for each speaker.
  • Finally, we will add that Event to the Room we created earlier.
for session in sessions:

   session_event = Event(id=session.id,
                         date=session.starts_at,
                         start=session.starts_at,
                         duration=str(session.ends_at - session.starts_at) + "00:00",
                         track=session.track.name,
                         abstract=session.short_abstract,
                         title=session.title,
                         type='Talk',
                         description=session.long_abstract,
                         conf_url=url_for('event_detail.display_event_detail_home',
                                          identifier=event.identifier),
                         full_conf_url=url_for('event_detail.display_event_detail_home',
                                               identifier=event.identifier, _external=True),
                         released="True" if event.schedule_published_on else "False")

   for speaker in session.speakers:
       person = Person(id=speaker.id, name=speaker.name)
       session_event.add_person(person)

   room.add_event(session_event)
  • Then we will add the room to the day and then add each day to the conference.
day.add_room(room)
conference.add_day(day)
  • Finally, we will call the generate method of the conference to generate the XML file. This can be directly written to the file.
return conference.generate("Generated by " + get_settings()['app_name'])

Obtaining the Pentabarf XML file:

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

GET - /v1/events/{event_identifier}/export/pentabarf

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the event as a Pentabarf XML file. It returns the task of the URL 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 Celery task. If the task completed successfully he 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.

Hence, now the event can be added to any scheduling app which recognizes the Pentabarf XML format.

References

Add RSS feed and JSON output based on type specified with query param

The idea behind writing this blog post is to discuss the method on how RSS feed and JSON output sources have been included in loklak to provide respective data sources based on the type specified with query as a parameter.

Accessing Current Query in Info-box

Accessing link to RSS feed and JSON output of loklak requires the Query to be passed as a value with parameter ‘q’ (e.g. api/search.json?q=FOSSASIA or api/search.rss?q=FOSSASIA). In order to represent links as buttons in Info-box at sidebar of loklak.org, current Query needs to be accessed/stored inside Info-box from ngrx store.

public stringQuery;
...
this.store.select(fromRoot.getQuery).subscribe(
    query => this.stringQuery = query.displayString);

 

Firstly stringQuery variable is created to store the current Query from store. As the Query can be changed in store (User might search for several Queries), storing of current Query needs to be done inside ngOnChanges().

Checking type associated with Query

There are various types associated with Query to get different type of results like ‘from:FOSSASIA will give results specifically from ‘FOSSASIA’ which will have different query parameter from other types like ‘@FOSSASIA’ or ‘#FOSSASIA’. To assign appropriate Query parameter to each of these types, we need to check the Query pattern to apply Query param based on the type.

import { hashtagRegExp, fromRegExp, mentionRegExp }
      from ‘../../utils/reg-exp’;
...

if ( hashtagRegExp.exec(this.stringQuery) !== null ) {
	// Check for hashtag this.stringQuery
	this.queryString = ‘%23 + hashtagRegExp.exec(
	this.stringQuery)[1] + '' + hashtagRegExp.exec(
	this.stringQuery)[0];
} else if ( fromRegExp.exec(this.stringQuery) !== null ) {
	// Check for from user this.stringQuery
	this.queryString = ‘from%3A’ + fromRegExp.exec(
	this.stringQuery)[1];
} else if ( mentionRegExp.exec(this.stringQuery) !== null ) {
	// Check for mention this.stringQuery
	this.queryString = ‘%40 + mentionRegExp.exec(
	this.stringQuery)[1];
} else {
	// for other queries
	this.queryString = this.stringQuery;
}

 

Note: hashtagRegExp, fromRegExp, mentionRegExp are the utility functions created to match the pattern of given string (Query) in order to classify the preceding type associated with Query. These are provided here as a reference, which can be used to add more of the types.

Passing current queryString in RSS and JSON link

General link for both RSS and JSON data remains same for each Query passed, only the type associated would be changed in the Query value. So representing the link in an anchor tag in UI would be as –

<a class=“data rss” href=“
		http://api.loklak.org/api/search.
		rss?timezoneOffset=-330&q=
		{{stringQuery}}” target=“_blank”>
</a>
<a class=”data json” href=”http://api.
		loklak.org/api/search
		.json?timezoneOffset=-330&q=
		{{stringQuery}}” target=”_blank“>
</a>

 

{{stringQuery}} is the actual query parameter to be passed to get the required results.

Testing RSS feed and JSON data

Search for a query on loklak, and click on the the RSS or JSON button below sidebar on results page and compare with the results.

RSS and JSON button should be similar to –

Resources

Stripe Authorization in Open Event Server

Stripe is a popular software platform for online payments. Since Open Event  allows the event organizers to sell tickets, an option to accept payments through Stripe is extremely beneficial to the organizer. Stripe allows accepting payments on other’s behalf using Connect. Connect is the Stripe’s full stack solution for platforms that need to process payments and process to multiple parties. This blog post goes over how Event organizers are able to link their Stripe accounts in order to accept payments later.

Registering the platform

The Admin of the Open Event Server will create an account on Stripe and register the platform. Upon creating the  account he/she will get a secret_key and publishable_key.  Moreover on registering the platform a client_id will be provided. These keys are saved in the application settings and only the Admin is authorized to view or change them.

Connecting the Organiser’s account

The Open Event Frontend has a wizard for creating an Event. It provides the organiser an option to connect his/her Stripe account in order to accept payments.

Upon clicking the following button, the organiser is directed to Stripe where he/she can fill the required details.  

The button directs the organizer to the following URL:

https://connect.stripe.com/oauth/authorize?response_type=code&client_id=client_id&scope=read_write&redirect_uri=redirect_uri 

The above URL has the following parameters:

  • client_id – The client ID acquired when registering your platform.required.
  • response_type – Response type. The value is always code. required.
  • redirect_uri – The URL to redirect the customer to after authorization.
  • scope – We need it to be read_write in order to be able to charge on behalf of the customer later.

After successfully entering the required details, the organizer is redirected to the redirect_url as specified in the above URL with a query parameter named as authorization_code. The Frontend sends this code to the Server using the Stripe Authorization endpoint which will be discussed in detail below.

Fetching Tokens from Stripe

The Server accepts the authorization_code by exposing the Stripe Authorization endpoint. It then uses it to fetch organizer’s details and token from Stripe and stores it for future use.

The schema for Stripe Authorization is extremely simple. We require the client to send an authorization_code which will be used to fetch the details. Stripe_publishable_key of the event organizer is exposed via the endpoint and will be used by the Frontend later.

class StripeAuthorizationSchema(Schema):
    """
        Stripe Authorization Schema
    """

    class Meta:
        """
        Meta class for StripeAuthorization Api Schema
        """
        type_ = 'stripe-authorization'
        self_view = 'v1.stripe_authorization_detail'
        self_view_kwargs = {'id': '<id>'}
        inflect = dasherize

    id = fields.Str(dump_only=True)
    stripe_publishable_key = fields.Str(dump_only=True)
    stripe_auth_code = fields.Str(load_only=True, required=True)

    event = Relationship(attribute='event',
                self_view='v1.stripe_authorization_event',
                self_view_kwargs={'id': '<id>'},
                related_view='v1.event_detail',
                related_view_kwargs={'stripe_authorization_id':                      '<id>'},
                schema="EventSchema",
                type_='event')

We use the Requests library in order to fetch the results. First we fetch the client_id that we had stored in the application settings using a helper method called get_credentials. We then use it along with the authorization_code in order to make a POST request to Stripe Connect API. The full method is given below for reference.

@staticmethod
def get_event_organizer_credentials_from_stripe(stripe_auth_code):
        """
        Uses the stripe_auth_code to get the other credentials for the event organizer's stripe account
        :param stripe_auth_code: stripe authorization code
        :return: response from stripe
        """
        credentials = StripePaymentsManager.get_credentials()

        if not credentials:
            raise Exception('Stripe is incorrectly configured')

        data = {
            'client_secret': credentials['SECRET_KEY'],
            'code': stripe_auth_code,
            'grant_type': 'authorization_code'
        }

        response = requests.post('https://connect.stripe.com/oauth/token', data=data)
        return json.loads(response.text)

We call the above method before creating the object using the before_create_object method of Marshmallow which allows us to do data preprocessing and validations.

If the request was a success, the response from Stripe connect API includes all the details necessary to accept payments on their behalf. We add these fields to the data and save it in the database.

{
  "token_type": "bearer",
  "stripe_publishable_key": PUBLISHABLE_KEY,
  "scope": "read_write",
  "livemode": false,
  "stripe_user_id": USER_ID,
  "refresh_token": REFRESH_TOKEN,
  "access_token": ACCESS_TOKEN
}

In case there was an error, an error_description would be returned. This error_description is sent back to the frontend and shown to the event organizer.

{
  "error": "invalid_grant",
  "error_description": "Authorization code already used:                                               
                        AUTHORIZATION_CODE"
}

After successfully fetching the results, we save it inside the database and return the stripe_publishable_key which will be used by the Frontend when charging the ticket buyers later.

Lastly we can go over the Stripe Authorization model as well. The stripe_secret_key will be used to charge the customers later.

id = db.Column(db.Integer, primary_key=True)
stripe_secret_key = db.Column(db.String)
stripe_refresh_token = db.Column(db.String)
stripe_publishable_key = db.Column(db.String)
stripe_user_id = db.Column(db.String)
stripe_auth_code = db.Column(db.String)

References

 

Open Event Server – Export Event as xCalendar 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 makes his event public, others can view it and buy tickets if interested.

To make event promotion easier, we also provide the event organizer to export his event as an xCalendar file. xCal is an XML representation of the iCalendar standard. xCal is not an alternative nor next generation of iCalendar. xCal represents iCalendar components, properties, and parameters as defined in iCalendar. This format was selected to ease its translation back to the iCalendar format using an XSLT transform.

Server side – generating the xCal file

Here we will be using the xml.etree.ElementTree package for Python for parsing and creating XML data.

from xml.etree.ElementTree import Element, SubElement, tostring
  • We define a class XCalExporter which has a static method export(event_id).
  • Query the event using the event_id passed and start forming the calendar:
event = Event.query.get(event_id)

tz = event.timezone or 'UTC'
tz = pytz.timezone(tz)

i_calendar_node = Element('iCalendar')
i_calendar_node.set('xmlns:xCal', 'urn:ietf:params:xml:ns:xcal')
v_calendar_node = SubElement(i_calendar_node, 'vcalendar')
version_node = SubElement(v_calendar_node, 'version')
version_node.text = '2.0'
prod_id_node = SubElement(v_calendar_node, 'prodid')
prod_id_node.text = '-//fossasia//open-event//EN'
cal_desc_node = SubElement(v_calendar_node, 'x-wr-caldesc')
cal_desc_node.text = "Schedule for sessions at " + event.name
cal_name_node = SubElement(v_calendar_node, 'x-wr-calname')
cal_name_node.text = event.name
  • We query for the accepted sessions of the event and store it in sessions
sessions = Session.query \
   .filter_by(event_id=event_id) \
   .filter_by(state='accepted') \
   .filter(Session.deleted_at.is_(None)) \
   .order_by(asc(Session.starts_at)).all()
  • We then iterate through all the sessions in sessions.
  • If it is a valid session, we instantiate a SubElement and store required details
v_event_node = SubElement(v_calendar_node, 'vevent')

method_node = SubElement(v_event_node, 'method')
method_node.text = 'PUBLISH'

uid_node = SubElement(v_event_node, 'uid')
uid_node.text = str(session.id) + "-" + event.identifier

dtstart_node = SubElement(v_event_node, 'dtstart')
dtstart_node.text = tz.localize(session.starts_at).isoformat()

…. So on
  • We then loop through all the speakers in that particular session and add it to the xCal calendar node object as well.
for speaker in session.speakers:
   attendee_node = SubElement(v_event_node, 'attendee')
   attendee_node.text = speaker.name
  • And finally, the string of the calendar node is returned. This is the xCalendar file contents. This can be directly written to a file.
return tostring(i_calendar_node)

Obtaining the xCal file:

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

GET - /v1/events/{event_identifier}/export/xcal

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the event as an xCal 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 Celery task. If the task completed successfully he 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.

Hence, now the event can be added to any scheduling app which recognizes the xcs format.

References

Displaying Top Hashtags by sorting Hashtags based on the frequency

It is a good idea to display top hashtags on sidebar of loklak. To represent them, it is really important to sort out all unique hashtags on basis of frequency from results obtained from api.loklak. The implementation of the process involved would be discussed in this blog.

Raw Hashtag result

The Hashtags obtained as a parameter of type array containing array of strings into the sortHashtags() method inside the component typescript file of Info-box Component is in Raw Hashtag form.

Making Array of all Hashtags

Firstly, all the Hashtags would be added to a new Array – stored.

sortHashtags( statistics ) {
    let stored = [];
    if (statistics !== undefined && 
        statistics.length !== 0) {
        for (const s in statistics) {
            if (s) {
                for (let i = 0;i <
                    statistics[s].length; i++) {
                    stored.push(statistics[s][i]);
                }
            }
        }
    }
}

 

stored.push( element ) will add each element ( Hashtag ) into the stored array.

Finding frequency of each unique Hashtag

array.reduce() method would be used to store all the Hashtags inside the stored array with frequency of each unique Hashtag (e.g. [ ‘Hashtag1’: 3, ‘Hashtag2’: 2, ‘Hashtag3’: 5, … ]), where Hashtag1 has appeared 3 times, Hashtag2 appeared 2 times and so on.

stored = stored.reduce(function (acc, curr) {
    if (typeof acc[curr] === undefined’) {
        acc[curr] = 1;
    } else {
        acc[curr] += 1;
    }
    return acc;
}, []);

 

stored.reduce() would store the result inside stored array in the format mentioned above.

Using Object to get the required result

Object would be used with different combination of associated methods such as map, filter, sort and slice to get the required Top 10 Hashtags sorted on the basis of frequency of each unique Hashtag.

this.topHashtags = Object.keys(stored)
    .map(key => key.trim())
    .filter(key => key !== ”)
    .map(key => ([key, stored[key]]))
    .sort((a, b) => b[1]  a[1])
    .slice(0, 10);

 

At last, the result is stored inside topHashtags array. First map method is used to trim out spaces from all the keys ( Hashtags ), first filter is applied to remove all those Hashtags which are empty and then mapping each Hashtag as an array with a unique index inside the containing array. At last, sorting each Hashtag on basis of the frequency using sort method and slicing the results to get Top 10 Hashtags to be displayed on sidebar of loklak.

Testing Top Hashtags

Search something on loklak.org to obtain sidebar with results. Now look through the Top 10 Hashtags being displayed on the Sidebar info-box.

Resources

Implementing Favorite Event option in the Toolbar of Event Details in the Open Event Android App

The Open Event Android app allows users to favorite any Event. The users can easily see their favorited events in a section called Favorites. This blog will illustrate about how favorite event option has been implemented in the Toolbar of Event Details section of the Open Event Android app.

We will straightaway move onto the code of event_details.xml. We can clearly observe that we need to implement a menu item in the toolbar or action bar. So, to implement that, we first need to add a favorite icon in the support actionbar. To do that, we add a menu item in the event_details.xml file present under the menu folder.

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <group android:id="@+id/event_menu">
      ...
  </group>
  
  <item
      android:id="@+id/eventShare"
      android:icon="@drawable/ic_share_white_24dp"
      android:title="@string/event_share"
      app:showAsAction="always"/>

  <item
      android:id="@+id/favoriteEvent"
      android:icon="@drawable/ic_baseline_favorite_border_white_24px"
      android:title="@string/favorite_event"
      app:showAsAction="always"/>
</menu>

We can clearly observe that a menu item having an id of favoriteEvent has been added. We create a vector drawable with a heart shaped having only border. We also create a vector drawable of heart shaped filled with white colour. android:icon is set to the border icon as default.

We have made our desired changes in the xml files. Now we will head onto the code of EventDetailsViewModel. Basically, we want the user to be able to favorite the event. So we add the function setFavoriteEvent to the ViewModel.

fun setFavorite(eventId: Long, favourite: Boolean) {
compositeDisposable.add(eventService.setFavorite(eventId, favourite)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
               Timber.d("Success")
}, {
               Timber.e(it, "Error")
               error.value = "Error"
}))
}

This function utilises eventId, which is of long type and favourite, which is of Boolean type. This function makes an RxAndroid network call to set favorite as false or true to the event having the eventId. Heading onto the code in EventDetailsFragment. In the onOptionsItemSelected function, we add the favoriteEvent item case in the switch statement.

R.id.favoriteEvent -> {
eventViewModel.setFavorite(eventId, !(eventShare.favorite))
if (eventShare.favorite) {
setFavoriteIcon(R.drawable.ic_baseline_favorite_border_white_24px)
} else {
setFavoriteIcon(R.drawable.ic_baseline_favorite_white_24px)
}
return true

}

We clearly observe that we are setting the eventId and boolean fields in the .setFavorite function. There is a function called setFavoriteIcon, present in a conditional which sets the appropriate favorite icon.

Let us head onto the code of the function setFavoriteIcon

private fun setFavoriteIcon(id: Int){
menuActionBar?.findItem(R.id.favoriteEvent)?.icon = context?.let { ContextCompat.getDrawable(it, id) }
}

We observe that the code is quite straight forward, We just replace the menu item icon of id favoriteEvent with id. We add a conditional in the ViewModel observer of event. This conditional helps in setting the correct favorite image icon even after configuration changes.

eventViewModel.event.observe(this, Observer {
if (eventShare.favorite) {
setFavoriteIcon(R.drawable.ic_baseline_favorite_white_24px)
}
})

This is how we have successfully implemented a Favorite Event option in the Toolbar of EventDetailsFragment.

Additional Resources

Integrating Stripe OAuth in Open Event Frontend

Why is Stripe Oauth needed in frontend? Open event allows organizers to add tickets and accepts payments for tickets through various modes for example, Credit card, Debit card, Netbanking and offline payments. Stripe allows users to accept payments into their linked accounts on various online platforms after they provide client secret and publishable key. So to enable online payments in open event, organizers were required to authenticate their stripe account. This is done through Stripe OAuth.

Flow of OAuth

To allow organizers to link their stripe account admin has to enable stripe under payment gateway in admin settings. Admin provides his client ID and secret key. Admin also sets the redirect URL for his app on the stripe dashboard. After enabling these settings organizer will see an option to link their stripe account to open event when they are creating an event with paid tickets.

Here is what open event frontend does when we click connect to stripe button:

  1. Opens a popup to allow organizer to fill his stripe credentials and authorize open event app to access their secret and publishable key.
  2. Once the organizer fills his credentials and authorizes open event app, open event frontend fetches organizers auth code and saves it to server.
  3. Server on receiving auth code from frontend makes a request to stripe using the auth code to retrieve the publishable key and secret key.
  4. Once these are fetched server saves this information against the event so that all payments for that event can go to the linked stripe account.

Implementing the Frontend portion:

  • Choosing the library:

After looking at various libraries that support OAuth for Ember applications we decided to use Torii. Torii is the library that allows the addition of OAuth for various social apps such as Facebook, Google and Stripe too. It allows writing a custom provider for OAuth in case we do not want to use clients for which torii provides supports by default.

  • Implementing Stripe Provider:

Default provider for stripe given by torii fetched the client ID and redirect URL from environment.js file. But since in open event we have already saved client id of admin in our database so we will extend default stripe provider and modify its client Id so that it fetches client id from server. Code for extending default provider is given here:

import stripeConnect from 'torii/providers/stripe-connect';
import { alias } from '@ember/object/computed';
import { inject } from '@ember/service';
import { configurable } from 'torii/configuration';

function currentUrl() {
 let url = [window.location.protocol,
   '//',
   window.location.host].join('');
 if (url.substr(-1) !== '/') {
   url += '/';
 }
 return url;
}

export default stripeConnect.extend({

 settings: inject(),

 clientId: alias('settings.stripeClientId'),

 redirectUri: configurable('redirectUri', function() {
   return `${currentUrl()}torii/redirect.html`;
 })

});

 

We have fetched clientId from our settings service as alias(‘settings.stripeClientId’).

We have already defined settings in our services so we just need to inject the service here to be able to use it.

By default torii provides redirect url as {currentUrl}/torii/redirect.html. But in open event frontend we allow organizers to edit information on two routes and torii suggests in its docs to use {baseUrl}/torii/redirect.html as the redirect url to avoid potential vulnerability. So we also modified the default redirect url building method.

Saving information to server

Once we get the authorization token from stripe we send it to the server and save it to stripe-authorization model. The logic for the same is given below:

connectStripe() {
     this.get('data.event.stripeAuthorization.content') ? '' : this.set('data.event.stripeAuthorization', this.store.createRecord('stripe-authorization'));
     this.get('torii').open('stripe')
       .then(authorization => {
         this.set('data.event.stripeAuthorization.stripeAuthCode', authorization.authorizationCode);
       })
       .catch(error => {
         this.get('notify').error(this.get('l10n').t(`${error.message}. Please try again`));
       });
   },

 

This action gets called when we click on connect to stripe button. This action calls the stripe provider and opens a popup to enable the organizer to authenticate his stripe account.
Full code for this can be seen here.

In this way we connect the stripe service to open event to allow the organizer to receive payments for his events.

Resources
  • Stripe : Documentation on Stripe-Connect : Link
  • Torii: Library to implement Oauth. : Link
  • Implementation: Link to PR showing its implementation : Link

Improving the JSON file upload structure – Open Event Web app

Open Event Web app generator also allows user to upload JSON file for the event data other than the API endpoint. The generator used the socket connection ID to uniquely identify uploaded files on the server which worked good for a single socket connection but failed for multiple due to overlap of connection IDs which resulted in crashing of web app. The problem is fixed by providing a unique ID to every file uploaded on the server and creating a separate field for uploaded file ID in the request body.

How to add listener for file upload?

A listener for socket ‘file’ event is added in the file app.js, which is triggered when the event namely file is emitted by the socket. The file ID kept unique by introducing a counter for number of files uploaded on the server till now and incrementing the counter subsequently for every new file.

ss(socket).on('file', function(stream, file) {
 generator.startZipUpload(count, socket);
 console.log(file);
 filename = path.join(__dirname, '..', 'uploads/connection-' +    count.toString()) + '/upload.zip';
 count += 1;
 stream.pipe(fs.createWriteStream(filename));
});

 

The procedure named startZipUpload in generator.js is executed when the zip file upload starts which further calls the helper function to make uploads directory on the server.

exports.startZipUpload = function(id, socket) {
 console.log('========================ZIP UPLOAD START\n\n');
 distHelper.makeUploadsDir(id, socket);
 distHelper.cleanUploads(id);
};

Creating uploads directory

Uploads directory is created in the root directory using the file system interfaces, the ID passed as parameter ensures that the file names do not overlap.

makeUploadsDir: function(id, socket) {
 fs.mkdirpSync(uploadsPath + '/connection-' + id.toString());
 socket.emit('uploadsId', id);
}

Embedding uploads ID with the data

After the successful creation of uploads directory and the file, the socket emits the ID of uploaded file through the event uploadsID. The value of ID thus received is embedded in the object namely data along with the other entries in the form.

socket.on('uploadsId', function(data) {
 initialValue = data;
});

function getData(initValue) {
 const data = initValue;
 const formData = $('#form').serializeArray();

 formData.forEach(function(field) {
   if (field.name === 'email') {
     data.email = field.value;
   }
   ....
   ....
   ....
   ....
 
   if (field.name === 'apiVersion') {
     data.apiVersion = field.value;
   }
 });

Resources

Tags:  GsoC’18, Fossasia, Eventyay, Open Event Web App, JSON file upload

 

Open Event Server – Export Event as an iCalendar 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 makes his event public, others can view it and buy tickets if interested.

To make event promotion easier, we also provide the event organizer to export his event as an iCalendar file. Going by the Wikipedia definition, iCalendar is a computer file format which allows Internet users to send meeting requests and tasks to other Internet users by sharing or sending files in this format through various methods. The files usually have an extension of .ics. With supporting software, such as an email reader or calendar application, recipients of an iCalendar data file can respond to the sender easily or counter propose another meeting date/time. The file format is specified in a proposed internet standard (RFC 5545) for calendar data exchange.

Server side – generating the iCal file

Here we will be using the icalendar package for Python as the file writer.

from icalendar import Calendar, vCalAddress, vText
  • We define a class ICalExporter which has a static method export(event_id).
  • Query the event using the event_id passed and start forming the calendar:
event = EventModel.query.get(event_id)

cal = Calendar()
cal.add('prodid', '-//fossasia//open-event//EN')
cal.add('version', '2.0')
cal.add('x-wr-calname', event.name)
cal.add('x-wr-caldesc', "Schedule for sessions at " + event.name)
  • We query for the accepted sessions of the event and store it in sessions.
sessions = Session.query \
   .filter_by(event_id=event_id) \
   .filter_by(state='accepted') \
   .filter(Session.deleted_at.is_(None)) \
   .order_by(asc(Session.starts_at)).all()
  • We then iterate through all the sessions in sessions.
  • If it is a valid session, we instantiate an icalendar event and store required details.
event_component = icalendar.Event()
event_component.add('summary', session.title)
event_component.add('uid', str(session.id) + "-" + event.identifier)
event_component.add('geo', (event.latitude, event.longitude))
event_component.add('location', session.microlocation.name or '' + " " + event.location_name)
event_component.add('dtstart', tz.localize(session.starts_at))
event_component.add('dtend', tz.localize(session.ends_at))
event_component.add('email', event.email)
event_component.add('description', session.short_abstract)
event_component.add('url', url_for('event_detail.display_event_detail_home',
                                  identifier=event.identifier, _external=True))
  • We then loop through all the speakers in that particular session and add it to the iCal Event object as well.
for speaker in session.speakers:
   # Ref: http://icalendar.readthedocs.io/en/latest/usage.html#file-structure
   # can use speaker.email below but privacy reasons
   attendee = vCalAddress('MAILTO:' + event.email if event.email else '[email protected]')
   attendee.params['cn'] = vText(speaker.name)
   event_component.add('attendee', attendee)
  • This event_component is then added to the cal object that we created in the beginning.
cal.add_component(event_component)
  • And finally, the cal.to_ical() is returned. This is the iCalendar file contents. This can be directly written to a file.
return cal.to_ical()

Obtaining the iCal file:

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

GET - /v1/events/{event_identifier}/export/ical

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the event as an iCal file. It returns the task of the URL 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 Celery task. If the task completed successfully he 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.

Hence, now the event can be added to any scheduling app which recognizes the ics format.

References