Παρασκευή 30 Μαρτίου 2012

Google Apps Script και (ημι)Αυτόματη Δημοσίευση Αναρτήσεων


Για την υλοποίηση της πλατφόρμας επίδειξης των έργων της Έκθεσης Μαθητικής Δημιουργίας Έργων Πληροφορικής νομών Ρεθύμνου Χανίων 2011-2012 (http://ekthesi-mathitikis-dimiourgias.blogspot.com/), χρειάστηκε να αναπτυχθεί μια διαδικασία, η οποία να επιτρέπει στους υπεύθυνους καθηγητές να καταχωρούν τα έργα των μαθητών τους με εύκολο - αυτόματο τρόπο στην πλατφόρμα, ώστε να μην χρειαστεί να επιφορτιστούν άλλα άτομα με την ευθύνη να καταχωρούν τα έργα των μαθητών ένα προς ένα.

Η λύση που επιλέχτηκε ήταν να δημιουργηθεί μια φόρμα με την χρήση Google Docs, την οποία θα συμπληρώνει ο καθηγητής για κάθε έργο και θα την αποστέλλει.
Με την αποστολή της φόρμας, τα στοιχεία θα καταχωρούνται (κλασικά) σε υπολογιστικό φύλλο Google και θα στέλνονται 2 ηλεκτρονικά μηνύματα. Το ένα θα είναι ηλεκτρονικό μήνυμα επιβεβαίωσης προς τον καθηγητή με τα στοιχεία που συμπλήρωσε ενώ το δεύτερο ηλεκτρονικό μήνυμα θα φτάνει στο ιστολόγιο που θα υλοποιεί την πλατφόρμα ώστε να δημιουργείται αυτόματα μια ανάρτηση για το έργο. Ο διαχειριστής του ιστολογίου της έκθεσης θα κάνει έναν έλεγχο στην ανάρτηση, θα της τοποθετεί την χαρακτηριστική φωτογραφία που αναφέρει ο καθηγητής και θα την δημοσιεύει.
Για την υποδομή του ιστολογίου επιλέχτηκε το blogger επειδή υποστηρίζει την δημιουργία αναρτήσεων μέσω της αποστολής email (http://support.google.com/blogger/bin/answer.py?hl=en&answer=41452) και λόγω της δυνατότητας εμφάνισης των αναρτήσεων με μοντέρνο και όμορφο τρόπο μέσω των dynamic views (http://support.google.com/blogger/bin/answer.py?hl=en&answer=1229061).
Αναπτύχθηκε λοιπόν κώδικας σε Google Apps Script, ο οποίος ενσωματώθηκε στην φόρμα υποβολής έργου για την έκθεση. Ο κώδικας αυτός ενεργοποιείται με την υποβολή της φόρμας.
Για την ανάπτυξη του κώδικα χρησιμοποιήθηκαν λειτουργίες που βρέθηκαν στο Tutorial: Reading Spreadsheet data using JavaScript Objects αλλά και αλλού :)

Ο κώδικας που χρησιμοποιήθηκε ακολουθεί...
// Μανόλης Φραγκονικολάκης
// συνάρτηση αποστολής μηνυμάτων...
function sendEmails() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  //το πρώτο φύλλο περιέχει τις καταχωρήσεις της φόρμας
  var dataSheet = ss.getSheets()[0];
  var dataRange = dataSheet.getRange(2, 2, dataSheet.getMaxRows() - 1, 16);

  //το δεύτερο φύλλο περιέχει την html template για το μήνυμα στον blogger
  var templateSheet = ss.getSheets()[1];
  var emailTemplate = templateSheet.getRange("A1").getValue();

  objects = getRowsData(dataSheet, dataRange);

  for (var i = 0; i < objects.length; ++i) {
    //μόνο για την τελευταία καταχώρηση στο υπολογιστικό φύλλο
    if (i==objects.length-1) {
      var rowData = objects[i];

      var emailText = fillInTemplateFromObject(emailTemplate, rowData);
      var emailSubject = rowData[normalizeHeader("${title}")] + " - " + rowData[normalizeHeader("${school}")];      //αποστολή μηνύματος στον blogger
      MailApp.sendEmail("username.secretword@blogger.com", emailSubject, "", { htmlBody: emailText });
    }
  }

  //το τρίτο φύλλο περιέχει την template για το μήνυμα στον καθηγητή
  templateSheet = ss.getSheets()[2];
  emailTemplate = templateSheet.getRange("A1").getValue();

  objects = getRowsData(dataSheet, dataRange);

  for (var i = 0; i < objects.length; ++i) {
    if (i==objects.length-1) {
      var rowData = objects[i];

      var emailText = fillInTemplateFromObject(emailTemplate, rowData);
      var emailSubject = "Υποβολή έργου μαθητικής δημιουργίας: " + rowData[normalizeHeader("${title}")] + " - " + rowData[normalizeHeader("${school}")];

      //αποστολή μηνύματος στον καθηγητή
      MailApp.sendEmail(rowData[normalizeHeader("${contact}")], emailSubject, emailText);
    }
  }

}


