Showing posts with label google+. Show all posts
Showing posts with label google+. Show all posts

Wednesday, October 29, 2014

The Problem With Google

I'm too old to be fan of technology, but I quite like lots of it, and you can't argue that Google have definitely taken the lead on collaboration. At the core of all its products is the idea that what you are working on, you will want to involve other people, as collaborators, as commenters, as mentors or viewers.

But Google's model of collaboration is all wrong. Or rather, we've adopted Google tools at the university and although they provide the best tools for collaboration, their model of collaboration is hurting us. 

Google's model of collaboration best matches a small business and individual. This is reflected in how Google Drive works. 

For example, in Google Drive, if you create a file, only you can delete it. That's great isn't it? Except because a file is yours, when you leave the university, unless your admins move ALL your files to someone else, they're gone. 

Before leaving the university, you could individually make someone else the owner of one of your files, like this...


But that is, to put it mildly a bit of faff... and if you put your files in a folder and make someone else the owner of the folder, the files still disappear when you leave ( the files don't inherit ownership from the folder ).

And then, you might get fancy and think you could create a solution with Apps Script.  So I tried that. My idea was to create a "dropbox" and a script to watch that dropbox and when a file is added to it, make a copy ( which I, or a departmental account would then own ). It worked fine. Except of course, the script can't delete the original file - because I don't own it. So, I was left with two copies of the file, one I ( or a departmental account ) owned, and the original. Sigh! ( The code below doesn't work by the way ) .




function check_dropbox() {

var dropbox_folder = DriveApp.getFolderById("FOLDER_ID")
var main_folder = DriveApp.getFolderById("OTHER_FOLDER_ID")
var files = dropbox_folder.getFiles()

while (files.hasNext()) {
var file = files.next()
var name = file.getName()

var new_file = file.makeCopy(name, main_folder)
Logger.log("Made a copy of: " + name)
Utilities.sleep(2000)
file.setTrashed(true)

}

}

Maybe you could write a script to simply move your files to someone else. Except you'd have to get  pretty fancy and page through your files if your script would need more than 9 seconds running time. Whilst this might seem like a good idea, you can't transfer ownership of a document to someone at another organisation.


What The Problem Is


The problem is that Google files are so tied to an individual. As an organisation, you need to be able to have documents that aren't tied to individual, but are tied to a role or a department.

And it gets worse as soon as say three universities want to collaborate on a project together. And remember, collaboration is what Google are supposed to be excellent at. Imagine these three universities want to collaborate by sharing documents.  You'd imagine that in the course of a project people might come and go, and ideally, you don't want files disappearing when people move on.

More subtly, you don't actually want any one university to own the files ( even if this was possible, which it isn't ). What is required is a form of shared ownership.

So Come On Google

Collaboration is your thing. I know these are easy problems to solve, but you can't argue that at times, we might not want to an individual, we might want our work to have longevity beyond our involvement and we might want to work fluidly with other organisations. 

At the moment I have someone asking, "We want to set up a five year project and share documents with three organisations. How do we do it with Google Drive?" ... and unless your view of collaboration is one where the documents are fleeting ephemeral things, rather than lasting records, there isn't really a Google-shaped solution that makes a lot of sense.














Monday, January 28, 2013

Making Your Own Version of Appointment Slots with Apps Script

Making Your Own Version of Appointment Slots

In a previous post I showed an alternative version of Google Calendar's Appointment Slot's functionality created in Apps Script. You can see it in action here.

Since making this app Google have, rather fantastically dropped their dropping of the Appointment Slots feature. This app may be useful to anyone wondering how to make an Apps Script Web App that loads jQuery and uses HTML templates.

If you want to edit the code that runs it, you can get a copy of the script here:


Then you need to go to...

File > Make a copy

... and then...

File > Manage Versions




... and create a new version of the app. This is needed when you want to publish your web app. So next, then go to ...

Publish > Deploy as web app


You might want to restrict Who has access to the the app setting to just people from your domain.

In theory you can now share your web app's URL and start making changes to the application. Most of the files are just HTML templates, but the main two code files are Code and Web App. Good luck!



Friday, June 1, 2012

Creating "Homework" Google Sites

Tom Stoneham came to us with an interesting problem... "Can I automatically create 80 or so HomeWork Google Sites from a template for students? And when the deadline has been reached can their access be revoked and links sent out to examiners". The students' task will be create a site about a particular philosopher. The prototype looks like this...



I'd had a stab at solving this earlier to see it was possible, and maybe too quickly I jumped for python. But in the spirit of making something that a. worked, b. was sharable, c. I wouldn't have to maintain ( hopefully ), I thought I'd have a go a re-doing it in AppScript.

Having met with Tom, there were a few addition requirements:

  • Can student sites have unique IDs that are mapped on to a marking sheet?
  • Can the URLs be kept in a list because, if you have 80 students then 8 markers may be given 10 students each?
  • What is the best way for the University of keep the snapshot but still give the student the ability to take their work with them? The student may even continue working on it.
  • Can an Editor of a site (i.e not the Owner) make a copy of a Site?
  • Just as an afterthought, can there be guidance about Copyright etc?
So, my early experiments were this.

1. Create the Spreadsheet



The siteurl column would be used to store the site created's URL for use later.


2. Create A Menu To Do Stuff

In the Script Editor I added:



function onOpen() {

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [ {name: "Create Sites...", functionName: "create_sites"},
                      {name: "Add Students To Sites", functionName: "add_students_to_sites"},
                      {name: "Email Examiners", functionName: "notify_examiners"} ];
  ss.addMenu("Administration", menuEntries);
}



After a while, I hit this wall...,  when creating a site, because it can take an elastic amount of time, but the code continues. That means if you try to create a site, then set up who the users are, the site might not be there yet. My workaround was to create separate functions... so the process, and indeed the Menu Items are...


  1. Create all the Google Sites based on the spreadsheet data, copying the chosen template site
  2. Add the students as Editors to the the sites 
  3. When the deadline is reached, remove the students as Editors ( making them Viewers ) and also make the Examiners Viewers ( sending them emails with the sites they have to mark in with each students unique ID.

So...



function create_a_site( template_site_name, student_unique_id ){
  var domain = "york.ac.uk" ;
  var template_site = SitesApp.getSite(domain, template_site_name );
  var name = "" + template_site.getName() + "-" + student_unique_id .toLowerCase() ;

  var title = name ;
  var summary = "Deadline 21st, December 2012" ;
  // See the warning in https://developers.google.com/apps-script/class_sitesapp about site creation speed
  var site = SitesApp.copySite(domain, name, title, summary, template_site);
  var sites_url = site.getUrl();
  return sites_url ;
}

function create_sites(){

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("students");
 
  var result = Browser.inputBox("Which Site is the template Site?", "project-template", Browser.Buttons.OK_CANCEL );
  if (result == "cancel"){
    //Browser.msgBox("CANCEL: " + result)
        }
   else{
     
     var range = ss.getDataRange().getValues();
     var students = rangeToObjects(range);
     var template_site_name = result
     
     try{
           
           for(var i = 0; i < students.length; i++){
             var student = students[i];          
             var site_name = template_site_name +"-" + student.uniquereference.toLowerCase();
             var domain = "york.ac.uk" ;
           
             var sites_url = create_a_site(template_site_name, student.uniquereference.toLowerCase() ); //Alter the values
             students[i].sites_url = sites_url;

             var x = i+2
             var cellnum = x.toString().replace("0", "");
             
             //Browser.msgBox(x);
             Logger.log( cellnum );

             var values = new Array();
             values[0] = sites_url
             var cellname = "D" + cellnum;
             var range = sheet.getRange( cellname )
             
             range.setValue( sites_url );
             
     }      
         
     }catch(e){
     Logger.log( e.message );
   }
     // Now write the URLs back to the spreadsheet. Or not.
     
   }
}

function add_students_to_sites(){
  //Browser.msgBox("Add Students To Sites!")
 
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("students");
  var range = ss.getDataRange().getValues();
  var students = rangeToObjects(range);
 
  for(var i = 0; i < students.length; i++){
    student = students[i];
    var url = student.siteurl;
    var email = student.email;
    var examiner = student.examiner;
    var site = SitesApp.getSiteByUrl(url);
    site.addEditor(email);
    site.addViewer(examiner);

   
  }
  Browser.msgBox("Added students to sites")
}





function test_create_a_site(){
 var x = create_a_site("project-template",  "Y63326039"  );
 Logger.log( "Done! " + x );
 //Browser.msgBox(x)
}

function add_people( site_name, student_email, examiner_email ) {
  var domain = "york.ac.uk" ;
  var site = SitesApp.getSite(domain, site_name );
  site.addEditor( student_email ).addViewer(examiner_email);
  the_url = site.getUrl();
 
  //email a link to the student
  /*MailApp.sendEmail(student_email,
                    "Your Philosophy Homework Site",
                    "A Google Site has been created for you to fill in. \n\n " +
                     the_url + "\n\n",                  
                    {name:"Philosophy Course"});*/
 

  Logger.log("Done!");
 
}

function notify_examiners ( ){
  // email a link to the examiner
  // https://developers.google.com/apps-script/class_mailapp
 
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("students");
  var range = ss.getDataRange().getValues();
  var students = rangeToObjects(range);
 
  for(var i = 0; i < students.length; i++){
    student = students[i];
    var url = student.siteurl;
    var email = student.email;
    var examiner = student.examiner;
    var site = SitesApp.getSiteByUrl(url);
   
    MailApp.sendEmail(examiner,
                      "TEST: A Philosophy Homework Site To Mark",
                      "A Google Site has been created for you to mark. \n\n " +
                     url + "\n\n",                    
                    {name:"Philosophy Course"});
    //At this point we might want to record that the mail has been sent...
     
  }
 
  Browser.msgBox("Done!");
}
 

Conclusions

There are still a few bugs to iron out. It really doesn't like creating a site if a site already exists with that name, and I've found that deleteSite() never runs smoothly.

The next step is maybe to add some UI to select which site you want to use as a template. And of course to start making it a bit more robust. Ahem.

I'm actually quite surprised that this was easier to achieve in AppScript than it was in Python... probably...






Sunday, May 20, 2012

2. Building a Booking System With Google AppScript...

Given the swingeing criteria in my first post, I decided to start by creating the simplest interface I could. 

I began with a simple database of Perches in a spreadsheet and then in the Script Editor created a rough GUI with a couple of dropdown menus, and a couple of buttons that I would fill with data from the a mixture of a Perches calendar and this spreadsheet.

I decided not to keep track of bookings in a separate spreadsheet, simply because this felt like it would just be a whole heap of work. I would just use a calendar to store bookings. The guest of each event would decide who's booking it was. 




There are two areas of the interface, in the top bit, you can pick a date and book it ( it shows how many perches there are left ). In the bottom bit the dropdown menu is a list of dates you have booked and you can delete them. Like this...




The green blob at the bottom is just where I splat debug stuff. The list of perches is kept in spreadsheet called "Perches" and availability is worked out by simply counting the number of events on a particular day and taking that away from the number of perches. Rocket science.

I thought having LOADS of events in a calendar might look very crap but, it seems to cope.







And in Agenda View it worked even better.




And because the user is "added a guest", it magically appears in their calendar like this shown below, with an email reminder if I want. 









Note: Declining the invitation doesn't delete the original event from the calendar ( but, if done from the interface could ). 

How I Did It


Firstly, please forgive my crap Javascript ( and tell me how to do it clearer ) I created the UI in the UI Builder.... and created the doGet() code...



function doGet( e){
  var app = UiApp.createApplication();
  app.add(app.loadComponent("MyGui"));
  //the bottom text thing is something I use for debugging...
   var bottomPanel = app.createHorizontalPanel();
   var contentBox = app.createTextArea().setSize('580px', '20px').setId('contentArea').setName('contentArea');
   contentBox.setStyleAttribute("color", "red");
   contentBox.setText('OK');
   bottomPanel.add(contentBox);
   bottomPanel.setStyleAttribute("background-color", "yellow");
   app.add(bottomPanel);
 
   label = app.getElementById("Label1");
   var user = Session.getUser();
   label.setText( user.toString() )
 
   // Get MY EVENTS
   my_events = getMyEvents()//See later...
   for (e in my_events){
     event = my_events[e];
     var the_date = event.getStartTime();
     Logger.log( "the_date:" + the_date);
     var the_str = the_date.toDateString() + " " + event.getLocation();
     app.getElementById("ListBox2").addItem(the_str );
   
   }
 
    try{
        // Load the Perches Spreadsheet.
      var ss = SpreadsheetApp.openById("***************"); 
      SpreadsheetApp.setActiveSpreadsheet(ss); // Make this the one "in focus" 
      SpreadsheetApp.setActiveSheet( ss.getSheetByName("Perches")   );
      var range = ss.getDataRange().getValues()//Get all the Perches
      var perches = rangeToObjects(range);
      var number_of_perches =  perches.length;
     
     
      //Load the Treehouse Perch Bookings calendar
      var number_of_days_ahead = 10;
      var cal = CalendarApp.getCalendarById('******************'); //Logger.log (cal.getName() );// Just check you got the right one
      cal.setSelected( true ); // Is this really needed?
     
      //Create a "list of days", with calendar events in each item
      var days = new Array()
      for (i=0;i<=number_of_days_ahead;i++){
        var perches_available = number_of_perches
        var this_days_events = new Array();
       
        var future_day_start = new Date(); //now...ish!
        var the_hours = future_day_start.setHours(9);
        var the_minutes = future_day_start.setMinutes(0);
        var the_seconds = future_day_start.setSeconds(0);
        var the_millis = future_day_start.setMilliseconds(0);
       
        var future_day_end = new Date(); //now...ish!
        var the_hours = future_day_end.setHours(17);
        var the_minutes = future_day_end.setMinutes(0);
        var the_seconds = future_day_end.setSeconds(0);
        var the_millis = future_day_end.setMilliseconds(999);
       

        future_day_start.setDate(future_day_start.getDate() + i ); //Logger.log( future_day )
        future_day_end.setDate(future_day_end.getDate() + i ); //Logger.log( future_day )
       
        this_days_events = cal.getEvents(future_day_start, future_day_end);
        var number_of_this_days_events = this_days_events.length;
       
        if (number_of_this_days_events > 0){
           var perches_available = number_of_perches - number_of_this_days_events;
        }else{
          //
        }
       
        var the_date_string = "" + future_day_start.getDate() + "/" + future_day_start.getMonth() + "/" +  future_day_start.getFullYear();
        var better_date_string = future_day_start.toDateString();
       
        if (perches_available > 0){
            //dont' show the ones that are full
            var the_string = better_date_string  + " (" + perches_available + " available )"
            //Add the items to the dropdown menu
            app.getElementById("ListBox1").addItem(the_string);
        }
      }
     
    }
 
    catch(e){
      Logger.log(e);
      contentBox.setText(e)
    }
 

  var handler = app.createServerClickHandler('bookPerch');
  handler.addCallbackElement(app.getElementById("ListBox1"));
  app.getElementById("Button1").addClickHandler(handler);
 
  return app;
 
}

... and this called ...

function getMyEvents(){
  var cal = CalendarApp.getCalendarById('*****'); //Logger.log (cal.getName() );// Just check you got the right one
  var user = Session.getUser();
  var email = user.getEmail();
 
  var now = new Date(  );
  var future = new Date(  );
  future.setDate(date.getDate() + 14 ) // Look forward two weeks?
  var events = cal.getEvents(now, future, [ CalendarApp.GuestStatus.YES] )
  var new_events = new Array()
     
  for (i=0;i<=events.length;i++){
    var event = events.pop();
    var guests = event.getGuestList();
    if (guests.length >=1){
      var g = 0;
      var glen = guests.length;
      for (g in guests){
        var guest = guests[g];
     
          var guest_email = guest.getEmail();
          if (guest_email == email){
            new_events.push( event);
          }
      }
    }
   
  }
 
  return new_events
 
}

... and the method that is called when the button is clicked...

function bookPerch(e){
   var cal = CalendarApp.getCalendarById('****'); //Logger.log (cal.getName() );// Just check you got the right one
   cal.setSelected( true ); // Is this really needed?
   cal.setTimeZone("Europe/London");
 
   var app = UiApp.getActiveApplication();
   var user = Session.getUser();
   var email = user.getEmail();
   var name =    user.getUsername();
 
 
   var panel = app.getElementById( "TextArea1" );
   //var source = e.parameter.source // what's been clicked
   var value = e.parameter.ListBox1
 
   
   //strip off the "( 32 available ) //hack!
   var date_str = value.replace(/ \(.*/g,"");
   var date = new Date( date_str );
   date.setDate(date.getDate() + 1 ); //WTAF? Dates are a nightmare!

  // STILL TO DO: get ALL the perches available
  // remove the perches currently booked on this day
  // select a random one from the ones left
 
  perch = "Perch 24"
   
  /*optAdvancedArgs = new Array()
  optAdvancedArgs.guests = email
  optAdvancedArgs.location = perch
  optAdvancedArgs.sendInvites = true*/

  // Crapola! Doesn't work. Known issue http://code.google.com/p/google-apps-script-issues/issues/detail?id=1055
   
    try{
      Logger.log( "date day:" + date.getDate() );
     
      event = cal.createAllDayEvent( name, date );
      //event.addEmailReminder(60) ;
      event.addGuest( email ) ;
      event.setLocation(perch);
 
      panel.setText(name +  " " + date.toString() + " " + event.isAllDayEvent().toString() );
    }
  catch(e){
    if (event != null ){
      //tidy up? event.deleteEvent() ;
      Logger.log(e.message);
    }
    panel.setText("ERROR: " + ": " +  e.message );
  }
 
  //Update dropdown
 
   my_events = getMyEvents();
 
   var listbox = app.getElementById("ListBox2");
   listbox.clear();
   for (e in my_events){
     event = my_events[e];
     var the_date = event.getStartTime();
     Logger.log( "the_date:" + the_date);
     var the_str = the_date.toDateString() + " " + event.getLocation();
    app.getElementById("ListBox2").addItem(the_str );
   
   } //*/
 
  return app; // do we need to refresh the dropdown menu here? How does this work?

}


So there we have it. Nowhere near finished but working well enough to prove that, given very simple restraints, something simple is feasible.

Known Bugs


Is it me or is working with All Day Events a bit buggy? They almost always end up on the wrong day. I'm doing something wrong.

The interface needs a "loading" animation or something when the button is clicked ( it takes about 4 seconds and nothing happens. People will just double book ). I've got a suspicion I don't need to send the rootComponent up and down onClick... I get the feeling I need to understand the mechanics of what is going on underneath the a mouseClick just to make it a bit snappier.

I found a "Known issue" which goes along the lines of "Google value your data integrity more than anything else in world, so were quite happy giving you spurious error messages that are essentially lies as long as your data isn't corrupted. Great. It happens when I try to add to many items at once to the calendar and a lock happens ( I think )... Google reports it's a "mismatch of keys".... 

Oh, and this is a handy error screen too.  Great. Thanks again Google.



And of course, the fact that the requirements I started with are all wrong in that the were necessarily restricted. This is definitely one of those problems that you are trying to match the tools to the solution. If the solution can be kept "simple enough" then it can be done quickly and perhaps evolved. 

Sometimes quirks, like only being able to see two weeks into the future... an accident of working around something ... could actually be recast as a feature, making the end user less likely to block book, making the availability of perches more equitable. Ahem. 









 

© 2013 Klick Dev. All rights resevered.

Back To Top