Supporting Toolbar Options – Edit and multiple deletions – in Open Event Organizer App

The Open Event Organizer Android App was supporting deletion for only one item, and didn’t support the edit option. It needed to support multiple deletions for items and the edit operation. This PR and hence the blog covers details on how to handle multiple deletions and implementing the edit option.

Here are the steps I followed to implement these options from scratch:

Step 1: Create toolbar menu

First create a menu for toolbar with the desired toolbar actions (delete and edit in our case)

<?xml version=“1.0” encoding=“utf-8”?>
<menu xmlns:android=“http://schemas.android.com/apk/res/android”
   xmlns:app=“http://schemas.android.com/apk/res-auto”>

   <item android:id=“@+id/del”
       android:title=“Delete”
       android:icon=“@drawable/ic_delete”
       android:visible=“false”
       app:showAsAction=“ifRoom”>
   </item>

   <item android:id=“@+id/edit”
       android:title=“Update”
       android:icon=“@drawable/ic_edit”
       android:visible=“false”
       app:showAsAction=“ifRoom”>
   </item>

</menu>

Step 2: Setup helper options:

We need to keep track of which items have been selected and also be able to get the count of these selected items many times. Therefore, we need to setup the following helper methods  isTrackSelected()  and countSelected()

Both these methods make use of the HashMap:

private final Map<Long, ObservableBoolean> selectedTracks = new HashMap<>();

Every time a method uses  isTrackSelected() , it checks whether or not there is an entry of that track in the HashMap selectedTracks. If not, it adds the entry and returns the corresponding default set ObservableBoolean.

public ObservableBoolean isTrackSelected(Long trackId) {
       if (!selectedTracks.containsKey(trackId))
           selectedTracks.put(trackId, new ObservableBoolean(false));

       return selectedTracks.get(trackId);
   }

   private int countSelected() {
       int count = 0;
       for (Long id : selectedTracks.keySet()) {
           if (selectedTracks.get(id).get())
               Count++;
       }
       return count;
   }

Step 3: Handling clicks

The logic to be followed had to be consistent with existing popular apps, which users are habitual of using. It thus had to be intuitive. And so here’s the logic we follow:

Click -> Opens intent (if toolbar not already active)

Click -> Selects item (if toolbar is active and item is not already selected)

Click -> Deselects item (if item already selected)

Click -> Deselects item and de-activates toolbar (if the already selected item was the only one)

Long Click -> Activates toolbar and selects item (if toolbar not already active)

Long Click -> Selects item (if toolbar already active)

> Handling single clicks:

Following the described logic, we see if a toolbar is not active, we simply open the detailed view of the already selected item.

public void click(Long clickedTrackId) {
       if (isToolbarActive) {

           if (countSelected() == 1 && isTrackSelected(clickedTrackId).get()) {
               selectedTracks.get(clickedTrackId).set(false);
               resetToolbarDefaultState();
           } else if (countSelected() == 2 && isTrackSelected(clickedTrackId).get()) {
               selectedTracks.get(clickedTrackId).set(false);
               getView().changeToolbarMode(true, true);
           } else if (isTrackSelected(clickedTrackId).get())
               selectedTracks.get(clickedTrackId).set(false);
           else
               selectedTracks.get(clickedTrackId).set(true);

           if (countSelected() > EDITABLE_AT_ONCE)
               getView().changeToolbarMode(false, true);

       } else
           getView().openSessionsFragment(clickedTrackId);
   }

> Handling long clicks:

If the toolbar is active, long clicks behave same as simple clicks. Otherwise, we first add them to the selectedTracks, set isToolbarActive to true and change toolbar mode to activate edit and delete options.

public void longClick(Track clickedTrack) {
       if (isToolbarActive)
           click(clickedTrack.getId());
       else {
           selectedTracks.get(clickedTrack.getId()).set(true);
           isToolbarActive = true;
           getView().changeToolbarMode(true, true);
       }
   }

   public void resetToolbarDefaultState() {
       isToolbarActive = false;
       getView().changeToolbarMode(false, false);
  }

Step 4: Add relevant methods in presenter

We need to add the deleteSelectedTracks() method to the presenter so that we can delete all the tracks selected for deletion.

This methods only iterates from iterable selectedTracks.entrySet() and then executes the already existing deleteTrack() method from the presenter

private void deleteTrack(Long trackId) {
        trackRepository
            .deleteTrack(trackId)
            .compose(disposeCompletable(getDisposable()))
           .compose(progressiveErroneousCompletable(getView()))
            .subscribe(() -> {
               getView().showTrackDeleted(“Track Deleted”);
               loadTracks(true);
               selectedTracks.remove(trackId);
               Logger.logSuccess(trackId);
           }, Logger::logError);
   }

   public void deleteSelectedTracks() {
       Observable.fromIterable(selectedTracks.entrySet())
           .compose(dispose(getDisposable()))
           .compose(progressiveErroneous(getView()))
           .doFinally(() -> {
               getView().showMessage(“Tracks Deleted”);
               resetToolbarDefaultState();
           })
           .subscribe(entry -> {
               if (entry.getValue().get()) {
                   deleteTrack(entry.getKey());
               }
            }, Logger::logError);
    }

This is how the result looks like:

Implementation in Open Event Organizer Android App

Resources

Continue ReadingSupporting Toolbar Options – Edit and multiple deletions – in Open Event Organizer App

Implementing Endpoint to Resend Email Verification

Earlier, when a user registered via Open Event Frontend, s/he received a verification link via email to confirm their account. However, this was not enough in the long-term. If the confirmation link expired, or for some reasons the verification mail got deleted on the user side, there was no functionality to resend the verification email, which prevented the user from getting fully registered. Although the front-end already showed the option to resend the verification link, there was no support from the server to do that, yet.

So it was decided that a separate endpoint should be implemented to allow re-sending the verification link to a user. /resend-verification-email was an endpoint that would fit this action. So we decided to go with it and create a route in `auth.py` file, which was the appropriate place for this feature to reside. First step was to do the necessary imports and then definition:

from app.api.helpers.mail import send_email_confirmation
from app.models.mail import USER_REGISTER_WITH_PASSWORD
...
...
@auth_routes.route('/resend-verification-email', methods=['POST'])
def resend_verification_email():
...

Now we safely fetch the email mentioned in the request and then search the database for the user corresponding to that email:

def resend_verification_email():
    try:
        email = request.json['data']['email']
    except TypeError:
        return BadRequestError({'source': ''}, 'Bad Request Error').respond()

    try:
        user = User.query.filter_by(email=email).one()
    except NoResultFound:
        return UnprocessableEntityError(
{'source': ''}, 'User with email: ' + email + ' not found.').respond()
    else:

    ...

Once a user has been identified in the database, we proceed further and create an essentially unique hash for the user verification. This hash is in turn used to generate a verification link that is then ready to be sent via email to the user:

else:
    serializer = get_serializer()
    hash_ = str(base64.b64encode(str(serializer.dumps(
[user.email, str_generator()])).encode()), 'utf-8')
    link = make_frontend_url(
'/email/verify'.format(id=user.id), {'token': hash_})

Finally, the email is sent:

send_email_with_action(
user, USER_REGISTER_WITH_PASSWORD,
app_name=get_settings()['app_name'], email=user.email)
    if not send_email_confirmation(user.email, link):
        return make_response(jsonify(message="Some error occured"), 500)
    return make_response(jsonify(message="Verification email resent"), 200)

But this was not enough. When the endpoint was tested, it was found that actual emails were not being delivered, even after correctly configuring the email settings locally. So, after a bit of debugging, it was found that the settings, which were using Sendgrid to send emails, were using a deprecated Sendgrid API endpoint. A separate email function is used to send emails via Sendgrid and it contained an old endpoint that was no longer recommended by Sendgrid:

@celery.task(name='send.email.post')
def send_email_task(payload, headers):
   requests.post(
       "https://api.sendgrid.com/api/mail.send.json",
       data=payload,
       headers=headers
   )

The new endpoint, as per Sendgrid’s documentation, is:

https://api.sendgrid.com/v3/mail/send

But this was not the only change required. Sendgrid had also modified the structure of requests they accepted, and the new structure was different from the existing one that was used in the server. Following is the new structure:

'{"personalizations": [{"to": [{"email": "example@example.com"}]}],"from": {"email": "example@example.com"},"subject": "Hello, World!","content": [{"type": "text/plain", "value": "Heya!"}]}'

The header structure was also changed, so the structure in the server was also updated to

headers = {
"Authorization": ("Bearer " + key),
"Content-Type": "application/json"
}

The Sendgrid function (which is executed as a Celery task) was modified as follows, to incorporate the changes in the API endpoint and structure:

import json
...
@celery.task(name='send.email.post')
def send_email_task(payload, headers):
    data = {"personalizations": [{"to": []}]}
    data["personalizations"][0]["to"].append({"email": payload["to"]})
    data["from"] = {"email": payload["from"]}
    data["subject"] = payload["subject"]
    data["content"] = [{"type": "text/html", "value": payload["html"]}]
    requests.post(
        "https://api.sendgrid.com/v3/mail/send",
        data=json.dumps(data),
        headers=headers,
        verify=False  # doesn't work with verification in celery context
    )

 

As can be seen, there is a bug that doesn’t allow SSL verification within the celery context. However, the verification is successful when the functionality is executed independent of the celery context. But now email sending via Sendgrid actually works, which makes our verification resend endpoint functional:Screen Shot 2018-08-10 at 10.04.12 PM.pngEmail is received successfully by the recipient:

Screen Shot 2018-08-10 at 10.04.30 PM.png

Thus, a working email verification endpoint is implemented, which can be easily integrated in the frontend.


Resources:

Continue ReadingImplementing Endpoint to Resend Email Verification

Implementing a device wise usage section on the skill page

SUSI Skill CMS showcases all the skills on the index page as skill cards and users can visit any skill page for any skill by clicking on any of these cards, skill pages for each skill hold some interesting metrics like rating, usage data, country wise usage data etc. But since SUSI runs on different devices so we need something to distribute and showcase how a skill is performing on each device so we implemented a pie chart for visualization of device wise usage data.

About the API

An API is developed at the server so from the client we call this API to fetch data from the server and plug this data into the chart we wish to render.

Endpoint :

/cms/getDeviceWiseSkillUsage.json

 

Parameters :

  • model
  • group
  • language
  • skill

Sample API call :

/cms/getDeviceWiseSkillUsage.json?model=general&group=Knowledge&language=en&skill=ceo

 

Response

{
 "skill_usage": [
   {
     "count": 3,
     "device_type": "Others"
   },
   {
     "count": 39,
     "device_type": "Android"
   },
   {
     "count": 1,
     "device_type": "Web Client"
   }
 ],
 "session": {"identity": {
   "type": "host",
   "name": "162.158.166.37_35449f1b",
   "anonymous": true
 }},
 "skill_name": "news",
 "accepted": true,
 "message": "Device wise skill usage fetched"
}

Fetching the data for the chart

Setting the URL to fetch data from, this URL will be used to make the AJAX call.

let deviceUsageUrl = `${urls.API_URL}/cms/getSkillsByAuthor.json?author_email=${cookies.get('emailId')}`;
deviceUsageUrl = deviceUsageUrl + '?model=' + modelValue + '&group=' + this.groupValue + '&language=' + this.languageValue + '&skill=' + this.name;

 

Make an ajax call to extract data from the response and call a function which saves the data to the application state, this data will later be used to render our chart we wish to render.

$.ajax({
 url: deviceUsageUrl,
 ...
 success: function(data) {
   if (data.skill_usage) {
     self.saveDeviceUsageData(data.skill_usage);
   }
 },
 error: function(e) {
   self.saveDeviceUsageData();
 },
});

 

Set the application state with the received data which the pie chart component will use as it’s data source.

saveDeviceUsageData = (device_usage_data = []) => {
 this.setState({
   device_usage_data,
 });
};

Implementing the UI

We already have a card component for device usage section so we append our device wise usage section to this already present card. We fetch the data in the skillListing component and pass that data as props to the skill usage component so using data from the received props we render our pie chart.

Importing the needed components from recharts library.

import { Tooltip, Legend, PieChart, Pie, Sector, Cell } from 'recharts';

 

Rendering the Piechart component with appropriate props, the data props is the most important which is taken from the application state which we saved earlier.

<PieChart width={600} height={350}>
 <Pie
   data={this.props.device_usage_data}
   nameKey="device_type"
   dataKey="count"
   onMouseEnter={this.onPieEnter}
   ...
 >
   ...
 </Pie>
 <Legend wrapperStyle={{ position: 'relative' }} />
</PieChart>

 

Configuring color for each Cell in the pie so it looks more interactive and we have distinguished colors for all devices.

{this.props.device_usage_data.map((entry, index) => (
 <Cell
   key={index}
   fill={
     [
       '#0088FE',
       '#00C49F',
       '#FFBB28',
       '#FF8042',
       '#EA4335',
     ][index % 5]
   }
 />
))}

 

Rendering the Pie only when data is available in props so we don’t end up rendering a blank chart which obviously won’t look good.

{
 this.props.device_usage_data !== [] ? (
   ...
 ): ''
}

 

Resources

  • Swizec Teller, Rendering a pie chart using react and d3, URL
  • Pie chart example from recharts, URL
Continue ReadingImplementing a device wise usage section on the skill page

Creating an apk in the apk branch using Travis CI

All Android apps in FOSSASIA have an apk branch where after a pull request is merged a new apk gets created so that even people who do not know how to setup the app locally can access it. Let’s see how it is done.

Lets get started

Create a bash file in the scripts folder and name it upload-apk.sh

Here is all the code that you require to create an apk in the apk branch.

First thing that we need to do is setup git. So we’ll set the user name and email just like we do when we setup git for the first time in our own systems.

git config --global user.name "Travis CI"
git config --global user.email "noreply+travis@fossasia.org"

 

Next we will clone the apk branch of the repository and copy all the files that we need ie the apk and the JSON files.

git clone --quiet --branch=apk https://fossasia:$GITHUB_API_KEY@github.com/fossasia/open-event-android apk > /dev/null
cd apk
\cp -r ../app/build/outputs/apk/*/**.apk .
\cp -r ../app/build/outputs/apk/debug/output.json debug-output.json
\cp -r ../app/build/outputs/apk/release/output.json release-output.json

 

Next we will create a new branch that contains only the latest apk. After that we will add the APK and then commit all those changes. You can see that the current date and time are printed out in the commit message.

git checkout --orphan temporary

git add --all .
git commit -am "[Auto] Update Test Apk ($(date +%Y-%m-%d.%H:%M:%S))"

 

We will delete the current apk branch and then rename the current branch to apk. In the end we will force push to origin since histories are unrelated.

git branch -D apk
git branch -m apk

git push origin apk --force --quiet > /dev/null

 

If you have already integrated Travis CI in your repository then you just need to add this line your travis.yml file.

after_success:
- bash scripts/update-apk.sh

 

Now every time a PR gets merged in the repository a new apk file is created in the apk branch of your repository.

Resources

  1. Travis CLI – https://github.com/travis-ci/travis.rb#readme
  2. Travis official documentation – https://travis-ci.org/
Continue ReadingCreating an apk in the apk branch using Travis CI

Implementing a skill rating over time graph section in SUSI Skill CMS

In SUSI.AI skill ratings is an invaluable aspect which greatly helps the users to know which skills are performing better than the rest and are more popular than the others. A robust skill rating system for the skills was developed recently which allows the users to rate skills as per their experience and thus data like average rating, total number of ratings is available but there was no provision to see the rating history or how the skills rating has changed over time, this could be an important aspect for users or developers to know what changes to the skill made it less/more popular. An API is developed at the server to retrieve the ratings over time data, we can use these details to render attractive components for a better visual understanding of how the skill is performing and get statistics like how the skill’s rating has changed over time.

About the API

Endpoint : /cms/getRatingsOverTime.json

Parameters

  • model
  • group
  • language
  • skill

After consuming these params the API will return the number of times a skill is called along with

the date on which it is called. We use that data as an input for the line chart component that we want to render. 

Fetching data from the server and storing in the application state

Make an AJAX call to the server to fetch the data from the URL which holds the server endpoint, on successfully receiving the data we do some formatting with the timestamp that comes along the data to make it more convenient to understand and then we call a saveRatingOverTime function which saves the data received from the server to the application state.

let ratingOverTimeUrl = `${urls.API_URL}/cms/getRatingsOverTime.json`;
skillUsageUrl = skillUsageUrl + '?model=' + modelValue + '&group=' + this.groupValue + '&language=' + this.languageValue + '&skill=' + this.name;
// Fetch the skill ratings over time
$.ajax({
 url: ratingOverTimeUrl,
 dataType: 'json',
 crossDomain: true,
 success: function(data) {
        if (data.ratings_over_time) {
         const ratingData = data.ratings_over_time.map(item => {
             return {
               rating: item.rating,
               count: item.count,
               timestamp: parseDate(item.timestamp)
                 .split(' ')
                 .slice(2, 4)
                 .join(' '),
                 };
           });
         self.saveRatingOverTime(ratingData);
        }
 },
 error: function(e) {
        console.log(e);
        self.saveRatingOverTime();
 },
});

 

Save the skill usage details in the component state.

// Save ratings over time data in the component state
saveRatingOverTime = (ratings_over_time = []) => {
 this.setState({
        ratings_over_time,
 });
};

 

Send the received data as props to the Skill Rating component and render it.

<SkillUsageCard skill_usage={this.state.skill_usage} /> 

Implementing the UI

Importing the packages for rendering the chart in the Skill Ratings component.

import { XAxis, YAxis, Tooltip, LineChart, Line, Legend, ResponsiveContainer } from 'recharts';

 

Display a small heading for the section in the ratings card and Render a Responsive container component which will form a parent component for out Chart which will be rendered when the ratings over time data received in the props is not empty.

<div className="sub-title" style={{ alignSelf: 'flex-start' }}>
 Rating over time
</div>
{this.props.ratings_over_time.length ? (
 <div>
        <ResponsiveContainer
         height={300}
         width={
           window.innerWidth < 660
             ? this.state.width
             : this.state.width * 1.5
         }
         debounce={1}
        >
         ...
        </ResponsiveContainer>
 </div>
) : (
 <div>No ratings data over time is present</div>
)}

 

Render a LineChart and supply data from the data prop received from the Skill Listing component, add X-Axis and Y-Axis by supplying corresponding dataKey props depending on the data received, Add a tooltip to describe points on the line chart and a legend which describes the lines, After that we have a Line component which depicts the change in ratings over time on the chart.

<LineChart
 data={this.props.ratings_over_time}
 margin={{
        top: 5,
        right: 30,
        left: 20,
        bottom: 5,
 }}
>
 <XAxis dataKey="timestamp" padding={{ right: 20 }} />
 <YAxis dataKey="rating" />
 <Tooltip wrapperStyle={{ height: '60px' }} />
 <Legend />
 <Line
        name="Average rating"
        type="monotone"
        dataKey="rating"
        stroke="#82ca9d"
        activeDot={{ r: 8 }}
 />
</LineChart>

 

So I hope after going through this blog it is more clear how the ratings over time section is implemented in the Skill CMS.

Resources

 

Continue ReadingImplementing a skill rating over time graph section in SUSI Skill CMS

Using Two-Way Data Binding in Open Event Organizer Android App:

Data Binding is the simple approach which relieves developers of repeated findViewById() calls. It is something that every developer must use if not using ButterKnife.

The Open Event Organizer Android App provides options to fill in an extensive set of details while creating an event, or any other entities. The problem at hand is that many of these options are common to many of these entities. For instance, currently the element date-time-picker and text fields are common to elements of different forms, as each one of them requires date-time checkboxes.

We need to be able to <include> a separate smaller and reusable layout file and bind event data to make the code shorter. This would help decreasing unnecessary code base and improving code readability.

We will see how using 2 way data binding and <include> tags in the small PR #929 reduced the code of 112 lines to just 9 lines:

Step 1: Configuration:

The very first step is to configure your project to enable data bindings in your
build.gradle (Module:app) file.

dataBinding should be included as follows:

android {
   // Rest of gradle file…
   dataBinding {
   enabled true
   }    // Rest of gradle file…
}

Step 2: Import and variable tags:

Data Binding uses the tag <data> to signify the data which will be referred to in lambda expressions inside the XML.

We also need to import any class, whose methods we need to use. This can be done using the <import> tag.

Finally, the <variable> tag is used to define any variables that will be referenced in the XML.

 

<data>
  <import type=”android.view.View” />
  <variable
      name=“date”
      type=”String” />
  <variable
      name=“label”
      type=”String“/>
</data>

Step 3: Binding the declared variables:

Data binding recognizes methods of the type set<variable>, where <variable> is event in our case.

We need to use  executePendingBindings();  so that any pending bindings are done and the UI of our app responds correctly as soon as the view data is updated.

@Override
public void showResult(Event event) {
  binding.setEvent(event);
  binding.executePendingBindings();
}

Step 4: Using the declared variables:

Making use of the declared variables is a very simple task and is as simple as a java statement. You can do almost everything that’s possible in the java file, the only constraint being that the used variables are declared in the xml and binded appropriately.

Most of the data binding expressions use data binding to condense the expression to its smallest possible form.

<LinearLayout
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  android:padding=“@dimen/spacing_extra_small”
  android:orientation=“horizontal”
  android:visibility=“@{ picker.checked ? View.VISIBLE : View.GONE }”>

2 Way Data Binding

In case of the Organizer App, we are using 2 way data binding.

Data Binding allows us to do much more than just set text in TextView or create listener in Button. If we want to use EditText and automatically update text variable in java code, we need to use observable fields and two way binding.

Thus, most variables like date, event that we are binding, are Observable fields.

* Sometimes there’s a use case of using a variable declared in another layout file.

For example, in:

<org.fossasia.openevent.app.ui.views.DatePicker
  style=“?attr/borderlessButtonStyle”
  android:layout_width=“wrap_content”
  android:layout_height=“wrap_content”
  android:textColor=“@color/purple_500”
  app:value=“@={ date }” />

The variable date isn’t binded in the java file but the xml files which include the layout time_picker.xml

Using the <include> tag:

The include tag is very simple to use, and we can simply bind the date and label element. The event_create_form.xml binds the variable using the bind attribute like this:

<include
  layout=“@layout/time_picker”
  bind:date=“@={ event.startsAt }”
  bind:label=“@{ @string/starts_at }”/>

The most common error you will face:

Often, when there’s something wrong with the XML, the most common error you will face is:

“Cannot resolve Data Binding class…”

This error is because Android Studio couldn’t generate the Data Binding class for your XML file because of some error. Presently, it doesn’t give much details about what’s wrong, so you’ll have to look for the errors yourselves.

The most common mistake newbie developers make is forgetting to bind the variables appropriately.

References:

Android Developer Guide:

https://developer.android.com/topic/libraries/data-binding/

Continue ReadingUsing Two-Way Data Binding in Open Event Organizer Android App:

Integrate prettier with lint-staged and ESLint for consistent code style throughout the project

SUSI Skill CMS presently use ESLint to check for code linting errors, the ESLint rules are written in a separate .eslintrc file which lives at the project root. The project didn’t follow any best practices for react apps and the rules were weak therefore a lot of bad/unindented code was present in the project which takes a lot of time to fix manually, not to mention there was no mechanism to auto-format the code while committing. Also, code reviews take a lot of time to discuss code styles and fixing them.

Prettier comes to the rescue as it’s a code formatter which provides a ton of options to achieve the desired well-formatted code. Prettier enforces a consistent code style across your entire codebase because it disregards the original styling by parsing it away and re-printing the parsed code with its own rules

Add prettier as a development dependency

npm install prettier --save-dev --save-exact

 

Similar to how we write ESLint rules in a separate .eslintrc file, we have a .prettierrc file which contains rules for prettier but since we already have ESLint so we run prettier on top of ESLint to leverage functionalities of both packages, this is achieved by using eslint-plugin-prettier and eslint-config-prettier which exist for ESLint. These packages are saved as devDependencies in the project and “prettier” as a plugin is added to .eslintrc file and recommended prettier rules are extended by adding “prettier” in .eslintrc file.

Install the config and plugin packages

npm i eslint-plugin-prettier eslint-config-prettier --save-dev

 

To run prettier using ESLint, add the prettier to ESLint plugins and add prettier errors to ESLint rules.

// .eslintrc
{
 "plugins": ["prettier"],
 "rules": {
   "prettier/prettier": "error"
 }
}

 

Extending Prettier rules in ESLint

// .eslintrc
{
 ...
 "extends": ["prettier"]
 ...
}

 

5856 linting errors found which were undetected initially (SUSI Skill CMS).

image

Add a .prettierrc file with some basic formatting rules for now like enabling single quotes wherever applicable and to have trailing comma at the end of JSON objects.

// .prettierrc
{
   "singleQuote": true,
   "trailingComma": "all",
   "parser": "flow"
}

 

Add a format script in package.json so that user can format the code whenever required manually too.

// package.json
"scripts": {
        ...
        "format": "prettier --write \"src/**/*.js\"",
        ...
},

 

