Different views for SUSI Skill Creator

SUSI Skill Creator is a service provided to easily create skills for SUSI. The skill can be written in the form of code. The coding syntax is described in SUSI skill tutorial. The problem with this is that we can’t expect everyone to be great coders and be able to understand the document and easily create a skill. Hence, we needed some robust alternatives for people who don’t want to write the code. This is where UI View and Tree View comes in.

What is UI View?

UI View or User Interface View shows the skill in form of chat bubbles. This is useful for demonstrating how the actual chat will look like.

Hello
Hi. I’m SUSI.

The following code will be shown as this in conversation view:

What is Tree View?

Tree view shows the conversation in form of a tree with its branching representing the chats.

Hello|Hi
Hi. I'm SUSI.|Hey there!

The following code will be shown as this in tree view:

How are the views synchronised?

We’re basically not making any API calls or performing any major function in any of the views. The components of different views are solely for their own function. All the API calls are made in a parent component named SkillCreator.

The main reason for creating a parent component is that we need the category, language, name and commit message options to appear on all the three views. Hence, it only makes sense to have a parent component for them and different components for the views.

The default code (that appears in code editor by default) is in a state in SkillCreator. We pass this state to the code view component as a props.

<CodeView
  skillCode={this.state.code}
  sendInfoToProps={this.sendInfoToProps}
/>

Now, we need to get any change made in the code in CodeView and change the code state of SkillCreator accordingly. For that, we have a function named sendInfoToProps which is passed as props to CodeView and is then called from CodeView whenever we make some change. This will be more clear after having a look at the function sendInfoToProps:

sendInfoToProps = value => {
 if (value) {
  this.setState(
   {
     code: value.code ? value.code : this.state.code,
     expertValue: value.expertValue
       ? value.expertValue
       : this.state.expertValue,
     groupValue: value.groupValue
       ? value.groupValue
       : this.state.groupValue,
     languageValue: value.languageValue
       ? value.languageValue
       : this.state.groupValue,
   imagUrl: value.imageUrl ? value.imageUrl : this.state.imageUrl,
   },
   () => this.generateSkillData(),
  );
 }
};

You can see that we update the code in SkillCreator and other states required. Also, you can notice the we’re calling generateSkillData function here. This is done to convert the code to skill data which is sent to Conversation view and Tree view as props.
We’re calling generateSkillData as a callback function because setState is an asynchronous function.
This will demonstrate how this is passed to Conversation view:

<ConversationView
  skillData={this.state.skillData}
  handleDeleteNode={this.handleDeleteNode}
/>

Same is the case for this handleDeleteNode function. We have an option in both Conversation view and Tree view to allow the user to delete a conversation (originally written as skill in code view). On delete, handleDeleteNode is called from the props and the conversation is deleted from the code which is then send to all the views (as props) and the views are updated as well.

References:

Continue ReadingDifferent views for SUSI Skill Creator

Code view and UI View in design tab of bot wizard

Design tab is for designing your SUSI AI chatbot. You can change the background colour, add an image as background color, change colour of user text box, bot text box etc. On SUSI AI bot wizard design tab, we have two views. These views show the same thing but in a different way for different people. The code view is mainly for the developers while the UI (User Interface) view is for a non-developer user because we can not expect them to know how to code even if it is easy.

Code View

The current code view of design tab looks like this:

::bodyBackground #ffffff
::bodyBackgroundImage
::userMessageBoxBackground #0077e5
::userMessageTextColor #ffffff
::botMessageBoxBackground #f8f8f8
::botMessageTextColor #455a64
::botIconColor #000000
::botIconImage

For changing colours, you simply have to change the hex codes and for adding images as background, you need to add the url of image. You can also upload an image but that function is provided in the UI View.

UI View

The current UI view of design tab looks like this:

Here, we have colour picker for user to choose a colour for various components of the chatbot. There’s also a small box with the same colour as specified in hex code for previewing.

Switching and Synchronising between Code view and UI view

We have different components for code view and UI view. In order to synchronise both the views, we need to have the same code in state of both components. To do that, we add the code in state of the parent component i.e. design.js and then pass it to code view and UI view as states.

The following code snippet will demonstrate this:

<CodeView
    design={{
        sendInfoToProps: this.sendInfoToProps,
        updateSettings: this.updateSettings,
        code: this.state.code,
    }}
/>

You can see that we pass a whole object as props.
Using this approach, same code is passed to both the views. This solves the first problem. The next problem is that if we do a change in code view then it should happen in the code state of parent file as well. Same goes for UI view.
To do this, whenever we change the code in code view or UI view, we call a function in the parent file (design.js) which was passed as props and we pass the state of code view or UI view as arguments of this function. This function then updates the state accordingly. The name of the function is sendInfoToProps and it is as follows:

sendInfoToProps = values => {
    this.setState({ ...values });
};

You can see that it simply updates the state value as per the parameters passed to it.
So this is how synchronisation works between different views.

References:

Continue ReadingCode view and UI View in design tab of bot wizard

Skills tab of SUSI AI Admin Panel

The Skills tab in SUSI.AI Admin Panel displays all the skills of SUSI in a tabular form. The table is created using the Table component of Ant Design. It’s preferred because we’re going to handle a lot of data here and that can turn out to be heavy if we use Google’s Material-ui.

Displaying the data:

The data is rendered in the form of a table which has seven columns – Name, Group, Language, Type, Author, Status and Action. The first 5 columns displays basic details of a skill. The “Status” column shows whether the skill has been reviewed by admin or not. All the reviewed skills are either “Approved” or “Not Approved”. The final column “Action” contains all the actions that admin can perform on the skill. Currently, an admin can change the review status of a skill. More actions will be added in future as this is still in beta.

All the column names are stored in a variable in the form of an array. The following code will demonstrate the way of making three columns – Name, Group, Language.

this.columns = [
  {
    title: 'Name',
    dataIndex: 'skill_name',
    sorter: false,
    width: '20%',
  },
  {
    title: 'Group',
    dataIndex: 'group',
    width: '15%',
  },
  {
    title: 'Language',
    dataIndex: 'language',
    width: '10%',
  },
];

All the skills are also stored in an array which is a state variable. These columns and skills are then passed to the Table component as props. The following code will demonstrate that:

<Table
  columns={this.columns}
  rowKey={record => record.registered}
  dataSource={this.state.skillsData}
/>

Fetching all the skills from SUSI Server:

All the public skills can be fetched from ListSkillService API of SUSI Server. We can filter all the skills using various filters. We want to display the skills alphabetically on Skills tab in Admin Panel. Hence, we filter the skills accordingly. To do this, we pass three parameters in the GET request. They are as follows:

  • applyFilter: true
  • filter_name: ascending
  • filter_type: lexicographical

This returns all the skills in an alphabetical order. The API request url looks like this:

https://api.susi.ai/cms/getSkillList.json?applyFilter=true&filter_name=ascending&filter_type=lexicographical

After fetching the skills, we put all the parameters of these skills required for our table in an object which is then pushed into an array. One object is created for each skill.
If the API call fails for some reason then a Google’s Material-ui Snackbar appears with a message that an error occurred.

Changing review status of a skill:

An admin can change the review status of any skill. This is done by making a GET request to ChangeSkillStatusService API. The request contains five parameters. They are as follows:

  1. Model: Model of the skill is passed here (string)
  2. Group: Group of the skill is passed here (string)
  3. Language: Language of the skill is passed here (string)
  4. Reviewed: true is passed is skill has been approved and false if skill is not approved. (boolean)
  5. Access_token: The access token of user is passed here. This is for verifying the user’s BASE ROLE. This is taken from cookies using cookies.get(‘loggedIn’). (string)

The call url looks like this:

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

If the API call is successful, then the review status of a skill is successfully changed. Otherwise, an error message is thrown.

References:

Continue ReadingSkills tab of SUSI AI Admin Panel

Code view in Configure tab of SUSI.AI Bot Wizard

The purpose of configure tab in SUSI.AI bot wizard is to provide the bot creator various options on how the bot will interact with different websites. It currently provides an option of enabling or disabling the chatbot on different websites.
The configure tab has two parts. One is the code view which allows the users to write websites on which they want to enable/disable the chatbot and a table below it which lists those websites.

The default code in code view is passed in a state in the Configure.js file. The following code demonstrates that:

this.state = {
  code: this.props.code,
};

Fetching data from code view:

After writing the websites on which the user wants the chatbot to be enabled/disabled and pressing on the ‘Save’ button, a functiongenerateConfigData is called.
This function takes the code in code view and stores in inside of a variable. Then it splits the code and makes two arrays:

  • enabledSites: This array contains all the websites that are written in  enabled field.
  • disabledSites: This array contains all the websites that are written in disabled field.

This process can be easily understood from the following code snippet:

let newCode = this.state.code;
let websiteData = newCode
 .split('::sites_enabled')[1]
 .split('::sites_disabled');
let enabledSites = websiteData[0].split(',');
let disabledSites = websiteData[1].split(',');

This data is stored inside an object.

Displaying the data:

The data fetched from the code view now has to be displayed in form of a table. This data is looks like this:

let configData = [
 {
   id: '1',
   name: 'website1.com',
   last: 'Jan 12, 2018 20:08 hrs',
   status: 1,
 },
 {
   id: '2',
   name: 'website2.com',
   last: 'Feb 19, 2018 13:00 hrs',
   status: 1,
 },
 {
   id: '3',
   name: 'website3.com',
   last: 'Mar 14, 2018 10:15 hrs',
   Status: 2,
 }
];

Status is 1 if the website is in enabled column and 2 if the website is in disabled column.
To display this data on the screen, we simply map through the data and display the rows of the table. The following code demonstrates it:

{configData.map((item, index) => {
 if (item.name) {
   return (
    <TableRow key={index}>
      <TableRowColumn style={{ fontSize: '16px' }}>
         {item.name}
     </TableRowColumn>
      <TableRowColumn style={{ fontSize: '16px' }}>
         {item.last}
     </TableRowColumn>
     <TableRowColumn>
       <SelectField
         floatingLabelText="Status"
         fullWidth={true}
         value={item.status}
       >
         <MenuItem value={1} primaryText="Enable" />
         <MenuItem value={2} primaryText="Disable" />
       </SelectField>
     </TableRowColumn>
    </TableRow>
   );
 }
})}

References:

Continue ReadingCode view in Configure tab of SUSI.AI Bot Wizard

Open Event Frontend – Settings Service

This blog illustrates how the settings of a particular user are obtained in the Open Event Frontend web app. To access the settings of the user a service has been created which fetches the settings from the endpoint provided by Open Event Server.

Let’s see why a special service was created for this.

Problem

In the first step of the event creation wizard, the user has the option to link Paypal or Stripe to accept payments. The option to accept payment through Paypal or Stripe was shown to the user without checking if it was enabled by the admin in his settings. To solve this problem, we needed to access the settings of the admin and check for the required conditions. But since queryRecord() returned a promise we had to re-render the page for the effect to show which resulted in this code:

canAcceptPayPal: computed('data.event.paymentCurrency', function() {     this.get('store').queryRecord('setting', {}) .then(setting => { this.set('canAcceptPayPal', (setting.paypalSandboxUsername || setting.paypalLiveUsername) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal); this.rerender(); });

This code was setting a computed property inside it and then re-rendering which is bad programming and can result in weird bugs.

Solution

The above problem was solved by creating a service for settings. This made sense as settings would be required at other places as well. The file was called settings.js and was placed in the services folder. Let me walk you through its code.

  • Extend the default Service provided by Ember.js and initialize store, session, authManager and _lastPromise.
import Service, { inject as service } from '@ember/service';
import { observer } from '@ember/object';

export default Service.extend({

 store       : service(),
 session     : service(),
 authManager : service(),

_lastPromise: Promise.resolve(),
  • The main method which fetches results from the server is called _loadSettings(). It is an async method. It queries setting from the server and then iterates through every attribute of the setting model and stores the corresponding value from the fetched result.
/**
* Load the settings from the API and set the attributes as properties on the service
*
* @return {Promise<void>}
* @private
*/
async _loadSettings() {
 const settingsModel = await this.get('store').queryRecord('setting', {});
 this.get('store').modelFor('setting').eachAttribute(attributeName => {
   this.set(attributeName, settingsModel.get(attributeName));
 });
},
  • The initialization of the settings service is handled by initialize(). This method returns a promise.
/**
* Initialize the settings service
* @return {*|Promise<void>}
*/
initialize() {
 const promise = this._loadSettings();
 this.set('_lastPromise', promise);
 return promise;
}
  • _authenticationObserver observes for changes in authentication changes and reloads the settings as required.
/**
* Reload settings when the authentication state changes.
*/
_authenticationObserver: observer('session.isAuthenticated', function() {
 this.get('_lastPromise')
   .then(() => this.set('_lastPromise', this._loadSettings()))
   .catch(() => this.set('_lastPromise', this._loadSettings()));
}),

The service we created can be directly used in the app to fetch the settings for the user. To solve the Paypal and Stripe payment problem described above, we use it as follows:

canAcceptPayPal: computed('data.event.paymentCurrency', 'settings.paypalSandboxUsername', 'settings.paypalLiveUsername', function() {
 return (this.get('settings.paypalSandboxUsername') || this.get('settings.paypalLiveUsername')) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal;
}),

canAcceptStripe: computed('data.event.paymentCurrency', 'settings.stripeClientId', function() {
 return this.get('settings.stripeClientId') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).stripe;
}),

Thus, there is no need to re-render the page and dangerously set the property inside its computed method.

References

Continue ReadingOpen Event Frontend – Settings Service

Using routerLink to make result tabs as links

In Susper, it was required that different result sections i.e All, Images,Videos and News should behave as links. Links provide us an advantage of opening the different result tabs in a New Tab by right clicking on it. Also at the same time we wanted that Active tabs must not behave as links. We wanted that tabs should behave in a similar way as Google, Bing and other search engine.In this blog, I will describe how we have achieved this by using routerLink and [hidden] attribute in Angular.

Steps:

1.Adding routerLink attribute in anchor tag.

Now, To make any element as link we must enclose it in anchor tag, therefore we will use <a> tag enclosing the <li> tag to make it a link.

<a [routerLink]="['/search']" [queryParams]="{query:this.searchdata.query,start:this.searchdata.start,rows:this.searchdata.rows,resultDisplay:'all',mode:'text',nopagechange:'false',append:'false'}"><li [class.active_view]="Display('all')" (click)="docClick()">All</li></a>
<a [routerLink]="['/search']" [queryParams]="{query:this.searchdata.query,start:this.searchdata.start,rows:this.searchdata.rows,mode:'text',nopagechange:'false',append:'false',fq:'url_file_ext_s%3A(png%2BOR%2Bjpeg%2BOR%2Bjpg%2BOR%2Bgif)',resultDisplay:'images'}"><li [class.active_view]="Display('images')" (click)="imageClick()">Images</li></a>
<a [routerLink]="['/search']" [queryParams]="{query:this.searchdata.query,start:this.searchdata.start,rows:this.searchdata.rows,resultDisplay:'videos',mode:'text',nopagechange:'false',append:'false',fq:'url_file_ext_s%3A(avi%2BOR%2Bmov%2BOR%2Bflv%2BOR%2Bmp4)'}"><li [class.active_view]="Display('videos')" (click)="videoClick()">Videos</li></a>
<a [routerLink]="['/search']" [queryParams]="{query:this.searchdata.query,start:this.searchdata.start,rows:this.searchdata.rows,resultDisplay:'news',mode:'text',nopagechange:'false',append:'false'}"><li [class.active_view]="Display('news')" (click)="newsClick()">News</li></a>

Now we must navigate to a link on the click of this tabs, for this we can simply use href attribute which we earlier tried to use but there is a disadvantage of using href tag. Using href tab reloads the whole page when we try to navigate to another tab.

Therefore we will use routerLink to achieve the same functionality as Susper is a single page application, where the page should not be reloaded. routerLink navigates to the new url and the component is rendered in place of <router-outlet> without reloading the whole page.

After using routerLink, we will use queryParams attribute to pass routing parameters to a route. Since these parameters will be variable we must use string interpolation to set query parameters. We will use searchdata object and set query parameters according to its value.

Now we are done with making tabs as links. But as of now all tabs will behave a links even if it is an active tab. Therefore we need to hide the link property of any tab when it is active.

2.Hiding the link property of the active tab:

Now we will use to different elements for each tab, one as link and other as normal li element.For hiding the link elements when the tab is active we will use [hidden] attribute in angular. We will just hide the link element when the value of resultDisplay variable is similar to current tab.

<a [routerLink]="['/search']" [queryParams]="{query:this.searchdata.query,start:this.searchdata.start,rows:this.searchdata.rows,resultDisplay:'all',mode:'text',nopagechange:'false',append:'false'}" [hidden]="this.resultDisplay==='all'"><li [class.active_view]="Display('all')" (click)="docClick()">All</li></a>
        <li [class.active_view]="Display('all')" (click)="docClick()" *ngIf="Display('all')">All</li>

For hiding the simple li element we will use *ngIf attribute which will remove the simple li element from the DOM for un active tabs. We might have used *ngIf attribute to hide link elements but rendering a lot of elements on DOM slows down the page and affects performance.

We will follow this for rest of the tabs also and we are done with our changes.

Now we can the inactive tabs will behave as link and the active tabs will not behave as link similar to Google.

Resources

  1. Angular Routing: https://angular.io/guide/router
  2. ngIf and [hidden]: https://stackoverflow.com/questions/39777990/angular2-conditional-display-bind-to-hidden-property-vs-ngif-directive
  3. Angular queryParams: https://stackoverflow.com/a/46008187
Continue ReadingUsing routerLink to make result tabs as links

Open Event Server – Export Speakers as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the speakers in a very detailed view in the event management dashboard. He can see the statuses of all the speakers. The possible statuses are pending, accepted and rejected. He/she can take actions such as editing/viewing speakers.

If the organizer wants to download the list of all the speakers as a CSV file, he or she can do it very easily by simply clicking on the Export As CSV button in the top right-hand corner.

Let us see how this is done on the server.

Server side – generating the Speakers CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_speakers_csv which takes the speakers to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_speakers_csv(speakers):
   headers = ['Speaker Name', 'Speaker Email', 'Speaker Session(s)',
              'Speaker Mobile', 'Speaker Bio', 'Speaker Organisation', 'Speaker Position']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each speaker in speakers and form a row for that speaker by separating the values of each of the columns by a comma. Here, every row is one speaker.
  • As a speaker can contain multiple sessions we iterate over each session for that particular speaker and append each session to a string. ‘;’ is used as a delimiter. This string is then added to the row. We also include the state of the session – accepted, rejected, confirmed.
  • The newly formed row is added to the rows list.
for speaker in speakers:
   column = [speaker.name if speaker.name else '', speaker.email if speaker.email else '']
   if speaker.sessions:
       session_details = ''
       for session in speaker.sessions:
           if not session.deleted_at:
               session_details += session.title + ' (' + session.state + '); '
       column.append(session_details[:-2])
   else:
       column.append('')
   column.append(speaker.mobile if speaker.mobile else '')
   column.append(speaker.short_biography if speaker.short_biography else '')
   column.append(speaker.organisation if speaker.organisation else '')
   column.append(speaker.position if speaker.position else '')
   rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
with open(file_path, "w") as temp_file:
   writer = csv.writer(temp_file)
   from app.api.helpers.csv_jobs_util import export_speakers_csv
   content = export_speakers_csv(speakers)
   for row in content:
       writer.writerow(row)

Obtaining the Speakers CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/speakers/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the speakers of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

Resources

Continue ReadingOpen Event Server – Export Speakers as CSV File

Bottom Navigation Bar in Open Event Android

Bottom navigation bar provides an easy access and convenient way for users to navigate in between different sections of an App where the sections are of equal importance. We use bottom navigation move between different fragments such as events screen, favorites, tickets profile and so on. This blog post will guide you on how its implemented in Open Event Android.

To create a bottom navigation bar first create items for your menu. Menu items will be rendered as elements of the bottom navigation bar. Add icon and title to your navigation item here.

   <item
       android:id=“@+id/navigation_profile”
       android:icon=“@drawable/ic_baseline_account_circle_24px”
       android:title=“@string/profile” />

After we have written menu xml for our navigation view, we will have to create a view for it. The following snippet of code creates a BottomNavigationView type view element on the screen with different attributes as specified and menu file as specified by app:menu. Make sure you have android support design dependency set up before this.

<android.support.design.widget.BottomNavigationView
           android:id=“@+id/navigation”
           android:layout_width=“match_parent”
           android:layout_height=“wrap_content”
           android:layout_gravity=“bottom”
           android:background=“?android:attr/windowBackground”
           app:itemBackground=“@android:color/white”
           android:foreground=“?attr/selectableItemBackground”
           app:itemTextColor=“@color/colorPrimaryDark”
           app:menu=“@menu/navigation” />

No that our view will be created we need to wire menu items with different fragment and set up listener for item selected events.

Inside onCreate of MainActivity we attach a ItemSelectListener with the bottom navigation bar, navigation.setOnNavigationItemSelectedListener(listener) .

private val listener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
       val fragment: Fragment
       when (item.itemId) {
           R.id.navigation_profile -> {
               supportActionBar?.title = “Profile”
               fragment = ProfileFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
           R.id.navigation_favorite -> {
               supportActionBar?.title = “Likes”
               fragment = FavoriteFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
           R.id.navigation_tickets -> {
               supportActionBar?.title = “Tickets”
               fragment = OrdersUnderUserFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
       }
       false
   }

The above listener object listens for item selection in the bottom navigation bar. Whenever any item is selected lambda function is executed and item.itemId is matched with different menu item ids. On any successful match, an instance of the fragment corresponding to the selected item is loaded.

References

Continue ReadingBottom Navigation Bar in Open Event Android

Open Event Server – Export Sessions as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the sessions in a very detailed view in the event management dashboard. He can see the statuses of all the sessions. The possible statuses are pending, accepted, confirmed and rejected. He/she can take actions such as accepting/rejecting the sessions.

If the organizer wants to download the list of all the sessions as a CSV file, he or she can do it very easily by simply clicking on the Export As CSV button in the top right-hand corner.

Let us see how this is done on the server.

Server side – generating the Sessions CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_sessions_csv which takes the sessions to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_sessions_csv(sessions):
   headers = ['Session Title', 'Session Speakers',
              'Session Track', 'Session Abstract', 'Created At', 'Email Sent']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each session in sessions and form a row for that session by separating the values of each of the columns by a comma. Here, every row is one session.
  • As a session can contain multiple speakers we iterate over each speaker for that particular session and append each speaker to a string. ‘;’ is used as a delimiter. This string is then added to the row.
  • The newly formed row is added to the rows list.
for session in sessions:
   if not session.deleted_at:
       column = [session.title + ' (' + session.state + ')' if session.title else '']
       if session.speakers:
           in_session = ''
           for speaker in session.speakers:
               if speaker.name:
                   in_session += (speaker.name + '; ')
           column.append(in_session[:-2])
       else:
           column.append('')
       column.append(session.track.name if session.track and session.track.name else '')
       column.append(strip_tags(session.short_abstract) if session.short_abstract else '')
       column.append(session.created_at if session.created_at else '')
       column.append('Yes' if session.is_mail_sent else 'No')
       rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
writer = csv.writer(temp_file)
from app.api.helpers.csv_jobs_util import export_sessions_csv
content = export_sessions_csv(sessions)
for row in content:
   writer.writerow(row)

Obtaining the Sessions CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/sessions/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the sessions of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

Resources

Continue ReadingOpen Event Server – Export Sessions as CSV File

Disable editing for non-editable skills for non-admin users

As the Skills in SUSI Skill CMS are publicly editable, any user has the access to edit them. Hence, there needed to be a better control over who can edit the Skills in CMS. We needed to implement a feature to allow Admins and higher user roles to change the status of a Skill to non-editable. The subsequent implementation on CMS would require disabling editing for non-editable Skills for non-admin users. This blog post explains how this feature has been implemented in SUSI.AI.

Adding a boolean parameter ‘editable’ to the Skill metadata

We needed to add a boolean parameter in the Skill metadata for each Skill. The boolean parameter is ‘editable’. If its value is true, then it implies that editing should be allowed for that Skill. If it is set to false, then the Skill should not be editable for non-admin users. By default, its value has been set to true for all Skills. This is implemented as follows in the SusiSkill.java file:

    // in the getSkillMetadata() method
  skillMetadata.put("editable", getSkillEditStatus(model, group, language, skillname));

    // declaration of the getSkillEditStatus() method
    public static boolean getSkillEditStatus(String model, String group, String language, String skillname) {
        // skill status
        JsonTray skillStatus = DAO.skillStatus;
        if (skillStatus.has(model)) {
            JSONObject modelName = skillStatus.getJSONObject(model);
            if (modelName.has(group)) {
                JSONObject groupName = modelName.getJSONObject(group);
                if (groupName.has(language)) {
                    JSONObject languageName = groupName.getJSONObject(language);
                    if (languageName.has(skillname)) {
                        JSONObject skillName = languageName.getJSONObject(skillname);

                        if (skillName.has("editable")) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

 

Allowing Admin and higher user roles to change edit status of any Skill

This is facilitated by the endpoint ‘/cms/changeSkillStatus.json’. Its minimum base user role is set to Admin so that only Admins and higher user roles are able to change status of any Skill. A sample API call to this endpoint to change the edit status of any Skill to ‘false’ is as follows:

http://127.0.0.1:4000/cms/changeSkillStatus.json?model=general&group=Knowledge&language=en&skill=aboutsusi&editable=false&access_token=zdasIagg71NF9S2Wu060ZxrRdHeFAx

 

If we want to change the edit status of any Skill to ‘false’, then we need to add the Skill to the ‘skillStatus.json’ file. For this, we need to traverse inside the JSONObject in the ‘skillStatus.json’ file. We need to traverse inside the model, group and language as specified in the query parameters. This is done as follows:

   if(editable.equals("false")) {
       skill_status.put("editable", false);
   }

   JsonTray skillStatus = DAO.skillStatus;

   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)) {
                    skillName = languageName.getJSONObject(skill_name);

                    if(editable != null && editable.equals("false")) {
                        skillName.put("editable", false);
                    }
                    else if(editable != null && editable.equals("true")) {
                        skillName.remove("editable");
                    }

                    skillStatus.commit();
                    result.put("accepted", true);
                    result.put("message", "Skill status changed successfully.");
                    return new ServiceResponse(result);
                }
            }
        }
    }

 

If we want to change the edit status of any Skill to ‘true’, then we need to remove the Skill from the ‘skillStatus.json’ file. We also need to remove all the empty JSONObjects inside the ‘skillStatus.json’ file, if they are created in the process of removing Skills from it. This is done as follows:

   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)) {
                    skillName = languageName.getJSONObject(skill_name);
                    if(editable != null && editable.equals("true")) {
                        skillName.remove("editable");
                    }
                    if(skillName.length() == 0) {
                        languageName.remove(skill_name);
                        if(languageName.length() == 0) {
                            groupName.remove(language_name);
                            if(groupName.length() == 0) {
                                modelName.remove(group_name);
                                if(modelName.length() == 0) {
                                    skillStatus.remove(model_name);
                                }
                            }
                        }
                    }
                    skillStatus.commit();
                }
            }
        }
    }

 

Disabling editing for non-editable Skills for non-admin users on Skill CMS

For the Skills whose edit status has been set to ‘false’ by the Admins, we need to allow the non-admin users to only be able to view the code of the Skill, and not permit them to change the code and save the changes to the Skill. We need to display a message to the users about the possible reasons. All the code for displaying the message is put in an if() condition as follows:

   if (
      cookies.get('loggedIn') &&
      !this.state.editable &&
      !this.state.showAdmin
    )

 

This is how the Skill edit page for a non-editable Skill would look like for a non-admin user:

For an Admin user, this would look exactly same like an editable Skill page. Admin user would be able to edit and make changes to the Skill code and save the changes.

This is how editing of non-editable Skills have been disabled for non-admin users.

Resources

Continue ReadingDisable editing for non-editable skills for non-admin users