Adding Helper and Adding Action Buttons to Orders List in Open Event Frontend

This blog post will illustrate how to add a helper to orders list and add action buttons to orders list to delete and cancel an order in Open Event Frontend. To cancel or delete an order item we need to communicate to the server. The API endpoints to which we communicate are:

  • PATCH        /v1/orders/{order_identifier}
  • DELETE    /v1/orders/{orders_identifier}

We will define the action buttons in ui-table component of open event frontend. We will use the cell-actions file to define the cell buttons that will be present in cell-actions column. The following handlebars code will render the buttons on website.

//components/ui-table/cell/events/view/tickets/orders/cell-actions.hbs

class="ui vertical compact basic buttons"> {{#if (and (not-eq record.status 'cancelled') (can-modify-order record))}} {{#ui-popup content=(t 'Cancel order') click=(action (confirm (t 'Are you sure you would like to cancel this Order?') (action cancelOrder record))) class='ui icon button' position='left center'}} class="delete icon"> {{/ui-popup}} {{/if}} {{#if (can-modify-order record)}} {{#ui-popup content=(t 'Delete order') click=(action (confirm (t 'Are you sure you would like to delete this Order?') (action deleteOrder record))) class='ui icon button' position='left center'}} class="trash icon"> {{/ui-popup}} {{/if}} {{#ui-popup content=(t 'Resend order confirmation') class='ui icon button' position='left center'}} class="mail outline icon"> {{/ui-popup}}

 

In above code you can see two things. First is can-modify-order which is a helper. Helper is used to simplify conditional logics which cannot be easily placed in handlebars. Second thing is action. There are two actions defined: cancelOrder and deleteOrder. We will see implementation of these later. First let’s see how we define can-modify-order helper.

In can-modify-order helper we want to return true or false in case we want cancel button and delete button to display or not respectively. We write the code of can-modify-order in helpers/can-modify-order.js file. When we want to get result from this helper we call it from handlebars file and pass any parameter that we want to use in helper. Code for can-modify-order helper is given below.

// helpers/can-modify-order.js

import Helper from '@ember/component/helper';

export function canModifyOrder(params) {
 let [order] = params;
 if (order.amount !== null && order.amount > 0) {
   // returns false if order is paid and completed
   return order.status !== 'completed';
 }
 // returns true for free ticket
 return true;
}

export default Helper.helper(canModifyOrder);

 

We extract the parameter and store it in order variable. We see if it satisfies our conditions we return true else false.

Now lets see how we can define actions to perform delete and cancel action on a order. We define these actions in controllers section of app. After performing suitable operation with order we call save to update modified order and destroyRecord() to delete an order. Let see the code implementation for these actions.

actions: {
   deleteOrder(order) {
     this.set('isLoading', true);
     order.destroyRecord()
       .then(() => {
         this.get('model').reload();
         this.notify.success(this.get('l10n').t('Order has been deleted successfully.'));
       })
       .catch(() => {
         this.notify.error(this.get('l10n').t('An unexpected error has occurred.'));
       })
       .finally(() => {
         this.set('isLoading', false);
       });
   },
   cancelOrder(order) {
     this.set('isLoading', true);
     order.set('status', 'cancelled');
     order.save()
       .then(() => {
         this.notify.success(this.get('l10n').t('Order has been cancelled successfully.'));
       })
       .catch(() => {
         this.notify.error(this.get('l10n').t('An unexpected error has occurred.'));
       })
       .finally(() => {
         this.set('isLoading', false);
       });
   }

 
After defining these actions, buttons in the orders list start working. In this way, we can make use of helper to simplify the conditional logic inside templates and define proper actions.

Resources:
Continue Reading

How to Make Promotional Codes Applicable on Tickets During Ordering in Open Event Frontend

This blog illustrate how to enable application of promotional codes on tickets during ordering tickets in Open Event Frontend to avail discounts and access to special tickets. Open event allows organizers to add some promotional codes on some tickets, which can be used by users to avail additional offers on tickets while ordering. Promotional codes can be of three types:

  1. Discount Codes: Allows customers to buy a ticket at discounted rates.
  2. Access Codes: Allows customers to access some hidden tickets which are accessible only to special customers.
  3. Discount + Access Code: Allows customer to access special tickets and avail discount at the same time.

Creating a discount/access code:

Organizers and admin can create an access code or a discount code from the event dashboard. They can specify the validity period of the code and can also specify the tickets on which the code will be applicable.

Validating promotional code after user enters the code:

User is allowed to enter the promotional code on events page upon selecting the tickets. IF promotional code is valid then suitable discount is provided on applicable tickets and if promotional code is an access code then hidden tickets for which the promotional code is valid are shown.

To check the validity of the promotional code we deal with the following APIs on the open event server:

  • GET             /v1/discount-codes/{Code}              (For Discount code)
  • GET             /v1/access-codes/{Code}                  (For Access code)

Code snippet to check the validity for access code is given below:

let promotionalCode = this.get('promotionalCode');
 let order = this.get('order');
   try {
     let accessCode = await this.get('store').findRecord('access-code', promotionalCode, {});
     order.set('accessCode', accessCode);
     let tickets = await accessCode.get('tickets');
     tickets.forEach(ticket => {
     ticket.set('isHidden', false);
     this.get('tickets').addObject(ticket);
     this.get('accessCodeTickets').addObject(ticket);
     this.set('invalidPromotionalCode', false);
  });
  } catch (e) {
     this.set('invalidPromotionalCode', true);
  }

 

Full code can be seen here https://github.com/fossasia/open-event-frontend/blob/development/app/components/public/ticket-list.js

Similarly for discount code we fetch the details of the discount code via the api and then validate the code. After the validation we apply the discount to the tickets applicable. Code snippet for the discount code part is given below:

try {
  let discountCode = await this.get('store').findRecord('discount-code', promotionalCode, { include: 'tickets' });
  let discountType = discountCode.get('type');
  let discountValue = discountCode.get('value');
  order.set('discountCode', discountCode);
  let tickets = await discountCode.get('tickets');
  tickets.forEach(ticket => {
     let ticketPrice = ticket.get('price');
     if (discountType === 'amount') {
       ticket.set('discount', Math.min(ticketPrice, discountValue));
       this.get('discountedTickets').addObject(ticket);
     } else {
       ticket.set('discount', ticketPrice * (discountValue / 100));
       this.get('discountedTickets').addObject(ticket);
     }
     this.set('invalidPromotionalCode', false);
  });
} catch (e) {
   if (this.get('invalidPromotionalCode')) {
      this.set('invalidPromotionalCode', true);
   }
}

 

Full code can be seen https://github.com/fossasia/open-event-frontend/blob/development/app/components/public/ticket-list.js

After promotional codes are verified we apply them to the selected tickets. In this way we apply the promotional codes to the tickets.

Resources

 

Continue Reading

Integrating Orders API to Allow User Select Tickets for Placing Order in Open Event Frontend

In Open Event Frontend organizer has option to sell tickets for his event. Tickets which are available to public are listed in public page of event for users to buy. In this blog post we will learn how to integrate orders API and manage multiple tickets of varying quantity under an order.

For orders we mainly interact with three API endpoints.

  1. Orders API endpoint
  2. Attendees API endpoint
  3. Tickets API endpoint

Orders and attendees have one to many relationship and similarly orders and tickets also have one to many relationship. Every attendee is related to one ticket. In simple words one attendee has one ticket and one order can contain many tickets of different quantity each meant for different attendee.

// routes/public/index.js

order: this.store.createRecord('order', {
  event     : eventDetails,
  user      : this.get('authManager.currentUser'),
  tickets   : [],
  attendees : []
})

 

We need to create instance of order model to fill in data in that record. We do this in our routes folder of public route. Code snippet is given below.

We create a empty array for tickets and attendees so that we can add their record instances as relationship to order.

As given in screenshot we have a dropdown for each ticket to select quantity of each ticket. To allow user select quantity of a ticket we use #ui-dropdown component of ember-semantic-ui in our template. The code snippet of that is given below.

// templates/components/public/ticket-list.js

{{#ui-dropdown class='compact selection' forceSelection=false onChange=(action 'updateOrder' ticket) as |execute mapper|}}
  {{input type='hidden' id=(concat ticket.id '_quantity') value=ticket.orderQuantity}}
    <i class="dropdown icon"></i>
    
class="default text">0
class="menu">
class="item" data-value="{{map-value mapper 0}}">{{0}}
{{#each (range ticket.minOrder ticket.maxOrder) as |count|}}
class="item" data-value="{{map-value mapper count}}">{{count}}
{{/each}} </div> {{/ui-dropdown}}

 

For every change in quantity of ticket selected we call a action named updateOrder. The code snippet for this action is given below.

// components/public/ticket-list.js

updateOrder(ticket, count) {
      let order = this.get('order');
      ticket.set('orderQuantity', count);
      order.set('amount', this.get('total'));
      if (count > 0) {
        order.tickets.addObject(ticket);
      } else {
        if (order.tickets.includes(ticket)) {
          order.tickets.removeObject(ticket);
        }
      }
    },

 

Here we can see that if quantity of selected ticket is more than 0 we add that ticket to our order otherwise we remove that ticket from the order if it already exists.

Once user selects his tickets for the order he/she can order the tickets. On clicking Order button we call placeOrder action. With help of this we add all the relations and finally send the order information to the server. Code snippet is given below.

// components/public/ticket-list.js
    
placeOrder() {
   let order = this.get('order');
   let event = order.get('event');
   order.tickets.forEach(ticket => {
     let attendee = ticket.orderQuantity;
     let i;
     for (i = 0; i < attendee; i++) {
       order.attendees.addObject(this.store.createRecord('attendee', {
         firstname : 'John',
         lastname  : 'Doe',
         email     : '[email protected]',
         event,
         ticket
       }));
     }
   });
   this.sendAction('save');
 }

 

Here for each ticket placed under an order we create a dummy attendee related to ticket and event. And then we call save action to save all the models. Code snippet for save is given:

actions: {
    async save() {
      try {
        this.set('isLoading', true);
        let order = this.get('model.order');
        let attendees = order.get('attendees');
        await order.save();
        attendees.forEach(async attendee => {
          await attendee.save();
        });
        await order.save()
          .then(order => {
            this.get('notify').success(this.get('l10n').t('Order created.'));
            this.transitionToRoute('orders.new', order.id);
          });
      } catch (e) {
        this.get('notify').error(this.get('l10n').t('Oops something went wrong. Please try again'));
      }
    }
  }

 

Here we finally save all the models and transition to orders page to enable user fill the attendees details.

Resources:
Continue Reading

Modifying Tickets API in Open Event Server to Return Hidden Tickets Only for Organizers and Admins

This blog article will illustrate how we can modify the permissions settings for an API to enable different kind of responses to users with different level of permissions. In this article we will discuss these changes with respect to Tickets API.

Initially we had a query where we were returning only those tickets who were set to be visible by the admin. Query for this was:

class TicketList(ResourceList):
   """
   List Tickets based on different params
   """
   def before_get(self, args, view_kwargs):
       """
       before get method to get the resource id for assigning schema
       :param args:
       :param view_kwargs:
       :return:
       """
       if view_kwargs.get('ticket_tag_id') or view_kwargs.get('access_code_id') or         view_kwargs.get('order_identifier'):
           self.schema = TicketSchemaPublic

   def query(self, view_kwargs):
       """
       query method for resource list
       :param view_kwargs:
       :return:
       """


       query_ = self.session.query(Ticket).filter_by(is_hidden=False)

 

Problem with this query was that this returned same response irrespective of who is logged in. Hence even the organizers were not able to modify hidden tickets because they were not returned by server.

Solution to this problem was to provide hidden tickets only to those who are organizer or are admin/super admins. For this we used the JWT token that was being sent from frontend in request headers for each authenticated request that was being made from frontend.

We modified the code to something like this:

class TicketList(ResourceList):
   """
   List Tickets based on different params
   """
   def before_get(self, args, view_kwargs):
       """
       before get method to get the resource id for assigning schema
       :param args:
       :param view_kwargs:
       :return:
       """
       if view_kwargs.get('ticket_tag_id') or view_kwargs.get('access_code_id') or view_kwargs.get('order_identifier'):
           self.schema = TicketSchemaPublic

   def query(self, view_kwargs):
       """
       query method for resource list
       :param view_kwargs:
       :return:
       """

       if 'Authorization' in request.headers:
           _jwt_required(current_app.config['JWT_DEFAULT_REALM'])
           if current_user.is_super_admin or current_user.is_admin:
               query_ = self.session.query(Ticket)
           elif view_kwargs.get('event_id') and has_access('is_organizer', event_id=view_kwargs['event_id']):
               query_ = self.session.query(Ticket)
           else:
               query_ = self.session.query(Ticket).filter_by(is_hidden=False)
       else:
           query_ = self.session.query(Ticket).filter_by(is_hidden=False)

 

Here we added some conditions which were used to check permission level of logged in user. After picking JWT token from request headers we check if the user was admin or super_admin, then we return all the tickets without any condition. Then we also check if the logged in user was organizer of event then also we send all the tickets without any conditions.

However if request comes from unauthenticated users (without valid token) or users with normal privileges, then we returned tickets whose isHidden property was set to False. The functions such as is_organizer and is_super_admin acted as helpers as they were imported from other helper files where they were defined.

Resources

Continue Reading

Ticket Details in the Open Event Android App

After entering all the attendee details and buying a ticket for an event the user expects to see the ticket so that he can use it later. This is why ticket details are shown in a separate fragment in the Open Event Android App. Let’s see how the tickets fragment was made in the Open Event Android App.

Two things that we require from the previous fragment are the event id and the order identifier so that we show the information related to the event as well as the order.

if (bundle != null) {
id = bundle.getLong(EVENT_ID, -1)
orderId = bundle.getString(ORDERS)
}

 

We are requesting data from the following two endpoints. In the first GET request we are passing the order identifier in the URL and we get the list of attendees from the server. In the second endpoint we simply pass the event identifier and get the event details from the server.

 

@GET("/v1/orders/{orderIdentifier}/attendees")
fun attendeesUnderOrder(@Path("orderIdentifier") orderIdentifier: String): Single<List<Attendee>>

@GET("/v1/events/{eventIdentifier}")
fun getEventFromApi(@Path("eventIdentifier") eventIdentifier: Long): Single<Event>

 

Here we are observing the attendees live data and adding the list of attendees returned from the server to the recyclerview so that we can show the user all the details of the attendees like the first name, last name etc. We then notify the adapter that the list of attendees have been added. In the end we log the number of attendees so that it is easier to debug in case there are any bugs.

orderDetailsViewModel.attendees.observe(this, Observer {
it?.let {
ordersRecyclerAdapter.addAll(it)
ordersRecyclerAdapter.notifyDataSetChanged()
}
Timber.d("Fetched attendees of size %s", ordersRecyclerAdapter.itemCount)
})

 

As mentioned earlier we need the event id and order identifier to show event and attendee related information to the user so here we are using the event id and appending it to the url. We are sending a GET request in a background thread and storing the list of events returned from the server in a mutable live data. In case of any errors we log it and show the error message to the user. Similarly we will use the order identifier to get the list of orders from the server and show it to the user.

compositeDisposable.add(eventService.getEventFromApi(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
event.value = it
}, {
Timber.e(it, "Error fetching event %d", id)
message.value = "Error fetching event"
}))

 

After fetching the list of attendees and event details, the only thing that we need to do is extract the important information and show it to the user so we pass the order and event objects to the ViewHolder. This can be done simply by using the attendee and event objects and accessing the fields required.

itemView.name.text = "${attendee.firstname} ${attendee.lastname}"
itemView.eventName.text = event?.name
itemView.date.text = "$formattedDate\n$formattedTime $timezone"

 

Resources

  1. ReactiveX official documentation: http://reactivex.io/
  2. Retrofit Android: http://square.github.io/retrofit/
  3. Google Android Developers Recycler View: https://developer.android.com/guide/topics/ui/layout/recyclerview
Continue Reading

Automating Play Store releases in Open Event Android with Fastlane

In Open Event Android we are using fastlane to automate the process of releasing the app to the Play Store, otherwise without this tool we would have to sign the release apk everytime before making a release but with fastlane all we need to do is just merge the development branch with the master branch and the updated app is released in Play Store.

The first thing that we need to do is encrypt the signing keys and upload it to the repository so we will create an encrypted tar file of the signing keys and upload it to the repository.

Now we want to decrypt the tar file everytime we merge our development branch into the master branch. We will create a bash script which does the above task which looks like this.

#!/bin/sh
set -e

export DEPLOY_BRANCH=${DEPLOY_BRANCH:-master}

if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_REPO_SLUG" != "fossasia/open-event-android" -o "$TRAVIS_BRANCH" != "$DEPLOY_BRANCH" ]; then
 echo "We decrypt key only for pushes to the master branch and not PRs. So, skip."
exit 0
fi

# Decrypt keys
openssl aes-256-cbc -K $encrypted_59a1db41ee4d_key -iv $encrypted_59a1db41ee4d_iv -in ./scripts/secrets.tar.enc -out ./scripts/secrets.tar -d
tar xvf ./scripts/secrets.tar -C scripts/

 

We need to define all these variables written after the $ sign as our Travis environment variables.

Next thing that we need to do is sign the apk so first thing that we check in the update-apk script is if we are on the master branch. We take the unsigned apk and sign it with the following commands.

 cp open-event-master-app-playStore-release-unsigned.apk open-event-master-app-playStore-release-unaligned.apk
jarsigner -verbose -tsa http://timestamp.comodoca.com/rfc3161 -sigalg SHA1withRSA -digestalg SHA1 -keystore ../scripts/key.jks -storepass $STORE_PASS -keypass $KEY_PASS open-event-master-app-playStore-release-unaligned.apk $ALIAS

 

The following command zipaligns the app

 ${ANDROID_HOME}/build-tools/27.0.3/zipalign -v -p 4 open-event-master-app-playStore-release-unaligned.apk open-event-master-app-playStore-release.apk

 

After signing the release apk, the final step is to publish the app to the playstore. The following command will install fastlane.

gem install fastlane

 

This command will take the signed apk and upload it in alpha channel of the playstore.

fastlane supply --apk open-event-master-app-playStore-release.apk --track alpha --json_key ../scripts/fastlane.json --package_name $PACKAGE_NAME

 

That’s it! Now releasing an update to the playstore is as simple as sending a pull request to the master branch. A process that would have otherwise required few minutes has been reduced to just seconds.

Resources

  1. Fastlane Official Site: https://fastlane.tools/
  2. Fastlane Android Documentaion: https://docs.fastlane.tools/getting-started/android/release-deployment/
  3. Fastlane Repository: https://github.com/fastlane/fastlane
Continue Reading

Building Open Event Android for F-Droid

According to the official website F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. Only apps that are open source and use only open source libraries are accepted in F-Droid. Let’s see what steps were taken to publish Open Event Android in F-Droid.

We need to build a different app for F-Droid because it cannot use any proprietary  libraries so instead of making a new app we made two flavors of the same app. One that would be published in playstore and the other for F-Droid. It is really easy to make different flavors of the same app. Just add the following lines in the build.gradle

flavorDimensions "default"

productFlavors {
fdroid {
dimension "default"
}

playStore {
dimension "default"
}
}

 

We specify the flavor dimension in the first line because all product flavors must have a flavor dimension and then we specify the name of our product flavors. That’s all that is required to make various flavors of an app after that we just need to remove the proprietary libraries from the F-Droid build.

When we have finished building the app we can start with the process of submitting the app to F-Droid. We need to send a Merge Request to the F-droid repository. Let’s see what all steps are required before we could submit our Merge Request to include our app in F-Droid

We need to clone the fdroid server directly from the source so that we have all the latest changes and set the path.

git clone https://gitlab.com/fdroid/fdroidserver.git
export PATH="$PATH:$PWD/fdroidserver"

 

Then clone your fork of the fdroiddata repository and open the directory. You need to write your own username here.

git clone https://gitlab.com/nikit19/fdroiddata.git
cd fdroiddata

 

The following command will make a template for you in the metadata directory, the name of the file is our application id. If you open the metadata folder you will see text files of all the apps that are published in F-Droid. If you want to publish an app in F-droid it is this file that needs to be sent in merge request. There are no other changes that needs to be done, we just have to fill the fields in the meta file correctly.

cp templates/app-minimal metadata/com.eventyay.attendee.txt

 

After filling all the details in the meta file we need to commit those changes. The following command will check if there are any syntax errors in the meta file.

fdroid readmeta

 

Finally we need to build the app. The following command builds the app from the source repository so if we have followed all the steps correctly, the app would build successfully and then we can send the merge request.

fdroid build -v -l com.eventyay.attendee

 

Resources

  1. Quick Start: https://gitlab.com/fdroid/fdroiddata/blob/master/README.md#quickstart
  2. Making merge requests: https://gitlab.com/fdroid/fdroiddata/blob/master/CONTRIBUTING.md#merge-requests

 

Continue Reading

Tickets fragment in the Open Event Android App

Ticketing is one of the most important part of any event management system. In the Open Event Android App you can view all the ticket details of any event. Let’s see how this was accomplished.

This is how our TicketApi class looks. We are sending a GET request to get the list of all tickets associated with an event. Id here is the event id which is used to uniquely identify an event.

interface TicketApi {

@GET("events/{id}/tickets?include=event&fields[event]=id&page[size]=0")
fun getTickets(@Path("id") id: Long): Flowable<List<Ticket>>

}

 

In the ticket details screen we see the event details on top. We are loading these details from the event which is stored in the database, we use the event id to query the database and load these details.

This is how the events are loaded from the database. We are observing the live data variable event and calling the loadEventDetails methods as soon as the value of the variable changes.

ticketsViewModel.event.observe(this, Observer {
it?.let { loadEventDetails(it) }
})

ticketsViewModel.loadEvent(id)

 

Let’s have a look at the loadEvent function that is there in our ViewModel. It only requires one parameter that is our event id. We first check if the id is correct or not then load the events in a background thread and report the error if there is any.

fun loadEvent(id: Long) {
if (id.equals(-1)) {
throw IllegalStateException("ID should never be -1")
}
compositeDisposable.add(eventService.getEvent(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
event.value = it
}, {
Timber.e(it, "Error fetching event %d", id)
error.value = "Error fetching event"
}))
}

 

The following function is used to load the event details with the date formatted in the appropriate manner. We first use the function getLocalizedDateTime to get the localized date in string and then format the date according to our needs.

private fun loadEventDetails(event: Event) {
rootView.eventName.text = event.name
rootView.organizerName.text = "by ${event.organizerName.nullToEmpty()}"
val dateString = StringBuilder()
val startsAt = EventUtils.getLocalizedDateTime(event.startsAt)
val endsAt = EventUtils.getLocalizedDateTime(event.endsAt)
rootView.time.text = dateString.append(EventUtils.getFormattedDate(startsAt))
.append(" - ")
.append(EventUtils.getFormattedDate(endsAt))
.append(" • ")
.append(EventUtils.getFormattedTime(startsAt))
}

 

That’s all that is needed to make the tickets screen. This how the fragment looks in the app.

Resources

  1. ReactiveX official documentation : http://reactivex.io/
  2. Vogella RxJava 2 – Tutorial : http://www.vogella.com/tutorials/RxJava/article.html
  3. Androidhive RxJava Tutorial : https://www.androidhive.info/RxJava/
Continue Reading

How App Social Links are specified in Open Event Frontend

This blog article will illustrate how the various social links are specified in the the footer of Open Event Frontend, using the settings API. Open Event Frontend, offers high flexibility to the admins regarding the settings of the App, and hence the media links are not hard coded, and can be changed easily via the admin settings panel.

The primary end point of Open Event API with which we are concerned with for fetching the settings  for the app is

GET /v1/settings

The model for settings has the following fields which concern the social links.

 googleUrl              : attr('string'),
 githubUrl              : attr('string'),
 twitterUrl             : attr('string')

Next we define them as segmented URL(s) so that they can make use of the link input widget.

segmentedTwitterUrl    : computedSegmentedLink.bind(this)('twitterUrl'),
 segmentedGoogleUrl     : computedSegmentedLink.bind(this)('googleUrl'),
 segmentedGithubUrl     : computedSegmentedLink.bind(this)('githubUrl'),

Now it is required for us to fetch the data from the API, by making the corresponding call to the API. Since the footer is present in every single page of the app, it is necessary that we make the call from the application route itself. Hence we add the following to the application route modal.

socialLinks: this.get('store').queryRecord('setting', {
})

Next we need to iterate over these social links, and add them to the footer as per their availability.So we will do so by first passing the model to the footer component, and then iterating over it in footer.hbs

{{footer-main socialLinks=model.socialLinks footerPages=footerPages}}


And thus we have passed the socialLinks portion of the model, under the alias socialLinks.Next, we iterate over them and each time check, if the link exists before rendering it.

<div class="three wide column">
     <div class="ui inverted link list">
       <strong class="item">{{t 'Connect with us'}}</strong>
       {{#if socialLinks.supportUrl}}
         <a class="item" href="{{socialLinks.supportUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="info icon"></i> {{t 'Support'}}
         </a>
       {{/if}}
       {{#if socialLinks.facebookUrl}}
         <a class="item" href="{{socialLinks.facebookUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="facebook f icon"></i> {{t 'Facebook'}}
         </a>
       {{/if}}
       {{#if socialLinks.youtubeUrl}}
         <a class="item" href="{{socialLinks.youtubeUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="youtube icon"></i> {{t 'Youtube'}}
         </a>
       {{/if}}
       {{#if socialLinks.googleUrl}}
         <a class="item" href="{{socialLinks.googleUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="google plus icon"></i> {{t 'Google +'}}
         </a>
       {{/if}}
     </div>
   </div>

Thus all the links in the app are easily manageable, from the admin settings menu, without the need of hard coding them. This approach also, makes it easy to preserve the configuration in a central location.

Resources

Continue Reading

Keeping Order of tickets in Event Wizard in Sync with API on Open Event Frontend

This blog article will illustrate how the various tickets are stored and displayed in order the event organiser decides  on  Open Event Frontend and also, how they are kept in sync with the backend.

First we will take a look at how the user is able to control the order of the tickets using the ticket widget.

{{#each tickets as |ticket index|}}
  {{widgets/forms/ticket-input ticket=ticket
  timezone=data.event.timezone
  canMoveUp=(not-eq index 0)
  canMoveDown=(not-eq ticket.position (dec
  data.event.tickets.length))
  moveTicketUp=(action 'moveTicket' ticket 'up')
  moveTicketDown=(action 'moveTicket' ticket 'down')
  removeTicket=(confirm 'Are you sure you  wish to delete this 
  ticket ?' (action 'removeTicket' ticket))}}
{{/each}}

The canMoveUp and canMoveDown are dynamic properties and are dependent upon the current positions of the tickets in the tickets array.  These properties define whether the up or down arraow or both should be visible alongside the ticket to trigger the moveTicket action.

There is an attribute called position in the ticket model which is responsible for storing the position of the ticket on the backend. Hence it is necessary that the list of the ticket available should always be ordered by position. However, it should be kept in mind, that even if the position attribute of the tickers is changed, it will not actually change the indices of the ticket records in the array fetched from the API. And since we want the ticker order in sync with the backend, i.e. user shouldn’t have to refresh to see the changes in ticket order, we are going to return the tickets via a computed function which sorts them in the required order.

tickets: computed('data.event.tickets.@each.isDeleted', 'data.event.tickets.@each.position', function() {
   return this.get('data.event.tickets').sortBy('position').filterBy('isDeleted', false);
 })

The sortBy method ensures that the tickets are always ordered and this computed property thus watches the position of each of the tickets to look out for any changes. Now we can finally define the moveTicket action to enable modification of position for tickets.

moveTicket(ticket, direction) {
     const index = ticket.get('position');
     const otherTicket = this.get('data.event.tickets').find(otherTicket => otherTicket.get('position') === (direction === 'up' ? (index - 1) : (index + 1)));
     otherTicket.set('position', index);
     ticket.set('position', direction === 'up' ? (index - 1) : (index + 1));
   }

The moveTicket action takes two arguments, ticket and direction. It temporarily stores the position of the current ticket and the position of the ticket which needs to be swapped with the current ticket.Based on the direction the positions are swapped. Since the position of each of the tickets is being watched by the tickets computed array, the change in order becomes apparent immediately.

Now when the User will trigger the save request, the positions of each of the tickets will be updated via a PATCH or POST (if the ticket is new) request.

Also, the positions of all the tickets maybe affected while adding a new ticket or deleting an existing one. In case of a new ticket, the position of the new ticket should be initialised while creating it and it should be below all the other tickets.

addTicket(type, position) {
     const salesStartDateTime = moment();
     const salesEndDateTime = this.get('data.event.startsAt');
     this.get('data.event.tickets').pushObject(this.store.createRecord('ticket', {
       type,
       position,
       salesStartsAt : salesStartDateTime,
       salesEndsAt   : salesEndDateTime
     }));
   }

Deleting a ticket requires updating positions of all the tickets below the deleted ticket. All of the positions need to be shifted one place up.

removeTicket(deleteTicket) {
     const index = deleteTicket.get('position');
     this.get('data.event.tickets').forEach(ticket => {
       if (ticket.get('position') > index) {
         ticket.set('position', ticket.get('position') - 1);
       }
     });
     deleteTicket.deleteRecord();
   }

The tickets whose position is to be updated are filtered by comparison of their position from the position of the deleted ticket.

Resources

Continue Reading
  • 1
  • 2
Close Menu
%d bloggers like this: