Implementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

In Eventyay Attendee, searching for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Mapbox?
  • Integrating places autosuggestion for searching
  • Conclusion
  • Resources

WHY USING MAPBOX?

There are many Map APIs to be taken into consideration but we choose Mapbox as it is really to set up and use, good documentation and reasonable pricing for an open-source project compared to other Map API.

INTEGRATING PLACES AUTOSUGGESTION FOR SEARCHING

Step 1: Setup dependency in the build.gradle + the MAPBOX key

//Mapbox java sdk
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:4.8.0'

Step 2: Set up functions inside ViewModel to handle autosuggestion based on user input:

private fun loadPlaceSuggestions(query: String) {
   // Cancel Previous Call
   geoCodingRequest?.cancelCall()
   doAsync {
       geoCodingRequest = makeGeocodingRequest(query)
       val list = geoCodingRequest?.executeCall()?.body()?.features()
       uiThread { placeSuggestions.value = list }
   }
}

private fun makeGeocodingRequest(query: String) = MapboxGeocoding.builder()
   .accessToken(BuildConfig.MAPBOX_KEY)
   .query(query)
   .languages("en")
   .build()

Based on the input, the functions will update the UI with new inputs of auto-suggested location texts. The MAPBOX_KEY can be given from the Mapbox API.

Step 3: Create an XML file to display autosuggestion strings item and set up RecyclerView in the main UI fragment

Step 4: Set up ListAdapter and ViewHolder to bind the list of auto-suggested location strings. Here, we use CamenFeature to set up with ListAdapter as the main object. With the function .placeName(), information about the location will be given so that ViewHolder can bind the data

class PlaceSuggestionsAdapter :
   ListAdapter<CarmenFeature,
       PlaceSuggestionViewHolder>(PlaceDiffCallback()) {

   var onSuggestionClick: ((String) -> Unit)? = null

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceSuggestionViewHolder {
       val itemView = LayoutInflater.from(parent.context)
           .inflate(R.layout.item_place_suggestion, parent, false)
       return PlaceSuggestionViewHolder(itemView)
   }

   override fun onBindViewHolder(holder: PlaceSuggestionViewHolder, position: Int) {
       holder.apply {
           bind(getItem(position))
           onSuggestionClick = this@PlaceSuggestionsAdapter.onSuggestionClick
       }
   }

   class PlaceDiffCallback : DiffUtil.ItemCallback<CarmenFeature>() {
       override fun areItemsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
           return oldItem.placeName() == newItem.placeName()
       }

       override fun areContentsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
           return oldItem.equals(newItem)
       }
   }
}
fun bind(carmenFeature: CarmenFeature) {
   carmenFeature.placeName()?.let {
       val placeDetails = extractPlaceDetails(it)
       itemView.placeName.text = placeDetails.first
       itemView.subPlaceName.text = placeDetails.second
       itemView.subPlaceName.isVisible = placeDetails.second.isNotEmpty()

       itemView.setOnClickListener {
           onSuggestionClick?.invoke(placeDetails.first)
       }
   }
}

Step 5: Set up RecyclerView with Adapter created above:

private fun setupRecyclerPlaceSuggestions() {
   rootView.rvAutoPlaces.layoutManager = LinearLayoutManager(context)
   rootView.rvAutoPlaces.adapter = placeSuggestionsAdapter

   placeSuggestionsAdapter.onSuggestionClick = {
       savePlaceAndRedirectToMain(it)
   }
}

RESULTS

CONCLUSION

Place Autocorrection is a really helpful and interesting feature to include in your next project. With the help of Mapbox SDK, it is really easy to implement to enhance your user experience in your application.

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee PR: #1594 – feat: Mapbox Autosuggest

Documentation: https://docs.mapbox.com/android/plugins/overview/places/

Continue ReadingImplementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

Implementing Attendee Forms in Wizard of Open Event Frontend

This blog post illustrates on how the order form is included in the attendee information of the Open Event Frontend form  and enabling the organizer to choosing what information to collect from the attendee apart from the mandatory data i.e. First Name, Last Name and the Email Id during the creation of event itself.

The addition of this feature required alteration in the existing wizard flow to accommodate this extra step. This new wizard flow contains the step :

  • Basic Details : Where organizer fills the basic details regarding the event.
  • Attendee Form : In this step, the organizer can choose what information he/she has to collect from the ticket buyers.
  • Sponsors : This step enables the organizer to fill in the sponsor details
  • Session and Speakers : As the name suggests, this final step enables the organizer to fill in session details to be undertaken during the event.

