Monday, October 6, 2014

Creating digital portfolios with google sites

Our school has used google sites for some time for students to use portfolios. We have a template portfolio that we use, and then we have traditionally had students create their own portfolios based on that template. The problem is that the "creating the site" step can take a while, and, in spite of clear directions, students get mixed up and don't follow our naming system, and then for years down the road whenever a teacher wants to see a portfolio, it's hard to find.

So, this year, I automated the process of creating student portfolios. This blog post serves as notes for me and anyone else who wants to automate the process of:

(A) Creating new google sites based on a template
(B) Re-assigning ownership to someone else
(C) Setting the permissions so everyone in the domain can view the site.

Though google documents this stuff, it's not as simple or transparent as it should be, and it's a bit error prone. There's also precious little help to be googled -- hopefully this post will help improve that situation.

Google, if you're listening, I would love to see an updated google sites API soon. Among other things, you really need to start supporting your page templates better through your API -- right now there's no working way to programmatically touch content on pages that use page templates (which, in the case of our school, is *all* pages)

Let me walk you through the different scripting steps...
  1. Log into google...

    import atom.data
    import gdata.sites.client
    import gdata.sites.data
    import urlparse
    
    client = gdata.sites.client.SitesClient(source='yourCo-yourAppName-v1', domain='domain.org')
    pw = 'SECRET'
    master_user = 'USERNAME@DOMAIN.org'
    client.ClientLogin(master_user,pw,client.source)

    This gets you the client object, which is what you'll use to do nearly everything.
  2. Create a new site based on a template.

    client.CreateSite is the command to create a site. The tricky part here is finding a template to use. Google's tutorial tells you to use the parameter source_site, but only template source_sites are usable. To find the right value, it's simplest to (1) create a site using the template you want (2) find the site object for that site (3) grab the source_site attribute off of that one. In my case, the basic code to create a site looks like this:
    entry = client.CreateSite(name+' '+str(yog), source_site='https://sites.google.com/feeds/site/innovationcharter.org/sampleportfolio')
  3. Fix permissions: My last step is changing the permissions so the site is owned by the student and not me. This code has to run after the site has successfully been created; when running code creating hundreds of sites, there can be an issue with this not working properly. I ended up writing the code so that first I created all the sites, then I fixed all the permissions, which worked more reliably than creating a site then fixing permissions. I believe there may be a switch I could hand to CreateSite to fix the issue I was having with editing permissions immediately after creating the site, but I've lost track of that documentation at this point.

    At any rate, here's the way to fix the permissions up.
    First, add the user to the entry:

    def add_user_to_entry (user, entry):
       print 'Adding user to entry...',user,entry
       role = gdata.acl.data.AclRole(value='owner')
       scope = gdata.acl.data.AclScope(value=user,type='user')
       user_acl = gdata.sites.data.AclEntry(scope=scope,role=role)
       client.Post(user_acl,entry.FindAclLink())


    Second, delete ourselves from the entry (this assumes our master user is stored in the variable master_user, as above)

    def delete_master_entry (entry):
        acl = client.GetAclFeed(entry.FindAclLink())
        for e in acl.entry:
            if master_user in e.to_string():
                client.Delete(e)

    Finally, in our school, we have portfolios readable by anyone within our domain. To enforce this, we run this code:

    def make_domain_readable (entry):
        print 'Make domain-readable'
        role = gdata.acl.data.AclRole(value='reader')
        scope = gdata.acl.data.AclScope(value='innovationcharter.org',type='domain')
        domain_acl = gdata.sites.data.AclEntry(scope=scope,role=role)
        client.Post(domain_acl,entry.FindAclLink())
    
To put it all together, here is the complete script I used to read in a list of usernames from a file and create digital portfolios accordingly. The file format is simply a CSV file with Email addresses on it. I inferred the first/last name based on the address -- it would be trivial to alter the code to put whatever data you wanted in a spreadsheet in.


