Fetching Skills using the ID of a user

The Dashboard on the SUSI Skill CMS is the page where all user specific Skill information is being displayed. It also has a section called “My Skills” which displays the Skills created by the user himself. The data for the “My Skills” section has to be fetched from the Skill metadata of each Skill. Hence, this blog post explains how the Skills created by an author are fetched using an API which takes author’s email as the only parameter.

Why is such an API required?

The task of displaying the “My Skills” section cannot be done entirely on the Client side with the use of existing APIs, as that would result in a lot of AJAX calls to the Server, because for each Skill, there would have to be an AJAX call for each user. Hence, we had to create an endpoint which could directly return the list of Skills created by a user using his ID. This would prevent multiple AJAX calls to the Server.

The endpoint used is:

"https://api.susi.ai/cms/getSkillsByAuthor.json?author_email="+author_email

 

Here, author_email is the email of the author who published the particular Skill, which is fetched from the Skill metadata object which looks like this:

"Antonyms": {
      // skill metadata
      "author_email": akjn11@gmail.com,
      // skill metadata
    }

Working of the API

The code to fetch the Skills created by an author is described in the GetSkillsByAuthor.java file. This API accepts either the author’s name, or the author’s email as the parameter. In our

We get all folders within a folder using the following method:

private void listFoldersForFolder(final File folder, ArrayList<String> fileList) {
        File[] filesInFolder = folder.listFiles();
        if (filesInFolder != null) {
            Arrays.stream(filesInFolder).filter(fileEntry -> fileEntry.isDirectory() && !fileEntry.getName().startsWith(".")).forEach(fileEntry->fileList.add(fileEntry.getName() + ""));
        }
    }

 

We use this method to get list of folders of all the group names for all the different Skills, and store them in a variable ‘folderList’ as per the following code:

File allGroup = new File(String.valueOf(model));
ArrayList<String> folderList = new ArrayList<String>();
listFoldersForFolder(allGroup, folderList);

 

For every folder corresponding to a group name, we traverse inside it and then further traverse inside the language folders to reach the place where all the Skill files are stored.

This is the code for the method to get all files within a folder:

private void listFilesForFolder(final File folder, ArrayList<String> fileList) {
    File[] filesInFolder = folder.listFiles();
    if (filesInFolder != null) {
        Arrays.stream(filesInFolder).filter(fileEntry -> !fileEntry.isDirectory() && !fileEntry.getName().startsWith(".")).forEach(fileEntry->fileList.add(fileEntry.getName() + ""));
    }
}

 

We need to get all Skill files inside the ‘languages’ folder, which itself is in ‘group’ folder. This is the code required to implement the same:

for (String temp_group_name : folderList){
        File group = new File(model, temp_group_name);
        File language = new File(group, language_name);
        ArrayList<String> fileList = new ArrayList<String>();
        listFilesForFolder(language, fileList);

        for (String skill_name : fileList) {
               // get skill metadata of each skill iteratively and put it in result JSONObject accordingly
            }
         }
    }

 

This is how the Skill metadata is fetched from the getSkillMetadata() method of SusiSkill class:

skill_name = skill_name.replace(".txt", "");
            JSONObject skillMetadata = SusiSkill.getSkillMetadata(model_name, temp_group_name, language_name, skill_name);

            if(skillMetadata.get("author_email").equals(author_email)) {
                skillMetadata.put("skill_name", skill_name);
                authorSkills.put(skillMetadata);

 

getSkillMetadata() method takes model of the Skill, group of the Skill, language of the Skill, Skill name and also duration (for usage statistical purposes) as the method parameters, and returns the metadata for the Skill in the format as shown below:

   "Antonyms": {
      "model": "general",
      "group": "Knowledge",
      "language": "en",
      "developer_privacy_policy": null,
      "descriptions": "A skill that returns the antonyms for a word",
      "image": "images/antonyms.png",
      "author": "akshat jain",
      "author_url": "https://github.com/Akshat-Jain",
      "author_email": akjn11@gmail.com,
      "skill_name": "Antonyms",
      "protected": false,
      "terms_of_use": null,
      "dynamic_content": true,
      "examples": ["Antonym of happy"],
      "skill_rating": {
        "negative": "0",
        "bookmark_count": 0,
        "positive": "0",
        "stars": {
          "one_star": 0,
          "four_star": 0,
          "five_star": 0,
          "total_star": 0,
          "three_star": 0,
          "avg_star": 0,
          "two_star": 0
        },
        "feedback_count": 0
      },
      "usage_count": 0,
      "skill_tag": "Antonyms",
      "creationTime": "2018-05-06T03:05:22Z",
      "lastAccessTime": "2018-06-23T18:48:36Z",
      "lastModifiedTime": "2018-05-06T03:05:22Z"
    }

 

This is how the Skills created by an author are fetched using this API. The JSON response of this endpoint shows the metadata of all Skills which have ‘author_email’ value the same as that given in the endpoint’s query parameter.

Resources

Continue ReadingFetching Skills using the ID of a user

Implementing Tax Endpoint in Open Event Server

The Open Event Server enables organizers to manage events from concerts to conferences and meetups. It offers features for events with several tracks and venues. The Event organizers may want to charge taxes on the event tickets. The Open Event Server has a Tax endpoint in order to support it. This blog goes over it’s implementation details in the project.

Model

First up, we will discuss what fields have been stored in the database for Tax endpoint. The most important fields are as follows:

  • The tax rate charged in percentage
  • The id for the Tax
  • The registered company
  • The country
  • The address of the event organiser
  • The additional message to be included as the invoice footer

We also store a field to specify whether the tax should be included in the ticket price or not. Each Event can have only one associated Tax information. You can checkout the full model for reference here.

Schema

We have defined two schemas for the Tax endpoint. This is because there are a few fields which contain sensitive information and should only be shown to the event organizer or the admin itself while the others can be shown to the public. Fields like name and rate aren’t sensitive and can be disclosed to the public. They have been defined in the TaxSchemaPublic class. Sensitive information like the tax id, address, registered company have been included in the TaxSchema class which inherits from the TaxSchemaPublic class. You can checkout the full schema for reference here.

Resources

The endpoint supports all the CRUD operations i.e. Create, Read, Update and Delete.

Create and Update

The Tax entry for an Event can be created using a POST request to the /taxes endpoint. We analyze if the posted data contains a related event id or identifier which is necessary as every tax entry is supposed to be related with an event. Moreover we also check whether a tax entry already exists for the event or not since an event should have only one tax entry. An error is raised if that is not the case otherwise the tax entry is created and saved in the database. An existing entry can be updated using the same endpoint by making a PATCH request.  

Read

A Tax entry can be fetched using a GET request to the  /taxes/{tax_id}  endpoint with the id for the tax entry. The entry for an Event can also be fetched from /events/{event_id}/tax  endpoint.

Delete

An existing Tax entry can be deleted by making a DELETE request to the /taxes/{tax_id} endpoint with the id of the entry. We make sure the tax entry exists. An error is raised if that is not the case else we delete it from the database.

References

Continue ReadingImplementing Tax Endpoint in Open Event Server

Creating the onScreen Monitor Using CardView

The PSLab works as a Wave generator and the Oscilloscope when connected with the Android device. The mockup of the Wave Generator has been created in the blog Creating the Mockup for Wave Generator using Moqups.

In this blog, we will create the monitor screen in the PSLab Android app using Android studio.

Deciding the layout to use for monitors

As monitors with rounded border look appealing on android device screen so we will select CardView to be the base layout of our monitors. The CardView element allows us to add certain properties like rounded corners, elevation etc.

To use the CardView in your app, add the following dependency to build.gradle file and sync the project.

dependencies {    // CardView    
implementation 'com.android.support:cardview-v7:27.1.1'
}

Add the <android.support.v7.widget.CardView> widget to your layout and all the other views to be shown on monitor screen will be placed inside this card layout as its child layout.

Creating the monitor cards

We need to decide how much spacing is to be provided to the cards to properly view them on different screens. Here we make use of the special attribute in the View class.

android:layout_weight=1

This attribute assigns an “importance” value to a View in terms of how much space it should occupy on the screen. That is if we give equal weight to two views which inside the same parent then they will both occupy equal space.

To implement this create two CardViews inside <LinearLayout> with the same layout_weight.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_weight="1"
        card_view:cardCornerRadius="10dp" />

    <android.support.v7.widget.CardView
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_weight="1"
        card_view:cardCornerRadius="10dp" />

</LinearLayout>

So this will give us a screen like this with equally weighted monitor card views as shown in Figure 1.

Figure 1 shows two CardView with equal weights(w/2) and equal width

Both the cards have the width equal to half the screen width which is set automatically by the Android.

Creating partitions in the monitors

We have to make partitions in the monitor screen so that every characteristic of Wave Generator like wave frequency, phase, selected pin etc. can be viewed simultaneously on the screen with proper spacing.

For making partitions we need to make a separator line which we can create by using <View> and setting the attributes as below:

<View
        android:layout_width="@dimen/length_0dp"
        android:layout_height="@dimen/divider_width"
        android:background="@color/dark_grey" />

This will create a thin line having the grey color. We have to make four partitions of the card view for both monitors as shown in Figure 2.

Figure 2 shows two monitors with line separator making partitions

Populating the screen with text and image icons

We have to include the <TextView> and <ImageView> to show the property data on the monitors.

Figure 3 shows partitions in the monitor CardView

Different partitions as shown in Figure 3 can be used for showing different data such as:

  1. Top Partition:- To view the selected wave. 
  2. Left Partition:- To view the selected waveform eg:- sine, triangular 
  3. Right Partition:- To view the selected wave characteristics like frequency, phase etc. 
  4. Bottom Partition:-  To view the property selected which has been selected by user

After inserting all the desired Views and providing the Views with dummy data and icons and customizing the Views by giving proper color and spacing finally we will have a layout as shown in Figure 4.

Figure 4 shows ImageView and TextView showing different dummy data

In Figure 4, the left CardView shows all the properties for the Analog Wave Generator and the right CardView shows all the properties for Digital Wave Generator.

All the characteristics on the monitors can be controlled by the controlling panel which is work in progress.

Resources:

  1. https://developer.android.com/guide/topics/ui/layout/cardview– Article on How to Create a card based layout by Android Developer Documentation 
  2. https://stackoverflow.com/questions/5049852/android-drawing-separator-divider-line-in-layout – Stack Overflow post to create separator line

 

 

Continue ReadingCreating the onScreen Monitor Using CardView

Implementing API to Fetch all Ratings by a User on Different Skills on SUSI.AI Web Client

SUSI Skill CMS allows the users to rate any Skill on a scale of 1 to 5. The user can also provide a feedback to any Skill. This paves the path to implementing a Dashboard, which has all the analytic data of the user. Hence, an API needed to be implemented which could return the ratings done by a particular user on all different Skills.

How are servlets implemented in SUSI.AI?

All servlets in SUSI extend AbstractAPIHandler class and implement APIHandler. All servlets have 4 methods, which we overwrite depending on what we want the servlet to do. They are as follows :

    @Override
    public String getAPIPath() {
        return null;
    }

    @Override
    public BaseUserRole getMinimalBaseUserRole() {
        return null;
    }

    @Override
    public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
        return null;
    }

    @Override
    public ServiceResponse serviceImpl(Query post, HttpServletResponse response, Authorization rights, JsonObjectWithDefault permissions) throws APIException {
        return null;
    }

 

