Friday, January 10, 2014

setting up a MinecraftEdu server

If you'd like your students to be able to play (or work in) Minecraft together, you can easily set up a MinecraftEdu server. There is more documentation on the MinecraftEdu wiki, but I'll quickly go over the basics.

From the MinecraftEdu launcher, click the "Start Minecraft Servertool" button.

If a teacher password hasn't been set yet, it will ask you to set one. You can always change this later.

You'll then be given some options about what kind of world you'd like to create (or open).

For example "Generate a Completely Flat World" if you'd like student to build things without worrying about cutting down trees or hills.

Once you have started the server, you can see the information and change settings from this window. It also shows you the IP address that students should directly connect to in order to join your world.

Before you quit the server, remember to save the world.

I also highly recommend that you make a backup of your saved world. Copy the appropriate folder to a USB drive or a network location that is backed up. If MinecraftEdu was installed by EIPS Tech Services, saved worlds will be in C:\Program Files (x86)\minecraftedu\servertool\worlds\savedworlds

Of course if you want to try out a simpler process, you (or a student) can start a single player world and allow others to join by typing /publish in the Minecraft chat or by opening the game menu and clicking the "Open to LAN" button. Unfortunately this doesn't allow you to use many of the MinecraftEdu features.

Friday, December 20, 2013

Displaying Calendar Events on Digital Signage Using a Raspberry Pi and Google Apps Script

In our office many of us are often out of the building, so we decided to hang a TV that would display our calendar events. The hardware consists of a 40" LED mounted on the wall, with a Raspberry Pi (with a Wi-Fi adapter) connected to the HDMI input.

To display the calendars, a Google Apps Script edits a Google Sites page, that is refreshed on the Raspberry Pi every 30 minutes. Google Apps Script is great at parsing Google Calendars, but unfortunately there is some incompatibility with our Exchange calendars, so I ended up using Yahoo!Pipes to parse the ICS files served by our Exchange server.

I'll write out some instructions for recreating what I've done, but your results may vary. Feel free to ask for help in the comments or on social media.

Start by creating yourself a Google Site that has no navigation elements or other clutter. Probably the easiest is to create one from this template I've shared.

Next, a Yahoo!Pipe to parse the ICS files. If you can subscribe to the calendars in Google Calendar then you don't need this because you can use the Calendar Service in Google Apps Script. I'll leave it to you to figure out how to do that, though. If you want to use Yahoo!Pipes to parse ICS files, check out the pipe I created.

Then a Google Apps Script to add events to the Google Site you created. If you're not familiar with Google Sites, there are many tutorials and code examples. I'll just paste my script code in here and hope you can make sense of it.

//Remember to set up a trigger set up to run every 30 minutes, or at whatever frequency you prefer
function runThis() {
  var page = SitesApp.getPageByUrl("https://sites.google.com/whatever-your-site-is-should-be-here");
  var dateToday = new Date();
  //format the date to print at the top of the webpage
  var dayToday = Utilities.formatDate(dateToday, Session.getTimeZone(), "EEEE, MMMM dd");
  //put the date at the top of the web page
  var pageContent = "<div style='font-size:larger'>Today is " + dayToday + "</div><br>";
  //add the calendar stuff by calling the appropriate functions and appending to the variable
  var pageContent = pageContent + parseCalendar("Your Name","#00FF00","http://your-link-to-an-online-ics-file.ics");
  var pageContent = pageContent + parseCalendar("Another Name","#FF0000","http://a-link-to-another-online-ics-file.ics");
  page.setHtmlContent(pageContent);
}

