Implementing ProGuard in Open Event Orga App

In the Open Event Orga App there has been an issue with the size of the app. So to decrease the size of the app, we had to enable Proguard. By implementing proguard the size of the app has reduced from 5.9 Mb to 2.9 Mb. The following article shows the steps taken to implement Proguard.

  • Firstly the following parameters need to set to true in the the app level build.gradle file.
  release {
      minifyEnabled true
      shrinkResources true
      proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’

The built-in shrinker in minify enabled removes the dead code. It doesn’t obfuscate or optimizes the code. In this section of code, we also set the proguard-rules.pro file in which all the rules regarding code obfuscation are set. The file is a custom file unlike the default proguard-android.txt file which is already a part of Android Studio.

  • Now we head over to the proguard-rules.pro file. But first we need to understand the 2 most important terms –dontwarn and –keep class.

 

2.1 -dontwarn → When proguard is implemented, code obfuscation takes place and the name of the classes get shortened to single letters. Because of this there might be a few conflicts. But there might be some unresolved references in the obfuscated code. dontwarn ignores all these references.

2.2 –keep class → Suppose there are certain classes which shouldn’t be obfuscated then they are preceded with keep class annotation.

 

  • In the proguard-rules.pro file the following lines are added
# Platform calls Class.forName on types which do not exist on Android to determine platform.
dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
keepattributes Exceptions

The keepattributes annotation is basically used for Generics which are JAVA JDK 5 and higher. It basically prevents the code from obfuscation.

  • Now Retrofit classes also need to be obfuscated. For that we add the following lines of code. The obfuscation considerably reduces the size of the app.
-dontwarn okio.**

-dontwarn com.squareup.okhttp.**
  • The model classes need to be prevented from getting obfuscated or else it may lead to app crash. Hence these classes need to be kept and so we add the following lines of code to prevent app crash. The link for the PR which fixed the app crash cause due to buggy proguard rule.
keep class org.fossasia.openevent.app.data.** {
*;
}
  • The orga app uses the Jackson library for parsing JSON data. Proguard rules also needs to be applied for this library.
# Jackson
keepattributes *Annotation*,EnclosingMethod,Signature
keepnames class com.fasterxml.jackson.** { *; }
dontwarn com.fasterxml.jackson.databind.**
keep class org.codehaus.** { *; }
-keepclassmembers public final enum org.codehaus.jackson.annotate.JsonAutoDetect$Visibility {
  public static final org.codehaus.jackson.annotate.JsonAutoDetect$Visibility *;
}
keep class com.github.jasminb.** { *; }
  • The other libraries used in the app also have specific proguard rules. These are listed below.
# General
keepattributes SourceFile,LineNumberTable,*Annotation*,EnclosingMethod,Signature,Exceptions,InnerClasses
keep class android.support.v7.widget.SearchView { *; }

keep class com.github.mikephil.charting.** { *; }
G
keep public class * extends com.bumptech.glide.module.AppGlideModule
keep class com.bumptech.glide.GeneratedAppGlideModuleImpl

 

References:

  1. Medium article by Jon Finerty https://medium.com/@jonfinerty/beginner-to-proguard-b3327ff3a831

Official Developer documentation  https://developer.android.com/studio/build/shrink-code

Continue ReadingImplementing ProGuard in Open Event Orga App

Using Android Architecture Components in Organizer App

In the Open Event Organizer Android App there is an issue with the Memory leaks, Data persistence on configuration changes and difficulty in managing the Activity lifecycle. So as to deal with these issues we have implemented Android Architecture Components.

The first step towards moving on with AAC’s was the conversion of a presenter class to a ViewModel class and then implementing LiveData in the refactored class.

LiveData

It is an Observable data holder. It notifies the observers whenever there is change in the data so that the UI can be updated.

Livedata is also bound to the lifecycle which means that it will be observing changes only when the activity is in started or resumed state and hence there is no chance of memory leaks or null pointer exceptions.

ViewModel

The ViewModel class is designed to hold and manage UI-related data in a life-cycle conscious way. This allows data to survive configuration changes such as screen rotations.

In the following I’ll be explaining how the LoginViewModel class was made in the Orga App.

Steps

  • Creating one’s own custom ViewModelFactory. This is done so as to follow the Single Responsibility Principle. This custom class extends the ViewModelProvider.Factory and handles the creation of View Models. Adding this class also ensures that a Constructor can also get injected in the View Model class.
@Singleton
public class OrgaViewModelFactory implements ViewModelProvider.Factory {

  private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;

  @Inject
  public OrgaViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
      this.creators = creators;
  }

  @NonNull
  @Override
  @SuppressWarnings({“unchecked”, “PMD.AvoidThrowingRawExceptionTypes”, “PMD.AvoidCatchingGenericException”})
  public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
      Provider<? extends ViewModel> creator = creators.get(modelClass);
      if (creator == null) {
          for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
              if (modelClass.isAssignableFrom(entry.getKey())) {
                  creator = entry.getValue();
                  break;
              }
          }
      }
      if (creator == null) {
          throw new IllegalArgumentException(“unknown model class “ + modelClass);
      }
      try {
          return (T) creator.get();
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
  }
}
  • Injecting this custom ViewModelFactory into the ViewModelModule. Adding the method bindLoginViewModel with the LoginViewModel as its parameter. Always add any new ViewModel class into the ViewModelModule otherwise it might show DaggerAppComponent errors.
