Integrating Gravatar and Anonymizing Email Address in Feedback Section

SUSI skills are having a very nice feedback system that allows the user to rate skills from 1-star to 5-star and showing ratings in skills screens. SUSI also allow the user to post feedback about skills and display them. You can check out how posting feedback implemented here and how displaying feedback feature implemented here. To enhance the user experience, we are adding user gravatar in the feedback section and to respect user privacy, we are anonymizing the user email displayed in the feedback section. In this post, we will see how these features implemented in SUSI iOS.

Integrating Gravatar –

We are showing gravatar of the user before feedback. Gravatar is a service for providing globally-unique avatars. We are using user email address to get the gravatar. The most basic gravatar image request URL looks like this:

https://www.gravatar.com/avatar/HASH

where HASH is replaced with the calculated hash for the specific email address we are requesting. We are using the MD5 hash function to hash the user’s email address.

The MD5 hashing algorithm is a one-way cryptographic function that accepts a message of any length as input and returns as output a fixed-length digest value to be used for authenticating the original message.

In SUSI iOS, we have MD5Digest.swift file that gives the hash value of email string. We are using the following method to set gravatar:

if let userEmail = feedback?.email {
setGravatar(from: userEmail)
}
func setGravatar(from emailString: String) {
let baseGravatarURL = "https://www.gravatar.com/avatar/"
let emailMD5 = emailString.utf8.md5.rawValue
let imageString = baseGravatarURL + emailMD5 + ".jpg"
let imageURL = URL(string: imageString)
gravatarImageView.kf.setImage(with: imageURL)
}

Anonymizing User’s Email Address –

Before the implementation of this feature, the user’s full email address was displayed in the feedback section and see all review screen. To respect the privacy of the user, we are now only showing user email until the `@` sign.

In Feedback object, we have the email address string that we modify to show until `@` sign by following way:

if let userEmail = feedback?.email, let emailIndex = userEmail.range(of: "@")?.upperBound {
userEmailLabel.text = String(userEmail.prefix(upTo: emailIndex)) + "..."
}

 

Final Output –

Resources –

  1. Post feedback for SUSI Skills in SUSI iOS
  2. Displaying Skills Feedback on SUSI iOS
  3. What is MD5?
Continue ReadingIntegrating Gravatar and Anonymizing Email Address in Feedback Section

Open Event Frontend – Settings Service

This blog illustrates how the settings of a particular user are obtained in the Open Event Frontend web app. To access the settings of the user a service has been created which fetches the settings from the endpoint provided by Open Event Server.

Let’s see why a special service was created for this.

Problem

In the first step of the event creation wizard, the user has the option to link Paypal or Stripe to accept payments. The option to accept payment through Paypal or Stripe was shown to the user without checking if it was enabled by the admin in his settings. To solve this problem, we needed to access the settings of the admin and check for the required conditions. But since queryRecord() returned a promise we had to re-render the page for the effect to show which resulted in this code:

canAcceptPayPal: computed('data.event.paymentCurrency', function() {     this.get('store').queryRecord('setting', {}) .then(setting => { this.set('canAcceptPayPal', (setting.paypalSandboxUsername || setting.paypalLiveUsername) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal); this.rerender(); });

This code was setting a computed property inside it and then re-rendering which is bad programming and can result in weird bugs.

Solution

The above problem was solved by creating a service for settings. This made sense as settings would be required at other places as well. The file was called settings.js and was placed in the services folder. Let me walk you through its code.

  • Extend the default Service provided by Ember.js and initialize store, session, authManager and _lastPromise.
import Service, { inject as service } from '@ember/service';
import { observer } from '@ember/object';

export default Service.extend({

 store       : service(),
 session     : service(),
 authManager : service(),

_lastPromise: Promise.resolve(),
  • The main method which fetches results from the server is called _loadSettings(). It is an async method. It queries setting from the server and then iterates through every attribute of the setting model and stores the corresponding value from the fetched result.
/**
* Load the settings from the API and set the attributes as properties on the service
*
* @return {Promise<void>}
* @private
*/
async _loadSettings() {
 const settingsModel = await this.get('store').queryRecord('setting', {});
 this.get('store').modelFor('setting').eachAttribute(attributeName => {
   this.set(attributeName, settingsModel.get(attributeName));
 });
},
  • The initialization of the settings service is handled by initialize(). This method returns a promise.
/**
* Initialize the settings service
* @return {*|Promise<void>}
*/
initialize() {
 const promise = this._loadSettings();
 this.set('_lastPromise', promise);
 return promise;
}
  • _authenticationObserver observes for changes in authentication changes and reloads the settings as required.
/**
* Reload settings when the authentication state changes.
*/
_authenticationObserver: observer('session.isAuthenticated', function() {
 this.get('_lastPromise')
   .then(() => this.set('_lastPromise', this._loadSettings()))
   .catch(() => this.set('_lastPromise', this._loadSettings()));
}),

The service we created can be directly used in the app to fetch the settings for the user. To solve the Paypal and Stripe payment problem described above, we use it as follows:

canAcceptPayPal: computed('data.event.paymentCurrency', 'settings.paypalSandboxUsername', 'settings.paypalLiveUsername', function() {
 return (this.get('settings.paypalSandboxUsername') || this.get('settings.paypalLiveUsername')) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal;
}),

canAcceptStripe: computed('data.event.paymentCurrency', 'settings.stripeClientId', function() {
 return this.get('settings.stripeClientId') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).stripe;
}),

Thus, there is no need to re-render the page and dangerously set the property inside its computed method.

References

Continue ReadingOpen Event Frontend – Settings Service

Open Event Server – Export Speakers 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 speakers in a very detailed view in the event management dashboard. He can see the statuses of all the speakers. The possible statuses are pending, accepted and rejected. He/she can take actions such as editing/viewing speakers.

If the organizer wants to download the list of all the speakers 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 Speakers 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_speakers_csv which takes the speakers 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_speakers_csv(speakers):
   headers = ['Speaker Name', 'Speaker Email', 'Speaker Session(s)',
              'Speaker Mobile', 'Speaker Bio', 'Speaker Organisation', 'Speaker Position']
  • 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 speaker in speakers and form a row for that speaker by separating the values of each of the columns by a comma. Here, every row is one speaker.
  • As a speaker can contain multiple sessions we iterate over each session for that particular speaker and append each session to a string. ‘;’ is used as a delimiter. This string is then added to the row. We also include the state of the session – accepted, rejected, confirmed.
  • The newly formed row is added to the rows list.
for speaker in speakers:
   column = [speaker.name if speaker.name else '', speaker.email if speaker.email else '']
   if speaker.sessions:
       session_details = ''
       for session in speaker.sessions:
           if not session.deleted_at:
               session_details += session.title + ' (' + session.state + '); '
       column.append(session_details[:-2])
   else:
       column.append('')
   column.append(speaker.mobile if speaker.mobile else '')
   column.append(speaker.short_biography if speaker.short_biography else '')
   column.append(speaker.organisation if speaker.organisation else '')
   column.append(speaker.position if speaker.position else '')
   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.
with open(file_path, "w") as temp_file:
   writer = csv.writer(temp_file)
   from app.api.helpers.csv_jobs_util import export_speakers_csv
   content = export_speakers_csv(speakers)
   for row in content:
       writer.writerow(row)

Obtaining the Speakers CSV file:

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

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

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the speakers 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 ReadingOpen Event Server – Export Speakers as CSV File

Bottom Navigation Bar in Open Event Android

Bottom navigation bar provides an easy access and convenient way for users to navigate in between different sections of an App where the sections are of equal importance. We use bottom navigation move between different fragments such as events screen, favorites, tickets profile and so on. This blog post will guide you on how its implemented in Open Event Android.

To create a bottom navigation bar first create items for your menu. Menu items will be rendered as elements of the bottom navigation bar. Add icon and title to your navigation item here.

   <item
       android:id=“@+id/navigation_profile”
       android:icon=“@drawable/ic_baseline_account_circle_24px”
       android:title=“@string/profile” />

After we have written menu xml for our navigation view, we will have to create a view for it. The following snippet of code creates a BottomNavigationView type view element on the screen with different attributes as specified and menu file as specified by app:menu. Make sure you have android support design dependency set up before this.

<android.support.design.widget.BottomNavigationView
           android:id=“@+id/navigation”
           android:layout_width=“match_parent”
           android:layout_height=“wrap_content”
           android:layout_gravity=“bottom”
           android:background=“?android:attr/windowBackground”
           app:itemBackground=“@android:color/white”
           android:foreground=“?attr/selectableItemBackground”
           app:itemTextColor=“@color/colorPrimaryDark”
           app:menu=“@menu/navigation” />

No that our view will be created we need to wire menu items with different fragment and set up listener for item selected events.

Inside onCreate of MainActivity we attach a ItemSelectListener with the bottom navigation bar, navigation.setOnNavigationItemSelectedListener(listener) .

private val listener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
       val fragment: Fragment
       when (item.itemId) {
           R.id.navigation_profile -> {
               supportActionBar?.title = “Profile”
               fragment = ProfileFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
           R.id.navigation_favorite -> {
               supportActionBar?.title = “Likes”
               fragment = FavoriteFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
           R.id.navigation_tickets -> {
               supportActionBar?.title = “Tickets”
               fragment = OrdersUnderUserFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
       }
       false
   }

The above listener object listens for item selection in the bottom navigation bar. Whenever any item is selected lambda function is executed and item.itemId is matched with different menu item ids. On any successful match, an instance of the fragment corresponding to the selected item is loaded.

References

Continue ReadingBottom Navigation Bar in Open Event Android

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 ReadingOpen Event Server – Export Sessions as CSV File

Displaying Events In Open Event Android

The user can browse through different events happening nearby or all over the world quickly from the events fragment, the latter is accessible from the bottom navigation bar and provides the user with some details about the event. The user if interested can select the event by clicking on it and will be taken to event details fragment where all the necessary information about the event is given. This blog post will help in understanding how events are fetched and shown in Open Event AndroidThe implementation of displaying events can be divided into two parts

  • Fetching and storing events
  • Displaying events on UI using recycler view

Fetching and Storing Events

To fetch an event we need to make get the request to the Open Event Server’s list events endpoint.

   @GET(“events?include=event-topic”)
   fun getEvents(): Single<List<Event>>

We are using retrofit to make the network calls @GET specifies that it a get request and the URL inside the parentheses is the endpoint we are hitting. This method returns a Single<List<Event>>.

Given below is the service layer method which uses DAO method and the API call and returns a Flowable<List<Event>>.

fun getEvents(): Flowable<List<Event>> {
       val eventsFlowable = eventDao.getAllEvents()
       return eventsFlowable.switchMap {
           if (it.isNotEmpty())
               eventsFlowable
           else
               eventApi.getEvents()
                       .map {
                           eventDao.insertEvents(it)
                           }

                       .toFlowable()
                       .flatMap {
                           eventsFlowable
                       }
       }
   }

Firstly all the events from the database are fetched using the DAO method getAllEvents. If the list returned from the DAO method is not empty this function simply returns the events otherwise it will make the get request to server endpoint using Event API’s getEvents method and the response of this is mapped and stored into the database using the DAO method insertEvents this enables local caching of events and the same list of events are converted into flowable and returned by the function.

The events fragment calls the view model function which specifies where to observe (we are observing on main thread because it is involved with UI related changes) and what to subscribe on also handles the displaying of progress bar. Given following is the implementation of the same.

fun loadEvents() {
       compositeDisposable.add(eventService.getEvents()
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe({
                   progress.value = true
               }).doFinally({
                   progress.value = false
               }).subscribe({
                   events.value = it
               }, {
                   Timber.e(it, “Error fetching events”)
                   error.value = “Error fetching events”
               }))
   }

You would notice the following parts in the above snippet

  • doOnSubscribe – This part of code is executed whenever the source is subscribed
  • Subscribe – This part of the code will have the end result when subscribed and success
  • doFinally – This part of the code will be executed at the end  

Creating Recycler View

Recycler view is similar to ListView with some enhancements. When we are dealing with larger datasets our typical choice for displaying lists type UI is Recycler view because its views can be recycled and scrolled very efficiently. Every custom RecyclerView consists of three parts

  • Layout for individual list items
  • Recycler View adapter
  • View Holder that holds views for individual list items

The item UI for events in Open Event Android contains two FABs (favorite and share), an image view (cover image of the event), text view for the date, month, event name and description.

The Recycler Adapter is like a middleware that connects our Recycler view with the view holder. The events recycler adapter contains a list of events and other functions to manipulate the data of this events list.

 private val events = ArrayList<Event>()

Once you have created the recycler view, the last step would be to insert the list of events into the recycler adapter.

eventsViewModel.events.observe(this, Observer {
           it?.let {
               eventsRecyclerAdapter.addAll(it)
               eventsRecyclerAdapter.notifyDataSetChanged()
           }
           Timber.d(“Fetched events of size %s”, eventsRecyclerAdapter.itemCount)
       })

The above code snippet is the implementation of how we add events into our recycler adapter. The idea is set an observer on the events mutable live data and put the code to insert events into recycler inside the observer block. This block is executed whenever there is any change in live data. The addAll method of recyclerview takes the list of events obtained inside the observer scope and updates the recycler view’s ArrayList of events. The update takes care no duplicate events are created by clearing the ArrayList if any events are present and then inserting the list of events.

Resources

Continue ReadingDisplaying Events In Open Event Android

Saving Current State when Redirected to Login Fragment in Open Event Android

Some features in Open Event Android requires the user to be logged in, for example, buying tickets or viewing bought tickets. If the user selects to buy tickets for an event and he/she is not logged in, the user will be redirected to login fragment and on successful login, the user will return to the last state. This blog post will help you understand how its done in Open Event Android we will be taking the example of buying tickets without logged in.

To give you an overview once a user selects to buy tickets he is taken to a Tickets Fragment which contains different tickets and allows the user to pick their respective quantity. After selecting the right quantities the user can select on the register and he/she will be taken to Attendee Fragment which requires the user to be logged in, if not he will be redirected to login fragment and on successful login he can continue from attendee fragment without losing the selected tickets and their quantity information

To do this when redirecting from Attendee Fragment to the AuthActivity we modify the redirect method and send additional information with the intent such as even id and list of ticket id and quantity selected by the user. Following is how it can be done.

val intent = Intent(activity, AuthActivity::class.java)
       val bundle = Bundle()
       bundle.putLong(EVENT_ID, id.toLong())
       if (ticketIdAndQty != null)
           bundle.putSerializable(TICKET_ID_AND_QTY, ticketIdAndQty as ArrayList)
intent.putExtras(bundle)
 startActivity(intent)

Now that we have sent additional information with intent we will also have to modify the login fragment to use this information.

The above-discussed method will redirect the user to AuthActivity which handles all the authentication part. The user can either select to log in with an existing account or he may sign up with a new account. For existing accounts login fragment is shown and similarly for new sign up sign up fragment is shown.

The AuthActivity simply takes the bundle it received using intent.getExtras and passes the same to the fragment it is rendering. The load fragment function in Auth Activity looks like below. Whatever fragment is opened (signup/login) it passes the bundle that AuthActivity received as arguments to the fragments. This is done by setting fragment.arguments = bundle

private fun loadFragment(fragment: Fragment) {
       if (bundle != null)
           fragment.arguments = bundle
       supportFragmentManager.beginTransaction()
               .replace(R.id.frameContainerAuth, fragment)
               .commit()

Now we have all the bundle information in our login fragment next what we want is whenever login completes successfully send the user back to Attendee Fragment along with the event and ticket id and selected quantity information.

The following snippet of code takes the user to MainActivity whenever user information is received which means the user has successfully logged in

 loginActivityViewModel.user.observe(this, Observer {
           redirectToMain(bundle)
       })

redirectToMain accepts bundle as a parameter this bundle contains the ticket id and quantity list and the event id this is what we received from AuthActivity.

 private fun redirectToMain(bundle: Bundle?) {
       val intent = Intent(activity, MainActivity::class.java)
       if (((bundle != null) && !id.equals(-1)) && (ticketIdAndQty != null)) {
           intent.putExtra(LAUNCH_ATTENDEE, true)
           intent.putExtras(bundle)
       }
       startActivity(intent)
       activity?.overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
       activity?.finish()
   }

The above code snippet shows the redirectToMain function in Login Fragment, this method opens the Main Activity and passes the bundle as a parameter to it along with a trigger LAUNCH_ATTENDEE set. This trigger is responsible for telling MainAcitivty that the login fragment was actually a redirect from attendee fragment and MainActivity uses the bundle information to recreate the Attendee Fragment screen.

The following code handles this part in Main Activity.

if (bundle != null && bundle.getBoolean(LAUNCH_ATTENDEE)) {
           val fragment = AttendeeFragment()
           fragment.arguments = bundle
           loadFragment(fragment)
           openEventsFragment = false
       }

If the bundle information is available and the Launch attendee trigger is set AttendeeFragment is created with arguments as the bundle (fragment.arguments = bundle).

Resources

Continue ReadingSaving Current State when Redirected to Login Fragment in Open Event Android

Generating Order QR codes in Open Event Android

QR codes provide a very fast and convenient way to share information to other device, one device generates a bitmap image with information encoded in it, this bitmap can be easily scanned from the other device. QR codes are used in Open Event Android to share order identifier information, the latter can be used to check in attendee in Open Event Orga App. This blog post will guide you on how its implemented in Open Event Android.

Add the dependency

We use Zixing’s Barcode scanning library in Open Event Android for generating QR bitmaps. Add the following dependency in your app level gradle file. Please visit zxing GitHub page for latest version information.

implementation ‘com.journeyapps:zxing-android-embedded:3.4.0’

Create View for QR code

To display QR code we need to create and bitmap and then populate it on an Image View. The following code snippet generated an Image View on the screen, we have set the scale type to center crop so that image is scaled uniformly and the dimension of the image is equal to the corresponding dimension of the view.

    <ImageView
               android:layout_width=“200dp”
               android:layout_gravity=“center”
               android:layout_height=“200dp”
               android:id=“@+id/qrCodeView”
               android:layout_margin=“@dimen/layout_margin_medium”
               android:scaleType=“centerCrop” />

Generating the QR Bitmap

The following code snippet generates QR bitmap for order identifier.

try {
           val bitMatrix = MultiFormatWriter().encode(orderIdentifier, BarcodeFormat.QR_CODE, 200, 200)
           itemView.qrCodeView.setImageBitmap(BarcodeEncoder().createBitmap(bitMatrix))
       } catch (e: WriterException) {
           e.printStackTrace()
       }

MultiformatWriter encodes the provided string in the format specified in the second parameter of encode function and generates a bitMatrix of specified dimensions in our case orderIdentifier is the string to be encoded and the format is QR_CODE. The last two parameters in the encode method are the width and height respectively for the Image Matrix to be generated. This bitMatrix can be then used to create QR bitmaps, BarcodeEncode() function creates a bitmap from this bitMatrix which is then set on the image View using setImageBitmap method. As a writer exception may occur while encoding string to specified format this whole block of code is put inside a try catch block which catches and logs the WriterException.

References

Continue ReadingGenerating Order QR codes in Open Event Android

Creating Orders in Open Event Android

An Order is generated whenever a user buys a ticket in Open Event Android. It contains all the details regarding the tickets and their quantity, also information regarding payment method and relation to list of attendees, through this blog post we will see how Orders are generated in Open Event Android. Implementing Order system can be divided into following parts

  • Writing model class to serialize/deserialize API responses
  • Creating TypeConverter for Object used in Model class
  • Creating the API interface method
  • Wiring everything together

Model Class

Model class server two purpose –

  • Entity class for storing orders in room
  • Serialize / Deserialize API response

The architecture of the Order Model Class depends upon the response returned by the API, different fields inside the Entity Class defines what different attributes an Order consists of and their data types. Since every Order has a relationship with Event and Attendee we also have to define foreign key relations with them. Given below is the implementation of the Order Class in Open Event Android.

@Type(“order”)
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
@Entity(foreignKeys = [(ForeignKey(entity = Event::class, parentColumns = [“id”], childColumns = [“event”], onDelete = ForeignKey.CASCADE)), (ForeignKey(entity = Attendee::class, parentColumns = [“id”], childColumns = [“attendees”], onDelete = ForeignKey.CASCADE))])
data class Order(
       @Id(IntegerIdHandler::class)
       @PrimaryKey
       @NonNull
       val id: Long,
       val paymentMode: String? = null,
       val country: String? = null,
       val status: String? = null,
       val amount: Float? = null,
       val orderNotes: String? = null,
       @ColumnInfo(index = true)
       @Relationship(“event”)
       var event: EventId? = null,
       @Relationship(“attendees”)
       var attendees: List<AttendeeId>? = null
)

 

We are using Jackson for serializing/deserializing JSON response, @Type(“order”) annotation tells jackson that the following object is for key order in json response. Since we are using this as our room entity class we will also have to add a @Entity annotation to this class. Order contains attendee and event id fields which are foreign keys to other entity classes, this also has to be explicitly mentioned while writing the @Entitty annotation as shown in the snippet above . All relationship must be annotated with @Relationship annotation. All the variables serves as attributes in the order table and key name for json conversions.

The fields of this class are the attributes for the Order Table. Payment mode, country, status are all made up of primitive data type and hence require no type convertors whereas we will have to specify type converter for objects like eventId and List<Attendees>

Type Converter

Type Converter allows us to store any custom object type inside room database. Essentially we break down the Object into smaller primitive data types that Object is composed of and which can be stored by room database.

To create a TypeConverter we have to add a @TypeConverter annotation to it, this tells room that this is a special function. For every custom Object, you have to create two different TypeConverter functions. One takes the Object and converts it into primitive data type and the other takes the primitive data type and constructs your custom Object from it. For the Order data class we discussed in the above section we will need two type converters, for EventId and List<Attendee> objects. We will take the example of List<Attendee>

class ListAttendeeIdConverter {
   @TypeConverter
   fun fromListAttendeeId(attendeeIdList: List<AttendeeId>): String {
       val objectMapper = ObjectMapper()
       return objectMapper.writeValueAsString(attendeeIdList)
   }
   @TypeConverter
   fun toListAttendeeId(attendeeList: String): List<AttendeeId> {
       val objectMapper = ObjectMapper()
       val mapType = object : TypeReference<List<AttendeeId>>() {}
       return objectMapper.readValue(attendeeList, mapType)
   }

A type converter shows how we can store an object in the form of primitive data type by performing some operation on it. Here we can see that List<AttendeeId> Object is converted into String (primitive data type) using jackson object mapper and similarly we will have to restore or recreate the List<AttendeeId> Object from the string converted output of the same. The first function fromListAttendeeId deals with converting Object into string type and toListAttendeeId deals with converting string output to List<AttendeeId> type Object.

Not that we have created a TypeConverter for our custom Object type we have to add it to the Open Event Database. This can be done by simply adding it to @TypeConverters list separated by commas as shown below.

@TypeConverters(EventIdConverter::class, EventTopicIdConverter::class, TicketIdConverter::class, AttendeeIdConverter::class, ListAttendeeIdConverter::class)

API Interface Method

Till now we have seen how Order body looks like and how we can store it in room database but we would also need an Order API class which specifies which endpoint to hit and what body and response type it is expecting.

Given below is the placeOrder function which hits the Order endpoint (baseURL/orders), with the body as an Order and returns a Single<Order> as a response. Since we are using retrofit for making network requests endpoint path is simply added inside @Path annotation and the body can be passed in parameters of the function using @Body annotation.

interface OrderApi {
   @POST(“orders”)
   fun placeOrder(@Body order: Order): Single<Order>
}

OrderService is the class which exposes the OrderApi functions and make it available to its ViewModel.

The placeOrder function inside the service class takes Order as body parameter, when this function is called it makes a call to the API function to place an order with the passed parameter the response of which is inserted into the database (caching of Orders) also returns the same value

class OrderService(private val orderApi: OrderApi, private val orderDao: OrderDao) {
   fun placeOrder(order: Order): Single<Order> {
       return orderApi.placeOrder(order)
               .map {
                   orderDao.insertOrder(it)
                   it
               }
   }

To create an Order from the Fragment or Activity one call implement the following function createOrder.

createOrder calls the service layer function placeOrder and subscribes to this observable. Here we are observing on main thread because all the UI related changes has to be done from the main thread. As soon as we subscribe to the function we also sets progress.value = true this allows us to show a progress bar on the UI this is changed to false once the response is received (see doFinally).

You can find the following function in the AttendeeViewModel class in Open Event Android project

fun createOrder(order: Order) {
       compositeDisposable.add(orderService.placeOrder(order)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe {
                   progress.value = true
               }.doFinally {
                   progress.value = false
               }.subscribe({
                   message.value = “Order created successfully!”
                   Timber.d(“Success placing order!”)
               }, {
                   message.value = “Unable to create Order!”
                   Timber.d(it, “Failed creating Order”)
               }))
   }

Whenever user fills in details of all the attendees sequence of calls and methods are invoked. Firstly attendees are created for the event with ticket details and the data as provided from the UI. On the successful generation of all the attendees ie when total ticket quantity equals the no of attendees an order object is generated with attendees as the list of attendee previously generated and other details as required, this is then passed to createOrder function which internally interacts with the service layer function to create Order.

Resources

Continue ReadingCreating Orders in Open Event Android

Adding Multiple Attendees inside an Order in Open Event Android

Orders endpoint allows multiple attendees to be passed in the body of post request which essentially means that and an order may contain tickets for different attendees.

This blog post will guide you on how multiple attendees under an Order is handled and implemented in Open Event Android.

An attendee as the name implies gives us information about how many people are attending any particular event. Let’s say if a User selects two tickets with 3 and 7 quantity total 10 attendees will be generated and can be put inside one Order request.

Whenever multiple tickets are selected by the user following function is called to create Attendee

fun createAttendees(attendees: List<Attendee>, country: String?, paymentOption: String) {
       this.country = country
       this.paymentOption = paymentOption
       this.attendees?.clear()
       attendees.forEach {
           createAttendee(it, attendees.size)
       }
   }

 

Create attendees accepts a list of attendees and calls the createAttendees for every attendee in the list. Actual attendee creation is handled in createAttendee. The above function also set up the country and payment option as provided by the user.

fun createAttendee(attendee: Attendee, totalAttendee: Int) {        compositeDisposable.add(attendeeService.postAttendee(attendee)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe {
                   progress.value = true
               }.doFinally {
                   progress.value = false
               }.subscribe({
                   attendees?.add(it)
                   if (attendees != null && attendees?.size == totalAttendee) {
                       loadTicketsAndCreateOrder()
                   }
                   message.value = “Attendee created successfully!”
                   Timber.d(“Success! %s”, attendees?.toList().toString())
               }, {
                   message.value = “Unable to create Attendee!”
                   Timber.d(it, “Failed”)
               }))
   }

Create an Attendee on the other hand deals with creating individual attendees on the server. It calls the service function attendeeService.postAttendee with attendee as the parameter if the post request made is succeeds the attendee returned from the API is added to the list of attendees, when the size of list of attendees become equal to the list of attendees that had to be generated loadTicketsAndCreateOrder is called which takes care of getting charges for user and creating an order. If the post request for the attendee is failed a message is displayed and a log is added to console with error details.

fun loadTicketsAndCreateOrder() {
       if (this.tickets.value == null) {
           this.tickets.value = ArrayList()
       }
       this.tickets.value?.clear()
       attendees?.forEach {
           loadTicket(it.ticket?.id)
       }
   }

Load tickets and create order also works in a similar way as of creating attendees as there are two functions in here also one of which takes a list of attendees and calls the other function which gets data for individual ticket items passes as the parameter. Here ticket details for every attendee are fetched using the loadTicket function which takes ticket id corresponding to an attendee (using it.tickets?.id) and returns the Ticket object. Given below is the implementation for loadTicket method.

fun loadTicket(ticketId: Long?) {        compositeDisposable.add(ticketService.getTicketDetails(ticketId)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                   tickets.value?.add(it)
                   Timber.d(“Loaded tickets! %s”, tickets.value?.toList().toString())
                   if (tickets.value?.size == attendees?.size) {
                       createOrder()
                   }
               }, {
                   Timber.d(it, “Error loading Ticket!”)
               }))
   }

Load ticket uses the service method to fetch the ticket details using the passed ticket id and the returned ticket object is added to the list of ticket. When the size of the ticket becomes equal to the size of total no of attendees which means that ticket details of each attendee are fetched createOrder is called which is responsible for creating order. If anything fails in fetching tickets log is statement is printed along with error information.

fun createOrder() {

val order = Order(getId(), paymentMode, country, “pending”, amount, attendees = attendeeList, event = EventId(eventId))


           compositeDisposable.add(orderService.placeOrder(order)
                   .subscribeOn(Schedulers.io())
                   .observeOn(AndroidSchedulers.mainThread())
                   .doOnSubscribe {
                       progress.value = true
                   }.doFinally {
                       progress.value = false
                   }.subscribe({
                       orderIdentifier = it.identifier.toString()
                       message.value = “Order created successfully!”
                       Timber.d(“Success placing order!”)
                       if (it.paymentMode == “free”)
                           paymentCompleted.value = true
                   }, {
                       message.value = “Unable to create Order!”
                       Timber.d(it, “Failed creating Order”)
                   }))
       } else {
           message.value = “Unable to create Order!”
       }
   }

Create order function is the final step in the process it takes the list of attendees, payment modes, amount and other information related to order and create an order object out of it which is then passed to the service layer method which makes a post request to generate an order on the server. In case of an error or success whole message is logged and toast is displayed. This method is also involved with showing progress bar once this is called and closing the same when the procedure is completed.

Finally, in case of free events the user is taken to order completed screen and in case of paid events the user is taken to the payment screen where the user has to provide payment related information

Resources

Continue ReadingAdding Multiple Attendees inside an Order in Open Event Android