# $Id: Course.py,v 1.128 2002/08/12 15:01:22 jmp Exp $ # # Copyright 2001, 2002 by Fle3 Team and contributors # # This file is part of Fle3. # # Fle3 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. # # Fle3 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 Fle3; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Contains class Course, which represents one course, which has one or more CourseContexts, which in turn contain knowledge building conversations.""" __version__ = '$Revision: 1.128 $'[11:-2] import time import string, types import OFS, Globals from Globals import Persistent, Acquisition import AccessControl from AccessControl import ClassSecurityInfo import copy from Jamming import Jamming from TraversableWrapper import Traversable from common import add_dtml, reload_dtml, intersect_bool, make_action, get_roles, get_local_roles from input_checks import strip_all, is_valid_title from CourseContext import CourseContext #from ThinkingTypeSetManager import ThinkingTypeSetManager as TTSM from Thread import Thread from Cruft import Cruft from TempObjectManager import TempObjectManager from input_checks import render, normal_entry_tags_and_link from common import perm_view, perm_edit, perm_manage, perm_add_lo from XmlRpcApi import CourseXMLRPC # Each Course object contains (usually) one or more CourseContexts, which # represent different aspects of the course. # A Course contains information and services on one course implementation. class Course( Persistent, Traversable, Cruft, OFS.Folder.Folder, AccessControl.Role.RoleManager, OFS.SimpleItem.Item, Thread, CourseXMLRPC, ): """Course, contained within CourseManager, represents one course.""" meta_type = 'Course' security = ClassSecurityInfo() security.declareObjectPublic() dtml_files = ( ('add_course_context_form', 'Add Course Context Form', 'ui/Course/add_course_context_form'), ('course_info', 'Course Information', 'ui/Course/course_info'), ('show_user_info', 'User info page for KB', 'ui/Course/show_user_info'), ('user_info', 'Actual content', 'ui/UserInfo/user_info'), ('fle_form_header', 'Standard Html Header for forms (KB)', 'ui/Course/fle_form_header'), ('fle_html_header', 'Standard FLE Html Header (KB)', 'ui/Course/fle_html_header'), ) # Call courses/course_html (This way we avoid copying index_html # to each instance of Course class.) # ATTENTION: We don't exactly know how this works (for example, # do we really need REQUEST ?) # #TODO: This could be done with a URL pointing to the course_html script. #But if we need this default implementation, we could use #restrictedTraverse('course_html') to activate Zope acquisition. security.declareProtected('View', 'index_html') def index_html(self, REQUEST=None): """Default script""" return self.fle_root().courses.course_html(self, REQUEST) # Parameters: # #- parent: should be a CourseManager # #- name: name of the Course # #- tts: list of ThinkingTypeSets (copies are made in manage_afterAdd) # #- teachers: list of users (of class UserInfo?) that are granted #Teacher privileges # #- etc: other textual information def __init__( self, parent, # Whatever you do, dont bind this to self. name, teachers, description='', organisation='', methods='', starting_date='', ending_date=''): """Constructor of the course.""" # Overriding all_meta_types is not not beautiful...but, hey!, it works! #self.all_meta_types = ( # {'name': 'ThinkingTypeSet', # 'action': 'get_id'},) # Cache active members. self.active_memb_cache = [] # Remove all HTML tags from parameters name = strip_all(name) description = strip_all(description) methods = strip_all(methods) organisation = strip_all(organisation) Thread.__init__(self, parent) # Takes care of id and title. for teacher in teachers: self.set_roles(teacher, ('Teacher',)) self.__name = name # name of the course self.__organisation = organisation self.__description = description self.__methods = methods self.__starting_date = starting_date self.__ending_date = ending_date # This is for group folder path listings - show path up to course. self.toplevel = 1 # Add dtml objects. for tup in self.dtml_files: add_dtml(self, tup) security.declarePrivate('manage_afterAdd') #Each course should have its own copy of the ThinkingTypeSets, #as the course administrator (teacher) should be able to edit them, #create new ones and so forth. These changes must not propagate to #other courses: hence the copying of the set. def manage_afterAdd(self, item, container): """foo""" from common import roles_student, roles_tutor, roles_teacher from common import roles_admin self.manage_permission(perm_manage, roles_admin, 0) self.manage_permission(perm_edit, roles_teacher, 0) self.manage_permission(perm_add_lo, roles_tutor, 0) #Staff role needs to see course information from course management self.manage_permission(perm_view, roles_student+('Staff',), 0) self._setObject('jamming', Jamming('jamming')) # TempObjectManager.manage_afterAdd(self, item, container) security.declareProtected(perm_view, 'get_printable_name') # No additional comments. def get_printable_name(self): """Return name of the course.""" return self.__name security.declareProtected(perm_manage, 'reload_dtml') # No additional comments. def reload_dtml(self, REQUEST=None): """Reload dtml files from the file system.""" reload_dtml(self, self.dtml_files) if REQUEST: self.get_lang(('common','kb'),REQUEST) return self.message_dialog( self, REQUEST, title=REQUEST['L_dtml_reloaded'], message=REQUEST['L_dtml_files_reloaded'], action='index_html') security.declareProtected(perm_view, 'get_bg_colour_name') def get_bg_colour_name(self): """...""" return 'gr' #security.declareProtected(perm_view, 'get_name') # No additional comments. def get_name(self): """Get course name.""" return self.__name security.declareProtected(perm_view, 'get_organisation') # No additional comments. def get_organisation(self): """Get organisation name.""" return self.__organisation security.declareProtected(perm_view, 'get_description') # No additional comments. def get_description(self): """Get description.""" return self.__description security.declareProtected(perm_view,'render_description') def render_description(self): """Render description.""" return render( self.get_description(), legal_tags=normal_entry_tags_and_link) security.declareProtected(perm_view, 'get_methods') # No additional comments. def get_methods(self): """Get info on methods.""" return self.__methods security.declareProtected(perm_view,'render_methods') def render_methods(self): """Render methods.""" return render( self.get_methods(), legal_tags=normal_entry_tags_and_link) security.declareProtected(perm_view, 'get_teachers') # No additional comments. def get_teachers(self): """Get teachers.""" retval = [] for (user, roles) in self.get_local_roles(): if 'Teacher' in roles: retval.append(user) return retval security.declareProtected(perm_view, 'get_start_dd') # No additional comments. def get_start_dd(self): """Return starting day.""" if self.__starting_date: return time.localtime(self.__starting_date)[2] else: return '' security.declareProtected(perm_view, 'get_start_mm') # No additional comments. def get_start_mm(self): """Return starting month.""" if self.__starting_date: return time.localtime(self.__starting_date)[1] else: return '' security.declareProtected(perm_view, 'get_start_yyyy') # No additional comments. def get_start_yyyy(self): """Return starting year.""" if self.__starting_date: return time.localtime(self.__starting_date)[0] else: return '' security.declareProtected(perm_view, 'get_end_dd') # No additional comments. def get_end_dd(self): """Return ending day.""" if self.__ending_date: return time.localtime(self.__ending_date)[2] else: return '' security.declareProtected(perm_view, 'get_end_mm') # No additional comments. def get_end_mm(self): """Return ending month.""" if self.__ending_date: return time.localtime(self.__ending_date)[1] else: return '' security.declareProtected(perm_view, 'get_end_yyyy') # No additional comments. def get_end_yyyy(self): """Return ending year.""" if self.__ending_date: return time.localtime(self.__ending_date)[0] else: return '' security.declareProtected(perm_view, 'get_printable_starting_date') # No additional comments. def get_printable_starting_date(self, REQUEST): """Get starting date.""" self.get_lang(('common',), REQUEST) return time.strftime(REQUEST['L_short_date_format'], time.localtime(self.__starting_date)) security.declareProtected(perm_view, 'get_printable_ending_date') # No additional comments. def get_printable_ending_date(self, REQUEST): """Get ending date.""" if self.__ending_date == 0: return '' self.get_lang(('common',), REQUEST) return time.strftime(REQUEST['L_short_date_format'], time.localtime(self.__ending_date)) security.declarePrivate('get_start_date') def get_start_date(self): return self.__starting_date security.declarePrivate('get_end_date') def get_end_date(self): return self.__ending_date security.declareProtected(perm_view, 'get_users') def get_users(self, REQUEST): """Return dict with 2 members of attendees as UserInfo objects. 'active_d' is the list of active members. 'others_d' is the rest .. [[UIObj'active1'], [UIObj'other1', UIObj'other2', ..]]""" rv = {'active_d':[], 'others_d': []} au = str(REQUEST.AUTHENTICATED_USER) for uname in [(u[0]) for u in self.get_local_roles()]: o = self.fle_users.get_user_info(uname) if uname == au or uname in self.active_memb_cache: rv['active_d'].append(o) else: rv['others_d'].append(o) return rv security.declareProtected(perm_view, 'get_all_users') def get_all_users(self): """Return a list of all users on course.""" rv = [] for uname in self.get_all_users_id(): rv.append(self.fle_users.get_user_info(uname)) return rv security.declareProtected(perm_view, 'get_all_users_id') def get_all_users_id(self): """Return a list of all users on this course. Same thing as get_all_users, but this one returns a list of id's of UserInfo objects, not the UserInfo object reference.""" return [u[0] for u in self.get_local_roles()] security.declareProtected(perm_view, 'get_users_with_role') def get_users_with_role(self, role): """Return a list of participants who have a specified role.""" rv = [] for e in self.get_local_roles(): if role in e[1]: rv.append(self.fle_users.get_user_info(e[0])) return rv security.declareProtected(perm_edit, 'add_student') #The person is added with Student role access. # NOTE: This method is no longer needed, except in the test cases! def add_student(self, name): """Add person to the course.""" # Check that user exists... Raises exception if not. self.fle_users.get_user_info(name) self.set_roles(name, ('Student',)) security.declareProtected(perm_view, 'get_valid_roles') # No additional comments. def get_valid_roles(self): """Get the roles valid for persons added to the course.""" from CourseManager import course_level_roles valid_roles = list(course_level_roles) #valid_roles.append('Teacher') return valid_roles security.declareProtected(perm_manage, 'remove_person') def remove_person(self, person): """Remove person from the course.""" # Check that user exists... Raises exception if not. self.fle_users.get_user_info(person) # We use the get_local_roles method, because we need to # see if the user has a role attached to this course object # specifically, and we don't want the roles in the acquisition # tree to interfere. if len(get_local_roles(self,person))==0: raise FleError, ("User "+person+" does not belong to this course.") self.__unset_roles((person,)) # Note: person _must_ be a sequence! def __unset_roles(self, persons): """Unset roles of one user.""" self.manage_delLocalRoles(persons) security.declareProtected(perm_view, 'has_role') # No additional comments. def has_role(self, person, role): """Return whether the user is in the specified role.""" return role in get_roles(self,person) security.declareProtected(perm_view, 'get_teacher') def get_teacher(self): """Get the name of the teacher (creator of the course).""" for user,roles in self.get_local_roles(): if 'Teacher' in roles: return user raise 'Course has no teacher!' security.declareProtected(perm_edit, 'set_roles') # Note: roles _must_ be a sequence! # Called from CourseManager.add_users_form_handler def set_roles(self, person, roles): """Set roles of one person.""" self.__unset_roles(person) self.manage_setLocalRoles(person, roles) # FIXME: input_checks: tt_set_name not checked # FIXME: input_checks: two course contexts can have identical name. security.declareProtected(perm_add_lo, 'add_course_context') # Handler for add_course_context_form def add_course_context( self, my_name, description, tt_set_name, description_long, REQUEST, # Submit buttons. publish='', cancel='', ): """Add CourseContext object.""" if publish: error_fields = [] errors = [] self.get_lang(('common', 'kb'), REQUEST) my_name = my_name.strip() if not is_valid_title(my_name): error_fields.append(REQUEST['L_title_of_context']) if my_name in [x[1] for x in self.get_course_context_names()]: errors.append(REQUEST['L_name_taken'] % my_name) # Variables 'description' and 'description_long' are not checked # because render_description() and render_long_description() # methods in CourseContext filter out unwanted HTML tags. if len(error_fields) > 0 or len(errors) > 0: msg = ", ".join(errors) if len(error_fields) > 0: msg = msg + "
" + REQUEST['L_invalid_fields'] + \ ": '" + "' , '".join(error_fields) + "'" return self.message_dialog_error( self, REQUEST, title=REQUEST['L_invalid_input'], message=msg, action=apply( make_action, ['add_course_context_form'] + [(x, eval(x)) for x in ('my_name', 'description', 'tt_set_name', 'description_long')])) uname=str(REQUEST.AUTHENTICATED_USER) obj = CourseContext( self, my_name, description, description_long, tt_set_name, uname,) id = obj.get_id() self._setObject(id, obj) obj.changeOwnership(self.acl_users.getUser(uname).__of__(self.acl_users)) obj.manage_setLocalRoles(uname,('Owner',)) if REQUEST: REQUEST.RESPONSE.redirect(self.state_href( REQUEST, '%s/' % (str(id)))) elif cancel: return REQUEST.RESPONSE.redirect(self.state_href(REQUEST,REQUEST.URL1)) else: raise "add_course_context called without 'publish' or 'cancel'" security.declareProtected(perm_view, 'get_course_context_names') # No additional comments. def get_course_context_names(self): """Return a list of CourseContext names.""" retval = {} for e in self.get_children('CourseContext'): id = e.get_id() name = e.get_name() retval[id] = name return retval.items() security.declareProtected(perm_view, 'get_n_notes') def get_n_notes(self): """Returns a sum of all notes in all contexts.""" count = 0 for cc in self.get_course_contexts(): count += cc.get_n_notes() return count security.declareProtected(perm_view, 'get_n_unread_notes') def get_n_unread_notes(self,uname): """Returns a sum of all unread notes in all contexts.""" count = 0 for cc in self.get_course_contexts(): count += cc.get_n_unread_notes(uname) return count security.declarePrivate('update') # Parameters are received from the form (apparently). def update( self, name, description, organisation, methods, starting_date, ending_date, ): """Edit course information.""" self.__name = name self.__description = description self.__organisation = organisation self.__methods = methods self.__starting_date = starting_date self.__ending_date = ending_date security.declareProtected(perm_view, 'get_course_contexts') def get_course_contexts(self): """Return a list of all course contexts in this course.""" return self.get_children('CourseContext') security.declareProtected(perm_view, 'get_course_context_ids_in_order') def get_course_context_ids_in_order(self, id_list): """Return a list of ids of all course contexts in this course.""" return [o.get_id() for o in self.get_course_contexts_in_order(id_list)] # FIXME: See the random comment below, random is not # FIXME: probably the order that we really want. security.declareProtected(perm_view, 'get_course_contexts_in_order') def get_course_contexts_in_order(self, id_list): """Return a list of all course contexts in this course.""" contexts = {} for c in self.get_children('CourseContext'): contexts[c.get_id()] = c retval = [] if id_list and id_list != ['']: if type(id_list) == types.StringType: id_list = (id_list, ) # Return courses contexts in a given order. for identifier in id_list: try: retval.append(contexts[identifier]) del contexts[identifier] except KeyError: # invalid id_list pass # If we still course contexts left (id_list is shorter # than the actual number of course_contexts), append # them to list in a random order. for key in contexts.keys(): retval.append(contexts[key]) return retval security.declarePublic('may_view_course') def may_view_course(self, REQUEST): """Return boolean depending on wether user may or may not view the course.""" from AccessControl.PermissionRole import rolesForPermissionOn return intersect_bool( get_roles(self,str(REQUEST.AUTHENTICATED_USER)), rolesForPermissionOn(perm_view,self)) security.declareProtected(perm_view, 'may_add_course_context') def may_add_course_context(self, person): """Return boolean depending on wether user may or may not add a course context to the course.""" from AccessControl.PermissionRole import rolesForPermissionOn return intersect_bool( get_roles(self,person), rolesForPermissionOn(perm_add_lo,self)) security.declareProtected(perm_view, 'may_edit_course') def may_edit_course(self, person): """Return boolean depending on whether person can edit the course or not.""" from AccessControl.PermissionRole import rolesForPermissionOn return intersect_bool( get_roles(self,person), rolesForPermissionOn(perm_edit,self)) security.declareProtected(perm_view, 'active_members') #FIXME: Why not "get_active_members" def active_members(self): """Return a list of names of users who are active.""" return self.active_memb_cache security.declareProtected(perm_view, 'has_group_folder') def has_group_folder(self): """Return whether course has a group folder or not.""" return len(self.objectIds('GroupFolder'))>0 def add_folder(self, my_name): from GroupFolder import GroupFolder from GroupFolderProxy import GroupFolderProxy new_id = 'gf' fol = GroupFolder(None,my_name) fol.id=new_id self._setObject(new_id,fol) fol=fol.__of__(self) self.make_group_folder_proxies(self.get_all_users()) proxy = GroupFolderProxy(None, # any sense? self.get_name(), self.get_id()) self.jamming._setObject('0', proxy) return fol def make_group_folder_proxies(self, users): for user in users: # user.webtop.add_link(self.get_name()+" (Shared)",self.get_url_to_object(self.gf),1) user.webtop.add_group_folder_proxy(self.get_name(), self.get_id()) security.declareProtected(perm_view, 'make_group_folder_proxy_handler') def make_group_folder_proxy_handler(self, REQUEST): """Make a group folder proxy on a webtop of a current user.""" user = self.fle_users.get_child(str(REQUEST.AUTHENTICATED_USER)) self.make_group_folder_proxies((user,)) self.get_lang(('common','webtop'), REQUEST) return self.message_dialog( self, REQUEST, title=REQUEST['L_proxy_added'], message=REQUEST['L_added_proxy'] % self.get_name(), action='index_html') security.declareProtected(perm_view, 'does_group_folder_proxy_exist') def does_group_folder_proxy_exist(self, REQUEST): """Does current user already has a proxy on her webtop for group folder of this course?""" wt = self.fle_users.get_child(str(REQUEST.AUTHENTICATED_USER)).webtop return self.__proxy_finder(wt) def __proxy_finder(self, folder): for proxy in folder.get_children('GroupFolderProxy'): if proxy.get_course_this_belongs_to() == self: return 1 for f in folder.get_children('WebtopFolder'): if self.__proxy_finder(f): return 1 return 0 def remove_folder_link(self,users): gf = self.get_child('gf') for user in users: fol = user.webtop self.__recurse_remove_folder_link(fol, gf) def __recurse_remove_folder_link(self, folder, gf): for (_id, proxy_fol) in folder.objectItems('GroupFolderProxy'): if proxy_fol.is_proxy_for(gf): folder._delObject(_id) for fol in folder.objectValues('WebtopFolder'): self.__recurse_remove_folder_link(fol, gf) def eml_export(self,REQUEST): """Exports the course in zipped EML format.""" from ImportExportEML import Exporter ex = Exporter() ex.exportCourse(self) import tempfile, os filename = tempfile.mktemp() ex.createZip(filename) file = open(filename,"rb") export_data=file.read() file.close() os.remove(filename) REQUEST.RESPONSE.setHeader('content-type','application/zip') return export_data def write_touchgraph_data(self,obj,type,children_types=None,extra_links=None): links = '' if children_types: links = ' '.join(obj.objectIds(children_types)) if extra_links: links = ' '.join((links,extra_links)) if not links: links=' ' return obj.get_id()+'\t'+\ type+"\t"+\ self.REQUEST.BASE0+self.get_url_to_object(obj)+'\t'+\ obj.get_name()+'\t'\ +links+'\t' # But should we protect it somehow? # The Java client would then need authentication def touchgraph_data(self): """This is a public method.""" data=self.write_touchgraph_data(self,"COURSE","CourseContext") for ctx in self.objectValues("CourseContext"): data=data+ctx.touchgraph_data() return data+"[END DATA]" Globals.default__class_init__(Course) # EOF