Jump to content

Timetable Widget


pconkie

Recommended Posts

The frog timetable widget does the job.....

590f0411420bf_ScreenShot2017-05-07at12_15_49.png.64cc347ba6d7792d79c739e23b86c1fb.png

But it's big, contains a lot of unused white space and you only get to see one day at a time. It's a matter of opinion, but we probably had more usable timetable widgets in earlier versions of frog. After spending the last week or so trying to get up to speed with FrogLearn development (thank you Graham and @Chris.Smith for your helpful and speedy responses!) I thought I would give re-designing this widget a go.

Here is what I came up with....

590f0411cdf2a_ScreenShot2017-05-07at12_17_05.png.48746d2a1e397ca3b09baf881c3ec165.png

As with the frog version you can lookup and display any student or teachers timetable.

This version is much more compact, has a week to view, supports two week timetables and is a bit more 'colourful'.

There are a few improvements that need to be made which i hope the community can help with, but i'm fairly pleased with the result and what is possible with the frog api's and built in access to jquery and bootstrap.

I plan to share and explain my approach so that the code can be improved (i'm a science teacher with no formal coding training) and perhaps others will find it useful for their own projects?

Edited by pconkie
  • Like 3
Link to comment
Share on other sites

STEP 1: Analyse the frog timetable widget

What information does it send and receive when it loads and as various parts of it are interacted with?  How to do this is already explained on this forum (https://frog.frogcommunity.com/understanding-api), but in Chrome you can right click any element of the screen and select "inspect" (or ctrl+shift+I). This opens the Chrome Developer Tools (select the 'network' tab).

Capture1.thumb.PNG.af3ecd8460d239736b9d7f405ab116cb.PNG

You can see from the screen shot that as the widget loads it calls a frog api - timetables.getTimetable. This is a GET request (rather than a POST) which is important for any code that we will write. You can tell this because all of the parameters have been sent as a query string (if our parameters were contained in a form section then a POST would have been involved).

The following parameters must be supplied with this call to timetables.getTimetable:

1. offset (number) - 0 = current week. Allows you to specify what week of timetable information you would like returned.  e.g. 5 = 5 weeks in the future, -8 = 8 weeks in the past.

2. limit (number) - (use in conjunction with offset) the number of weeks of timetable data to return e.g. 3 = 3 weeks of data from the offset value is returned.

3. week_starts_on (number) - what should the first day of the week be? 1 = use Monday as first day of week.

4. user_uuid (string) - the id of the currently logged in user

5. target[] (array) - the id of the user whose timetable you want to look up. If omitted or left null the user_uuid is used for this purpose.  Haven't tested, but assume you can supply more than one uuid.

 

From using other examples on this forum here is a function for calling this api:

var getTimetable = function(anyuuid) {
  Frog.Model
    .api(
    'timetables.getTimetable',
    {
      limit: 2,
      offset: 0,
      week_starts_on: 1, //Monday
      user_uuid: currentuser.uuid,
      target: [anyuuid]  //uuid for user we are looking up
    }
  ).done(function(thisResponse) {
    //error handling
    if (thisResponse.status.code == 0) {
    	//more code here
    }
  });   
};


//usage
getTimetable();  		//returns the timetable of the currently logged in user
getTimetable('ABC12345');	//returns the timetable of the user with uuid ABC1234

 

Now back to Chrome to look at the structure of the data returned (preview tab).

Capture2.thumb.PNG.b0afb1b14b3b857acfde27e9127f495c.PNG

This is some highly nested JSON data.  I find it useful to copy and paste it into a JSON viewer like this...

Capture3.thumb.PNG.aab01f0c07a201246f2941d96b126f99.PNG

The data section is of most interest.

For each user whose timetable we are requesting (in our example just one) there is information about the user, the days in the timetable cycle and the timetable week (or weeks) requested. For each week there is information about the days in that week, the periods in that day and the group, room and teacher allocated to that period. That's USER->WEEK->DAY->PERIOD.

If we want to display the timetable information for a day, we would need to select a day and loop through all of the periods.  If we want to display the timetable information for a week we would need to select a week and loop through each day AND loop through each period.

//this code inside the done() of the api call
//would print details of every lesson in the entire response regardless of offset or how many weeks returned (limit)

var data = thisResponse.data[0];
$.each(data.weeks, function(i,weeks) {
  $.each(weeks.days, function(p,days) {
    $.each(days.periods, function(q,periods) {
      console.log(periods.name+' '+periods.subject+' '+periods.teacher.displayname);
    });
  });
});

//feels like there should be a better way of doing this?

 

The frog timetable widget also uses a second api to search for other users and return their uuid. We will look at this call (textsearch.search) in step 2.

Edited by pconkie
Link to comment
Share on other sites

STEP 2: Analyse the other frog timetable widget api call.

The second api the frog timetable widget makes use of is textsearch.search

Capture1.thumb.PNG.1cb0d2c21ff6b4d76a325bbfe3eec1a0.PNG

Above you can see that I am searching for 'smith' by typing that into the search box.  Each time a key is pressed (once there is more than one letter typed) a call to the api is made.

Again it is a GET request with the following parameters:

1. profile (string) - which profiles to include in the search

2. type (string) - presume type of entity (user, site? resource?)  So that the api is re-usable in different contexts.

3. include_associations (boolean?) - no idea

4. exclude_sources (array) - no idea

5. find (string) - the term to search for

 

Example function attached to a keyup event:

$('input').keyup(function() {
	var s = this.val();
	if(s.length > 1) {

		Frog.Model
		.api(
		'textsearch.search',
		{
            	profile: 'student,staff',
            	find: s,
            	type: 'user',
            	include_associations: true,
            	exclude_sources: ['temporary']
		} 
		).done(function(thisResponse) {
			if (thisResponse.status.code == 0) {
			//empty some ul
			//looop through the users returned
                	//re-fill the ul with li containing user information
            		}
		});
	}
}); 

 

The information returned by this api is much more straight forward.

Capture2.thumb.PNG.d5e24846643107d95ec54ba51c09620d.PNG

If we look in data we find a user collection containing all of the users that match my search for 'smith'.  You can see that I have 17 returned matches.

I can loop through the users and print out their profile, display name and uuid (which is what we need to pass to tables.getTimetable

something like this....

//again this should go inside the done() of the api call, that way it will wait for a response

if (thisResponse.status.code == 0) {
  $(".mywrapper ul").empty();
  var rusers = thisResponse.data.user;
  if (rusers.length) {
    $.each(rusers, function(i,users) {
      $(".mywrapper ul").append('<li data-value="'+users.uuid+'">'+users.profile.name+':'+users.displayname+'</li>');     
    });
  }
}

So we now have a fairly good idea how the frog timetable viewer works and in particular the mechanics of the two api's it uses.

We are now ready in step 3 to make an alternative timetable viewer!

Edited by pconkie
Link to comment
Share on other sites

Step 3: Basic mark-up and CSS for the 'week to view'

We have to bear in mind that different schools have different timetable structures. In the final version the layout html is generated dynamically based on the number of days in the cycle and the number of periods in each day.

For now we will keep it simple and fix the layout for a 5 day timetable with 7 periods each day.

<style>
  .mywrapper .table tbody tr {
      height: 50px;
  }
  .mywrapper .table th {
      text-align: center;
  }
</style>
    
<div class="mywrapper">  
<table class="table table-bordered">
    <thead>
    <tr>
        <th>This Week</th>
        <th>Monday</th>
        <th>Tuesday</th>
        <th>Wednesday</th>
        <th>Thursday</th>
        <th>Friday</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <th>REG</th>
        <td id="1Mon REG"></td>
        <td id="1Tue REG"></td>
        <td id="1Wed REG"></td>
        <td id="1Thu REG"></td>
        <td id="1Fri REG"></td>
    </tr>
     <tr>
        <th>LESSON 1</th>
        <td id="1Mon 1"></td>
        <td id="1Tue 1"></td>
        <td id="1Wed 1"></td>
        <td id="1Thu 1"></td>
        <td id="1Fri 1"></td>
    </tr>
     /////////////////////
      ///Repeat <tr> to </tr> 
     ///for additional periods
    /////////////////////
    </tbody>
</table>
</div>

 This gives the following output on a frog page:

Capture1.PNG.6d7f70c8e2b513e59e8e55dc038f0a74.PNG

<div class="mywrapper"> ......</div>   Everything is inside this div.  I've noticed that code in a HTML widget on a page has the potential to affect other open pages and sometimes have even worse consequences!  By having everything inside a div with a (undefined) class we can attempt to better control the consequences of our code.

For example, i chose to use a table (I know, i'm a bit old fashioned) and as frog utilities bootstrap css (http://getbootstrap.com/css/#tables) I have taken advantage of the built in styles available....

<table class="table table-bordered">  Those two classes (table and table-bordered) style the table producing the thin border and the bold column and 'row' headings. I wanted to over-ride the table class a little bit by adding a fixed row height and center the text, but was concerned that I might affect other tables on the frog platform.  So I made use of the div everything is inside with the class - mywrapper (which i assume is unique, but could change it to make it 'more unique').

<style>
  .mywrapper .table tbody tr {
      height: 50px;
  }
  .mywrapper .table th {
      text-align: center;
  }
</style>

The space between the classes is the descendant selector. So this line reads.... .mywrapper .table tbody tr   select all the table rows (tr) that are in all the body's (tbody) of all the tables with a class of table (.table) as long as those tables are inside an element with the mywrapper class (.mywarpper). As far as I am aware this is pretty precise styling and should cause problems elsewhere, but please chip-in if you disagree or think there is a better way etc

The ids on the table cells (td) are crucial and must exactly match the period labels used by the school/MIS.

Step 4 when i get a chance - filling the table with this weeks timetable for the currently logged in user.

Link to comment
Share on other sites

Step 4: Filling the table with this weeks timetable for the currently logged in user

This javascript should go before the code in step 3.

<script>
var currentuser = FrogOS.getUser();		//currentuser.uuid now contains the logged in users uuid

//this function sends a request for the selected users timetable and is called as soon as the document is ready (below)
var getTimetable = function(anyuuid) {
   $(".mywrapper table td").html('');				//these two lines act as a 're-set' for the timetable grid (clear text and background colour)
   $(".mywrapper table td").css("background-color",'');		//not strictly required but as soon as we want to show another users timetable will be handy!
        Frog.Model
        .api(
        'timetables.getTimetable',
        {
            limit: 2,    //how many weeks of timetable data to return?
            offset: 0,    //how many weeks ahead of current week? 0=current week (can be negative value)
            week_starts_on: 1,   //what day of the week is the first day 1=Monday
            user_uuid: currentuser.uuid,    //uuid of current user
            target: [anyuuid]   //uuid for timetable we are looking up
        }
    ).done(function(thisResponse) {
    if (thisResponse.status.code == 0) {		//check the response returned without error. Should this be ===?
        var data = thisResponse.data[0];		//get the bit of the reponse we want
        $.each(data.weeks, function(i,weeks) {		//here loop through each week of data look at each period of each day of each of those weeks.
                $.each(weeks.days, function(p,days) {
                        $.each(days.periods, function(q,periods) {
                            $(".mywrapper table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room);		
                          //line above matches correct cell in table to correct period in data returned using periods.name
                       });
               });
        });
    }
    });   
};

$(document).ready(function() {		//when the document is ready
     getTimetable();			//call the api passing no parameter.  Default behaviour is that we will get back the current users timetable.
});
</script>

If there are issues make sure that the cells in your table (td) have an id that exactly match the period names that your MIS (and therefore frog) are using.

It doesn't look very colourfull yet! In the next step - the mathematics of colour

 

Link to comment
Share on other sites

Step 5: Giving each subject their own background colour

Untitled.png.76a615fb38465b697b910fab5a8dc6ca.png

Each subject gets in own background colour (actually if a student has two different teachers for the same subject those lessons get different background colours too, but you could change that).

What I wanted to avoid was listed every possible subject in frog and assigning a corresponding colour.  If possible this should have zero configuration.

I wondered if i could somehow turn unique strings into unique colours e.g "science" --> blue, but "maths" --> green.  There are a few ways of setting a colour in html5 but one way is using a hex.  3 pairs of code, the first for the red content, the next for the green content and the last for the blue content e.g. #FF0000 (Red)  FF=255 red, 00=0 green, 00=0 blue.  So can i turn strings of different length into a hex? Random wouldn't work as I would want the same colour each time the subject appeared.

Didn't have to write this code, the answer (and credit) was found here: http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-javascript

//this is a function to consistently generate a colour for any string passed to it e.g. a subject name
var stringToColour = function(str) {
  var hash = 0;
  for (var i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  var colour = '#';
  for (var p = 0; p < 3; p++) {
    var value = (hash >> (p * 8)) & 0xFF;
    colour += ('00' + value.toString(16)).substr(-2);
  }
  return colour;
};

This works great, but there is an issue.  All of the font is black, and so can not be read if the background colour is black (or dark).  Any font colour would have the same issue! 

To fix this issue we need to test the brightness of the colour generated from the function above and set the font to either black or white based on which has the best contrast.

Ideas found here: https://24ways.org/2010/calculating-color-contrast I preferred the results when calculating the YIQ (https://en.wikipedia.org/wiki/YIQ) rather than comparing the hex to the hex which is halfway between pure black and pure white.

var getForegroundColor = function(hexcolor) {
    var r = parseInt(hexcolor.substr(1,2),16);
    var g = parseInt(hexcolor.substr(3,2),16);
    var b = parseInt(hexcolor.substr(5,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
};

I could possibly put these two functions together, but at the moment I have them separate and use them to colour the timetable like so...

//this is a pre-existing line in the done() of the api call 
$(".mywrapper table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room.slice(-3));
///////////////////////////////////////////////////////////////

//add these three lines and the two functions from above
var bgcolor = stringToColour(periods.subject+periods.teacher.username);
$(".mywrapper table tr td[id='"+periods.name+"']").css("background-color", bgcolor);
$(".mywrapper table tr td[id='"+periods.name+"']").css("color", getForegroundColor(bgcolor));

//interested in more efficient ways of doing this.

So you won't have any control over what colours are used, but this requires no configuration and you will get a nice consistently coloured timetable.

 

Edited by pconkie
Link to comment
Share on other sites

Step 6: Searching and displaying other users timetables

I just stole the mark-up from the frog timetable viewer which has a hidden search box, which once revealed allows you to start typing a users name and displays the results just below. Once a user is selected from the list their timetable loads.

Untitled.png.5e5dc626ba208e86bfd93c16db195f4c.png

Mark-up code is:

<div class="widget_timetable widget_timetable_user widget_timetable_user_search">
    <div class="theme-headerbg clearfix">
      <h2 class="widget-title btn btn-text os-ellipsis" id="searchResult"></h2>
      <button type="button" class="btn btn-text pull-right" id="toggleSearch"><i class="os-icon search-icon"></i></button>
    </div>
    <div class="frogui_components_search" data-component="search" style="display: none;">
      <form class="search">
        <input autocomplete="off" type="text" name="needle" placeholder="">
        <ul class="typeahead dropdown-menu" style="top: 30px; left: 0px; display: none;"></ul> 
        <button id="btn-clear" class="search-btn os-icon search-icon search-btn-clear" type="button"></button>
      </form>
    </div>  
  </div>

Keeping the frog classes in meant that it looked really good straight away! The it was *just* the case of attached all of the events again...

1. Toggling the visibility of the search box when the search icon is clicked

2. Calling the textsearch.search api and filling the dropdown-menu ul

3. Re-setting the search box when the clear-search button is clicked

4. Hiding the dropdown-menu when it has lost focus

5. Calling the getTimetable api when a user is selected from the drop-down menu and displaying their timetable and displayname in the widget title

6. Preventing the default behaviour of the return key (which wants to submit the form which would navigate us away from the page)

Code to follow....

 

 

 

 

 

Link to comment
Share on other sites

4 hours ago, ADT said:

@Graham Quince @Chris.Smith  I take it you boys are converting this into a Frog Code widget right?

Well Adrian, I think that is a brilliant idea! If only I knew an ICT teacher and VLE coordinator whose Year 11 and 13s where about to leave school thus freeing up some of his famously busy schedule allowing him to make said widget :P

Honest answer is we are busy getting some surprises ready in time for conference; so we don't have a great deal of time. 

@pconkie are you up for a challenge?

  • Like 1
Link to comment
Share on other sites

 

2 minutes ago, Chris.Smith said:

Well Adrian, I think that is a brilliant idea! If only I knew an ICT teacher and VLE coordinator whose Year 11 and 13s where about to leave school thus freeing up some of his famously busy schedule allowing him to make said widget :P

Honest answer is we are busy getting some surprises ready in time for conference; so we don't have a great deal of time. 

@pconkie are you up for a challenge?

ICT teacher and VLE coordinator who unlike most schools...  timetable changes after half term to our year groups move up....  new classes new courses.......... extra time 0!!

Link to comment
Share on other sites

Step 6b

The code (re-written following advice from Chris)

1. Toggling the visibility of the search box when the search icon is clicked

var $mywrapper = this.element.find(".mywrapper");

$mywrapper.find("#toggleSearch").click(function(e) {
   $mywrapper.find(".frogui_components_search").toggle();
    e.stopPropagation();
});

2. Calling the textsearch.search api and filling the dropdown-menu ul

$mywrapper.find('.search input').keyup(function() {
  var s = $mywrapper.find('.search input').val();
  if(s.length > 1) {
      
          Frog.Model
          .api(
          'textsearch.search',
            {
              profile: 'student,staff',
              find: s,
              type: 'user',
              include_associations: true,
              exclude_sources: ['temporary']
            } 
          ).done(function(thisResponse) {
            if (thisResponse.status.code == 0) {
                $mywrapper.find("ul.typeahead.dropdown-menu").empty();
                    var rusers = thisResponse.data.user;
                    if (rusers.length) {
                      $.each(rusers, function(i,users) {
                        $mywrapper.find("ul.typeahead.dropdown-menu").append('<li data-value="'+users.uuid+'"><a href="#"><span title="'+users.displayname+'">'+users.profile.name+': '+users.displayname+'</span></a></li>');     
                      });
                    $mywrapper.find('.dropdown-menu').show();
                    } else {
                     $mywrapper.find('.dropdown-menu').hide();   
                    } 
            }
          }); 
     
  }
});

3. Re-setting the search box when the clear-search button is clicked

$mywrapper.find("#btn-clear").click(function() {
   $mywrapper.find('.search input').val('');
});

4. Hiding the dropdown-menu when it has lost focus

$(this.body).click(function() {
    $mywrapper.find('.dropdown-menu').hide();
});

5. Calling the getTimetable api when a user is selected from the drop-down menu and displaying their timetable and displayname in the widget title

$mywrapper.find('ul.typeahead.dropdown-menu').on('click', 'li', function (e) {
    e.preventDefault();
    $mywrapper.find('.search input').val($mywrapper.find("span").attr('title'));
    getTimetable($(this).data("value"));
});

6. Preventing the default behaviour of the return key (which wants to submit the form which would navigate us away from the page)

$mywrapper.find('.search input').keypress(function(e) { 
    if (e.which == 13) {
      e.preventDefault();
      return false;
      }
});    

//this seems to be needed for IE to do anything right!
$mywrapper.find('.search input').on('click', function(e) {
     e.stopPropagation();
});

That's it. I'm done.

I'll post the entire code when i have had a chance to change everything suggested by @Chris.Smith. As for turning it into a FrogCode widget - sure, how hard could that be?!

Especially if that means getting to use the FrogCode area of my FrogDrive that just appeared?!

Link to comment
Share on other sites

Alternative Timetable Viewer

Here is all the code in one go:

<style>
  .frogui_components_search {
      border: 0 !important;
  }
  .table tbody tr {
      height: 50px;
  }
  .table th {
      text-align: center;
  }
  .table {
      background-color: #FFFFFF;
  }
  .table td {
      line-height: 15px;
      padding: 4px;
  }
   img {
      margin: 0px auto;
      display: block;
   }
</style>
    
<div class="mywrapper">  
    
  <div class="widget_timetable widget_timetable_user widget_timetable_user_search">
    <div class="theme-headerbg clearfix">
      <h2 class="widget-title btn btn-text os-ellipsis" id="searchResult"></h2>
      <button type="button" class="btn btn-text pull-right" id="toggleSearch"><i class="os-icon search-icon"></i></button>
    </div>
    <div class="frogui_components_search" data-component="search" style="display: none;">
      <form class="search">
        <input autocomplete="off" type="text" name="needle" placeholder="">
        <ul class="typeahead dropdown-menu" style="top: 30px; left: 0px; display: none;"></ul> 
        <button id="btn-clear" class="search-btn os-icon search-icon search-btn-clear" type="button"></button>
      </form>
    </div>  
  </div>
    
<img id="loader" src="../app/public/icon/UI/loading.gif">
<div id="week1" class="table-responsive">
<table class="table table-bordered">
    <thead>
    <tr>
        <th class="span1">WEEK 1</th>
        <th class="span2">Monday</th>
        <th class="span2">Tuesday</th>
        <th class="span2">Wednesday</th>
        <th class="span2">Thursday</th>
        <th class="span2">Friday</th>
    </tr>
    </thead>
    <tbody></tbody>
</table><br />
<button type="button" id="wk1btn" class="btn btn-primary" style="display: none;">Click for Week 2</button>
</div>
    
<div id="week2" class="table-responsive">
<table class="table table-bordered">
    <thead>
    <tr>
        <th class="span1">WEEK 2</th>
        <th class="span2">Monday</th>
        <th class="span2">Tuesday</th>
        <th class="span2">Wednesday</th>
        <th class="span2">Thursday</th>
        <th class="span2">Friday</th>
    </tr>
    </thead>
    <tbody></tbody>
</table><br />
<button type="button" id="wk2btn" class="btn btn-primary">Click for Week 1</button>
</div>
    
</div>

<script>
///////////these vars should become the preferences in the eventual forg code widget ////////
//////////change these to match the timetable used by your school or nothing will work!///////
var school_weeks = 2;
var school_periods_in_day = 7;  //total including any reg/tutor that will appear in your achool_lesson_labels
var school_lesson_labels = ["REG","LESSON 1","LESSON 2","LESSON 3","LESSON 4","LESSON 5","LESSON 6"];
var school_period_labels = ["1Mon REG","1Mon 1","1Mon 2","1Mon 3","1Mon 4","1Mon 5","1Mon 6","1Tue REG","1Tue 1","1Tue 2","1Tue 3","1Tue 4","1Tue 5","1Tue 6","1Wed REG","1Wed 1","1Wed 2","1Wed 3","1Wed 4","1Wed 5","1Wed 6","1Thu REG","1Thu 1","1Thu 2","1Thu 3","1Thu 4","1Thu 5","1Thu 6","1Fri REG","1Fri 1","1Fri 2","1Fri 3","1Fri 4","1Fri 5","1Fri 6","2Mon REG","2Mon 1","2Mon 2","2Mon 3","2Mon 4","2Mon 5","2Mon 6","2Tue REG","2Tue 1","2Tue 2","2Tue 3","2Tue 4","2Tue 5","2Tue 6","2Wed REG","2Wed 1","2Wed 2","2Wed 3","2Wed 4","2Wed 5","2Wed 6","2Thu REG","2Thu 1","2Thu 2","2Thu 3","2Thu 4","2Thu 5","2Thu 6","2Fri REG","2Fri 1","2Fri 2","2Fri 3","2Fri 4","2Fri 5","2Fri 6"]; 
var timetableweek_toview = new Date("2017-07-01"); //this is the start of the week or fortnight we will use as 'week1' and 'week2'. Make sure its a normal representative block of time as late as possible in the academic year (avoid holidays, bank holiday funny timetable weeks like enrichment week etc).
///////////other vars ///////////////////////////////////////  
var currentuser = FrogOS.getUser();
var today = new Date();
var offsetweeks = Math.round((timetableweek_toview-today)/ 604800000);
var school_days_in_week = 5;
var school_cyclelength = school_weeks * school_days_in_week * school_periods_in_day;
var school_weeklength = school_days_in_week * school_periods_in_day;
var $mywrapper = this.element.find(".mywrapper");
//////////////////////////////////////////////////////////

var getTimetable = function(anyuuid) {
   $mywrapper.find("#week1").hide();
   $mywrapper.find("#week2").hide();
   $mywrapper.find("#loader").show();
   $mywrapper.find("table td").html('');
   $mywrapper.find("table td").css("background-color",'');
        Frog.Model
        .api(
        'timetables.getTimetable',
        {
            limit: 2,
            offset: offsetweeks,
            week_starts_on: 1,
            user_uuid: currentuser.uuid,
            target: [anyuuid]
        }
    ).done(function(thisResponse) {
    if (thisResponse.status.code === 0) {
        var data = thisResponse.data[0];
        $.each(data.weeks, function(i,weeks) {
                $.each(weeks.days, function(p,days) {
                        $.each(days.periods, function(q,periods) {
                            $mywrapper.find("table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room.slice(-3));
                            var bgcolor = stringToColour(periods.subject+periods.teacher.username);
                            $mywrapper.find("table tr td[id='"+periods.name+"']").css("background-color", bgcolor);
                            $mywrapper.find("table tr td[id='"+periods.name+"']").css("color", getForegroundColor(bgcolor));
                       });
               });
        });
    $mywrapper.find('#searchResult').html('Timetable for '+data.user.displayname);  
    $mywrapper.find("#loader").hide();
    $mywrapper.find("#week1").show();
    }
    });   
};

var delay = (function(){
  var timer = 0;
  return function(callback, ms){
    clearTimeout (timer);
    timer = setTimeout(callback, ms);
  };
})();
    
var stringToColour = function(str) {
  var hash = 0;
  for (var i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  var colour = '#';
  for (var p = 0; p < 3; p++) {
    var value = (hash >> (p * 8)) & 0xFF;
    colour += ('00' + value.toString(16)).substr(-2);
  }
  return colour;
};

var getForegroundColor = function(hexcolor) {
    var r = parseInt(hexcolor.substr(1,2),16);
    var g = parseInt(hexcolor.substr(3,2),16);
    var b = parseInt(hexcolor.substr(5,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
};

$mywrapper.find('#wk1btn').click(function() {
    $mywrapper.find('#week1').hide();
    $mywrapper.find('#week2').show();
});
    
$mywrapper.find("#wk2btn").click(function() {
  $mywrapper.find("#week1").show();
  $mywrapper.find("#week2").hide();
});
    
$mywrapper.find("#toggleSearch").click(function(e) {
   $mywrapper.find(".frogui_components_search").toggle();
    e.stopPropagation();
});

$mywrapper.find("#btn-clear").click(function() {
   $mywrapper.find('.search input').val('');
   $mywrapper.find('.dropdown-menu').hide();
   $mywrapper.find("ul.typeahead.dropdown-menu").empty();
});
    
$(this.body).click(function() {
    $mywrapper.find('.dropdown-menu').hide();
});

//this seems to be needed for IE to do anything right!
$mywrapper.find('.search input').on('click', function(e) {
     e.stopPropagation();
});
    
$mywrapper.find('.search input').keypress(function(e) { 
    if (e.which === 13) {
      e.preventDefault();
      return false;
      }
});    
    
$mywrapper.find('ul.typeahead.dropdown-menu').on('click', 'li', function (e) {
    e.preventDefault();
    $mywrapper.find(".frogui_components_search").hide();
    $mywrapper.find('.dropdown-menu').hide();
    $mywrapper.find("ul.typeahead.dropdown-menu").empty();
    $mywrapper.find('.search input').val('');
    getTimetable($(this).data("value"));
});
    
$mywrapper.find('.search input').keyup(function() {
  var s = $mywrapper.find('.search input').val();
  if(s.length > 1) {
      
      delay(function(){
          Frog.Model
          .api(
          'textsearch.search',
            {
              profile: 'student,staff',
              find: s,
              type: 'user',
              include_associations: true,
              exclude_sources: ['temporary']
            } 
          ).done(function(thisResponse) {
            if (thisResponse.status.code === 0) {
                $mywrapper.find("ul.typeahead.dropdown-menu").empty();
                    var rusers = thisResponse.data.user;
                    if (rusers.length) {
                      $.each(rusers, function(i,users) {
                        $mywrapper.find("ul.typeahead.dropdown-menu").append('<li data-value="'+users.uuid+'"><a href="#"><span title="'+users.displayname+'">'+users.profile.name+': '+users.displayname+'</span></a></li>');     
                      });
                    $mywrapper.find('.dropdown-menu').show();
                    } else {
                     $mywrapper.find('.dropdown-menu').hide();   
                    } 
            }
          }); 
    }, 800 );   
  }
});
   
    
//render the timetable grid
var y;  
var b = 0;
for (var i = 0; i < school_periods_in_day; i++) {
    var trsource = '';
    trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>';
      for (y = b; y < school_weeklength; y=y+school_periods_in_day) {
          trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>';
      }
    trsource = trsource + '</tr>';
    $mywrapper.find("#week1 table tbody").append(trsource);
    b++;
}

if (school_weeks === 2) {
    
    var b = 0;
    for (var i = 0; i < school_periods_in_day; i++) {
    var trsource = '';
    trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>';
      for (y = (b+school_weeklength); y < school_cyclelength; y=y+school_periods_in_day) {
          trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>';
      }
    trsource = trsource + '</tr>';
    $mywrapper.find("#week2 table tbody").append(trsource);
    $mywrapper.find("#wk1btn").show();
    b++;
  }
      
}
    
$mywrapper.find('#searchResult').html('Timetable for '+currentuser.displayname);
getTimetable();
</script>

 

Copy and paste into a HTML widget on a page and then you must change the variables in this section

//////////////////////////////////////////////////////////////////////////////////////////////////////////
var school_weeks                          (number) 1 or 2 supported
var school_periods_in_day           (number)  as many as you need
var school_lesson_labels[]            (array) whatever you like but should be the same number of items in this array as value for school_periods_in_day
var school_period_labels[]             (array) type exactly what SIMS uses as shorthand for your periods e.g. 'Mon1', 'Mon 1', '1Mon1', '1Mon 1' etc in order
var timetableweek_toview              (date) pick a Monday as late in the academic year as possible to be the start of the week (or fortnight) that we will use as 'week1' and 'week2'. A date in the future will always result in the most recent timetable being displayed to the user (avoid holidays, bank holiday funny timetable weeks like enrichment week etc).

//////////////////////////////////////////////////////////////////////////////////////////////////////

@Chris.Smith one issue remains that I am aware of.  The searching for a user works great on all desktop browsers that I have tried it on. However on a touch device (e.g. iPad) the list of matched users appears but tapping on one of them fails to load their timetable. I guess this is the event that fails to fire on iPad...

$mywrapper.find('ul.typeahead.dropdown-menu').on('click', 'li', function (e) {
    e.preventDefault();
    $mywrapper.find(".frogui_components_search").hide();
    $mywrapper.find('.dropdown-menu').hide();
    $mywrapper.find("ul.typeahead.dropdown-menu").empty();
    $mywrapper.find('.search input').val('');
    getTimetable($(this).data("value"));
});

 

 

 

  • Like 1
Link to comment
Share on other sites

Actually i figured this one out....found the tap event.

changed to...

$mywrapper.find('ul.typeahead.dropdown-menu').on('click tap', 'li', function (e) {

});

 

Right fully working code here

<style>
  .frogui_components_search {
      border: 0 !important;
  }
  .table tbody tr {
      height: 50px;
  }
  .table th {
      text-align: center;
  }
  .table {
      background-color: #FFFFFF;
  }
  .table td {
      line-height: 15px;
      padding: 4px;
  }
   img {
      margin: 0px auto;
      display: block;
   }
</style>
    
<div class="mywrapper">  
    
  <div class="widget_timetable widget_timetable_user widget_timetable_user_search">
    <div class="theme-headerbg clearfix">
      <h2 class="widget-title btn btn-text os-ellipsis" id="searchResult"></h2>
      <button type="button" class="btn btn-text pull-right" id="toggleSearch"><i class="os-icon search-icon"></i></button>
    </div>
    <div class="frogui_components_search" data-component="search" style="display: none;">
      <form class="search">
        <input autocomplete="off" type="text" name="needle" placeholder="">
        <ul class="typeahead dropdown-menu" style="top: 30px; left: 0px; display: none;"></ul> 
        <button id="btn-clear" class="search-btn os-icon search-icon search-btn-clear" type="button"></button>
      </form>
    </div>  
  </div>
    
<img id="loader" src="../app/public/icon/UI/loading.gif">
<div id="week1" class="table-responsive">
<table class="table table-bordered">
    <thead>
    <tr>
        <th class="span1">WEEK 1</th>
        <th class="span2">Monday</th>
        <th class="span2">Tuesday</th>
        <th class="span2">Wednesday</th>
        <th class="span2">Thursday</th>
        <th class="span2">Friday</th>
    </tr>
    </thead>
    <tbody></tbody>
</table><br />
<button type="button" id="wk1btn" class="btn btn-primary" style="display: none;">Click for Week 2</button>
</div>
    
<div id="week2" class="table-responsive">
<table class="table table-bordered">
    <thead>
    <tr>
        <th class="span1">WEEK 2</th>
        <th class="span2">Monday</th>
        <th class="span2">Tuesday</th>
        <th class="span2">Wednesday</th>
        <th class="span2">Thursday</th>
        <th class="span2">Friday</th>
    </tr>
    </thead>
    <tbody></tbody>
</table><br />
<button type="button" id="wk2btn" class="btn btn-primary">Click for Week 1</button>
</div>
    
</div>

<script>
///////////these vars should become the preferences in the eventual forg code widget ////////
var school_weeks = 2;
var school_periods_in_day = 7;  //total including any reg/tutor that will appear in your achool_lesson_labels
var school_lesson_labels = ["REG","LESSON 1","LESSON 2","LESSON 3","LESSON 4","LESSON 5","LESSON 6"];
var school_period_labels = ["1Mon REG","1Mon 1","1Mon 2","1Mon 3","1Mon 4","1Mon 5","1Mon 6","1Tue REG","1Tue 1","1Tue 2","1Tue 3","1Tue 4","1Tue 5","1Tue 6","1Wed REG","1Wed 1","1Wed 2","1Wed 3","1Wed 4","1Wed 5","1Wed 6","1Thu REG","1Thu 1","1Thu 2","1Thu 3","1Thu 4","1Thu 5","1Thu 6","1Fri REG","1Fri 1","1Fri 2","1Fri 3","1Fri 4","1Fri 5","1Fri 6","2Mon REG","2Mon 1","2Mon 2","2Mon 3","2Mon 4","2Mon 5","2Mon 6","2Tue REG","2Tue 1","2Tue 2","2Tue 3","2Tue 4","2Tue 5","2Tue 6","2Wed REG","2Wed 1","2Wed 2","2Wed 3","2Wed 4","2Wed 5","2Wed 6","2Thu REG","2Thu 1","2Thu 2","2Thu 3","2Thu 4","2Thu 5","2Thu 6","2Fri REG","2Fri 1","2Fri 2","2Fri 3","2Fri 4","2Fri 5","2Fri 6"]; 
var timetableweek_toview = new Date("2017-07-01"); //this is the start of the week or fortnight we will use as 'week1' and 'week2'. Make sure its a normal representative block of time as late as possible in the academic year (avoid holidays, bank holiday funny timetable weeks like enrichment week etc).
///////////other vars ///////////////////////////////////////  
var currentuser = FrogOS.getUser();
var today = new Date();
var offsetweeks = Math.round((timetableweek_toview-today)/ 604800000);
var school_days_in_week = 5;
var school_cyclelength = school_weeks * school_days_in_week * school_periods_in_day;
var school_weeklength = school_days_in_week * school_periods_in_day;
var $mywrapper = this.element.find(".mywrapper");
//////////////////////////////////////////////////////////

var getTimetable = function(anyuuid) {
   $mywrapper.find("#week1").hide();
   $mywrapper.find("#week2").hide();
   $mywrapper.find("#loader").show();
   $mywrapper.find("table td").html('');
   $mywrapper.find("table td").css("background-color",'');
        Frog.Model
        .api(
        'timetables.getTimetable',
        {
            limit: 2,
            offset: offsetweeks,
            week_starts_on: 1,
            user_uuid: currentuser.uuid,
            target: [anyuuid]
        }
    ).done(function(thisResponse) {
    if (thisResponse.status.code === 0) {
        var data = thisResponse.data[0];
        $.each(data.weeks, function(i,weeks) {
                $.each(weeks.days, function(p,days) {
                        $.each(days.periods, function(q,periods) {
                            $mywrapper.find("table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room.slice(-3));
                            var bgcolor = stringToColour(periods.subject+periods.teacher.username);
                            $mywrapper.find("table tr td[id='"+periods.name+"']").css("background-color", bgcolor);
                            $mywrapper.find("table tr td[id='"+periods.name+"']").css("color", getForegroundColor(bgcolor));
                       });
               });
        });
    $mywrapper.find('#searchResult').html('Timetable for '+data.user.displayname);
    $mywrapper.find("#loader").hide();
    $mywrapper.find("#week1").show();
    }
    });   
};

var delay = (function(){
  var timer = 0;
  return function(callback, ms){
    clearTimeout (timer);
    timer = setTimeout(callback, ms);
  };
})();
    
var stringToColour = function(str) {
  var hash = 0;
  for (var i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  var colour = '#';
  for (var p = 0; p < 3; p++) {
    var value = (hash >> (p * 8)) & 0xFF;
    colour += ('00' + value.toString(16)).substr(-2);
  }
  return colour;
};

var getForegroundColor = function(hexcolor) {
    var r = parseInt(hexcolor.substr(1,2),16);
    var g = parseInt(hexcolor.substr(3,2),16);
    var b = parseInt(hexcolor.substr(5,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
};

$mywrapper.find('#wk1btn').click(function() {
    $mywrapper.find('#week1').hide();
    $mywrapper.find('#week2').show();
});
    
$mywrapper.find("#wk2btn").click(function() {
  $mywrapper.find("#week1").show();
  $mywrapper.find("#week2").hide();
});
    
$mywrapper.find("#toggleSearch").click(function(e) {
   $mywrapper.find(".frogui_components_search").toggle();
    e.stopPropagation();
});

$mywrapper.find("#btn-clear").click(function() {
   $mywrapper.find('.search input').val('');
   $mywrapper.find('.dropdown-menu').hide();
   $mywrapper.find("ul.typeahead.dropdown-menu").empty();
});
    
$(this.body).click(function() {
    $mywrapper.find('.dropdown-menu').hide();
});

//this seems to be needed for IE to do anything right!
$mywrapper.find('.search input').on('click', function(e) {
     e.stopPropagation();
});
    
$mywrapper.find('.search input').keypress(function(e) { 
    if (e.which === 13) {
      e.preventDefault();
      return false;
      }
});    
    
$mywrapper.find('ul.typeahead.dropdown-menu').on('click tap', 'li', function (e) {
    $mywrapper.find(".frogui_components_search").hide();
    $mywrapper.find('.dropdown-menu').hide();
    $mywrapper.find("ul.typeahead.dropdown-menu").empty();
    $mywrapper.find('.search input').val('');    
    getTimetable($(this).data("value"));
});
    
$mywrapper.find('.search input').keyup(function() {
  var s = $mywrapper.find('.search input').val();
  if(s.length > 1) {
      
      delay(function(){
          Frog.Model
          .api(
          'textsearch.search',
            {
              profile: 'student,staff',
              find: s,
              type: 'user',
              include_associations: true,
              exclude_sources: ['temporary']
            } 
          ).done(function(thisResponse) {
            if (thisResponse.status.code === 0) {
                $mywrapper.find("ul.typeahead.dropdown-menu").empty();
                    var rusers = thisResponse.data.user;
                    if (rusers.length) {
                      $.each(rusers, function(i,users) {
                        $mywrapper.find("ul.typeahead.dropdown-menu").append('<li data-value="'+users.uuid+'"><a href="#"><span title="'+users.displayname+'">'+users.profile.name+': '+users.displayname+'</span></a></li>');     
                      });
                    $mywrapper.find('.dropdown-menu').show();
                    } else {
                     $mywrapper.find('.dropdown-menu').hide();   
                    } 
            }
          }); 
    }, 800 );   
  }
});
   
    
//render the timetable grid
var y;  
var b = 0;
for (var i = 0; i < school_periods_in_day; i++) {
    var trsource = '';
    trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>';
      for (y = b; y < school_weeklength; y=y+school_periods_in_day) {
          trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>';
      }
    trsource = trsource + '</tr>';
    $mywrapper.find("#week1 table tbody").append(trsource);
    b++;
}

if (school_weeks === 2) {
    
    var b = 0;
    for (var i = 0; i < school_periods_in_day; i++) {
    var trsource = '';
    trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>';
      for (y = (b+school_weeklength); y < school_cyclelength; y=y+school_periods_in_day) {
          trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>';
      }
    trsource = trsource + '</tr>';
    $mywrapper.find("#week2 table tbody").append(trsource);
    $mywrapper.find("#wk1btn").show();
    b++;
  }
      
}
    
$mywrapper.find('#searchResult').html('Timetable for '+currentuser.displayname);
getTimetable();
</script>

 

Edited by pconkie
  • Like 1
Link to comment
Share on other sites

@Chris.Smith @Graham Quince

OK - it's now a FrogCode Widget!  Deployed on the alpha site.

All seems to be working, but as your site doesn't have any timetable data in it the testing that can be done is limited!

What do you think?  I'd like to get rid of the preference for the lesson labels and either get this from the api or get the user to select the correct format for lesson labels.  The api only sends the lesson labels for the lessons where a user has a lesson (if that makes sense) which wouldn't work. Already been asked if it can do room timetables as staff are often looking for a free room - I think the answer is no as the api doesn't seem to support room timetables?

Thanks for your help with this.

Paul

  • Like 2
Link to comment
Share on other sites

I'm in awe!  This is absolutely brilliant, well done.

If you're serious about wanting ideas for improvements.  I know you mentioned pre-setting the colours?  Using the accordion preference - (I'm at home, I'll post tomorrow) you could hide extra configuration stuff for the casual user.

And have you thought about being able to define site links so that any timetabled lesson takes you to the department site?

Link to comment
Share on other sites

Adding this element to a preference type:

accordion: 'settings.advanced'

allows you to "hide" advanced settings in a drop down area, for example:

prefs: {
    label: {
        type: 'text',
        label: 'This is a text field',
        defaultValue: '',
        accordion: 'settings.advanced'
    }
},

 

 

  • Like 2
Link to comment
Share on other sites

  • 1 month later...

@pconkie I've just been having a play with this code and have got it to work!! (after some serious trial and error...)

Brilliant stuff, though!

In section 5 you describe how you generated the subject colours, based upon their names as they arrive. I've managed to amend the colour schemes by adjusting the 'p * 8' multiplier here:

var stringToColour = function(str) {
  var hash = 0;
  for (var i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  var colour = '#';
  for (var p = 0; p < 3; p++) {
    var value = (hash >> (p * 8)) & 0xFF;
    colour += ('00' + value.toString(16)).substr(-2);
  }
  return colour;
};

My question is, would it be possible to pre-select a colour per subject? For example, we have a colour coded building, and it would be nice to match the subjects with their location colour.

I know in you post you mentioned that you tried to avoid this, though.

 

Thanks, Michael.

Link to comment
Share on other sites

Hi @mobrien

setting this up with widget preferences would be ideal - this way we could use the frog colour pickers to set the subject colours.

However, this would take some thinking about as we would need to set a colour for every possible subject that could come up and we would have to do this every time the widget was dragged into a page. So I think for now, if this is what you want, perhaps it's best just to write an alternative function for the one you pasted.

Something like this might work...

var subjectToColour = function(subject) {

var colour = "";

switch(subject.toLowerCase()) {
    case "maths":
    case "economics":
        colour = "green";
        break;
    case "english":
    case "english language":
    case "english literature":
        colour = "#FF0000";
        break;
    default:
        colour = "black";
}

return colour;

};

 

you can can use certain colour names, but you would be better off googling HTML colour codes as per the English example.

You need to keep you subject names identical to what you use in your MIS. All in lower case.

Look at how I've grouped some subjects together so that they have the same colour.

You will need to copy from "case" to "break" and paste between the { } for additional subjects/colours.

Once completed you would need to copy the new function into the code and replace all instances of "stringToColour" for the new "subjectToColour".

Good luck!

Link to comment
Share on other sites

Hi @pconkie,

Thank you for this! It has not quite worked, but nearly there.

I've amended it for the two subjects in my own TT (science and physics) and double checked that this is the SIMs name. I also made the 'stringtocolour' changes to 'subjecttocolour', as suggested. I also took out all other subject references to make sure eliminate any other variables and gave it a go.

However, the timetable is only returned in the default colour specification (black in this instance). Is there anything that I am missing?:

var subjectToColour = function(subject) {
var colour = "";

switch(subject.toLowerCase()) {
    case "science":
        colour = "#4daa50";
        break;
    case "physics":
        colour = "#007681";
        break;
    default:
        colour = "black";
}
return colour;
};

Thank you for your help with this.

Link to comment
Share on other sites

@mobrien

Most likely explanation is - we are not sending the name of the subject into the function.

This is why all lessons return the colour black - nothing can be matched.

Do you know how to use the console?  You can check what is being passed to the function (what is contained in the variable 'subject') by adding console.log(subject); to line 2 of the function.

You can then open the console and have a look at what has been outputted.

 

I have a feeling that it was advantageous when creating dynamic colours to make the string that was sent to the older function as long as possible. It is possible therefore that we are not sending the subject name alone into the function.

 I'll just check now......

Ok - find this line: 

var bgcolor = stringToColour(periods.subject+periods.teacher.username);

You have presumably already changed it to:

var bgcolor = subjectToColour(periods.subject+periods.teacher.username);

Just one more small tweak so that it is:

var bgcolor = subjectToColour(periods.subject);

That should do it.  Any further issues will be most likely to do with subject names in the function not matching SIMS.

Hope this gets it working for you.

Paul

Link to comment
Share on other sites

  • 2 months later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...