Creating Orders in Open Event Android

An Order is generated whenever a user buys a ticket in Open Event Android. It contains all the details regarding the tickets and their quantity, also information regarding payment method and relation to list of attendees, through this blog post we will see how Orders are generated in Open Event Android. Implementing Order system can be divided into following parts

  • Writing model class to serialize/deserialize API responses
  • Creating TypeConverter for Object used in Model class
  • Creating the API interface method
  • Wiring everything together

Model Class

Model class server two purpose –

  • Entity class for storing orders in room
  • Serialize / Deserialize API response

The architecture of the Order Model Class depends upon the response returned by the API, different fields inside the Entity Class defines what different attributes an Order consists of and their data types. Since every Order has a relationship with Event and Attendee we also have to define foreign key relations with them. Given below is the implementation of the Order Class in Open Event Android.

@Type(“order”)
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
@Entity(foreignKeys = [(ForeignKey(entity = Event::class, parentColumns = [“id”], childColumns = [“event”], onDelete = ForeignKey.CASCADE)), (ForeignKey(entity = Attendee::class, parentColumns = [“id”], childColumns = [“attendees”], onDelete = ForeignKey.CASCADE))])
data class Order(
       @Id(IntegerIdHandler::class)
       @PrimaryKey
       @NonNull
       val id: Long,
       val paymentMode: String? = null,
       val country: String? = null,
       val status: String? = null,
       val amount: Float? = null,
       val orderNotes: String? = null,
       @ColumnInfo(index = true)
       @Relationship(“event”)
       var event: EventId? = null,
       @Relationship(“attendees”)
       var attendees: List<AttendeeId>? = null
)

 

We are using Jackson for serializing/deserializing JSON response, @Type(“order”) annotation tells jackson that the following object is for key order in json response. Since we are using this as our room entity class we will also have to add a @Entity annotation to this class. Order contains attendee and event id fields which are foreign keys to other entity classes, this also has to be explicitly mentioned while writing the @Entitty annotation as shown in the snippet above . All relationship must be annotated with @Relationship annotation. All the variables serves as attributes in the order table and key name for json conversions.

The fields of this class are the attributes for the Order Table. Payment mode, country, status are all made up of primitive data type and hence require no type convertors whereas we will have to specify type converter for objects like eventId and List<Attendees>

Type Converter

Type Converter allows us to store any custom object type inside room database. Essentially we break down the Object into smaller primitive data types that Object is composed of and which can be stored by room database.

To create a TypeConverter we have to add a @TypeConverter annotation to it, this tells room that this is a special function. For every custom Object, you have to create two different TypeConverter functions. One takes the Object and converts it into primitive data type and the other takes the primitive data type and constructs your custom Object from it. For the Order data class we discussed in the above section we will need two type converters, for EventId and List<Attendee> objects. We will take the example of List<Attendee>

class ListAttendeeIdConverter {
   @TypeConverter
   fun fromListAttendeeId(attendeeIdList: List<AttendeeId>): String {
       val objectMapper = ObjectMapper()
       return objectMapper.writeValueAsString(attendeeIdList)
   }
   @TypeConverter
   fun toListAttendeeId(attendeeList: String): List<AttendeeId> {
       val objectMapper = ObjectMapper()
       val mapType = object : TypeReference<List<AttendeeId>>() {}
       return objectMapper.readValue(attendeeList, mapType)
   }

A type converter shows how we can store an object in the form of primitive data type by performing some operation on it. Here we can see that List<AttendeeId> Object is converted into String (primitive data type) using jackson object mapper and similarly we will have to restore or recreate the List<AttendeeId> Object from the string converted output of the same. The first function fromListAttendeeId deals with converting Object into string type and toListAttendeeId deals with converting string output to List<AttendeeId> type Object.

Not that we have created a TypeConverter for our custom Object type we have to add it to the Open Event Database. This can be done by simply adding it to @TypeConverters list separated by commas as shown below.

@TypeConverters(EventIdConverter::class, EventTopicIdConverter::class, TicketIdConverter::class, AttendeeIdConverter::class, ListAttendeeIdConverter::class)

API Interface Method

Till now we have seen how Order body looks like and how we can store it in room database but we would also need an Order API class which specifies which endpoint to hit and what body and response type it is expecting.

Given below is the placeOrder function which hits the Order endpoint (baseURL/orders), with the body as an Order and returns a Single<Order> as a response. Since we are using retrofit for making network requests endpoint path is simply added inside @Path annotation and the body can be passed in parameters of the function using @Body annotation.

interface OrderApi {
   @POST(“orders”)
   fun placeOrder(@Body order: Order): Single<Order>
}

OrderService is the class which exposes the OrderApi functions and make it available to its ViewModel.

The placeOrder function inside the service class takes Order as body parameter, when this function is called it makes a call to the API function to place an order with the passed parameter the response of which is inserted into the database (caching of Orders) also returns the same value

class OrderService(private val orderApi: OrderApi, private val orderDao: OrderDao) {
   fun placeOrder(order: Order): Single<Order> {
       return orderApi.placeOrder(order)
               .map {
                   orderDao.insertOrder(it)
                   it
               }
   }

To create an Order from the Fragment or Activity one call implement the following function createOrder.

createOrder calls the service layer function placeOrder and subscribes to this observable. Here we are observing on main thread because all the UI related changes has to be done from the main thread. As soon as we subscribe to the function we also sets progress.value = true this allows us to show a progress bar on the UI this is changed to false once the response is received (see doFinally).

You can find the following function in the AttendeeViewModel class in Open Event Android project

fun createOrder(order: Order) {
       compositeDisposable.add(orderService.placeOrder(order)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe {
                   progress.value = true
               }.doFinally {
                   progress.value = false
               }.subscribe({
                   message.value = “Order created successfully!”
                   Timber.d(“Success placing order!”)
               }, {
                   message.value = “Unable to create Order!”
                   Timber.d(it, “Failed creating Order”)
               }))
   }

Whenever user fills in details of all the attendees sequence of calls and methods are invoked. Firstly attendees are created for the event with ticket details and the data as provided from the UI. On the successful generation of all the attendees ie when total ticket quantity equals the no of attendees an order object is generated with attendees as the list of attendee previously generated and other details as required, this is then passed to createOrder function which internally interacts with the service layer function to create Order.

Resources

Continue ReadingCreating Orders in Open Event Android

Giving users option to switch between All and Reviewed Only Skills on SUSI Skill CMS

There are a lot of Skills on SUSI Skill CMS. Any registered user has the access to creating his/her own Skills. Hence, we need to give the users an option on SUSI Skill CMS whether they want to see all the Skills, or only those Skills that have been tested thoroughly and have been approved by the Admin and higher user roles. This blog post explains how this feature has been implemented on the SUSI Skill CMS.

How is review status of any Skill changed on the server?

The API endpoint which allows Admin and higher user roles to change the review status of any Skill on the server is ‘/cms/changeSkillStatus.json’. It takes the following parameters:

  • model: Model of the Skill
  • group: Group of the Skill
  • language: Language of the Skill
  • skill: Skill name
  • reviewed: A boolean parameter which if true, signifies that the Skill has been approved.

Sample API call:

https://api.susi.ai/cms/changeSkillStatus.json?model=general&group=Knowledge&language=en&skill=aboutsusi&reviewed=true&access_token=yourAccessToken

 

Fetching reviewed only Skills from the server

The ‘/cms/getSkillList.json’ endpoint has been modified to facilitate returning only the Skills whose review status is true. This is done by the following API call:

https://api.susi.ai/cms/getSkillList.json?reviewed=true

 

Creating checkbox to switch between All and Reviewed Only Skills

Checkbox is one of the many Material-UI components. Hence, we need to first import it before we can use it directly in our BrowseSkill component.

import Checkbox from 'material-ui/Checkbox';

 

In the constructor of the BrowseSkill class, we set states for two variables as follows:

constructor(props) {
  super(props);
    this.state = {
      // other state variables
      showSkills: '',
      showReviewedSkills: false,
    };
}

 

In the above code for the constructor, we have set two state variables. ‘showSkills’ is a string which can either be an empty string, or ‘&reviewed=true’. We want to append this string to the ‘/cms/getSkillList.json’ API call because it would determine whether we want to fetch All Skills or reviewed only Skills. The second variable ‘showReviewedSkills’ is a boolean used to keep record of the current state of the page. If it is true, then it means that currently, only the reviewed Skills are being displayed on the CMS site.

Implementation of the checkbox

This is how the Checkbox has been implemented for the purpose of switching between All and Reviewed Only Skills:

 <Checkbox
    label="Show Only Reviewed Skills"
    labelPosition="right"
    className="select"
    checked={this.state.showReviewedSkills}
    labelStyle={{ fontSize: '14px' }}
    iconStyle={{ left: '4px' }}
    style={{
      width: '256px',
      paddingLeft: '8px',
      top: '3px',
    }}
    onCheck={this.handleShowSkills}
  />

 

As can be seen from the above code, the initial state of the checkbox is unchecked as initially, the value of the state variable ‘showReviewedSkills’ is set to false in the constructor. This means that initially all Skills will be shown to the user. On clicking on the checkbox, handleShowSkills() function is called. Its implementation is as follows:

  handleShowSkills = () => {
    let value = !this.state.showReviewedSkills;
    let showSkills = value ? '&reviewed=true' : '';
    this.setState(
      {
        showReviewedSkills: value,
        showSkills: showSkills,
      },
      function() {
        this.loadCards();
      },
    );
  };

 

In the handleShowSkills() function, firstly we store the current value of the state variable ‘showReviewedSkills’. The value of ‘showSkills’ string is determined according to the value of ‘showReviewedSkills’. Then the states of both these variables are updated in the setState() function. Lastly, loadCards() function is called.

In the loadCards() function, we append the value of the state variable ‘showSkills’ to the AJAX call to the ‘/cms/getSkillList.json’ endpoint. The URL used for the API call is as follows:

https://api.susi.ai/cms/getSkillList.json?group=' +
  this.props.routeValue +
  '&language=' +
  this.state.languageValue +
  this.state.filter +
  this.state.showSkills;

 

This is how the implementation of the feature to give users an option to switch between All and Reviewed Only Skills has been done.

Resources

Continue ReadingGiving users option to switch between All and Reviewed Only Skills on SUSI Skill CMS

Adding Multiple Attendees inside an Order in Open Event Android

Orders endpoint allows multiple attendees to be passed in the body of post request which essentially means that and an order may contain tickets for different attendees.

This blog post will guide you on how multiple attendees under an Order is handled and implemented in Open Event Android.

An attendee as the name implies gives us information about how many people are attending any particular event. Let’s say if a User selects two tickets with 3 and 7 quantity total 10 attendees will be generated and can be put inside one Order request.

Whenever multiple tickets are selected by the user following function is called to create Attendee

fun createAttendees(attendees: List<Attendee>, country: String?, paymentOption: String) {
       this.country = country
       this.paymentOption = paymentOption
       this.attendees?.clear()
       attendees.forEach {
           createAttendee(it, attendees.size)
       }
   }

 

Create attendees accepts a list of attendees and calls the createAttendees for every attendee in the list. Actual attendee creation is handled in createAttendee. The above function also set up the country and payment option as provided by the user.

fun createAttendee(attendee: Attendee, totalAttendee: Int) {        compositeDisposable.add(attendeeService.postAttendee(attendee)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe {
                   progress.value = true
               }.doFinally {
                   progress.value = false
               }.subscribe({
                   attendees?.add(it)
                   if (attendees != null && attendees?.size == totalAttendee) {
                       loadTicketsAndCreateOrder()
                   }
                   message.value = “Attendee created successfully!”
                   Timber.d(“Success! %s”, attendees?.toList().toString())
               }, {
                   message.value = “Unable to create Attendee!”
                   Timber.d(it, “Failed”)
               }))
   }

Create an Attendee on the other hand deals with creating individual attendees on the server. It calls the service function attendeeService.postAttendee with attendee as the parameter if the post request made is succeeds the attendee returned from the API is added to the list of attendees, when the size of list of attendees become equal to the list of attendees that had to be generated loadTicketsAndCreateOrder is called which takes care of getting charges for user and creating an order. If the post request for the attendee is failed a message is displayed and a log is added to console with error details.

fun loadTicketsAndCreateOrder() {
       if (this.tickets.value == null) {
           this.tickets.value = ArrayList()
       }
       this.tickets.value?.clear()
       attendees?.forEach {
           loadTicket(it.ticket?.id)
       }
   }

Load tickets and create order also works in a similar way as of creating attendees as there are two functions in here also one of which takes a list of attendees and calls the other function which gets data for individual ticket items passes as the parameter. Here ticket details for every attendee are fetched using the loadTicket function which takes ticket id corresponding to an attendee (using it.tickets?.id) and returns the Ticket object. Given below is the implementation for loadTicket method.

fun loadTicket(ticketId: Long?) {        compositeDisposable.add(ticketService.getTicketDetails(ticketId)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                   tickets.value?.add(it)
                   Timber.d(“Loaded tickets! %s”, tickets.value?.toList().toString())
                   if (tickets.value?.size == attendees?.size) {
                       createOrder()
                   }
               }, {
                   Timber.d(it, “Error loading Ticket!”)
               }))
   }

Load ticket uses the service method to fetch the ticket details using the passed ticket id and the returned ticket object is added to the list of ticket. When the size of the ticket becomes equal to the size of total no of attendees which means that ticket details of each attendee are fetched createOrder is called which is responsible for creating order. If anything fails in fetching tickets log is statement is printed along with error information.

