Addition of new filters for event search

Open Event has an event search provided but it lacked two of the userful filters which will make searching for an event for a user easier than the current ecosystem. In this blog post, I describe how I implemented filtering of events on the basis of CFP status and Ticket Type.

About the issue

Addition of these two filters were subparts of improving the browse events page better. The browse events page currently functions in an optimal way but these improvements make it even better.

How the filters are added

For adding these two filters, it was required that the request sent to server to filter the event must contain ticket type and cfp so that the results from the server can be received. To achieve this, the frontend code needed the following changes:

  1. Passing of request variable into sidebar component present in app/templates/explore.hbs and addition of a clear filter for the same in the same file.e startDate=start_date endDate=end_date location=location ticket_type=ticket_type}
{{explore/side-bar model=model category=category sub_category=sub_category event_type=event_type startDate=start_date endDate=end_date location=location ticket_type=ticket_type}}
{{#if filters.ticket_type}}
  <div class=”ui mini label”>
    {{ticket_type}}
    <a role=”button” {{action ‘clearFilter’ ‘ticket_type’}}>
      <i class=”icon close”></i>
    </a>
  </div>
{{/if}}
  1. Adding UI for both the filters in app/templates/components/explore/side-bar.hbs. Accordion UI is used to achieve this.
  <div class=”item”>
    {{#ui-accordion}}
      <span class=”title”>
        <i class=”dropdown icon”></i>
        {{t ‘Ticket Type’ }}
      </span>
      <div class=”content menu”>
        <a href=”#”
          class=”link item {{if (eq ticket_type ‘free’) ‘active’}}”
          {{action ‘selectTicketType’ ‘free’}}>
          {{t ‘Free’}}
        </a>
        <a href=”#”
          class=”link item {{if (eq ticket_type ‘paid’) ‘active’}}”
          {{action ‘selectTicketType’ ‘paid’}}>
          {{t ‘Paid’}}
        </a>
      </div>
    {{/ui-accordion}}
  </div>
  1. Editing the routes (app/routes/explore.js) to make sure if the request has new filter parameter then it should be sent to server.
if (params.ticket_type) {
      filterOptions.push({
        name : ‘tickets’,
        op   : ‘any’,
        val  : {
          name : ‘type’,
          op   : ‘eq’,
          val  : params.ticket_type
        }
      });
    }
  1. Addition of new request parameter in the controller (app/controllers/explore.js)
queryParams  : [‘category’, ‘sub_category’, ‘event_type’, ‘start_date’, ‘end_date’, ‘location’, ‘ticket_type’],

ticket_type  : null,

if (filterType === ‘ticket_type’) {  this.set(‘ticket_type’, null);
}
  1. Editing the sidebar component to set the value of request parameter when the user interacts with the filter.
hideClearFilters: computed(‘category’, ‘sub_category’, ‘event_type’, ‘startDate’, ‘endDate’, ‘location’, ‘ticket_type’, function() {
    return !(this.category || this.sub_category || this.event_type || this.startDate || this.endDate || this.location || this.ticket_type !== null);
}),

selectTicketType(ticketType) {
  this.set(‘ticket_type’, ticketType === this.ticket_type ? null : ticketType);
},

this.set(‘ticket_type’, null);

Sidebar after the addition of filters.

Resources:

Issue: https://github.com/fossasia/open-event-frontend/issues/3098
Pull Requests:

Ticket Type: https://github.com/fossasia/open-event-frontend/pull/3158
CFS: https://github.com/fossasia/open-event-frontend/pull/3144

Specifying Query Params: https://guides.emberjs.com/release/routing/query-params/

UI Resources for the feature: https://semantic-org.github.io/Semantic-UI-Ember/#/modules/accordion

Continue Reading Addition of new filters for event search

Feeds Moderation in loklak Media Wall

Loklak Media Wall provides client side filters for entities received from loklak status.json API like blocking feeds from a particular user, removing duplicate feeds, hiding a particular feed post for moderating feeds. To implement it, we need pure functions which remove the requested type of feeds and returns a new array of feeds. Moreover, the original set of data must also be stored in an array so that if filters are removed, the requested data is provided to the user

In this blog, I would be explaining how I implemented client side filters to filter out a particular type of feeds and provide the user with a cleaner data as requested.

Types of filters

There are four client-side filters currently provided by Loklak media wall:

    • Profanity Filter: Checks for the feeds that might be offensive and removes it.
    • Remove Duplicate: Removes duplicate feeds and the retweets from the original feeds
    • Hide Feed: Removes a particular feed from the feeds
    • Block User: Blocks a User and removes all the feeds from the particular user

It is also important to ensure that on pagination, new feeds are filtered out based on the previous user requested moderation.

Flow Chart

The flow chart explains how different entities received from the server is filtered and how original set of entities is maintained so that if the user removes the filter, the original filtered entities are recovered.

Working

Profanity Filter

To avoid any obscene language used in the feed status to be shown up on media wall and providing a rather clean data, profanity filter can be used. For this filter, loklak search.json APIs provide a field classifier_profanity which states if there is some swear word is used in the status. We can check for the value of this field and filter out the feed accordingly.

export function profanityFilter(feeds: ApiResponseResult[]): ApiResponseResult[] {
const filteredFeeds: ApiResponseResult[] = [];
feeds.forEach((feed) => {
if ( feed.classifier_language !== null && feed.classifier_profanity !== undefined ) {
if (feed.classifier_profanity !== ‘sex’ &&  feed.classifier_profanity !== ‘swear’) {
filteredFeeds.push(feed);
}
}
else {
filteredFeeds.push(feed);
}
});
return filteredFeeds || feeds;
}

Here, we check if the classifier_profanity field is either not ‘swear’ or ‘sex’ which clearly classifies the feeds and we can push the status accordingly. Moreover, if no classifier_profanity field is provided for a particular field, we can push the feed in the filtered feeds.

Remove Duplicate

Remove duplicate filter removes the tweets that are either retweets or even copy of some feed and return just one original feed. We need to compare field id_str which is the status id of the feed and remove the duplicate feeds. For this filter, we need to create a map and compare feeds on map object and remove the duplicate feeds iteratively and return the array of feeds with unique elements.

export function removeDuplicateCheck(feeds: ApiResponseResult[]): ApiResponseResult[] {
const map = { };
const filteredFeeds: ApiResponseResult[] = [];
const newFeeds: ApiResponseResult[] = feeds;
let v: string;
for (let a = 0; a < feeds.length; a++) {
v = feeds[a].id_str;
if (!map[v]) {
filteredFeeds.push(feeds[a]);
map[v] = true;
}
}
return filteredFeeds;
}

Hide Feed

Hide Feed filter can be used to hide a particular feed from showing up on media wall. It can be a great option for the user to hide some particular feed that user might not want to see. Basically, when the user selects a particular feed, an action is dispatched with payload being the status id i.e. id_str. Now, we pass feeds and status id through a function which returns the particular feed. All the feeds with the same id_str are also removed from the feeds array.

export function hideFeed(feeds: ApiResponseResult[], statusId: string ): ApiResponseResult[] {
const filteredFeeds: ApiResponseResult[] = [];
feeds.forEach((feed) => {
if (feed.id_str !== statusId) {
filteredFeeds.push(feed);
}
});
return filteredFeeds || feeds;
}

User can undo the action and let the filtered feed again show up on media wall. Now, for implementing this, we need to pass original entities, filtered entities and the id_str of the particular entity through a function which checks for the particular entity with the same id_str and add it in the filtered entities and return the new array of filtered entities.

export function showFeed(originalFeeds: ApiResponseResult[], feeds: ApiResponseResult[], statusId: string ): ApiResponseResult[] {
const newFeeds = […feeds];
originalFeeds.forEach((feed) => {
if (feed.id_str === statusId) {
newFeeds.push(feed);
}
});
return newFeeds;
}

Block User

Block User filter can be used blocking feeds from a particular user/account from showing up on media wall. To implement this, we need to check for the User ID field user_id of the user and remove all the feeds from the same User ID. The function accountExclusion takes feeds and user_id (of the accounts) as a parameter and returns an array of filtered feeds removing all the feeds of the requested users/accounts.

export function accountExclusion(feeds: ApiResponseResult[], userId: string[] ): ApiResponseResult[] {
const filteredFeeds: ApiResponseResult[] = [];
let flag: boolean;
feeds.forEach((feed) => {
flag = false;
userId.forEach((user) => {
if (feed.user.user_id === user) {
flag = true;
}
});
if (!flag) {
filteredFeeds.push(feed);
}
});return filteredFeeds || feeds;
}

Key points

It is important to ensure that the new feeds (received on pagination or on a new query) must also be filtered according to the user requested filter. Therefore, before storing feeds in a state and supplying to templates after pagination, it must be ensured that new entities are also filtered out. For this, we need to keep boolean variables as a state property which checks if a particular filter is requested by a user and applies the filter to the new feeds accordingly and store filtered feeds in the filteredFeeds accordingly.

Also, the original feeds must be stored separately so that on removing filters the original feeds are regained. Here, entities stores the original entities received from the server.

case apiAction.ActionTypes.WALL_SEARCH_COMPLETE_SUCCESS: {
const apiResponse = action.payload;
let newFeeds = accountExclusion(apiResponse.statuses, state.blockedUser);
if (state.profanityCheck) {
newFeeds = profanityFilter(newFeeds);
}
if (state.removeDuplicate) {
newFeeds = removeDuplicateCheck(newFeeds);
}return Object.assign({}, state, {
entities: apiResponse.statuses,
filteredEntities: newFeeds,
lastResponseLength: apiResponse.statuses.length
});
}

case wallPaginationAction.ActionTypes.WALL_PAGINATION_COMPLETE_SUCCESS: {
const apiResponse = action.payload;
let newFeeds = accountExclusion(apiResponse.statuses, state.blockedUser);
if (state.profanityCheck) {
newFeeds = profanityFilter(apiResponse.statuses);
}
let filteredEntities = […newFeeds, state.filteredEntities];
if (state.removeDuplicate) {
filteredEntities = removeDuplicateCheck(filteredEntities);
}

return Object.assign({}, state, {
entities: [ apiResponse.statuses, state.entities ],
filteredEntities
});
}

Reference

Continue Reading Feeds Moderation in loklak Media Wall

Implementing Tracks Filter in Open Event Webapp using the side track name list

4f451d29-c6c2-44be-9ef7-d91a45fc1eb0.png

On Clicking the Design, Art, Community Track

ff85d907-512b-4a41-be49-888bbd17bf83.png

But, it was not an elegant solution. We already had a track names list present on the side of the page which remained unused. A better idea was to use this side track names list to filter the sessions. Other event management sites like http://sched.org follow the same idea. The relevant issue for it is here and the major work can be seen in this Pull Request. Below is the screenshot of the unused side track names list.

5b15a297-fd5e-4c23-bc1b-dbed193db0f4.png

The end behavior should be something like this, the user clicks on a track and only sessions belonging to the track should be visible and the rest be hidden. There should also be a button for clearing the applied filter and reverting the page back to its default view. Let’s jump to the implementation part.

First, we make the side track name list and make the individual tracks clickable.

<div class="track-names col-md-3 col-sm-3"> 
  {{#tracknames}}
    <div class="track-info">
      <span style="background-color: {{color}};" 
      class="titlecolor"></span>
      <span class="track-name" style="cursor: pointer">{{title}}
      </span>
    </div>
  {{/tracknames}}
</div>

f45f1591-937d-4e2f-9245-237cfaf3af0d.png

Now we need to write a function for handling the user click event on the track name. Before writing the function, we need to see the basic structure of the tracks page. The divs with the class date-filter contain all the sessions scheduled on a given day. Inside that div, we have another div with class tracks-filter which contains the name of the track and all the sessions of that track are inside the div with class room-filter.

Below is a relevant block of code from the tracks.hbs file

<div class="date-filter">
  // Contains all the sessions present in a single day
  <div class="track-filter row">
    // Contains all the sessions of a single track
    <div class="row">
      // Contains the name of the track
      <h5 class="text">{{caption}}</h4>
    </div>
    <div class="room-filter" id="{{session_id}}">
      // Contain the information about the session
    </div>
  </div>
</div>

We iterate over all the date-filter divs and check all the track-filter divs inside it. We extract the name of the track and compare it to the name of the track which the user selected. If both of them are same, then we show that track div and all the sessions inside it. If the names don’t match, then we hide that track div and all the content inside it. We also keep a variable named flag and set it to 0 initially. If the user selected track is present on a given day, we set the flag to 1. Based on it, we decide whether to display that particular day or not. If the flag is set, we display the date-filter div of that day and the matched track inside it. Otherwise, we hide the div and all tracks inside it.

$('.track-name').click(function() {
  // Get the name of the track which the user clicked
  trackName = $(this).text();
  // Show the button for clearing the filter applied and reverting to the default view
  $('#filterInfo').show();
  $('#curFilter').text(trackName);
  // Iterate through the divs and show sessions of user selected track
  $('.date-filter').each(function() {
    var flag = 0;
    $(this).find('.track-filter').each(function() {
      var name = $(this).find('.text').text();
      if(name != trackName) {
        $(this).hide();
        return;
      }
      flag = 1;
      $(this).show();
    });
    if (flag) {
     $(this).show();
    } else {
      $(this).hide();
    }
  });
});

On Selecting the Android Track of FOSSASIA Summit, we see something like this

935f208b-c17c-4d41-abb6-7197c003d962.png

Now the user may want to remove the filter. He/she can just click on the Clear Filter button shown in the above screenshot to remove the filter and revert back to the default view of the page.

$('#clearFilter').click(function() {                                                                                                   
  trackFilterMode = 0;                                                                                                                 
  display();                                                                                                                           
  $('#filterInfo').hide();                                                                                                             
});

Back to the default view of the page

2ff61ccf-17cf-4595-9c2f-e6825de549f7.png

References:

Continue Reading Implementing Tracks Filter in Open Event Webapp using the side track name list