import re
import csv
import atom.data
import gdata.sites.client
import gdata.sites.data
import urlparse
import traceback
import csv
import time

client = gdata.sites.client.SitesClient(source='yourCo-yourAppName-v1', domain='mydomain.org')
pw = 'topsecretpassword'
master_user = 'admin@mydomain.org'
client.ClientLogin(master_user,pw,client.source)
# Convenience Functions

def delete_master_entry (entry):
    '''Remove master user from site - get rid of our ownership'''
    acl = client.GetAclFeed(entry.FindAclLink())
    for e in acl.entry:
        if master_user in e.to_string():
            client.Delete(e)
    
def add_user_to_entry (user, entry):
    '''Add user as owner of site entry'''
    role = gdata.acl.data.AclRole(value='owner')
    scope = gdata.acl.data.AclScope(value=user,type='user')
    user_acl = gdata.sites.data.AclEntry(scope=scope,role=role)
    client.Post(user_acl,entry.FindAclLink())

def make_domain_readable (entry):
    '''Make site readable by everyone in domain'''
    role = gdata.acl.data.AclRole(value='reader')
    scope = gdata.acl.data.AclScope(value='mydomain.org',type='domain')
    domain_acl = gdata.sites.data.AclEntry(scope=scope,role=role)
    client.Post(domain_acl,entry.FindAclLink())

def create_dp (email, name, yog):
    '''Create a digital portfolio based with a title based on name
     and year of graduation. 
    '''
    print 'Create DP',email,name,yog
    try:
        entry = client.CreateSite(name+' '+str(yog),
            source_site='https://sites.google.com/feeds/site/mydomain.org/sampleportfolio')
    except:
        # Print exceptions but don't raise them - this allows us to run
        # the script through even if there's a few bad data entries that
        # need correcting later.
        traceback.print_exc()
        entry = None
    if entry:
        # We postpone fixing permissions since there seems to be a bug in the API
        # that means updating permissions right away will often silently fail.
        def pull_trigger_later ():
            print 'Fixing user info for ',email,name,yog
            add_user_to_entry(email,entry)
            delete_master_entry(entry)        
            make_domain_readable(entry)
    else:
        def pull_trigger_later():
            print 'No entry was created for ',email,name,yog,'so no further action'
    # We return a function to fix permissions and the entry -- that way whoever
    # calls us can decide when to fix the permissions.
    return entry,pull_trigger_later

def print_permissions (entry):
    afeed = client.GetAclFeed(entry.FindAclLink())
    for e in afeed.entry:
        print e.to_string()
# LOOP TO READ IN DATA FROM CSV FILE, CREATE SITES, AND THEN, LATER, 
# FIX PERMISSIONS.

entries = [] # To store sites we've created
triggers = [] # Functions to fix permissions (we'll do this later)

reader = csv.reader(file('digital_portfolio_data.csv','r'))
reader.next() # Remove first line - header
for line in reader:
    # You'd update this for whatever data source you can conveniently create
    # for your own domain. This is based on our own usernames which are
    # first.last@innovationcharter.org, and it's based on the fact that I'm
    # creating a group of sites for the class of 2018 only (if I weren't, I'd
    # have added a field to the csv with the YOG).
    email = line[0]
    first = email.split('.')[0].capitalize()
    last = email.split('.')[1].split('@')[0].capitalize()
    name = ' '.join([first,last])
    e,f = create_dp(email,name,'2018')
    entries.append(e)
    triggers.append(f) # Save the permissions-fixing for later

# We're done creating sites, now let's fix permissions...
print len(triggers),'triggers to run.'
print 'Sleeping while google finishes its business' 
time.sleep(30) # Ugly ugly ugly
print "I'm alive again!"
for n,t in enumerate(triggers):
    try:
        print 'Running trigger #',n
        t()
    except:
        # Again, report errors but don't stop loop for them.
        traceback.print_exc()

No comments: