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

How webclient Settings are Implemented in SUSI.AI Accounts

The goal of accounts.susi is to implement all settings from different clients at a single place where user can change their android, iOS or webclient settings altogether and changes should be implemented directly on the susi server and hence the respective clients. This post focuses on how webclient settings are implemented on accounts.susi. SUSI.AI follows the accounting model across all clients and every settings of a user are stores at a single place in the accounting model itself. These are stored in a json object format and can be fetched directly from the ListUserSettings.json endpoint of the server.

Continue ReadingHow webclient Settings are Implemented in SUSI.AI Accounts

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

Implement Sensor Data Fetching Using AsyncTask

In PSLab android app we have implemented sensor data fetching of various sensors inbuilt in the mobile device like light sensor, accelerometer, gyrometer. We can use PSLab to log the data and show in the form of the graph or maybe export the data in the form of CSV format for future use.

But recording data from the phone sensor imposes a serious problem in the performance of the Android app as it is a costly to process in terms of memory, resources and time. In CS terms there is too much work that has to be done on the single main thread which sometimes leads to lag and compromises the UX.

So as a solution we applied a concept of the Multithreading provided by Java in which we can shift the heavy process to a separate background thread so that the main thread never gets interrupted during fetching the sensor data and the background thread handles all the fetching and updates the UI as soon as it gets the data, till then the Main thread continues to serves the user so to user the application remains always responsive.

For implementing this we used a special class provided by Android Framework called AsyncTask. Which provides below methods:-

  • doInBackground() : This method contains the code which needs to be executed in the background. In this method, we can send results multiple times to the UI thread by publishProgress() method.

  • onPreExecute() : This method contains the code which is executed before the background processing starts.

  • onPostExecute() : This method is called after doInBackground method completes processing. Result from doInBackground is passed to this method.

  • onProgressUpdate() : This method receives progress updates from doInBackground() method, which is published via publishProgress() method, and this method can use this progress update to update the UI thread.

  • onCancelled(): This method is called when the background task has been canceled. Here we can free up resources or write some cleanup code to avoid memory leaks.

We created a class SensorDataFetch and extended this AsyncTask class and override its methods according to our needs.

private class SensorDataFetch extends AsyncTask<Void, Void, Void> implements SensorEventListener {

   private float data;
   private long timeElapsed;

   @Override
   protected Void doInBackground(Void... params) {
      
       sensorManager.registerListener(this, sensor, updatePeriod);
       return null;
   }

   protected void onPostExecute(Void aVoid) {
       super.onPostExecute(aVoid);
       visualizeData();
   }

   @Override
   protected void onPreExecute() {
 super.onPreExecute();
   //do nothing
   }

   @Override
   protected void onProgressUpdate(Void... values) {
       super.onProgressUpdate(values);
          //do nothing
   }

   @Override
   protected void onCancelled() {
       super.onCancelled();
          //do nothing
   }

In doInBackground() method we implemented the fetching raw data from the sensor by registering the listener and in onPostExecute() method we updated that data on the UI to be viewed by the user.

When this process is being run in the background thread the Main UI thread is free and remains responsive to the user. We can see in Figure 1 below that the UI is responsive to the user swipe action even when the sensor data is updating continuously on the screen.

Figure 1 shows Lux Meter responding to user swipe while fetching sensor data flawlessly.

 

Resources

https://developer.android.com/reference/android/os/AsyncTask – Android Developer documentation for Async Task class.

Continue ReadingImplement Sensor Data Fetching Using AsyncTask

Snackbar Implementation in PSLab Android App

In PSLab android app we have developed the functionality of logging sensor data in CSV format. We can start and stop the data recording using the save button in the upper right corner of the menu bar and toast message was shown to notify the user for logging status whether it is started or stopped but it leads to some problem like:-

