#!/usr/bin/python
# EasyScout Backup Restorer

# Copyright (c) 2004 by Ryan Kirkpatrick (scouts@rkirkpat.net). 
#   All rights reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING.  If not, write to
# the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

# Documentation.
"""
This is a restore application to reload the contents of an EasyScout
XML backup file back into the server. This is done via the web interface
of the EasyScout software, as that is the only practical method. Command
line arguments taken, all but the first are required:
  -v                -- If present, turns on verbose mode, which prints status
                       and progress at regular intervals, along with elapsed
                       time.
  -U {base_url}     -- The base URL of the EasyScout website to restore to (i.e.
                       http://www.bsa171.org/easyscout/servlet/ES/).
  -R {ranks_dir}    -- Directory containing the files with XML definitions of
                       the requirements for each rank (found in
                       '.../cx/warp/scout/lookup' of the EasyScout source code).
  -b {backup_file}  -- The name of the XML backup file to restore.

When the restore is complete, it is prudent to get a new backup file from the
restored troop and compare it to the original backup file restored. The 'id'
values will be different, but everything else should be the same. If there are 
differences, then please contact me and let me know what is different. i.e.

# sed 's/ id=.*>/>/' orig.xml > orig.stripped.xml
# sed 's/ id=.*>/>/' new.xml > new.stripped.xml
# diff orig.stripped.xml new.stripped.xml
{only the value of the Created tag should vary}

Functions found in this module:
  DateConvert(date)              -- Converts std dates to EasyScout entry dates.
  RankSetup(dir)                 -- Reads XML rank/requirements files and builds
                                    a dictionary.
  RankConvert(req_version,
              rank_name,
              req_index)         -- Converts rank names and requirement indexes,
                                    for the given requirements version, to
                                    EasyScout entry index.
  DefaultQuery(defin, data)      -- Prepares the data for a POST query in a
                                    default manner.
  Troop(mode, defin, data)       -- Creates a new EasyScout troop.
  Leader(mode, defin, data)      -- Creates a new leader for an EasyScout troop.
  Scout(mode, defin, data)       -- Creates a new scout for an EasyScout troop.
  Rank(mode, defin, data)        -- Sets data for a rank for an EasyScout scout.
  Requirement(mode, defin, data) -- Sets data for a rank requirement for an
                                    EasyScout scout.
  MeritBadge(mode, defin, data)  -- Sets data for a merit badge for an EasyScout
                                    scout.
  Activity(mode, defin, data)    -- Sets data for a activity for an EasyScout
                                    scout.
  Leadership(mode, defin, data)  -- Sets data for a leadership for an EasyScout
                                    scout.
  ReloadRecurse(data)            -- Loads the contents of a parsed EasyScout
                                    backup file into an EasyScout site.
"""
__credits__ = 'Copyright 2004 (c) by Ryan Kirkpatrick. All Rights Reserved'
__author__ = 'Ryan Kirkpatrick <scouts@rkirkpat.net>'
__date__ = '2004/01/05'
__version__ = 'ESPyUtils 1.0'

# Needed modules.
import sys
import os
import re
import xml
import utils

# State data.
state = {"base_url":"","session":"","scoutid":"","leaderid":"",
         "currentrank":"","rank":"","reqversion":""}

# Convert date to EasyScout entry format.
def DateConvert(date):
    """
    Converts a standard MM/DD/YYYY style date to three values, month (short
    name), day (number), and year (number). Parameters:
      date -- A date to be converted.

    Returns a tuple of (Month,Day,Year).
    """

    # Split the date into its elements.
    (month,day,year) = re.split("/",date)

    # Convert the month to a short name.
    months = {1:"Jan",2:"Feb",3:"Mar",4:"Apr",5:"May",6:"Jun",
              7:"Jul",8:"Aug",9:"Sep",10:"Oct",11:"Nov",12:"Dec"}
    month = months[int(month)]

    # Return the converted data.
    return month, day, year