@Module
public abstract class ViewModelModule {

  @Binds
  @IntoMap
  @ViewModelKey(LoginViewModel.class)
  public abstract ViewModel bindLoginViewModel(LoginViewModel loginViewModel);

  @Binds
  public abstract ViewModelProvider.Factory bindViewModelFactory(OrgaViewModelFactory factory);

}
  1. Refactoring the LoginPresenter to LoginViewModel class and extending it to ViewModel.
  2. In the fragment class injecting the ViewModelProviderFactory.
@Inject
ViewModelProvider.Factory viewModelFactory;
  • Pass this parameter in the ViewModelProviders.of( ) method as follows:
loginFragmentViewModel = ViewModelProviders.of(this, viewModelFactory).get(LoginViewModel.class);
  • Now the only task in hand is the use of LiveData in the ViewModels and observing the LiveData from the Fragments. In the following LiveData has been applied to observe the state of Progress Bar. When the login button is pressed, the value of MutableLiveData<Boolean> progress is set to true.
public void login() {
  compositeDisposable.add(loginModel.login(login)
      .doOnSubscribe(disposable -> progress.setValue(true))
      .doFinally(() -> progress.setValue(false))
      .subscribe(() -> isLoggedIn.setValue(true),
          throwable -> error.setValue(ErrorUtils.getMessage(throwable))));
}

 

  • This change in state is observed by the following code in the fragment class:
loginFragmentViewModel.getProgress().observe(this, this::showProgress);

On observing this, the showProgress is called which handles the visibility if the progress bar. Currently as the progress value was set to True, the progress bar is visible till the processing goes on.

  • Once the login takes place the progress of the LoginViewModel is set to false and the progress bar gets hidden, again which gets observed in the fragment class.

References:

https://android.jlelse.eu/android-architecture-components-livedata-1ce4ab3c0466

Continue ReadingUsing Android Architecture Components in Organizer App

Adding Open Event Orga App as a Product Flavor on FDroid

To release the orga app on fdroid, product flavors need to be added to the app. This means that 2 different packages need to be maintained that handle the code for fdroid and playstore separately. For eg. There are certain proprietary software that are used in the app such as Google Play services which won’t get accepted on fdroid. Hence alternatives need to be found out and separate code has to be maintained.  

Steps:

  • Go to the build.gradle file where in the Android section add the following line of code in the groovy language. We specify the flavorDimension to be default and then add the productFlavor tag. Inside this tag there will be 2 more sub tags namely fdroid and playStore. These are the 2 different flavors we will be maintaining in the app. Because of this command, 4 different build variants will be created namely : fdroidDebug, fdroidRelease, playStoreDebug and playStoreRelease. The dimension for each is specified to be default.
flavorDimensions “default”

productFlavors {
  fdroid {
      dimension “default”
  }

  playStore {
      dimension “default”
  }
}
  • Now as testCoverage is being used in the app the file name also need to be changed from debug to playStore. So firstly go to the travis.yml file and change the testDebugUnitCoverage command to testPlayStoreDebugUnitCoverage. Hence when the travis will build, the modified command will be executed rather than the old command. We could also have used testfdroidDebugUnitCoverage.Following is the code snippet from the travis.yml file.
script:
./gradlew build
./gradlew testPlayStoreDebugUnitTestCoverage
  • Now command for testCoverage also needs to be changed in the config.yml. So navigate to this file and modify the command.