This essentially condensed the flow to this :

The updated wizard checklist

To implement this, the navigation needed to be altered first in the way that Forward and Previous buttons comply to the status bar steps

// app/controller/create.jsmove() {
    this.saveEventDataAndRedirectTo(
      'events.view.edit.attendee',
      ['tickets', 'socialLinks', 'copyright', 'tax', 'stripeAuthorization']
    );
  }
//app/controller/events/view/edit/sponsorship
move(direction) {
    this.saveEventDataAndRedirectTo(
      direction === 'forwards' ? 'events.view.edit.sessions-speakers' : 'events.view.edit.attendee',
      ['sponsors']
    );
  }

Once the navigation was done, I decided to add the step in the progress bar by simply including the attendees form in the event mixin.

// app/mixins/event-wizard.js
    {
      title     : this.l10n.t('Attendee Form'),
      description : this.l10n.t('Know your audience'),
      icon     : 'list icon',
      route     : 'events.view.edit.attendee'
    }

Now a basic layout for the wizard is prepared, all what is left is setting up the route for this step and including it in the router file. I took my inspiration for setting up the route from events/view/tickets/order-from.js and implemented it like this:

// app/routes/events/view/edit/attendee.js
import Route from '@ember/routing/route';
import CustomFormMixin from 'open-event-frontend/mixins/event-wizard';
import { A } from '@ember/array';
export default Route.extend(CustomFormMixin, {

titleToken() {
  return this.l10n.t('Attendee Form');
},

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;
},
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;
  }
}
});

With the route setup and included in the router, I just need to take care of the form data and pass it to the server. Thankfully, the project was already using EventWizardMixin so all I had to do was utilize these functions (save and move) which saves the event data in the status user decides to save it in i.e. either published or draft state

// app/controllers/events/view/edit/attendee.js
import Controller from '@ember/controller';
import EventWizardMixin from 'open-event-frontend/mixins/event-wizard';

export default Controller.extend(EventWizardMixin, {
async saveForms(data) {
  for (const customForm of data.customForms ? data.customForms.toArray() : []) {
    await customForm.save();
  }
  return data;
},
actions: {
  async save(data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        'events.view.index',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  },
  async move(direction, data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        direction === 'forwards' ? 'events.view.edit.sponsors' : 'events.view.edit.basic-details',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  }
}
});

Apart from that, the form design was already there, essentially, I reutilized the form design provided to an event organizer / co-organizer in the ticket section of the event dashboard to make it look like this form :

Basic attendee information collection

In the end, after utilizing the existing template and adding it in the route’s template, the implementation is ready for a test run!

// app/templates/events/view/edit/attendee.hbs
{{forms/wizard/attendee-step data=model move='move' save='save' isLoading=isLoading}}

This is a simple test run of how the attendees form step works as others work fine along with it!

Demonstration of new event submission workflow

Resources

Related Work and Code Repository

Continue ReadingImplementing Attendee Forms in Wizard of Open Event Frontend

Dependency Injection with Kotlin Koin in Eventyay Attendee

Eventyay Attendee Android app contains a lot of shared components between classes that should be reused. Dependency Injection with Koin really comes in as a great problem solver.

Dependency Injection is a common design pattern used in various projects, especially with Android Development. In short, dependency injection helps to create/provide instances to the dependent class, and share it among other classes.

  • Why using Koin?
  • Process of setting up Koin in the application
  • Results
  • Conclusion
  • Resources

Let’s get into the details

WHY USING KOIN?

Before Koin, dependency injection in Android Development was mainly used with other support libraries like Dagger or Guice. Koin is a lightweight alternative that was developed for Kotlin developers. Here are some of the major things that Koin can do for your project:

  • Modularizing your project by declaring modules
  • Injecting class instances into Android classes
  • Injecting class instance by the constructor
  • Supporting with Android Architecture Component and Kotlin
  • Testing easily

SETTING UP KOIN IN THE ANDROID APPLICATION

Adding the dependencies to build.gradle

// Koin
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-androidx-scope:$koin_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"

Create a folder to manage all the dependent classes.

Inside this Modules class, we define modules and create “dependency” class instances/singletons that can be reused or injected. For Eventyay Attendee, we define 5 modules: commonModule, apiModule, viewModelModule, networkModule, databaseModule. This saves a lot of time as we can make changes like adding/removing/editing the dependency in one place.

