Showing Order Details in Eventyay Organizer Android App

In Open Event Organizer App, the organizer was not able to view the details for the Orders received from attendees for his/her events. So in this blog we’ll see how we implemented this functionality in the Orga App.

Specifications

There is a fragment showing the list of all orders for that event. The user will be able to click on order from the list which will then take the user to another fragment where Order details will be displayed. We will be following MVVM architecture to implement this functionality using REST API provided by Open Event Server. Let’s get started.

Firstly, we will create Order Model class. This contains various fields and relationship attributes to setup the table in database using RazizLabs DbFlow annotations.

Then, We will make a GET request to the server using Retrofit 2  to fetch Order object.

@GET(“orders/{identifier}?include=event”)
Observable<Order> getOrder(@Path(“identifier”) String identifier);

The server will return the Order details in form of a Order object and then we will save it in local  database so that when there is no network connectivity then also we can show data to the user and user can refresh to fetch the latest data from network. The network observable handles fetching data from network and disk observable handles saving data in local database.

@NonNull
@Override
public Observable<Order> getOrder(String orderIdentifier, boolean reload) {
Observable<Order> diskObservable = Observable.defer(() ->
repository
.getItems(Order.class, Order_Table.identifier.eq(orderIdentifier)).take(1)
);

Observable<Order> networkObservable = Observable.defer(() ->
orderApi.getOrder(orderIdentifier)
.doOnNext(order -> repository
.save(Order.class, order)
.subscribe()));

return repository
.observableOf(Order.class)
.reload(reload)
.withDiskObservable(diskObservable)
.withNetworkObservable(networkObservable)
.build();
}

Now, we will make a Fragment class that will bind the layout file to the model in the onCreateView method using DataBindingUtil. Further, we will be observing on ViewModel to reflect changes of Order, Progress and Error objects in the UI in the onStart method of the Fragment.

public class OrderDetailFragment extends BaseFragment implements OrderDetailView {

@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, R.layout.order_detail_fragment, container, false);

orderDetailViewModel = ViewModelProviders.of(this, viewModelFactory).get(OrderDetailViewModel.class);

return binding.getRoot();
}

@Override
public void onStart() {
super.onStart();
setupRefreshListener();

orderDetailViewModel.getOrder(orderIdentifier, eventId, false).observe(this, this::showOrderDetails);
orderDetailViewModel.getProgress().observe(this, this::showProgress);
orderDetailViewModel.getError().observe(this, this::showError);
}
}

Next, we will create OrderDetailsViewModel.This is the ViewModel class which interacts with the repository class to get data and the fragment class to show that data in UI.

Whenever the user opens Order details page, the method getOrder() twill be called which will request an Order object from OrderRepository, wrap it in MutableLiveData and provide it to Fragment.

Using MutableLiveData to hold the data makes the data reactive i.e. changes in UI are reflected automatically when the object changes. Further, we don’t have to worry handling the screen rotation as LIveData handles it all by itself.

  public LiveData<Order> getOrder(String identifier, long eventId, boolean reload) {
if (orderLiveData.getValue() != null && !reload)
return orderLiveData;

compositeDisposable.add(orderRepository.getOrder(identifier, reload)
.compose(dispose(compositeDisposable))
.doOnSubscribe(disposable -> progress.setValue(true))
.doFinally(() -> progress.setValue(false))
.subscribe(order -> orderLiveData.setValue(order),
throwable -> error.setValue(ErrorUtils.getMessage(throwable))));

if (!reload) {
getEvent(eventId);
}

return orderLiveData;
}
}

References

  1. Codebase for Open Event Orga App https://github.com/fossasia/open-event-orga-app
  2. Official documentation for LiveData Architecture Component https://developer.android.com/topic/libraries/architecture/livedata
  3. Official Github Repository of Retrofit  https://github.com/square/retrofit
  4. Official Github Repository for RxJava https://github.com/ReactiveX/RxJava
Continue ReadingShowing Order Details in Eventyay Organizer Android App

Working with Route Hooks in Badgeyay

Badgeyay is an open source project developed by FOSSASIA Community to generate badges for conferences and events through a simple user interface.