– run:
  name: Test Coverage
  command: ./gradlew testPlayStoreDebugUnitTestCoverage
  • Now navigate to the update-apk.sh script where we will change the name of the output apk in case of fdroid build and playStore build. As can be seen below we have first changed the name of the json file in each build. Also the name of the folder where they get generated are changed.
\cp -r ../app/build/outputs/apk/playStore/*/**.apk .
\cp -r ../app/build/outputs/apk/fdroid/*/**.apk .
\cp -r ../app/build/outputs/apk/playStore/debug/output.json playStore-debug-output.json
\cp -r ../app/build/outputs/apk/playStore/release/output.json playStore-release-output.json
\cp -r ../app/build/outputs/apk/fdroid/debug/output.json fdroid-debug-output.json
\cp -r ../app/build/outputs/apk/fdroid/release/output.json fdroid-release-output.json
  • Also the apk names in fastlane  are changed for playStore with the help of the following code.
gem install fastlane
fastlane supply –apk app-playStore-release.apk –track alpha –json_key ../scripts/fastlane.json –package_name $PACKAGE_NAME

Resources

  1. Official fdroid website https://f-droid.org/en/
  2. Google Developer website https://developer.android.com/studio/build/build-variants
Continue ReadingAdding Open Event Orga App as a Product Flavor on FDroid

Implement Tests for Feedback List in Open Event Orga App

In the Open Event Orga App test have been written for all the presenters and viewmodel classes to ensure that the implemented functionalities work well.

In the following blog post I have discussed one particular test which I implemented which is the FeedbackList Presenter Test.

Implementation

  1. Instantiation of the variables.
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock
public FeedbackListView feedbackListView;
@Mock
public FeedbackRepository feedbackRepository;

We should first know the meaning of the Annotations being used:

@Rule : It tells mockito to create the mocks based on the @Mock annotation. This annotation always needs to be used.

@Mock: It tells Mockito to mock the FeedbackListView interface and FeedbackRepository class.

Here we are mocking 3 classes namely: MockitoRule, FeedbackListView, FeedbackRepository.

Before moving forward we first need to understand the meaning of Mock. A mock object is a dummy implementation for an interface or a class in which you define the output of certain method calls. Mock objects are configured to perform a certain behavior during a test. They typically record the interaction with the system and tests can validate that.

 

private static final List<Feedback> FEEDBACKS = Arrays.asList(
  Feedback.builder().id(2L).comment(“Amazing!”).build(),
  Feedback.builder().id(3L).comment(“Awesome!”).build(),
  Feedback.builder().id(4L).comment(“Poor!”).build()
);

The list of feedbacks is populated with demo values which can be used for testing purpose later.

2) The @Before annotation is applied before the set up. Before any tests are created, the setUp( ) is executed. A feedbackListPresenter object is created and the required parameters are passed. The RxJava Plugin’s setIoSchedulerHandler, setComputationSchedulerHandler and setInitmainThreadSchedulerHandler use the Scheduler.Trampoline( ) .  It lets the internal call Observable call to end before asserting the result.

setIOSchedulerHandler( ) -> It basically is a type of Scheduler which handles the Input and Output of the RxJava code.

setComputationSchedulerHandler( ) -> It is another Scheduler which handles the computations which are carried out during call to RxJava methods.

setInitMainThreadSchedulerHandler( ) -> It is called to notify the Scheduler that the IO operations would be carried out on the main thread.

@Before
public void setUp() {
  feedbackListPresenter = new FeedbackListPresenter(feedbackRepository);
  feedbackListPresenter.attach(ID, feedbackListView);

  RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
  RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
  RxAndroidPlugins.setInitMainThreadSchedulerHandler(schedulerCallable -> Schedulers.trampoline());
}

Some of the tests are discussed below:

→  The following test is written to ensure that the feedback list gets updated automatically after a feedback is received.

@Test
public void shouldLoadFeedbackListAutomatically() {
  when(feedbackRepository.getFeedbacks(anyLong(), anyBoolean())).thenReturn(Observable.fromIterable(FEEDBACKS));

  feedbackListPresenter.start();

  verify(feedbackRepository).getFeedbacks(ID, false);
}

As can be seen above , I have used the when and return functionality of Mockito. It is basically used to check the return type of the object. So when the required parameters are passed in the getFeedback( ) , then the return type of what is expected is mentioned in the thenReturn( ).