function parseCalendar(name,color,iCalUrl) {
  //declare and empty pageContent variable that we will fill with calendar entries
  var pageContent = "";
  //format the iCal URL for submission to Yahoo!Pipes
   var replaceColon = iCalUrl.replace(":","%3A");
   var replaceSlash = replaceColon.replace(/\//g,"%2F");
   var translatediCalUrl = replaceSlash.replace(/@/g,"%40");
   //replace spaces with + signs
   var translatedName = name.replace(" ","+");
   //concatenate the strings to make a URL
   var rssUrl = "http://pipes.yahoo.com/pipes/pipe.run?CalendarURL=" + translatediCalUrl + "&Name=" + translatedName + "&_id=9e11d02f251ade5c10a6f5501bfe181f&_render=rss"; 
  //fetch the RSS feed from that URL
  var rssContent = UrlFetchApp.fetch(rssUrl).getContentText();
  //parse the RSS feed that we just fetched
  var items = XmlService.parse(rssContent).getRootElement().getChild("channel").getChildren("item");
  //loop through the items we just parsed
  for (var i = 0; i < items.length; i++) {
    var item = items[i];
    var title = item.getChild("title").getText();
    // if there is a location, then get it from the "description" field
    if (item.getChild("description") != null) {
      var location = item.getChild("description").getText();
      var output = title + " at " + location;
      }
    // if there isn't a location, output just the title
    else {output = title};
    var pageContent = pageContent + "<div style='color:" + color + "'>" + output + "</div>\r";
    }
  return pageContent;
}
Finally, on the Raspberry Pi:
  1. Set up Debian Linux on the Pi:http://www.raspberrypi.org/downloads
  2. (optional): Force the Raspberry Pi to use HDMI output even if it doesn't detect a display there:
    1. in the terminal emulator type  sudo leafpad /boot/config.txt
    2. add the following lines to that file:
      1. hdmi_force_hotplug=1
      2. hdmi_drive=2
    3. remove (or comment out) any similar lines at the bottom of the file that may have been added by the NOOBS install process
    4. save and close the config.txt file
  3. Have the mouse cursor auto-hide using unclutter: in the terminal emulator type  sudo apt-get install unclutter
  4. Edit the autostart file: in the terminal emulator type  sudo leafpad /etc/xdg/lxsession/LXDE/autostart
  5. Disable screen sleeping and autostart the browser by adding the following lines to the file you just opened for editing (include the @ signs, but not the line numbers):
    1. @xset s off
    2. @set -dpms
    3. @xset s noblank
    4. @midori -e Fullscreen -i 1800 -a https://sites.google.com/whatever-your-site-is-should-be-here
    5. @unclutter -display :0.0 -idle 5
  6. Reboot the Raspberry Pi, and you're done.

Friday, October 18, 2013

School Announcements: Auto-Generating Announcement Documents (printable and viewable online)

Rather than having to manually create a document every day with the daily announcements, I've created a script that will do it for you. There are, of course, other features that could be added, but this is good enough for today.

To start, announcements are submitted via a Google Form, so they end in a spreadsheet. There are three pieces of data: the text of the announcement, the category, and the expiry date.


The script creates a new Google Document (in a public folder), then takes data from the spreadsheet and pastes it into that newly created document. The code for the script follows. (Creative Commons Attribution-ShareAlike).


function createAnnoucementDocument() {
  // Set up a trigger to run this every weekday, perhaps at 8:00 am
 // Define a custom paragraph style.
var styleHeading = {};
 styleHeading[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.CENTER;
 styleHeading[DocumentApp.Attribute.FONT_SIZE] = 18;
 styleHeading[DocumentApp.Attribute.BOLD] = true;

var styleCategory = {};
 styleCategory[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.LEFT;
 styleCategory[DocumentApp.Attribute.FONT_SIZE] = 12;
 styleCategory[DocumentApp.Attribute.BOLD] = true;

var styleText = {};
 styleText[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.LEFT;
 styleText[DocumentApp.Attribute.FONT_SIZE] = 12;
 styleText[DocumentApp.Attribute.BOLD] = false;

  // Get the current date
  var app = UiApp.createApplication();
  var dateToday = new Date();
  // date formatting here: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html
  var formattedDateToday = Utilities.formatDate(new Date(), "MST", "yyyy-MM-dd");
  
  // Create the document
  var documentName = formattedDateToday + " School Announcements";
  var doc = DocumentApp.create(documentName);
  // Move the document to the shared folder entitled "Announcements"
  var documentFile = DocsList.getFileById(doc.getId());
  var folderName = DocsList.getFolder("Announcements");
  documentFile.addToFolder(folderName);
  
  // the spreadsheet that contains the results of the announcement submission form
  var sheet = SpreadsheetApp.openById("***INSERT_SPREADSHEET_KEY_HERE***").getSheets()[0];

  // Start creating the body of the document
  var body = doc.getBody();
  
  body.appendParagraph("Name of School Goes Here\r"+formattedDateToday).setAttributes(styleHeading);

  // Read the whole spreadsheet into a list
  //  if Expiry Date (column D) is > or equal to today's date then process it, else ignore
  //  switch if category match then append to document

  var data = sheet.getRange(2, 2, sheet.getLastRow(), sheet.getLastColumn()).getValues();
  for (var row=0, total=data.length; row < total; row++) {
    var rowData = data[row];
    var announcementText = rowData[0];
    var announcementCategory = rowData[1];
    var announcementExpiry = rowData[2];
    if (announcementExpiry >= dateToday) {
      switch (announcementCategory) {
        case "General":
          body.appendParagraph(announcementText).setAttributes(styleText);
        break;
      }
    }
  }
  body.appendParagraph("Events and Meetings").setAttributes(styleCategory);
    for (var row=0, total=data.length; row < total; row++) {
    var rowData = data[row];
    var announcementText = rowData[0];
    var announcementCategory = rowData[1];
    var announcementExpiry = rowData[2];
    if (announcementExpiry >= dateToday) {
      switch (announcementCategory) {
        case "Events and Meetings":
          body.appendParagraph(announcementText).setAttributes(styleText);
        break;
      }
    }
  }
  body.appendParagraph("Athletics").setAttributes(styleCategory);
    for (var row=0, total=data.length; row < total; row++) {
    var rowData = data[row];
    var announcementText = rowData[0];
    var announcementCategory = rowData[1];
    var announcementExpiry = rowData[2];
    if (announcementExpiry >= dateToday) {
      switch (announcementCategory) {
        case "Athletics":
          body.appendParagraph(announcementText).setAttributes(styleText);
        break;
      }
    }
  }
  body.appendParagraph("Cafeteria").setAttributes(styleCategory);
    for (var row=0, total=data.length; row < total; row++) {
    var rowData = data[row];
    var announcementText = rowData[0];
    var announcementCategory = rowData[1];
    var announcementExpiry = rowData[2];
    if (announcementExpiry >= dateToday) {
      switch (announcementCategory) {
          case "Cafeteria":
        body.appendParagraph(announcementText).setAttributes(styleText);
        break;
      }
    }
  }
  return app;
}

Monday, October 14, 2013

Gamification of Health

Feel free to correct me in the comments, but I'm seeing two main trends in the gamification of health. The first is using game mechanics for fitness, motivating us to get us off the couch. The second is gamified, or at least game-based, treatments.

Since I'm and educator and not a health professional, I'm not particularly qualified to comment on the latter. However I have been reading many interesting articles about video games for pain reduction or for treating ADHD, and I'm very interested in devices that help us measure ourselves. For example, check out this story about using an inexpensive EEG device together with video games as therapy for ADHD.

Of broader application, though, is the use of game mechanics to help reverse our sedentary patterns. Devices such as the FitBit products, and games such as Nike+ Kinect Training help us to measure ourselves and set goals. As I write this during a weekend of overeating, I realize that these things need to be as "frictionless" as possible. It's much easier to have another slice of pie than to go to the gym, or to turn on the Xbox and spend half an hour working out. There continues to be a lot of thought put into seamlessly incorporating fitness (and motivation) into our daily lives.

Often the best motivation, for fitness or anything else, involves collaboration or competition with other people. Upcoming competitions such as triathlons or games encourage us to train, and collaborations such the November Project motivate us because our friends are doing it. Of course this means that we need to convince our friends to participate.

In education, however, we have a unique environment with a captive audience. Students participate in events such as the Terry Fox Run, as well as school sports and intramurals. Some organizations try to replicate this with Corporate Challenges, or with online gamification systems such as OfficeVibe, but for some reason those don't seem as successful. Maybe because we don't usually have PhysEd teachers working in our offices.

So there are two things that I'm thinking about related to this. First, of course, is how to continue use our time with students to encourage lifelong fitness. The second, though, is how to replicate for adults the fitness motivation that we see in schools. I see the principles of gamification as one of the best ways to continue doing that.

But first I think I'll go have another slice of pie.

Gamification of Health

Feel free to correct me in the comments, but I'm seeing two main trends in the gamification of health. The first is using game mechanics for fitness, motivating us to get us off the couch. The second is gamified, or at least game-based, treatments.

Since I'm and educator and not a health professional, I'm not particularly qualified to comment on the latter. However I have been reading many interesting articles about video games for pain reduction or for treating ADHD, and I'm very interested in devices that help us measure ourselves. For example, check out this story about using an inexpensive EEG device together with video games as therapy for ADHD.

Of broader application, though, is the use of game mechanics to help reverse our sedentary patterns. Devices such as the FitBit products, and games such as Nike+ Kinect Training help us to measure ourselves and set goals. As I write this during a weekend of overeating, I realize that these things need to be as "frictionless" as possible. It's much easier to have another slice of pie than to go to the gym, or to turn on the Xbox and spend half an hour working out. There continues to be a lot of thought put into seamlessly incorporating fitness (and motivation) into our daily lives.

Often the best motivation, for fitness or anything else, involves collaboration or competition with other people. Upcoming competitions such as triathlons or games encourage us to train, and collaborations such the November Project motivate us because our friends are doing it. Of course this means that we need to convince our friends to participate.

In education, however, we have a unique environment with a captive audience. Students participate in events such as the Terry Fox Run, as well as school sports and intramurals. Some organizations try to replicate this with Corporate Challenges, or with online gamification systems such as OfficeVibe, but for some reason those don't seem as successful. Maybe because we don't usually have PhysEd teachers working in our offices.

So there are two things that I'm thinking about related to this. First, of course, is how to continue use our time with students to encourage lifelong fitness. The second, though, is how to replicate for adults the fitness motivation that we see in schools. I see the principles of gamification as one of the best ways to continue doing that.

But first I think I'll go have another slice of pie.

Thursday, September 19, 2013

Empowered by Technology

The reason that I like technology so much is that it's empowering. The Internet, programming, and even video games allow us to do things that would be otherwise impossible. These technologies also give us a very real sense of power and control over our lives.

The Internet is the greatest repository of information, and misinformation, ever created. You can learn how to do things, share your own expertise, and even look up obscure facts. Scientia potentia est.

Programming is another way that technology empowers us. Basic services such as "If This Then That" (ifttt.com) and more advanced programming languages such as Python allow us to make these machines do our bidding. We can turn our ideas into reality.

Video games are, for many, much more than entertainment. They are empowering in that they allow us control over our own stories and our virtual worlds. They make possible things that are otherwise impossible or impractical, such as driving really fast, flying, or building worlds for others to experience. Minecraft is a great example.

Of course we also need to consider the negative impacts that technology is having on our society and on ourselves. TLDR.

Saturday, June 1, 2013

Google Apps Bulk Delete Users Script

Google Apps has a bulk account creation tool, but nothing for bulk deleting accounts. I had previously written something in Python to bulk delete accounts from a text file, but it was time to create something web-based. This will only work if you have the Domain API enabled, which means that you'll have to enable it in your Google Apps for Education Admin console.

Just a note, though, before deleting users you may want to direct them to the Data Liberation tools for downloading their data.

The actual script is in Google Apps Script. You can see it by visiting Google Apps Delete Users, or create your own copy at script.google.com using the source code below.

Let me know if this works for you, or if you have suggestions for improvements.


 function doGet() {  
 // get the user's credentials for their Google Apps account  
  var user = Session.getEffectiveUser().getUserLoginId()  
  var domain = UserManager.getDomain();  
  var welcome = "You are running this script as " + user + " on the domain " + domain;  
 // set up the user interface  
  var app = UiApp.createApplication().setTitle('Delete Google Apps Users by MisterHay');  
  app.add(app.createLabel(welcome));  
  app.add(app.createHTML("<br>Make sure you have enabled the Provisioning API (support.google.com/a/bin/answer.py?hl=en&answer=60757).<br>This script will delete Google Apps user accounts that you paste or type below. Each account name must be on its own line.<br>e.g.<br>misterhay<br>mpython<br>unladen.swallow<p>"));  
  var textArea = app.createTextArea().setName("textArea").setSize("20%", "60%").setStyleAttribute("background", "white").setStyleAttribute("color", "black").setFocus(true);  
  var serverHandler = app.createServerHandler("deleteAccounts").addCallbackElement(textArea);  
  var clientHandler = app.createClientHandler().forEventSource().setEnabled(false).setText("deleting accounts...");  
  app.add(textArea);  
  var button = app.createButton("Delete Accounts");  
  button.addClickHandler(serverHandler);  
  button.addClickHandler(clientHandler);  
  app.add(button);  
  app.add(app.createLabel("no accounts deleted").setId("finishedLabel").setVisible(false));  
  return app;  
 }  
   
 function deleteAccounts(eventInfo) {  
  var app = UiApp.createApplication();  
  var deleteThese = eventInfo.parameter.textArea;  
  var stringArray = deleteThese.split(/\n/);  
  for (var loopNumber = 0; loopNumber < stringArray.length; loopNumber++) {  
   var deleteThisUser = stringArray[loopNumber];  
 // rename the accounts before we delete them to avoid the five day wait before account names can be reused  
   var renamedUser = deleteThisUser + '_old';  
   Logger.log(renamedUser);  
 // delete the account  
   UserManager.getUser(deleteThisUser).setUsername(renamedUser);  
   UserManager.getUser(renamedUser).deleteUser();  
   }  
 // tell the user how many accounts we deleted.  
  app.getElementById("finishedLabel").setText(loopNumber + " accounts deleted").setVisible(true);  
  return app;  
 }