Page Source

from utils.display import Display, FileCopy, Database, Arguments, Modal
from utils.controls import *
from utils.web_exc import WebError
from utils import bounce, misc, perms, actions, shapes
import queries, debugging
from config import docroot
import string, os
import time, MySQLdb
import re # regular expression
from utils.output import *

ACTIVE_FLAG='9999-12-31 00:00:00.00'
ACTIVE_FLAG_SQL="""'9999-12-31'"""

class PowerEditControl(IfThenElseControl):
  """Draw NoEditControl if user does not have edit permissions,
  else draw TextFieldControl. So only instructors fall into the
  else category"""
  def __init__(self):
    elsecontrols = [NoEditControl(field="course",title="Course")]
    thencontrols = [SelectQueryControl(field="course2",title="Course",
			query=queries.Select(tables="courses",
			columns={"sys_value" : "concat(subject,course_num)",
				"user_value":  "concat(subject,course_num,': ',left(title,35))"},
			order=["sys_value"],
			where="retire_date="+ACTIVE_FLAG_SQL)), # currently active
		TextFieldControl(field="course_sec",title="Sec Num")]
    IfThenElseControl.__init__(self, thencontrols,elsecontrols)

  def output_form(self,display,container):
    self.thencontrols[0].default = display.fields['course2'] # Set a default
    IfThenElseControl.output_form(self,display,container)

  def condition_form(self, display):
    return display.req.login and perms.may(display.req.login, 'edit', 'schedules')

  def condition_disp(self, display):
    return 0                            # show all the information

# Always called with full set of arguments <page>/year/qtr/course
# course is like CS105-02
# If only one argument given assumed to be course and year and qtr becomes
# current qtr.
# This displays the info page for the sub-num-sec.
# Always instantiated in the single mode, never in Multiple mode.

class InstrControl(SelectQueryControl):
    """Output a comma seperated links to each instructors homepage"""

    def __init__(self):
        self.field = "instructors"
        self.title = "Instructors"
        self.query = None
        self.comment=""
        self.multi=1
        self.nbsppad=1
        self.size = None
        self.attrs = None

    def output_disp(self,display,container):
        """All the instructors are in display.instructors, as a list of dictionaries. Each dictionary has
        fullname, login, pseudo"""
	ans = []
	for dict in display.instructors:
	    if dict['pseudo']:
		op = dict['fullname']
	    else:
		op = '<A HREF="%(person:'+ dict['login']+ ')s">'
		op = op % display.urls()
		op = (op+ '%(fullname)s</A>') % dict
	    ans = ans + [op]
	container.append(string.join(ans,", "))

    def output_form(self,display,container):
	self.default = []
	for dict in display.instructors:
	    self.default.append(dict['login'])
	self.query = queries.Select(tables=["instructors as i","people as p"], 
				    columns={'sys_value' :'i.login',
				    'user_value' : "concat(fname,' ',mname,' ',lname)"},
				    where="i.login=p.login",
				    order=["lname"]
				    )
	SelectQueryControl.output_form(self,display,container)

class IsOldCourse(ConditionalControl):
    def __init__(self,datefield,text):
	self.datefield = datefield
	self.text = text

    def condition_disp(self,display):
	date = display.fields[self.datefield]
	# return None means show "Currently Active"
	if str(date) == ACTIVE_FLAG: return None
	# check if date < today (date in SQLformat)
	# I'm not sure if "9999-12-31 00:00:00.00" and "2005-9-28" are so comparable
	return date < misc.SqlToday()

    def met_output_disp(self,display,container):
	container.append(self.text % display.fields)

    def not_met_output_disp(self,display,container):
	container.append("&lt;Currently Active&gt;")

    def output_form(self,display,container):
	pass

    def input_form(self,display,form_fields):
	pass