verify ensures that the getFeedback( ) is called on the feedbackfeedbackRepository mock only.

→ The following test is written to ensure that there is an error message on loading data after swipe refresh is made. Firstly the list of feedbacks is fetched from the feedbackRepository with the help of getFeedbacks( ) where the parameters event id and the boolean variable true are passed. Then the thenReturn( ) has the statement Observable.error(Logger.TEST_ERROR) which is actually written to specify the expected result we want i.e in this case we are expecting the TEST_ERROR statement as a response and hence it is written before.

At the end it is verified using the statement verify statement where the feedbackListView is passed and the error is captured.

 

@Test
public void shouldShowErrorMessageOnSwipeRefreshError() {
  when(feedbackRepository.getFeedbacks(ID, true)).thenReturn(Observable.error(Logger.TEST_ERROR));

  feedbackListPresenter.loadFeedbacks(true);

  verify(feedbackListView).showError(Logger.TEST_ERROR.getMessage());
}

3) After the tests have been applied, the RxJava plugins are reset.

@After
public void tearDown() {
  RxJavaPlugins.reset();
  RxAndroidPlugins.reset();

}

Resources:

→ Mockito tutorial :

http://www.vogella.com/tutorials/Mockito/article.html

→ Testing in RxJava:

https://stackoverflow.com/questions/40233956/how-to-use-schedulers-trampoline-inrxjava

Continue ReadingImplement Tests for Feedback List in Open Event Orga App

Adding Support for Playing Audio in SUSI iOS App

SUSI.AI supports various actions like the answer, map, table, video play and many more. You can play youtube videos in the chat screen. It also supports for playing audio in the chat screen. In this post, we will see that how playing audio feature implemented in SUSI iOS.

Getting audio action from server side –

In the chat screen, when we ask SUSI to play audio, we get the audio source from the server side. For example, if we ask SUSI “open the pod bay door”, we get the following action object:

"actions": [
{
"type": "audio_play",
"identifier_type": "youtube",
"identifier": "7qnd-hdmgfk"
},
{
"language": "en",
"type": "answer",
"expression": "I'm sorry, Dave. I'm afraid I can't do that."
}
]

In the above action object, we can see that we get two actions, audio_play and answer. In audio_play action, we are getting an identifier type which tells us about the source of audio. Identifier type can be youtube or local or any other source. When the identifier is youtube, we play audio from youtube stream. In identifier, we get the audio file path. In case of youtube identifier type, we get youtube video ID and play from youtube stream. In answer action type, we get the expression which we display in chat screen after thumbnail.

Implementing Audio Support in App –

We use Google’s youtube Iframe API to stream audio from youtube videos. We have a VideoPlayerView that handle all the iFrame API methods and player events with help of YTPlayer HTML file.

Presenting the YouTubePlayerCell –

If the action type is audio_play, we are presenting the cell in chat screen using cellForItemAt method of UICollectionView.

if message.actionType == ActionType.audio_play.rawValue {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ControllerConstants.youtubePlayerCell, for: indexPath) as? YouTubePlayerCell {
cell.message = message
cell.delegate = self
return cell
}
}

Setting size for cell –

Using sizeForItemAt method of UICollectionView to set the size.

if message.actionType == ActionType.audio_play.rawValue {
return CGSize(width: view.frame.width, height: 158)
}

In YouTubePlayerCell, we fetch thumbnail and display in the cell with a play button. On clicking the play button, we open the player and stream music.

Final Output –

Resources –

  1. Apple’s Documentations on sizeForItemAt
  2. SUSI API Sample for Audio Play Action
  3. YouTube iFrame API for iOS
  4. Apple’s Documentations on cellForItemAt
Continue ReadingAdding Support for Playing Audio in SUSI iOS App

Selecting Multiple Tickets in Open Event Android

Open Event Android allows the user to select multiple tickets for an event this blog post will guide you on how its done. Ticket Id and quantity of ticket selected is stored in the form of List of pair. The first element of the List is the Id of the ticket and the second element is its quantity.

private var tickeIdAndQty = ArrayList<Pair<Int, Int>>()

Whenever the user selects any ticket using the dropdown the following snippet of code is executed. handleTicketSelect takes id and quantity of the selected ticket and search if any pair with passed id as the first element exists in the ticketIdAndQty list, if exists it updates that record else it inserts a new pair with given id and quantity into the list.