Since we want to prevent contributors from committing unindented code we used lint-staged, a package which runs tasks on a set of specified files. We achieve this by adding a set of tasks in the lint-staged object and then run lint-staged as a pre-commit hook using husky.

Install lint-staged as a development dependency which will be visible in package.json file.

npm i lint-staged --save-dev 

 

Add tasks for lint-staged as an object in package.json, we add a lint-staged object in the pacage.json file and grab all .js files in the project and run eslint, prettier over them and then we finally run git add to stage the changes done by prettier.

// package.json

"lint-staged": {
   "src/**/*.js": [
     "eslint src --ext .js",
     "prettier --write",
     "git add"
   ]
 }

 

Call lint-staged in pre-commit git-hook to run the specified tasks in the package.json.

// package.json

"scripts": {
   ...
   "precommit": "lint-staged",
   ...
},

 

Running lint-staged tasks before committing

image

As a result, we save a lot of time in reviewing the code since we don’t have to be worried about the code styles anymore as pre-commit hook already takes care of that also this ensures consistent code style throughout the project which improves the overall quality.

Resources

Prettier library website
Use ESLint to run Prettier
Link to my Pull Request
eslint-plugin-prettier
eslint-config-prettier

Continue ReadingIntegrate prettier with lint-staged and ESLint for consistent code style throughout the project