In this post I am going to explain about the route lifecycle hooks in ember and how we have utilized one lifecycle component to reset the controller state in badge generation form. In Ember every entity has some predefined set of methods, that it goes through the existence of the application. Route is not different from it. Our main goal is to restore the state of the controller every time we entered into a route, so that we receive a clean and new instance and not the previous state. The hook that fits in the situation is setupController(). This method is called after model() hook to set the controller state in the route. We will restore the variables in the controller here in this method to reset it to original state. This will help in removing the messages and progress on the previous state of the page on a new visit.

Procedure

  1. Open the route, where we want to override the hook, and create a method setupController() this will call the base hook and override its behaviour.
setupController(controller, model) {
  this._super(…arguments);
  set(controller, ‘defImages’, model.def_images);
  set(controller, ‘user’, model.user);
  this.set(‘controller.badgeGenerated’, false);
  this.set(‘controller.showProgress’, false);
}

 

As we can see in the method, it first initialises the super constructor and then we are writing the logic for the reset state. This will reset the badgeGenerated and showProgress variable in the controller to mentioned values.

  1. Getting the generated badge link from the last promise resolved to set the value of the variable in the controller action.
sendBadge(badgeData) {

        this.set(‘badgeGenerated’, true);

  },

 

This will set the value of the variable to the received object from backend.

  1. Showing the content in frontend based on the values of the variable. When we initially visit the route it is set to as false in the setupController() hook and is changed later on through some promise resolvement in an action.
{{#if badgeGenerated}}

 . . .


{{/if}}

 

This content will be only be shown in the present state and when the user revisits the route the state will be resetted.

Pull Request for the respective issue – https://github.com/fossasia/badgeyay/pull/1313

Resources

Continue ReadingWorking with Route Hooks in Badgeyay

Share Events in the Open Event Organizer Android App

In the Open Event Organizer Android App, after creating an event the organizer was unable to share it. We handled this challenge and came up with options to share Event with other social media apps. Along with that user can send Email to users containing event description with just a click. All this through a UI that our user will love interacting with. Let’s see how we implemented this.

Specifications

We designed a UI given below which offer four functionalities to the user in a single screen.

  1. The Event Name and Date are shown in the CardView.
  2. User can copy Event External URL by clicking on Copy URL option.
  3. User can send Email containing information about the Event like ( Name, Description, Starting and Ending Date-Time for the Event) via Email by clicking on Email option.
  4. User can share the same information described in point three via other social media/chatting apps etc by clicking on many more option.

The is the Event Model class ia a POJO containing the associated attributes.

We will use Retrofit to fetch Event object from server through a GET request in EventApi class.

@GET(“events/{id}?include=tickets”)
Observable<Event> getEvent(@Path(“id”) long id);

Then we will use the getEvent method of EventRepositoryImpl class to make the request for us using EventApi class and then pass on the Response Event object to the ViewModel by wrapping it in RxJava Observable. We are accepting a boolean field named reload which decdes whether we need to reuse the existing Event object from Local Database  or fetch a new object from server.

@Override
public Observable<Event> getEvent(long eventId, boolean reload) {
Observable<Event> diskObservable = Observable.defer(() ->
repository
.getItems(Event.class, Event_Table.id.eq(eventId))
.filter(Event::isComplete)
.take(1)
);

Observable<Event> networkObservable = Observable.defer(() ->
eventApi
.getEvent(eventId)
.doOnNext(this::saveEvent));

return repository.observableOf(Event.class)
.reload(reload)
.withDiskObservable(diskObservable)
.withNetworkObservable(networkObservable)
.build();
}

In ShareEventVewModel, we are calling the getEvent from EventRepositoryImpl class, construct a LiveData object from it so that UI could observe changes on it. Methods getShareableInformation, getShareableUrl, getShareableSubject provide the shareable information to the UI which is further shared with other apps.

public class ShareEventViewModel extends ViewModel {

protected LiveData<Event> getEvent(long eventId, boolean reload) {
if (eventLiveData.getValue() != null && !reload)
return eventLiveData;

compositeDisposable.add(eventRepository.getEvent(eventId, reload)
.doOnSubscribe(disposable -> progress.setValue(true))
.doFinally(() -> progress.setValue(false))
.subscribe(event -> {
this.event = event;
eventLiveData.setValue(event);
},
throwable -> error.setValue(ErrorUtils.getMessage(throwable))));

return eventLiveData;
}

public String getShareableInformation() {
return Utils.getShareableInformation(event);
}

}

In ShareEventFragment class does the work of binding the UI to the model using data binding. It observes the LiveData objects supplied by presenter and reflect the changes in UI to the LiveData object.

public class ShareEventFragment extends BaseFragment implements ShareEventView {

public void shareEvent() {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, shareEventViewModel.getShareableInformation());
shareIntent.setType(“text/plain”);
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.send_to)));
}

