Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius/store.py
diff options
context:
space:
mode:
Diffstat (limited to 'tutorius/store.py')
-rw-r--r--tutorius/store.py473
1 files changed, 473 insertions, 0 deletions
diff --git a/tutorius/store.py b/tutorius/store.py
new file mode 100644
index 0000000..81925ed
--- /dev/null
+++ b/tutorius/store.py
@@ -0,0 +1,473 @@
+# 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 False
+
+ return True
+
+ # Otherwise, we want to publish a new tutorial
+ if tutorial_info == None:
+ return False
+
+ 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 False
+
+ return True
+
+
+ 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 = [
+ '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