# Setup a data structure of ranks and requirements.
def RanksSetup(dir):
    """
    Reads a directory full of XML rank/requirement files, and build a single
    dictionary from them, with one key in the top level dictionary for each
    version of the requirements. Will only read files with the name of the
    form of '[0-9]{4,4}-[0-9]{2,2} to .+\.xml'. Parameters:
      dir -- Directory with XML files to read.

    Returns a dictionary with the structure:
      { [req version] : { [rank name] : {'seq', [req index] : { 'seq' } } }
    """
    
    # Parse each file in the directory.
    reqs = {}
    files = os.listdir(dir)
    for file in files:
        # Does this look like an XML rank/requirements file? If not, skip it.
        if not re.search("[0-9]{4,4}-[0-9]{2,2}.+\.xml$",file): continue

        # Open the file and parse it.
        utils.Verbose("  Parsing rank/requirements from '%s'...\n" % file,0)
        vreqs = xml.Parse(xml.ReqPreParse(open("%s/%s" % (dir,file))))
        vers = re.sub("_"," ",re.sub("\.xml$","",file))

        # Copy the data into our own structure.
        reqs[vers] = {}; rank_count = 0
        for rank in vreqs["requirements"]["rank"]:
            # Create an entry for this rank, and save its sequence number.
            reqs[vers][rank["name"]] = {"seq":"%d" % rank_count}

            # Now, copy the requirements in for this rank, if there are any.
            if rank.has_key("requirement"):
                req_count = 0
                for req in rank["requirement"]:
                    # Create an entry for this requirement, and save its
                    #   sequence number.
                    reqs[vers][rank["name"]][req["index"]] = {"seq":"%d" % req_count}
                    req_count += 1

            # Prepare for the next rank.
            rank_count += 1

    # Return the structure.
    return reqs

# Convert a rank name or requirement index to an sequence value.
def RankConvert(req_version, rank_name, req_index = None):
    """
    Converts the given rank name or requirement index of a rank for the
    given version of the requirements, to a sequence value usable with the
    EasyScout web entry forms. Parameters:
      req_version -- The requirements version to lookup in.
      rank_name   -- The rank name to look up.
      req_index   -- The requirement index to look up (optional).

    If the req_index is not specified or is None, then the sequence value of
    the given rank_index is returned. Otherwise the sequence value of the
    given req_index is returned.
    """

    # What are we looking up?
    if req_index != None:
        # Requirement index, do so and return it.
        return globals()["rank_req"][req_version][rank_name][req_index]["seq"]
    else:
        # Rank name, do so and return it.
        return globals()["rank_req"][req_version][rank_name]["seq"]

# Default prepartion of URL query.
def DefaultQuery(defin, data):
    """
    Prepares the data for a POST query in a default manner. Parameters:
      defin -- The form data definition.
      data  -- The data to be used in the query.

    For every key in 'defin', if its value is a key in 'data', than the associated
    value from 'data' is stored at the 'defin' key in the returned dictionary.
    Otherwise the value for the 'defin' key is stored at the 'defin' key in the
    returned dictionary.
    """
        
    # Look at each defined query field in turn.
    query = {}
    for key in defin.keys():
        if defin[key] in data.keys():
            # Query field has a corresponding data field, use its value.
            query[key] = data[defin[key]]
        else:
            # Query field does not, assume value is static.
            query[key] = defin[key]

    # Return the query dictionary.
    return query

# Reload troop data.
def Troop(mode, defin, data):
    """
    Creates a new troop on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements (scouts, leaders) have been created.
      defin -- The definition of data needed to create the troop.
      data  -- The data to create the troop with.

    Stores the session id for the new troop in the 'state' global
    variable. Returns nothing, raises an exception if something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return
    utils.Verbose("  Creating troop '%s' in EasyScout..." % data["name"])
    
    # Prepare the query using defaults, and get a reference to the state.
    query = DefaultQuery(defin, data)
    state = globals()["state"]
    
    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # Extract the session key.
    session = re.search("\?z=[^&]+&",result)
    if not session: raise StandardError("Could not find session id in query result! %s" % result)
    state["session"] = result[session.start()+3:session.end()-1]

    # If a readonly password is available, set it too.
    if data.has_key("readonly"):
        # Yes we do, setup the query object.
        query["action"] = "setpass"
        del query["name"]
        query["readonly"] = data["readonly"]
        
        # Update the passwords.
        (okay, result) = utils.GetURL(state["base_url"],state,query)
        if not okay: raise StandardError(result)

    # We are done.
    utils.Verbose()

# Reload leader data.
def Leader(mode, defin, data):
    """
    Creates a new leader on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements have been created.
      defin -- The definition of data needed to create the leader.
      data  -- The data to create the leader with.

    Stores the id for the new leader in the 'state' global variable.
    Returns nothing, raises an exception if something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return
    utils.Verbose("    Creating leader '%s %s'..." % (data["firstname"],data["lastname"]))

    # Prepare the query using defualts, get a reference to the state, and then
    #   handle birthday ourselves.
    query = DefaultQuery(defin,data)
    state = globals()["state"]
    (query["Month"],query["Day"],query["Year"]) = DateConvert(data["birthdate"])
    
    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # Extract the leaderid key.
    fullname = "%s[^<]+%s" % (data["firstname"],data["lastname"])
    leaderid = re.search("<a href=[^>]+leaderid=[^&]+&[^>]+>[^<]+<font[^>]+>%s" % fullname,result)
    if not leaderid: raise StandardError("Could not find leaderid in query result! %s" % result)
    leader = re.search("leaderid=[^&]+&",result[leaderid.start():leaderid.end()])
    state["leaderid"] = result[leaderid.start():leaderid.end()][leader.start()+8:leader.end()-1]

    # We are done.
    utils.Verbose()

# Reload scout data.
def Scout(mode, defin, data):
    """
    Creates a new scout on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements (merit badges, ranks) have been created.
      defin -- The definition of data needed to create the scout.
      data  -- The data to create the scout with.

    Stores the id for the new scout, the scout's current rank, and the scout's
    rank/requirements version, in the 'state' global variable.  Returns nothing,
    raises an exception if something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return
    utils.Verbose("    Creating scout '%s %s'..." % (data["firstname"],data["lastname"]))

    # Prepare the query using defaults, get a reference to the state, and then
    #   handle patrol and birthday ourselves.
    query = DefaultQuery(defin,data)
    state = globals()["state"]
    if query["patrol"] == "patrol": query["patrol"] = ""
    (query["Month"],query["Day"],query["Year"]) = DateConvert(data["birthdate"])
    
    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # Extract the scoutid key.
    fullname = "%s[^<]+%s" % (data["firstname"],data["lastname"])
    scoutid = re.search("<a href=[^>]+scoutid=[^&]+&[^>]+>[^<]+<font[^>]+>%s" % fullname,result)
    if not scoutid: raise StandardError("Could not find scoutid in query result! %s" % result)
    scout = re.search("scoutid=[^&]+&",result[scoutid.start():scoutid.end()])
    state["scoutid"] = result[scoutid.start():scoutid.end()][scout.start()+8:scout.end()-1]

    # Save the current rank and version for later use.
    state["currentrank"] = data["currentrank"]
    state["reqversion"] = data["requirementsversion"]

    # We are done.
    utils.Verbose()

# Reload rank data.
def Rank(mode,defin, data):
    """
    Creates a new rank for a scout on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements (requirements) have been created.
      defin -- The definition of the data needed to create the rank.
      data  -- The data to create the rank with.

    Creates the rank (setting award date and current rank as needed) for the
    current scout id found in the 'state' global variable. Sets the index of the
    rank being created in the 'state' global variable. Returns nothing, raises
    an exception if something fails.
    """

    # Get a reference to the state.
    state = globals()["state"]
    
    # Before the requirements have been created, just store the index for the rank
    #   we will be creating requirements for.
    if mode == "pre":
        utils.Verbose("      With rank '%s'...\n" % data["name"],0)
        state["rank"] = data["name"]
        return

    # Otherwise, first create the rank by setting the awarded by date, if provided.
    if data["awarded"]:
        utils.Verbose("        Awarded on '%s'..." % data["awarded"])
        
        # Prepare the query using defaults, and then handle the date and rank ourselves.
        query = DefaultQuery(defin,data)
        (query["Month"],query["Day"],query["Year"]) = DateConvert(data["awarded"])
        query["rank"] = RankConvert(state["reqversion"],data["name"])

        # Submit the query.
        (okay, result) = utils.GetURL(state["base_url"],state,query)
        if not okay: raise StandardError(result)
        utils.Verbose()

    # Lastly, promote the scout to this rank if it is his current one.
    if state["currentrank"] == data["name"]:
        utils.Verbose("        Promoted to this rank...")

        # Setup the query values:
        query = {"action":"scoutdetail",
                 "subaction":"promoteconfirm",
                 "rank":RankConvert(state["reqversion"],data["name"]),
                 "screen":"2",
                 "answer":"Yes"}

        # Submit the query.
        (okay, result) = utils.GetURL(state["base_url"],state,query)
        if not okay: raise StandardError(result)
        utils.Verbose()

# Reload requirement data.
def Requirement(mode, defin, data):
    """
    Creates a new rank requirement for a scout on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements have been created.
      defin -- The definition of the data needed to create the rank requirement.
      data  -- The data to create the rank requirement with.

    Creates the rank requirement for the current scout id and rank index found
    in the 'state' global variable. Returns nothing, raises an exception if
    something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return

    # Only do this if the requirement has been passed off.
    if not data["passed"]: return
    utils.Verbose("        With rank requirement '%s' on %s..." % (data["index"],data["passed"]))

    # Prepare the query using defaults, get a reference to the state, and then
    #   handle the date, rank index, and requirement value ourselves.
    query = DefaultQuery(defin,data)
    state = globals()["state"]
    (query["Month"],query["Day"],query["Year"]) = DateConvert(data["passed"])
    query["rank"] = RankConvert(state["reqversion"],state["rank"])
    query["requirement"] = RankConvert(state["reqversion"],
                                       state["rank"],
                                       data["index"])
    
    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # We are done.
    utils.Verbose()