Let’s take a look at what is inside some of the modules:

DatabaseModule

val databaseModule = module {

   single {
       Room.databaseBuilder(androidApplication(),
           OpenEventDatabase::class.java, "open_event_database")
           .fallbackToDestructiveMigration()
           .build()
   }

   factory {
       val database: OpenEventDatabase = get()
       database.eventDao()
   }

   factory {
       val database: OpenEventDatabase = get()
       database.sessionDao()
   }

CommonModule

val commonModule = module {
   single { Preference() }
   single { Network() }
   single { Resource() }
   factory { MutableConnectionLiveData() }
   factory<LocationService> { LocationServiceImpl(androidContext()) }
}

ApiModule

val apiModule = module {
   single {
       val retrofit: Retrofit = get()
       retrofit.create(EventApi::class.java)
   }
   single {
       val retrofit: Retrofit = get()
       retrofit.create(AuthApi::class.java)
   }

NetworkModule

single {
   val connectTimeout = 15 // 15s
   val readTimeout = 15 // 15s

   val builder = OkHttpClient().newBuilder()
       .connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
       .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
       .addInterceptor(HostSelectionInterceptor(get()))
       .addInterceptor(RequestAuthenticator(get()))
       .addNetworkInterceptor(StethoInterceptor())

   if (BuildConfig.DEBUG) {
       val httpLoggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
       builder.addInterceptor(httpLoggingInterceptor)
   }
   builder.build()
}

single {
   val baseUrl = BuildConfig.DEFAULT_BASE_URL
   val objectMapper: ObjectMapper = get()
   val onlineApiResourceConverter = ResourceConverter(
       objectMapper, Event::class.java, User::class.java,
       SignUp::class.java, Ticket::class.java, SocialLink::class.java, EventId::class.java,
       EventTopic::class.java, Attendee::class.java, TicketId::class.java, Order::class.java,
       AttendeeId::class.java, Charge::class.java, Paypal::class.java, ConfirmOrder::class.java,
       CustomForm::class.java, EventLocation::class.java, EventType::class.java,
       EventSubTopic::class.java, Feedback::class.java, Speaker::class.java, FavoriteEvent::class.java,
       Session::class.java, SessionType::class.java, MicroLocation::class.java, SpeakersCall::class.java,
       Sponsor::class.java, EventFAQ::class.java, Notification::class.java, Track::class.java,
       DiscountCode::class.java, Settings::class.java, Proposal::class.java)

   Retrofit.Builder()
       .client(get())
       .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
       .addConverterFactory(JSONAPIConverterFactory(onlineApiResourceConverter))
       .addConverterFactory(JacksonConverterFactory.create(objectMapper))
       .baseUrl(baseUrl)
       .build()
}

As described in the code, Koin support single for creating a singleton object, factory for creating a new instance every time an object is injected.

With all the modules created, it is really simple to get Koin running in the project with the function startKoin() and a few lines of code. We use it inside the application class:

startKoin {
   androidLogger()
   androidContext(this@OpenEventGeneral)
   modules(listOf(
       commonModule,
       apiModule,
       viewModelModule,
       networkModule,
       databaseModule
   ))
}

Injecting created instances defined in the modules can be used in two way, directly inside a constructor or injecting into Android classes.  

Here is an example of dependency injection to the constructor that we used for a ViewModel class and injecting that ViewModel class into the Fragment:

class EventsViewModel(
   private val eventService: EventService,
   private val preference: Preference,
   private val resource: Resource,
   private val mutableConnectionLiveData: MutableConnectionLiveData,
   private val config: PagedList.Config,
   private val authHolder: AuthHolder
) : ViewModel() {
class EventsFragment : Fragment(), BottomIconDoubleClick {
   private val eventsViewModel by viewModel<EventsViewModel>()
   private val startupViewModel by viewModel<StartupViewModel>()

For testing, it is also really easy with support library from Koin.

@Test
fun testDependencies() {
   koinApplication {
       androidContext(mock(Application::class.java))
       modules(listOf(commonModule, apiModule, databaseModule, networkModule, viewModelModule))
   }.checkModules()
}

CONCLUSION

Koin is really easy to use and integrate into Kotlin Android project. Apart from some of the basic functionalities mention above, Koin also supports other helpful features like Scoping or Logging with well-written documentation and examples. Even though it is only developed a short time ago, Koin has proved to be a great use in the Android community. So the more complicated your project is, the more likely it is that dependency injection with Koin will be a good idea.

RESOURCES 

Documentation: https://insert-koin.io/

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Continue ReadingDependency Injection with Kotlin Koin in Eventyay Attendee

Omise Integration in Open Event Frontend

This blog post will elaborate on how omise has been integrated into the Open Event Frontend project. Omise is Thailand’s leading online payment gateway offering a wide range of processing solutions for this project and integrating it as a payment option widens the possibilities for user base and ease of payment workflow.

Similar to Paypal, Omise offers two alternatives for using their gateway, Test mode and Live mode, where the former is generally favoured for usage in Development and Testing phase while the latter is used in actual production for capturing live payments. Both these modes require a Public key and Secret key each and are only update-able on the admin route.

This was implemented by introducing appropriate fields in the settings model.

// app/models/setting.js
omiseMode            : attr('string'),
omiseTestPublic      : attr('string'),
omiseTestSecret      : attr('string'),
omiseLivePublic      : attr('string'),
omiseLiveSecret      : attr('string')

Once your Omise credentials are configured, you can go ahead and include the options in your event creation form. You will see an option to include Omise in your payment options if you have configured your keys correctly and if the gateway supports the currency your event is dealing with, for example, even if your keys are correctly configured, you will not get the option to use omise gateway for money collection if the currency is INR.

For showing omise option in the template, a simple computed property did the trick canAcceptOmise  in the form’s component file and the template as follows:

// app/components/forms/wizard/basic-details-step.js
canAcceptOmise: computed('data.event.paymentCurrency', 'settings.isOmiseActivated', function() {
  return this.get('settings.isOmiseActivated') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).omise;
})
// app/templates/components/forms/wizard/basic-details-step.js
{{#
if canAcceptOmise}}
      <
label>{{t 'Payment with Omise'}}</label>
      <
div class="field payments">
        <
div class="ui checkbox">
          {{input type='checkbox' id='payment_by_omise' checked=data.event.canPayByOmise}}
          <
label for="payment_by_omise">
            {{t 'Yes, accept payment through Omise Gateway'}}
            <
div class="ui hidden divider"></div>
            <
span class="text muted">
              {{t 'Omise can accept Credit and Debit Cards , Net-Banking and AliPay. Find more details '}}
              <
a href="https://www.omise.co/payment-methods" target="_blank" rel="noopener noreferrer">{{t 'here'}}</a>.
            </
span>
          </
label>
        </
div>
      </
div>
      {{#
if data.event.canPayByOmise}}
        <
label>{{t 'Omise Gateway has been successfully activated'}}</label>
      {{/
if}}
    {{/
if}}

Once the event has the payment option enabled, an attendee has chosen the option to pay up using omise, they will encounter this screen on their pending order page 

On entering the credentials correctly, they will be forwarded to order completion page. On clicking the “Pay” button, the omise cdn used hits the server with a POST request to the order endpoint  and is implemented as follows :

//controllers/orders/pending.js
isOmise: computed('model.order.paymentMode', function() {
  return this.get('model.order.paymentMode') === 'omise';
}),

publicKeyOmise: computed('settings.omiseLivePublic', 'settings.omiseLivePublic', function() {
  return this.get('settings.omiseLivePublic') || this.get('settings.omiseTestPublic');
}),

omiseFormAction: computed('model.order.identifier', function() {
  let identifier = this.get('model.order.identifier');
  return `${ENV.APP.apiHost}/v1/orders/${identifier}/omise-checkout`;
})
// app/templates/orders/pending.hbs
    {{#
if isOmise}}
      <
div>
        <
form class="checkout-form" name="checkoutForm" method='POST' action={{omiseFormAction}}>
          <
script type="text/javascript" src="https://cdn.omise.co/omise.js"
                  data-key="{{publicKeyOmise}}"
                  data-amount="{{paymentAmount}}"
                  data-currency="{{model.order.event.paymentCurrency}}"
                  data-default-payment-method="credit_card">
          </
script>
        </
form>
      </
div>
    {{/
if}}

Thus primarily using Omise.js CDN and introducing the omise workflow, the project now has accessibility to Omise payment gateway service and the organiser can see his successful charge.

Resources

Related Work and Code Repository

Continue ReadingOmise Integration in Open Event Frontend

Integrating System Roles API in Open Event Frontend

The Eventyay system supports different system roles and allows to set panel permissions for every role. The system supports two inbuilt roles namely Admin and Super Admin. The users having access to permissions panel can create new custom system roles and define set of panel permissions for them. Also the users are provided with the option of editing and deleting any system role except the two inbuilt system roles. The feature is implemented using custom-system-roles and panel-permissions API on the server.

Adding route for system-roles

The route for custom-system-system roles is defined which contains a model returning user permissions, system roles and the panel permissions. The model is defined as async so that the execution is paused while fetching the data from the store by adding the await expression.

async model() {
 return {
   userPermissions  : await this.get('store').findAll('user-permission'),
   systemRoles      : await this.get('store').findAll('custom-system-role'),
   panelPermissions : await this.get('store').findAll('panel-permission')
 };
},

The route created above gets all the data for user permissions, system-roles and panel permissions which is later used by the template for rendering of data.

Adding model for system-roles and panel-permissions

The model for system-roles is created which contains the ‘name’ attribute of type string and a relationship with panel permissions. Every system role can have multiple panel permissions, therefore a hasMany relationship is defined in the model.

export default ModelBase.extend({
 name: attr('string'),

 panelPermissions: hasMany('panelPermission')
});

Similarly, the model for panel-permissions is added to the models directory. The defined model contains ‘panelName’ as an attribute of type string and a bool value canAccess, defining if the panel is accessible by any role or not.

export default ModelBase.extend({
 panelName : attr('string'),
 canAccess : attr('boolean')
});

Defining controller for system-roles

The controller for system-roles is defined in the controllers/admin/permissions directory. The action for adding, updating and deleting system roles are defined in the controller. While adding the system roles, all the panels are fetched and checked which panel permissions are selected by the admin. A special property namely ‘isChecked’ is added to every panel permission checkbox which toggles on change. If the property is set true the corresponding panel is added to the panel permissions relationship of corresponding role. If no panel is selected, an error message to select atleast one panel is displayed.

deleteSystemRole(role) {
 this.set('isLoading', true);
 role.destroyRecord()
  ...
  // Notify success or failure
},
addSystemRole() {
 this.set('isLoading', true);
 let panels = this.get('panelPermissions');

 panels.forEach(panel => {
   if (panel.isChecked) {
     this.get('role.panelPermissions').addObject(panel);
   } else {
     this.get('role.panelPermissions').removeObject(panel);
   }
 });
 if (!this.get('role.panelPermissions').length) {
  // Notification to select atleast one panel
 } else {
   this.get('role').save()
    // Notify success or failure
 }
},
updatePermissions() {
 this.set('isLoading', true);
 this.get('model.userPermissions').save()
  ...
  // Notify success or failure
}

The actions defined above in the controller can be used in template by passing the appropriate parameters if required. The addSystemRole action makes a POST request to server for creating a new system role, the updatePermissions action makes a PATCH request for updating the existing system role and the deleteSystemRole action makes a delete request to the server for deleting the role.

Adding data to template for system-roles

The data obtained from the model defined in route is rendered in the template for system-roles. A loop for showing all system roles is added to the template with the name attribute containing the name of system role and another loop is added to display the panel permissions for the corresponding role.

{{#each model.systemRoles as |role|}}
 <tr>
   <td>{{role.name}}</td>
   <td>
     <div class="ui bulleted list">
       {{#each role.panelPermissions as |permission|}}
         <div class="item">{{concat permission.panelName ' panel'}}</div>
       {{/each}}
     </div>
   </td>
   <td>
    // Buttons for editing and deleting roles
   </td>
 </tr>
{{/each}}

A modal is to the component for creating and editing system roles. The data from this template is passed to the modal where the existing permissions are already checked and can be modified by the admins.

Resources

Continue ReadingIntegrating System Roles API in Open Event Frontend

Integrating Event Roles API in Open Event Frontend

The Eventyay system supports different type of roles for an event like Attendee, organizer, co-organizer, track-organizer, moderator and the registrar. Every role has certain set of permissions such as Create, Read, Update, Delete. The Admin of the system is allowed to change the permissions for any role. The interface for updating the even role permissions was already available on the server but was not integrated on the frontend. The system is now integrated with the API and allows admin to change event role permission for any role.

Adding model for event role permissions

The model for event role permissions is added to the models directory. The model contains the attributes like canDelete, canUpdate, canCreate, canRead and the relationship with event role and the service.

export default ModelBase.extend({
 canDelete : attr('boolean'),
 canUpdate : attr('boolean'),
 canCreate : attr('boolean'),
 canRead   : attr('boolean'),

 role        : belongsTo('role'),
 service     : belongsTo('service'),
 serviceName : computed.alias('service.name')
});

The above defined model ensures that every permission belongs to a role and service. An alias is declared in the model using the computed property which is later used in the controller to sort the permissions according to service name in lexicographical order.

Adding route for event roles

The route for event role is created which contains model returning an object containing the list of roles, services and permissions. The model is defined as async so that the execution is paused while fetching the data from the store by adding the await expression.

export default Route.extend({
 titleToken() {
   return this.get('l10n').t('Event Roles');
 },
 async model() {
   return {
     roles       : ['Attendee', 'Co-organizer', 'Moderator', 'Organizer', 'Track Organizer', 'Registrar'],
     services    : await this.get('store').query('service', {}),
     permissions : await this.get('store').query('event-role-permission', { 'page[size]': 30 })
   };
 }
});

The route created above queries the data for roles, services and permissions which is later used by the template for rendering of the data obtained.

Adding controller for event roles

The controller for event roles is added to the controllers/admin/permissions directory. The computed property is used to sort the services obtained from model lexicographically and the permissions are sorted by the help of alias created in the model.

services: computed('model', function() {
 return this.get('model.services').sortBy('name');
}),
sortDefinition : ['serviceName'],
permissions    : computed.sort('model.permissions', 'sortDefinition'),
actions        : {
 updatePermissions() {
   this.set('isLoading', true);
   this.get('model.permissions').save()
     .then(() => {
       // Notify success and add Error handler
      }
   }
}

An action named updatePermissions is defined which is triggered when the admin updates and saves the permissions for any role where a PATCH request is made to the server in order to update the permissions.

Rendering data in the template

The data obtained from the model is manipulated in the controller and is rendered to the table in the event-roles template. Every role is fetched from the model and added to the template, all the permissions in sorted order are obtained from the controller and matched with the current role name. The relationship of permissions with role is used to check if its title is equal to the the current role. The permissions are updated accordingly, if the role title is equal to current role.

<tbody>
 {{#each model.roles as |role|}}
   <tr>
     <td>{{role}}</td>
     {{#each permissions as |permission|}}
       {{#if (eq permission.role.titleName role)}}
         <td>
           {{ui-checkbox label=(t 'Create') checked=permission.canCreate onChange=(action (mut permission.canCreate))}}
           <br>
           {{ui-checkbox label=(t 'Read') checked=permission.canRead onChange=(action (mut permission.canRead))}}
           <br>
           {{ui-checkbox label=(t 'Update') checked=permission.canUpdate onChange=(action (mut permission.canUpdate))}}
           <br>
           {{ui-checkbox label=(t 'Delete') checked=permission.canDelete onChange=(action (mut permission.canDelete))}}
         </td>
       {{/if}}
     {{/each}}
   </tr>
 {{/each}}
</tbody>

After rendering the data as shown above, the checkbox for permissions of different services for different roles are checked or unchecked depending upon the bool value of corresponding permission. The admin can update the permissions by checking or unchecking the checkbox and saving the changes made.

Resources

Continue ReadingIntegrating Event Roles API in Open Event Frontend

Adding Speakers Page in Open Event Frontend

Open Event Frontend earlier displayed all the speakers of an event on the main info page only, now a separate route for speakers is created and a separate page is added to display the speakers of an event. The design and layout of speakers page is kept similar to that on Open Event Web app. The info page only shows the featured speakers for an event and the complete list of speakers with additional information is present on speakers route.

Getting the event speakers data

The event data is obtained from the public model and a query is made for the speakers to get the required data. The speakers are fetched only for the sessions which are accepted, this is done by applying a filter while the query is made.

async model() {
 const eventDetails = this.modelFor('public');
 return {
   event    : eventDetails,
   speakers : await eventDetails.query('speakers', {
     filter: [
       {
         name : 'sessions',
         op   : 'any',
         val  : {
           name : 'state',
           op   : 'eq',
           val  : 'accepted'
         }
       }
     ]
   })
 };
}

Adding template for displaying speakers

A template is added to display three speakers in a row. The speakers data obtained from the model is looped through and details of every speaker is passed to the speaker-item component, which handles the design and layout for every item in the speakers list.

<div class="ui stackable grid container">
 {{#each model.speakers as |speaker|}}
   <div class="five wide column speaker-column">
     {{public/speaker-item speaker=speaker}}
   </div>
 {{/each}}
</div>

Adding component for speaker-item

A component for displaying the speaker-item is added to templates/component/public directory. The component contains of an accordion which displays the speaker details like biography, social links and the sessions that would be taken by him.

{{#ui-accordion}}
 <div class="title">
   <div class="ui">
     <img alt="speaker" class="ui medium rounded image" src="{{if speaker.photo.iconImageUrl speaker.image '/images/placeholders/avatar.png'}}">
    ...
    ... 
    ...
    // Speaker Details
   </div>
 </div>
{{/ui-accordion}}

The accordion with speaker image and other details appears for every speaker of an event.

Resources

Continue ReadingAdding Speakers Page in Open Event Frontend

Implementing the PDF download of Schedule in Open Event Web app

Open Event Web app now provides an option to its users to download the PDF of event schedule. Earlier it supported the download of list-view only, now it provides the support to download calendar-view as well. The problem incurred while downloading the calendar-view was that the view gets cropped due to limitations with the library used for PDF generation, thus only some parts of the calendar remained in the PDF. The problem is resolved by creating an image for every date in the schedule and adding the generated image to the PDF.

Selecting and adding the data for PDF generation

The data to be added to PDF depending on the filters and date-selectors applied is chosen from the DOM. Selection of data is done by looping through all the dates and adding only the ones which do not have ‘hide’ class added to them. The selected dates are first expanded such that their complete view is available while generating the image. The complete data is stored in a variable depending on if the complete schedule is requested for download or some filter is applied, which is later used for generating the image.

let fullScheduler = true;
let mapValue = '';

pdf = new jsPDF('l', 'pt', 'a1');
$('.calendar').each(function() {
 let hidePresent = $(this).attr('class').split(' ').indexOf('hide') <= 0;

 if (hidePresent) {
   $timeline = $(this);

  // Expanding the schedule for current date
  ...
  ...
 }
 fullScheduler = hidePresent && fullScheduler;
});

if(fullScheduler) {
 $timeline = $('.calendar').parent();
 mapValue = $timeline.children();
}

Adding the notification while generating the PDF

A loader with the notification is added to provide better user experience, as the PDF generation takes place at the time of request itself it may take some time depending on the size of the schedule. The notifications are added using ‘sweetalert’ library already added for Add to calendar notifications.

swal("Generating the PDF",{
 icon: "./images/loader.gif",
 buttons: false
});

Downloading the PDF

The selected dates are stored in an array named ‘schedArr’ whose data sequentially is passed for canvas generation. A new page is added to the PDF of size equal to canvas and the generated canvas is added to that page. With every new page added to the calendar a count is increased to keep a track if all the selected dates are added to PDF.

async.eachSeries(schedArr, function (child, callback) {
 html2canvas(child, {
   onrendered: function (canvas) {
     pdf.addPage(canvas.width, canvas.height);
     child.style.width = initialWidth[count] + 'px';
     pdf.addImage(canvas, 'png', 0, 0, canvas.width, canvas.height);
     currDate++;
     if(currDate === schedArr.length){
       pdf.deletePage(1);
       swal.close();
       pdf.save(scheduleDate + '.pdf');
     }
     count++;
     callback();
   }
 });
});

When the last page is added, the notification is closed and user is prompted to download pop-box.

Resources

Continue ReadingImplementing the PDF download of Schedule in Open Event Web app

Integrating Google Calendar API in Open Event Web app

Open Event Web app allows organizers to add a feature which enables the users to add any session to their google calendar. The organizer is required to generate an API key and client ID on Google developers console and add the generated credentials to web app generator. The credentials are added to the generated application and every session is added with an Add to calendar button, which on click makes the request to add the corresponding session to the calendar.

Creating Client ID and API key

Enable the Google calendar API from Google developers console. Go to `Create Credentials` tab and generate an API key and client ID for your app. While creating the client ID, an input field is present which requires Authorised javascript origins, mention the domains where the generated application would be deployed.

Adding Client ID and API key to the generator

The Client ID and API key obtained from the developer console is added to the web app generator. The event generated uses these credentials to make a request to the server for adding any session to the calendar.

The added credentials are used to initialise the client in the procedure `initClient()` –

function initClient() {
 let id = document.getElementById('gcalendar-id').value;
 let key = document.getElementById('gcalendar-key').value;
 let CLIENT_ID = id;
 let API_KEY = key;
 let DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"];
 let SCOPES = "https://www.googleapis.com/auth/calendar";

 gapi.client.init({
   apiKey: API_KEY,
   clientId: CLIENT_ID,
   discoveryDocs: DISCOVERY_DOCS,
   scope: SCOPES
 })
}

Adding session to the Google calendar

Every Google calendar enabled event is provided a button with every session, so that corresponding session can be added to the calendar. A procedure named `handleAuthClick` is called with the details of session being passed as parameter when the user clicks on the button. This function handles the authentication required for adding session to the calendar.

function handleAuthClick(title, location, calendarStart, calendarEnd, timezone, description) {
 let isSignedIn = gapi.auth2.getAuthInstance().isSignedIn.get();
 if (!isSignedIn) {
   gapi.auth2.getAuthInstance().signIn().then(function() {
     main.listUpcomingEvents(title, location, calendarStart, calendarEnd, timezone, description);
   });
 } else {
   main.listUpcomingEvents(title, location, calendarStart, calendarEnd, timezone, description);
 }
}

A function named `listUpcomingEvents` makes the request to insert the event object with details of the session to the calendar.

function listUpcomingEvents(title, location, calendarStart, calendarEnd, timezone, description) {
 let event = {
  ... // Event details  


  ... // Code for notifications
  ...
  ...
   'colorId': '5'
 };

 let request = gapi.client.calendar.events.insert({
   'calendarId': 'primary',
   'resource': event
 });
 request.execute(function(event) {
  // Success notification
 });
}

When the session with the corresponding data is added to calendar, an alert box notifying successful addition of session is shown up on the screen.

Resources

Continue ReadingIntegrating Google Calendar API in Open Event Web app

Add feature to view slides and videos of sessions in Open Event Webapp

The Open Event Web App has two components :

  • An event website generator
  • The actual generated website output.

The web generator application can generate event websites by getting data from event JSON files and binary media files, that are stored in a compressed zip file or through an API endpoint. The JSON data format of version 1 as well as version 2, provides user an option to add the slide and video URLs of the sessions. The data from JSONs is extracted and stored in the objects for a particular session, and in the template, the data for videos and slides are rendered in their corresponding iframes.

Extracting data from event JSONs

The data is extracted from the JSONs and is stored in an object. The object containing the data is sent to the procedure which compiles the handlebars templates with that data.

JSON data format v1

video: session.video,
slides: session.slides,
audio: session.audio

 

JSON data format v2

video: session['video-url'],
slides: session['slides-url'],
Audio: session['audio-url']

 

The JSON data format for v1 and v2 are different and thus the data is extracted from the file depending on API version chosen for web app generation. The files where data extraction takes place are fold_v1.js and fold_v2.js for API v1 and v2 respectively.

Adding event emitter

Onclick event emitter on schedule division calls the procedure “loadVideoAndSlides” with the parameters corresponding to the session clicked.

<div class="schedule-track" id="{{session_id}}" onclick = "loadVideoAndSlides('{{session_id}}', '{{video}}', '{{slides}}')">
   .....
   .....
</div>

The parameters Session ID, Video URL and Slide URL are passed to the procedure which is responsible for displaying the slides and video iframes for the sessions. This resolves the problem of heavy data binding to the page, as the frames for videos and slides are loaded on page only when the session is clicked.

Procedure called on click event

The performance of web app is significantly improved by using the call and listen mechanism as only the requested videos are loaded into the document object model.

function loadVideoAndSlides(div, videoURL, slideURL){
 if(videoURL !== null && videoURL !== '') {
     $('#desc-' + div).children('div').prepend(' + div + '" class = "video-iframe col-xs-12 col-sm-12 col-md-12" src="https://www.youtube.com/embed/' + videoURL + '" frameborder="0" allowfullscreen>');
 }
 if(slideURL !== null && slideURL !== '') {
     $('#desc-' + div).children('div').prepend(' + div + '" class = "iframe col-xs-12 col-sm-12 col-md-12" frameborder="0" src="https://view.officeapps.live.com/op/embed.aspx?src=' + slideURL  +'">');
 }
}

 

The video and slide URLs passed to the procedure are used for loading the iframes from youtube and office apps or google docs respectively as shown above, and the resulting slide view is as shown below

Resources

Continue ReadingAdd feature to view slides and videos of sessions in Open Event Webapp