How these 4 methods work together?

  • First method is getAPIPath(). It returns the endpoint of the servlet.
  • The second method is getMinimalBaseUserRole(). It returns the minimum privilege level required to access the endpoint.
  • The third method is getDefaultPermissions(). It gets the Default Permissions of a UserRole in SUSI Server. Different UserRoles have different permissions defined in SUSI Server.
  • Whenever the endpoint defined in the getAPIPath() method is called properly, it responds with whatever is defined in the fourth method, which is serviceImpl().

Implementing a servlet to fetch all ratings by a user on different Skills

The task of this servlet is to fetch all the ratings done by a user on all the different Skills, so that this fetched data could be used later on for implementation of various user specific features like Dashboard page. This is the implementation of the 4 methods of this servlet:

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

    @Override
    public JSONObject getDefaultPermissions(UserRole baseUserRole) {
        return null;
    }

    @Override
    public String getAPIPath() {
        return "/cms/getProfileDetails.json";
    }

    @Override
    public ServiceResp onse serviceImpl(Query query, HttpServletResponse response, Authorization authorization, final JsonObjectWithDefault permissions) throws APIException {

    JsonTray fiveStarSkillRating = DAO.fiveStarSkillRating;
    // JSONObject and JsonArray Declarations

    // Checking if access_token has been given and is valid
    if (authorization.getIdentity() == null) {
        throw new APIException(400, "Specified user data not found, ensure you are logged in");
    } 


    // Fetching email of the user from access token and storing it in a string
    String email = authorization.getIdentity().getName();


    // Iterating over the fiveStarSkillRating JsonTray by extracting keys at every level and accessing their child objects through the extracted keys
    for(String model_name : fiveStarSkillRating.keys())
    {
        // Storing the list of group names and iterating over them
        for(String group_name : groupnameKeysList)
        {
            // Storing the list of language names and iterating over them
            for(String language_name : languagenameKeysList)
            {
                // Storing the list of skill names and iterating over them
                for(String skill_name : skillnamesKeysList)
                {
                    skillnameArray = languageObject.getJSONArray(skill_name);
                    // Iterating over the different Skill JSONObjects
                    for(int i=0; i<skillnameArray.length(); i++) {
                        String jsonEmail = skillnameArray.getJSONObject(i).get("email").toString();
                        if(jsonEmail.equals(email)) {
                        // If the Skill has been rated by the required author, then put the Skill JSONObject into the result array
                        }
                    }
                }
            }
        }
    }

    if(result.length()==0) {
        // Handling case when user hasn’t rated any Skill yet
        result.put("accepted", false);
        result.put("message", "User has not rated any Skills yet.");
        return new ServiceResponse(result);
    }
    result.put("accepted", true);
    result.put("message", "User ratings fetched.");
    return new ServiceResponse(result);
    }

 

As it can be seen from the above code, the endpoint for this servlet is /cms/getProfileDetails.json and it requires 1 parameter – the access token of the user.

As the main task of this servlet is user specific, and should only be accessible to the particular user, hence we returned UserRole as USER in the getMinimalUserRole() method.

In the serviceImpl() method, we iterate over the fiveStarSkillRating.json JsonTray and keep on extracting keys and accessing the corresponding JSONObjects until we reach the lowermost layer, where all the Skill names are listed. Iterating over the JSONObjects corresponding to each Skill, we check the email in the Skill JSONObject to identify the user. If the email present in the Skill JSONObject matches with the email corresponding to the access token provided as a query parameter, we extract rating and the timestamp for the Skill and put it in a JSONArray. Then, finally we put the JSONArray into our resulting JSONObject.

This is how we can fetch all the ratings by a user on all the different Skills.

Resources

 

Continue ReadingImplementing API to Fetch all Ratings by a User on Different Skills on SUSI.AI Web Client

Allowing user to submit ratings for skills in SUSI iOS

Rating is a great way to get feedback from the user. Generally, the 5-Star rating system used to get feedback. The Five-Star Quality Rating System was developed as an easy-to-understand rating system for users.

In SUSI iOS we are using the Star Rating field to get feedback of SUSI skills. We enable the user to rate the skills from one to five star. In the rating submission, we get the number of stars user picked.

The stars help show if you would recommend the skill to others. The values start at 1 (being the lowest) and go to 5 (being the highest), as seen below –

Server-Side Implementation –

fiveStarRatings API is using to submit users rating. Whenever the user taps the star fiveStarRatings being called:

func submitRating(_ params: [String: AnyObject], _ completion: @escaping(_ updatedRatings: Ratings?, _ success: Bool, _ error: String?) -> Void) {
let url = getApiUrl(UserDefaults.standard.object(forKey: ControllerConstants.UserDefaultsKeys.ipAddress) as! String, Methods.fiveStarRateSkill)
_ = makeRequest(url, .get, [:], parameters: params, completion: { (results, message) in
// handle request
....
})
}

The following params we send in the request:

  • Model
  • Group
  • Language
  • Skill
  • Stars
  • Access Token

After successfully rating submission we get updated ratings for each star of the particular skill. The following response we get from the server after successfully submitted rating:

{
"ratings": {
"one_star": 0,
"four_star": 1,
"five_star": 0,
"total_star": 1,
"three_star": 0,
"avg_star": 4,
"two_star": 0
},
"session": {"identity": {
"type": "email",
"name": "abc@a.com",
"anonymous": false
}},
"accepted": true,
"message": "Skill ratings updated"
}

We use ratings object from fiveStarRatings API to update the ratings displayed on charts and labels and also, we use ratings object to update Skill model which we passed from SkillListingController to SkillDetailController so the user can see updated rating chart when clicking back to skill.

func updateSkill(with ratings: Ratings) {..}

If the user already submitted ratings for a skill, we are using getRatingByUser API with same params as in fiveStarRatings except ratings to get already submitted user rating and we display that ratings as initial ratings in UI.

Implementation of Submit Rating UI –

RatingView is behind the submit rating UI. FloatRatingViewDelegate protocol is implemented to get ratings is updating or get updated.

@objc public protocol FloatRatingViewDelegate {
/// Returns the rating value when touch events end
@objc optional func floatRatingView(_ ratingView: RatingView, didUpdate rating: Double)

/// Returns the rating value as the user pans
@objc optional func floatRatingView(_ ratingView: RatingView, isUpdating rating: Double)
}

