2018. szept. 03. - Szerző: Sicz-Mesziár János

Script Your Own Company - Google Drive Folder Template

Első gyakorlatiasabb példánkban egy könnyebb témát vennék elő. Fókuszáljunk csak a Google Drivera és nézzük meg, hogyan tudunk könyvtárakkal és fájlokkal dolgozni. Készítsünk egy olyan Google Apps Scriptet ami rendelkezik egy webes frontend felülettel, standalone webalkalmazás és a cég igényeinek megfelelő Projekt könyvtárstruktúrát hozza létre a jogosultságokkal együtt. Jó kezdésnek gondolom, mivel a különböző típusú dokumentumokat itt fogod megtalálni.

syoc_-_google-drive-folder-temaplate-cover.png

Mit tud a Google Drive?

A Google Drive egy online elérhető (felhő alapú) tárhely szolgáltatás, előfizetői csomagtól függő kapacitással és limitekkel. A számítógépeden és telefonodon tárolt adatokat automatikusan tudja szinkronizálni. Lehetőséged van megosztani másokkal is. Rendelkezik webes felülettel így a top böngészők bármelyikéből authentikációt követően is elérheted a fájlokat.

Szerintem ezek a hétköznapi felhasználók számára is világosak.

Ugyanakkor ne érd be ennyivel, ennél több érdekességet is tartalmaz!

1.) Nem sokan tudják, hogy a Drive nem a klasszikus, jól ismert fájl és könyvtár működési modellel operál.

  • minden fájlt és könyvtárat egy ID azonosít, nem az elérési útja. Azaz ha mozgatod vagy átnevezed ugyanúgy működik a hivatkozás, ami programozási szempontból nagy könnyebbség lesz
  • megengedi, hogy egy könyvtárban ugyanolyan néven és kiterjesztésen több fájl is feltöltésre kerüljön
  • a feltöltött fájlokat verziózni is tudja
  • sokkal kevesebben tudják, hogy egy fájl vagy egy könyvtár több könyvtárban is létezhet. Másképpen fogalmazva több szülője is lehet. Ez sokkal nagyobb kincs céges környezetben mint elsőre gondolnánk. Hányszor is kellett döntened, hogy ez most melyik helyre illik a legjobban? Hát tedd ugyanazt a fájlt mindkét helyre. Ehhez jelöld ki a fájlt vagy mappát és nyomj Shift+Z billentyűt. Ekkor kiválaszthatod, hogy melyik másik mappában legyen még jelen. 

    syoc_-_google-drive-folder-template-shiftz.jpg

    Tehát nem kell gondolkozni azon, hogy mondjuk a bemutató anyagok a projekt könyvtárába, vagy egy összes bemutatót tartalmazó könyvtárba tegyük. Kvázi úgy is tekinthetjük, hogy minden mappa egyben egy címke is, amit fájlokon alkalmazhatunk.

    (Megjegyzés: ezért sem lehet útvonal a fájl azonosítása, mivel függ attól, hogy melyik könyvtárból közelítesz)

    syoc_-_google-drive-folder-template-multi.png

2.) Team Drive megint egy másik koncepció, ahol csapatokban ill. projektekben lehet gondolkozni. A Drive egyik szent és sérthetetlen szabálya, hogy minden feltöltött tartalomnak van egy tulajdonosa. Ez persze átruházható, de ha egy-egy kolléga távozik, majd a fiókját meg kell szüntetni és tegyük fel nincs utódja, akkor ez nem feltétlen egyszerű. A Team Drive ennek feloldása. Teszi ezt oly módon, hogy ma már a projektek esetén nem az aktuális egyén a meghatározó, hanem a projekt vagy a csapat. A dokumentumok tulajdonosa maga a team, így rugalmas a személyi cseréket illetően.

Ez a koncepció jól összeegyeztethető a Slack-nél alkalmazott channel filozófiájával.

3.) G-Suite alatt értelmet nyer a Google Groups. Nem csak levelező listának lehet jó, hanem Google Drive alatt a megosztásokat csoportnak is beállíthatod. Kezdetnek egy klasszikus hierarchiában működő cég esetében létrehozhatsz szerepkörök szerinti csoportokat, például Project Manager, Business Analyst, Sales, stb... Majd csoportokra mondod meg mely mappákat osztod meg. Így személyi cserék vagy új alkalmazottak esetén csak a csoport tagságot kell frissíteni és a megosztások már működnek is. Ezzel a céged hatékonysága nő, mivel csökkenteni tudod a fluktuációs terheket, időt és pénzt spórolsz.

syoc_-_google-drive-folder-template-group-share.jpg

Innen már csak egy lépés, hogy ugyanúgy flat módon projekteknek és csapatoknak adj jogosultságot. Egyszerűen egy-egy Google Groups csoport egy-egy csapatnak és/vagy projektnek felel meg.

Google Drive könyvtár sablon készítése

Most hogy jobban megismertük a Google Drive működését nézzük meg hogyan tudnánk ezt összehangolni és Google Apps Scripttel megtámogatni.

Ponte műhelyéből átemelve néhány bevált módszert legyen cél az alábbi működés megteremtése:

Szerepek:

  • Mindenki - minden alkalmazott a cégben
  • CEO - ügyvezető
  • PM - projekt menedzserek
  • BA - üzleti elemző
  • [Project] members - projektenkénti szerep, akik egy adott projektben részt vesznek

Könyvtár struktúrája:

Projektek / [Ügyfél neve] / [Projekt neve] / ...

Majd azon belül az alábbi struktúrát követi:

 ⤷ Üzleti ajánlat
 ⤷ Ügyfél dokumentumok
   ⤷ Jegyzőkönyvek
   ⤷ Specifikációk

 ⤷ Fejlesztés
   ⤷ Arculat
   ⤷ Specifikációk
   ⤷ Dokumentációk

*A két Specifikációk könyvtár ugyanaz, élünk a többszörös szülő lehetőségével.

Jogosultság:

  • alapvetően Mindenki csak olvashat, ami öröklődik a lentebbi mappákba
  • CEO és PM írhat minden könyvtárba, ami öröklődik a lentebbi mappákba
  • CEO és PM olvashatja csak az Üzleti ajánlat mappát, azaz meg kell vonni az örökölt Mindenki olvasás jogát
  • Projekt members írhatnak de csak az adott projekten belül

Bár a fenti csak egy minimál, nem túl bonyolult koncepció ettől függetlenül már ezen is látszik, hogy minden projekt esetében újra beállítani jól a dolgokat időrabló, ismétlődő feladat. Így vessük be a Google Apps Scriptet és készüljön egy frontenddel rendelkező webalkalmazás, ahol egy új projekt nevének megadása után minden a kívántak szerint létrejön.

I. Backend funkciók

Tisztázzuk le milyen backend funkciók lesznek, melyeket aszinkron fogunk meghívni frontend oldalról. Hibatűrő (failtolerant) megoldást igyekszünk létrehozni. Így ha már létezik egy ügyfél vagy egy projekt mappa a megadott adatok alapján akkor azt nem hozzuk létre. Emlékeztetőül a Drive megengedi ugyanazon a néven több mappa létrehozását.

Kulcs funkciók megvalósítása

Készítsünk egy könyvtárat ahova a projektek kerülnek. Vegyük ennek az ID-ját, mert így fogjuk azonosítani a kódunkban. Ezt a mappa megnyitásakor a böngészősávból ki tudjuk lesni.

syoc_-_google-drive-folder-template-folder-id.jpg

A kódunkban elsőként vegyük fel a fix értékeket.

var PROJECTS_FOLDER_ID = '...'; // Projekt könyvtárunk azonosítója

Utána következzen az összes meglévő ügyfél mappa kilistázása:

function listExistsClients(){
  var rootFolder = DriveApp.getFolderById(PROJECTS_FOLDER_ID);
  var clientsFoldersIterator = rootFolder.getFolders();
  var result = [];
  while(clientsFoldersIterator.hasNext()){
    var clientFolder = clientsFoldersIterator.next();
    result.push({
      name : removeAccent(clientFolder.getName().toLowerCase()),  // normalized name for sort
      folder : clientFolder
    });
  }

  result.sort(function(a, b){
    return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
  });
  
  return result;
}

Ha jól megnézzük, akkor látható, hogy kapunk egy iterátort amivel végig mehetünk egy adott könyvtár alkönyvtárain. Ezek alapján gyűjtjük össze az információkat, kiegészítve a név egy olyan változatával ahol eltávolítjuk az ékezeteket. A kód utolsó lépéseként látható rendezés miatt érdemes megtenni. 

Az alábbi kód részlet az ékezetek eltávolításának egy Javascript megvalósítása:

var ACCENTS = {
  a: 'àáâãäåæÀÁÂÃÄÅÆ',
  c: 'çÇ',
  e: 'ÈÉÊËÆ',
  i: 'ìíîïÌÍÎÏ',
  n: 'ñÑ',
  o: 'òóôõöøÒÓÔÕÖØ',
  s: 'ß',
  u: 'ùúûüÙÚÛÜ',
  y: 'ÿŸ'
};  

function removeAccent(input) {
  for(var c in ACCENTS)
    if(ACCENTS.hasOwnProperty(c))
      input = input.replace(new RegExp('[' + ACCENTS[c] + ']', 'g'), c.toString());
  return input;
}

Következzen az a funkció amivel megtaláljuk a létrehozni kívánt könyvtárat ha létezik, egyébként létrehozzuk. Így a Google Drive alatt is figyelünk, hogy ugyanolyan néven csak egy mappánk legyen. Ez várhatóan több alkalommal is jól fog jönni.

function findOrCreateFolder(rootFolder, clientName){
  // Check exists
  var foldersIterator = rootFolder.getFoldersByName(clientName);
  if(foldersIterator.hasNext())
    return { state: 'exists', folder: foldersIterator.next() };  
  // Or create new one
  return { state: 'new', folder: rootFolder.createFolder(clientName) };
}

Mostanra az alapvető műveletekkel már rendelkezünk, tudunk listázni és létrehozni. Így nincs más hátra, mint a lényegre fókuszálni, azaz jöjjön létre a kívánt struktúra. Mivel nem kizárt, hogy később ez a struktúra változhat, így legyen egy leírása JSON formátumban.

[
	{
		"name" : "Üzleti ajánlat",
		"description" : "Szerződések, ajánlatok, megrendelők, teljesítés igazolások",
		"share" : {
			"[email protected]" : "organize",
			"[email protected]" : "organize"
		}
	},
	{
		"@id" : "ugyfel-dokumentaciok",
		"name" : "Ügyfél dokumentációk",
		"description" : "Ügyféltől érkező dokumentumok, jegyzőkönyvek, átadandók, stb...",
		"folders" : [
			{
				"name" : "Jegyzőkönyvek",
				"description" : "Memók, meeting jegyzetek"
			}
		]
	},
	{
		"name" : "Fejlesztői dokumentációk",
		"description" : "Fejlesztéshez felhasznált erőforrások",
		"folders" : [
			{
				"name" : "Specifikációk",
				"description" : "Igény és/vagy követelmény specifikációk",
				"@parents" : [
					"ugyfel-dokumentaciok"
				]
			},
			{
				"name" : "Dokumentációk"
			},
			{
				"name" : "Arculat",
				"description" : "Design, arculati elemek, kézikönyv, wireframe, fontok"
			}
		]
	}
]

Egy kis magyarázat a felépítésre vonatkozólag. Melyik mező mit jelent és milyen célt szolgál:

  • name: a könyvtár neve
  • description: a könyvtár leírása
  • folders: gyermek könyvtárak, ahol hasonlóan használható a mappa leírása
  • share: felülírja az örökölt jogosultságot, és konkrétan a felsorolt személyek láthatják
  • @id: JSON fájlban legyen azonosítható
  • @parents: ahol az adott könyvtár még gyermekkönyvtárként megjelenhet, így tudunk több szülőt leírni. @id elemeket lehet felsorolni.

Jöjjön a kód, ami veszi az aktuális struktúrát és az alapján létrehozza a mappákat.

var __IDS = {};
var __PARENTS = {};

function createStructureWithMultipleParents(rootFolder, structure){
  __IDS = {};
  __PARENTS = {};
  Logger.log("Create structure by JSON in: " + rootFolder.getName());
  createStructure(rootFolder, structure);
  Logger.log("Structure successful created.");
  Logger.log("Set multiple parent");
  // Set multiple parent
  for (var key in __PARENTS){
    if (__PARENTS.hasOwnProperty(key)) {
      var arr = __PARENTS[key];
      for(var index in arr){
        __IDS[key].addFolder(arr[index]);
      }
    }
  }
  Logger.log("Done");
  
}

function createStructure(rootFolder, structure){
  if(!rootFolder || !structure) return;
  
  if(isArray(structure)){
    structure.forEach(function(config){
      if(config && config.name){
        // Create new
        var newFolder = rootFolder.createFolder(config.name);
        // Add description when neccessary
        if(config.description)
          newFolder.setDescription(config.description);
        // Collect id for use later
        if(config["@id"]){
          __IDS[config["@id"]] = newFolder;
        }
        // Collect parents for use later
        if(config["@parents"] && isArray(config["@parents"])){
          config["@parents"].forEach(function(parentId){
            if(!__PARENTS[parentId])
              __PARENTS[parentId] = [];
            __PARENTS[parentId].push(newFolder);            
          });
        }
        // Override inherited share settings
        if(config.share){
          // newFolder.setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.NONE);
        }
        // Create sub folders
        if(config.folders){
          createStructure(newFolder, config.folders);
        }
      }
    });
  }
  
}

A fenti kód átnézésére bizonyára több időre volt szükség. Amit érdemes tudni, hogy akármilyen mélyen is van az adott könyvtár, a létrehozás módja nem változik, így jelen esetben rekurziót használunk. A másik, hogy a többszörös szülő hivatkozást csak azután lehet beállítani, miután mindegyik szülő mappa létrehozásával végeztünk. Ezért is válik ketté createStructure és createStructureWithMultipleParents metódusokra.

Jogosultság kérdésköre

Futtassuk a scriptünk egy egy funkcióját és nézzük meg mi történik. Látjuk, hogy feljön egy jogosultság kérő ablak. A kód futtatása előtt a háttérrendszer összeszedi a különböző API hívások alapján, hogy milyen erőforrásokat használnánk és abból melyek azok, amelyek engedélykötelesek. Csak úgy szabadon egy script nem kezdhet el egy adott felhasználó nevében garázdálkodni. Ez csak akkor jön elő ha a kódunkban olyan változások voltak, amelyek még el nem fogadott engedély kéréssel járnak.

syoc_-_google-drive-folder-template-permission.jpg

syoc_-_google-drive-folder-template-permission-2.jpg

Ezeket természetesen el kell fogadni a sikeres futtatáshoz.

Frontend kiszolgáláshoz szükséges végpontok

Előző cikkben utaltam már rá, hogy Apps Script segítségével ki tudunk szolgálni HTML oldalt is. Ehhez annyit érdemes tudni, hogy az alábbi függvények valamelyikének implementálása szükséges.

  • doGet()
  • doPost()

A kettő közül a doGet() implementálása fog megtörténni a frontend igények megvalósítása miatt. Így böngészőből is szépen be lehet tölteni.

function doGet(e) {
    return HtmlService
       .createTemplateFromFile('index.html')
       .evaluate()
       .setTitle("Project Creator");
}

Egészítsük ki még egy metódussal. Méghozzá azzal ami az egész létrehozást elindítja:

function doCreateNewProject(clientName, projectName){
  var rootFolder = DriveApp.getFolderById(PROJECTS_FOLDER_ID);
  if(!rootFolder) return err('Root folder not found!');
  var clientFolderResult = findOrCreateFolder(rootFolder, clientName);
  if(!clientFolderResult.folder) return err('Client folder create failed: ' + clientName);
  var projectFolderResult = findOrCreateFolder(clientFolderResult.folder, projectName);
  if(!projectFolderResult.folder) return err('Project folder create failed: ' + projectName);
  
  // Create structure
  var structureJSONFile = DriveApp.getFileById(PROJECTS_STRUCTURES_JSON_FILE_ID);
  var structureJSON = structureJSONFile.getBlob().getDataAsString();
  var structure = JSON.parse(structureJSON);
  createStructureWithMultipleParents(projectFolderResult.folder, structure);

  return ok({ 
    state: projectFolderResult.state, 
    url: projectFolderResult.folder.getUrl() 
  });
  
}

Két dologgal térünk vissza. Az egyik, hogy milyen state-el jutottunk a mappához, azaz újonnan lett létrehozva vagy már létezett. A másik a url, így létrehozás után át is irányíthatjuk oda a frontend oldalon.

II. Frontend

Miután HTML válasza lehet egy Apps Scriptnek, így nincs akadálya hogy a HTML-be CSS/CSS3 definíciót vagy kliens oldali Javascript kódot tegyünk. Ha pedig ez lehetséges, akkor frontend megjelenéshez lehet használni kliens oldali frameworkoket, mint például Bootstrap vagy Material Design valamelyik implementációja.

Anno az utóbbi mellett döntöttem, mivel jól illeszkedik a többi Google termékek közé. Elvégre a Material Design is tőlük jött. Több megvalósítás is létezik, de végül a Materialize CSS mellett tettem le a voksom. A döntésem alapja az volt, hogy ad egy egész jól használható Select komponenst, míg a getmdl.io nem tartalmaz ilyet.

Nem terveztem, hogy a frontend HTML kódját magyarázzam. Azt viszont szeretném megmutatni, hogyan tudod betenni a scriptbe.

Adjunk hozzá a *.gs fájlok után 3 darab HTML fájlt, rendre az alábbi néven:

  • index.html
  • javascript.html
  • stylesheet.html

Azt láttuk a doGet() esetében, hogyan szolgálja ki az index.html fájlt. Ennek tartalmából pár részlet ami releváns arra nézve hogyan kerül be a javascript és a CSS kód.

index.html

...
<head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> <title>Client Project</title> <?!= HtmlService.createHtmlOutputFromFile('stylesheet.html').getContent(); ?> <?!= HtmlService.createHtmlOutputFromFile('javascript.html').getContent(); ?> <? var clients = listExistsClients(); ?> </head>
...

javascript.html - kliens oldali Javascript

<script src="https://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/js/materialize.min.js" type="text/javascript"></script>
<script>

function toast(message, callback){
   if(callback)
      Materialize.toast(message, 3000, '', callback);
   else
      Materialize.toast(message, 3000);
}
...

stylesheet.html

<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en" />
<!-- http://materializecss.com/getting-started.html -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/css/materialize.min.css" />

<style>
.company-logo{
   max-width:144px;
   max-height:48px;
   vertical-align: middle;
}
...

Amire még figyelnünk kell, azok a mobil eszközök. A mai frameworkok már reszponzív elrendezést is támogatnak, sőt van amelyiket a mobile first elv mentén alakították ki. Ugyanakkor ha megnézzük a felületünket egy mobil képernyőjén akkor látjuk, hogy valami nincs rendjén. Ez pedig a container miatt van. Ugyanis a fejlécet ki kell egészíteni a viewporttal.

function doGet(e) {
    return HtmlService
       .createTemplateFromFile('index.html')
       .evaluate()
       .addMetaTag('viewport', 'width=device-width, initial-scale=1')
       .setTitle("Project Creator");
}

III. Ajax

Szeretnénk, hogy oldal újratöltés nélkül cseréljünk adatot, azaz aszinkron módon használva az AJAX technikát. Jogosan merül fel, hogy nincs egyszerű módja ha már szerver oldalt is és kliens oldalon is Javascript kód van.

A jó hír, hogy bizony van. Az Apps Script támogatja így lényegében kód szinten kliens oldali kódból hívhatom a szerver oldalit.

       google.script.run.withSuccessHandler(function(r){
              
              if(r.result == 'OK'){
                 // Redirect after message
                 if(r.content.state == 'exists'){
                      toast('Project folder already exists!', function(){
                         window.top.location.href = r.content.url; // Redirect
                      });
                      return;
                 }
                 // Redirect inmediate
                 window.top.location.href = r.content.url; 
              } else {
                 toast(r.message);
              }
              
              formEnable($form, true);
              
            }).withFailureHandler(function(e){
               toast(e);
               formEnable($form, true);
            }).doCreateNewProject(client, project);

Amint azt jól látni, a kliens oldalról nevén nevezve hívhatjuk meg a szerver oldali metódusunkat.

Webalkalmazás indítása

A vége felé már csak az a kérdés, hogyan kell elindítani az alkalmazásunkat és kipróbálni amit eddig csináltunk.

Publish > Deploy as web app

syoc_-_google-drive-folder-template-deploy.jpg

A felugró ablakon első alkalommal még létre kell hoznunk egy verziót. Ezt követően pedig a latest code linkkel meg is tudjuk nyitni.

Eredmény

syoc_-_google-drive-folder-template-result.jpg

 

syoc_-_google-drive-folder-template-result-mobile.jpg syoc_-_google-drive-folder-template-result-mobile-2.jpg

 

Hogyan tovább?

Innentől csak kreativitás kérdése, hogy mire lehet még jó. Terjeszd ki a projekt létrehozás funkcióit a többi eszközeidre is. Nézd meg, hogy milyen API van hozzájuk és hívd meg őket. Esetleg egészítsd ki még visszajelzés vagy értesítési funkciókkal.

Ha még tart a lelkesedés, akkor egy jóval komplexebb alkalmazás megvalósítását is láthatod a következő fejezetben.

Ha tetszett a cikk oszd meg másokkal is.