# Reload meritbadge data.
def MeritBadge(mode, defin, data):
    """
    Creates a new merit badge for a scout on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements have been created.
      defin -- The definition of the data needed to create the merit badge.
      data  -- The data to create the merit badge with.

    Creates the merit badge for the current scout id found in the 'state' global
    variable. Returns nothing, raises an exception if something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return
    utils.Verbose("      With merit badge '%s' on %s..." % (data["name"],data["earned"]))

    # Prepare the query using defaults, get a reference to the state, and then
    #   handle the date ourselves.
    query = DefaultQuery(defin,data)
    state = globals()["state"]
    (query["Month"],query["Day"],query["Year"]) = DateConvert(data["earned"])
    
    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # We are done.
    utils.Verbose()

# Reload activity data.
def Activity(mode, defin, data):
    """
    Creates a new activity for a scout on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements have been created.
      defin -- The definition of the data needed to create the activity.
      data  -- The data to create the activity with.

    Creates the activity for the current scout id found in the 'state' global
    variable. Returns nothing, raises an exception if something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return
    utils.Verbose("      With activity '%s' on %s..." % (data["name"],data["date"]))

    # Prepare the query using defaults, get a reference to the state, and then
    #   handle the date ourselves.
    query = DefaultQuery(defin,data)
    state = globals()["state"]
    (query["Month"],query["Day"],query["Year"]) = DateConvert(data["date"])
    
    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # We are done.
    utils.Verbose()

# Reload leadership data.
def Leadership(mode, defin, data):
    """
    Creates a new leadership for a scout on the EasyScout website. Parameters:
      mode  -- Perform actions for before ('pre') or after ('post')
                 sub elements have been created.
      defin -- The definition of the data needed to create the leadership.
      data  -- The data to create the leadership with.

    Creates the leadership for the current scout id found in the 'state' global
    variable. Returns nothing, raises an exception if something fails.
    """

    # There is nothing to do afterwards, only before.
    if mode == "post": return
    utils.Verbose("      With leadership '%s' from %s to %s..." % (data["name"],data["start"],data["end"]))

    # Prepare the query using defaults, get a reference to the state, and then
    #   handle the dates ourselves.
    query = DefaultQuery(defin,data)
    state = globals()["state"]
    (query["1Month"],query["1Day"],query["1Year"]) = DateConvert(data["start"])
    if data["end"]:
        # There is an end date, so indicate that and set the date.
        (query["2Month"],query["2Day"],query["2Year"]) = DateConvert(data["end"])
        query["endenabled"] = "on"

    # Submit the query.
    (okay, result) = utils.GetURL(state["base_url"],state,query)
    if not okay: raise StandardError(result)

    # We are done.
    utils.Verbose()

# Form definitions.
forms = {"troop":[Troop,{"action":"Signup",
                         "name":"name",
                         "password":"password",
                         "password2":"password",
                         "owner":"owner",
                         "email":"email"}
                  ],
         "leader":[Leader,{"action":"createleader",
                           "leaderid":"new",
                           "fname":"firstname",
                           "mname":"middlename",
                           "lname":"lastname",
                           "position":"position",
                           "Month":"",
                           "Day":"",
                           "Year":"",
                           "address":"address",
                           "phone":"phone",
                           "email":"email",
                           "notes":"notes"}
                   ],
         "scout":[Scout,{"action":"createscout",
                         "scoutid":"new",
                         "fname":"firstname",
                         "mname":"middlename",
                         "lname":"lastname",
                         "phone":"phone",
                         "email":"email",
                         "patrol":"patrol",
                         "registration":"registration",
                         "password":"password",
                         "Month":"",
                         "Day":"",
                         "Year":"",
                         "version":"requirementsversion",
                         "address":"address",
                         "contact":"contact",
                         "medical":"medicalnotes",
                         "emergency":"emergency",
                         "notes":"notes"}
                  ],
         "rank":[Rank,{"action":"scoutdetail",
                       "subaction":"award",
                       "screen":"2",
                       "rank":"",
                       "Month":"",
                       "Day":"",
                       "Year":""}
                 ],
         "requirement":[Requirement,{"action":"scoutdetail",
                                     "subaction":"pass",
                                     "screen":"2",
                                     "rank":"",
                                     "pass":"1",
                                     "requirement":"",
                                     "Month":"",
                                     "Day":"",
                                     "Year":""}
                        ],
         "meritbadge":[MeritBadge,{"action":"scoutdetail",
                                   "subaction":"create",
                                   "screen":"3",
                                   "badgeid":"new",
                                   "name":"name",
                                   "Month":"",
                                   "Day":"",
                                   "Year":"",
                                   "counselor":"counselor"}
                       ],
         "activity":[Activity,{"action":"scoutdetail",
                               "subaction":"create",
                               "screen":"4",
                               "activityid":"new",
                               "name":"name",
                               "Month":"",
                               "Day":"",
                               "Year":"",
                               "description":"description"}
                     ],
         "leadership":[Leadership,{"action":"scoutdetail",
                                   "subaction":"create",
                                   "screen":"5",
                                   "positionid":"new",
                                   "position":"name",
                                   "1Month":"",
                                   "1Day":"",
                                   "1Year":"",
                                   "2Month":"",
                                   "2Day":"",
                                   "2Year":"",
                                   "endenabled":"off"}
                       ]
         }

# Recurse through data structure, reloading data.
def ReloadRecurse(data):
    """
    Recurse through the EasyScout backup file data structure looking
    for objects (keys) we can reload into the website. Parameters:
      data -- A dictionary data structure of the XML EasyScout backup
              file.

    Returns nothing.
    """
    
    # Handle each key we know about at this level.
    for key in data.keys():
        if key in forms.keys():
            # We know how to reload this element, do it, though handle the issue
            #   of this element having an array value.
            sets = data[key]
            if type(sets) != type([]): sets = [ sets ]
            for set in sets:
                # Do any handling of the element before we descend.
                forms[key][0]("pre",forms[key][1],set)
                    
                # Then recurse into it, to see if there is anything within
                #   we can handle.
                ReloadRecurse(set)

                # Do any handling of the element after we have descended.
                forms[key][0]("post",forms[key][1],set)

# Get program name, and set defaults.
name = sys.argv.pop(0)
base_url = ""
backup_file = ""

# What the command should look like.
def Usage():
    print "Usage: reload.py [-v] -U {base_url} -R {ranks_dir} -b {backup_file}"
    sys.exit(1)

# Were there options? Handle them.
for argv in sys.argv:
    # Verbosity? Set if so.
    if argv == "-v": utils.verbose = 1

    # Base URL? Indicate next parameter is it.
    elif argv == "-U": base_url = "next"
    elif base_url == "next": base_url = argv

    # Ranks dir? Indicate next parameter is it.
    elif argv == "-R": ranks_dir = "next"
    elif ranks_dir == "next": ranks_dir = argv

    # Backup file? Indicate next parameter is it.
    elif argv == "-b": backup_file = "next"
    elif backup_file == "next": backup_file = argv

# Test the command line parameters.
(okay,result) = utils.GetURL(base_url,state)
if not okay: print result; Usage()
try: backup = open(backup_file,"r")
except:
    print "Could not open backup file, %s!" % backup_file
    Usage()
try: rank_files = os.listdir(ranks_dir)
except:
    print "Could not open rank directory, %s!" % ranks_dir
    Usage()

# We are ready to start reloading data.
utils.Verbose("%s Backup Script [%s]\n" % (__version__,__date__))
utils.Verbose("Reloading EasyScout data from XML backup file '%s'...\n" % backup_file,0)

# Parse the rank/requirements information.
rank_req = RanksSetup(ranks_dir)

# Parse the backup file into a dictionary.
utils.Verbose("  Parsing EasyScout data from XML backup...\n",0)
data = xml.Parse(backup)

# Now, reload the data into the EasyScout server.
state["base_url"] = base_url
ReloadRecurse(data)

# We are done, clean up.
utils.Verbose("Done!\n",0)
backup.close()
