# 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 = """
"""
class Heading(widgets.Widget):
"""display a heading"""
text = "Heading"
is_named=False
params = ['text']
params_doc = {'text': 'Value will be text displayed as the heading'}
name = ""
label = ""
field_id = ""
help_text = ""
validator = None
template="""
"""
class Slider(widgets.Widget):
"""display the divs for a jquery.slider object"""
params = ['left_text', 'right_text', 'steps', 'id']
name = ""
label = ""
field_id = ""
help_text = ""
validator = None
template="""
| ${left_text} |
|
${right_text} |
"""
class SliderGraph(widgets.TextArea):
"""displays the divs for a jquery.graph object"""
params = ['left_text', 'right_text', 'steps', 'id']
template="""
| ${left_text} |
|
${right_text} |
"""
class SimpleFormWidget(widgets.Widget):
"encloses another widget in a form"
template = 'felicity.widget_templates.simple_form'
params = ['form_action', 'submit_name', 'submit_value', 'inner_widget']
class DataGridSub(widgets.DataGrid):
"""like datagrid but suitable for a subtable (no headings)"""
template = 'felicity.widget_templates.datagrid_no_headings'
class FelicityDataGrid(widgets.DataGrid):
"""like datagrid but has class and id suitable for making editable using jquery.jeditable"""
template = 'felicity.widget_templates.felicity_datagrid'
class SingleRowTableForm(widgets.Form):
"""just like TableForm but only suitable for a single field.
Everything appears in a single row"""
template = """
"""
params = ["table_attrs"]
params_doc = {'table_attrs' : 'Extra (X)HTML attributes for the Table tag'}
table_attrs = {}
# ------------------------------
# forms and tables
#making these functions ensures always up to date
def _dept_options():
return [(dept.id, dept.name) for dept in Dept.all()]
def _year_options():
return [(year.id, year.name) for year in Year.all()]
def _term_options():
return [(term.id, term.name) for term in Term.all()]
def _module_options():
dept = cookies.dept()
return [(module.id, module.code+" : "+module.name) for module in Module.all(dept=dept)]
def _format_options():
return [(format.id, format.name) for format in ReportFormat.all()]
def _student_options():
dept = cookies.dept()
students = Student.all(dept=dept, only_current=True)
return [(stud.id, stud.lastname+", "+stud.firstname) for stud in students]
def _staff_options():
dept = cookies.dept()
staffs = Staff.all(dept=dept, only_current=True)
return [(staff.id, staff.lastname+", "+staff.firstname) for staff in staffs]
class form_parts(object):
"""holds parts of forms for use in building forms"""
name_input = widgets.TextField(name='name', label='name', validator=validators.NotEmpty())
name_input_disabled = widgets.TextField(name='name', label='name', attrs=dict(disabled=True))
dept_select = widgets.SingleSelectField(name='dept_id', label='dept', options=_dept_options)
@staticmethod
def year_select(**kw):
return widgets.SingleSelectField(name='year_id', label='year', options=_year_options, **kw)
@staticmethod
def term_select(**kw):
return widgets.SingleSelectField(name='term_id', label='term', options=_term_options, **kw)
@classmethod
def staff_select(cls,name=None, **kw):
#nb using validator is essential--prevents form validation from init options too early
if name is None:
name = "staff_id"
return widgets.SingleSelectField(name=name, label="staff", options=_staff_options,
validator=validators.NotEmpty(), **kw)
class forms(object):
#(a) parts of forms
#TODO move these over to form_parts
#TODO should use date validator (must adapt the one supplied for EU date format
start_date_input_required = widgets.CalendarDatePicker(name='start_date',
label='start date',
format="%d/%m/%y")
#TODO should use date validator (must adapt the one supplied for EU date format
#date_input=widgets.CalendarDatePicker(name='date', label='date', format="%d/%m/%y")
end_date_input=widgets.CalendarDatePicker(name='end_date',
label='end date', format="%d/%m/%y")
#used for create_report
# for some reason this only works if we include the validator!
student_select = widgets.SingleSelectField(name="student_id", label="student",
options=_student_options,
validator=validators.NotEmpty())
#used for re-assigning reports
# for some reason this only works if we include the validator!
staff_select = widgets.SingleSelectField(name="staff_id", label="re-assign this report to",
options=_staff_options,
validator=validators.NotEmpty())
#lots of things for the module_group_edit table and form
module_select = widgets.SingleSelectField(name="module_id", label="module", options=_module_options, default=None, validator=validators.NotEmpty())
report_format_select = widgets.SingleSelectField(name="format_id", label="report format", options=_format_options, default=None, validator=validators.NotEmpty())
venue_input = widgets.TextField(name='venue', label='venue')
time_input = widgets.TextField(name='time', label='time') #not an actual time, rather text that may include a day of the week
max_students_input = widgets.TextField(name="max_students", label="max students",
validator = validators.Int())
year_hidden = widgets.HiddenField(name="year_id")
#(b) whole forms
#for selecting a year
year_form = widgets.TableForm(fields=[form_parts.year_select()], submit_text="change year")
#for editing a year
year_edit = widgets.TableForm(fields=[form_parts.name_input, start_date_input_required, end_date_input],
submit_text="create year")
term_edit = widgets.TableForm(fields=[form_parts.name_input], submit_text="create term")
#for selecting a dept
dept_form = widgets.TableForm(fields=[form_parts.dept_select], submit_text="change dept")
_module_search_widget = widgets.AutoCompleteField(name="module_name", \
label="module name or code", \
search_controller=tg.url("/module/_autocomplete"), \
search_param="module_name", \
result_name="module_names")
module_search = custom_widgets.SingleRowTableForm(fields=[ _module_search_widget], submit_text="search")
_student_search_widget = widgets.AutoCompleteField(name="lastname", \
label="lastname", \
search_controller=tg.url("/student/_autocomplete"), \
search_param="lastname", \
result_name="studnames")
student_search = custom_widgets.SingleRowTableForm(fields=[_student_search_widget], submit_text="search")
#student edit form
_fields = []
#nb student_form_field_names is exposed -- used to update records from rof
student_form_field_names = ['email_address']
_fields.append(widgets.TextField(name="email_address", \
label="email address", \
validator=validators.Email(not_empty=False)))
student_edit = widgets.TableForm(fields=_fields, submit_text="save changes")
#report forms
create_report= widgets.TableForm(fields=[student_select, module_select, report_format_select],
submit_text="create report")
subject_input_required = widgets.TextField(name='subject', label='subject',
validator=validators.PlainText(not_empty=True))
body_input_required = widgets.TextArea(name='body', label='note',
validator=validators.PlainText(not_empty=True))
student_notes_edit = widgets.TableForm(fields=[subject_input_required, body_input_required],
submit_text="add note")
@classmethod
def module_group_edit(cls, disabled=False):
"""when disabled displays more fields for info"""
_fields = []
if disabled:
_fields+=[form_parts.name_input_disabled]
#extra fields
module_name=widgets.TextField(name="module_name", label="module name",
attrs=dict(disabled=True))
_fields+=[module_name, form_parts.year_select(attrs=dict(disabled=True))]
_fields +=[form_parts.term_select(attrs=dict(disabled=True))]
else:
_fields+=[form_parts.name_input, form_parts.term_select()]
_fields += [cls.year_hidden, form_parts.staff_select(),
cls.time_input, cls.venue_input, cls.max_students_input,
widgets.HiddenField(name="add_module_group")]
return widgets.TableForm(fields=_fields, submit_text="create module group")
@classmethod
def staff(cls, edit=False, select_dept=True):
_staff_fields=[]
if not edit:
_staff_fields.append(widgets.TextField(name='code', label='SITS code',
validator=validators.PlainText(not_empty=True)))
else:
_staff_fields.append(widgets.TextField(name='code2', label='SITS code',
attrs=dict(disabled=True)))
_staff_fields.append(widgets.HiddenField(name='code'))
_staff_fields.append(widgets.TextField(name='its_code',
label='ITS code (e.g. pyscug)',
validator=validators.PlainText(not_empty=True)))
_staff_fields.append(widgets.TextField(name='firstname',
label='firstname',
validator=validators.NotEmpty()))
_staff_fields.append(widgets.TextField(name='lastname', label='lastname',
validator=validators.NotEmpty()))
_staff_fields.append(widgets.TextField(name='email_address', label='email',
validator=validators.Email()))
if select_dept:
_staff_fields.append(widgets.SingleSelectField(name='dept_id', label='dept',
options=_dept_options))
#removed these because they don't allow null values!!!
# _staff_fields.append(forms.start_date_input_required)
# _staff_fields.append(widgets.CalendarDatePicker(name='end_date', label='end date',
# attrs=dict(disabled=True)))
if edit:
staff_form = widgets.TableForm(fields=_staff_fields, submit_text="update")
else:
staff_form = widgets.TableForm(fields=_staff_fields, submit_text="create")
return staff_form
#helper functions for tables
#todo make these into a class and make sure all links are defined in this class for easy restructuring
def make_link(extract_text_function, extract_link_function):
def _link(item):
link = ElementTree.Element('a', href=extract_link_function(item))
link.text = extract_text_function(item)
return link
return _link
def _studentLink(student):
link = ElementTree.Element('a', href=student.url)
link.text = student.lastname
return link
def _studentOmrLink(student):
if not student.is_current:
return student.code
link = ElementTree.Element('a', href='https://secure.admin.warwick.ac.uk/omr/student/entry.jsp?sid='+str(student.code), target='_blank')
link.text = student.code
return link
def _staff_link(staff):
link = ElementTree.Element('a', href=staff.url)
link.text = staff.lastname
return link
def _module_link(module, display_attr='name'):
"returns a link to a module given a module"
link = ElementTree.Element('a', href=module.url)
if type(display_attr) is str:
link.text = getattr(module, display_attr)
else:
link.text = apply(display_attr, module)
return link
def _module_group_edit_link(module):
"returns a link to a module's seminar groups page given a module"
module_url = module.url
if( module_url.endswith('/')):
module_url = module_url[0:-1]
link = ElementTree.Element('a', href=module_url+'/module_groups')
link.text = module.name
return link
def _omr_module_link(student_module):
"returns a link to a module given an StudentModule object"
module_name= student_module.module_name
if module_name is None or len(module_name)<1:
return module_name
link = ElementTree.Element('a', href=tg.url('/module/%s/' % student_module.module_code))
link.text=module_name
return link
def _module_groups_edit_link(module_group):
"returns a link to the page for editing which students are in a give ModuleGroup"
name = module_group.name
module_code = module_group.module.code
link = ElementTree.Element('a', href=module_group.url)
link.text=name
return link
def _module_group_join_link(module_group):
"""returns a link that allows a student to join this group"""
name = "join this group"
link = ElementTree.Element('a', href=module_group.url_join)
link.text = name
return link
def _make_report_links(reports):
"returns list of reports with links"
span = ElementTree.Element('span')
for report in reports:
link = ElementTree.Element('a', href=report.url)
link.text = str(report)
link.tail = ", "
span.append(link)
return span
def _make_link(this_page, _id, link_action, link_text):
"makes links of the form this_page/_id/link_action"
link = ElementTree.Element('a', href=this_page+'/'+str(_id)+'/'+link_action)
link.text = str(link_text)
return link
def _module_group_delete_link(module_group):
"makes a delete link for a module_group object to be used from a module page"
_id=module_group.id
try:
module_code = module_group.module.code
except Exception, e:
log.debug("error getting the module for module_group %s, exception was %s" % (str(_id), str(e)))
return ""
href = module_group.url+"/delete"
href +="?forward_url=/module/"+module_code+"/module_groups"
link = ElementTree.Element('a', href=href)
link.text="X"
return link
def _make_checkbox(name):
link = ElementTree.Element('input', type='checkbox', name=name)
return link
#------------------------------
#table html components to be exposed
def programme_link(programme):
link = ElementTree.Element('a', href=tg.url('/programme/code/%s' % programme.code))
link.text = programme.code
return link
def delete_dept_degree_btn(degree):
_form = ElementTree.Element('form', action=tg.url('/dept/degree_remove/'))
_btn = ElementTree.Element('input', type='submit', name='remove', value='remove')
_input = ElementTree.Element('input', type='hidden', name='degree_code', value=degree.code)
_form.append(_btn)
_form.append(_input)
return _form
def delete_dept_programme_btn(programme):
_form = ElementTree.Element('form', action=tg.url('/dept/programme_remove/'))
_btn = ElementTree.Element('input', type='submit', name='remove', value='remove')
_input = ElementTree.Element('input', type='hidden', name='programme_code', value=programme.code)
_form.append(_btn)
_form.append(_input)
return _form
def delete_programme_degree_btn(programme):
def _delete_programme_degree_btn(degree):
_form = ElementTree.Element('form', action=tg.url('/programme/degree_remove/'))
_btn = ElementTree.Element('input', type='submit', name='remove', value='remove')
_form.append(_btn)
_input = ElementTree.Element('input', type='hidden', name='degree_code', value=degree.code)
_form.append(_input)
_input2 = ElementTree.Element('input', type='hidden', name='programme_code', value=programme.code)
_form.append(_input2)
return _form
return _delete_programme_degree_btn
#this should have been exposed
def module_link(module, display_attr='code'):
return _module_link(module, display_attr)
class tables(object):
@staticmethod
def years(with_delete=False, this_page=None, with_make_current=True):
"returns table for displaying years. If with_delete is specified, this_page must also be specified"
_year_fields=[('name', 'name'), ('ordering', 'ordering'),
('start_date', 'start_date'), ('end_date', 'end_date'),
('current', 'current')]
def _year_delete_link(year):
return _make_link(this_page, str(year.id), 'delete', 'X')
if with_delete:
_year_fields+=[('delete', _year_delete_link)]
def _year_make_current_link(year):
return _make_link(this_page, str(year.id), 'make_current', '<<')
if with_make_current:
_year_fields+=[('make current', _year_make_current_link)]
return widgets.DataGrid(fields=_year_fields)
@staticmethod
def terms(with_delete=False, with_make_current=True, this_page=None):
"""returns table for displaying terms.
If with_delete is specified, this_page must also be specified"""
_term_fields=[('name', 'name'), ('ordering', 'ordering'), ('current', 'current')]
def _term_delete_link(term):
return _make_link(this_page, str(term.id), 'delete', 'X')
if with_delete:
_term_fields+=[('delete', _term_delete_link)]
def _term_make_current_link(term):
return _make_link(this_page, str(term.id), 'make_current', '<<')
if with_make_current:
_term_fields+=[('make current', _term_make_current_link)]
return widgets.DataGrid(fields=_term_fields)
@staticmethod
def depts(with_delete=False, this_page=None):
"""returns table for displaying depts.
If with_delete is specified, this_page must also be specified"""
_dept_fields=[('code', 'code'), ('name', 'name')]
def _delete_link(dept):
link = ElementTree.Element('a', href=this_page+'/'+(dept.code)+'/delete')
link.text = 'X'
return link
if with_delete:
_dept_fields+=[('delete', _delete_link)]
return widgets.DataGrid(fields=_dept_fields)
#TODO break students into classes and subclasses (too complex as it stands!)
@staticmethod
def students(with_x_degree_code=False, with_degree_codes=False,
with_current=False, with_email_address=False,
with_x_reports=False, with_omr_codes=True,
with_module_group=False, with_x_module_group=False,
module_group=None, module=None, year=None, term=None):
"""returns a student table with the optionally specified fields.
param with_degree_codes includes the codes of any degrees the student is currently taking.
param with_module_group displays current module group plus a check-box for assigning
param with_module_group requires module_group, module, year and term to be specified.
param with_reports requires each student have a property x_reports which is a list of
reports to include in the table"""
_students_fields = [('first name', 'firstname'),
('last name', _studentLink),
]
if with_omr_codes:
_students_fields +=[('omr code', _studentOmrLink)]
if with_email_address:
_students_fields +=[('email','email_address')]
if with_x_degree_code:
_students_fields += [('degree', 'x_degree_code')]
if with_degree_codes:
_students_fields += [('degree(s)', lambda x: x.degree_codes())]
if with_module_group or with_x_module_group:
def _make_module_group_link_from_student(module,year,term):
def inner(student):
if with_x_module_group:
try:
mg_url = student.x_module_group_url
mg_name = student.x_module_group_name
except AttributeError:
return ""
else:
mg = student.module_group(module=module, year=year, term=term)
if mg is None:
return ""
mg_url = mg.url
mg_name = mg.name
link = ElementTree.Element('a', href=mg_url)
link.text = mg_name
return link
return inner
_students_fields += [('group', _make_module_group_link_from_student(module, year, term))]
def _check_table(module_group):
"""makes a function to return checkbox for display in table.
The function accepts a student and returns a checkbox widget displayed."""
students = module_group.students
def inner(student):
in_group=student in students
attrs={}
if in_group: attrs['checked']=True
return widgets.CheckBox(name="student_"+str(student.id), attrs=attrs).display()
return inner
_students_fields += [('assign/remove', _check_table(module_group))]
if with_current:
_students_fields += [('current', lambda x: x.is_current)]
if with_x_reports:
def _report_links(student):
reports = student.x_reports
if len(reports) > 0:
return _make_report_links(reports)
else:
return ""
_students_fields += [('reports', _report_links)]
return widgets.DataGrid(fields=_students_fields, attrs={'id':"student_table"})
@staticmethod
def staff(with_dept=False, with_user=False, with_register_checkbox=False):
"""displays staff."""
fields = []
if with_register_checkbox:
fields += [('select',lambda s: _make_checkbox(name="_create_%s" % s.id))]
fields+=[('firstname','firstname'), ('lastname',lambda staff:_staff_link(staff)),
('code','code'),('its code','its_code')]
if with_dept:
fields+=[('dept','dept')]
fields += [('email address', 'email_address'),
('start date','start_date'), ('end date', 'end_date'),]
if with_user:
fields+=[('reporttool user', lambda staff:staff.userID != None)]
staff_table = widgets.DataGrid(fields=fields)
return staff_table
@staticmethod
def omr_module():
"""table for listing a student's modules, with seminar groups and reports.
uses StudentModule objects. Main table contains subtable.
Each line has information about a module a student is taking in a particular year.
A subtable has info for each term of that year--seminar groups and reports"""
def _module_group_subtable(student_module):
"""Subtable of omr_module. lists ModuleGroups and Reports by term"""
student = student_module.student
module = student_module.module
year = student_module.year
def _seminar_group_link(term):
mg = student.module_group(module=module, year=year,term=term)
if mg:
return _module_groups_edit_link(mg)
else:
return ""
_fields = [('term','name')]
_fields += [('seminar group', _seminar_group_link)]
#now add one more field for the reports
def _report_links(term):
reports = Report.all(student=student, module=module, year=year,
term=term, only_submitted=True)
if len(reports) > 0:
return _make_report_links(reports)
else:
return ""
_fields += [ ('reports', _report_links)]
return custom_widgets.DataGridSub(fields=_fields).display(Term.all())
_module_fields = [('code', 'module_code'), ('name', _omr_module_link),
('term, seminar group, reports',_module_group_subtable),
('year', 'year'), ('cats', 'cats')
]
_omr_module=widgets.DataGrid(fields=_module_fields)
return _omr_module
@staticmethod
def modules(term=None, year=None,
with_check_box=False, check_box_name="",
extra_fields=[],
for_edit_groups=False):
"""returns a table listing modules.
Term and Year default to current.
It's necessary to invoke term and year values because data on student numbers and
staff teaching are included.
parameter with_check_box specifies that a checkbox will be included.
Check box ids are 'module_'."""
if term is None:
term = Term.current_term()
if year is None:
year = Year.current_year()
def _make_staff_fn(term, year):
def inner(module):
leader = module.leader(term, year)
try:
return leader.lastname+", "+leader.firstname
except AttributeError:
return ""
return inner
_fields=[('module code', 'code')]
if for_edit_groups:
_fields += [('module name', _module_group_edit_link)]
else:
_fields += [('module name', _module_link)]
#this is a parameter
_fields += extra_fields
_fields+=[('staff', _make_staff_fn(term, year))]
if with_check_box:
def _check_box(module):
"""to return checkbox for display in table.
The function accepts a module and returns a checkbox widget displayed.
All checkboxes are unchecked"""
return widgets.CheckBox(name="module_"+str(module.id)).display()
_fields += [(check_box_name, _check_box)]
return widgets.DataGrid(fields=_fields)
@staticmethod
def _student_note_link(note):
return _make_link(tg.url("/note"), note.id, "", note.date)
_student_note_fields=[('date', lambda x:tables._student_note_link(x)),
('author', 'author'), ('subject', 'subject'),
('confirmed', 'is_confirmed')]
student_notes = widgets.DataGrid(fields=_student_note_fields)
student_notes_with_student= widgets.DataGrid(fields=_student_note_fields+[('student','student')])
@staticmethod
def module_group(mg, allow_edit=False, form_action=None,
submit_name="update_module_group_properties", submit_value="save changes"):
"""table for displaying a single module group.
If allow_edit displays as a form, and form_action must be set."""
#these fields can never be edited
rows = [('group name',mg.name),('module', _module_link(mg.module)),
('term',mg.term), ('year',mg.year),
]
#these fields may be edited
if not allow_edit:
rows +=[('leader', str(mg.staff)),
('time', str(mg.time)),
('venue', str(mg.venue)),
('max students', str(mg.max_students))
]
else:
leader = form_parts.staff_select()
rows += [('leader', leader.display(value=mg.staff.id) ),
('time', widgets.TextField(name="time", attrs=dict(value=mg.time)).display()),
('venue', widgets.TextField(name="venue", attrs=dict(value=mg.venue)).display()),
('max students', widgets.TextField(name="max_students", attrs=dict(value=mg.max_students)).display())
]
#one last non-editable field
rows +=[ ('current students', str(len(mg.students))) ]
table = custom_widgets.SimpleTableWidget(rows=rows)
if not allow_edit:
return table #just display
else:
#return form for editing
return custom_widgets.SimpleFormWidget(inner_widget=table, form_action=form_action,
submit_name=submit_name,
submit_value=submit_value)
@staticmethod
def module_groups(show_staff=True, show_module=False, show_students=True,
allow_delete=True, show_signup_link=False):
"table for displaying multiple ModuleGroups"
_fields=[('name', _module_groups_edit_link)]
if show_module:
_fields +=[('module', 'module')]
if show_staff:
_fields +=[('leader', lambda mg: "%s, %s" % (mg.staff.lastname, mg.staff.firstname))]
_fields+=[('time', 'time'), ('venue', 'venue')]
_fields+=[('max students', 'max_students')]
if show_students:
_fields +=[('actual students', 'nof_students')]
_fields +=[('students', lambda x: ", ".join([str(student) for student in x.students]))]
#_fields +=[('students', 'students')]
if allow_delete:
_fields +=[('delete group', _module_group_delete_link)]
if show_signup_link:
_fields +=[('join group', _module_group_join_link)]
return widgets.DataGrid(fields=_fields)
@staticmethod
def note(_note, show_student=True):
"returns a table that displays the specified note."
nlink = ElementTree.Element('a', href=_note.url)
nlink.text= str(_note.date)
rows=[('date', nlink)]
if show_student:
student = _note.student
slink = ElementTree.Element('a', href=student.url)
slink.text = str(student)
rows+=[('Student', slink)]
rows+=[('Author', _note.staff), ('confirmed', _note.is_confirmed),
('Subject', _note.subject), ('Text', _note.body)]
return custom_widgets.SimpleTableWidget(rows=rows)
@staticmethod
def notes(_notes, show_student=True):
"returns a table that displays the list of notes specified."
result = widgets.WidgetsList()
for _note in _notes:
result.append(tables.note(_note, show_student=show_student))
return result
@staticmethod
def module_assign(term, year, with_assign=False):
"""returns a table for displaying staff teaching modules, and for
assigning new staff to teach them.
Params term_id and year_id are needed because table displays staff to teach module
in the specified year/term
Param with_assign determines whether new staff can be assigned. Select option
for staff is limited to staff in current dept (as determined by webhelp.dept()).
NB where modules are assigned, the id of the select-one component is vital
because used on the other end for recovering which module staff are assigned to."""
_fields = []
def _module_leader_str(module):
module_leader = module.leader(term=term, year=year)
if module_leader is not None:
return "%s, %s" %(module_leader.lastname, module_leader.firstname)
return ""
_fields += [('code','code'), ('name','name'),
('current lecturer', _module_leader_str )]
#checkbox for making assignments
def _check_box(module):
"""to return checkbox for display in table.
The function accepts a module and returns a checkbox widget displayed.
The name of the checkbox is update_${module.id}.
The checkbox is unchecked"""
select_name = 'assign_%s' % module.id #name of select-one item defined below
return widgets.CheckBox(name="update_"+str(module.id),
attrs=dict(onClick='fix_opts(%s)' % select_name),
).display()
#select-one for selecting staff to assign
def _select_one(module):
"""see _check_box above
The name of the select-one is assign_${module.id}."""
#item = form_parts.staff_select(name='assign_%s' % module.id)
item = widgets.SingleSelectField(name='assign_%s' % module.id, label="none",
options=[],
attrs=dict(disabled=True),
validator=validators.NotEmpty())
return item.display()
if with_assign:
_fields += [('update',_check_box)]
_fields += [('new lecturer',_select_one)]
return widgets.DataGrid(fields=_fields)
components = widgets.DataGrid(fields=[('id','id'),('ordering','ordering'),('name','name')])
exam_marks = widgets.DataGrid(fields=[('year','year'),('module','module'),('cats','cats'),('mark','mark')])
@staticmethod
def tuple_table(list_of_headers, list_of_functions=[]):
"""creates a table where the data is a list of tuples"""
fields = []
for count, header in enumerate(list_of_headers):
def _getit(pos):
#use this rather than anonymous function directly because otherwise count is in scope of closure
return lambda tuple:tuple[pos]
def _getit2(pos):
def _inner(tuple):
try:
return apply(list_of_functions[pos], (tuple[pos],))
except Exception, e:
return "error (%s)" % e
return _inner
if count >= len(list_of_functions):
fields += [(header, _getit(count))]
else:
fields += [(header, _getit2(count))]
return widgets.DataGrid(fields=fields)