Implementing Session Bookmark Feature in Open Event Webapp

The event websites generated by Open Event Webapp can be large in size with a massive number of sessions in it. As such, it becomes a little difficult for the user to keep track of the sessions he is interested in without any bookmark facility.  With the help of it, he/she can easily mark a session as his favorite and then see them all at once at the click of the button. This feature is available and synchronized across all the pages (track, rooms, and schedule) of the event. That is, if you bookmark a session on the tracks page, it would automatically appear on the other pages too. Likewise, if you unmark a session on any of the page, it would be reflected on rest of the pages.

The event websites generated by the webapp are static in nature. It means that there is no backend server and database where the data about the bookmarked sessions and the user can be stored. So how are we storing information about the marked sessions and persisting it across different browser sessions? There must be a place where we are storing the data! Yes. It’s the LocalStorage Object. HTML5 introduced a localStorage object for storing information which persists even when the site or the browser is closed and opened again. Also, the max size of the localStorage object is quite sufficient (around 5 – 10 MB) for storing textual data and should not be a bottleneck in the majority of the cases.

One very important thing to remember about the localStorage object is that it only supports strings in key and value pairs. Hence, we can’t store any other data type like Boolean, Number or Date directly in it. We would have to convert the concerned object into a string using the JSON.stringify() method and then store it.

Okay, let’s see how we implement the bookmark facility of sessions. We initialize the localStorage object. We create a key in it which is the name of the event and corresponding to its value, we store an object which will contain the information about the bookmarked sessions of that particular event. It’s key will be the id of the session and the value (0 or 1) will define whether it is bookmarked or not. Initially, when no sessions have been bookmarked, it will be empty.

function initPage() {
 //Check whether the localStorage object is already instantiated

 if(localStorage.hasOwnProperty(eventName) === false) {
   localStorage[eventName] = '{}'; // This object will store the bookmarked sessions info
 }
}

Now, we need to actually provide a button to the user for marking the sessions and then link it to the localStorage object. This is the session element with the star-shaped bookmark button

b77bf0d9-7303-4c03-ae29-bd8a4725002b.png

Here is the related code

<a class="bookmark" id="b{{session_id}}">
 <i class="fa fa-star" aria-hidden="true">
 </i>
</a>

Then we need to handle the click event which occurs when the user clicks this button. We will extract the id of the session and then store this info in the event object inside the localStorage object.

$('.bookmark').click(function (event) {
   // Get the event object from the localStorage
   var temp = JSON.parse(localStorage[eventName]);

   // Get the current color of the bookmark icon. If it's black, it means that the session is already bookmarked.
   var curColor = $(this).css("color");

   // Extract the id of the session
   var id = $(this).attr('id').substring(1);

   if (curColor == "rgb(0, 0, 0)") {
     // Unmark the session and revert the font-icon color to default
     temp[id] = 0;
     $(this).css("color", "");
   } else {
     // Bookmark the session and color the font icon black
     temp[id] = 1;
     $(this).css("color", "black");
   }

   localStorage[eventName] = JSON.stringify(temp);
   event.stopPropagation();
 });

So suppose, our event name is FOSSASIA Summit and we bookmarked sessions with the id of 2898 and 3100 respectively, then the structure of the localStorage object will be something like this:-

{'FOSSASIA Summit': { '2898': '1', '3100': '1'}};

To finally show all the bookmarked sessions at once, we defined two modes of the page: Starred and All. In the Starred mode, only the bookmarked sessions are shown. We simply select the event object inside the localStorage and iterate over all the session id’s and only show those which have their value set to 1. In the latter mode, all the sessions are displayed.

Starred Mode (Showing only the bookmarked sessions)

ca1f5fa8-29a2-4b96-8eb1-b22d8b07bda8.png

All Mode (Every session is displayed, both marked and unmarked)

cd23f5f5-4bd1-4d1e-aa96-f4aa28cb5e07.png

References:

Continue Reading

Implementing PNG Export of Schedule in Open Event Webapp

Fortunately for us, we don’t have to implement it from scratch (which would have been extremely difficult and time-consuming). Enter html2canvas library. It renders an element onto the canvas after which we can convert it into an image. I will now explain how we implemented png export in the calendar mode. You can view the whole schedule template file here

Here is a screenshot of calendar or grid view of the schedule. Currently selected date is 18th Mar, Saturday. The PNG Export button is on the top-right corner beside the ‘Calendar View’ button.

32fa6d15-59da-4521-b348-6c01f6af7825.png

Here is a little excerpt of the basic structure of the calendar mode of the sessions. I have given an overview of it in the comments.

<div class="{{slug}} calendar">
 <!-- slug represents the currently selected date -->
 <!-- This div contains all the sessions scheduled on the selected date -->
 <div class="col-md-12 paddingzero">
   <!-- Contain content related to current date and time -->
 </div>
 <div class="calendar-content">
   <div class="times">
     <!-- This div contains the list of all the session times on the current day -->
     <!-- It is the left most column of the grid view which contains all the times →
     <div class="time">
       <!-- This div contains information about the particular time -->
     </div>
   </div>
   <div class="rooms">
     <!-- This div contains all the rooms of an event -->
     <!-- Each particular room has a set of sessions associated with it on that particular date -->
     <div class="room">
       <!-- This div contains the list of session happening in a particular room -->
       <!-- Session Details -->
     </div>
   </div>
 </div>
</div>

Now, let us see how we will actually capture an image of the HTML element shown above. Here is the code related to it:

$(".export-png").click(function() {
 if (isCalendarView === true) {

   $('.calendar').each(function() {
     if ($(this).attr('class').split(' ').indexOf('hide') <= 0) {

       $timeline = $(this);
       initialWidth = $timeline.width();
       numberOfChildElements = $timeline.find('.rooms')[0].childElementCount;
       numberOfChildElements = numberOfChildElements - 1;
       widthOfChild = $timeline.find('.room').width();
       canvasWidth = numberOfChildElements * widthOfChild + 50;
       $timeline.width(canvasWidth);
     }
   });
 }

 html2canvas($timeline, {
   onrendered: function(canvas) {
     canvas.id = "generated-canvas";
     canvas.toBlob(function(blob) {
       saveAs(blob, '' + $timeline.attr('class') + '.png');
     });
   },
 });
 $timeline.width(initialWidth);
});

Note that this initial width calculated is the width which is visible to us on the screen. In reality, the element might be scrollable and its actual width might be different. If we render the element using the initial width, we would not be able to see the full contents of that element. It will not show the whole view. Hence we need to calculate the actual width.Let us see what is going on in this code. When the user clicks on the export PNG button, we check whether we are in the calendar mode or not. If yes, then we proceed further. We then see which date is currently selected and accordingly select that div. After selecting it, we then get the initial width of that element.

So, we check all the child elements inside it, get their count and width and then calculate the actual width of the parent element based on it. Temporarily, we set this actual width as the width of the session element and pass it to the html2canvas function. That in turn, renders the whole element onto a canvas. After it has been successfully rendered onto the canvas, we save it to as an image and present a download box to the user for downloading that image.

Here is the download pop-up box

bfbd12c2-f6ed-4dae-8485-bfd1638faf74.png

And this is the downloaded PNG Image. Click on it for a higher resolution!

Screenshot from 2017-07-03 00-31-56.png

Resources

Continue Reading

Integrating Selenium Testing in the Open Event Webapp

Enter Selenium. Selenium is a suite of tools to automate web browsers across many platforms. Using Selenium, we can control the browser and instruct it to do an ‘action’ programmatically. We can then check whether that action had the appropriate reaction and make our test cases based on this concept. There are various implementations of Selenium available in many different languages: Java, Ruby, Javascript, Python etc. As the main language used in the project is Javascript, we decided to use it.

https://www.npmjs.com/package/selenium-webdriver
https://seleniumhq.github.io/selenium/docs/api/javascript/index.html

After deciding on the framework to be used in the project, we had to find a way to integrate it in the project. We wanted to run the tests on every PR made to the repo. If it failed, the build would be stopped and it would be shown to the user. Now, the problem was that the Travis doesn’t natively support running Selenium on their virtual machine. Fortunately, we have a company called Sauce Labs which provides automated testing for the web and mobile applications. And the best part, it is totally free for open source projects. And Travis supports Sauce Labs. The details of how to connect to Sauce Labs is described in detail on this page:

https://docs.travis-ci.com/user/gui-and-headless-browsers/

Basically, we have to create an account on Sauce Labs and get a sauce_username and sauce_access_key which will be used to connect to the sauce cloud. Travis provides a sauce_connect addon which creates a tunnel which allows the Sauce browsers to easily access our application. Once the tunnel is established, the browser in the Sauce cloud can use it to access the localhost where we serve the pages of the generated sites. A little code would make it more clear at this stage:

Here is a short excerpt from the travis.yml file :-

addons:
 sauce_connect:
   username: princu7
 jwt:
   secure: FslueGK2gtPHkRANMpUlGyCGsr1jTVuaKpP+SvYUxBYh5zbz73GMq+VsqlE29IZ1ER1+xMfWuCCvg3VA7HePyN6hzoZ/t0LADureYVPur6R5ZJgqgQpBinjpytIjo2BhN3NqaNWaIJZTLDSAT76R7HuNm01=

As we can see from the code, we have installed the sauce_connect addon and then added the sauce_username and sauce_access_key which we got when we registered on the cloud. Now, what is this gibberish we are seeing? Well, that is actually the sauce_access_key. It is just in its encrypted form. Generally, it is not a good practice to show the access keys in the source code. Anyone else can then use it and can cause harm to the resources allocated to us. You can read all about encrypting environment variables and JWT (JSON Web Tokens) here:-

https://docs.travis-ci.com/user/environment-variables/
https://docs.travis-ci.com/user/jwt

So, this sets up our tunnel to the Sauce Cloud. Here is one of the screenshots showing that our tunnel is opened and tests can be run through it.

sauce.png

After this, our next step is to make our test scripts run in the Sauce Cloud through the tunnel. We already use a testing framework mocha in the project. We can easily use mocha to run our client-side tests too. Here is a link to study it in a little more detail:

http://samsaccone.com/posts/testing-with-travis-and-sauce-labs.html

This is a short excerpt of the code from the test script

describe("Running Selenium tests on Chrome Driver", function() {
 this.timeout(600000);
 var driver;
 before(function() {
   if (process.env.SAUCE_USERNAME !== undefined) {
     driver = new webdriver.Builder()
       .usingServer('http://'+    process.env.SAUCE_USERNAME+':'+process.env.SAUCE_ACCESS_KEY+'@ondemand.saucelabs.com:80/wd/hub')
       .withCapabilities({
         'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
         build: process.env.TRAVIS_BUILD_NUMBER,
         username: process.env.SAUCE_USERNAME,
         accessKey: process.env.SAUCE_ACCESS_KEY,
         browserName: "chrome"
       }).build();
   } else {
     driver = new webdriver.Builder()
       .withCapabilities({
         browserName: "chrome"
       }).build();
   }
 });

 after(function() {
   return driver.quit();
 });

 describe('Testing event page', function() {

   before(function() {
     eventPage.init(driver);
           eventPage.visit('http://localhost:5000/live/preview/[email protected]/FOSSASIASummit');
   });

   it('Checking the title of the page', function(done) {
     eventPage.getEventName().then(function(eventName) {
       assert.equal(eventName, "FOSSASIA Summit");
       done();
     });
   });
 });
});

Without going too much into the detail, I would like to offer a brief overview of what is going on. At a high level, before starting any tests, we are checking whether the test is being run in a Travis environment. If yes, then we are appropriately setting up the webdriver for it to run on the Sauce Cloud through the tunnel which we opened previously. We also specify the browser on which we would like to run, which in this care here, is Chrome.

After this preliminary setup is done, we move on to the actual tests. Currently, we only have a basic test to check whether the title of the event site generated is correct or not. We had generated FOSSASIA Summit in the earlier part of the test script. So we just run the site and check its title which should obviously be ‘FOSSASIA Summit’. If due to some error it is not the case, then an error will be thrown and the Travis build we fail. Here is the screenshot of a successful passing test:

6c0a0775-6ad4-45f9-bfa1-a8baef4e6401.png

More tests will be added over the upcoming weeks.

Resources:

Continue Reading

Cross-Browser Issue: Dealing With an UI Problem Arising Due to Floating HTML Elements in Open Event Webapp

A few days back, I came across a rather interesting but hard to debug bug. The weird part was that it presented itself only on particular browsers like Firefox and didn’t exist on other browsers like Chrome.

In Open Event Webapp, we have collapsible session elements on track, room and schedule pages. The uncollapsed element would show you the major details like the title of the session, its type, name and a small thumbnail picture of the speaker. If the user clicks on it, then the element would collapse and more detail would pop up which would include the detailed description of the session and the speaker(s) presenting it.

Now, the problem which was occurring was that the collapsible sessions on the rooms page were behaving erratically. They were collapsing predictably but left behind a huge white space after they wound up on clicking. I have attached some screenshots to better demonstrate the problem. At first, I was confused. Because the same thing worked perfectly on Chrome.

collapsed.png

Collapsed State

uncollapsed.png

On Clicking Again

The actual solution for this problem turned out to be quite short (a one-liner in fact) but it took me a little while to solve it and actually understand what was going on. The solution:-

.room-filter {
 overflow: hidden;
}

Here, room-filter is the class applied to the session element.

Wait? But how did that solved the problem? Read on!!

As I mentioned in the title, the problem arises due to the floating HTML elements. I am just going to give a quick introduction to the float property. In case you want to read more about it, I have provided links at the end of the article which you can go through for a detailed explanation. Float is a CSS positioning property. It tells whether an element would shift to the right or left of its parent container or another floated element while other HTML elements like images, text and inline elements would wrap around it. One popular analogy which is often given is that of a textbook where the text wraps around the images. We can say that the image is floating to the left (or right) while the text wraps around it.

But how is this problem related to the floating elements still remain unclear? Hold on. This is the structure of the session div on the rooms page:

<div class="room-filter" id="{{session_id}}">

<div class="eventtime col-xs-2 col-sm-2 col-md-2">
<!-- Some Content -->
</div>

<div class="room-container">
<div class="left-border col-xs-10 col-sm-10 col-md-10">
<!-- Some Content -->
</div>
</div>

</div>

 

As we can see from the code, we have a session div with two divs inside it. We have applied bootstrap grid classes to them. These bootstrap layout classes actually make the elements float with a specified width (You can check it on the developer console). So, we have two floating div elements inside the parent div. Remember that although the floating elements remain the part of the document, they are taken outside of the normal flow of the page. Since the parent div here contain nothing but the floating elements, its height collapses to zero.

Because of this, Firefox was not able to correctly expand and collapse the session element because it’s actual height was not being computed and was set to zero. Now, if we apply any of the properties which can make the height of the parent element non-zero, we would be able to solve this problem. Chrome somehow managed to escape this issue and worked predictably.

To make the height non-zero, we apply this property:-

.room-filter {
   overflow: hidden;
}

An intuitive reason to explain why this fixes the problem is that when the element is in its default state (overflow: visible), it does not need to calculate its height to display properly. Once we set overflow: hidden, it needs to see whether the max-height or max-width has been surpassed and hence it needs to calculate its dimensions. Once the height is calculated, it is used in layout and the element no longer collapses.

correct.png

Problem solved.

Another alternative solution to solve the problem can be:

.room-filter {
   display: inline-block;
   width: 100%;
}

In case you want to read more about the float property, you can consult these links:

https://css-tricks.com/all-about-floats/

https://www.smashingmagazine.com/2009/10/the-mystery-of-css-float-property/

Continue Reading

Using Flexbox for Responsive Layout in Open Event Webapp

Recently, I tackled the issue of alignment of different buttons and input bar in the Open Event Webapp. The major challenge was to create a responsive design which adapts well on all the platforms: desktop, tablets and mobile. Doing it using grid layout provided by Bootstrap was rather tough and complicated and I solved the problem using the flexbox.

What is Flexbox?

Flexible Boxes, also called as Flexbox, is a new layout mode introduced in CSS3. The best part of using flexbox is that it ensures that the elements behave in a predictable manner when the page layout accommodates different screen sizes and display devices. In other words, it helps us in making a responsive design in a simpler manner. It is an alternative to block elements being floated and manipulated using media queries. Using Flexbox, a container and its children can be arranged in any direction: up, down or left and right. Size is flexible and elements inside the flexbox can grow or shrink to occupy unused space or prevent overflow respectively.

The difference with grid layouts?

There is a slight difference between flexbox and grid layouts which makes one suitable for creating a fully complete layout and the other not so much.

Ideally, grids (provided by Bootstrap for example) are used for creating an entire layout. Flexbox is suited for styling separate containers such as navbars for example.

flex_terms.png

                                 Structure of Flexbox Container

How did I solve it?

First I defined a container which acted as a wrapper for all the buttons and the search bar. Now, to convert this container into an actual flexbox, we have to add display: flex property to it.

.container {
 display: flex;
}

Did the issue got solved? It doesn’t look so. The container enclosed in the red border is the flexbox.

flexnogrow.png

As we can see from the picture itself, the elements are quite close to each other and a lot of space is wasted. Fortunately, flexbox provides us a handy property called flex-grow which deals with this type of problem. It defines the ability for a flex item to grow if necessary. That it, it tells how much amount of the available space inside the flex container the item is allowed to take. If all the items have flex-grow set to 1, then the remaining space in the container would be equally distributed to all the items. In a similar way, if an item has flex-grow set to 2, then it would occupy twice the amount of available space when compared to the other items.

So, I applied the flex-grow:1 on the items.

.list-btn {
   flex-grow: 1;
}

.search-filter {
   flex-grow: 1;
}
.starred-btn {
   flex-grow: 1;
}

.calendar-btn {
   flex-grow: 1;
}

How does it look now?

flexbox_initial.png

Much better. But is the problem solved now? No. There is one more thing that we haven’t checked yet. Yes. It’s the responsiveness. We haven’t yet checked how it displays on tablets and mobiles yet? Let’s test and check and what we see.

mobilenowrap.png

Ok. Something is not right. The items are squeezing together. That doesn’t look good. We don’t want the elements to wrap on a single line. What we actually want is that the items would stack on top of each other when the screen size is reduced. That would look much more neat and tidy.

To change this default behavior of wrapping of items, we use the flex-wrap property on the container. Specifying flex-wrap: wrap does the trick and the items wrap as needed.

mobilewrap2.pngmobilewrap1.png

 

.container {
   display: flex;
   flex-wrap: wrap;
}

The result looks much better now. The items wrap on a single line only when there is enough space available. Otherwise, they wrap up onto multiple lines from top to bottom.

Flexbox is a great tool for creating custom layouts for separate containers. Apart from the properties discussed here, there are a plethora of other options which can be used to customize the behavior of flexbox and the items contained inside it.  Check out the links below for more information!

Resources:

Continue Reading

Handling Zip Upload in Open Event Webapp

In Open Event Webapp, we use Socket.IO library for real-time communication between client and the server. To be able to generate a site for an event, the user has to first upload the file to the generator. There are a lot of node libraries like multer and formidable which exists for this purpose. But they don’t support display of real-time stats regarding the progress of the uploaded file. To solve this issue, the project used socket-io-file-upload library which uploads the file to the Node.js server using Socket.IO and show live percentage denoting how much of the zip has been uploaded to the server.

It was working quite well until we discovered a major problem with the library. It didn’t support canceling the upload. If we clicked on the cancel button, it stopped showing the progress on the front end but actually continued to upload the file to the server on the back end. We had only two choices: Either to refresh the page or just wait for the existing zip file to upload completely. The former is a really bad solution and the latter result in wastage of time and bandwidth.

On investigating the issue, the problem was with how the library is designed. This was not a fault in our code or the library either. It was just that the library didn’t support this functionality.  After thoroughly searching for the solutions, we came across this library named socket-io-stream which met our requirements. It supports bidirectional binary data transfer with Stream API through Socket.IO. We can also cancel the upload in between.

For streaming between server and client, we will send stream instances first. To receive streams, we just wrap socket with socket-io-stream and then listen for any events as usual.

Client Side:

function uploadFile(file) {
  // Calculating size of the file and creating a stream to be sent to the server

  var size = (file.size/(1024*1024)).toString().substring(0, 3);
  var stream = ss.createStream();

  /* Creating a read stream and initializing variable to keep track of the size of the file uploaded so far */

  var blobStream = ss.createBlobReadStream(file);
  var fileUploadSize = 0;

  // Tracking upload progress and updating the variables

  blobStream.on('data', function(chunk) {
    fileUploadSize += chunk.length;
    var percent = (fileUploadSize / file.size * 100);

    /* isCancelling is a boolean which stores the status of the zip upload.That is, whether it is live or canceled */

      if (isCancelling) {
           var msg = 'File upload was cancelled by user';
           stream.destroy();
           socket.emit('cancelled', msg);
           isCancelling = false;
           console.log(msg);
       }

       updateUploadProgress(Math.floor(percent));
       if (percent === 100) {
           uploadFinished = true;
           enableGenerateButton(true);
       }
     });

  // Sending the stream to the server and transferring read data to it

   ss(socket).emit('file', stream, {size: file.size, name: file.name});
   blobStream.pipe(stream);
}

Server Side:

// Importing the library
const ss = require('socket.io-stream');

// Listening to the stream sent from the client
ss(socket).on('file', function(stream, file) {
  generator.startZipUpload(socket.connId);

  // Declare a filename and write the incoming data to it
  var filename = path.join(__dirname, '..', 'uploads/connection-' + id.toString()) + '/upload.zip';

  stream.pipe(fs.createWriteStream(filename));
});

That’s all we need to do to stream data from the client to the server. Head over here for more information regarding the library.

Continue Reading
  • 1
  • 2
Close Menu