  • The user doesn’t know where the logged file has been created in the external storage.
  • If the user accidentally clicked on the save button the data logging will start the user have to manually go the storage location and delete the recently created unwanted CSV file.

What’s the solution?

The solution to both these problem is solved by implementing Snackbar instead of Toast message.

According to Material Design documentation:-

The Snackbar widget provides brief feedback about an operation through a message at the bottom of the screen. Snackbar disappears automatically, either after a timeout or after a user interaction elsewhere on the screen, and can also be swiped off the screen.

Snackbar can also offer the ability to perform an action, such as undoing an action that was just taken or retrying an action that had failed.

 

Figure 1 shows a Snackbar sample
(Source: – https://material.io/develop/android/components/snackbar/ )

 

To implement the Snackbar in our Android app I started by creating a custom snack bar class which contains all the code to create and show the Snackbar on the screen.

public class CustomSnackBar {

   public static void showSnackBar(@NonNull CoordinatorLayout holderLayout,  
                                   @NonNull String displayText,
                                   String actionText, 
                                   View.OnClickListener clickListener){
       
   Snackbar snackbar =     
              Snackbar.make(holderLayout,displayText,Snackbar.LENGTH_LONG)
              .setAction(actionText, clickListener);

  //do your customization here
}

The custom class contains a static method ‘showSnackBar()’ having parameters:

Parameter Return Type Description
holderLayout CoordinatorLayout Container layout in which the snack bar will be shown at the bottom (should not be null)
displayText String Text to be displayed in the content of Snackbar (should not be null)
actionText String Clickable text which has some action associated with it
clickListener View.OnClickListener On click listener specifying an action to be performed when actionText is clicked

 

Inside the method, I called the static make()  method provided by the Snackbar class and passed holderlayout, displayText and duration of Snackbar in this case Snackbar.LENGTH_LONG as parameters.

Then I called setAction() and passed in the actionText and the clickListener as parameters in it to set the action text. If we pass in null no action text will be generated.

Then, if we want to changes the action text color we can do that by calling setActionTextColor() and passing in the desired color.

snackbar.setActionTextColor(ContextCompat.getColor(holderLayout.getContext(), R.color.colorPrimary));

And if we want to change the content text color then we need to first get the view then we need to get the instance of TextView containing the content text using findViewById() and passing android.support.design.R.id.snackbar_text which is default ID for context TextView, and then call setTextColor() to set the desired color.

View sbView = snackbar.getView();
   TextView textView =     
             sbView.findViewById(android.support.design.R.id.snackbar_text);
       textView.setTextColor(Color.WHITE);
   }

So, now our Snackbar engine is complete now we need to call CustomSnackBar class static method showSnackbar() in our sensor data logger.

For doing this I replaced all the instances of the Toast message with the ‘CustomSnackBar’ by passing in the desired messages that were being passed in Toast message.

But I still need to find the location of our stored CSV file and a method to delete the current generated CSV file.

For that, I did below modification to the CSVLogger class in PSLab android app.

public class CSVLogger {
   private static final String CSV_DIRECTORY = "PSLab";
   public CSVLogger(String category) {
       this.category = category;
       setupPath();
   }
   /*Below methods are included at the bottom of the class */
   public String getCurrentFilePath() {
       return Environment.getExternalStorageDirectory().getAbsolutePath() +
               File.separator + CSV_DIRECTORY + File.separator + category;
   }
   public void deleteFile() {
       csvFile.delete();
   }
}

Now for passing the location of the stored file and implementing delete option, I called the below method when the CSV logging is stopped by the user:

CustomSnackBar.showSnackBar((CoordinatorLayout) parent.findViewById(R.id.cl),

                    “CSV File stored at” + " " +lux_logger.getCurrentFilePath(),
  
                    “DELETE”,

                    new View.OnClickListener() {
                              Override
                              public void onClick(View view) {
                                             lux_logger.deleteFile();    
                              });

By doing this I get a Snackbar as shown in Figure 2, clicking on the “DELETE” text deletes the current CSV file.

Figure 2 shows snackbar showing file stored location and delete option

 

So, implementing Snackbar helped to make the app interactive and keeps user notified and control the data logging.

Resources

  1. https://www.journaldev.com/10324/android-snackbar-example-tutorial – Android SnackBar example implemetation tutorial
  2. https://material.io/develop/android/components/snackbar/ – Android Material Desing implementation of Snackbar.
Continue ReadingSnackbar Implementation in PSLab Android App

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