Display Skill Usage of the Past Week in a Line Chart

Skill usage statistics in SUSI.AI is an important aspect which greatly helps the skill developers know what is more engaging for the users and the users know which skills are more popular than the others and are being used widely. So data like this should be interactively available on the clients. An API is developed at the server to retrieve the skill usage details, we can use these details to render attractive components for a better visual understanding of how the skill is performing and get statistics like on which days the skill has been active in particular.

 

About the API

Endpoint : /cms/getSkillUsage.json

Parameters

  • model
  • group
  • language
  • skill

After consuming these params the API will return the number of times a skill is called along with the date on which it is called. We use that data as an input for the line chart component that we want to render.

Creating a Skill Usage card component

Import the required packages from corresponding libraries. We are using recharts as the library for the charts support and thus we import several components required for the line chart at the top of the Skill Usage component.

// Packages
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend } from 'recharts';
import { Paper } from 'material-ui';

 

Create a SkillUsageCard component which is enclosed in a Paper component from material ui library to give it a card like look and then we render a Line chart inside it using appropriate data props which are received from the skill page component, we set a height and a width for the chart, Add an X-Axis, Y-Axis, Tooltip which shows up on hovering over points on the graph, legends to describe the line on the chart and then finally a line with several styling and other props.

class SkillUsageCard extends Component {
 render() {
   return(
     <Paper className="margin-b-md margin-t-md">
       <h1 className='title'>
           Skill Usage
       </h1>
       <div className="skill-usage-graph">
         <LineChart width={600} height={300} data={this.props.skill_usage}
               margin={{top: 5, right: 30, left: 20, bottom: 5}}>
           <XAxis dataKey="date" padding={{right: 20}} />
           <YAxis/>
           <Tooltip wrapperStyle={{height: '60px'}}/>
           <Legend />
           <Line name='Skill usage count' type="monotone" dataKey="count" stroke="#82ca9d" activeDot={{r: 8}}/>
         </LineChart>
       </div>
     </Paper>
   )
 }
}

 

Add prop validation at the end of the file to validate the coming props from the skill page component to validate that correct props are being received.

SkillUsageCard.propTypes = {
   skill_usage: PropTypes.array
}

 

Export the component so it can be used in other components.

export default SkillUsageCard;

 

Fetch the data for the component from the API in the skill page component where the skill usage component will be rendered. First set the API url and then make an AJAX call to that URL and once the data is received from the server pass that received data to a saveSkillUsage function which does the simple task of saving the data to the state and passing the saved data as a prop to the skill usage component. In case the call fails we log the error to the console.

let skillRatingUrl = `${urls.API_URL}/cms/getSkillRating.json`;
skillUsageUrl = skillUsageUrl + '?model=' + modelValue + '&group=' + this.groupValue + '&language=' + this.languageValue + '&skill=' + this.name;
// Fetch skill usage of the visited skill
$.ajax({
  url: skillUsageUrl,
  dataType: 'json',
  crossDomain: true,
  success: function (data) {
      self.saveSkillUsage(data.skill_usage)
  },
  error: function(e) {
       console.log(e)
  }
});

 