private fun handleTicketSelect(id: Int, quantity: Int) {
       val pos = ticketIdAndQty.map { it.first }.indexOf(id)
       if (pos == –1) {
           ticketIdAndQty.add(Pair(id, quantity))
       } else {
           ticketIdAndQty[pos] = Pair(id, quantity)
       }

When the user selects register the ticketIdAndQty list which contains details of selected tickets is added to the bundle along with the event id and are passed to attendee fragment. Attendee fragment allows the user to fill in other details such as country, payment option, first name etc.

Later attendee fragment with the help of attendee view model creates individual attendee on the server

rootView.register.setOnClickListener {
           if (!ticketsViewModel.totalTicketsEmpty(ticketIdAndQty)) {
               val fragment = AttendeeFragment()
               val bundle = Bundle()
               bundle.putLong(EVENT_ID, id)
               bundle.putSerializable(TICKET_ID_AND_QTY, ticketIdAndQty)
               fragment.arguments = bundle
               activity?.supportFragmentManager
                       ?.beginTransaction()
                       ?.replace(R.id.rootLayout, fragment)
                       ?.addToBackStack(null)
                       ?.commit()
           } else {
               handleNoTicketsSelected()
           }
       }

When the register is selected an attendee is created for every ticket id inside ticketIdandQty list wherever the second element of the pair that is quantity is greater than zero. FFirst name last name, email and other information is taken from the text views populated on the screen and attendee object is created using these data. Lastly, createAttendee view model function is called passing the generated attendee, country, and selected payment option. The former calls service layer function and created an attendee by making a post request to the server.

 ticketIdAndQty?.forEach {
                   if (it.second > 0) {
                       val attendee = Attendee(id = attendeeFragmentViewModel.getId(),
                               firstname = firstName.text.toString(),
                               lastname = lastName.text.toString(),
                               email = email.text.toString(),
                               ticket = TicketId(it.first.toLong()),
                               event = eventId)
                       val country = if (country.text.isEmpty()) country.text.toString() else null
                       attendeeFragmentViewModel.createAttendee(attendee, id, country, selectedPaymentOption)
                   }
               }

The above-discussed procedure creates one attendee for every ticket quantity which means if a user select ticket id 1 with 3 quantity and ticket id 2 with 5 as quantity total 8 attendees will be generated. Later after successfully generating the attendee’s order is created. Every order has an array of attendees along with other other related information.

Resources

Continue ReadingSelecting Multiple Tickets in Open Event Android

Updating User information in Open Event Android

A user can update its information such as first name, last name from the Edit Profile Fragment in Open Event Android. Edit Profile Fragment can be accessed from the menu inside the Profile page. On opening Edit Profile Fragment user can interact with the simple UI to update his/her information. This blog post will guide you on how its implemented in Open Event Android.

To update a User we send a patch request to Open Event Server. The patch request contains the Updated User as body and auth token as header and it returns the updated user in response. Following it what the interface method looks like

@PATCH(“users/{id}”)
fun updateUser(@Body user: User, @Path(“id”) id: Long): Single<User>

This method is exposed to the View Model using a service layer function which calls the above function and also inserts the returned user in the database.

fun updateUser(user: User, id: Long): Single<User> {
       return authApi.updateUser(user, id).map {
           userDao.insertUser(it)
           it
       }
   }

On using map on Single<User> returned by updateUser we can access the user inside the scope which is then inserted into the database using the DAO method insert user and the same user object is returned by the function.

This service layer method is then used in updateUser method of EditProfileViewModel class which specifies how it is subscribed and on which thread observer should be set etc. The Edit Profile Fragment fragment calls this method whenever the user clicks on the Update button.

fun updateUser(firstName: String, lastName: String) {
       val id = authHolder.getId()
       if (firstName.isEmpty() || lastName.isEmpty()) {
           message.value = “Please provide first name and last name!”
           return
       }
       compositeDisposable.add(authService.updateUser(User(id = id,          firstName = firstName, lastName = lastName), id)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe {
                   progress.value = true
               }
               .doFinally {
                   progress.value = false
               }
               .subscribe({
                   message.value = “User updated successfully!”
                   Timber.d(“User updated”)
               }) {
                   message.value = “Error updating user!”
                   Timber.e(it, “Error updating user!”)
               })
   }

UpdateUser takes two parameters first name and last name if any of these parameters is empty the function returns with an error message which is displayed on the UI else service layer update user function is called with argument a User object with first name and last name as provided to view model function and an id which is accessed using authHolder’s getId method. Whenever this is subscribed we set progress.value true which displays spinner on the UI this is set false after the operation is complete. If the patch request results in success then toast message is shown on screen and a success message is logged similar to this in case of error, an error toast is displayed and an error is logged.

This goes for the logic to update user we also need UI and menu item which launches this fragment.

Inside Menu.xml add the following snippet of code

<item
  android:id=“@+id/edit_profile”
  android:title=“@string/edit_profile” />

This will create a menu item inside the ProfileFragment. The next step is to wire this logic which tells what happens when the user selects this menu item. The following code wires it to EditProfileFragment.

R.id.edit_profile -> {
               val fragment = EditProfileFragment()              activity?.supportFragmentManager?.beginTransaction()?.replace(R.id.frameContainer, fragment)?.addToBackStack(null)?.commit()
               return true

Resources

Continue ReadingUpdating User information in Open Event Android

Attendee Form Builder in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create tickets for his or her event. Other uses can buy these tickets in order to attend the event. When buying a ticket we ask for certain information from the buyer. Ideally the event organizer should get to choose what information they want to ask from the buyer. This blog post goes over the implementation of the attendee form builder in the Open Event Frontend.

Information to Collect

The event organizer can choose what details to ask from the order buyer. In order to specify the choices, we present a table with the entries of allowed fields that the organizer can ask for. Moreover there is an option to mark the field as required and hence making it compulsory for the order buyer to add that information in order to buy the tickets.

Route

The route is mainly responsible for fetching the required custom forms.

async model() {
    let filterOptions = [{
      name : 'form',
      op   : 'eq',
      val  : 'attendee'
    }];

    let data = {
      event: this.modelFor('events.view')
    };
    data.customForms = await data.event.query('customForms', {
      filter       : filterOptions,
      sort         : 'id',
      'page[size]' : 50
    });

    return data;
  }

If they don’t exist then we create them in afterModel hook. We check if the size of the list of custom forms sent from the server is 3 or not. If it is 3 then we create the additional custom forms for the builder. Upon creating an event, the server automatically creates 3 custom forms for the builder. These 3 forms are firstName, lastName and email.

afterModel(data) {
    /**
     * Create the additional custom forms if only the compulsory forms exist.
     */
    if (data.customForms.length === 3) {
      let customForms = A();
      for (const customForm of data.customForms ? data.customForms.toArray() : []) {
        customForms.pushObject(customForm);
      }

      const createdCustomForms = this.getCustomAttendeeForm(data.event);

      for (const customForm of createdCustomForms ? createdCustomForms : []) {
        customForms.pushObject(customForm);
      }

      data.customForms = customForms;
    }
  }

Complete source code for reference can be found here.

Component Template

The component template for the form builder is supposed to show the forms and other options to the user in a presentable manner. Due to pre-existing components for handling custom forms, the template is extremely simple. We just loop over the list of custom forms and present the event organizer with a table comprising of the forms. Apart from the forms the organizer can specify the order expiry time. Lastly we present a save button in order to save the changes.

<form class="ui form {{if isLoading 'loading'}}"  {{action 'submit' data on='submit'}} autocomplete="off">
  <h3 class="ui dividing header">
    <i class="checkmark box icon"></i>
    <div class="content">
      {{t 'Information to Collect'}}
    </div>
  </h3>
  <div class="ui two column stackable grid">
    <div class="column">
      <table class="ui selectable celled table">
        <thead>
          <tr>
            {{#if device.isMobile}}
              <th class="center aligned">
                {{t 'Options'}}
              </th>
            {{else}}
              <th class="right aligned">
                {{t 'Option'}}
              </th>
              <th class="center aligned">
                {{t 'Include'}}
              </th>
              <th class="center aligned">
                {{t 'Require'}}
              </th>
            {{/if}}
          </tr>
        </thead>
        <tbody>
          {{#each data.customForms as |field|}}
            <tr class="{{if field.isIncluded 'positive'}}">
              <td class="{{if device.isMobile 'center' 'right'}} aligned">
                <label class="{{if field.isFixed 'required'}}">
                  {{field.name}}
                </label>
              </td>
              <td class="center aligned">
                {{ui-checkbox class='slider'
                              checked=field.isIncluded
                              disabled=field.isFixed
                              onChange=(action (mut field.isIncluded))
                              label=(if device.isMobile (t 'Include'))}}
              </td>
              <td class="center aligned">
                {{ui-checkbox class='slider'
                              checked=field.isRequired
                              disabled=field.isFixed
                              onChange=(action (mut field.isRequired))
                              label=(if device.isMobile (t 'Require'))}}
              </td>
            </tr>
          {{/each}}
        </tbody>
      </table>
    </div>
  </div>
  <h3 class="ui dividing header">
    <i class="options box icon"></i>
    <div class="content">
      {{t 'Registration Options'}}
    </div>
  </h3>
  <div class="field">
    <label>{{t 'REGISTRATION TIME LIMIT'}}</label>
    <div class="{{unless device.isMobile 'two wide'}} field">
      {{input type='number' id='orderExpiryTime' value=data.event.orderExpiryTime min="1" max="60" step="1"}}
    </div>
  </div>
  <div class="ui hidden divider"></div>
  <button type="submit" class="ui teal submit button" name="submit">{{t 'Save'}}</button>
</form>

 

Component

The component is responsible for saving the form. It also provides runtime validations to ensure that the entries entered in the fields of the form are valid.

import Component from '@ember/component';
import FormMixin from 'open-event-frontend/mixins/form';

export default Component.extend(FormMixin, {
  getValidationRules() {
    return {
      inline : true,
      delay  : false,
      on     : 'blur',
      fields : {
        orderExpiryTime: {
          identifier : 'orderExpiryTime',
          rules      : [
            {
              type   : 'integer[1..60]',
              prompt : this.get('l10n').t('Please enter a valid registration time limit between 1 to 60 minutes.')
            }
          ]
        }
      }
    };
  },
  actions: {
    submit(data) {
      this.onValid(() => {
        this.save(data);
      });
    }
  }
});

References

 

Continue ReadingAttendee Form Builder in Open Event Frontend

Redirecting to Previous Route in Ember

The Open-Event-Frontend allows the event organiser to create tickets for his or her event. Other uses can buy these tickets in order to attend the event. In order to make the user experience smooth, we redirect the user to their previous route when they successfully login into their account. This blog explains how we have achieved this functionality in the project.

Insight

We have two different cases to handle in order to solve this problem:

  1. The user was in route A and wanted to move to route B. Here route A doesn’t require authorization and route B requires authorization. In this case, we would like to direct the user to the login route and once they are done, redirect them back to route B.
  2. The user was in route A and directly entered the login route using the login button. In this case we want to direct them back to the route A after successful login.

We use Ember-simple-auth in order to manage authentication in the project. Not only does it make it easy to manage authentication, it also handles the case 1 for us out of the box. So now the simplified problem is to redirect the user back to the previous route if they entered the login route directly using the web address or the login button.

Approach

If we can somehow store the previous route visited by a user, then we can easily redirect them back once they are logged in.

We will add a custom property in the session service called previousRouteName which will store the URL of the previous route visited by the user. We will make use of the willTransition hook in the application.js file. This hook is called everytime the user transitions from one route to another which makes it suitable for us to update the previousRouteName.

actions: {
    willTransition(transition) {
      transition.then(() => {
        let params = this._mergeParams(transition.params);
        let url;

        // generate doesn't like empty params.
        if (isEmpty(params)) {
          url = transition.router.generate(transition.targetName);
        } else {
          url = transition.router.generate(transition.targetName, params);
        }
        // Do not save the url of the transition to login route.
        if (!url.includes('login')) {
          this.set('session.previousRouteName', url);
        }
      });
    }
  }

_mergeParams is a helper function which makes use of merge function of the Lodash library.

/**
   * Merge all params into one param.
   * @param params
   * @return {*}
   * @private
   */
  _mergeParams(params) {
    return merge({}, ...values(params));
  },

Now we’re done with saving the URL of the previous route. All that remains is to trigger the redirect once the user has successfully logged in. We will use the sessionAuthenticated hook which is triggered everytime the user logs in.

sessionAuthenticated() {
    if (this.get('session.previousRouteName')) {
      this.transitionTo(this.get('session.previousRouteName'));
    } else {
      this._super(...arguments);
    }
  },

If the previous route variable is set, we redirect to it otherwise we can the super method and let Ember-simple-auth handle case 1 mentioned earlier for us.

References

Continue ReadingRedirecting to Previous Route in Ember

My Tickets in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create tickets for his or her event. Other uses can buy these tickets in order to attend the event. The My tickets section lists all the tickets that have been bought by a user. This blog post explains how it has been implemented in the project.

Route

The My-Tickets list route has three responsibilities:

  1. Showing appropriate title according to the current tab.
  2. Setting the filter options according to the tab.
  3. Fetching the data from the store according to the filter options.

The title of the route is decided by the following snippet:

titleToken() {
    switch (this.get('params.ticket_status')) {
      case 'upcoming':
        return this.get('l10n').t('Upcoming');
      case 'past':
        return this.get('l10n').t('Past');
      case 'saved':
        return this.get('l10n').t('Saved');
    }
  },

The second and the third requirement is satisfied inside the model hook. We define the filterOptions according to the current tab and then make the request to fetch the data accordingly. The following code snippet is responsible for this:

model(params) {
    this.set('params', params);
    let filterOptions = [{
      name : 'completed-at',
      op   : 'ne',
      val  : null
    }];
    if (params.ticket_status === 'upcoming') {
      filterOptions.push(
        {
          name : 'event',
          op   : 'has',
          val  : {
            name : 'starts-at',
            op   : 'ge',
            val  : moment().toISOString()
          }
        });
    } else if (params.ticket_status === 'past') {
      filterOptions.push(
        {
          name : 'event',
          op   : 'has',
          val  : {
            name : 'ends-at',
            op   : 'lt',
            val  : moment().toISOString()
          }
        }
      );
    }

    return this.get('authManager.currentUser').query('orders', {
      include : 'event',
      filter  : filterOptions
    });
  }

Template

The template of the My tickets list is extremely simple. We simply loop over all the orders and use the order-card component to display each of them. The order-card component is discussed in detail later. If there are no orders under the user, we show the appropriate message.

<div class="row">
  <div class="sixteen wide column">
    {{#if model}}
      {{#each model as |order|}}
        {{#order-card order=order}}
        {{/order-card}}
        <div class="ui hidden divider"></div>
      {{/each}}
    {{else}}
      <div class="ui disabled header">{{t 'No tickets found'}}</div>
    {{/if}}
  </div>
</div>

Order-card Component

The order card component is responsible for handling a single order and showing its details in as a card. In order to decide whether the order is a paid order or not, we have defined a computed property inside the order-card.js file.

import Component from '@ember/component';
import { computed } from '@ember/object';
import { isEqual } from '@ember/utils';

export default Component.extend({
  isFreeOrder: computed('order', function() {
    const amount = this.get('order.amount');
    return amount === null || isEqual(amount, '0');
  })
});

The template for the component contains the event logo aligned to the left in the card. We show the event details such as the name, location and start date on the right. Below the event details we show the order details such as the order amount, currency, the identifier and the date and time on which the order was completed. Below is the full code for reference:

<div class="event wide ui grid row">
  {{#unless device.isMobile}}
    <div class="ui card three wide computer six wide tablet column">
      <a class="image" href="#">
        {{widgets/safe-image src=(if order.event.originalImageUrl order.event.originalImageUrl order.event.originalImageUrl)}}
      </a>
    </div>
  {{/unless}}
  <div class="ui card thirteen wide computer ten wide tablet sixteen wide mobile column">
    <a class="main content" href="#">
      {{#smart-overflow class='header'}}
        {{order.event.name}}
      {{/smart-overflow}}
      <div class="meta">
        <span class="date">
          {{moment-format order.event.startsAt 'ddd, DD MMMM YYYY, h:mm A'}}
        </span>
      </div>
      {{#smart-overflow class='description'}}
        {{order.event.shortLocationName}}
      {{/smart-overflow}}
    </a>
    <div class="extra content small text">
      <span>
        <span>
          {{#if isFreeOrder}}
            {{t 'Free'}}
          {{else}}
            {{order.event.paymentCurrency}}{{order.amount}}
          {{/if}}
          {{t 'order'}}
        </span>
        <span>#{{order.identifier}}</span>
        <span>{{t 'on'}} {{moment-format order.completedAt 'MMMM DD, YYYY h:mm A'}}</span>
      </span>
    </div>
  </div>
</div>

References

Continue ReadingMy Tickets in Open Event Frontend