# This file is part of ReportTool # ReportTool (Felicity) is copyright 2004-8 Steve Butterfill. # # ReportTool 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. # # ReportTool 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 ReportTool. If not, see . # # If you want to use ReportTool under a difference licence, email # s.butterfill@warwick.ac.uk. # (c) Stephen Butterfill 2006 # use subject to licence, see licence.txt for details # --------------------------------------------------- # (a) some functions that help creating objects for displaying and editing reports # (b) some functions that help with navigation # (c) email functions # (d) tables and forms # (e) custom widgets # (f) passwords #for email import smtplib from email.MIMEText import MIMEText from turbogears import identity, widgets, validate, validators from elementtree import ElementTree from datetime import date import turbogears import cherrypy tg = turbogears #shortcut from sqlobject import SQLObjectNotFound from model import * #required for cookies, forms and tables from felicity.hacks import safe_str import logging log = logging.getLogger(__name__) #-------- # useful constants # TODO move these to global config file #used for sending emails REPORTTOOL_URL = "https://reporttool.warwick.ac.uk/philosophy" SMTP_HOST='mail-relay.warwick.ac.uk' SMTP_USER=None SMTP_PASSWORD=None #---- # (a) display reports def _make_name(val): "makes a name for use in a form, must be unique for different val" return str(val) def _is_name(obj): "returns true if obj is a valid name of a field in a form" #TODO current implementation risks false positives--use obscure field prefix? try: int(obj) return True except (ValueError, TypeError): return False def _name_to_val(name): "converts a field name from a form back to an int specifying index of this field in Report.content list" return int(name) def create_fields(format): """create widget fields for dispalying the format (which can be either ReportFormat or FeedbackFormat) in a form. Returns a list of the widgest that are the fields of a form. """ fields = [] for counter, rf in enumerate(format.fields): if rf.size is None: rf.size = 3 #counter is used as the basis for the name name = _make_name(counter) if rf.field_type=="text": if hasattr(rf, 'label_above') and rf.label_above == True: field = custom_widgets.TextBlock(name=name, label=rf.name, cols=60, rows=rf.size) else: field = widgets.TextArea(name=name, label=rf.name, validator=validators.UnicodeString(), cols=60, rows=rf.size) fields_to_add = [field] elif rf.field_type=="integer": field = widgets.TextArea(name=name, label=rf.name, validator=validators.Int(), cols=rf.size, rows=1) fields_to_add = [field] elif rf.field_type=="select-one": options = [(ind, rf.options[ind]) for ind in range(len(rf.options))] field = widgets.SingleSelectField(name=name, label=rf.name, options=options, size=rf.size) fields_to_add = [field] elif rf.field_type=="select-many": options = [(ind, rf.options[ind]) for ind in range(len(rf.options))] field = widgets.MultipleSelectField(name=name, id=name, label=rf.name, options=options, size=rf.size) fields_to_add = [field] elif rf.field_type=="heading": field1 = custom_widgets.Heading(text=rf.name ) field2 = widgets.HiddenField(name=name) fields_to_add = [field1, field2] elif rf.field_type=="slider": field1 = custom_widgets.Slider(name="slider_"+name, id="slider_"+name, left_text=rf.slider_left_text, right_text=rf.slider_right_text, steps=rf.slider_steps-1) field2 = widgets.HiddenField(name=name, id="slider_input_"+name) fields_to_add = [field1, field2] else: raise FelicityException, "field_type %s is invalid" % rf.field_type fields = fields + fields_to_add return fields def create_fields_for_aggregated_feedback(format): """create widget fields for dispalying a summary of feedbacks in the format. Returns a list of the widgests that are the fields of a form. """ fields = [] for counter, rf in enumerate(format.fields): #counter is used as the basis for the name name = _make_name(counter) if rf.size is None: rf.size = 3 if rf.field_type=="text": field = custom_widgets.TextBlock(name=name, label=rf.name, cols=60, rows=rf.size) fields_to_add = [field] elif rf.field_type=="slider": field1 = custom_widgets.SliderGraph(name=name, id="slider_graph_"+name, left_text=rf.slider_left_text, right_text=rf.slider_right_text, steps=rf.slider_steps-1) fields_to_add = [field1] elif rf.field_type=="heading": field1 = custom_widgets.Heading(text=rf.name ) field2 = widgets.HiddenField(name=name) fields_to_add = [field1, field2] else: #default is this text block. field = custom_widgets.TextBlock(name=name, label=rf.name, cols=60, rows=rf.size) fields_to_add = [field] fields = fields + fields_to_add return fields def format_to_form(report_format, submit_text="submit", disable_edits=False, disable_submit=False): """converts a ReportFormat to a tg widgets.TableForm object This is for ReportFormat only.""" #now do main content of report fields = create_fields(report_format) #possibly disable editing if disable_edits: for field in fields: field.attrs=dict(disabled=True) #following field is used to verify that report data is being sent #NB: this field must not be disabled _check_field = widgets.HiddenField(name="report_data") fields.append(_check_field) #used for reassigning reports to another staff if not disable_edits: staff_select = widgets.SingleSelectField(name="staff_id", label="re-assign this report to", options=lambda : [(0,'[select]'),]+_staff_options(), help_text="To re-assign this report, select who here hit 'save' below. Do not submit a report if you want to re-assign it.") fields.append(staff_select) #add submitted check box if not disable_submit: submitted_box = widgets.CheckBox(name="submitted", label="submit", help_text="Until you submit a report only you can access it. After you submit a report it cannot be edited.") fields.append(submitted_box) return widgets.TableForm(name="report_form", fields=fields, submit_text=submit_text) def feedback_format_to_form(feedback_format, submit_text="submit", disable_edits=False, disable_submit=False): """converts a FeedbackFormat to a tg widgets.TableForm object""" #now do main content of feedback form fields = create_fields(feedback_format) #add feedback id to form fields.append(widgets.HiddenField(name="feedback_id")) #possibly disable editing if disable_edits: for field in fields: field.attrs=dict(disabled=True) #following field is used to verify that feedback data is being sent #NB: this field must not be disabled _check_field = widgets.HiddenField(name="feedback_data") fields.append(_check_field) #used for reassigning reports to another staff return widgets.ListForm(name="feedback_form", fields=fields, submit_text=submit_text) def feedback_format_to_form_aggregated(feedback_format): """converts a FeedbackFormat to a tg widgets.TableForm object for displaying aggregated data on many feedback forms""" fields = create_fields_for_aggregated_feedback(feedback_format) return widgets.ListForm(name="feedback_form", fields=fields, submit_text="n/a") def content_to_data(content, submitted, format): """converts the Report. or Feedback. content to data suitable for working with a form""" data = {} if content is None : return {} for counter, item in enumerate(content): #content is just an ordered list of field values data[_make_name(counter)]=item #index is used as basis of name data['submitted']=submitted return data def data_to_content(messy_data, _data={}): """converts the data from a form back to the appropriate form for the Report.content field. Automatically disregards keys which are not valid field names. Param _data is for internal use: if set, it is used as a base for the content (see update_content_from_data)""" if messy_data is None : return [] for key in messy_data: if _is_name(key): _data[_name_to_val(key)] = messy_data[key] sorted_keys = list(_data.keys()) sorted_keys.sort() content = [_data[key] for key in sorted_keys] return content def update_content_from_data(form_data, content): """given some form data, will update the content and return the updated content. Will filter out any invalid names from form_data and ignore (so you don't have to clean the form_data first). """ data = {} #store old content in data for counter,item in enumerate(content): data[counter] = item return data_to_content(form_data, _data=data) #--------------- # (b) navigation def staff(): "returns staff object corresponding to current user, or None if not found" user = identity.current.user return Staff.from_user(user) def student(): "returns student object for current user or None if user is not a student" user = identity.current.user return Student.from_user(user) def staff_or_student(): "returns staff or student object corresponding to current user" _staff = staff() if _staff is None: return student() return _staff def thispage(): """returns url of current page with no trailing slash and any get parameters removed. This will include the server_path. If you get 'thispage' then do turbogears.redirect(thispage) you get the server_path added twice.""" #res = cherrypy.request.browser_url #replace above to #avoid http://localhost:8080/philosophy/student/0525695/degree/1 res = cherrypy.request.path #may not be necessary try: i = res.index("?") res=res[0:i] except ValueError: pass res = res.rstrip('/') return res def redirect(url): """safe version of turbogears.redirect""" #remove server_webpath from start of url before redirect. #this is ugly hack! server_webpath=tg.url('/').rstrip('/') #log.warn("server_webpath="+server_webpath) #remove server_webpath if it's already on it if url.startswith(server_webpath): url=url[len(server_webpath):] #log.warn("url="+url) turbogears.redirect(url) class cookies(object): "simple class for holding cookie or session methods. Currently uses only cookies" @staticmethod def _encode_ids(ids, cookie_name, expires=None): "encodes a list of objects or object ids and stores them in cookie_name" if len(ids) > 0 and hasattr(ids[0], 'id'): #convert list of students to list of student ids ids = [ _id.id for _id in ids] ids = ",".join([str(_id) for _id in ids]) cherrypy.response.simple_cookie[cookie_name] = ids cherrypy.response.simple_cookie[cookie_name]['path'] = '/'#tg.url('/') #makes always available if expires is not None: cherrypy.response.simple_cookie[cookie_name]['expires'] = expires @staticmethod def _decode_ids(clz, cookie_name, ids_only=False): """decodes a list of objects or object ids from cookie_name. returns [] if no cookie. Silently ignores items which are not valid ids for clz""" if not cherrypy.request.simple_cookie.has_key(cookie_name): return [] raw = cherrypy.request.simple_cookie[cookie_name].value ids = raw.split(",") if ids_only: results = ids else: #convert ids to objects (use select IN?) results = [] for _id in ids: if _id is not None and len(_id)>0: try: results.append(clz.get(_id)) except SQLObjectNotFound, e: #silently ignore cookies with defective ids in them pass return results @staticmethod def _encode_id(_id, cookie_name, expires=None): "stores the id of an object in cookie_name" if hasattr(_id, 'id'): _id = _id.id cherrypy.response.simple_cookie[cookie_name] = _id cherrypy.response.simple_cookie[cookie_name]['path'] = '/' if expires is not None: cherrypy.response.simple_cookie[cookie_name]['expires'] = expires @staticmethod def _decode_id(clz, cookie_name): "decode object clz from cookie_name. return None if no cooke" if not cherrypy.request.simple_cookie.has_key(cookie_name): return None raw = cherrypy.request.simple_cookie[cookie_name].value return clz.get(raw) @staticmethod def students(): "returns a list of students as stored in a cookie, or [] if no cookie" return cookies._decode_ids(Student, 'student_ids') @staticmethod def students_store(student_ids): cookies._encode_ids(student_ids, 'student_ids', expires=300) @staticmethod def modules(): "returns a list of modules as stored in a cookie, or [] if no cookie" return cookies._decode_ids(Module, 'module_ids') @staticmethod def modules_store(module_ids): cookies._encode_ids(module_ids, 'module_ids', expires=300) @staticmethod def reports(): """returns a list of reports as stored in a cookie, or [] if no cookie""" return cookies._decode_ids(Report, 'report_ids') @staticmethod def report_ids(): """returns a list of reports as stored in a cookie, or [] if no cookie""" return cookies._decode_ids(Report, 'report_ids', ids_only=True) @staticmethod def reports_store(report_ids): cookies._encode_ids(report_ids, 'report_ids') @staticmethod def dept(): "returns the currenly selected dept, defaulting to the user's staff's dept" _dept = cookies._decode_id(Dept, 'deptid') if _dept is None: _staff = staff() _dept = _staff.dept cookies._encode_id(_dept.id, 'deptid') #store cookie for next time return _dept @staticmethod def dept_set(dept_id): cookies._encode_id(dept_id, 'deptid') @staticmethod def term(): "selected term, or current term" _term = cookies._decode_id(Term, 'termid') if _term is None: _term = Term.current_term() return _term @staticmethod def term_set(term_id): cookies._encode_id(term_id, 'termid') @staticmethod def year(): "selected year, or current year" _year = cookies._decode_id(Year, 'yearid') if _year is None: _year = Year.current_year() return _year @staticmethod def year_set(year_id): cookies._encode_id(year_id, 'yearid') # -------------------------------- # email def email(to, subject, msg, sender="do-not-reply@reporttool.net"): """Sends emails. Does not catch any exceptions.""" msg = MIMEText(msg) msg['Subject'] = subject msg['From'] = sender msg['To'] = to #send it session = smtplib.SMTP(host=SMTP_HOST) session.ehlo() session.starttls() session.ehlo() if SMTP_USER: session.login(SMTP_USER, SMTP_PASSWORD) session.sendmail(msg['From'], msg['to'], msg.as_string()) session.close() def email_student_userdetails(student, password, password_reset=False): """emails username and password to student. Uses student.email_address first, then student.user.email_address. If no email address raises an exception. If param password_reset==False, sends an intro email; if True, sends an email giving new password""" firstname, lastname = student.firstname, student.lastname user = student.user email_address = student.email_address if (email_address is None or email_address == "") and student.user is not None: email_address = student.user.email_address if email_address is None or email_address == "": raise FelicityException, "No known email address for this student." msg = "" msg += "Dear %s %s," % (firstname, lastname) + "\n" msg += "" + "\n" if not password_reset: msg += "A reporttool account has been created for you." + "\n" else: msg += "A new reporttool password has been created for you." + "\n" msg += "" + "\n" msg += "Please login here:" + "\n" msg += REPORTTOOL_URL + "\n" msg += "" + "\n" msg += "Your login details:" + "\n" msg += "user: %s" % user.user_name+ "\n" msg += "password: %s" % password + "\n" msg += "" + "\n" msg += "Please change your password immediately. Please do not use the same password for ReportTool that you use for other accounts." + "\n" msg += "" + "\n" email(to=email_address, subject="ReportTool information", msg=msg) # ------------------------------ # widgets class custom_widgets(object): "custom widgets" class SimpleTableWidget(widgets.Widget): "displays n-column data in rows." template = 'felicity.widget_templates.simple_table' params = ['rows', 'height'] params_doc = {'rows': 'List of rows containing tuples of the data to be displayed', 'height':'height of table (css parameter); if not specified, will try to guess based on number of rows.'} rows=[('no data', 'empty table')] height=None def update_params(self, params): super(custom_widgets.SimpleTableWidget, self).update_params(params) rows = params['rows'] if 'height' not in params or params['height'] is None: params['height']=str(26*len(rows))+"px" simple_table = SimpleTableWidget() class TextBlock(widgets.TextArea): """Just like TextArea except label appears above the text""" template = """