Save the skill usage details in the component state to render the skill usage component.

saveSkillUsage = (skill_usage = []) => {
  this.setState({
     skill_usage
 })
}

 

Send the received data as props to the Skill Usage component and render it.

<SkillUsageCard skill_usage={this.state.skill_usage} /> 

 

So I hope after reading this blog you have a more clearer insight into how the skill usage details are implemented in the CMS.

Resources –

  • Jerry J. Muzsik, Creating and deploying a react app using recharts URL.
  • Recharts, Documentation, URL.
Continue ReadingDisplay Skill Usage of the Past Week in a Line Chart

Rendering a Uniform StaticAppBar Component across all SUSI Web Clients on all Routes.

The Problem –
We have three SUSI Web Clients namely

Webchat
Skills CMS
Accounts

And it’s important to keep the design guidelines in sync across all the clients, StaticAppBar is a component which forms the header of all the pages and thus it is important to keep it uniform in all clients which was clearly missing before. There is also a lot of code duplication of the AppBar component (in accounts app) since it is used on all the pages so our approach will be to prepare a single component and render it on all routes.

Tackling the problem – Since the StaticAppBar component is present on all the clients we simply make the menu items uniform across all the clients and apply a check on those menu items on which are a premium feature or should appear only once the user is logged in.

Building blocks of the StaticAppBar component

  • AppBar
  • SUSI logo on the left end
  • Drop down hamburger menu on the right

Here’s how the JSX for the StaticAppBar component in CMS looks like, it uses an AppBar component from the material-ui library and has several props and styling as per the requirement.

<AppBar
   className='topAppBar'
   id='appBar'
   title={<div id='rightIconButton' ><Link to='/' style={{ float: 'left', marginTop: '-10px',height:'25px',width:'122px' }}>
       <img src={susiWhite} alt='susi-logo' className='siteTitle' /></Link></div>}
   style={{
       backgroundColor: colors.header,
       height: '46px',
       boxShadow: 'none',
       margin: '0 auto',
   }}
   iconStyleRight={{ marginTop: '-2px' }}
   iconElementRight={<TopRightMenu />}
/>

 

TopRightMenu is a function that returns JSX for the hamburger dropdown and is rendered in the AppBar as depicted below. It is a conditional menubar meaning some menu items are only rendered when the user is logged in and thus this helps cover those features which should only be available to logged in users. After that we use a popover component which shows up when the 3 dots or the expander on the top right is clicked. Almost all of the components in material ui has a style prop so styling is easy for the components moreover the workflow for the popover click goes like once the expander is clicked a boolean state variable named showOptions is toggled which in turn toggles the opening or closing state of the Popover as per the open prop.

let TopRightMenu = (props) => (
           <div onScroll={this.handleScroll}>
               <div>
                   {cookies.get('loggedIn') ?
                       (<label
                           style={{color: 'white', fontSize: '16px', verticalAlign:'super'}}>
                           {cookies.get('emailId')}
                           </label>) :
                       (<label>
                           </label>)
                   }
                   <IconMenu
                       {...props}
                       iconButtonElement={
                           <IconButton
                               iconStyle={{ fill: 'white' }}><MoreVertIcon /></IconButton>
                       }
                       targetOrigin={{ horizontal: 'right', vertical: 'top' }}
                       anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
                       onTouchTap={this.showOptions}
                   >
                   </IconMenu>
                   <Popover
                       {...props}
                       animated={false}
                       style={{ float: 'right', position: 'relative', marginTop: '46px', marginLeft: leftGap }}
                       open={this.state.showOptions}
                       anchorEl={this.state.anchorEl}
                       anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
                       targetOrigin={{ horizontal: 'right', vertical: 'top' }}
                       onRequestClose={this.closeOptions}
                   >
                       <TopRightMenuItems />
                       {cookies.get('loggedIn') ?
                           (<MenuItem primaryText='Botbuilder'
                               containerElement={<Link to='/botbuilder' />}
                               rightIcon={<Extension />} />) :
                           null
                       }
                       <MenuItem primaryText='Settings'
                           containerElement={<Link to='/settings' />}
                           rightIcon={<Settings />} />
                       {cookies.get('loggedIn') ?
                           (<MenuItem primaryText='Logout'
                               containerElement={<Link to='/logout' />}
                               rightIcon={<Exit />} />) :
                           (<MenuItem primaryText='Login'
                               onTouchTap={this.handleLogin}
                               rightIcon={<LoginIcon />} />)
                       }
                   </Popover>
               </div>
           </div>
       );
Handling the conditional display of menu items based on the user session

Some features are to be offered to only those who are logged in and thus we need to display them depending on the user session i.e they should be visible when the user is logged in and hidden when the user is logged out. User state is stored in the browser’s cookies and using that we can achieve the desired result.

