# Copyright (C) 2009, Tutorius.org # # 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; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import urllib import urllib2 from xml.dom import minidom from apilib.restful_lib import Connection from array import array class StoreProxy(object): """ Implements a communication channel with the Tutorius Store, where tutorials are shared from around the world. This proxy is meant to offer a one-stop shop to implement all the requests that could be made to the Store. """ def __init__(self, base_url): # Base Urls for the api self.base_url = base_url self.remora_api = "api/1.4" self.tutorius_api = "TutoriusApi" self.bandwagon_api = "api/1.4/sharing" self.api_auth_key = None # Prepares the connection with the api self.conn = Connection(self.base_url) # Setup the helper self.helper = StoreProxyHelper() def get_categories(self): """ Returns all the categories registered in the store. Categories are used to classify tutorials according to a theme. (e.g. Mathematics, History, etc...) @return The list of category names stored on the server. """ request_url = "/%s/categories" % (self.tutorius_api) response = self.conn.request_get(request_url) if self.helper.iserror(response): return None xml_response = minidom.parseString(response['body']) xml_categories = xml_response.getElementsByTagName('category') categories = list() # Loop through the categories and create the list to be returned for xml_category in xml_categories: category = {} category['id'] = xml_category.getElementsByTagName('id')[0].firstChild.nodeValue category['name'] = xml_category.getElementsByTagName('name')[0].firstChild.nodeValue categories.append(category) return categories def search(self, keywords, category='all', page=1, numResults=10, sortBy='name'): """ Returns a list of tutorials that correspond to the given search criteria. @param keywords The keywords to search for @param page The page in the result set from which to return results. This is used to allow applications to fetch results one set at a time. @param numResults The max number of results that can be returned in a page @param sortBy The field on which to sort the results @return A list of tutorial meta-data that corresponds to the query """ request_url = "/%s/search/%s/%s/%d/%d/%s" % (self.tutorius_api, keywords, category, page, numResults, sortBy) response = self.conn.request_get(request_url) if (self.helper.iserror(response)): return None xml_response = minidom.parseString(response['body']) xml_tutorials = xml_response.getElementsByTagName('tutorial') tutorials = list() for xml_tutorial in xml_tutorials: tutorial = self.helper.parse_tutorial(xml_tutorial) tutorials.append(tutorial) return tutorials def get_tutorials(self, category='all', page=1, numResults=10, sortBy='name'): """ Returns the list of tutorials that correspond to the given search criteria. @param category The category in which to restrict the search. @param page The page in the result set from which to return results. This is used to allow applications to fetch results one set at a time. @param numResults The max number of results that can be returned in a page @param sortBy The field on which to sort the results @return A list of tutorial meta-data that corresponds to the query """ request_url = "/%s/tutorials/%s/%d/%d/%s" % (self.tutorius_api, category, page, numResults, sortBy) response = self.conn.request_get(request_url) if (self.helper.iserror(response)): return None xml_response = minidom.parseString(response['body']) xml_tutorials = xml_response.getElementsByTagName('tutorial') tutorials = list() for xml_tutorial in xml_tutorials: tutorial = self.helper.parse_tutorial(xml_tutorial) tutorials.append(tutorial) return tutorials def list(self, type='recommended', numResults=3): """ Returns a list of tutorials corresponding to the type specified. Type examples: 'Most downloaded', 'recommended', etc. @param type The type of list (Most downloaded, recommended, etc.) @return A list of tutorials """ request_url = "/%s/list/%s/tutorial/%s" % (self.remora_api, type, numResults) response = self.conn.request_get(request_url) if (self.helper.iserror(response)): return None xml_response = minidom.parseString(response['body']) xml_tutorials = xml_response.getElementsByTagName('addon') tutorials = list() for xml_tutorial in xml_tutorials: tutorial = self.helper.parse_tutorial(xml_tutorial) tutorials.append(tutorial) return tutorials def get_latest_version(self, tutorial_id_list): """ Returns the latest version number on the server, for each tutorial ID in the list. @param tutorial_id_list The list of tutorial IDs from which we want to known the latest version number. @return A dictionary having the tutorial ID as the key and the version as the value. """ versions = {} for tutorial_id in tutorial_id_list: request_url = "/%s/addon/%s/" % (self.remora_api, tutorial_id) response = self.conn.request_get(request_url) if (self.helper.iserror(response)): return None xml = minidom.parseString(response['body']) versionnode = xml.getElementsByTagName("version")[0] version = versionnode.firstChild.nodeValue versions[tutorial_id] = version return versions def download_tutorial(self, tutorial_id, version=None): """ Fetches the tutorial file from the server and returns the @param tutorial_id The tutorial that we want to get @param version The version number that we want to download. If None, the latest version will be downloaded. @return The downloaded file itself (an in-memory representation of the file, not a path to it on the disk) TODO : We should decide if we're saving to disk or in mem. """ request_url = "/%s/addon/%s/" % (self.remora_api, tutorial_id) response = self.conn.request_get(request_url) if (self.helper.iserror(response)): return None xml = minidom.parseString(response['body']) installnode = xml.getElementsByTagName("install")[0] installurl = installnode.firstChild.nodeValue fp = urllib.urlopen(installurl) return fp def login(self, username, password): """ Logs in the user on the store and saves the login status in the proxy state. After a successful logon, the operation requiring a login will be successful. @param username @param password @return True if the login was successful, False otherwise """ request_url = "/%s/auth/" % (self.tutorius_api) params = {'username': username, 'password': password} response = self.conn.request_post(request_url, params) if (self.helper.iserror(response)): return False xml_response = minidom.parseString(response['body']) keynode = xml_response.getElementsByTagName("token")[0] key = keynode.getAttribute('value') self.api_auth_key = key return True def close_session(self): """ Ends the user's session on the server and changes the state of the proxy to disallow the calls to the store that requires to be logged in. @return True if the user was disconnected, False otherwise """ request_url = "/%s/auth/%s" % (self.tutorius_api, self.api_auth_key) headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_delete(request_url, None, headers) if (self.helper.iserror(response)): return False self.api_auth_key = None return True def get_session_id(self): """ Gives the current session ID cached in the Store Proxy, or returns None is the user is not logged yet. @return The current session's ID, or None if the user is not logged """ return self.api_auth_key def rate(self, value, tutorial_store_id): """ Sends a rating for the given tutorial. This function requires the user to be logged in. @param value The value of the rating. It must be an integer with a value from 1 to 5. @param tutorial_store_id The ID of the tutorial that was rated @return True if the rating was sent to the Store, False otherwise. """ request_url = "/%s/review/%s" % (self.tutorius_api, tutorial_store_id) params = {'title': 'from api', 'body': 'from api', 'rating': value} headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_post(request_url, params, None, None, headers) if self.helper.iserror(response): return False return True def publish(self, tutorial, tutorial_info=None, tutorial_store_id = None): """ Sends a tutorial to the store. This function requires the user to be logged in. @param tutorial The tutorial file to be sent. Note that this is the content itself and not the path to the file. @param tutorial_info An array containing the tutorial information @return True if the tutorial was sent correctly, False otherwise. """ # This is in the case we have to re-publish a tutorial if tutorial_store_id is not None: request_url = "/%s/publish/%s" % (self.tutorius_api, tutorial_store_id) headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_post(request_url, None, None, None, headers) if self.helper.iserror(response): return -1 return tutorial_store_id # Otherwise, we want to publish a new tutorial if tutorial_info == None: return -1 request_url = "/%s/publish/" % (self.tutorius_api) headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_post(request_url, tutorial_info, tutorial, tutorial_info['filename'], headers) if self.helper.iserror(response): return -1 xml_response = minidom.parseString(response['body']) id_node = xml_response.getElementsByTagName("id")[0].firstChild id = id_node.nodeValue return id def unpublish(self, tutorial_store_id): """ Removes a tutorial from the server. The user in the current session needs to be the creator for it to be unpublished. This will remove the file from the server and from all its collections and categories. This function requires the user to be logged in. @param tutorial_store_id The ID of the tutorial to be removed @return True if the tutorial was properly removed from the server """ request_url = "/%s/publish/%s" % (self.tutorius_api, tutorial_store_id) headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_delete(request_url, None, headers) if self.helper.iserror(response): return False return True def update_published_tutorial(self, tutorial_id, tutorial, tutorial_info): """ Sends the new content for the tutorial with the given ID. This function requires the user to be logged in. @param tutorial_id The ID of the tutorial to be updated @param tutorial The bundled tutorial file content (not a path!) @return True if the tutorial was sent and updated, False otherwise """ request_url = "/%s/update/%s" % (self.tutorius_api, tutorial_id) headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_post(request_url, tutorial_info, tutorial, tutorial_info['filename'], headers) if self.helper.iserror(response): return False return True def register_new_user(self, user_info): """ Creates a new user from the given user information. @param user_info A structure containing all the data required to do a login. @return True if the new account was created, false otherwise """ request_url = "/%s/registerNewUser" % (self.tutorius_api) params = {'nickname': user_info['nickname'], 'password': user_info['password'], 'email': user_info['email']} response = self.conn.request_post(request_url, params) if self.helper.iserror(response): return False return True class StoreProxyHelper(object): """ Implements helper methods for the Store, more specifically methods to handle xml responses and errors """ def iserror(self, response): """ Check if the response received from the server is an error @param response The XML response from the server @return True if the response is an error """ # first look for HTTP errors http_status = response['headers']['status'] if http_status in ['400', '401', '403', '500' ]: return True # Now check if the response is valid XML try: minidom.parseString(response['body']) except Exception, e: return True # The response is valid XML, parse it and look for # an error in xml format xml_response = minidom.parseString(response['body']) errors = xml_response.getElementsByTagName('error') if (len(errors) > 0): return True return False def parse_tutorial(self, xml_tutorial): """ Parse a tutorial's XML metadata and returns a dictionnary containing the metadata @param xml_tutorial The tutorial metadata in XML format @return A dictionnary containing the metadata """ tutorial = {} params = [ 'id', 'name', 'summary', 'version', 'description', 'author', 'rating' ] for param in params: xml_node = xml_tutorial.getElementsByTagName(param)[0].firstChild if xml_node != None: tutorial[param] = xml_node.nodeValue else: tutorial[param] = '' return tutorial