// Replaces markers in a template string with values define in a JavaScript data object.
// Arguments:
//   - template: string containing markers, for instance ${"Column name"}
//   - data: JavaScript object with values to that will replace markers. For instance
//           data.columnName will replace marker ${"Column name"}
// Returns a string without markers. If no data is found to replace a marker, it is
// simply removed.
function fillInTemplateFromObject(template, data) {
  var email = template;
  // Search for all the variables to be replaced, for instance ${"Column name"}
  var templateVars = template.match(/\$\{\"[^\"]+\"\}/g);

  // Replace variables from the template with the actual values from the data object.
  // If no value is available, replace with the empty string.
  for (var i = 0; i < templateVars.length; ++i) {
    // normalizeHeader ignores ${"} so we can call it directly here.
    var variableData = data[normalizeHeader(templateVars[i])];
    email = email.replace(templateVars[i], variableData || "");
  }

  return email;
}

//////////////////////////////////////////////////////////////////////////////////////////
//
// The code below is reused from the 'Reading Spreadsheet data using JavaScript Objects'
// tutorial.
//
//////////////////////////////////////////////////////////////////////////////////////////

// getRowsData iterates row by row in the input range and returns an array of objects.
// Each object contains all the data for a given row, indexed by its normalized column name.
// Arguments:
//   - sheet: the sheet object that contains the data to be processed
//   - range: the exact range of cells where the data is stored
//   - columnHeadersRowIndex: specifies the row number where the column names are stored.
//       This argument is optional and it defaults to the row immediately above range; 
// Returns an Array of objects.
function getRowsData(sheet, range, columnHeadersRowIndex) {
  columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;
  var numColumns = range.getEndColumn() - range.getColumn() + 1;
  var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);
  var headers = headersRange.getValues()[0];
  return getObjects(range.getValues(), normalizeHeaders(headers));
}

// For every row of data in data, generates an object that contains the data. Names of
// object fields are defined in keys.
// Arguments:
//   - data: JavaScript 2d array
//   - keys: Array of Strings that define the property names for the objects to create
function getObjects(data, keys) {
  var objects = [];
  for (var i = 0; i < data.length; ++i) {
    var object = {};
    var hasData = false;
    for (var j = 0; j < data[i].length; ++j) {
      var cellData = data[i][j];
      if (isCellEmpty(cellData)) {
        continue;
      }
      object[keys[j]] = cellData;
      hasData = true;
    }
    if (hasData) {
      objects.push(object);
    }
  }
  return objects;
}

// Returns an Array of normalized Strings.
// Arguments:
//   - headers: Array of Strings to normalize
function normalizeHeaders(headers) {
  var keys = [];
  for (var i = 0; i < headers.length; ++i) {
    var key = normalizeHeader(headers[i]);
    if (key.length > 0) {
      keys.push(key);
    }
  }
  return keys;
}

// Normalizes a string, by removing all alphanumeric characters and using mixed case
// to separate words. The output will always start with a lower case letter.
// This function is designed to produce JavaScript object property names.
// Arguments:
//   - header: string to normalize
// Examples:
//   "First Name" -> "firstName"
//   "Market Cap (millions) -> "marketCapMillions
//   "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored"
function normalizeHeader(header) {
  var key = "";
  var upperCase = false;
  for (var i = 0; i < header.length; ++i) {
    var letter = header[i];
    if (letter == " " && key.length > 0) {
      upperCase = true;
      continue;
    }
    if (!isAlnum(letter)) {
      continue;
    }
    if (key.length == 0 && isDigit(letter)) {
      continue; // first character must be a letter
    }
    if (upperCase) {
      upperCase = false;
      key += letter.toUpperCase();
    } else {
      key += letter.toLowerCase();
    }
  }
  return key;
}

// Returns true if the cell where cellData was read from is empty.
// Arguments:
//   - cellData: string
function isCellEmpty(cellData) {
  return typeof(cellData) == "string" && cellData == "";
}

// Returns true if the character char is alphabetical, false otherwise.
function isAlnum(char) {
  return char >= 'A' && char <= 'Z' ||
    char >= 'a' && char <= 'z' ||
    isDigit(char);
}

// Returns true if the character char is a digit, false otherwise.
function isDigit(char) {
  return char >= '0' && char <= '9';
}

Το template του ηλεκτρονικού μηνύματος για την ανάρτηση στον blogger
<span style="font-size: small;">${"title"}</span>
<span class="year2012"><b>Σχολική χρονιά>: 2011-2012</span>
<span class="prefecture"><b>Νομός>: ${"prefecture"}</span>
<span class="school"><b>Σχολείο>: ${"school"}</span>
<span class="class"><b>Τάξη>: ${"class"}</span>
<span class="teachers"><b>Υπεύθυνοι καθηγητές></span>
<span class="teachersmore">${"teachers"}</span>
<span class="students"><b>Ομάδα μαθητών></span>
<span class="studentsmore"> ${"students"}</span>
<span class="description"><b>Περιγραφή</b></span>
<span class="descriptionmore">${"description"}</span>
<span class="info"><b>Άλλες πληροφορίες></span>
<span class="infomore">${"info"}</span>
<span class="software"><b>Λογισμικό></span>
<span class="softwaremore">${"software"}</span>
<div align="center">
${"code"}
</div>
<span class="website"><b>Ιστοσελίδα έργου ή αποθήκευση πρωτότυπου</b></span> 
<span class="websitemore"><a href="${"webpage"}" target="_blank" webpage"}"="">${"webpage"}</a></span>


Το template του ηλεκτρονικού μηνύματος για τον καθηγητή που υποβάλει την φόρμα
Η υποβολή έργου έγινε με επιτυχία!
Για ερωτήσεις ή παρατηρήσεις επικοινωνήστε με plinetcn@sch.gr

Καταχωρήσατε τις παρακάτω πληροφορίες.
---------------------------------------------------------------

Τίτλος: ${"title"}
Σχολική χρονιά: 2011-2012
Νομός: ${"prefecture"}
Σχολείο: ${"school"}
Τάξη: ${"class"}
Email επικοινωνίας: ${"contact"}
Κατηγορία: ${"category"}

Υπεύθυνοι καθηγητές
${"teachers"}

Ομάδα μαθητών 
${"students"}

Περιγραφή
${"description"}

Άλλες πληροφορίες
${"info"}

Λογισμικό
${"software"}

κώδικας ενσωμάτωσης
${"code"}

Ιστοσελίδα έργου ή αποθήκευση πρωτότυπου
${"webpage"}

Χαρακτηριστική εικόνα
${"image"}

Σχόλια, παρατηρήσεις
${"comments"}

Μια καλή πηγή για την εκμάθηση συγγραφής κώδικα σε Google Apps Script είναι: https://developers.google.com/apps-script/

Δεν υπάρχουν σχόλια: