Das Unternehmen office:control + setzt für die Büroarbeit verschiedene Google-Anwendungen ein und der Geschäftsführer, Michael Witte, fragte mich, ob ich dafür schon mal etwas programmiert hätte. Ich habe zwar in mehreren Unternehmensprojekten Google Web Toolkit (GWT) eingesetzt und auch mal mit der Google App Engine rumgespielt, aber das meinte er offenbar nicht. Für kollaboratives Arbeiten soll sich ja Google Documents, Spreadsheets und Drive gut eignen, aber wie und was soll man da programmieren?
Er meinte, die Büroarbeit habe er mit einem einfachen papierlosen Workflow schon gut automatisiert und einige Optimierungen wären noch möglich:
- eintreffendes Dokument scannen, abheften und nie wieder raussuchen müssen
- Verzeichnis der eingescannten Dateien mit Google Drive synchronisieren
- Google Documents für selbst geschriebene Dokumente nutzen und unter Google Drive ablegen
- Nutzung der Google Drive – Volltextsuche zum Auffinden von eigenen oder eingescannten Dokumenten
- Erfassung von Dokument-Zusatzdaten (Adresse, Datum) mit dem Formulardesigner Google Forms
- Bereitstellen des Formulars auf einer Website (Google Site)
- Abspeichern der Formulardaten in einer Tabelle (Google Forms -> Google Spreadsheet)
Das funktioniert schon alles direkt mit den Google-Anwendungen. Und wenn wir etwas wollen, was Google noch nicht anbietet? Dann verwenden wir GAS (Google Apps Script) und die APIs der verschiedenen Google-Anwendungen (Documents, Calendar, Contacts, Drive, Forms, Gmail, Maps, Sites, Spreadsheets):
Was ist GAS, was kann GAS und wie lege ich mit GAS los ?
Google Apps Script ist also eine Skriptsprache, die auf JavaScript beruht und mit der wir Zugriff auf die verschiedenen Google-APIs haben. Entwickelt wird ein Skript innerhalb des Script Editors, einer im Browser ausgeführten Entwicklungsumgebung. Die einfachen GAS-Beispiele unter Quickstart mit nur wenigen Zeilen Code sind schon beeindruckend:
- E-Mail mit dem Link eines neu erstellten Dokuments verschicken
- Spreadsheet um Menueinträge erweitern, um damit für eine weitere Tabellenspalte die Routen und Distanzen zwischen Orten zu berechnen
- Formular für eine Veranstaltungsteilnahme bereitstellen und per Mail verschicken, die Teilnahmedaten in einem Spreadsheet sammeln und entsprechende Kalendereinträge erzeugen
Wenn man mit App-Script eine Anwendung entwickelt hat, kann die Ausführung auf unterschiedliche Weise erfolgen:
- Starten als Standalone-Skript durch den Entwickler aus dem Skript-Editor heraus
- Starten eines container-bound Skripts aus einer Anwendung (z.B. Documents, Spreadsheet), beispielsweise über einen eigenen Menueintrag
- Anwendung ist eine Web-App mit einer eigenen Benutzeroberfläche, die über eine Internetadresse aufrufbar ist
- Anwendung ist eine Web-App und wird als sogenanntes Gadget in einer Google Site ausgeführt
- Anwendung ist eine Web-App und wird im Chrome Web Store registriert, wodurch jeder Chrome-Nutzer die Chrome-App installieren und verwenden kann
Ein Praxisbeispiel – klein aber fein
Als kleines Beispiel habe ich mit GAS eine Web-App erstellt, die die Dateien von Google-Drive in einer Liste darstellt. Die Datei kann der Anwender sich anzeigen lassen oder in den Papierkorb verschieben.
Für die Web-App wird der HTML Service genutzt, sodass wir im Javascript-Code die Methode doGet()
implementieren und darin die Template-Datei index.html
zurückliefern.
/** * Hauptmethode - liefert das Template "index.html" beimladen der Seite. */ function doGet() { return HtmlService .createTemplateFromFile('index') .evaluate() .setSandboxMode(HtmlService.SandboxMode.NATIVE); } /** * Liefert alle Dateien zurück. */ function getAllFiles() { return DocsList.getAllFiles();; } /** * Formatiert das gegebene Datum. */ function formattedDate(date) { return Utilities.formatDate(date, "GMT+1", "dd.MM.yy HH:mm"); } /** * Verarbeiten der Form-Daten. */ function processFormData(formObject) { Logger.log('Submitted data: ' + JSON.stringify(formObject)); var isError = true; var message = "Unbekannter Fehler"; try{ var fileId = formObject.fileId; var file = DocsList.getFileById(fileId); if (file.isTrashed()) { throw "Datei '" + file.getName() + "' ist schon im Papierkorb."; } file.setTrashed(true); if (!file.isTrashed()) { throw "Datei '" + file.getName() + "' konnte nicht in den Papierkorb verschoben werden."; } isError = false; message = "Datei '" + file.getName() + "' in den Papierkorb verschoben." } catch(e){ message = JSON.stringify(e); Logger.log("Error: " + e); } var resultData = { "fileId": file.getId(), isError : isError, "message": message }; Logger.log('Result data: ' + JSON.stringify(resultData)); return resultData; } /** * Liefert die Bild-Alt-Text zu einer Google-Drive Datei. */ function typeImageTitle(file) { if (file.getFileType() == "document") return "Document" if (file.getFileType() == "spreadsheet") return "Spreadsheet" if (file.getFileType() == "form") return "Form" if (file.getFileType() == "other" && file.getThumbnail()) return "PDF"; return "Script" } /** * Liefert die Bild-Url zu einer Google-Drive Datei (die Bild-URLs sind vielleicht nicht stabil). */ function typeImageUrl(file) { var imageTitle = typeImageTitle(file); if (imageTitle == "Document") return "https://lh5.googleusercontent.com/-sCg4gG_WAnM/UNS3_JRkvVI/AAAAAAAAAyk/8OT1wOmSC30/s16-c-k/icon_11_document_list.png" if (imageTitle == "Spreadsheet") return "https://lh6.googleusercontent.com/-FZUEfCuNFvk/UNS7N6lG3pI/AAAAAAAAA1M/B4BuwXzsaww/s16-c-k/icon_11_spreadsheet_list.png" if (imageTitle == "Form") return "https://lh5.googleusercontent.com/-R8Mc7jjWGq0/UNS3W6hl6aI/AAAAAAAAAxo/xJLH8ZHWs_4/s16-c-k/icon_11_form_list.png" if (imageTitle == "PDF") return "https://ssl.gstatic.com/docs/doclist/images/icon_10_pdf_list.png"; if (imageTitle == "Script") return "https://lh4.googleusercontent.com/-P5Z16f0Uq3M/UOSgfQryqOI/AAAAAAAAAro/13qi80f3mRI/s16-c-k/icon_11_script_list.png"; return ""; }
Für ein solides Layout nutzt das Template index.html
Bootstrap und JQuery.
<!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> <!-- Optional theme, liefert Farbverläufe für Meldung und Buttons --> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css"> <style> #resultBoxSuccess, #resultBoxDanger { display: none; } .formAction { display: inline-block; margin-left: 5px; } .ocRow .ocCell { font-size: 16px; vertical-align: middle; } </style> <div class="container"> <div class="page-header"> <h1>Andreas Bruns <small> GAS-Beispiel mit Google-Drive</small> </h1> </div> <div id="resultBoxSuccess" class="alert alert-success"> <strong>Ergebnis: </strong><span id="resultTextSuccess">---</span> </div> <div id="resultBoxDanger" class="alert alert-danger"> <strong>Ergebnis: </strong><span id="resultTextDanger">---</span> </div> <? var files = getAllFiles(); ?> <? if (files.length == 0) { ?> <div class="alert alert-success"> <strong>Keine Dateien vorhanden.</strong> </div> <? } else { ?> <div class="table-responsive"> <table class="table table-bordered table-hover"> <thead> <tr> <th class="text-center">Icon</th> <th class="text-center">Name</th> <th class="text-center">Erstellt</th> <th class="text-center">Aktion</th> </tr> </thead> <tbody> <? for (var i = 0; i < files.length; i++) { ?> <?!= '<tr id="' + files[i].getId() + '" class="ocRow">' ?> <td class="text-center"> <?!= '<img src="' + typeImageUrl(files[i]) + '" title="' + typeImageTitle(files[i]) +'" height="33" width="33">' ?> </td> <td class="ocCell"><?= files[i].getName() ?> </td> <td class="ocCell text-center"><?= formattedDate(files[i].getDateCreated()) ?> </td> <td class="ocCell text-center"> <?!= '<a href="' + files[i].getUrl() + '" class="btn btn-primary">anzeigen</a>' ?> <form class="formAction"> <?!= '<input name="fileId" type="hidden" value="' + files[i].getId() + '" />' ?> <input type="button" value="Papierkorb" onclick="trashFile(this.parentNode)" class="btn"/> </form> </td> <?!= '</tr>' ?> <? } ?> </tbody> </table> </div> <? } ?> </div> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script> <script type="text/javascript" src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> <script> function trashFile(formData) { google.script.run.withFailureHandler(failureResult).withSuccessHandler(successResult).processFormData(formData); } function failureResult(result) { $(".alert-danger").show(); $(".alert-success").hide(); $("#resultTextDanger").html("Unbekannter Fehler"); } function successResult(result) { if (result.isError) { $(".alert-danger").show(); $(".alert-success").hide(); $("#resultTextDanger").html(result.message); } else { var rowSelector = "#" + result.fileId; var rowFormSelector = "#" + result.fileId + " form"; var rowFormButtonSelector = "#" + result.fileId + " form input"; $(rowFormButtonSelector).remove(); $(rowFormSelector).parent().html("<b>gelöscht</b>"); $("#resultBoxSuccess").show(); $("#resultBoxDanger").hide(); $("#resultTextSuccess").html(result.message); } } </script>
Wenn wir die Web-App veröffentlichen, können wir unter dem angegebenen Link die Web-App nutzen. Mit dem selben Link können wir auch die Web-App als Gadget in eine Google-Site einbauen:
Fazit: Umfangreich und einfach mit ein paar Einschränkungen
Schon bei dieser kleinen Anwendung merkt der GAS-Neuling, dass Google die GAS-APIs rasant weiterentwickelt. Manche Anforderungen lassen sich nicht umsetzen, manche Lösungen aus Foren sind nicht mehr aktuell oder es gibt alternative Lösungswege:
- in einem IFrame soll ein PDF-Dokument angezeigt und dynamisch ausgetauscht werden
=> GAS nutzt den Caja Compiler, der keine IFrames unterstützt: Bug: 852, weitere Restriktionen - erstelle ich die Web-App mit dem UI Service oder dem HTML Service ?
=> Empfehlung: HTML Service - nutze ich lieber die Drive-API oder DocList-API ?
=> Empfehlung: Drive-API
GAS bietet viele Möglichkeiten, Google Anwendungen zu erweitern oder eigene Web-Apps mit wenig Code zu erstellen. Für office:control + habe ich erst einmal eine kleine Web-App entwickelt, deren Praxistauglichkeit jetzt geprüft wird. Wer keine Lust zum selber Loslegen hat, kann mit diesem (etwas veralteten) Video tiefer in die GAS-Welt eintauchen – viel Spaß 😉