public void shareByEmail() {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SENDTO);
shareIntent.setType(“message/rfc822”);
shareIntent.setData(Uri.parse(“mailto:”));
shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareEventViewModel.getEmailSubject());
shareIntent.putExtra(Intent.EXTRA_TEXT, shareEventViewModel.getShareableInformation());
try {
startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.send_to)));
} catch (android.content.ActivityNotFoundException ex) {
ViewUtils.showSnackbar(binding.getRoot(), “There are no email clients installed”);
}
}

public void copyUrlToClipboard() {
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
String eventUrl = shareEventViewModel.getShareableUrl();
if (eventUrl == null) {
ViewUtils.showSnackbar(binding.getRoot(), “Event does not have a Public URL”);
} else {
ClipData clip = ClipData.newPlainText(“Event URL”, shareEventViewModel.getShareableUrl());
clipboard.setPrimaryClip(clip);
ViewUtils.showSnackbar(binding.getRoot(), “Event URL Copied to Clipboard”);
}
}
}

The layout file contains the Event object bind to the UI using Two way Data Binding. Here is an extract from the layout file. For viewing entire file, please refer here.

References

  1. Official documentation of Retrofit 2.x http://square.github.io/retrofit/
  2. Official documentation for RxJava 2.x https://github.com/ReactiveX/RxJava
  3. Official documentation for ViewModel https://developer.android.com/topic/libraries/architecture/viewmodel
  4. Codebase for Open Event Organizer App https://github.com/fossasia/open-event-orga-app
Continue ReadingShare Events in the Open Event Organizer Android App

Change Password Feature for Open Event Android Organizer App

In Open Event Organizer Android App, the users were able to successfully login and sign up but in case they wanted to change their login password they could not. So, we added a feature to allow users to change their existing password. This blog explains the technical details to implement this feature following MVVM architecture and using highly efficient libraries like Retrofit, RxJava, Raziz Labs DbFlow, Data Binding.

Specifications

We will implement a page where users can enter their old password and new password along with a confirm password field. Their will be a login button to send the password change request to server. Server then return a response and we will provide feedback regarding the request. We are following MVP architecture so there will be a Model class, Fragment class, Presenter class and Network Layer to make network requests.

Let’s start with creating ChangePassword model class. There are three fields to store old password, new password and new confirmed password. Several Lombok annotations like @Data, @AllArgsConstructor, @NoArgsConstructor are used to avoid boilerplate code for getters, setters and constructors. @JsonNaming annotation is used to translate the Java Object names to KebabCase when they are serialized.

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy.class)
public class ChangePassword {

public String oldPassword;
public String newPassword;

@JsonIgnore
public String confirmNewPassword;
}

The layout file is binded to model using Data Binding. There will be three TextInputEditText fields for user input. An option to toggle password visibility and a login button.

The Fragment class binds layout file to the Fragment and handle UI stuff. Presenter is called to make Login request when login button is pressed.

public class ChangePasswordFragment extends BaseFragment<ChangePasswordPresenter> implements ChangePasswordView {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, R.layout.change_password_fragment, container, false);
validator = new Validator(binding);

AppCompatActivity activity = ((AppCompatActivity) getActivity());
activity.setSupportActionBar(binding.toolbar);

ActionBar actionBar = activity.getSupportActionBar();
if (actionBar != null) {
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
}

return binding.getRoot();
}

@Override
public void onStart() {
super.onStart();
getPresenter().attach(this);
binding.setOrganizerPassword(getPresenter().getChangePasswordObject());
getPresenter().start();

binding.btnChangePassword.setOnClickListener(view -> {
if (!validator.validate())
return;

String url = binding.url.baseUrl.getText().toString().trim();
getPresenter().setBaseUrl(url, binding.url.overrideUrl.isChecked());
getPresenter().changePasswordRequest(binding.oldPassword.getText().toString(),
binding.newPassword.getText().toString(),
binding.confirmNewPassword.getText().toString());

});
}

When the Login button is pressed, changePasswordRequest() method is called which makes an asynchronous call to ChangePasswordModel in order to perform the task of sending and receiving data from network in a different thread than the UI thread. Along with making requests, this method also verifies the password typed in confirm password field and send the the error as well as success message to the fragment.

public class ChangePasswordPresenter extends AbstractBasePresenter<ChangePasswordView> {

public void changePasswordRequest(String oldPassword, String newPassword, String confirmPassword) {
if (!newPassword.equals(confirmPassword)) {
getView().showError(“Passwords Do Not Match”);
return;
}

organizerPasswordObject.setOldPassword(oldPassword);
organizerPasswordObject.setNewPassword(newPassword);
organizerPasswordObject.setConfirmNewPassword(confirmPassword);

changePasswordModel.changePassword(organizerPasswordObject)
.compose(disposeCompletable(getDisposable()))
.compose(progressiveErroneousCompletable(getView()))
.subscribe(() -> getView().onSuccess(“Password Changed Successfully”), Logger::logError);
}
}

We are using Retrofit to make POST Request to server using the REST API. @Body annotation denotes the object request body which here contains a Map<String, ChangePassword> object. The Response from server is captured in Observable<ChangePasswordResponse> which is an RxJava Observable.

@POST(“auth/change-password”)
Observable<ChangePasswordResponse> changePassword(@Body Map<String, ChangePassword> changePassword);

This is the declaration for the method in Network Layer where the actual network request is made. It takes as input the changePassword object from Presenter which is already binded with data. Then it uses RxJava to asynchronously call the Api class and pass in the Map<String, ChangePassword> object. The result is then processed and Completable object is returned to the presenter. The Presenter processes the Completable object and shows user feedback in the form of a message in SnackBar.

References

  1. Official documentation for RxJava by ReactiveX https://github.com/ReactiveX/RxJava
  2. Official documentation for Retrofit by Square Inc https://github.com/square/retrofit
  3. Codebase for Open Event Organizer App on Github https://github.com/fossasia/open-event-orga-app
  4. Open Event Server deployment at heroku https://open-event-api-dev.herokuapp.com/
Continue ReadingChange Password Feature for Open Event Android Organizer App

Create Session in Open Event Android Organizer Application

Open Event Android Organizer Application offered variety of features to Organizers from all over the world to help them host their Events globally but it didn’t had the functionality to create Sessions in the app itself and associate it to Tracks. This feature addition was crucial since it enables Organizer to create Sessions which every common person enquires about before attending and event. In this Blog Post we will see how we added this functionality in the app.

Open Event Android Organizer Application is a client for Open Event Server which provides the REST API.

Problem

There can be various Sessions associated with Tracks for an Event. Open Event API had the endpoint to implement Creating Session but the Orga app didn’t, so we worked on creating a Session in the app.

The UI for creating a Session is shown above. User can fill in the necessary details and click on the green Floating Action Button to create a Session.

How to implement functionality?

We will follow MVP Architecture and use Retrofit 2.x, RxJava, Dagger, Jackson, Data Binding and other industry standard libraries to implement this functionality.

Firstly, let’s create Session model class specifying the attributes and relationships to set up in database using RazizLabs DbFlow library. The POJO will be serialized into JSON by Jackson library to be passed on as a part of RequestBody to server.

Now we will create SessionApi class that will contain the request details to be passed to Retrofit. @POST denotes a POST request and @Body denotes the requestBody of the request which is a Session object.

public interface SessionApi {
@POST(“sessions”)
Observable<Session> postSession(@Body Session session);
}

This is the CreateSessionFragment class that contains the code binding model to the view. The Fragment class implements the CreateSessionView class overriding the method declarations present there. The @Inject annotation of Dagger is used to load singleton presenter instance lazily to improve app’s performance.

Event-Id and Track-Id’s are retrieved from Bundle from Fragment Transaction. These are then passed on to presenter when Create Session button is pressed. There are other methods to show binding progressbar, snackbar and other UI components to show progress of the background request to server and database.

public class CreateSessionFragment extends BaseFragment<CreateSessionPresenter> implements CreateSessionView {

@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup

container, @Nullable Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater,

R.layout.session_create_layout, container, false);
validator = new Validator(binding.form);

binding.sessionCreate.setOnClickListener(view -> {
if (validator.validate()) {
getPresenter().createSession(trackId, eventId);
}
});

return binding.getRoot();
}

@Override
public void onStart() {
super.onStart();
getPresenter().attach(this);
binding.setSession(getPresenter().getSession());
}
}

In the Presenter createSession method is called when create button is pressed in UI. The method attaches track-id and event-id to Session object. This is necessary for Relationship constraints on Session Model. Then after binding all the data to Session object, we pass it on to SessionRepository. The success response is provided to user by passing success response in getView().onSuccess() method.

public class CreateSessionPresenter extends

AbstractBasePresenter<CreateSessionView> {

public Session getSession() {
return session;
}

public void createSession(long trackId, long eventId) {

Track track = new Track();
Event event = new Event();

track.setId(trackId);
event.setId(eventId);
session.setTrack(track);
session.setEvent(event);

sessionRepository
.createSession(session)
.compose(dispose(getDisposable()))
.compose(progressiveErroneous(getView()))
.subscribe(createdSession ->

getView().onSuccess(“Session Created”), Logger::logError);
}
}

The SessionRepository uses RxJava to make asynchronous Retrofit Call to Server. We throw a Network Error to user if the device does not have Internet Connectivity.

The session object accepted as a parameter in createSession method is passed on to sessionApi. It will return Observable<Session> Response which we will process in doOnNext() method. Then the Session object along with required foreign key relationships with Track and Event is saved in database for offline use.

@Override
public Observable<Session> createSession(Session session) {
if (!repository.isConnected()) {
return Observable.error(new Throwable(Constants.NO_NETWORK));
}return sessionApi
.postSession(session)
.doOnNext(created -> {
created.setTrack(session.getTrack());
created.setEvent(session.getEvent());
repository
.save(Session.class, created)
.subscribe();
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}

The above code snippets are from Open Event Orga Application. For exploring the entire codebase please refer here. For details about the REST API used by the app please visit here.

References

  1. Official RxJava Project on Github by ReactiveX https://github.com/ReactiveX/RxJava.
  2. Official Retrofit Project on Github by Square Inc https://github.com/square/retrofit.
  3. Official Open Event Organizer App on Github by FOSSASIA https://github.com/fossasia/open-event-orga-app.
  4. Documentation for REST API of Open Event Server on Heroku by FOSSASIA https://open-event-api-dev.herokuapp.com/.
Continue ReadingCreate Session in Open Event Android Organizer Application

Adding Marketer and Sales Admin Events Relationship with User on Open Event Server

In this blog, we will talk about how to add API for adding and displaying events in with a user acts as a Marketer and/or Sales Admin on Open Event Server. The focus is on Model Updation and Schema updation of User.

Model Updation

For the Marketer and Sales Admin events, we’ll update User model as follows

Now, let’s try to understand these relationships.

In this feature, we are providing user to act as a marketer and sales admin for a event.

  1. Both the relationships will return the events in which the user is acting as a Marketer and/or Sales Admin.
  2. There are two custom system roles in model CustomSysRole which are Marketer and Sales Admin. A user can act as these custom system roles with respect to an event.
  3. In this relationship, we will return those events from UserSystemRole model in which a user is acting as Marketer Custom System Role and Sales Admin Custom System Role.
  4. We make use of Event and join UserSystemRole and CustomSysRole where we use that user where UserSystemRole.user_id == User.id , CustomSysRole.id == UserSystemRole.role_id, CustomSysRole.name == “Sales Admin” and then we return events in which Event.id == UserSystemRole.event_id
  5. Similarly, for Marketer events we make use of Event and join UserSystemRole and CustomSysRole where we use that user where UserSystemRole.user_id == User.id , CustomSysRole.id == UserSystemRole.role_id, CustomSysRole.name == “Marketer” and then we return events in which Event.id == UserSystemRole.event_id

Schema Updation

For the Marketer and Sales Admin events, we’ll update UserSchema as follows

Now, let’s try to understand this Schema.

In this feature, we are providing user to act as a marketer and sales admin for a event.

  1. For displaying marketer_events relation self_view is displayed by API v1.user_marketer_events and collection of these events is displayed by API v1.event_list
  2. These APIs will return the Events as schema=”EventSchema”. Here, many=True tells us that this is One to many relationship with Events model.

So, we saw how an user can act as a marketer and/or sales admin for many events.

Resources

Continue ReadingAdding Marketer and Sales Admin Events Relationship with User on Open Event Server

Adding Custom System Roles in Open Event Server

In this blog, we will talk about how to add different custom system roles concerning a user on Open Event Server. The focus is on its model and Schema updation.

Model Updation

For the User Table, we’ll update our User Model as follows:

Now, let’s try to understand these hybrid properties.

In this feature, we are providing Admin the rights to see whether a user is acting as a Marketer and / or  Sales Admin of any of the event or not. Here, _is__system_role method is used to check whether an user plays a system role like Marketer, Sales Admin or not. This is done by querying the record from UserSystemRole model. If the record is present then the returned value is True otherwise false.

Schema Updation

For the User Model, we’ll update our Schema as follows:

Now, let’s try to understand this Schema.

Since all the properties will return either True or false so these all properties are set to Boolean in Schema.Here dump_only means, we will return this property in the Schema.

So, we saw how User Model and Schema is updated to show custom system roles concerning a user on Open Event Server.

Resources

Continue ReadingAdding Custom System Roles in Open Event Server

Adding a Countdown to Orders Page in Open Event Frontend

This blog post will illustrate how you can add a countdown to orders page which on finishing expires the ticket in Open Event. In Open Event we allow some predefined time for users to fill in their details and once the time expires order gets expired and tickets get released. Users can order the tickets again if they want.

We start by adding a createdAt field to orders model so that we can keep track of remaining time. To calculate the time when the order should expire we add predefined time in which user should fill their details to createdAt time. In this way, we get the time when the order will expire.

So now to calculate the remaining time we just subtract the expiring time from current time. And then we render this data into the template. We define getRemainingTime property in our template and fetch the data for that property with help of javascript.

To see the template code visit this link.

The challenge here is to update the time remaining after every second. For this, we take the help of ember runloop. The run.later() function of ember runloop helps us to calculate the property after every second and set it. Code for setting the remaining time with the help of javascript is given below.

// app/components/forms/orders/order-form.js

getRemainingTime: computed('data', function() {
    let willExpireAt = this.get('data.createdAt').add(10, 'minutes');
    this.timer(willExpireAt, this.get('data.identifier'));
  }),

  timer(willExpireAt, orderIdentifier) {
    run.later(() => {
      let currentTime = moment();
      let diff = moment.duration(willExpireAt.diff(currentTime));
      this.set('getRemainingTime', moment.utc(diff.asMilliseconds()).format('mm:ss'));
      if (diff > 0) {
        this.timer(willExpireAt, orderIdentifier);
      } else {
        this.get('data').reload();
        this.get('router').transitionTo('orders.expired', orderIdentifier);
      }
    }, 1000);
  }

 

As given in the code. We pass expiring time and order’s model instance to the timer function. Timer function calculates the remaining time and sets it to getRemainingTime property of template. Timer function runs after every second with the help of run.later() function of ember runloop. To format the remaining time into MM:SS we take help of moment.js library and format the data accordingly.

Once the remaining time is less than zero (time expires) we reload the model and transition current route to expired route. We do not have to set order status as expired inside the FE. Server sets the order as expired after the predefined time. So we just reload the model from the server and we get the updated status of the order.

Resources:
Continue ReadingAdding a Countdown to Orders Page in Open Event Frontend

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 ReadingAdding Helper and Adding Action Buttons to Orders List in Open Event Frontend

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 ReadingHow to Make Promotional Codes Applicable on Tickets During Ordering in Open Event Frontend