fun createOrder() {

val order = Order(getId(), paymentMode, country, “pending”, amount, attendees = attendeeList, event = EventId(eventId))


           compositeDisposable.add(orderService.placeOrder(order)
                   .subscribeOn(Schedulers.io())
                   .observeOn(AndroidSchedulers.mainThread())
                   .doOnSubscribe {
                       progress.value = true
                   }.doFinally {
                       progress.value = false
                   }.subscribe({
                       orderIdentifier = it.identifier.toString()
                       message.value = “Order created successfully!”
                       Timber.d(“Success placing order!”)
                       if (it.paymentMode == “free”)
                           paymentCompleted.value = true
                   }, {
                       message.value = “Unable to create Order!”
                       Timber.d(it, “Failed creating Order”)
                   }))
       } else {
           message.value = “Unable to create Order!”
       }
   }

Create order function is the final step in the process it takes the list of attendees, payment modes, amount and other information related to order and create an order object out of it which is then passed to the service layer method which makes a post request to generate an order on the server. In case of an error or success whole message is logged and toast is displayed. This method is also involved with showing progress bar once this is called and closing the same when the procedure is completed.

Finally, in case of free events the user is taken to order completed screen and in case of paid events the user is taken to the payment screen where the user has to provide payment related information

Resources

Continue ReadingAdding Multiple Attendees inside an Order in Open Event Android

Hiding Payment Options for Free Tickets in Open Event Android

Hiding Payment Options for Free Tickets in Open Event Android

Payment Options spinner allows the user to pick any convenient payment method for an order however, Payment Options should not be shown if the total worth of order is zero or the event is free. This blog post will guide you on how its implemented in Open Event AndroidFollowing are the sequence of steps that are followed for this

  • Allow the user to select tickets and their quantity
  • Use DAO method to get the prices of the tickets selected by the user
  • Iterate through the List and keep storing the prices
  • Display Payment Selector for order with the total amount greater than zero

Given below is DAO method to get the list of prices for different tickets. The method makes a simple select statement and returns price attribute of tickets with id in passed ids. A single list of float is returned by the function.

 @Query(“SELECT price from Ticket WHERE id in (:ids)”)
   fun getTicketPriceWithIds(ids : List<Int>): Single<List<Float>>

This DAO method is then exposed to other parts (ViewModels, Fragments) using service layer class method getTicketPriceWithIds which just makes calls to DAO method and return the result provided by it. Service classes are used for separation of concerns.

fun getTicketPriceWithIds(ids: List<Int>): Single<List<Float>> {
       return ticketsDao.getTicketPriceWithIds(ids)
   }

When a list of tickets and their quantity is specified by the user the prices of the ticket are fetched using the above-explained methods. These are then stored it in a List of pair with the first element being ticket id and second its quantity.

fun updatePaymentSelectorVisibility(ticketIdAndQty: List<Pair<Int, Int>>?) {
       val ticketIds = ArrayList<Int>()
       ticketIdAndQty?.forEach { if (it.second > 0) ticketIds.add(it.first) }
       compositeDisposable.add(ticketService.getTicketPriceWithIds(ticketIds)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                   var total = 0.toFloat()
                   it?.forEach {
                       if (it.toFloat() > 0) total += it.toFloat()
                   }
                   paymentSelectorVisibility.value = total != 0.toFloat()
               }, {
                   Timber.e(it, “Error Loading tickets!”)
               }))
   }

The above method controls the visibility boolean for payment selector, it essentially iterates over the List<Pair<Int, Int>> and add all the ticket id to a temporary list if quantity for that ticket is greater than zero after this prices for the tickets in temporary list is fetched and sum total is calculated. If this total is greater than zero the visibility boolean is set else reset.

Finally an observable is set on this boolean which automatically updates the UI whenever boolean changes accordingly.

attendeeFragmentViewModel.paymentSelectorVisibility.observe(this, Observer {
           if (it !=null && it) {
               rootView.paymentSelector.visibility = View.VISIBLE
           } else {
               rootView.paymentSelector.visibility = View.GONE
           }
        })

Resources

Continue ReadingHiding Payment Options for Free Tickets in Open Event Android

Nomodeset in Meilix

Meilix contains configuration file which is required to config the ways of Meilix booting. There are several parameter out of which is nomodeset is one of them.

Problem:

Meilix result in blank screen in a machine which boots splash screen with a driver.

Solution:

When the OS boots, nomodeset takes care that the video like splash screen happen in kernel rather than on driver. Sometimes the driver is unable to run the video which results in blank screen issue. nomodeset confirms that no video get loaded and OS boots in BIOS mode until the driver get loaded.

Way 1:

menuentry "Try Live Meilix" {
	linux	/casper/vmlinuz  file=/cdrom/preseed/lubuntu.seed boot=casper iso-scan/filename=${iso_path} nomodeset quiet splash --
	initrd	/casper/initrd.lz
}
menuentry "Install Meilix" {
	linux	/casper/vmlinuz  file=/cdrom/preseed/lubuntu.seed boot=casper only-ubiquity iso-scan/filename=${iso_path} quiet splash --
	initrd	/casper/initrd.lz
}
menuentry "Check disc for defects" {
	linux	/casper/vmlinuz  boot=casper integrity-check iso-scan/filename=${iso_path} quiet splash --
	initrd	/casper/initrd.lz
}
menuentry "Test memory" {
	linux16	/install/mt86plus
}

We put nomodeset in Try Live Meilix option for testing purpose. This file can be found https://github.com/fossasia/meilix/edit/master/image/boot/grub/loopback.cfg.

Parameter quiet splash — is responsible for showing the splash screen and at the same time, it takes care to not to display other messages while the splash screen is loaded.

We can tweak parameter with the present options during the boot screen.

This is the one way through which we can use nomodeset while editing the configuration file.

Way 2:

During running of the OS, we can use this feature by editing this file /etc/default/grub and including nomodeset in it.

GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debi`
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash nomodeset"
GRUB_CMDLINE_LINUX=""

And then save and exiting the file. Then we need to update grub using

sudo update-grub

Reference:

Configuring nomodeset

nomodeset, quiet and splash

 

Continue ReadingNomodeset in Meilix

apt-get update in building Meilix through Travis

Meilix uses Travis to build and then make a github release of the ISO. There are packages that get built in the script executed by Travis.

What problem it is solving? It’s the need of apt-get update which are:

  • Update of the system to support for the newest builds.
  • Adding of the repo after adding a Personal Package Archives (‘PPAs’)

By default, Travis disabled apt-get update for every build. So if we want to run it automatically for each build, we can do it in two different ways.

Way 1:Running apt-get update in before_install step.

Implementation in .travis.yml of Meilix

print 'hello world!'
sudo: required

before_install:
  - apt-get update

 

We already used sudo so there is no need to use sudo in the apt-get update.

This is the most simplistic approach and it update the system packages and source list before installing any packages.

Way 2: executing apt-get update in Travis through APT addon.

The addon comes under the include step and the lint follows this order:

include:
  - os: linux
    addons:
      apt:
        update: true
        sources:
          - ubuntu-toolchain-r-test
        packages:
          - debootstrap
          - genisoimage
          - p7zip-full
          - squashfs-tools
          - ubuntu-dev-tools
          - dpkg-dev
          - debhelper
          - fakeroot
          - devscripts

 

This is helpful in case when we don’t have the before_install step. Additionally, it allows us to specify the packages we need to install.

Choosing between the two options

One can adopt any of the above process to implement apt-update in Travis.

Meilix uses the second approach as because we don’t have before_install parameter.

Sometimes we need some commands to get executed before updating the system, so in that case we prefer before_install to prioritize the tasks.

Resources:

 

 

Continue Readingapt-get update in building Meilix through Travis

Speaker details in the Open Event Orga App

The Open Event Organiser Android App is currently released in the Alpha phase on the Google Play Store here. This blog post explains how the speaker details feature has been implemented in the app.

Model

The model for Speaker is pretty straightforward. It includes the personal details of the speaker such as name, biography, country, social media profiles, designation etc. Apart from these details, every instance of speaker is associated with a single event. A speaker will also have multiple instances of sessions. Full implementation of the speaker’s model can be found here.

Network Call

We use Retrofit in order to make the network call and Jackson Factory to deserialize the data received from the call into an instance of the speaker model. The following endpoint provides us with the required information:

https://open-event-api-dev.herokuapp.com/speakers/{speaker_id}

Repository

In any typical android application using both network calls and data persistence, there is a need of a repository class to handle them. Speaker Repository handles the network call to the API in order to fetch the speaker details. It then saves the data returned by the api into the database asynchronously. It also ensures that we send the latest data that we have stored in the database to the view model. Given below is the full implementation for reference:

@Override
    public Observable<Speaker> getSpeaker(long speakerId, boolean reload) {
        Observable<Speaker> diskObservable = Observable.defer(() ->
            repository
                .getItems(Speaker.class, Speaker_Table.id.eq(speakerId)).take(1)
        );

        Observable<Speaker> networkObservable = Observable.defer(() ->
            speakerApi.getSpeaker(speakerId)
                .doOnNext(speaker -> repository
                    .save(Speaker.class, speaker)
                    .subscribe()));

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

ViewModel

The View Model is responsible for fetching the necessary details from the repository and displaying it in the view. It handles all the view binding logic. The most important method in the SpeakerDetailsViewModel is the getSpeakers method. It accepts a speaker id from the fragment, queries the repository for the details of the speaker and returns it back to the fragment in the form of a LiveData. Below is the full implementation of the getSpeakers method:

protected LiveData<Speaker> getSpeaker(long speakerId, boolean reload) {
        if (speakerLiveData.getValue() != null && !reload)
            return speakerLiveData;

        compositeDisposable.add(speakerRepository.getSpeaker(speakerId, reload)
            .compose(dispose(compositeDisposable))
            .doOnSubscribe(disposable -> progress.setValue(true))
            .doFinally(() -> progress.setValue(false))
            .doOnNext(speaker -> speakerLiveData.setValue(speaker))
            .flatMap(speaker -> sessionRepository.getSessionsUnderSpeaker(speakerId, reload))
            .toList()
            .subscribe(sessionList -> sessionLiveData.setValue(sessionList),
                throwable -> error.setValue(ErrorUtils.getMessage(throwable))));

        return speakerLiveData;
    }

We add the disposable to a composite disposable and dispose it in the onCleared method of the View Model. The full implementation of the View Model can be found here.

Fragment

The SpeakerDetailsFragment acts as the view and is responsible for everything the user sees on the screen. It accepts the id of the speaker whose details are to be displayed in the constructor. When an instance of the fragment is created it sets up it’s view model and inflates it’s layout using the Data binding framework.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
   context = getContext();
   binding = DataBindingUtil.inflate(inflater, R.layout.speaker_details_fragment, container, false);
   speakerDetailsViewModel = ViewModelProviders.of(this, viewModelFactory).get(SpeakerDetailsViewModel.class);
   speakerId = getArguments().getLong(SPEAKER_ID);

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

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

   return binding.getRoot();
}

In the onStart method of the fragment we load the data by calling the getSpeaker method in the view model. Then we set up the RecyclerView for the sessions associated with the speaker. Lastly we also set up the refresh listener which can be used by the user to refresh the data.

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

   speakerDetailsViewModel.getProgress().observe(this,this::showProgress);
   speakerDetailsViewModel.getError().observe(this, this::showError);
   loadSpeaker(false);

   setupRecyclerView();
   setupRefreshListener();
}

Once the data is returned we simply set it on the layout by calling setSpeaker on the binding.

@Override
public void showResult(Speaker item) {
   binding.setSpeaker(item);
}

The full implementation of the SpeakerDetailsFragment can be found here.

Sessions Adapter

The SessionsAdapter is responsible for handling the RecyclerView of sessions associated with the speaker. The most important method in the adapter is the setSessions method. It accepts a list of sessions and shows it as the contents of the recycler view. It uses the DiffUtil.calculateDiff method to create a DiffResult which will be used by the adapter to figure out where to insert items.

protected void setSessions(final List<Session> newSessions) {
        if (sessions == null) {
            sessions = newSessions;
            notifyItemRangeInserted(0, newSessions.size());
        } else {
            DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                @Override
                public int getOldListSize() {
                    return sessions.size();
                }

                @Override
                public int getNewListSize() {
                    return newSessions.size();
                }

                @Override
                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                    return sessions.get(oldItemPosition).getId()
                        .equals(newSessions.get(newItemPosition).getId());
                }

                @Override
                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                    return sessions.get(oldItemPosition).equals(newSessions.get(newItemPosition));
                }
            });
            sessions = newSessions;
            result.dispatchUpdatesTo(this);
        }
    }

The full implementation of the Adapter can be found here.

References

Continue ReadingSpeaker details in the Open Event Orga App

Fetching Info of All Users and their connected devices for the SUSI.AI Admin Panel

Fetching the data of all users is required for displaying the list of users on the SUSI.AI Admin panel. It was also required to fetch the information of connected devices of the user along with the other user data. The right to fetch the data of all users should only be permitted to user roles “OPERATOR” and above. This blog post explains how the data of connected devices of all users is fetched, which can then be used in the Admin panel.

How is user data stored on the server?

All the personal accounting information of any user is stored in the user’s accounting object. This is stored in the “accounting.json” file. The structure of this file is as follows:

{
  "email:akjn11@gmail.com": {
    "devices": {
      "8C-39-45-23-D8-95": {
        "name": "Device 2",
        "room": "Room 2",
        "geolocation": {
          "latitude": "54.34567",
          "longitude": "64.34567"
        }
      }
    },
    "lastLoginIP": "127.0.0.1"
  },
  "email:akjn22@gmail.com": {
    "devices": {
      "1C-29-46-24-D3-55": {
        "name": "Device 2",
        "room": "Room 2",
        "geolocation": {
          "latitude": "54.34567",
          "longitude": "64.34567"
        }
      }
    },
    "lastLoginIP": "127.0.0.1"
  }
}

 

As can be seen from the above sample content of the “accounting.json” file, we need to fetch this data so that it can then be used to display the list of users along with their connected devices on the Admin panel.

Implementing API to fetch user data and their connected devices

The endpoint of the servlet is “/aaa/getUsers.json” and the minimum user role for this servlet is “OPERATOR”. This is implemented as follows:

   @Override
    public String getAPIPath() {
        return "/aaa/getUsers.json";
    }

    @Override
    public UserRole getMinimalUserRole() {
        return UserRole.OPERATOR;
    }

 

Let us go over the main method serviceImpl() of the servlet:

  • We need to traverse through the user data of all authorized users. This is done by getting the data using DAO.getAuthorizedClients() and storing them in a Collection. Then we extract all the keys from this collection, which is then used to traverse into the Collection and fetch the user data. The implementation is as follows:

    Collection<ClientIdentity> authorized = DAO.getAuthorizedClients();
    List<String> keysList = new ArrayList<String>();
    authorized.forEach(client -> keysList.add(client.toString()));

    for (Client client : authorized) {
        // code           
    }

 

  • Then we traverse through each client and generate a client identity to get the user role of the client. This is done using the DAO.getAuthorization() method. The user role of the client is also put in the final object which we want to return. This is implemented as follows:

    JSONObject json = client.toJSON();
    ClientIdentity identity = new 
    ClientIdentity(ClientIdentity.Type.email, client.getName());
    Authorization authorization = DAO.getAuthorization(identity);
    UserRole userRole = authorization.getUserRole();
    json.put("userRole", userRole.toString().toLowerCase());

 

  • Then the client credentials are generated and it is checked whether the user is verified or not. If the user is verified, then in the final object, “confirmed” is set to true, else it is set to false.

    ClientCredential clientCredential = new ClientCredential (ClientCredential.Type.passwd_login, identity.getName());
    Authentication authentication = DAO.getAuthentication(clientCredential);

    json.put("confirmed", authentication.getBoolean("activated", false));

 

  • Then we fetch the accounting object of the user using DAO.getAccounting(), and extract all the user data and put them in separate key value pairs in the final object which we want to return. As the information of all connected devices of a user is also stored in the user’s accounting object, that info is also extracted the same way and put into the final object.

    Accounting accounting = DAO.getAccounting(authorization.getIdentity());
    if (accounting.getJSON().has("lastLoginIP")) {
        json.put("lastLoginIP", accounting.getJSON().getString("lastLoginIP"));
    } else {
        json.put("lastLoginIP", "");
    }

    if(accounting.getJSON().has("signupTime")) {
        json.put("signupTime", accounting.getJSON().getString("signupTime"));
    } else {
        json.put("signupTime", "");
    }

    if(accounting.getJSON().has("lastLoginTime")) {
        json.put("lastLoginTime", accounting.getJSON().getString("lastLoginTime"));
    } else {
        json.put("lastLoginTime", "");
    }

    if(accounting.getJSON().has("devices")) {
        json.put("devices", accounting.getJSON().getJSONObject("devices"));
    } else {
        json.put("devices", "");
    }
    accounting.commit();

 

This is how the data of all users is fetched by any Admin or higher user role, and is then used to display the user list on the Admin panel.

Resources

Continue ReadingFetching Info of All Users and their connected devices for the SUSI.AI Admin Panel

Ticket Quantity Spinner in Open Event Android

Spinners are basically drop down menu which provides an easy way to select an item from a set of items. Spinner is used in Open Event Android to allow user select quantity of tickets as shown in the image above. This blog post will guide you on how its implemented in Open Event Android.

Add Spinner to the XML file

<Spinner
           android:id=“@+id/orderRange”
           android:layout_width=“30dp”
           android:layout_height=“30dp”
           android:spinnerMode=“dialog”
           android:textSize=“@dimen/text_size_small” />

The above code spinnet will create a spinner type view with 30dp height and width, spinnerMode can be dialog or dropDown following are example spinner for both of the modes left being dialog type and right dropDown type.

                

(Image Source: Stack Overflow)

Set Up Adapter and Populate Data on Spinner

To show the list of acceptable Quantities for a Ticket, create an ArrayList of String and add all values from ticket.minOrder to ticket.maxOrder along with a zero option. Since ArrayList is of String and the values are integer they need to be converted to String using Integer.toString(i) method. Following code snippet will give you more understanding about it.

val spinnerList = ArrayList<String>()
           spinnerList.add(“0”)
           for (i in ticket.minOrder..ticket.maxOrder) {
               spinnerList.add(Integer.toString(i))
           }

We will also need to set up an onItemSelectedListener to listen for Item Selections and override onItemSelected and onNothingSelected functions in it. Whenever an item is selected onItemSelected is called with arguments such as view, position of the selected item, id etc, these can be used to find the selected Quantity from the ArrayList of acceptable Quantities.      

    itemView.orderRange.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
               override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) {
                   itemView.order.text = spinnerList[pos]
                   selectedListener?.onSelected(ticket.id, spinnerList[pos].toInt())
               }

               override fun onNothingSelected(parent: AdapterView<*>) {
               }
           }

Since Spinner is an Adapter Based view we need to create a SpinnerAdapter and attach it with the Spinner. If you want to create custom Spinners you would have to create a custom adapter for it along with a layout for its row elements or else one can use library-provided layout. Given below is the implementation for library-provided spinner row type. There are different options available for row type we are using R.layout.select_dialog_singlechoice because we need single selectable Quantity spinner along with Radio button for every ticket.

     itemView.orderRange.adapter = ArrayAdapter(itemView.context, R.layout.select_dialog_singlechoice, spinnerList)

Resources

Continue ReadingTicket Quantity Spinner in Open Event Android

Deploying Susper On Heroku

In Susper, currently we have two branches, master and development branch. The master branch is deployed on susper.com and we needed a deployment of development branch. For this we are using heroku and in this blog I will describe how we have deployed Susper’s development branch on heroku at http://susper-dev.herokuapp.com

Here are the steps:

1.Get a free heroku account:

To deploy our apps on heroku we must have a heroku account which can be created for free on https://www.heroku.com.

2.Create a new app on heroku:

After creating an account we need to create an app with a name and a region, for Susper we have used name susper-dev and region as United States.

3.Link the app to Susper’s development branch:

After successfully creating the app we need to link our app to Susper’s development branch and in Deploy section and then we have to enable automatic deployment from development branch after CI passes.

  1. Setup the Susper Project:

Now we have deployed our and we need to configure it so that it can successfully start on heroku. Following are the steps to configure Susper.

a) Add node and npm engine in package.json:

Now we need to tell heroku which npm and node version to use while running our app,this can be done by defining an engine field in package.json and adding npm and node versions as values as shown here:

"engines": {
   "node": "8.2.1",
   "npm": "6.0.1"
 }

 

b) Adding a postinstall field under scripts field in package.json:

Now we need to tell heroku the command that we will be using to build our app. Since on susper.com we are using ng build –prod –build-optimizer that builds our app for production by optimizing the build artifacts. Therefore on heroku also we will be using the same command:

"postinstall": "ng build --prod --build-optimizer"

 

c) Defining dependencies and typescript version for our project:This can be done by defining the @angular/cli ,@angular/compiler-cli and typescript and their version under dependencies field in package.json. In Susper we are using the following dependency versions.

"@angular/cli": "^1.1.0",
"@angular/compiler": "4.1.3",
"typescript": "~2.3.4"

 

d) Creating a javascript file to install express server and run our app: Locally when we run ng serve angular cli automatically creates a express server where our app is deployed locally but on heroku we will be required to start our express server which will be used by our app using a javascript file. Here is the code for starting the server.

//Install the server
const express = require('express');
const path = require('path');
const app = express();
// Serving the static file from dist folder
app.use(express.static(__dirname + '/dist'));
app.get('/*', function(request,response) {
response.sendFile(path.join(__dirname+'/dist/index.html'));
});
// Starting the app and listening on default heroku port.
app.listen(process.env.PORT || 8080);

 

Now we need to run this file, for this we will change start command in package.json file under script to start this file.

"start": "node server.js"

 

Now everytime a new commit is made to development branch our app will be automatically deployed on heroku at https://susper-dev.herokuapp.com

Resources

 

 

Continue ReadingDeploying Susper On Heroku