class InfoDisplay(Display, Database, FileCopy, Arguments, Modal):
    single_controls = [ 
	"<table>",
	"<tr>",
	    '<td align="right">', "<b>Course&nbsp;:</b>", "</td>\n",
	     '<td align="left">', PowerEditControl(),"</td>\n",
	"</tr>\n",
	"<tr>",
	    '<td align="right">', "<b>Title&nbsp;:</b>", "</td>\n",
	    '<td align="left">', NoEditControl(field="title",title="Title"), "</td>\n",
	"</tr>\n",
	"<tr>",
	     '<td align="right">', "<b>Description&nbsp;:</b>", "</td>\n",
	      '<td align="left">', SingleLinkControl(
		url='%(sect:courses)s/description/%(!subject)s/%(!course_num)s/%(!retire_date)s',
		linktext='Click here for the course description',
		nbsppad=1
		),"</td>\n",
	"</tr>\n",
	"<tr>",
	      '<td align="right">', "<b>Instructor(s)&nbsp;:</b>","</td>\n",
	      '<td align="left">', InstrControl(), "</td>\n",
	"</tr>\n",
	"<tr>",
	     '<td align="right">',"<b>Class&nbsp;Schedule&nbsp;:</b>", "</td>\n",
	    '<td align="left">', TextFieldControl(field="timeday",title="Time of day"), " at ", 
	                         TextFieldControl(field="room",title="Room"), "</td>\n",
	"</tr>\n",
	"<tr>",
	    '<td align="right">', "<b>Course&nbsp;Homepage&nbsp;:</b>","</td>\n",
	    '<td align="left">', IfExistsControl(field="homepage",
						 thencontrols= IfShapeControl(
						     dispcontrols=[URLControl(field="homepage",
								linktext="Course Home Page")],
						     formcontrols=[TextFieldControl(field="homepage",
										    title="Home page",
										    comment="Absolute URLs please")]),
						 elsecontrols=["&lt;No Course home page given&gt;"]),
	    "</td>\n",
	"</tr>\n",
	"<tr>",
	     '<td align="right" valign="top">',"<b>Textbook(s)&nbsp;:</b>","</td>\n",
	     '<td align="left">', IfExistsControl(field="text_book",
						       elsecontrols="&lt;No textbook info given&gt;",
	                                               thencontrols=TextAreaControl(field="text_book",
										title="Textbook(s)",
                                                                                comment="")),
	     "<td>\n",
	"</tr>\n",
	"<tr>",
	    '<td align="right">',"<b> Course Retired&nbsp;:</b>", "</td>\n",
	    '<td align="left">',IsOldCourse("retire_date","Retired on %(retire_date)s"), "</td>\n",
	"</tr>\n",
	"<tr>",
	    '<td align="right">',"<b> Remarks&nbsp;:</b>", "</td>\n",
	    '<td align="left">', IfExistsControl(field="remarks",
						 thencontrols= [
						     "<i>",
						     TextAreaControl(field="remarks",title="Remarks",
						     comment="Remarks about the course"),
						     "</i>" ],
						 elsecontrols="&lt;None&gt;"
						 ), "</td>\n",
	"</tr>\n",
	"<tr>",
	    '<td align="right">',"<b> Announcement&nbsp;:</b>", "</td>\n",
	    '<td align="left">', IfExistsControl(field="extra_info",
						 thencontrols= [
						     "<i>",
						     TextAreaControl(field="extra_info",title="Extra Info",
						     comment="Important course specific info"),
						     "</i>" ],
						 elsecontrols="&lt;None&gt;"
						 ), "</td>\n",
	"</tr>\n",
	"</table>"
	]

    error_messages = {
        'bad_args'  : ("Insufficient data",
                      "Wrong number of arguments"),
        'bad_fmt'  : ("Insufficient data",
                      "Course Info insufficient or wrong format"),
        'edit_perm': ("Permission Denied",
                      "You don't have permission to edit this page"),
        'no_data'  : ("Data not found",
                      """Check course number or other arguments and try again.
                      If it still persists contact webmaster@cs.uchicago.edu"""),
        'no_course': ("Course does not exist",
                      "Reasons: No instructor / No course / course not active"),
        'bad_iden' : ("No such active course",
                      """You tried to change the course details of this schedule.
                      The changed value does not have a corresponding active course.
                      Contact webmaster@cs.uchicago.edu, and get the edit done manually"""),
        'sql_err'  : ("SQL_Error",
                      """Contact webmaster@cs.uchicago.edu, with details as
                      to how to reproduce the error""")}
    
    def unpack_course(self,course):
	# course is a string like "CS102-1" or "CMSC174-1". Return a 3-tuple CS,102,1
	regexp = '([a-zA-Z]+)(\d+)-(\d+)' # any # letters + any # digit + "-" + any # of digits
	MatchObj = re.match(regexp,course)
	if not MatchObj: # if not in the correct format
	    return None
	else: return MatchObj.groups()

    def SQLify(self,dict):
	# takes a dictionary and returns another one with same keys and values = sql_representvalue(old value)
	SQLargs = {}
	for k,v in dict.items():
	    SQLargs[k] = queries.represent_value(v)
	return SQLargs

    def process_arguments(self,form_data=1):
	"""parse the arguments got either thru cmd line and POST DATA
        unless form_data is set to None here"""
	self.args = {}
	qtr,year = misc.current_qtr()
	arg_names = [('year',year),('quarter',qtr),'course']
	if len(self.req.url.arguments) == 1: # only one argument, then it is course
	    self.args['course'] = self.req.url.arguments[0]
	    self.args['quarter'] = qtr
	    self.args['year'] = year
	else:
	    self.args = self.parse_arguments(arg_names,form_data=form_data)
	# All args should be present now except possibly for course
	if not self.args['course']: 
	    # if argument not given
	    raise WebError("bad_args{}")

	# Now that all the data is there add three more args for 'subject','number','section'
	# unpacked from 'course'
        unpacked = self.unpack_course(self.args['course'])
        if unpacked:
            self.args['subject'], self.args['number'], self.args['section'] = self.unpack_course(self.args['course'])
            # finally check if the number is three digit in which case need to add two zeroes.
            cnum = self.args['number']
            if cnum and len(cnum) == 3:
                self.args['number'] =cnum+"00"
        else:
            raise WebError ("bad_fmt{}")
        
    def perform_action(self):
        # This time ignore the form data else if the course information
        # has been modified we will get an error
	self.process_arguments(form_data=None)
        self.prep_database()
	if self.permit_mode('edit'):
	    self.perform_database_update()
	    actions.notify(region='schedules', data=self.args, user=self.req.login, type_op = "edit")
	    return "%(sect:courses)s/%(!year)s/%(!quarter)s" % self.req.urls(self.args) # Same page
	else:
	    raise WebError ("edit_perm{}")

    def permit_mode(self, mode):
	if ((mode == 'display') or      # display mode
		( mode == 'edit' and         # permitted to edit
                 perms.may(self.req.login,'edit','schedules'))):
            return 1
        # only other possibility is if mode=edit and login is one of the instructors
        if mode != 'edit': return 0
        for ins in self.instructors:
            if ins['login'] == self.req.login:
                return 1
        return None

    def add_nav_links(self,page):
      if perms.may(self.req.login,'edit','instructors'):
        page.add_navigation("%(sect:courses)s/instructors" % self.req.urls(),"Instructors List")

    def make_page(self,page):
	self.process_arguments()
	page.set_type("courses")
	page.set_title(self.get_single_title())
        self.prep_database()
	self.add_modes_to_page(page)
        self.add_nav_links(page)

        # deletion takes one-argument, then sched_id
	if self.mode == 'edit':
	    self.shape = shapes.FORM
	page.append(self.single_database())
        
        # If user can delete schedules, and currently in edit mode then present the link
        if perms.may(self.req.login,'delete','schedules') and self.mode== 'edit':
            url = "%(sect:courses)s/del_schedule/%(!sched_id)s" % self.req.urls(self.fields)
            page.append('<A HREF="'+url+'"> Delete this schedule</A>')

            
    def get_single_title(self):
	return "Course Information for %(course)s" % self.args

    def is_multiple(self): # only single mode
	return None 

    def db_rows(self):
	return self.query.count() # should be one

    def db_query(self):
	tables=["schedules as s", "people as p", "courses as c", "instructorship as i"]
	columns={"course" : "concat(s.subject,s.course_num,'-',s.course_sec)",
		 "course2" :"concat(s.subject,s.course_num)",
		 "subject" : "s.subject",
		 "course_sec" : "concat(s.course_sec)",
		 "course_num" :"s.course_num",
		 "homepage":"s.homepage",
		 "pseudo":"pseudo",
		 "title" :"c.title",
		 "login":"login",
		 "fullname" : "concat(fname,' ',mname,' ',lname)",
		 "lname":"lname",
		 "timeday":"s.timeday",
		 "room":"s.room",
		 "text_book" :"text_book",
		 "retired":"retired",
		 "remarks" :"remarks",
		 "extra_info" :"extra_info",
		 "retire_date" : "s.retire_date",
		 "sched_id" : "s.sched_id",
		 "num" : "count(*)" # number of instructors
		 }
	# most of the time, only one instructor in which case stored in schedules.
	wc = " i.instructor=login AND s.course_num=c.course_num AND s.subject=c.subject "
	wc = wc + " AND s.year=%(year)s AND s.quarter=%(quarter)s AND s.subject=%(subject)s "
	wc = wc + " AND s.course_num=%(number)s AND s.course_sec=%(section)s "
	wc = wc + " AND s.retire_date=c.retire_date "
	# Convert into SQL form, i.e. quote the strings...
	wc = wc % self.SQLify(self.args)
	groupby = "s.sched_id"
	q = queries.Select(tables=tables,columns = columns,where=wc,groupby=groupby)
	q.execute()
	if q.count() != 1:
	    raise WebError("no_data{}")
	self.query = q


    def db_fetch(self):
	self.fields = self.query.fetch()
        if self.fields['num'] > 1: # More than one instructor then get their info
	    tables=["instructorship","people"]
	    columns={ 'fullname' : "concat(fname,' ',mname,' ',lname)",
		      'login' : 'login',
		      'pseudo': 'pseudo or retired'}
	    wc = "login = instructor and sched_id=" 
	    wc = wc + queries.represent_value(self.fields['sched_id'])
	    q = queries.Select(tables=tables,columns=columns,where=wc)
	    q.execute()
	    self.instructors = q.fetchall() # get all the instructors
	else:
	    self.instructors = [{'fullname' : self.fields['fullname'],
				 'login': self.fields['login'],
				 'pseudo':self.fields['pseudo'] or display.fields['retired']}]
	return self.fields

    def wrong_input(self):
	# check if we are editing an existing record
	# i.e. course should exist -- need not be active though
	# if wrong returns 0 else returns the sched_id for the record to edit. 
	# Get the subject and other stuff from the URL arguments
	if not self.form_fields['instructors']: # no instructor selected
	    return None
	(sub,num,sec) = self.unpack_course(self.args['course'])
	ff = {}
	ff['subject'] = sub
	ff['course_num'] = num
	ff['course_sec'] = sec
	ff['year'] = int(self.args['year'])
	ff['quarter'] = self.args['quarter']

	where = "subject = %(subject)s and course_num = %(course_num)s and course_sec=%(course_sec)s"
	where = where + " and year=%(year)s and quarter=%(quarter)s "
	# search for a corresponding schedule
	columns = {"sched_id":"sched_id"}
	query = queries.Select(tables=["schedules"],columns=columns,
			       where= where % self.SQLify(ff))
	query.execute()

        # Does the record we are trying to change exist?
	if query.count() != 1:
            return None
        row = query.fetch() # All the old data of this schedule

	# if the course field is not there construct it from the other two.
        if not self.form_fields.has_key('course'):
            if not self.form_fields.has_key('course2'):
                self.form_fields['course'] = self.args['course']
            else:
	        self.form_fields['course'] = self.form_fields['course2'] + "-" + self.form_fields['course_sec']
	        del self.form_fields['course2']
	        del self.form_fields['course_sec']
        # are we trying to change the course identity?
        if self.form_fields['course'] == self.args['course']:
            # No then all is well
            # Remove the 'course' entry
            del self.form_fields['course']
            return row['sched_id']
        # We are trying to change the identity of the course
        # That's OK. But check if the new identity points to an
        # existing course which is active.
        # Active Version because we dont have access to any
        # retire_date info and we dont want the user to enter
        # such data.
        
        # Try to parse the new identity
        ff = self.unpack_course(self.form_fields['course'])
        if not ff: # Did not parse?
            raise WebError("bad_fmt{}")
        (sub,num,sec) = ff # Extract the parts
        # and save it as if this was entered separately by the user
        newdata = {}
        newdata['subject'] = sub
        newdata['course_num'] = num
        newdata['course_sec'] = sec
        # Now see if there is an active course with this data
        where = "subject = %(subject)s and course_num = %(course_num)s and "
        where = where + ("retire_date="+ACTIVE_FLAG_SQL)
        columns = {'subject' : 'subject'} # Something here
        query = queries.Select(tables=["courses"],columns=columns,
                               where = where % self.SQLify(newdata))
        query.execute()
        if query.count() != 1: # Does such a course exist?
            # No then tell user about the error
            raise WebError ("bad_iden{}")
        else:
            # Copy the new unpacked data into form_fields
            # and get rid of the packed data
            self.form_fields['subject'] = ff[0] # First entry
            self.form_fields['course_num'] = ff[1] # Second entry
            self.form_fields['course_sec'] = ff[2]
            del self.form_fields['course']
	    return row['sched_id'] # the sched_id

    def massage_form_fields(self):
	# massage form_fields into the right format
	if not self.form_fields.has_key('instructors'):
	    self.form_fields['instructors'] = None
	if type(self.form_fields['instructors']) == types.StringType: # only one instructor
	    self.form_fields['instructors'] = [self.form_fields['instructors']]

    def input_error(self):
	raise WebError ("no_course{}")

    def SQL_Error(self,stage,e):
	# stage is "update","delete" or "insert". E is exception object
	actions.notify(region='courses',
                       user=self.req.login,
                       type_op="edit",
                       data=self.args,
                       message="Stage = '%s' Error = '%s'" % (stage,str(e)))
	raise WebError ("sql_err{}")

    def db_update(self):
	self.massage_form_fields()
	try:
	    lock = queries.Lock(tables=["schedules","instructorship"])
	    id = self.wrong_input() # None if wrong input or the sched_id if correct
	    if not id: 
		self.input_error() # Gave wrong data
	    sqlid = queries.represent_value(id) # SQLify id

	    self.instructors = self.form_fields['instructors']
	    del self.form_fields['instructors'] # remove this from form_fields
	    # if only one instructor save it also in schedules for optimization
	    if len(self.instructors) == 1: #only one instructor
		self.form_fields['instructor'] = self.instructors[0]
	    else: self.form_fields['instructor'] = ""

	    # Update the schedules table first, all values in self.form_fields
	    upd = queries.Update(table="schedules",where="sched_id = %s" % sqlid, sets=self.form_fields)
	    try:
		upd.execute()
	    except MySQLdb.Error,e:
		self.SQL_Error("update",e)

	    # Delete all the instructors for this course in instructorship table
	    delq = queries.Delete(table="instructorship",where="sched_id = %s" % sqlid)
	    try:
		delq.execute()
	    except MySQLdb.Error,e:
		self.SQL_Error("delete",e)

	    # Add the new set of instructors
	    rows = []
	    for inst in self.instructors:
		rows.append({'instructor' : inst, 'sched_id' : id}) # SQLification done automagically
	    ins = queries.Insert(table="instructorship",rows=rows)
	    try:
		ins.execute()
	    except MySQLdb.Error,e:
		self.SQL_Error("insert",e)

	finally: # unlock the table, whatever happens, exception or no exception
	    lock.unlock()

def new(req):
    return InfoDisplay(req)