{
     cookies.get('loggedIn') ?
       (<MenuItem primaryText="Botbuilder"
         href="https://skills.susi.ai/botbuilder"
         rightIcon={<Extension/>}/>):
         null
}

 

So I hope after going through this blog you have a much more clearer insight to how the StaticAppBar is implemented.

 

References

  1. Check out AppBar component from material-ui library here.
  2. Check blog post introducing the usage of material-ui in react here
Continue ReadingRendering a Uniform StaticAppBar Component across all SUSI Web Clients on all Routes.

Appending a rating section of SUSI SKILL CMS to the skill page

Ratings is an essential component of skills which provides the developers an insight into how the SUSI Skill is functioning and how to further improve it which ultimately leads to great user experience so this was the motivation to allow users to be able to rate skills, once the rating system is implemented we need to show some statistics like average rating, total users who have rated the skills etc on the skill page for each skill, and this will also enable users to get top rated skills and thus users can get to use the best skills rated by the community. So we implemented a rating section to SUSI SKILL CMS

Implementation

Server –

  1. Two APIs were implemented by the analytics team on the server which allows the user to rate skill and fetch rating for each skill.
    1. To rate the skill (Sample)

      /cms/getSkillRating.json?model=general&group=Knowledge&language=en&skill=aboutsusi&callback=pc&_=1525446551181
      

       

    2. To get the ratings data for any skill (Sample)

      /cms/fiveStarRateSkill.json?model=general&group=Knowledge&language=en&skill=aboutsusi&stars=3&callback=p&_=1526813916145
      

       

CMS –

    1. When visiting any skill make an ajax call to the server to fetch the skill data for the visited skill. The call takes in the URL from which we have to fetch data from and of course a datatype which is jsonp since server returns data in the JSON format, when the request succeeds we save the received rating to the application state and in the case or any errors we log the error for developers to debug.

// Fetch ratings for the visited skill
           $.ajax({
               url: skillRatingUrl,
               jsonpCallback: 'pc',
               dataType: 'jsonp',
               jsonp: 'callback',
               crossDomain: true,
               success: function (data) {
                   self.saveSkillRatings(data.skill_rating)
               },
               error: function(e) {
                   console.log(e);
               }
           });
    1. Save the fetched data to the application state, this data saved in the state will be used in several components and graph present on the ratings section.

saveSkillRatings = (skill_ratings) => {
    this.setState({
        skill_ratings: data
    })
 }
  1. Plug in the data received to the Bar chart component to visualize how ratings are divide.
    1. Import the required components on the top of the file from the recharts library which provides us with several interactive charts.
    2. import {BarChart, Cell, LabelList, Bar, XAxis, YAxis, Tooltip} from 'recharts';
      
        1. Plug the data to the BarChart component through the data prop and render them to the page, this data is coming from the application state which we saved earlier. After that we define keys and styling for the X-Axis and Y-Axis and an interactive tooltip which shows up on hovering over any bar of that chart. We have 5 bars on the chart for each star rating all of different and unique colors and labels which appear on the right of each bar.

      <div className="rating-bar-chart">
         <BarChart layout='vertical' width={400} height={250}
              data={this.state.skill_ratings}>
              <XAxis type="number" padding={{right: 20}} />
              <YAxis dataKey="name" type="category"/>
              <Tooltip
      
                   wrapperStyle={{height: '60px'}} />
              <Bar name="Skill Rating" dataKey="value" fill="#8884d8">
                   <LabelList dataKey="value" position="right" />
                       {
                           this.state.skill_ratings
                            .map((entry, index) =>
                              <Cell key={index} fill={
      
                                 ['#0088FE', '#00C49F', '#FFBB28',
                                '#FF8042', '#FF2323'][index % 5]
                             }/>)
                       }
              </Bar>
          </BarChart>
      </div>
      
    3. Display the average rating of the skill along with the stars
        1. Import the stars component from the react-ratings-declarative component.

                  import Ratings from 'react-ratings-declarative';
          

           

        2. Render the average ratings and the stars component which is available in the app state as saved before.

          <div className="average">
                  Average Rating
          <div>
                     {this.state.avg_rating ? this.state.avg_rating : 2.5}
                  </div>
                  <Ratings
                     rating={this.state.avg_rating || 2.5}
                     widgetDimensions="20px"
                     widgetSpacings="5px"
                   >
                     <Ratings.Widget />
                     <Ratings.Widget />
                     <Ratings.Widget />
                     <Ratings.Widget />
                     <Ratings.Widget />
                  </Ratings>
          </div> 
          

           

    1. Display the total no of people who rated the skill, again, by using the ratings data saved in the state and calculating the total users who rated the skill by using a reduce function in ES6.

      <div className="total-rating">
              Total Ratings
              <div>
                 {this.state.skill_ratings.reduce((total, num) => {
                     return total + num.value
                 }, 0)}
              </div>
      </div>
      

      Outcome –

      I hope this post helped you in understanding how the rating system is implemented in the CMS.

      References –

Continue ReadingAppending a rating section of SUSI SKILL CMS to the skill page