Rating updated on rating display chart:

Now see, how we handle the case when Skill is not rated before and the user first time rate the skill.

There is a label that shows the “Skill not rated yet” when a skill is not rated. When the user rates the skill, the label is hidden and chart bar and the label is shown.

if self.ratingsBackViewHeightConstraint.constant == 0 {
self.ratingsBackViewHeightConstraint.constant = 128.0
self.contentType.topAnchor.constraint(equalTo: self.ratingBackView.bottomAnchor, constant: 16).isActive = true
self.ratingsBackStackView.isHidden = false
self.topAvgRatingStackView.isHidden = false
self.notRatedLabel.isHidden = true
}

 

Resources –

  1. Swift Protocol: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html
  2. SUSI Skills: https://skills.susi.ai/
  3. SUSI Server Link: https://github.com/fossasia/susi_server
  4. SUSI iOS Link: https://github.com/fossasia/susi_iOS
Continue ReadingAllowing user to submit ratings for skills in SUSI iOS

Filtering and Session API Integration on Admin User Route Open Event Frontend

This blog article will illustrate how the Session API has been integrated into the admin users route  Open Event Frontend, as well as how it’s now possible to filter active and deleted users using the new filters implemented.

To make the sessions buttons on the users table functional a new sub route is added to the app’s user route as follows:

this.route('users', function() {
     this.route('view', { path: '/:user_id' }, function() {
       this.route('sessions', function() {
         this.route('list', { path: '/:session_status' });
       });
     });
     this.route('list', { path: '/:users_status' });

The newly added route further contains a dynamic sub route called list. This nested route fulfills the requirement of filtering the various sessions of a given user according to their states. Interestingly, the routes admin/users/view and admin/users/list are both dynamic and expect a parameter after /users/ hence, the app cannot distinguish between them on it’s own, thus explicit handling of the dynamic parameter of the routes was implemented, differentiating them on the basis of the route’s state as follows:

beforeModel(transition) {
this._super(...arguments);
const userState = transition.params[transition.targetName].users_status;
if (!['all', 'deleted', 'active'].includes(userState)) {
this.replaceWith('admin.users.view', userState);
}
}

Thus if the dynamic portion of the route doesn’t contain the parameters all, deleted or active, then it must be referring to a user’s sessions and the route needs to be replaced with the desired sessions route accordingly. Also, the template admin/users.hbs needs to be changed to display the navigation bar  only when required. It is efficiently handled by an IF condition as follows:

{{#if (and (not-includes session.currentRouteName ‘admin.users.user’) (not-includes session.currentRouteName ‘admin.users.view.sessions.list’))}}

The server is queried to fetch the sessions of a given user by making use of the hasMany relationship a user has with his sessions. They are loaded in the route admin/users/view/sessions/list.js

model() {
const userDetails = this.modelFor('admin.users.view');
return this.store.findRecord('user', userDetails.id, {
include: 'sessions'
});

After fetching the the sessions from the server, the existing session-card  component is reused in the route’s template to display the sessions.

{{#if model.sessions}}
{{#each model.sessions as |session|}}
{{session-card session=session}}

{{/each}}
{{else}}

{{t ‘No session proposals found for the events’}}

{{/if}}
</div>
</div>

Also, in the admin/users route the filtering of deleted users was not functional. Thus  the property deleted-at of the users model which stores the timestamp of the deletion of a user was utilised. deleted-at is null for a user which is active. Hence the active and deleted users can be filtered as :

if (params.users_status === 'active') {
filterOptions = [
{
name : 'deleted-at',
op : 'eq',
val  : null
}
];
} else if (params.users_status === 'deleted') {
filterOptions = [
{
name : 'deleted-at',
op : 'ne',
val  : null
}
];
}
return this.get('store').query('user', {
get_trashed  : true,
filter       : filterOptions,
'page[size]' : 10
});

It’s important to pass the get_trashed parameter as true in the query as the the deleted user records are actually soft deleted records and will be fetched only when explicitly queried for.

Resources

Continue ReadingFiltering and Session API Integration on Admin User Route Open Event Frontend

Implementing API to facilitate changing review status of a Skill

As any registered user can make Skills in SUSI Skill CMS, sometimes the Skill made is not up to the mark to be displayed on the CMS site. There needs to be a feature implemented where the Admin and higher user roles can review a Skill and approve it accordingly. Then only the approved Skills should be displayed on the Skill CMS. This feature required implementation of an API to allow Admin and higher user roles to change the review status of a Skill. This blog post explains how such an API has been implemented.

Implementing a servlet to allow changing review status of a Skill

The basic task of the servlet is to allow Admin and higher user roles to allow changing the review status of a Skill. The review status of a Skill can either be ‘true’, indicating that the Skill has been approved by the Admin, or ‘false’, which would indicate that the Skill still needs some improvements before it is actually displayed on the SUSI Skill CMS.

Here is the implementation of the API:

  1. The API should be usable to only the users who have a user role Admin or higher. Only those with minimum Admin rights should be allowed to control what Skills are displayed on the CMS site. This is implemented as follows:
   @Override
    public UserRole getMinimalUserRole() {
        return UserRole.ADMIN;
    }

 

  1. The endpoint for the API is ‘/cms/changeSkillStatus.json’. This is implemented as follows:
   @Override
    public String getAPIPath() {
        return "/cms/changeSkillStatus.json";
    }

 

  1. The main method of the servlet is the serviceImpl() method. This is where the actual code goes which will be executed each time the API is called. This is implemented as follows:

    @Override
    public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization, JsonObjectWithDefault permissions) throws APIException {

       // Store the query parameters in variables

        if(skill_name == null || !(reviewed.equals("true") || reviewed.equals("false"))) {
            throw new APIException(400, "Bad service call, missing arguments.");
        }

        JSONObject result = new JSONObject();
        JsonTray skillStatus = DAO.skillStatus;
        JSONObject modelName = new JSONObject();
        JSONObject groupName = new JSONObject();
        JSONObject languageName = new JSONObject();
        JSONObject skillName = new JSONObject();

        if(reviewed.equals("true")) {
            JSONObject reviewStatus = new JSONObject();
            reviewStatus.put("reviewed", true);
                // traverse through the skillStatus.json file down to the Skill name and put review status ‘true’ in it
                skillName.put("reviewed", true);
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }


            // deleting skill name from skillStatus.json file if it’s review status is being changed to ‘false’
            else {
                if (skillStatus.has(model_name)) {
                    modelName = skillStatus.getJSONObject(model_name);
                    if (modelName.has(group_name)) {
                        groupName = modelName.getJSONObject(group_name);
                        if (groupName.has(language_name)) {
                          languageName = groupName.getJSONObject(language_name);
                            if (languageName.has(skill_name)) {
                                languageName.remove(skill_name);
                                    skillStatus.remove(model_name);
                                }
                                skillStatus.commit();
                            }
                        }
                    }
                }
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }
        }

 

The list of reviewed Skills is being stored in the ‘skillStatus.json’ file. So we need to traverse through that file and store the review status of the Skill as required while making the API call.

The API takes 5 parameters:

  • Model of the Skill
  • Group of the Skill
  • Language of the Skill
  • Skill name
  • Review status to be set for the Skill – true or false

On making the API call, if the value of the query parameter ‘reviewed’ is ‘true’, then the Skill name along with its review status is being appended in the ‘skillStatus.json’ file. This is done by traversing through the file using the specified model, group and language info of the Skill.

However, if the value of the query parameter ‘reviewed’ is ‘false’, then we need to check if the Skill is already there in the ‘skillStatus.json’ file. If it is already there, then we remove its entry from the file. If it isn’t in the file already, then it’s review status is already ‘false’. We also need to commit this change to the JsonTray for it to get reflected in the Server.

This is how an API has been implemented which would allow Admin and higher user roles to change the review status of any Skill, which would then facilitate showing only the approved Skills on the CMS site.

  1. Resources

     

Continue ReadingImplementing API to facilitate changing review status of a Skill

Adding a Horizontally scrollable component to display Skills based on metrics

In this blog post, I will discuss about the implementation of a horizontally scrollable component to display skill based on metrics. The purpose of the implementation is to show top skills based on metrics related to usage, ratings, etc in SUSI.AI Skills CMS.

Implementational details

  • We call this component SkillCardScrollList which takes in a list of cards to be displayed along with some other properties and returns an UI, as shown in the above GIF.
  • The parameters that the component takes are:
    • scrollId: It is a required field of the type String. It is the id name of the horizontally scrollable div.
    • skills: It contains an array of cards that are to displayed inside the container.
    • languageValue: It represents the language of the skills that are shown.
    • skillUrl: It contains the URL that the app would be taken to, on clicking individual Skill Card.
    • modelValue: It contains the model that the skill belongs to.
  • Here is a sample of how it is used in the BrowseSkill component, for showing the Top Rated Skills in a SkillCardsScrollList
<SkillCardScrollList
    scrollId="topRated"
    skills={this.state.topRatedSkills}
    modalValue={this.state.modalValue}
    languageValue={this.state.languageValue}
    skillUrl={this.state.skillUrl}
/>

 

  • The reason behind passing an unique scrollId as a prop to the component is that, there was a need to trigger the scroll event of the scrollable div n the click of left and right Floating Action Buttons (FABs) as shown in the UI. And, on multiple imports of this component, there would have been inconsistent scroll behaviour seen, had it not been unique.
  • Following in the code block of the component, which will be explained in details, that deals with the main implementation –
.
.
.
.
  scrollLeft = () => {
    let parentEle = document.getElementById(this.props.scrollId);
    let scrollValue = $(parentEle).scrollLeft() - 200;
    $(parentEle)
      .stop()
      .animate({ scrollLeft: scrollValue }, 100);
  };

  scrollRight = () => {
    // Similar function of scrollLeft
  };

  loadSkillCards = () => {
    let cards = [];
    Object.keys(this.state.skills).forEach(el => {
      .
      /* Each skill object is passed and then pushed to the cards
        array*/
      .
      );
    });
    // Set the cards array in the state 
    this.setState({
      cards,
    });
  };

  render() {
    return (
      <div
        style={{
          marginTop: '20px',
          marginBottom: '40px',
          textAlign: 'justify',
          fontSize: '0.1px',
          width: '100%',
        }}
      >
        <div>
          <div
            id={this.props.scrollId}
            className="scrolling-wrapper"
            style={styles.gridList}
          >
            <FloatingActionButton
              mini={true}
              backgroundColor={'#4285f4'}
              style={styles.leftFab}
              onClick={this.scrollLeft}
            >
              <NavigationChevronLeft />
            </FloatingActionButton>
            {this.state.cards}
            <FloatingActionButton
              mini={true}
              backgroundColor={'#4285f4'}
              style={styles.rightFab}
              onClick={this.scrollRight}
            >
              <NavigationChevronRight />
            </FloatingActionButton>
          </div>
        </div>
      </div>
    );
  }
}

 

  • The div with class scrolling-wrapper is actually scrolled on the click of the left and right FAB. For choosing the correct div to be scrolled, there was a necessary condition of an unique id as explained earlier, which has been set to the div.
  • For making the component horizontally scrollable, specific CSS rules are added to the div. They are –
gridList: {
  margin: '10px',
  textAlign: 'center',
  overflowX: 'scroll',
  overflowY: 'hidden',
  whiteSpace: 'nowrap',
},
leftFab: {
  position: 'absolute',
  left: 260,
  marginTop: 75,
},
rightFab: {
  position: 'absolute',
  right: 0,
  marginTop: 75,
  marginRight: 10,
},

 

  • The CSS rules for the FABs make them fixed in a position and only lets the card list scroll.
  • Lastly, there was a issue regarding the presence of horizontal scroll-bar been shown, which makes the UI look a bit unpleasant.

  • It was hidden with a pseudo selector CSS rule.
div.scrolling-wrapper::-webkit-scrollbar {
    display: none;
}

 

This was the implementation for the horizontally scrollable component for displaying Skill List based on a standard metrics. I hope, you found the blog helpful in making the understanding of the implementation better.

Resources

Continue ReadingAdding a Horizontally scrollable component to display Skills based on metrics

Change Text-to-Speech Voice Language of SUSI in SUSI iOS

SUSI iOS app now enables the user to change the text-to-speech voice language within the app. Now, the user can select any language of their choice from the list of 37 languages list. To change the text-to-speech voice language, go to, Settings > Change SUSI’s Voice, choose the language of your choice. Let see here how this feature implemented.

Apple’s AVFoundation API is used to implement the text-to-speech feature in SUSI iOS. AVFoundation API offers 37 voice languages which can be used for text-to-speech voice accent. AVFoundation’s AVSpeechSynthesisVoice API can be used to select a voice appropriate to the language of the text to be spoken or to select a voice exhibiting a particular local variant of that language (such as Australian or South African English).

To print the list of all languages offered by AVFoundation:

import AVFoundation

print(AVSpeechSynthesisVoice.speechVoices())

Or the complete list of supported languages can be found at Languages Supported by VoiceOver.

When the user clicks Change SUSI’s voice in settings, a screen is presented with the list of available languages with the language code.

Dictionary holds the list of available languages with language name and language code and used as Data Source for tableView.

var voiceLanguagesList: [Dictionary<String, String>] = []

When user choose the language and click on done, we store language chosen by user in UserDefaults:

UserDefaults.standard.set(voiceLanguagesList[selectedVoiceLanguage][ControllerConstants.ChooseLanguage.languageCode], forKey: ControllerConstants.UserDefaultsKeys.languageCode)
UserDefaults.standard.set(voiceLanguagesList[selectedVoiceLanguage][ControllerConstants.ChooseLanguage.languageName], forKey: ControllerConstants.UserDefaultsKeys.languageName)

Language name with language code chosen by user displayed in settings so the user can know which language is currently being used for text-to-speech voice.

To select a voice for use in speech, we obtain an AVSpeechSynthesisVoice instance using one of the methods in Finding Voices and then set it as the value of the voice property on an AVSpeechUtterance instance containing text to be spoken.

Earlier stored language code in UserDefaults shared instance used for setting the text-to-speech language for AVSpeechSynthesisVoice.

if let selectedLanguage = UserDefaults.standard.object(forKey: ControllerConstants.UserDefaultsKeys.languageCode) as? String {
speechUtterance.voice = AVSpeechSynthesisVoice(language: selectedLanguage)
}

AVSpeechUtterance is responsible for a chunk of text to be spoken, along with parameters that affect its speech.

Resources –

  1. UserDefaults: https://developer.apple.com/documentation/foundation/userdefaults
  2. AVSpeechSynthesisVoice: https://developer.apple.com/documentation/avfoundation/avspeechsynthesisvoice
  3. AVFoundation: https://developer.apple.com/av-foundation/
  4. SUSI iOS Link: https://github.com/fossasia/susi_iOS
Continue ReadingChange Text-to-Speech Voice Language of SUSI in SUSI iOS

Skill Ratings Over Time

The SUSI SKill CMS provides an option to rate and review a skill. These feedbacks help the skill creators to improve the skills. Also, the ratings and reviews can be updated by the reviewer. But the CMS only provides the current rating of a skill. What if a user or a developer wants to see how that skill has performed over time? Are there any improvements in the skill or not?

For that, we need the skill ratings over time !

Server side implementation

Create a ratingsOverTime.json file to store the monthly average rating of the skills and make a JSONTray object for that in src/ai/susi/DAO.java file. The JSON file contains the timestamp for every month, the average ratings on a skill in that month and the total number of ratings in that month.

public static JsonTray ratingsOverTime;

Path ratingsOverTime_per = susi_skill_rating_dir.resolve("ratingsOverTime.json");
Path ratingsOverTime_vol = susi_skill_rating_dir.resolve("ratingsOverTime_session.json");
ratingsOverTime = new JsonTray(ratingsOverTime_per.toFile(), ratingsOverTime_vol.toFile(), 1000000);
OS.protectPath(ratingsOverTime_per);
OS.protectPath(ratingsOverTime_vol);

Now whenever a user rates a skill, the data in ratingsOverTime.json needs to be updated. For this fetch the overall rating data of the current month. Multiply the average rating with the total number of ratings (count) of that month.

sum = average_rating X number_of_ratings

Then add the rating given by the current user to this sum and divide by count + 1 to again get the new average rating. Also increment the total number of ratings by 1.

new_sum = sum + rating_by_user

new_avg = new_sum/(count+1)

number_of_ratings =  number_of_ratings + 1

float totalRating = skillRating * ratingCount;
float newAvgRating = (totalRating + skill_stars)/(ratingCount + 1);
ratingObject.put("rating", newAvgRating);
ratingObject.put("count", ratingCount + 1);

Now we have got the ratings over time stored in ratingsOverTime.json file. An API to access this data is also required. So create an API GetRatingOverTime.java returns the ratings over time of a particular skill. The API has the following attributes :

Endpoint : /cms/getRatingsOverTime.json

Minimum user role : anonymous

Parameters : model, group, language and skill

JSONArray skillRatings = languageName.getJSONArray(skill_name);
result.put("skill_name", skill_name);
result.put("ratings_over_time", skillRatings);
return new ServiceResponse(result);

It fetches the data corresponding to the skill from ratingsOverTime.json and returns it to the CMS.

Add this API to SusiServer.java

//Skill ratings over time
GetRatingsOverTime.class

References

Continue ReadingSkill Ratings Over Time