Initial commit

This commit is contained in:
Dan-in-CA 2013-08-25 16:02:56 -07:00
parent a5c2580b3f
commit 6139a27500
82 changed files with 12536 additions and 0 deletions

1
data/meta.txt Normal file
View File

@ -0,0 +1 @@
<meta name=viewport content="width=640">

5
gv.py Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/python
class gv:
"""An empty class for storing global vars."""
pass

BIN
gv.pyc Normal file

Binary file not shown.

953
ospi.py Normal file
View File

@ -0,0 +1,953 @@
#!/usr/bin/python
"""Updated 25/August/2013."""
import re, os, json, time, base64, thread # standard Python modules
import web # the Web.py module. See webpy.org (Enables the OpenSprinkler web interface)
import gv # 'global vars' An empty module, used for storing vars (as attributes), that need to be 'global' across threads.
import RPi.GPIO as GPIO # Required for accessing General Purpose Input Output pins on Raspberry Pi
#### urls is a feature of web.py. When a GET request is recieved , the corrisponding class is exicuted.
urls = [
'/', 'home',
'/cv', 'change_values',
'/vo', 'view_options',
'/co', 'change_options',
'/vs', 'view_stations',
'/cs', 'change_stations',
'/sn(\d+?\Z)', 'get_station', # regular expression, accepts any station number
'/sn(\d+?=\d(&t=\d+?\Z)?)', 'set_station', # regular expression, accepts any digits
'/vr', 'view_runonce',
'/cr', 'change_runonce',
'/vp', 'view_programs',
'/mp', 'modify_program',
'/cp', 'change_program',
'/dp', 'delete_program',
'/gp', 'graph_programs',
'/vl', 'view_log',
'/cl', 'clear_log',
'/lo', 'log_options',
'/rp', 'run_now',
]
#### Import ospi_addon module (ospi_addon.py) if it exists. ####
try:
import ospi_addon #This provides a stub for adding custom features to ospi.py as external modules.
except ImportError:
print 'add_on not imported'
#### Function Definitions ####
def baseurl():
"""Return URL app is running under."""
baseurl = web.ctx['home']
return baseurl
def board_rev():
"""Auto-detect the Raspberry Pi board rev."""
revision = "unknown"
with open('/proc/cmdline', 'r') as f:
line = f.readline()
m = re.search('bcm2708.boardrev=(0x[0123456789abcdef]*) ', line)
revision = m.group(1)
revcode = int(revision, 16)
if revcode <= 3:
rev = 1
else:
rev = 2
return rev
def clear_mm():
"""Clear manual mode settings."""
if gv.sd['mm']:
gv.sbits = [0] * (gv.sd['nbrd'] +1)
gv.ps = []
for i in range(gv.sd['nst']):
gv.ps.append([0,0])
gv.rs = []
for i in range(gv.sd['nst']):
gv.rs.append([0,0,0,0])
gv.srvals = [0]*(gv.sd['nst'])
set_output()
return
def CPU_temperature():
"""Returns the temperature of the Raspberry Pi's CPU."""
res = os.popen('vcgencmd measure_temp').readline()
return(res.replace("temp=","").replace("'C\n",""))
def log_run(datetime):
"""add run data to csv file - most recent first."""
if gv.lg:
snames = data('snames')
zones=re.findall(r"\'(.+?)\'",snames)
if gv.lrun[1] == 98:
pgr = 'Run-once'
elif gv.lrun[1] == 99:
pgr = 'Manual'
else:
pgr = str(gv.lrun[1])
datastr = (pgr +', '+str(zones[gv.lrun[0]])+', '+str(gv.lrun[2]/60)+'m'+str(gv.lrun[2]%60)+
's, '+time.strftime("%H:%M:%S, %a. %d %b %Y", time.localtime(datetime))+'\n')
f = open('./static/log/water_log.csv', 'r')
log = f.readlines()
f.close()
log.insert(1, datastr)
f = open('./static/log/water_log.csv', 'w')
if gv.lr:
f.writelines(log[:gv.lr+1])
else:
f.writelines(log)
f.close
return
def prog_match(now, prog):
"""Test a program for current date and time match."""
if not prog[0]: return 0 # Skip if program is not enabled
devday = int(now/86400) # Check day match
lt = time.localtime(now)
if (prog[1]>=128) and (prog[2]>1): #Inverval program
if (devday %prog[2]) != (prog[1] - 128): return 0
else: # Weekday program
if not prog[1]-128 & 1<<lt[6]: return 0
if prog[1]>=128 and prog[2] == 0: #even days
if lt[2]%2 != 0: return 0
if prog[1]>=128 and prog[2] == 1: #Odd days
if lt[2]==31 or (lt[1]==2 and lt[2]==29): return 0
elif lt[2]%2 !=1: return 0
this_minute = (lt[3]*60)+lt[4] # Check time match
if this_minute < prog[3] or this_minute > prog[4]: return 0
if prog[5] == 0: return 0
if ((this_minute - prog[3]) / prog[5]) * prog[5] == this_minute - prog[3]:
return 1 # Program matched
return 0
def schedule_stations(curr_time):
"""Schedule stattions/valves/zones to run."""
if gv.sd['rd']: # Skip if rain delay
return
if gv.sd['urs'] and gv.sd['rs']: # Skip if use rain sensor and rain detected.
return
accumulate_time = curr_time
if gv.sd['seq']: #sequential mode, stations run one after another
for sid in range(gv.sd['nst']):
if gv.rs[sid][2]: # if station has a duration value
gv.rs[sid][0] = accumulate_time # start at accumulated time
accumulate_time += gv.rs[sid][2] # add duration
gv.rs[sid][1] = accumulate_time # set new stop time
accumulate_time += gv.sd['sdt'] # add station delay
else: # concurrent mode, stations allowed to run in parallel
for sid in range(gv.sd['nst']):
if gv.rs[sid][2]and not gv.srvals[sid]: # if station has a duration value and is not running
gv.rs[sid][0] = accumulate_time # set start time
gv.rs[sid][1] = accumulate_time + gv.rs[sid][2] # Stop time = Start time + duration
gv.sd['bsy'] = 1
return
def stop_stations():
gv.srvals = [0]*(gv.sd['nst'])
set_output()
gv.ps = []
for i in range(gv.sd['nst']):
gv.ps.append([0,0])
gv.sbits = [0] * (gv.sd['nbrd'] +1)
gv.rs = []
for i in range(gv.sd['nst']):
gv.rs.append([0,0,0,0])
gv.sd['bsy'] = 0
return
def main_loop(): # Runs in a seperate thread
""" ***** Main algorithm.***** """
print 'Starting main loop \n'
last_min = 0
while True: # infinite loop
match = 0
now = time.time()
if gv.sd['en'] and not gv.sd['mm'] and (not gv.sd['bsy'] or not gv.sd['seq']) and not gv.sd['rd']:
lt = time.localtime(now)
if (lt[3]*60)+lt[4] != last_min: # only check programs once a minute
last_min = (lt[3]*60)+lt[4]
for i, p in enumerate(gv.pd): # get both index and prog item
if prog_match(now, p) and p[0] and p[6]: # check if program time matches now, is active, and has a duration
for b in range(gv.sd['nbrd']): # check each station
for s in range(8):
sid = b*8+s # station index
if sid+1 == gv.sd['mas']: continue # skip if this is master valve
if gv.srvals[sid]: continue # skip if currently on
if p[7+b]&1<<s: # if this station is scheduled in this program
gv.rs[sid][2] = p[6]*gv.sd['wl']/100 # duration scaled by water level
gv.rs[sid][3] = i+1 # store program number
gv.ps[sid][0] = i+1 # store program number for display
gv.ps[sid][1] = gv.rs[sid][2] # duration
match = True
if match:
schedule_stations(now) # turns on gv.sd['bsy']
if gv.sd['bsy']:
for b in range(gv.sd['nbrd']):
for s in range(8):
sid = b*8 + s # station index
if gv.srvals[sid]: # if this station is on
if now >= gv.rs[sid][1]: # check if time is up
gv.srvals[sid] = 0
set_output()
if gv.sd['mas']-1 != sid: # if not master, fill out log
gv.sbits[b] = gv.sbits[b]&~2**s
gv.ps[sid] = [0,0]
gv.lrun[0] = sid
gv.lrun[1] = gv.rs[sid][3]
gv.lrun[2] = int(now - gv.rs[sid][0])
gv.lrun[3] = now+((gv.sd['tz']/4)-12)*3600
log_run(now)
gv.pon = None # Program has ended
elif gv.sd['mas']-1 == sid:
gv.sbits[b] = gv.sbits[b]&~2**s
gv.rs[sid] = [0,0,0,0]
else: # if this station is not yet on
if now >= gv.rs[sid][0] and now < gv.rs[sid][1]:
if gv.sd['mas']-1 != sid: # if not master
gv.srvals[sid] = 1 # station is turned on
set_output()
gv.sbits[b] = gv.sbits[b]|2**s # Set display to on
gv.ps[sid][0] = gv.rs[sid][3]
gv.ps[sid][1] = gv.rs[sid][2]
if gv.sd['mas'] and gv.sd['mo'][b]&1<<(s-(s/8)*80): # and not gv.sd['mm'] and gv.sd['seq']: # Master settings
masid = gv.sd['mas'] - 1 # master index
gv.rs[masid][0] = gv.rs[sid][0] + gv.sd['mton']
gv.rs[masid][1] = gv.rs[sid][1] + gv.sd['mtoff']
gv.rs[masid][3] = gv.rs[sid][3]
elif gv.sd['mas'] == sid+1:
gv.sbits[b] = gv.sbits[b]|2**sid #(gv.sd['mas'] - 1)
gv.srvals[masid] = 1
set_output()
for s in range(gv.sd['nst']):
if gv.rs[s][1]: # if any station is running
program_running = True
gv.pon = gv.rs[s][3] # Store number of running program
break
program_running = False
gv.pon = None
if program_running:
if gv.sd['urs'] and gv.sd['rs']: # Stop stations if use rain sensor and rain detected.
stop_stations()
for idx in range(len(gv.ps)): # loop through program schedule (gv.ps)
if gv.ps[idx][1] == 0: # skip stations with no duration
continue
if gv.srvals[idx]: # If station is on, decrement time remaining
gv.ps[idx][1] -= 1
if gv.ps[idx][1] == 0:
gv.ps[idx][0] = 0
if not program_running:
gv.srvals = [0]*(gv.sd['nst'])
set_output()
gv.sbits = [0] * (gv.sd['nbrd'] +1)
gv.ps = []
for i in range(gv.sd['nst']):
gv.ps.append([0,0])
gv.rs = []
for i in range(gv.sd['nst']):
gv.rs.append([0,0,0,0])
gv.sd['bsy'] = 0
if gv.sd['mas'] and (gv.sd['mm'] or not gv.sd['seq']): # handle master for maual or consecutave mode.
mval = 0
for sid in range(gv.sd['nst']):
bid = sid/8
s = sid-bid*8
if gv.sd['mas'] != sid +1 and (gv.srvals[sid] and gv.sd['mo'][bid]&1<<s):
mval = 1
break
if not mval:
gv.rs[gv.sd['mas']-1][1] = time.time() # turn off master
if gv.sd['rd'] and now+((gv.sd['tz']/4)-12)*3600 >= gv.sd['rdst']:
gv.sd['rd'] = 0
gv.sd['rdst'] = 0 # Rain delay stop time
jsave(gv.sd, 'sd')
time.sleep(1)
#### End of main loop ####
def data(dataf):
"""Return contents of requested text file as string or create file if a missing config file."""
try:
f = open('./data/'+dataf+'.txt', 'r')
data = f.read()
f.close()
except IOError:
if dataf == 'options': ## A config file -- return defaults and create file if not found. ##
data = 'var opts=["Time zone:",0,48,1,"HTTP port:",0,80,12,"",0,0,13,"Ext. boards:",0,0,15,"Sequential:",1,1,16,"Station delay:",0,0,17,"Master station:",0,0,18,"Mas. on adj.:",0,0,19,"Mas. off adj.:",0,0,20,"Use rain sensor:",1,0,21,"Normally open:",1,1,22,"Water level (%):",0,100,23,"Ignore password:",1,0,25,0];var nopts=12,loc="";'
f = open('./data/'+dataf+'.txt', 'w')
f.write(data)
f.close()
elif dataf == 'snames': ## A config file -- return defaults and create file if not found. ##
data = "['S01','S02','S03','S04','S05','S06','S07','S08',]"
f = open('./data/'+dataf+'.txt', 'w')
f.write(data)
f.close()
else:
return None
return data
def save(dataf, datastr):
"""Save data to text file. dataf = file to save to, datastr = data string to save."""
f = open('./data/'+dataf+'.txt', 'w')
f.write(datastr)
f.close()
return
def jsave(data, fname):
"""Save data to a json file."""
f = open('./data/'+fname+'.json', 'w')
json.dump(data, f)
f.close()
def load_programs():
"""Load program data from json file if it exists into memory, otherwise create an empty programs var."""
try:
pf = open('./data/programs.json', 'r')
gv.pd = json.load(pf)
pf.close()
except IOError:
gv.pd = [] ## A config file -- return default and create file if not found. ##
pf = open('./data/programs.json', 'w')
json.dump(gv.pd, pf)
pf.close()
return gv.pd
def output_prog():
"""Converts program data to text string and outputs JavaScript vars used to display program page."""
lpd = []
dse = int((time.time()+((gv.sd['tz']/4)-12)*3600)/86400) # days since epoch
for p in gv.pd:
op = p[:] # Make local copy of each program
if op[1] >= 128 and op[2] > 1:
rel_rem = (((op[1]-128) + op[2])-(dse%op[2]))%op[2]
op[1] = rel_rem + 128
lpd.append(op)
progstr = 'var nprogs='+str(len(lpd))+',nboards='+str(gv.sd['nbrd'])+',ipas='+str(gv.sd['ipas'])+',mnp='+str(gv.sd['mnp'])+',pd=[];'
for i, pro in enumerate(lpd): #gets both index and object
progstr += 'pd['+str(i)+']='+str(pro).replace(' ', '')+';'
return progstr
##### GPIO #####
def set_output():
"""Activate triacs according to shift register state."""
disableShiftRegisterOutput()
setShiftRegister(gv.srvals) # gv.srvals stores shift register state
enableShiftRegisterOutput()
def to_sec(d=0, h=0, m=0, s=0):
"""Convert Day, Hour, minute, seconds to number of seconds."""
secs = d*86400
secs += h*3600
secs += m*60
secs += s
return secs
##################
#### Global vars #####
try:
sdf = open('./data/sd.json', 'r') ## A config file ##
gv.sd = json.load(sdf) #Settings Dictionary. A set of vars kept in memory and persisted in a file
sdf.close()
# test for missing or extra vars (update to current state)
gv.sd.pop('m0', None)
gv.sd.pop('m1', None)
gv.sd.pop('m2', None)
gv.sd.pop('m3', None)
if not 'mo' in gv.sd: gv.sd['mo'] = [0]
if not 'lg' in gv.sd: gv.sd['lg'] = 0
if not 'lr' in gv.sd: gv.sd['lr'] = 100
if not 'seq' in gv.sd: gv.sd['seq'] = 1
except IOError: # If file does not exist, create with defaults.
gv.sd = ({"en": 1, "seq": 1, "mnp": 32, "rsn": 0, "htp": 80, "nst": 8,
"rdst": 0, "loc": "", "tz": 48, "rs": 0, "rd": 0, "mton": 0,
"lr": "100", "sdt": 0, "mas": 0, "wl": 100, "bsy": 0, "lg": "checked",
"urs": 0, "nopts": 13, "pwd": "b3BlbmRvb3I=", "ipas": 0, "rst": 1,
"mm": 0, "mo": [0], "rbt": 0, "mtoff": 0, "nprogs": 1, "nbrd": 1})
sdf = open('./data/sd.json', 'w')
json.dump(gv.sd, sdf)
sdf.close()
try:
gv.lg = gv.sd['lg'] # Controlls logging
except KeyError:
pass
try:
gv.lr = int(gv.sd['lr'])
except KeyError:
pass
sdref = {'15':'nbrd', '16':'seq', '18':'mas', '21':'urs', '23':'wl', '25':'ipas'} #lookup table (Dictionary)
gv.srvals = [0]*(gv.sd['nst']) #Shift Register values
gv.rovals = [0]* gv.sd['nbrd']*7 #Run Once durations
gv.pd = load_programs() # Load program data from file
gv.ps = [] #Program schedule (used for UI diaplay)
for i in range(gv.sd['nst']):
gv.ps.append([0,0])
gv.pon = None #Program on (Holds program number of a running program
gv.sbits = [0] * (gv.sd['nbrd'] +1) # Used to display stations that are on in UI
gv.rs = [] #run schedule
for i in range(gv.sd['nst']):
gv.rs.append([0,0,0,0]) #scheduled start time, scheduled stop time, duration, program index
gv.lrun=[0,0,0,0] #station index, program number, duration, end time (Used in UI)
gv.scount = 0 # Station count, used in set station to track on stations with master association.
#### GPIO #####
GPIO.setwarnings(False)
#### pin defines ####
if board_rev() == 1:
pin_sr_dat = 21
else:
pin_sr_dat = 27
pin_sr_clk = 4
pin_sr_noe = 17
pin_sr_lat = 22
#### NUMBER OF STATIONS
num_stations = gv.sd['nst']
def enableShiftRegisterOutput():
GPIO.output(pin_sr_noe, False)
def disableShiftRegisterOutput():
GPIO.output(pin_sr_noe, True)
GPIO.cleanup()
#### setup GPIO pins to interface with shift register ####
GPIO.setmode(GPIO.BCM)
GPIO.setup(pin_sr_clk, GPIO.OUT)
GPIO.setup(pin_sr_noe, GPIO.OUT)
disableShiftRegisterOutput()
GPIO.setup(pin_sr_dat, GPIO.OUT)
GPIO.setup(pin_sr_lat, GPIO.OUT)
def setShiftRegister(srvals):
GPIO.output(pin_sr_clk, False)
GPIO.output(pin_sr_lat, False)
for s in range(num_stations):
GPIO.output(pin_sr_clk, False)
GPIO.output(pin_sr_dat, srvals[num_stations-1-s])
GPIO.output(pin_sr_clk, True)
GPIO.output(pin_sr_lat, True)
##################
#### Class Definitions ####
class home:
"""Open Home page."""
def GET(self):
homepg = '<!DOCTYPE html>\n'
homepg += data('meta')+'\n'
homepg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
homepg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
homepg += '<script>var baseurl=\"'+baseurl()+'\"</script>\n'
homepg += '<script>var ver=183,devt='+str(time.time()+((gv.sd['tz']/4)-12)*3600)+';var nbrd='+str(gv.sd['nbrd'])+',tz='+str(gv.sd['tz'])+';</script>\n'
homepg += '<script>var en='+str(gv.sd['en'])+',rd='+str(gv.sd['rd'])+',mm='+str(gv.sd['mm'])+',rdst='+str(gv.sd['rdst'])+',mas='+str(gv.sd['mas'])+',urs='+str(gv.sd['urs'])+',rs='+str(gv.sd['rs'])+',wl='+str(gv.sd['wl'])+',ipas='+str(gv.sd['ipas'])+',loc="'+str(gv.sd['loc'])+'";</script>\n'
homepg += '<script>var sbits='+str(gv.sbits).replace(' ', '')+',ps='+str(gv.ps).replace(' ', '')+';</script>\n'
homepg += '<script>var lrun='+str(gv.lrun).replace(' ', '')+';</script>\n'
homepg += '<script>var snames='+data('snames')+';</script>\n'
homepg += '<script>var cputemp='+str(9.0/5.0*int(float(CPU_temperature()))+32)+';</script>\n'
homepg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/home.js\"></script>'
return homepg
class change_values:
"""Save controller values, return browser to home page."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
if qdict.has_key('rsn') and qdict['rsn'] == '1':
stop_stations()
raise web.seeother('/')
return
if qdict.has_key('en') and qdict['en'] == '':
qdict['en'] = '1' #default
elif qdict.has_key('en') and qdict['en'] == '0':
gv.srvals = [0]*(gv.sd['nst']) # turn off all stations
set_output()
if qdict.has_key('mm') and qdict['mm'] == '0': clear_mm() #self.clear_mm()
if qdict.has_key('rd') and qdict['rd'] != '0':
gv.sd['rdst'] = ((time.time()+((gv.sd['tz']/4)-12)*3600)
+(int(qdict['rd'])*3600))
stop_stations()
elif qdict.has_key('rd') and qdict['rd'] == '0': gv.sd['rdst'] = 0
if qdict.has_key('rbt') and qdict['rbt'] == '1':
jsave(gv.sd, 'sd')
gv.srvals = [0]*(gv.sd['nst'])
set_output()
os.system('reboot')
raise web.seeother('/')
for key in qdict.keys():
try:
gv.sd[key] = int(qdict[key])
except:
pass
jsave(gv.sd, 'sd')
raise web.seeother('/')# Send browser back to home page
return
class view_options:
"""Open the options page for viewing and editing."""
def GET(self):
optpg = '<!DOCTYPE html>\n'
optpg += data('meta')+'\n'
optpg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
optpg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
optpg += '<script>var baseurl=\"'+baseurl()+'\"</script>\n'
optpg += '<script>var opts=["Time zone:",0,'+str(gv.sd['tz'])+',1,"HTTP port:",0,'+str(gv.sd['htp'])+',12,"",0,0,13,"Ext. boards:",\
0,'+str(gv.sd['nbrd']-1)+',15,"Sequential:",1,'+str(gv.sd['seq'])+',16,"Station delay:",0,'+str(gv.sd['sdt'])+',17,"Master station:",0,'+str(gv.sd['mas'])+',18,"Mas. on adj.:",0,'+str(gv.sd['mton'])+',19,"Mas. off adj.:",0,'+str(gv.sd['mtoff'])+',20,\
"Use rain sensor:",1,'+str(gv.sd['urs'])+',21,"Normally open:",1,'+str(gv.sd['rst'])+',22,"Water level (%):",0,'+str(gv.sd['wl'])+',23,\
"Ignore password:",1,'+str(gv.sd['ipas'])+',25,0];</script>\n'
optpg += '<script>var nopts='+str(gv.sd['nopts'])+',loc="'+str(gv.sd['loc'])+'";</script>\n'
optpg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/viewoptions.js\"></script>'
return optpg
class change_options:
"""Save changes to options made on the options page."""
def GET(self):
qdict = web.input()
try:
if not qdict.has_key('o25') and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
elif qdict.has_key('o25') and gv.sd['ipas'] == 0 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
elif qdict.has_key('o25') and gv.sd['ipas'] == 0 and qdict['pw'] == base64.b64decode(gv.sd['pwd']):
gv.sd['ipas'] = 1
except KeyError:
pass
try:
if qdict['cpw'] !='' and qdict['cpw'] == qdict['npw']:
gv.sd['pwd'] = base64.b64encode(qdict['npw'])
except KeyError:
pass
vstr = data('options')
ops = vstr.index('[')+1
ope = vstr.index(']')
optstr = vstr[ops:ope]
optlst = optstr.split(',')
onumlst = []
i=3
while i < len(optlst):
onumlst.append(optlst[i].replace(' ', ''))
if optlst[i-2] == '1': #clear check box items
optlst[i-1]= '0'
try:
sdref[optlst[i]];
gv.sd[sdref[optlst[i]]]=0
except KeyError:
pass
i+=4
for key in qdict.keys():
if key[:1] == 'o':
oidx = onumlst.index(key[1:])
if qdict[key] == 'on' or '':
qdict[key] = '1'
optlst[(oidx*4)+2] = qdict[key]
optstr = ','.join(optlst)
optstr = optstr.replace(', ', ',')
vstr = vstr.replace(vstr[ops:ope], optstr)
save('options', vstr)
if int(qdict['o15'])+1 != gv.sd['nbrd']: self.update_scount(qdict)
if int(qdict['o18']) != gv.sd['mas']:
clear_mm()
self.update_sd(qdict)
raise web.seeother('/')
#alert = '<script>alert("Options values saved.");window.location="/";</script>'
return #alert # -- Alerts are not considered good interface progrmming. Use sparingly!
def update_sd(self, qdict):
"""Transfer user input to vars."""
gv.sd['nbrd'] = int(qdict['o15'])+1
gv.sd['nst'] = gv.sd['nbrd']*8
gv.sd['sdt']= int(qdict['o17'])
gv.sd['mas'] = int(qdict['o18'])
gv.sd['mton']= int(qdict['o19'])
gv.sd['mtoff']= int(qdict['o20'])
gv.sd['tz'] = int(qdict['o1'])
if qdict.has_key('o16'): gv.sd['seq'] = int(qdict['o16'])
if qdict.has_key('o21'): gv.sd['urs'] = int(qdict['o21'])
gv.sd['wl'] = int(qdict['o23'])
if qdict.has_key('o25'): gv.sd['ipas'] = int(qdict['o25'])
gv.sd['loc'] = qdict['loc']
gv.srvals = [0]*(gv.sd['nst']) # Shift Register values
gv.rovals = [0]*(gv.sd['nst']) # Run Once Durations
jsave(gv.sd, 'sd')
return
def update_scount(self, qdict):
"""Increase or decrease the number of stations shown when expansion boards are added in options."""
if int(qdict['o15'])+1 > gv.sd['nbrd']: # Lengthen lists
incr = int(qdict['o15']) - (gv.sd['nbrd']-1)
for i in range(incr):
gv.sd['mo'].append(0)
snames = data('snames')
nlst = re.findall('[\'"].*?[\'"]', snames)
ln = len(nlst)
nlst.pop()
for i in range((incr*8)+1):
nlst.append("'S"+('%d'%(i+ln)).zfill(2)+"'")
nstr = '['+','.join(nlst)
nstr = nstr.replace("', ", "',")+",'']"
save('snames', nstr)
elif int(qdict['o15'])+1 < gv.sd['nbrd']: # Shorten lists
decr = gv.sd['nbrd'] - (int(qdict['o15'])+1)
gv.sd['mo'] = gv.sd['mo'][:(int(qdict['o15'])+1)]
snames = data('snames')
nlst = re.findall('[\'"].*?[\'"]', snames)
nstr = '['+','.join(nlst[:8+(int(qdict['o15'])*8)])+','']'
save('snames', nstr)
gv.srvals = [0] * (int(qdict['o15'])+1) * 8
gv.ps = []
for i in range((int(qdict['o15'])+1) * 8):
gv.ps.append([0,0])
gv.rs = []
for i in range((int(qdict['o15'])+1) * 8):
gv.rs.append([0,0,0,0])
gv.sbits = [0] * (int(qdict['o15'])+2)
return
class view_stations:
"""Open a page to view and edit station names and master associations."""
def GET(self):
stationpg = '<!DOCTYPE html>\n'
stationpg += data('meta')+'\n'
stationpg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
stationpg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
stationpg += '<script>var baseurl=\"'+baseurl()+'\"</script>\n'
stationpg += '<script>var nboards='+str(gv.sd['nbrd'])+',maxlen=12,mas='+str(gv.sd['mas'])+',ipas='+str(gv.sd['ipas'])+';</script>\n'
#stationpg += '<script>var masop='+str(gv.sd['mo'])+';</script>\n'
stationpg += '<script>var masop='+str(gv.sd['mo'])+',urs='+str(gv.sd['urs'])+',rsop='+str(0)+';</script>\n' ## added experimental urs
stationpg += '<script>snames='+data('snames')+';</script>\n'
stationpg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/viewstations.js\"></script>'
return stationpg
class change_stations:
"""Save changes to station names and master associations."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
for i in range(gv.sd['nbrd']): # capture master associations
if qdict.has_key('m'+str(i)):
try:
gv.sd['mo'][i] = int(qdict['m'+str(i)])
except ValueError:
gv.sd['mo'][i] = 0
names = '['
for i in range(gv.sd['nst']):
names += "'" + qdict['s'+str(i)] + "',"
names += ']'
save('snames', names.encode('ascii', 'backslashreplace'))
jsave(gv.sd, 'sd')
raise web.seeother('/')
return
class get_station:
"""Return a page containing a number representing the state of a station or all stations if 0 is entered as statin number."""
def GET(self, sn):
if sn == '0':
status = '<!DOCTYPE html>\n'
status += ''.join(str(x) for x in gv.srvals)
return status
elif int(sn)-1 <= gv.sd['nbrd']*7:
status = '<!DOCTYPE html>\n'
status += str(gv.srvals[int(sn)-1])
return status
else:
return 'Station '+sn+' not found.'
class set_station:
"""turn a station (valve/zone) on=1 or off=0 in manual mode."""
def GET(self, nst, t=None): # nst = station number, status, optional duration
nstlst = [int(i) for i in re.split('=|&t=', nst)]
if len(nstlst) == 2:
nstlst.append(0)
sid = int(nstlst[0])-1 # station index
b = sid/8 #board index
if nstlst[1] == 1 and gv.sd['mm']: # if status is on and manual mode is set
gv.rs[sid][0] = time.time() # set start time to current time
if nstlst[2]: # if an optional duration time is given
gv.rs[sid][2] = nstlst[2]
gv.rs[sid][1] = gv.rs[sid][0] + nstlst[2] # stop time = start time + duration
else:
gv.rs[sid][1] = float('inf') # stop time = infinity
gv.rs[sid][3] = 99 # set program index
gv.ps[sid][1] = nstlst[2]
gv.sd['bsy']=1
time.sleep(1.5)
if nstlst[1] == 0 and gv.sd['mm']: # If status is off
gv.rs[sid][1] = time.time()
time.sleep(1.5)
raise web.seeother('/')
class view_runonce:
"""Open a page to view and edit a run once program."""
def GET(self):
ropg = '<!DOCTYPE html>\n'
ropg += data('meta')+'\n'
ropg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
ropg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
ropg += '<script >var baseurl=\"'+baseurl()+'\"</script>\n'
ropg += '<script >var nboards='+str(gv.sd['nbrd'])+',mas='+str(gv.sd['mas'])+',ipas='+str(gv.sd['ipas'])+',dur='+str(gv.rovals).replace(' ', '')+';</script>\n'
ropg += '<script >snames='+data('snames')+';</script>\n'
ropg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/viewro.js\"></script>'
return ropg
class change_runonce:
"""Start a Run Once program. This wil. override any running program."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
if not gv.sd['en']: return # check operation status
gv.rovals = json.loads(qdict['t'])
gv.rovals.pop()
gv.ps = []
for i in range(gv.sd['nst']):
gv.ps.append([0,0])
gv.rs = [] #run schedule
for i in range(gv.sd['nst']): # clear run schedule
gv.rs.append([0,0,0,0])
ro_now = time.time()
for i, v in enumerate(gv.rovals):
if v: # if this element has a value
gv.rs[i][0] = ro_now
gv.rs[i][2] = v
gv.rs[i][3] = 98
gv.ps[i][0] = 98
gv.ps[i][1] = v
schedule_stations(ro_now)
raise web.seeother('/')
class view_programs:
"""Open programs page."""
def GET(self):
programpg = '<!DOCTYPE html>\n'
programpg += data('meta')+'\n'
programpg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
programpg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
programpg += '<script >var baseurl=\"'+baseurl()+'\"</script>\n'
programpg += '<script >'+output_prog()+'</script>\n'
programpg += '<script >snames='+data('snames')+';</script>\n'
programpg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/viewprog.js\"></script>'
return programpg
class modify_program:
"""Open page to allow program modification"""
def GET(self):
qdict = web.input()
modprogpg = '<!DOCTYPE html>\n'
modprogpg += data('meta')+'\n'
modprogpg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
modprogpg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
modprogpg += '<script >var baseurl=\"'+baseurl()+'\"</script>\n'
modprogpg += '<script >var nboards='+str(gv.sd['nbrd'])+',ipas='+str(gv.sd['ipas'])+';\n'
if qdict['pid'] != '-1':
mp = gv.pd[int(qdict['pid'])][:]
if mp[1] >= 128 and mp[2] > 1: # If this is an interval program
dse = int((time.time()-time.timezone)/86400)
rel_rem = (((mp[1]-128) + mp[2])-(dse%mp[2]))%mp[2] # Convert absolute to relative days remaining for display
mp[1] = rel_rem + 128
modprogpg += 'var pid='+qdict['pid']+', prog='+str(mp).replace(' ', '')+';</script>\n'
else:
modprogpg += 'var pid=-1;</script>\n'
modprogpg += '<script >var snames='+data('snames').replace(' ', '')+';</script>\n'
modprogpg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/modprog.js\"></script>'
return modprogpg
class change_program:
"""Add a program or modify an existing one."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
pnum = int(qdict['pid'])+1 # program number
cp = json.loads(qdict['v'])
if cp[0] == 0 and pnum == gv.pon: # if disabled and program is running
for i in range(len(gv.ps)):
if gv.ps[i][0] == pnum:
gv.ps[i] = [0,0]
if gv.srvals[i]:
gv.srvals[i] = 0
for i in range(len(gv.rs)):
if gv.rs[i][3] == pnum:
gv.rs[i] = [0,0,0,0]
if cp[1] >= 128 and cp[2] > 1:
dse = int((time.time()-time.timezone)/86400)
ref = dse + cp[1]-128
cp[1] = (ref%cp[2])+128
if int(qdict['pid']) > gv.sd['mnp']:
alert = '<script>alert("Maximum number of programs\n has been reached.");window.location="/";</script>'
return alert
elif qdict['pid'] == '-1': #add new program
gv.pd.append(cp)
else:
gv.pd[int(qdict['pid'])] = cp #replace program
jsave(gv.pd, 'programs')
gv.sd['nprogs'] = len(gv.pd)
raise web.seeother('/vp')
return
class delete_program:
"""Delete one or all existing program(s)."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
if qdict['pid'] == '-1':
del gv.pd[:]
jsave(gv.pd, 'programs')
else:
del gv.pd[int(qdict['pid'])]
jsave(gv.pd, 'programs')
gv.sd['nprogs'] = len(gv.pd)
raise web.seeother('/vp')
return
class graph_programs:
"""Open page to display program schedule"""
def GET(self):
qdict = web.input()
t = time.time()
lt = time.gmtime(t+((gv.sd['tz']/4)-12)*3600)
if qdict['d'] == '0': dd = str(lt.tm_mday)
else: dd = str(qdict['d'])
if qdict.has_key('m'): mm = str(qdict['m'])
else: mm = str(lt.tm_mon)
if qdict.has_key('y'): yy = str(qdict['y'])
else: yy = str(lt.tm_year)
graphpg = '<script >var baseurl=\"'+baseurl()+'\"</script>\n'
graphpg += '<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />\n'
graphpg += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
graphpg += ('<script >var mas='+str(gv.sd['mas'])+',wl='+str(gv.sd['wl'])+',sdt='+str(gv.sd['sdt'])+
',mton='+str(gv.sd['mton'])+',mtoff='+str(gv.sd['mtoff'])+',devday='+str(int(t/86400))+
',devmin='+str((lt.tm_hour*60)+lt.tm_min)+',dd='+dd+',mm='+mm+',yy='+yy+';var masop='+
str(gv.sd['mo'])+';'+output_prog()+'</script>\n')
graphpg += '<script>var seq='+str(gv.sd['seq'])+';</script>\n'
graphpg += '<script >var snames='+data('snames').replace(' ', '')+';</script>\n'
graphpg += '<script src=\"'+baseurl()+'/static/scripts/java/svc1.8.3/plotprog.js\"></script>'
return graphpg
class view_log:
def __init__(self):
self.render = web.template.render('templates/', globals={'sd':gv.sd})
def GET(self):
logf = open('static/log/water_log.csv')
records = logf.readlines()
logf.close()
data = []
for r in records:
t = r.split(', ')
t[1] = t[1].decode('unicode-escape')
data.append(t)
return self.render.log(data)
class clear_log:
"""Delete all log records"""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
f = open('./static/log/water_log.csv', 'w')
f.write('Program, Zone, Duration, Finish Time, Date'+'\n')
f.close
raise web.seeother('/vl')
return
class log_options:
"""Set log options from dialog."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
if qdict.has_key('log'): gv.sd['lg'] = "checked"
else: gv.sd['lg'] = ""
gv.lg = gv.sd['lg'] # necessary to make logging work correctly on Pi (see run_log())
gv.sd['lr'] = qdict['nrecords']
gv.lr = int(gv.sd['lr'])
jsave(gv.sd, 'sd')
raise web.seeother('/vl')
return
class run_now:
"""Run a scheduled program now. This will override any running programs."""
def GET(self):
qdict = web.input()
try:
if gv.sd['ipas'] != 1 and qdict['pw'] != base64.b64decode(gv.sd['pwd']):
raise web.unauthorized()
return
except KeyError:
pass
pid = int(qdict['pid'])
p = gv.pd[int(qdict['pid'])] # program data
if not p[0]: # if program is disabled
raise web.seeother('/vp')
stop_stations()
for b in range(gv.sd['nbrd']): # check each station
for s in range(8):
sid = b*8+s # station index
if sid+1 == gv.sd['mas']: continue # skip if this is master valve
if p[7+b]&1<<s: # if this station is scheduled in this program
gv.rs[sid][2] = p[6]*gv.sd['wl']/100 # duration scaled by water level
gv.rs[sid][3] = pid+1 # store program number in schedule
gv.ps[sid][0] = pid+1 # store program number for display
gv.ps[sid][1] = gv.rs[sid][2] # duration
schedule_stations(time.time())
raise web.seeother('/')
if __name__ == '__main__':
app = web.application(urls, globals())
thread.start_new_thread(main_loop, ())
app.run()

BIN
ospi.pyc Normal file

Binary file not shown.

19
ospi_addon.py Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/python
import ospi
#### Add any new page urls here ####
ospi.urls.extend(['/c1', 'ospi_addon.custom_page_1']) # example: (['/c1', 'ospi_addon.custom_page_1', '/c2', 'ospi_addon.custom_page_2', '/c3', 'ospi_addon.custom_page_3'])
#### add new functions and classes here ####
### Example custom class ###
class custom_page_1:
"""Add description here"""
def GET(self):
custpg = '<!DOCTYPE html>\n'
#Insert Custom Code here.
custpg += '<body>Hello form an ospi_addon program!</body>'
return custpg

BIN
ospi_addon.pyc Normal file

Binary file not shown.

38
sd_reference.txt Normal file
View File

@ -0,0 +1,38 @@
Vars held in the settings dict (gv.sd)
from controller values (cvalues):
en:1 enabled (operation)
rd:0 rain delay (hours)
mm:0 manual mode (bool)
rbt:0 reboot (bool)
from options:
htp:80 http port pi is using (o12 - not used)
seq:1 sequential/concurrent operation (o16)
sdt:0 station delay time(o17)
mton:0 master on delay (o19)
mtoff:0 master off delay (020)
nbrd:1 number of boards (o15)
tz:16 time zone (o1)
urs:0 use rain sensor (o21 - not used)
rst:1 Rain sensor type (normaly open =1, dafault, or closed, o22 - not used)
wl:100 water level (percent, o23)
mas:0 master station index (o18)
ipas:1 ignore passwprd (bool, o25)
pwd:"b3BlbmRvb3I=" encoded password (default shown here)
loc:"" location (for weather - not used)
rdst:0 rain delay stop time (unix time stamp)
rs:0 rain sensed (o22 - not used)
nopts:13 Number of optiions to be displayed
for scheduling:
bsy:0 program buisy
mnp:32 maximum number of programs
nprogs:0 number of programs (can be calculated form length of prog array)
mo:[0] master operation bytes - contains bits per board for stations with master set
rsn:0 Reset all stations
nst:8 number of stations
for logging:
lg:0 log runs if = "checked"
lr:100 limit number of log records to keep, 0 = no limit

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

1
static/log/water_log.csv Normal file
View File

@ -0,0 +1 @@
Program, Zone, Duration, Finish Time, Date
1 Program Zone Duration Finish Time Date

View File

@ -0,0 +1,48 @@
// Javascript for printing OpenSprinkler homepage
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
function w(s) {document.writeln(s);}
function link(s) {window.location=s;}
function linkn(s){window.open(s, '_blank');}
// input rain delay value
function setrd(form,idx) {var h=prompt("Enter hours to delay","0");if(h!=null){form.elements[idx].value=h;form.submit()};}
function imgstr(s) {return "<img src=\""+baseurl+"/static/images/icons/svc_"+s+".png\" height=20 align=absmiddle>&nbsp;";}
function datestr(t) {var _t=tz-48; return (new Date(t)).toUTCString()+((_t>=0)?"+":"-")+(Math.abs(_t)/4>>0)+":"+((Math.abs(_t)%4)*15/10>>0)+((Math.abs(_t)%4)*15%10);}
// print menu links
w("<button style=\"height:44\" onclick=link(\"/\")>"+imgstr("reset")+"Refresh</button>");
w("<button style=\"height:44\" onclick=link(\"/vo\")>"+imgstr("options")+"Options</button>");
w("<button style=\"height:44\" onclick=link(\"/vs\")>"+imgstr("edit")+"Stations</button>");
w("<button style=\"height:44\" onclick=link(\"/vp\")>"+imgstr("cal")+"Programs</button>");
//w("<button style=\"height:44\" onclick=linkn(\"http://igoogle.wunderground.com/cgi-bin/findweather/getForecast?query="+loc+"\")>"+imgstr("weather")+"Weather</button><p></p>");
w("<button style=\"height:44\" onclick=link(\"/vl\")>"+imgstr("log")+"Log</button><p></p>");
// print device information
if(ver>=100) w("<b>Firmware version</b>: "+(ver/100>>0)+"."+((ver/10>>0)%10)+"."+(ver%10)+"<br>");
else w("<b>Firmware version</b>: "+(ver/10>>0)+"."+(ver%10)+"<br>");
w("<b>Device time</b>: "+datestr(devt*1000)+"<br>");
if (typeof cputemp === 'undefined') cputemp="";
w("<b>CPU Temp</b>: "+cputemp+"&deg;F"+"<hr>");
w("<script type=\"text/javascript\" src=\""+baseurl+"/static/scripts/java/svc1.8.3/"+((mm)?"manualmode.js":"progmode.js")+"\"></script>");
// print status and other information
w("<br><b>Operation</b>: "+(en?("on").fontcolor("green"):("OFF").fontcolor("red")));
w("<br><b>Raindelay</b>: "+(rd?("ON").fontcolor("red")+" (till "+datestr(rdst*1000)+")":("off").fontcolor("black")));
w("<br><b>Rainsense</b>: "+(urs?(rs?("Rain Detected").fontcolor("red"):("no rain").fontcolor("green")):"<font color=gray>n/a</font>"));
w("<br><b>Water level</b>: <font color="+((wl==100)?"green":"red")+">"+wl+"\%</font>");
var lrsid=lrun[0],lrpid=lrun[1],lrdur=lrun[2],lret=lrun[3];
var pname="P"+lrpid;
if(lrpid==255||lrpid==99) pname="Manual Mode";
if(lrpid==254||lrpid==98) pname="Run-once Program";
dstr=(new Date(lret*1000)).toUTCString().replace(" GMT","");
if(lrpid!=0) w("<br><b>Log</b>: "+(snames[lrsid]+" ran "+pname+" for "+(lrdur/60>>0)+"m"+(lrdur%60)+"s @ "+dstr).fontcolor("gray"));
else w("<br><b>Log</b>: <font color=gray>n/a</font>");
w("<hr>");
// print html form
w("<form name=hf action=cv method=get><p>Password:<input type=password "+(ipas?"disabled":"")+" size=10 id=pwd name=pw></p>");
w("<input type=hidden name=en><input type=hidden name=rd value=0><input type=hidden name=rbt value=0><input type=hidden name=mm value=0></form>");
w("<button style=\"height:36\" onclick=\"hf.elements[1].value="+(1-en)+";hf.submit();\">"+imgstr(en?"stop":"start")+(en?"Stop Operation":"Start Operation")+"</button>");
w("<button style=\"height:36\" onclick=\"hf.elements[4].value="+(1-mm)+";hf.submit();\">"+imgstr(mm?"auto":"manual")+(mm?"Manual Off":"Manual On")+"</button>");
w("<button style=\"height:36\" onclick=\"setrd(hf,2)\">"+imgstr("rain")+"Rain Delay</button>");
w("<button style=\"height:36\" onclick=\"hf.elements[3].value=1;hf.submit();\">"+imgstr("reboot")+"Reboot</button>");
w("<p></p><hr><br>");

View File

@ -0,0 +1,42 @@
// Javascript for printing OpenSprinkler homepage (manual mode)
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
function id(s) {return document.getElementById(s);}
function snf(sid,sbit) {
if(sbit==1) window.location="/sn"+(sid+1)+"=0"; // turn off station
else {
var strmm=id("mm"+sid).value, strss=id("ss"+sid).value;
var mm=(strmm=="")?0:parseInt(strmm);
var ss=(strss=="")?0:parseInt(strss);
if(!(mm>=0&&ss>=0&&ss<60)) {alert("Timer values wrong: "+strmm+":"+strss);return;}
window.location="/sn"+(sid+1)+"=1"+"&t="+(mm*60+ss); // turn it off with timer
}
}
w("<b>Manual Control:</b> (timer is optional)<p></p>");
w("<table border=1>");
var bid,s,sid,sn,rem,remm,rems,sbit;
for(bid=0;bid<nbrd;bid++){
for(s=0;s<8;s++){
w("<tr><td bgcolor='#E4E4E4'>");
sid=bid*8+s;
sn=sid+1;
//w("Station "+(sn/10>>0)+(sn%10)+": ");
w(snames[sid]+":&nbsp;&nbsp;</td><td>");
if(sn==mas) {w(((sbits[bid]>>s)&1?("<b>On</b>").fontcolor("green"):("Off").fontcolor("black"))+" (<b>Master</b>)");}
else {
rem=ps[sid][1];
if(rem>65536) rem=0;
remm=rem/60>>0;rems=rem%60;sbit=(sbits[bid]>>s)&1;
var bg=(sbit?"#FFCCCC":"#CCFFCC"),tx=(sbit?"off":"on"),dis=(sbit?"disabled":"");
w("<button style=\"width:100px;height:32px;background-color:"+bg+";border-radius:8px;\" id=bb"+sid+" onclick=\"snf("+sid+","+sbit+")\">Turn "+tx+"</button>");
w(sbit?" in ":" with timer ");
w("<input type=text id=mm"+sid+" size=2 maxlength=3 value="+remm+" "+dis+" />:");
w("<input type=text id=ss"+sid+" size=2 maxlength=2 value="+rems+" "+dis+" /> (mm:ss)");
}
w("</td>");
}
}
w("</table>");

View File

@ -0,0 +1,142 @@
// Javascript for printing OpenSprinkler modify program page
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
function w(s) {document.writeln(s);}
function id(s){return document.getElementById(s);}
function imgstr(s) {return "<img src=\""+baseurl+"/static/images/icons/svc_"+s+".png\" height=20 align=absmiddle>&nbsp;";}
// parse time
function parse_time(prefix) {
var h=parseInt(id(prefix+"h").value,10);
var m=parseInt(id(prefix+"m").value,10);
if(!(h>=0&&h<24&&m>=0&&m<60)) {alert("Error: Incorrect time input "+prefix+".");return -1;}
return h*60+m;
}
// fill time
function fill_time(prefix,idx) {
var t=prog[idx];
id(prefix+"h").value=""+((t/60>>0)/10>>0)+((t/60>>0)%10);
id(prefix+"m").value=""+((t%60)/10>>0)+((t%60)%10);
}
// check/uncheck all days
function seldays(v) {
var i;
for(i=0;i<7;i++) id("d"+i).checked=(v>0)?true:false;
}
// handle form submit
function fsubmit(f) {
var errmsg = "",days=[0,0],i,s,sid;
var en=0;
if(id("en_on").checked) en=1;
// process days
if(id("days_week").checked) {
for(i=0;i<7;i++) {if(id("d"+i).checked) {days[0] |= (1<<i); }}
if(id("days_odd").checked) {days[0]|=0x80; days[1]=1;}
else if(id("days_even").checked) {days[0]|=0x80; days[1]=0;}
} else if(id("days_n").checked) {
days[1]=parseInt(id("dn").value,10);
if(!(days[1]>=2&&days[1]<=128)) {alert("Error: interval days must be between 2 and 128.");return;}
days[0]=parseInt(id("drem").value,10);
if(!(days[0]>=0&&days[0]<days[1])) {alert("Error: starting in days wrong.");return;}
days[0]|=0x80;
}
if(days[0]==0||(days[1]<2&&(days[0]&0x7f)==0)) {alert("Error: You have not selected any day.");return;}
// process stations
var stations=[0],station_selected=0,bid;
for(bid=0;bid<nboards;bid++) {
stations[bid]=0;
for(s=0;s<8;s++) {
sid=bid*8+s;
if(id("s"+sid).checked) {
stations[bid] |= 1<<s; station_selected=1;
}
}
}
if(station_selected==0) {alert("Error: You have not selected any station.");return;}
// process time
var start_time,end_time,interval,duration;
if((start_time=parse_time("ts")) < 0) return;
if((end_time=parse_time("te")) < 0) return;
if(!(start_time<end_time)) {alert("Error: Start time must be prior to end time.");return;}
if((interval=parse_time("ti")) < 0) return;
var dm=parseInt(id("tdm").value,10);
var ds=parseInt(id("tds").value,10);
duration=dm*60+ds;
if(!(dm>=0&&ds>=0&&ds<60&&duration>0)) {alert("Error: Incorrect duration.");return;}
// password
var p="";
if(!ipas) p=prompt("Please enter your password:","");
if(p!=null){
f.elements[0].value=p;
f.elements[1].value=pid;
f.elements[2].value="["+en+","+days[0]+","+days[1]+","+start_time+","+end_time+","+interval+","+duration;
for(i=0;i<nboards;i++) {f.elements[2].value+=","+stations[i];}
f.elements[2].value+="]";
f.submit();
}
}
// handle form cancel
function fcancel() {window.location="/vp";}
// print html form
w("<div style=\"padding-top:10px;padding-bottom:0px;\"><b>"+((pid>-1)?"Modify Program "+(pid+1):"Add a New Program")+"</b></div><hr>");
w("<form name=mf action=cp method=get><input type=hidden name=pw><input type=hidden name=pid><input type=hidden name=v>");
w("<div style=\"padding-left:5px;padding-right:5px;\">");
w("<p><b>This program is: </b><input type=radio name=rad_en id=en_on><b>On</b><input type=radio name=rad_en id=en_off><b>Off</b></p>");
w("<p><b>Select Days:</b></p><input type=radio name=rad_day id=days_week><b><u>Weekly</u>:</b><input type=checkbox id=d0>Mon<input type=checkbox id=d1>Tue<input type=checkbox id=d2>Wed<input type=checkbox id=d3>Thu<input type=checkbox id=d4>Fri<input type=checkbox id=d5>Sat<input type=checkbox id=d6>Sun<br>")
w("<div style=\"padding-left:80px;\"><button onclick=\"seldays(1);return false;\">Select All</button><button onclick=\"seldays(0);return false;\">Select None</button></div>");
w("<div style=\"padding-left:20px;\"><p><b>Select Restrictions:</b><br><input type=radio name=rad_rst id=days_norst>No restriction<br><input type=radio name=rad_rst id=days_odd>Odd days only (except 31st and Feb 29th)<br><input type=radio name=rad_rst id=days_even>Even days only<br></p></div>");
w("<input type=radio name=rad_day id=days_n><b><u>Interval</u>:</b> Every <input type=text size=2 maxlength=2 id=dn> days, starting in <input type=text size=2 maxlength=2 id=drem> days.<hr>");
w("<p><b>Select Stations:</b></p>");
w("<table border=1 cellpadding=3>");
var bid,s,sid;
for(bid=0;bid<nboards;bid++) {
for(s=0;s<8;s++) {
sid=bid*8+s;
if(sid%4==0) w("<tr>");
w("<td>");
w("<input type=checkbox id=s"+sid+">"+snames[sid]);
w("</td>");
if(sid%4==3) w("</tr>");
}
}
w("</table><p></p><hr>");
w("<p></p><b>Time:</b> <input type=text size=2 maxlength=2 id=tsh> : <input type=text size=2 maxlength=2 id=tsm> -> <input type=text size=2 maxlength=2 id=teh> : <input type=text size=2 maxlength=2 id=tem> (hh:mm)<br><b>Every:</b> <input type=text size=2 maxlength=2 id=tih> hours <input type=text size=2 maxlength=2 id=tim> minutes <br><b>Duration:</b> <input type=text size=2 maxlength=3 id=tdm> minutes <input type=text size=2 maxlength=2 id=tds> seconds<p></p><hr>");
w("</div></form>");
w("<button style=\"height:36\" onclick=\"fsubmit(mf)\">"+imgstr("submit")+"<b>Submit</b></button>");
w("<button style=\"height:36\" onclick=\"fcancel()\">"+imgstr("delall")+"Cancel</button>");
// default values
id("en_on").checked=true;
id("days_week").checked=true;id("days_norst").checked=true;
id("dn").value="3";id("drem").value="0";
id("tsh").value="06";id("tsm").value="00";id("teh").value="18";id("tem").value="00";
id("tih").value="04";id("tim").value="00";id("tdm").value="15";id("tds").value="00";
// fill in existing program values
if(pid>-1) {
if(prog[0]==0) id("en_off").checked=true;
// process days
var _days=[prog[1],prog[2]];
if((_days[0]&0x80)&&(_days[1]>1)) {
id("days_n").checked=true;
id("dn").value=_days[1];id("drem").value=_days[0]&0x7f;
} else {
id("days_week").checked=true;
for(i=0;i<7;i++) {if(_days[0]&(1<<i)) id("d"+i).checked=true;}
if((_days[0]&0x80)&&(_days[1]==0)) {id("days_even").checked=true;}
if((_days[0]&0x80)&&(_days[1]==1)) {id("days_odd").checked=true;}
}
// process time
fill_time("ts",3);
fill_time("te",4);
fill_time("ti",5);
var t=prog[6];
id("tdm").value=""+((t/60>>0)/10>>0)+((t/60>>0)%10);
id("tds").value=""+((t%60)/10>>0)+((t%60)%10);
// process stations
var bits;
for(bid=0;bid<nboards;bid++) {
bits=prog[bid+7];
for(s=0;s<8;s++) {sid=bid*8+s;id("s"+sid).checked=(bits&(1<<s)?true:false);}
}
}

View File

@ -0,0 +1,164 @@
// Javascript for printing OpenSprinkler schedule page
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
// colors to draw different programs
var prog_color=["rgba(0,0,200,0.5)","rgba(0,200,0,0.5)","rgba(200,0,0,0.5)","rgba(0,200,200,0.5)"];
var days_str=["Sun","Mon","Tue","Wed","Thur","Fri","Sat"];
var xstart=80,ystart=80,stwidth=40,stheight=180;
var winwidth=stwidth*nboards*8+xstart, winheight=26*stheight+ystart;
var sid,sn,t;
var simt=Date.UTC(yy,mm-1,dd,0,0,0,0);
var simdate=new Date(simt);
var simday = (simt/1000/3600/24)>>0;
function w(s) {document.writeln(s);}
function check_match(prog,simminutes,simdate,simday) {
// simdate is Java date object, simday is the #days since 1970 01-01
var wd,dn,drem;
if(prog[0]==0) return 0;
if ((prog[1]&0x80)&&(prog[2]>1)) { // inverval checking
dn=prog[2];drem=prog[1]&0x7f;
if((simday%dn)!=((devday+drem)%dn)) return 0; // remainder checking
} else {
wd=(simdate.getUTCDay()+6)%7; // getDay assumes sunday is 0, converts to Monday 0
if((prog[1]&(1<<wd))==0) return 0; // weekday checking
dt=simdate.getUTCDate(); // day of the month
if((prog[1]&0x80)&&(prog[2]==0)) {if((dt%2)!=0) return 0;} // even day checking
if((prog[1]&0x80)&&(prog[2]==1)) { // odd day checking
if(dt==31) return 0;
else if (dt==29 && simdate.getUTCMonth()==1) return 0;
else if ((dt%2)!=1) return 0;
}
}
if(simminutes<prog[3] || simminutes>prog[4]) return 0; // start and end time checking
if(prog[5]==0) return 0;
if(((simminutes-prog[3])/prog[5]>>0)*prog[5] == (simminutes-prog[3])) { // interval checking
return 1;
}
return 0; // no match found
}
function getx(sid) {return xstart+sid*stwidth-stwidth/2;} // x coordinate given a station
function gety(t) {return ystart+t*stheight/60;} // y coordinate given a time
function getrunstr(start,end){ // run time string
var h,m,s,str;
h=start/3600>>0;m=(start/60>>0)%60;s=start%60;
str=""+(h/10>>0)+(h%10)+":"+(m/10>>0)+(m%10)+":"+(s/10>>0)+(s%10);
h=end/3600>>0;m=(end/60>>0)%60;s=end%60;
str+="->"+(h/10>>0)+(h%10)+":"+(m/10>>0)+(m%10)+":"+(s/10>>0)+(s%10);
return str;
}
function plot_bar(sid,start,pid,end) { // plot program bar
w("<div title=\""+snames[sid]+" ["+getrunstr(start,end)+"]\" align=\"center\" style=\"position:absolute;background-color:"+prog_color[(pid+3)%4]+";left:"+getx(sid)+";top:"+gety(start/60)+";border:0;width:"+stwidth+";height:"+((end-start)/60*stheight/60)+"\">P"+pid+"</div>");
}
function plot_master(start,end) { // plot master station
w("<div title=\"Master ["+getrunstr(start,end)+"]\" style=\"position:absolute;background-color:#CCCC80;left:"+getx(mas-1)+";top:"+gety(start/60)+";border:0;width:"+stwidth+";height:"+((end-start)/60*stheight/60)+"\"></div>");
//if(mas==0||start==end) return;
//ctx.fillStyle="rgba(64,64,64,0.5)";
//ctx.fillRect(getx(mas-1),gety(start/60),stwidth,(end-start)/60*stheight/60);
}
function plot_currtime() {
w("<div style=\"position:absolute;left:"+(xstart-stwidth/2-10)+";top:"+gety(devmin)+";border:1px solid rgba(200,0,0,0.5);width:"+(winwidth-xstart+stwidth/2)+";height:0;\"></div>");
}
function run_sched(simseconds,st_array,pid_array,et_array) { // run and plot schedule stored in array data
var sid,endtime=simseconds;
for(sid=0;sid<nboards*8;sid++) {
if(pid_array[sid]) {
if(seq==1) { // sequential
plot_bar(sid,st_array[sid],pid_array[sid],et_array[sid]);
if((mas>0)&&(mas!=sid+1)&&(masop[sid>>3]&(1<<(sid%8))))
plot_master(st_array[sid]+mton, et_array[sid]+mtoff-60);
endtime=et_array[sid];
} else { // concurrent
plot_bar(sid,simseconds,pid_array[sid],et_array[sid]);
// check if this station activates master
if((mas>0)&&(mas!=sid+1)&&(masop[sid>>3]&(1<<(sid%8))))
endtime=(endtime>et_array[sid])?endtime:et_array[sid];
}
}
}
if(seq==0&&mas>0) plot_master(simseconds,endtime);
return endtime;
}
function draw_title() {
w("<div align=\"center\" style=\"background-color:#EEEEEE;position:absolute;left:0px;top:10px;border:2px solid gray;padding:5px 0px;width:"+(winwidth)+";border-radius:10px;box-shadow:3px 3px 2px #888888;\"><b>Program Preview of</b>&nbsp;");
w(days_str[simdate.getUTCDay()]+" "+(simdate.getUTCMonth()+1)+"/"+(simdate.getUTCDate())+" "+(simdate.getUTCFullYear()));
w("<br><font size=2>(Hover over each colored bar to see tooltip)</font>");
w("</div>");
}
function draw_grid() {
// draw table and grid
for(sid=0;sid<=nboards*8;sid++) {
sn=sid+1;
if(sid<nboards*8) w("<div style=\"position:absolute;left:"+(xstart+sid*stwidth-10)+";top:"+(ystart-15)+";width:"+stwidth+";height:20;border:0;padding:0;\"><font size=2>S"+(sn/10>>0)+(sn%10)+"</font></div>");
w("<div style=\"position:absolute;left:"+getx(sid)+";top:"+(ystart-10)+";border:1px solid gray;width:0;height:"+(winheight-ystart+30)+";\"></div>");
}
// horizontal grid, time
for(t=0;t<=24;t++) {
w("<div style=\"position:absolute;left:"+(xstart-stwidth/2-15)+";top:"+gety(t*60)+";border:1px solid gray;width:15;height:0;\"></div>");
w("<div style=\"position:absolute;left:"+(xstart-stwidth/2-8)+";top:"+(gety(t*60)+stheight/2)+";border:1px solid gray;width:8;height:0;\"></div>");
w("<div style=\"position:absolute;left:"+(xstart-70)+";top:"+(ystart+t*stheight-7)+";width=70;height:20;border:0;padding:0;\"><font size=2>"+(t/10>>0)+(t%10)+":00</font></div>");
}
plot_currtime();
}
function draw_program() {
// plot program data by a full simulation
var simminutes=0,busy=0,match_found=0,bid,s,sid,pid,match=[0,0];
var st_array=new Array(nboards*8),pid_array=new Array(nboards*8);
var et_array=new Array(nboards*8);
for(sid=0;sid<nboards*8;sid++) {
st_array[sid]=0;pid_array[sid]=0;et_array[sid]=0;
}
do { // check through every program
busy=0;
match_found=0;
for(pid=0;pid<nprogs;pid++) {
var prog=pd[pid];
if(check_match(prog,simminutes,simdate,simday)) {
for(sid=0;sid<nboards*8;sid++) {
bid=sid>>3;s=sid%8;
if(mas==(sid+1)) continue; // skip master station
if(prog[7+bid]&(1<<s)) {
et_array[sid]=prog[6]*wl/100>>0;pid_array[sid]=pid+1;
match_found=1;
}//if
}//for_sid
}//if_match
}//for_pid
if(match_found) {
var acctime=simminutes*60;
if(seq) { // sequential
for(sid=0;sid<nboards*8;sid++) {
if(et_array[sid]) {
st_array[sid]=acctime;acctime+=et_array[sid];
et_array[sid]=acctime;acctime+=sdt;
busy=1;
}//if
}//for
} else {
for(sid=0;sid<nboards*8;sid++) {
if(et_array[sid]) {
st_array[sid]=simminutes*60;
et_array[sid]=simminutes*60+et_array[sid];
busy=1;
}//if(et_array)
}//for(sid)
}//else(seq)
}//if(match_found)
if (busy) {
var endminutes=run_sched(simminutes*60,st_array,pid_array,et_array)/60>>0;
if(seq&&simminutes!=endminutes) simminutes=endminutes;
else simminutes++;
for(sid=0;sid<nboards*8;sid++) {st_array[sid]=0;pid_array[sid]=0;et_array[sid]=0;} // clear program data
} else {
simminutes++; // increment simulation time
}
} while(simminutes<24*60); // simulation ends
window.scrollTo(0,gety((devmin/60>>0)*60)); // scroll to the hour line cloest to the current time
}
draw_title();
draw_grid();
draw_program();

View File

@ -0,0 +1,164 @@
// Javascript for printing OpenSprinkler schedule page
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
// colors to draw different programs
var prog_color=["rgba(0,0,200,0.5)","rgba(0,200,0,0.5)","rgba(200,0,0,0.5)","rgba(0,200,200,0.5)"];
var days_str=["Sun","Mon","Tue","Wed","Thur","Fri","Sat"];
var xstart=80,ystart=80,stwidth=40,stheight=180;
var winwidth=stwidth*nboards*8+xstart, winheight=26*stheight+ystart;
var sid,sn,t;
var simt=Date.UTC(yy,mm-1,dd,0,0,0,0);
var simdate=new Date(simt);
var simday = (simt/1000/3600/24)>>0;
function w(s) {document.writeln(s);}
function check_match(prog,simminutes,simdate,simday) {
// simdate is Java date object, simday is the #days since 1970 01-01
var wd,dn,drem;
if(prog[0]==0) return 0;
if ((prog[1]&0x80)&&(prog[2]>1)) { // inverval checking
dn=prog[2];drem=prog[1]&0x7f;
if((simday%dn)!=((devday+drem)%dn)) return 0; // remainder checking
} else {
wd=(simdate.getUTCDay()+6)%7; // getDay assumes sunday is 0, converts to Monday 0
if((prog[1]&(1<<wd))==0) return 0; // weekday checking
dt=simdate.getUTCDate(); // day of the month
if((prog[1]&0x80)&&(prog[2]==0)) {if((dt%2)!=0) return 0;} // even day checking
if((prog[1]&0x80)&&(prog[2]==1)) { // odd day checking
if(dt==31) return 0;
else if (dt==29 && simdate.getUTCMonth()==1) return 0;
else if ((dt%2)!=1) return 0;
}
}
if(simminutes<prog[3] || simminutes>prog[4]) return 0; // start and end time checking
if(prog[5]==0) return 0;
if(((simminutes-prog[3])/prog[5]>>0)*prog[5] == (simminutes-prog[3])) { // interval checking
return 1;
}
return 0; // no match found
}
function getx(sid) {return xstart+sid*stwidth-stwidth/2;} // x coordinate given a station
function gety(t) {return ystart+t*stheight/60;} // y coordinate given a time
function getrunstr(start,end){ // run time string
var h,m,s,str;
h=start/3600>>0;m=(start/60>>0)%60;s=start%60;
str=""+(h/10>>0)+(h%10)+":"+(m/10>>0)+(m%10)+":"+(s/10>>0)+(s%10);
h=end/3600>>0;m=(end/60>>0)%60;s=end%60;
str+="->"+(h/10>>0)+(h%10)+":"+(m/10>>0)+(m%10)+":"+(s/10>>0)+(s%10);
return str;
}
function plot_bar(sid,start,pid,end) { // plot program bar
w("<div title=\""+snames[sid]+" ["+getrunstr(start,end)+"]\" align=\"center\" style=\"position:absolute;background-color:"+prog_color[(pid+3)%4]+";left:"+getx(sid)+";top:"+gety(start/60)+";border:0;width:"+stwidth+";height:"+((end-start)/60*stheight/60)+"\">P"+pid+"</div>");
}
function plot_master(start,end) { // plot master station
w("<div title=\"Master ["+getrunstr(start,end)+"]\" style=\"position:absolute;background-color:#CCCC80;left:"+getx(mas-1)+";top:"+gety(start/60)+";border:0;width:"+stwidth+";height:"+((end-start)/60*stheight/60)+"\"></div>");
//if(mas==0||start==end) return;
//ctx.fillStyle="rgba(64,64,64,0.5)";
//ctx.fillRect(getx(mas-1),gety(start/60),stwidth,(end-start)/60*stheight/60);
}
function plot_currtime() {
w("<div style=\"position:absolute;left:"+(xstart-stwidth/2-10)+";top:"+gety(devmin)+";border:1px solid rgba(200,0,0,0.5);width:"+(winwidth-xstart+stwidth/2)+";height:0;\"></div>");
}
function run_sched(simseconds,st_array,pid_array,et_array) { // run and plot schedule stored in array data
var sid,endtime=simseconds;
for(sid=0;sid<nboards*8;sid++) {
if(pid_array[sid]) {
if(seq==1) { // sequential
plot_bar(sid,st_array[sid],pid_array[sid],et_array[sid]);
if((mas>0)&&(mas!=sid+1)&&(masop[sid>>3]&(1<<(sid%8))))
plot_master(st_array[sid]+mton, et_array[sid]+mtoff);
endtime=et_array[sid];
} else { // concurrent
plot_bar(sid,simseconds,pid_array[sid],et_array[sid]);
// check if this station activates master
if((mas>0)&&(mas!=sid+1)&&(masop[sid>>3]&(1<<(sid%8))))
endtime=(endtime>et_array[sid])?endtime:et_array[sid];
}
}
}
if(seq==0&&mas>0) plot_master(simseconds,endtime);
return endtime;
}
function draw_title() {
w("<div align=\"center\" style=\"background-color:#EEEEEE;position:absolute;left:0px;top:10px;border:2px solid gray;padding:5px 0px;width:"+(winwidth)+";border-radius:10px;box-shadow:3px 3px 2px #888888;\"><b>Program Preview of</b>&nbsp;");
w(days_str[simdate.getUTCDay()]+" "+(simdate.getUTCMonth()+1)+"/"+(simdate.getUTCDate())+" "+(simdate.getUTCFullYear()));
w("<br><font size=2>(Hover over each colored bar to see tooltip)</font>");
w("</div>");
}
function draw_grid() {
// draw table and grid
for(sid=0;sid<=nboards*8;sid++) {
sn=sid+1;
if(sid<nboards*8) w("<div style=\"position:absolute;left:"+(xstart+sid*stwidth-10)+";top:"+(ystart-15)+";width:"+stwidth+";height:20;border:0;padding:0;\"><font size=2>S"+(sn/10>>0)+(sn%10)+"</font></div>");
w("<div style=\"position:absolute;left:"+getx(sid)+";top:"+(ystart-10)+";border:1px solid gray;width:0;height:"+(winheight-ystart+30)+";\"></div>");
}
// horizontal grid, time
for(t=0;t<=24;t++) {
w("<div style=\"position:absolute;left:"+(xstart-stwidth/2-15)+";top:"+gety(t*60)+";border:1px solid gray;width:15;height:0;\"></div>");
w("<div style=\"position:absolute;left:"+(xstart-stwidth/2-8)+";top:"+(gety(t*60)+stheight/2)+";border:1px solid gray;width:8;height:0;\"></div>");
w("<div style=\"position:absolute;left:"+(xstart-70)+";top:"+(ystart+t*stheight-7)+";width=70;height:20;border:0;padding:0;\"><font size=2>"+(t/10>>0)+(t%10)+":00</font></div>");
}
plot_currtime();
}
function draw_program() {
// plot program data by a full simulation
var simminutes=0,busy=0,match_found=0,bid,s,sid,pid,match=[0,0];
var st_array=new Array(nboards*8),pid_array=new Array(nboards*8);
var et_array=new Array(nboards*8);
for(sid=0;sid<nboards*8;sid++) {
st_array[sid]=0;pid_array[sid]=0;et_array[sid]=0;
}
do { // check through every program
busy=0;
match_found=0;
for(pid=0;pid<nprogs;pid++) {
var prog=pd[pid];
if(check_match(prog,simminutes,simdate,simday)) {
for(sid=0;sid<nboards*8;sid++) {
bid=sid>>3;s=sid%8;
if(mas==(sid+1)) continue; // skip master station
if(prog[7+bid]&(1<<s)) {
et_array[sid]=prog[6]*wl/100>>0;pid_array[sid]=pid+1;
match_found=1;
}//if
}//for_sid
}//if_match
}//for_pid
if(match_found) {
var acctime=simminutes*60;
if(seq) { // sequential
for(sid=0;sid<nboards*8;sid++) {
if(et_array[sid]) {
st_array[sid]=acctime;acctime+=et_array[sid];
et_array[sid]=acctime;acctime+=sdt;
busy=1;
}//if
}//for
} else {
for(sid=0;sid<nboards*8;sid++) {
if(et_array[sid]) {
st_array[sid]=simminutes*60;
et_array[sid]=simminutes*60+et_array[sid];
busy=1;
}//if(et_array)
}//for(sid)
}//else(seq)
}//if(match_found)
if (busy) {
var endminutes=run_sched(simminutes*60,st_array,pid_array,et_array)/60>>0;
if(seq&&simminutes!=endminutes) simminutes=endminutes;
else simminutes++;
for(sid=0;sid<nboards*8;sid++) {st_array[sid]=0;pid_array[sid]=0;et_array[sid]=0;} // clear program data
} else {
simminutes++; // increment simulation time
}
} while(simminutes<24*60); // simulation ends
window.scrollTo(0,gety((devmin/60>>0)*60)); // scroll to the hour line cloest to the current time
}
draw_title();
draw_grid();
draw_program();

View File

@ -0,0 +1,46 @@
// Javascript for printing OpenSprinkler homepage (program mode)
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
// print station status
function rsn() {
var p="";
if(!ipas) p=prompt("Please enter your password:","");
if(p!=null) window.location="/cv?pw="+p+"&rsn=1";
}
w("<button style=\"height:32\" onclick=linkn(\"/gp?d=0\")>"+imgstr("preview")+"Program Preview</button>");
w("<button style=\"height:32\" onclick=rsn()>"+imgstr("del")+"Stop All Stations</button>");
w("<button style=\"height:32\" onclick=link(\"/vr\")>"+imgstr("start")+"Run-Once Program</button><br>");
w("<p><b>Station Status</b>:</p>");
w("<table border=1>");
var bid,s,sid,sn,rem,remm,rems,off,pname;
off=((en==0||rd!=0||(urs!=0&&rs!=0))?1:0);
for(bid=0;bid<nbrd;bid++){
for(s=0;s<8;s++){
w("<tr><td bgcolor=\"#E4E4E4\">");
sid=bid*8+s;
sn=sid+1;
w(snames[sid]+':&nbsp;&nbsp;');
w("</td><td>");
if(off) w("<strike>");
if(sn==mas) {w(((sbits[bid]>>s)&1?("<b>On</b>").fontcolor("green"):("Off").fontcolor("black"))+" (<b>Master</b>)");}
else {
rem=ps[sid][1];remm=rem/60>>0;rems=rem%60;
pname="P"+ps[sid][0];
if(ps[sid][0]==255||ps[sid][0]==99) pname="Manual Program";
if(ps[sid][0]==254||ps[sid][0]==98) pname="Run-once Program";
if((sbits[bid]>>s)&1) {
w(("<b>Running "+pname).fontcolor("green")+"</b> ("+(remm/10>>0)+(remm%10)+":"+(rems/10>>0)+(rems%10)+" remaining)");
} else {
if(ps[sid][0]==0) w("<font color=lightgray>(closed)</font>");
else w(("Waiting "+pname+" ("+(remm/10>>0)+(remm%10)+":"+(rems/10>>0)+(rems%10)+" scheduled)").fontcolor("gray"));
}
}
if(off) w("</strike>");
w("</td></tr>");
}
}
w("</table>");

View File

@ -0,0 +1,74 @@
// Javascript for printing OpenSprinkler option page
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
var str_tooltips=["Example: GMT-4:00, GMT+5:30 (effective after reboot).", "HTTP port (effective after reboot).", "HTTP port (effective after reboot).", "Number of extension boards", "Sequential running or concurrent running", "Station delay time (in seconds), between 0 and 240.", "Select master station", "Master on delay (in seconds), between +0 and +60.", "Master off delay (in seconds), between -60 and +60.", "Use rain sensor", "Rain sensor type", "Water level, between 0% and 250%.", "Ignore web password"];
function w(s) {document.writeln(s);}
function imgstr(s) {return "<img src=\""+baseurl+"/static/images/icons/svc_"+s+".png\" height=20 align=absmiddle>&nbsp;";}
function submit_form(f) {
// process time zone value
var th=parseInt(f.elements["th"].value,10);
var tq=parseInt(f.elements["tq"].value,10);
tq=(tq/15>>0)/4.0;th=th+(th>=0?tq:-tq);
// huge hack, needs to find a more elegant way
f.elements["o1"].value=((th+12)*4)>>0;
f.elements["o12"].value=(f.elements["htp"].value)&0xff;
f.elements["o13"].value=(f.elements["htp"].value>>8)&0xff;
f.elements["o18"].value=f.elements["mas"].value;
f.submit();
}
function fcancel() {window.location="/";}
function fshow() {
var oid,tip;
for(oid=0;oid<nopts;oid++){
tip=document.getElementById("tip"+oid);
if(tip!=null) tip.hidden=false;
}
}
w("<div align=\"center\" style=\"background-color:#EEEEEE;border:2px solid gray;padding:5px 10px;width:240px;border-radius:10px;box-shadow:3px 3px 2px #888888;\">");
w("<b>Set Options</b>:<br><font size=2>(Hover on each option to see tooltip)</font></div>");
w("<p></p>");
w("<button style=\"height:24\" onclick=\"fshow();return false;\">Show Tooltips</button>");
// print html form
w("<form name=of action=co method=get>");
var oid,name,isbool,value,index,pasoid=0;
for(oid=0;oid<nopts;oid++){
name=opts[oid*4+0];
isbool=opts[oid*4+1];
value=opts[oid*4+2];
index=opts[oid*4+3];
if(name=="Ignore password:") pasoid=oid;
if(isbool) w("<p title=\""+str_tooltips[oid]+"\"><b>"+name+"</b> <input type=checkbox "+(value>0?"checked":"")+" name=o"+index+">");
else {
// hack
if (name=="Time zone:") {
w("<input type=hidden value=0 name=o"+index+">");
tz=value-48;
w("<p title=\""+str_tooltips[oid]+"\"><b>"+name+"</b> GMT<input type=text size=3 maxlength=3 value="+(tz>=0?"+":"-")+(Math.abs(tz)/4>>0)+" name=th>");
w(":<input type=text size=3 maxlength=3 value="+((Math.abs(tz)%4)*15/10>>0)+((Math.abs(tz)%4)*15%10)+" name=tq>");
} else if (name=="Master station:") {
w("<input type=hidden value=0 name=o"+index+">");
w("<p title=\""+str_tooltips[oid]+"\"><b>"+name+"</b> <select name=mas><option "+(value==0?" selected ":" ")+"value=0>None</option>");
for(i=1;i<=8;i++) w("<option "+(value==i?" selected ":" ")+"value="+i+">Station 0"+i+"</option>");
w("</select>");
} else if (name=="HTTP port:") {
w("<input type=hidden value=0 name=o"+index+"><input type=hidden value=0 name=o"+(index+1)+">");
var port=value+(opts[(oid+1)*4+2]<<8);
w("<p title=\""+str_tooltips[oid]+"\"><b>"+name+"</b> <input type=text size=5 maxlength=5 value="+port+" name=htp>");
oid++;
}
else {
w("<p title=\""+str_tooltips[oid]+"\"><b>"+name+"</b> <input type=text size=3 maxlength=3 value="+value+" name=o"+index+">");
}
}
//w("</p>");
w(" <span style=\"background-color:#FFF2B8;\" id=tip"+oid+" hidden=\"hidden\"><font size=2>"+str_tooltips[oid]+"</font></span></p>");
}
w("<p title=\"City name or zip code. Use comma or + in place of space.\"><b>Location:</b> <input type=text maxlength=31 value=\""+loc+"\" name=loc></p>");
w("<h4>Password:<input type=password size=10 "+(opts[pasoid*4+2]?"disabled":"")+" name=pw></h4>");
w("<button style=\"height:36\" onclick=\"submit_form(of)\">"+imgstr("submit")+"<b>Submit Changes</b></button>");
w("<button style=\"height:36\" onclick=\"fcancel();return false;\">"+imgstr("delall")+"Cancel</button>");
w("<h4>Change password</b>:<input type=password size=10 name=npw>&nbsp;&nbsp;Confirm:&nbsp;<input type=password size=10 name=cpw></h4>");
w("</form>");

View File

@ -0,0 +1,92 @@
// Javascript for printing OpenSprinkler schedule page
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Rayshobby.net, Sep 2012
var str_days=["Mon","Tue","Wed","Thur","Fri","Sat","Sun"];
function w(s) {document.writeln(s);}
function imgstr(s) {return "<img src=\""+baseurl+"/static/images/icons/svc_"+s+".png\" height=20 align=absmiddle>&nbsp;";}
function del(form,idx) {
var p="";
if(!ipas) p=prompt("Please enter your password:","");
if(p!=null){form.elements[0].value=p;form.elements[1].value=idx;form.submit();}
}
function mod(form,idx) {form.elements[0].value=idx;form.submit();}
function rnow(form,idx) {
//form.elements[0].value=idx;form.submit();
var p="";
if(!ipas) p=prompt("Please enter your password:","");
if(p!=null){form.elements[0].value=p;form.elements[1].value=idx;form.submit();}
}
// parse and print days
function pdays(days){
if((days[0]&0x80)&&(days[1]>1)){
// this is an interval program
days[0]=days[0]&0x7f;
w("Every "+days[1]+" days, starting in "+days[0]+" days.");
} else {
// this is a weekly program
for(d=0;d<7;d++) {if(days[0]&(1<<d)) {w(str_days[d]);}}
if((days[0]&0x80)&&(days[1]==0)) {w("(Even days only)");}
if((days[0]&0x80)&&(days[1]==1)) {w("(Odd days only)");}
}
}
// parse and print stations
function pstations(data){
w("<table border=1 cellpadding=3px>");
var bid,s,bits,sid;
for(bid=0;bid<nboards;bid++){
bits=data[bid+7];
for(s=0;s<8;s++){
sid=bid*8+s;
if(sid%4==0) w("<tr>");
w("<td style=\"background-color:");
if(bits&(1<<s)) w("#9AFA9A\"><font size=2 color=black>"+snames[sid]);
else w("white\"><font size=2 color=lightgray>"+snames[sid]);
w("</font></td>");
if(sid%4==3) w("</tr>");
}
}
w("</table>\n");
}
function fcancel() {window.location="/";}
function fplot() {window.open("/gp?d=0","_blank");}
w("<form name=df action=dp method=get><input type=hidden name=pw><input type=hidden name=pid></form>");
w("<form name=rn action=rp method=get><input type=hidden name=pw><input type=hidden name=pid></form>");
w("<form name=mf action=mp method=get><input type=hidden name=pid></form>");
w("<button style=\"height:44\" onclick=\"fcancel()\">"+imgstr("back")+"Back</button>");
w("<button style=\"height:44\" onclick=\"mod(mf,-1)\">"+imgstr("addall")+"<b>Add a New Program</b></button>");
w("<button style=\"height:44\" onclick=\"del(df,-1)\">"+imgstr("delall")+"Delete All</button>");
w("<button style=\"height:44\" onclick=\"fplot()\">"+imgstr("preview")+"Preview</button><hr>");
w("<b>Total number of programs: "+nprogs+" (maximum is "+mnp+")</b><br>");
// print programs
var pid,st,et,iv,du,sd;
for(pid=0;pid<nprogs;pid++) {
w("<span style=\"line-height:22px\">");
if(pd[pid][0]==0) w("<strike>");
w("<br><b>Program "+(pid+1)+": ");
// parse and print days
pdays([pd[pid][1],pd[pid][2]]);
w("</b>");
if((pd[pid][0]&0x01)==0) w("</strike><font color=red>(Disabled)</font>");
// print time
st=pd[pid][3];
et=pd[pid][4];
iv=pd[pid][5];
du=pd[pid][6];
w("<br><b>Time</b>: "+((st/60>>0)/10>>0)+((st/60>>0)%10)+":"+((st%60)/10>>0)+((st%60)%10));
w(" - "+((et/60>>0)/10>>0)+((et/60>>0)%10)+":"+((et%60)/10>>0)+((et%60)%10));
w(",<b> Every</b> "+(iv/60>>0)+" hrs "+(iv%60)+" mins,");
w("<br><b>Run</b>: "+(du/60>>0)+" mins "+(du%60)+" secs.<br>");
// parse and print stations
pstations(pd[pid]);
w("</span>");
// print buttons
w("<br><button style=\"height:28\" onclick=del(df,"+pid+")>Delete</button>");
w("<button style=\"height:28\" onclick=mod(mf,"+pid+")>Modify</button>");
w("<button style=\"height:28\" onclick=rnow(rn,"+pid+")>Run Now</button>");
w("<hr>");
}

View File

@ -0,0 +1,53 @@
// Javascript for printing OpenSprinkler Run Once page
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
function w(s) {document.writeln(s);}
function imgstr(s) {return "<img src=\""+baseurl+"/static/images/icons/svc_"+s+".png\" height=20 align=absmiddle>&nbsp;";}
function rst(f) {
var sid,sn;
for(sid=0;sid<nboards*8;sid++) {
if(sid+1==mas) continue;
f.elements["mm"+sid].value=0;
f.elements["ss"+sid].value=0;
}
}
function fsubmit(f) {
var comm="/cr?pw="+(ipas?"":f.elements["pw"].value)+"&t=[";
var sid,strmm,strss,mm,ss,matchfound=0;
for(sid=0;sid<nboards*8;sid++) {
if(sid+1==mas) {comm+="0,";continue;}
strmm=f.elements["mm"+sid].value;
strss=f.elements["ss"+sid].value;
mm=(strmm=="")?0:parseInt(strmm);
ss=(strss=="")?0:parseInt(strss);
if(!(mm>=0&&ss>=0&&ss<60)) {alert("Timer values wrong: "+strmm+":"+strss);return;}
if(mm*60+ss>0) matchfound=1;
comm+=(mm*60+ss)+",";
}
comm+="0]"
if(!matchfound) {alert("No station is schedule to run");return;}
window.location=comm;
}
function fcancel() {window.location="/";}
w("<div align=\"center\" style=\"background-color:#EEEEEE;border:2px solid gray;padding:5px 10px;width:240px;border-radius:10px;box-shadow:3px 3px 2px #888888;\">");
w("<font size=3><b>Run-Once Program:</b></font></div><p></p>");
var sid;
w("<table border=1>");
w("<form name=rf action=cr method=get>");
for(sid=0;sid<nboards*8;sid++) {
w("<tr><td bgcolor=\"#E4E4E4\">");
w(snames[sid]+":&nbsp;&nbsp;</td><td>");
if (sid+1==mas) {w("(<b>Master</b>)<br>");continue;}
w("<input type=text size=3 maxlength=3 value=0 name=mm"+sid+">:");
w("<input type=text size=2 maxlength=2 value=0 name=ss"+sid+"> (mm:ss)<br>");
w("</td>");
}
w("</table>");
w("<hr><font size=3><b>Password:</b><input type=password size=10 "+(ipas?"disabled":"")+" name=pw></font><p></p>");
w("</form></span>");
w("<button style=\"height:36\" onclick=\"fsubmit(rf)\">"+imgstr("submit")+"<b>Run Now</b></button>");
w("<button style=\"height:36\" onclick=\"rst(rf)\">"+imgstr("reset")+"Reset Time</button>");
w("<button style=\"height:36\" onclick=\"fcancel()\">"+imgstr("delall")+"Cancel</button>");

View File

@ -0,0 +1,57 @@
// Javascript for changing OpenSprinkler station names and master operation bits
// Firmware v1.8
// All content is published under:
// Creative Commons Attribution ShareAlike 3.0 License
// Sep 2012, Rayshobby.net
function w(s) {document.writeln(s);}
function imgstr(s) {return "<img src=\""+baseurl+"/static/images/icons/svc_"+s+".png\" height=20 align=absmiddle>&nbsp;";}
function rst() {
var sid,sn;
for(sid=0;sid<nboards*8;sid++) {
sn=sid+1;
document.getElementById("n"+sid).value="S"+(sn/10>>0)+(sn%10);
}
}
function fsubmit(f) {
if(mas>0) {
var s, bid, sid, v;
for(bid=0;bid<nboards;bid++) {
v=0;
for(s=0;s<8;s++){
sid=bid*8+(7-s);
v=v<<1;
if(sid+1==mas) {v=v+1;continue;}
if(document.getElementById("mc"+sid).checked) {
v=v+1;
}
}
f.elements["m"+bid].value=v;
}
}
f.submit();
}
function fcancel() {window.location="/";}
w("<div align=\"center\" style=\"background-color:#EEEEEE;border:2px solid gray;padding:5px 10px;width:240px;border-radius:10px;box-shadow:3px 3px 2px #888888;\">");
w("<font size=3><b>Set Stations:</b></font><br>");
w("<font size=2>(Maximum name length is "+maxlen+" letters).</font></div><p></p>");
var sid,sn,bid,s;
w("<span style=\"line-height:32px\"><form name=sf action=cs method=get>");
for(sid=0;sid<nboards*8;sid++) {
sn=sid+1;
bid=sid>>3;
s=sid%8;
w("Station "+(sn/10>>0)+(sn%10)+":");
w("<input type=text size="+maxlen+" maxlength="+maxlen+" value=\""+snames[sid]+"\" name=s"+sid+" id=n"+sid+">&nbsp;");
if (sid+1==mas) w("(<b>Master</b>)");
else if (mas>0) w("<input type=checkbox "+(masop[bid]&(1<<s)?"checked":"")+" id=mc"+sid+">Activate master?");
w("<br>");
}
w("<hr><font size=3><b>Password:</b><input type=password size=10 "+(ipas?"disabled":"")+" name=pw></font><p></p>");
for(bid=0;bid<nboards;bid++) {
w("<input type=hidden name=m"+bid+">");
}
w("</form></span>");
w("<button style=\"height:36\" onclick=\"fsubmit(sf)\">"+imgstr("submit")+"<b>Submit Changes</b></button>");
w("<button style=\"height:36\" onclick=\"rst()\">"+imgstr("reset")+"Reset Names</button>");
w("<button style=\"height:36\" onclick=\"fcancel()\">"+imgstr("delall")+"Cancel</button>");

134
templates/log.html Normal file
View File

@ -0,0 +1,134 @@
$def with (records)
<!doctype html>
<html>
<head>
<title>Water Log</title>
<meta content="width=640" name="viewport">
<link href="./static/images/icons/favicon.ico" rel="icon" type="image/x-icon" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
#overlay {
visibility: hidden;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height:100%;
text-align:center;
z-index: 1000;
background-image:url(../static/images/misc/BlueTile.png);
}
#overlay div {
width:200px;
margin: 100px auto;
background-color: #fff;
border:1px solid #000;
padding:10px;
text-align:left;
}
h2 {
text-align: center;
font-family: Sans-serif;
}
.pwspan {
float:left;
border-style:none;
clear:both;
margin:10px 0px 10px 0px;
}
</style>
<script>
function fcancel() {window.location="/";}
function ocancel() {window.location="/vl";}
function link(s) {window.location=s;}
function del(form) {
var p="";
if(!$sd['ipas']) p=prompt("Password Required","");
if(p!=null){form.elements[0].value=p;form.submit();}
}
function subm(form) {
form.submit();
}
function hidepw(){
if ($sd['ipas']) {
document.getElementById("pwarea").style.display = "none";
}
else {
document.getElementById("pwarea").style.display = "block";
}
}
function overlay() {
el = document.getElementById("overlay");
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible";
}
</script>
</head>
<body onload="hidepw()">
<form method="get" action="cl" name="df">
<input type="hidden" name="pw">
</form>
<form method="get" action="mp" name="mf">
<input type="hidden" name="pid" value="-1">
</form>
<button onclick="fcancel()" style="height:44">
<img align="absmiddle" height="20" src="../static/images/icons/svc_back.png">
Back
</button>
<button onclick="link('/vl')" style="height:44">
<img align="absmiddle" height="20" src="../static/images/icons/svc_refresh.png">
<b>Refresh</b>
</button>
<button onclick="del(df, 0)" style="height:44">
<img align="absmiddle" height="20" src="../static/images/icons/svc_delall.png">
Delete All
</button>
<button onclick="overlay()" style="height:44">
<img align="absmiddle" height="20" src="../static/images/icons/svc_options.png">
Log Options
</button>
$code:
if sd['lg'] == 'checked':
log_state = "Enabled"
else:
log_state = "Disabled"
<br><br>
Logging $log_state
<br>
<b>Total number of records: $(len(records)-1)</b>
<hr>
<table>
$for r in records:
<tr class="log_rec">
$for d in r:
<td align='center'>$d</td>
</tr>
</table>
<div id="overlay">
<div>
<form name="logopts" action="/lo" method="get">
<p><h2>Log Options</h2></p>
<label for="log">Enable Logging</label> <input type="checkbox" id="log" name="log" $sd['lg'] ><br>
<label for="max">Maximum records to keep:</label> <input type="text" size="4" value="$sd['lr']" id="max" name="nrecords">(0 = no limit)<br>
<span id='pwarea' style='display:block'; class="pwspan">
<label for="pw">Password Required:</label> <input type="password" size="10"id="pw">
</span><br>
<input type="submit" value="Submit">
<button type="button" onclick="overlay()"; return false;>Cancel</button>
</form>
</div>
</div>
</body>
</html>

33
web/__init__.py Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python
"""web.py: makes web apps (http://webpy.org)"""
from __future__ import generators
__version__ = "0.37"
__author__ = [
"Aaron Swartz <me@aaronsw.com>",
"Anand Chitipothu <anandology@gmail.com>"
]
__license__ = "public domain"
__contributors__ = "see http://webpy.org/changes"
import utils, db, net, wsgi, http, webapi, httpserver, debugerror
import template, form
import session
from utils import *
from db import *
from net import *
from wsgi import *
from http import *
from webapi import *
from httpserver import *
from debugerror import *
from application import *
from browser import *
try:
import webopenid as openid
except ImportError:
pass # requires openid module

BIN
web/__init__.pyc Normal file

Binary file not shown.

687
web/application.py Normal file
View File

@ -0,0 +1,687 @@
"""
Web application
(from web.py)
"""
import webapi as web
import webapi, wsgi, utils
import debugerror
import httpserver
from utils import lstrips, safeunicode
import sys
import urllib
import traceback
import itertools
import os
import types
from exceptions import SystemExit
try:
import wsgiref.handlers
except ImportError:
pass # don't break people with old Pythons
__all__ = [
"application", "auto_application",
"subdir_application", "subdomain_application",
"loadhook", "unloadhook",
"autodelegate"
]
class application:
"""
Application to delegate requests based on path.
>>> urls = ("/hello", "hello")
>>> app = application(urls, globals())
>>> class hello:
... def GET(self): return "hello"
>>>
>>> app.request("/hello").data
'hello'
"""
def __init__(self, mapping=(), fvars={}, autoreload=None):
if autoreload is None:
autoreload = web.config.get('debug', False)
self.init_mapping(mapping)
self.fvars = fvars
self.processors = []
self.add_processor(loadhook(self._load))
self.add_processor(unloadhook(self._unload))
if autoreload:
def main_module_name():
mod = sys.modules['__main__']
file = getattr(mod, '__file__', None) # make sure this works even from python interpreter
return file and os.path.splitext(os.path.basename(file))[0]
def modname(fvars):
"""find name of the module name from fvars."""
file, name = fvars.get('__file__'), fvars.get('__name__')
if file is None or name is None:
return None
if name == '__main__':
# Since the __main__ module can't be reloaded, the module has
# to be imported using its file name.
name = main_module_name()
return name
mapping_name = utils.dictfind(fvars, mapping)
module_name = modname(fvars)
def reload_mapping():
"""loadhook to reload mapping and fvars."""
mod = __import__(module_name, None, None, [''])
mapping = getattr(mod, mapping_name, None)
if mapping:
self.fvars = mod.__dict__
self.init_mapping(mapping)
self.add_processor(loadhook(Reloader()))
if mapping_name and module_name:
self.add_processor(loadhook(reload_mapping))
# load __main__ module usings its filename, so that it can be reloaded.
if main_module_name() and '__main__' in sys.argv:
try:
__import__(main_module_name())
except ImportError:
pass
def _load(self):
web.ctx.app_stack.append(self)
def _unload(self):
web.ctx.app_stack = web.ctx.app_stack[:-1]
if web.ctx.app_stack:
# this is a sub-application, revert ctx to earlier state.
oldctx = web.ctx.get('_oldctx')
if oldctx:
web.ctx.home = oldctx.home
web.ctx.homepath = oldctx.homepath
web.ctx.path = oldctx.path
web.ctx.fullpath = oldctx.fullpath
def _cleanup(self):
# Threads can be recycled by WSGI servers.
# Clearing up all thread-local state to avoid interefereing with subsequent requests.
utils.ThreadedDict.clear_all()
def init_mapping(self, mapping):
self.mapping = list(utils.group(mapping, 2))
def add_mapping(self, pattern, classname):
self.mapping.append((pattern, classname))
def add_processor(self, processor):
"""
Adds a processor to the application.
>>> urls = ("/(.*)", "echo")
>>> app = application(urls, globals())
>>> class echo:
... def GET(self, name): return name
...
>>>
>>> def hello(handler): return "hello, " + handler()
...
>>> app.add_processor(hello)
>>> app.request("/web.py").data
'hello, web.py'
"""
self.processors.append(processor)
def request(self, localpart='/', method='GET', data=None,
host="0.0.0.0:8080", headers=None, https=False, **kw):
"""Makes request to this application for the specified path and method.
Response will be a storage object with data, status and headers.
>>> urls = ("/hello", "hello")
>>> app = application(urls, globals())
>>> class hello:
... def GET(self):
... web.header('Content-Type', 'text/plain')
... return "hello"
...
>>> response = app.request("/hello")
>>> response.data
'hello'
>>> response.status
'200 OK'
>>> response.headers['Content-Type']
'text/plain'
To use https, use https=True.
>>> urls = ("/redirect", "redirect")
>>> app = application(urls, globals())
>>> class redirect:
... def GET(self): raise web.seeother("/foo")
...
>>> response = app.request("/redirect")
>>> response.headers['Location']
'http://0.0.0.0:8080/foo'
>>> response = app.request("/redirect", https=True)
>>> response.headers['Location']
'https://0.0.0.0:8080/foo'
The headers argument specifies HTTP headers as a mapping object
such as a dict.
>>> urls = ('/ua', 'uaprinter')
>>> class uaprinter:
... def GET(self):
... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
...
>>> app = application(urls, globals())
>>> app.request('/ua', headers = {
... 'User-Agent': 'a small jumping bean/1.0 (compatible)'
... }).data
'your user-agent is a small jumping bean/1.0 (compatible)'
"""
path, maybe_query = urllib.splitquery(localpart)
query = maybe_query or ""
if 'env' in kw:
env = kw['env']
else:
env = {}
env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https))
headers = headers or {}
for k, v in headers.items():
env['HTTP_' + k.upper().replace('-', '_')] = v
if 'HTTP_CONTENT_LENGTH' in env:
env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH')
if 'HTTP_CONTENT_TYPE' in env:
env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
if method not in ["HEAD", "GET"]:
data = data or ''
import StringIO
if isinstance(data, dict):
q = urllib.urlencode(data)
else:
q = data
env['wsgi.input'] = StringIO.StringIO(q)
if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
env['CONTENT_LENGTH'] = len(q)
response = web.storage()
def start_response(status, headers):
response.status = status
response.headers = dict(headers)
response.header_items = headers
response.data = "".join(self.wsgifunc()(env, start_response))
return response
def browser(self):
import browser
return browser.AppBrowser(self)
def handle(self):
fn, args = self._match(self.mapping, web.ctx.path)
return self._delegate(fn, self.fvars, args)
def handle_with_processors(self):
def process(processors):
try:
if processors:
p, processors = processors[0], processors[1:]
return p(lambda: process(processors))
else:
return self.handle()
except web.HTTPError:
raise
except (KeyboardInterrupt, SystemExit):
raise
except:
print >> web.debug, traceback.format_exc()
raise self.internalerror()
# processors must be applied in the resvere order. (??)
return process(self.processors)
def wsgifunc(self, *middleware):
"""Returns a WSGI-compatible function for this application."""
def peep(iterator):
"""Peeps into an iterator by doing an iteration
and returns an equivalent iterator.
"""
# wsgi requires the headers first
# so we need to do an iteration
# and save the result for later
try:
firstchunk = iterator.next()
except StopIteration:
firstchunk = ''
return itertools.chain([firstchunk], iterator)
def is_generator(x): return x and hasattr(x, 'next')
def wsgi(env, start_resp):
# clear threadlocal to avoid inteference of previous requests
self._cleanup()
self.load(env)
try:
# allow uppercase methods only
if web.ctx.method.upper() != web.ctx.method:
raise web.nomethod()
result = self.handle_with_processors()
if is_generator(result):
result = peep(result)
else:
result = [result]
except web.HTTPError, e:
result = [e.data]
result = web.safestr(iter(result))
status, headers = web.ctx.status, web.ctx.headers
start_resp(status, headers)
def cleanup():
self._cleanup()
yield '' # force this function to be a generator
return itertools.chain(result, cleanup())
for m in middleware:
wsgi = m(wsgi)
return wsgi
def run(self, *middleware):
"""
Starts handling requests. If called in a CGI or FastCGI context, it will follow
that protocol. If called from the command line, it will start an HTTP
server on the port named in the first command line argument, or, if there
is no argument, on port 8080.
`middleware` is a list of WSGI middleware which is applied to the resulting WSGI
function.
"""
return wsgi.runwsgi(self.wsgifunc(*middleware))
def stop(self):
"""Stops the http server started by run.
"""
if httpserver.server:
httpserver.server.stop()
httpserver.server = None
def cgirun(self, *middleware):
"""
Return a CGI handler. This is mostly useful with Google App Engine.
There you can just do:
main = app.cgirun()
"""
wsgiapp = self.wsgifunc(*middleware)
try:
from google.appengine.ext.webapp.util import run_wsgi_app
return run_wsgi_app(wsgiapp)
except ImportError:
# we're not running from within Google App Engine
return wsgiref.handlers.CGIHandler().run(wsgiapp)
def load(self, env):
"""Initializes ctx using env."""
ctx = web.ctx
ctx.clear()
ctx.status = '200 OK'
ctx.headers = []
ctx.output = ''
ctx.environ = ctx.env = env
ctx.host = env.get('HTTP_HOST')
if env.get('wsgi.url_scheme') in ['http', 'https']:
ctx.protocol = env['wsgi.url_scheme']
elif env.get('HTTPS', '').lower() in ['on', 'true', '1']:
ctx.protocol = 'https'
else:
ctx.protocol = 'http'
ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]')
ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
ctx.home = ctx.homedomain + ctx.homepath
#@@ home is changed when the request is handled to a sub-application.
#@@ but the real home is required for doing absolute redirects.
ctx.realhome = ctx.home
ctx.ip = env.get('REMOTE_ADDR')
ctx.method = env.get('REQUEST_METHOD')
ctx.path = env.get('PATH_INFO')
# http://trac.lighttpd.net/trac/ticket/406 requires:
if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath)
# Apache and CherryPy webservers unquote the url but lighttpd doesn't.
# unquote explicitly for lighttpd to make ctx.path uniform across all servers.
ctx.path = urllib.unquote(ctx.path)
if env.get('QUERY_STRING'):
ctx.query = '?' + env.get('QUERY_STRING', '')
else:
ctx.query = ''
ctx.fullpath = ctx.path + ctx.query
for k, v in ctx.iteritems():
# convert all string values to unicode values and replace
# malformed data with a suitable replacement marker.
if isinstance(v, str):
ctx[k] = v.decode('utf-8', 'replace')
# status must always be str
ctx.status = '200 OK'
ctx.app_stack = []
def _delegate(self, f, fvars, args=[]):
def handle_class(cls):
meth = web.ctx.method
if meth == 'HEAD' and not hasattr(cls, meth):
meth = 'GET'
if not hasattr(cls, meth):
raise web.nomethod(cls)
tocall = getattr(cls(), meth)
return tocall(*args)
def is_class(o): return isinstance(o, (types.ClassType, type))
if f is None:
raise web.notfound()
elif isinstance(f, application):
return f.handle_with_processors()
elif is_class(f):
return handle_class(f)
elif isinstance(f, basestring):
if f.startswith('redirect '):
url = f.split(' ', 1)[1]
if web.ctx.method == "GET":
x = web.ctx.env.get('QUERY_STRING', '')
if x:
url += '?' + x
raise web.redirect(url)
elif '.' in f:
mod, cls = f.rsplit('.', 1)
mod = __import__(mod, None, None, [''])
cls = getattr(mod, cls)
else:
cls = fvars[f]
return handle_class(cls)
elif hasattr(f, '__call__'):
return f()
else:
return web.notfound()
def _match(self, mapping, value):
for pat, what in mapping:
if isinstance(what, application):
if value.startswith(pat):
f = lambda: self._delegate_sub_application(pat, what)
return f, None
else:
continue
elif isinstance(what, basestring):
what, result = utils.re_subm('^' + pat + '$', what, value)
else:
result = utils.re_compile('^' + pat + '$').match(value)
if result: # it's a match
return what, [x for x in result.groups()]
return None, None
def _delegate_sub_application(self, dir, app):
"""Deletes request to sub application `app` rooted at the directory `dir`.
The home, homepath, path and fullpath values in web.ctx are updated to mimic request
to the subapp and are restored after it is handled.
@@Any issues with when used with yield?
"""
web.ctx._oldctx = web.storage(web.ctx)
web.ctx.home += dir
web.ctx.homepath += dir
web.ctx.path = web.ctx.path[len(dir):]
web.ctx.fullpath = web.ctx.fullpath[len(dir):]
return app.handle_with_processors()
def get_parent_app(self):
if self in web.ctx.app_stack:
index = web.ctx.app_stack.index(self)
if index > 0:
return web.ctx.app_stack[index-1]
def notfound(self):
"""Returns HTTPError with '404 not found' message"""
parent = self.get_parent_app()
if parent:
return parent.notfound()
else:
return web._NotFound()
def internalerror(self):
"""Returns HTTPError with '500 internal error' message"""
parent = self.get_parent_app()
if parent:
return parent.internalerror()
elif web.config.get('debug'):
import debugerror
return debugerror.debugerror()
else:
return web._InternalError()
class auto_application(application):
"""Application similar to `application` but urls are constructed
automatiacally using metaclass.
>>> app = auto_application()
>>> class hello(app.page):
... def GET(self): return "hello, world"
...
>>> class foo(app.page):
... path = '/foo/.*'
... def GET(self): return "foo"
>>> app.request("/hello").data
'hello, world'
>>> app.request('/foo/bar').data
'foo'
"""
def __init__(self):
application.__init__(self)
class metapage(type):
def __init__(klass, name, bases, attrs):
type.__init__(klass, name, bases, attrs)
path = attrs.get('path', '/' + name)
# path can be specified as None to ignore that class
# typically required to create a abstract base class.
if path is not None:
self.add_mapping(path, klass)
class page:
path = None
__metaclass__ = metapage
self.page = page
# The application class already has the required functionality of subdir_application
subdir_application = application
class subdomain_application(application):
"""
Application to delegate requests based on the host.
>>> urls = ("/hello", "hello")
>>> app = application(urls, globals())
>>> class hello:
... def GET(self): return "hello"
>>>
>>> mapping = (r"hello\.example\.com", app)
>>> app2 = subdomain_application(mapping)
>>> app2.request("/hello", host="hello.example.com").data
'hello'
>>> response = app2.request("/hello", host="something.example.com")
>>> response.status
'404 Not Found'
>>> response.data
'not found'
"""
def handle(self):
host = web.ctx.host.split(':')[0] #strip port
fn, args = self._match(self.mapping, host)
return self._delegate(fn, self.fvars, args)
def _match(self, mapping, value):
for pat, what in mapping:
if isinstance(what, basestring):
what, result = utils.re_subm('^' + pat + '$', what, value)
else:
result = utils.re_compile('^' + pat + '$').match(value)
if result: # it's a match
return what, [x for x in result.groups()]
return None, None
def loadhook(h):
"""
Converts a load hook into an application processor.
>>> app = auto_application()
>>> def f(): "something done before handling request"
...
>>> app.add_processor(loadhook(f))
"""
def processor(handler):
h()
return handler()
return processor
def unloadhook(h):
"""
Converts an unload hook into an application processor.
>>> app = auto_application()
>>> def f(): "something done after handling request"
...
>>> app.add_processor(unloadhook(f))
"""
def processor(handler):
try:
result = handler()
is_generator = result and hasattr(result, 'next')
except:
# run the hook even when handler raises some exception
h()
raise
if is_generator:
return wrap(result)
else:
h()
return result
def wrap(result):
def next():
try:
return result.next()
except:
# call the hook at the and of iterator
h()
raise
result = iter(result)
while True:
yield next()
return processor
def autodelegate(prefix=''):
"""
Returns a method that takes one argument and calls the method named prefix+arg,
calling `notfound()` if there isn't one. Example:
urls = ('/prefs/(.*)', 'prefs')
class prefs:
GET = autodelegate('GET_')
def GET_password(self): pass
def GET_privacy(self): pass
`GET_password` would get called for `/prefs/password` while `GET_privacy` for
`GET_privacy` gets called for `/prefs/privacy`.
If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
is called.
"""
def internal(self, arg):
if '/' in arg:
first, rest = arg.split('/', 1)
func = prefix + first
args = ['/' + rest]
else:
func = prefix + arg
args = []
if hasattr(self, func):
try:
return getattr(self, func)(*args)
except TypeError:
raise web.notfound()
else:
raise web.notfound()
return internal
class Reloader:
"""Checks to see if any loaded modules have changed on disk and,
if so, reloads them.
"""
"""File suffix of compiled modules."""
if sys.platform.startswith('java'):
SUFFIX = '$py.class'
else:
SUFFIX = '.pyc'
def __init__(self):
self.mtimes = {}
def __call__(self):
for mod in sys.modules.values():
self.check(mod)
def check(self, mod):
# jython registers java packages as modules but they either
# don't have a __file__ attribute or its value is None
if not (mod and hasattr(mod, '__file__') and mod.__file__):
return
try:
mtime = os.stat(mod.__file__).st_mtime
except (OSError, IOError):
return
if mod.__file__.endswith(self.__class__.SUFFIX) and os.path.exists(mod.__file__[:-1]):
mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
if mod not in self.mtimes:
self.mtimes[mod] = mtime
elif self.mtimes[mod] < mtime:
try:
reload(mod)
self.mtimes[mod] = mtime
except ImportError:
pass
if __name__ == "__main__":
import doctest
doctest.testmod()

BIN
web/application.pyc Normal file

Binary file not shown.

236
web/browser.py Normal file
View File

@ -0,0 +1,236 @@
"""Browser to test web applications.
(from web.py)
"""
from utils import re_compile
from net import htmlunquote
import httplib, urllib, urllib2
import copy
from StringIO import StringIO
DEBUG = False
__all__ = [
"BrowserError",
"Browser", "AppBrowser",
"AppHandler"
]
class BrowserError(Exception):
pass
class Browser:
def __init__(self):
import cookielib
self.cookiejar = cookielib.CookieJar()
self._cookie_processor = urllib2.HTTPCookieProcessor(self.cookiejar)
self.form = None
self.url = "http://0.0.0.0:8080/"
self.path = "/"
self.status = None
self.data = None
self._response = None
self._forms = None
def reset(self):
"""Clears all cookies and history."""
self.cookiejar.clear()
def build_opener(self):
"""Builds the opener using urllib2.build_opener.
Subclasses can override this function to prodive custom openers.
"""
return urllib2.build_opener()
def do_request(self, req):
if DEBUG:
print 'requesting', req.get_method(), req.get_full_url()
opener = self.build_opener()
opener.add_handler(self._cookie_processor)
try:
self._response = opener.open(req)
except urllib2.HTTPError, e:
self._response = e
self.url = self._response.geturl()
self.path = urllib2.Request(self.url).get_selector()
self.data = self._response.read()
self.status = self._response.code
self._forms = None
self.form = None
return self.get_response()
def open(self, url, data=None, headers={}):
"""Opens the specified url."""
url = urllib.basejoin(self.url, url)
req = urllib2.Request(url, data, headers)
return self.do_request(req)
def show(self):
"""Opens the current page in real web browser."""
f = open('page.html', 'w')
f.write(self.data)
f.close()
import webbrowser, os
url = 'file://' + os.path.abspath('page.html')
webbrowser.open(url)
def get_response(self):
"""Returns a copy of the current response."""
return urllib.addinfourl(StringIO(self.data), self._response.info(), self._response.geturl())
def get_soup(self):
"""Returns beautiful soup of the current document."""
import BeautifulSoup
return BeautifulSoup.BeautifulSoup(self.data)
def get_text(self, e=None):
"""Returns content of e or the current document as plain text."""
e = e or self.get_soup()
return ''.join([htmlunquote(c) for c in e.recursiveChildGenerator() if isinstance(c, unicode)])
def _get_links(self):
soup = self.get_soup()
return [a for a in soup.findAll(name='a')]
def get_links(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None):
"""Returns all links in the document."""
return self._filter_links(self._get_links(),
text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate)
def follow_link(self, link=None, text=None, text_regex=None, url=None, url_regex=None, predicate=None):
if link is None:
links = self._filter_links(self.get_links(),
text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate)
link = links and links[0]
if link:
return self.open(link['href'])
else:
raise BrowserError("No link found")
def find_link(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None):
links = self._filter_links(self.get_links(),
text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate)
return links and links[0] or None
def _filter_links(self, links,
text=None, text_regex=None,
url=None, url_regex=None,
predicate=None):
predicates = []
if text is not None:
predicates.append(lambda link: link.string == text)
if text_regex is not None:
predicates.append(lambda link: re_compile(text_regex).search(link.string or ''))
if url is not None:
predicates.append(lambda link: link.get('href') == url)
if url_regex is not None:
predicates.append(lambda link: re_compile(url_regex).search(link.get('href', '')))
if predicate:
predicate.append(predicate)
def f(link):
for p in predicates:
if not p(link):
return False
return True
return [link for link in links if f(link)]
def get_forms(self):
"""Returns all forms in the current document.
The returned form objects implement the ClientForm.HTMLForm interface.
"""
if self._forms is None:
import ClientForm
self._forms = ClientForm.ParseResponse(self.get_response(), backwards_compat=False)
return self._forms
def select_form(self, name=None, predicate=None, index=0):
"""Selects the specified form."""
forms = self.get_forms()
if name is not None:
forms = [f for f in forms if f.name == name]
if predicate:
forms = [f for f in forms if predicate(f)]
if forms:
self.form = forms[index]
return self.form
else:
raise BrowserError("No form selected.")
def submit(self, **kw):
"""submits the currently selected form."""
if self.form is None:
raise BrowserError("No form selected.")
req = self.form.click(**kw)
return self.do_request(req)
def __getitem__(self, key):
return self.form[key]
def __setitem__(self, key, value):
self.form[key] = value
class AppBrowser(Browser):
"""Browser interface to test web.py apps.
b = AppBrowser(app)
b.open('/')
b.follow_link(text='Login')
b.select_form(name='login')
b['username'] = 'joe'
b['password'] = 'secret'
b.submit()
assert b.path == '/'
assert 'Welcome joe' in b.get_text()
"""
def __init__(self, app):
Browser.__init__(self)
self.app = app
def build_opener(self):
return urllib2.build_opener(AppHandler(self.app))
class AppHandler(urllib2.HTTPHandler):
"""urllib2 handler to handle requests using web.py application."""
handler_order = 100
def __init__(self, app):
self.app = app
def http_open(self, req):
result = self.app.request(
localpart=req.get_selector(),
method=req.get_method(),
host=req.get_host(),
data=req.get_data(),
headers=dict(req.header_items()),
https=req.get_type() == "https"
)
return self._make_response(result, req.get_full_url())
def https_open(self, req):
return self.http_open(req)
try:
https_request = urllib2.HTTPHandler.do_request_
except AttributeError:
# for python 2.3
pass
def _make_response(self, result, url):
data = "\r\n".join(["%s: %s" % (k, v) for k, v in result.header_items])
headers = httplib.HTTPMessage(StringIO(data))
response = urllib.addinfourl(StringIO(result.data), headers, url)
code, msg = result.status.split(None, 1)
response.code, response.msg = int(code), msg
return response

BIN
web/browser.pyc Normal file

Binary file not shown.

0
web/contrib/__init__.py Normal file
View File

131
web/contrib/template.py Normal file
View File

@ -0,0 +1,131 @@
"""
Interface to various templating engines.
"""
import os.path
__all__ = [
"render_cheetah", "render_genshi", "render_mako",
"cache",
]
class render_cheetah:
"""Rendering interface to Cheetah Templates.
Example:
render = render_cheetah('templates')
render.hello(name="cheetah")
"""
def __init__(self, path):
# give error if Chetah is not installed
from Cheetah.Template import Template
self.path = path
def __getattr__(self, name):
from Cheetah.Template import Template
path = os.path.join(self.path, name + ".html")
def template(**kw):
t = Template(file=path, searchList=[kw])
return t.respond()
return template
class render_genshi:
"""Rendering interface genshi templates.
Example:
for xml/html templates.
render = render_genshi(['templates/'])
render.hello(name='genshi')
For text templates:
render = render_genshi(['templates/'], type='text')
render.hello(name='genshi')
"""
def __init__(self, *a, **kwargs):
from genshi.template import TemplateLoader
self._type = kwargs.pop('type', None)
self._loader = TemplateLoader(*a, **kwargs)
def __getattr__(self, name):
# Assuming all templates are html
path = name + ".html"
if self._type == "text":
from genshi.template import TextTemplate
cls = TextTemplate
type = "text"
else:
cls = None
type = None
t = self._loader.load(path, cls=cls)
def template(**kw):
stream = t.generate(**kw)
if type:
return stream.render(type)
else:
return stream.render()
return template
class render_jinja:
"""Rendering interface to Jinja2 Templates
Example:
render= render_jinja('templates')
render.hello(name='jinja2')
"""
def __init__(self, *a, **kwargs):
extensions = kwargs.pop('extensions', [])
globals = kwargs.pop('globals', {})
from jinja2 import Environment,FileSystemLoader
self._lookup = Environment(loader=FileSystemLoader(*a, **kwargs), extensions=extensions)
self._lookup.globals.update(globals)
def __getattr__(self, name):
# Assuming all templates end with .html
path = name + '.html'
t = self._lookup.get_template(path)
return t.render
class render_mako:
"""Rendering interface to Mako Templates.
Example:
render = render_mako(directories=['templates'])
render.hello(name="mako")
"""
def __init__(self, *a, **kwargs):
from mako.lookup import TemplateLookup
self._lookup = TemplateLookup(*a, **kwargs)
def __getattr__(self, name):
# Assuming all templates are html
path = name + ".html"
t = self._lookup.get_template(path)
return t.render
class cache:
"""Cache for any rendering interface.
Example:
render = cache(render_cheetah("templates/"))
render.hello(name='cache')
"""
def __init__(self, render):
self._render = render
self._cache = {}
def __getattr__(self, name):
if name not in self._cache:
self._cache[name] = getattr(self._render, name)
return self._cache[name]

1237
web/db.py Normal file

File diff suppressed because it is too large Load Diff

BIN
web/db.pyc Normal file

Binary file not shown.

354
web/debugerror.py Normal file
View File

@ -0,0 +1,354 @@
"""
pretty debug errors
(part of web.py)
portions adapted from Django <djangoproject.com>
Copyright (c) 2005, the Lawrence Journal-World
Used under the modified BSD license:
http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
"""
__all__ = ["debugerror", "djangoerror", "emailerrors"]
import sys, urlparse, pprint, traceback
from template import Template
from net import websafe
from utils import sendmail, safestr
import webapi as web
import os, os.path
whereami = os.path.join(os.getcwd(), __file__)
whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1])
djangoerror_t = """\
$def with (exception_type, exception_value, frames)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title>$exception_type at $ctx.path</title>
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; }
h2 { margin-bottom:.8em; }
h2 span { font-size:80%; color:#666; font-weight:normal; }
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
table {
border:1px solid #ccc; border-collapse: collapse; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
font-weight:normal; font-size:11px; border:1px solid #ddd; }
tbody th { text-align:right; color:#666; padding-right:.5em; }
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%;}
table td.code div { overflow:hidden; }
table.source th { color:#666; }
table.source td {
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
ul.traceback li.frame { margin-bottom:1em; }
div.context { margin: 10px 0; }
div.context ol {
padding-left:30px; margin:0 10px; list-style-position: inside; }
div.context ol li {
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
div.context ol.context-line li { color:black; background-color:#ccc; }
div.context ol.context-line li span { float: right; }
div.commands { margin-left: 40px; }
div.commands a { color:black; text-decoration:none; }
#summary { background: #ffc; }
#summary h2 { font-weight: normal; color: #666; }
#explanation { background:#eee; }
#template, #template-not-exist { background:#f6f6f6; }
#template-not-exist ul { margin: 0 0 0 20px; }
#traceback { background:#eee; }
#requestinfo { background:#f6f6f6; padding-left:120px; }
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
.error { background: #ffc; }
.specific { color:#cc3300; font-weight:bold; }
</style>
<script type="text/javascript">
//<!--
function getElementsByClassName(oElm, strTagName, strClassName){
// Written by Jonathan Snook, http://www.snook.ca/jon;
// Add-ons by Robert Nyman, http://www.robertnyman.com
var arrElements = (strTagName == "*" && document.all)? document.all :
oElm.getElementsByTagName(strTagName);
var arrReturnElements = new Array();
strClassName = strClassName.replace(/\-/g, "\\-");
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
var oElement;
for(var i=0; i<arrElements.length; i++){
oElement = arrElements[i];
if(oRegExp.test(oElement.className)){
arrReturnElements.push(oElement);
}
}
return (arrReturnElements)
}
function hideAll(elems) {
for (var e = 0; e < elems.length; e++) {
elems[e].style.display = 'none';
}
}
window.onload = function() {
hideAll(getElementsByClassName(document, 'table', 'vars'));
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
}
function toggle() {
for (var i = 0; i < arguments.length; i++) {
var e = document.getElementById(arguments[i]);
if (e) {
e.style.display = e.style.display == 'none' ? 'block' : 'none';
}
}
return false;
}
function varToggle(link, id) {
toggle('v' + id);
var s = link.getElementsByTagName('span')[0];
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
return false;
}
//-->
</script>
</head>
<body>
$def dicttable (d, kls='req', id=None):
$ items = d and d.items() or []
$items.sort()
$:dicttable_items(items, kls, id)
$def dicttable_items(items, kls='req', id=None):
$if items:
<table class="$kls"
$if id: id="$id"
><thead><tr><th>Variable</th><th>Value</th></tr></thead>
<tbody>
$for k, v in items:
<tr><td>$k</td><td class="code"><div>$prettify(v)</div></td></tr>
</tbody>
</table>
$else:
<p>No data.</p>
<div id="summary">
<h1>$exception_type at $ctx.path</h1>
<h2>$exception_value</h2>
<table><tr>
<th>Python</th>
<td>$frames[0].filename in $frames[0].function, line $frames[0].lineno</td>
</tr><tr>
<th>Web</th>
<td>$ctx.method $ctx.home$ctx.path</td>
</tr></table>
</div>
<div id="traceback">
<h2>Traceback <span>(innermost first)</span></h2>
<ul class="traceback">
$for frame in frames:
<li class="frame">
<code>$frame.filename</code> in <code>$frame.function</code>
$if frame.context_line is not None:
<div class="context" id="c$frame.id">
$if frame.pre_context:
<ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">
$for line in frame.pre_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
<ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
$if frame.post_context:
<ol start='${frame.lineno + 1}' class="post-context" id="post$frame.id">
$for line in frame.post_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
</div>
$if frame.vars:
<div class="commands">
<a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>
$# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
</div>
$:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id)))
</li>
</ul>
</div>
<div id="requestinfo">
$if ctx.output or ctx.headers:
<h2>Response so far</h2>
<h3>HEADERS</h3>
$:dicttable_items(ctx.headers)
<h3>BODY</h3>
<p class="req" style="padding-bottom: 2em"><code>
$ctx.output
</code></p>
<h2>Request information</h2>
<h3>INPUT</h3>
$:dicttable(web.input(_unicode=False))
<h3 id="cookie-info">COOKIES</h3>
$:dicttable(web.cookies())
<h3 id="meta-info">META</h3>
$ newctx = [(k, v) for (k, v) in ctx.iteritems() if not k.startswith('_') and not isinstance(v, dict)]
$:dicttable(dict(newctx))
<h3 id="meta-info">ENVIRONMENT</h3>
$:dicttable(ctx.env)
</div>
<div id="explanation">
<p>
You're seeing this error because you have <code>web.config.debug</code>
set to <code>True</code>. Set that to <code>False</code> if you don't want to see this.
</p>
</div>
</body>
</html>
"""
djangoerror_r = None
def djangoerror():
def _get_lines_from_file(filename, lineno, context_lines):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
try:
source = open(filename).readlines()
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = \
[line.strip('\n') for line in source[lower_bound:lineno]]
context_line = source[lineno].strip('\n')
post_context = \
[line.strip('\n') for line in source[lineno + 1:upper_bound]]
return lower_bound, pre_context, context_line, post_context
except (OSError, IOError, IndexError):
return None, [], None, []
exception_type, exception_value, tback = sys.exc_info()
frames = []
while tback is not None:
filename = tback.tb_frame.f_code.co_filename
function = tback.tb_frame.f_code.co_name
lineno = tback.tb_lineno - 1
# hack to get correct line number for templates
lineno += tback.tb_frame.f_locals.get("__lineoffset__", 0)
pre_context_lineno, pre_context, context_line, post_context = \
_get_lines_from_file(filename, lineno, 7)
if '__hidetraceback__' not in tback.tb_frame.f_locals:
frames.append(web.storage({
'tback': tback,
'filename': filename,
'function': function,
'lineno': lineno,
'vars': tback.tb_frame.f_locals,
'id': id(tback),
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
'pre_context_lineno': pre_context_lineno,
}))
tback = tback.tb_next
frames.reverse()
urljoin = urlparse.urljoin
def prettify(x):
try:
out = pprint.pformat(x)
except Exception, e:
out = '[could not display: <' + e.__class__.__name__ + \
': '+str(e)+'>]'
return out
global djangoerror_r
if djangoerror_r is None:
djangoerror_r = Template(djangoerror_t, filename=__file__, filter=websafe)
t = djangoerror_r
globals = {'ctx': web.ctx, 'web':web, 'dict':dict, 'str':str, 'prettify': prettify}
t.t.func_globals.update(globals)
return t(exception_type, exception_value, frames)
def debugerror():
"""
A replacement for `internalerror` that presents a nice page with lots
of debug information for the programmer.
(Based on the beautiful 500 page from [Django](http://djangoproject.com/),
designed by [Wilson Miner](http://wilsonminer.com/).)
"""
return web._InternalError(djangoerror())
def emailerrors(to_address, olderror, from_address=None):
"""
Wraps the old `internalerror` handler (pass as `olderror`) to
additionally email all errors to `to_address`, to aid in
debugging production websites.
Emails contain a normal text traceback as well as an
attachment containing the nice `debugerror` page.
"""
from_address = from_address or to_address
def emailerrors_internal():
error = olderror()
tb = sys.exc_info()
error_name = tb[0]
error_value = tb[1]
tb_txt = ''.join(traceback.format_exception(*tb))
path = web.ctx.path
request = web.ctx.method + ' ' + web.ctx.home + web.ctx.fullpath
message = "\n%s\n\n%s\n\n" % (request, tb_txt)
sendmail(
"your buggy site <%s>" % from_address,
"the bugfixer <%s>" % to_address,
"bug: %(error_name)s: %(error_value)s (%(path)s)" % locals(),
message,
attachments=[
dict(filename="bug.html", content=safestr(djangoerror()))
],
)
return error
return emailerrors_internal
if __name__ == "__main__":
urls = (
'/', 'index'
)
from application import application
app = application(urls, globals())
app.internalerror = debugerror
class index:
def GET(self):
thisdoesnotexist
app.run()

BIN
web/debugerror.pyc Normal file

Binary file not shown.

410
web/form.py Normal file
View File

@ -0,0 +1,410 @@
"""
HTML forms
(part of web.py)
"""
import copy, re
import webapi as web
import utils, net
def attrget(obj, attr, value=None):
try:
if hasattr(obj, 'has_key') and obj.has_key(attr):
return obj[attr]
except TypeError:
# Handle the case where has_key takes different number of arguments.
# This is the case with Model objects on appengine. See #134
pass
if hasattr(obj, attr):
return getattr(obj, attr)
return value
class Form(object):
r"""
HTML form.
>>> f = Form(Textbox("x"))
>>> f.render()
u'<table>\n <tr><th><label for="x">x</label></th><td><input type="text" id="x" name="x"/></td></tr>\n</table>'
"""
def __init__(self, *inputs, **kw):
self.inputs = inputs
self.valid = True
self.note = None
self.validators = kw.pop('validators', [])
def __call__(self, x=None):
o = copy.deepcopy(self)
if x: o.validates(x)
return o
def render(self):
out = ''
out += self.rendernote(self.note)
out += '<table>\n'
for i in self.inputs:
html = utils.safeunicode(i.pre) + i.render() + self.rendernote(i.note) + utils.safeunicode(i.post)
if i.is_hidden():
out += ' <tr style="display: none;"><th></th><td>%s</td></tr>\n' % (html)
else:
out += ' <tr><th><label for="%s">%s</label></th><td>%s</td></tr>\n' % (i.id, net.websafe(i.description), html)
out += "</table>"
return out
def render_css(self):
out = []
out.append(self.rendernote(self.note))
for i in self.inputs:
if not i.is_hidden():
out.append('<label for="%s">%s</label>' % (i.id, net.websafe(i.description)))
out.append(i.pre)
out.append(i.render())
out.append(self.rendernote(i.note))
out.append(i.post)
out.append('\n')
return ''.join(out)
def rendernote(self, note):
if note: return '<strong class="wrong">%s</strong>' % net.websafe(note)
else: return ""
def validates(self, source=None, _validate=True, **kw):
source = source or kw or web.input()
out = True
for i in self.inputs:
v = attrget(source, i.name)
if _validate:
out = i.validate(v) and out
else:
i.set_value(v)
if _validate:
out = out and self._validate(source)
self.valid = out
return out
def _validate(self, value):
self.value = value
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def fill(self, source=None, **kw):
return self.validates(source, _validate=False, **kw)
def __getitem__(self, i):
for x in self.inputs:
if x.name == i: return x
raise KeyError, i
def __getattr__(self, name):
# don't interfere with deepcopy
inputs = self.__dict__.get('inputs') or []
for x in inputs:
if x.name == name: return x
raise AttributeError, name
def get(self, i, default=None):
try:
return self[i]
except KeyError:
return default
def _get_d(self): #@@ should really be form.attr, no?
return utils.storage([(i.name, i.get_value()) for i in self.inputs])
d = property(_get_d)
class Input(object):
def __init__(self, name, *validators, **attrs):
self.name = name
self.validators = validators
self.attrs = attrs = AttributeList(attrs)
self.description = attrs.pop('description', name)
self.value = attrs.pop('value', None)
self.pre = attrs.pop('pre', "")
self.post = attrs.pop('post', "")
self.note = None
self.id = attrs.setdefault('id', self.get_default_id())
if 'class_' in attrs:
attrs['class'] = attrs['class_']
del attrs['class_']
def is_hidden(self):
return False
def get_type(self):
raise NotImplementedError
def get_default_id(self):
return self.name
def validate(self, value):
self.set_value(value)
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def set_value(self, value):
self.value = value
def get_value(self):
return self.value
def render(self):
attrs = self.attrs.copy()
attrs['type'] = self.get_type()
if self.value is not None:
attrs['value'] = self.value
attrs['name'] = self.name
return '<input %s/>' % attrs
def rendernote(self, note):
if note: return '<strong class="wrong">%s</strong>' % net.websafe(note)
else: return ""
def addatts(self):
# add leading space for backward-compatibility
return " " + str(self.attrs)
class AttributeList(dict):
"""List of atributes of input.
>>> a = AttributeList(type='text', name='x', value=20)
>>> a
<attrs: 'type="text" name="x" value="20"'>
"""
def copy(self):
return AttributeList(self)
def __str__(self):
return " ".join(['%s="%s"' % (k, net.websafe(v)) for k, v in self.items()])
def __repr__(self):
return '<attrs: %s>' % repr(str(self))
class Textbox(Input):
"""Textbox input.
>>> Textbox(name='foo', value='bar').render()
u'<input type="text" id="foo" value="bar" name="foo"/>'
>>> Textbox(name='foo', value=0).render()
u'<input type="text" id="foo" value="0" name="foo"/>'
"""
def get_type(self):
return 'text'
class Password(Input):
"""Password input.
>>> Password(name='password', value='secret').render()
u'<input type="password" id="password" value="secret" name="password"/>'
"""
def get_type(self):
return 'password'
class Textarea(Input):
"""Textarea input.
>>> Textarea(name='foo', value='bar').render()
u'<textarea id="foo" name="foo">bar</textarea>'
"""
def render(self):
attrs = self.attrs.copy()
attrs['name'] = self.name
value = net.websafe(self.value or '')
return '<textarea %s>%s</textarea>' % (attrs, value)
class Dropdown(Input):
r"""Dropdown/select input.
>>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render()
u'<select id="foo" name="foo">\n <option value="a">a</option>\n <option selected="selected" value="b">b</option>\n <option value="c">c</option>\n</select>\n'
>>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], value='b').render()
u'<select id="foo" name="foo">\n <option value="a">aa</option>\n <option selected="selected" value="b">bb</option>\n <option value="c">cc</option>\n</select>\n'
"""
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Dropdown, self).__init__(name, *validators, **attrs)
def render(self):
attrs = self.attrs.copy()
attrs['name'] = self.name
x = '<select %s>\n' % attrs
for arg in self.args:
x += self._render_option(arg)
x += '</select>\n'
return x
def _render_option(self, arg, indent=' '):
if isinstance(arg, (tuple, list)):
value, desc= arg
else:
value, desc = arg, arg
if self.value == value or (isinstance(self.value, list) and value in self.value):
select_p = ' selected="selected"'
else:
select_p = ''
return indent + '<option%s value="%s">%s</option>\n' % (select_p, net.websafe(value), net.websafe(desc))
class GroupedDropdown(Dropdown):
r"""Grouped Dropdown/select input.
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', ('Volvo', 'Saab')), ('German Cars', ('Mercedes', 'Audi'))), value='Audi').render()
u'<select id="car_type" name="car_type">\n <optgroup label="Swedish Cars">\n <option value="Volvo">Volvo</option>\n <option value="Saab">Saab</option>\n </optgroup>\n <optgroup label="German Cars">\n <option value="Mercedes">Mercedes</option>\n <option selected="selected" value="Audi">Audi</option>\n </optgroup>\n</select>\n'
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 'Audi')))), value='a').render()
u'<select id="car_type" name="car_type">\n <optgroup label="Swedish Cars">\n <option value="v">Volvo</option>\n <option value="s">Saab</option>\n </optgroup>\n <optgroup label="German Cars">\n <option value="m">Mercedes</option>\n <option selected="selected" value="a">Audi</option>\n </optgroup>\n</select>\n'
"""
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Dropdown, self).__init__(name, *validators, **attrs)
def render(self):
attrs = self.attrs.copy()
attrs['name'] = self.name
x = '<select %s>\n' % attrs
for label, options in self.args:
x += ' <optgroup label="%s">\n' % net.websafe(label)
for arg in options:
x += self._render_option(arg, indent = ' ')
x += ' </optgroup>\n'
x += '</select>\n'
return x
class Radio(Input):
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Radio, self).__init__(name, *validators, **attrs)
def render(self):
x = '<span>'
for arg in self.args:
if isinstance(arg, (tuple, list)):
value, desc= arg
else:
value, desc = arg, arg
attrs = self.attrs.copy()
attrs['name'] = self.name
attrs['type'] = 'radio'
attrs['value'] = value
if self.value == value:
attrs['checked'] = 'checked'
x += '<input %s/> %s' % (attrs, net.websafe(desc))
x += '</span>'
return x
class Checkbox(Input):
"""Checkbox input.
>>> Checkbox('foo', value='bar', checked=True).render()
u'<input checked="checked" type="checkbox" id="foo_bar" value="bar" name="foo"/>'
>>> Checkbox('foo', value='bar').render()
u'<input type="checkbox" id="foo_bar" value="bar" name="foo"/>'
>>> c = Checkbox('foo', value='bar')
>>> c.validate('on')
True
>>> c.render()
u'<input checked="checked" type="checkbox" id="foo_bar" value="bar" name="foo"/>'
"""
def __init__(self, name, *validators, **attrs):
self.checked = attrs.pop('checked', False)
Input.__init__(self, name, *validators, **attrs)
def get_default_id(self):
value = utils.safestr(self.value or "")
return self.name + '_' + value.replace(' ', '_')
def render(self):
attrs = self.attrs.copy()
attrs['type'] = 'checkbox'
attrs['name'] = self.name
attrs['value'] = self.value
if self.checked:
attrs['checked'] = 'checked'
return '<input %s/>' % attrs
def set_value(self, value):
self.checked = bool(value)
def get_value(self):
return self.checked
class Button(Input):
"""HTML Button.
>>> Button("save").render()
u'<button id="save" name="save">save</button>'
>>> Button("action", value="save", html="<b>Save Changes</b>").render()
u'<button id="action" value="save" name="action"><b>Save Changes</b></button>'
"""
def __init__(self, name, *validators, **attrs):
super(Button, self).__init__(name, *validators, **attrs)
self.description = ""
def render(self):
attrs = self.attrs.copy()
attrs['name'] = self.name
if self.value is not None:
attrs['value'] = self.value
html = attrs.pop('html', None) or net.websafe(self.name)
return '<button %s>%s</button>' % (attrs, html)
class Hidden(Input):
"""Hidden Input.
>>> Hidden(name='foo', value='bar').render()
u'<input type="hidden" id="foo" value="bar" name="foo"/>'
"""
def is_hidden(self):
return True
def get_type(self):
return 'hidden'
class File(Input):
"""File input.
>>> File(name='f').render()
u'<input type="file" id="f" name="f"/>'
"""
def get_type(self):
return 'file'
class Validator:
def __deepcopy__(self, memo): return copy.copy(self)
def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals())
def valid(self, value):
try: return self.test(value)
except: return False
notnull = Validator("Required", bool)
class regexp(Validator):
def __init__(self, rexp, msg):
self.rexp = re.compile(rexp)
self.msg = msg
def valid(self, value):
return bool(self.rexp.match(value))
if __name__ == "__main__":
import doctest
doctest.testmod()

BIN
web/form.pyc Normal file

Binary file not shown.

150
web/http.py Normal file
View File

@ -0,0 +1,150 @@
"""
HTTP Utilities
(from web.py)
"""
__all__ = [
"expires", "lastmodified",
"prefixurl", "modified",
"changequery", "url",
"profiler",
]
import sys, os, threading, urllib, urlparse
try: import datetime
except ImportError: pass
import net, utils, webapi as web
def prefixurl(base=''):
"""
Sorry, this function is really difficult to explain.
Maybe some other time.
"""
url = web.ctx.path.lstrip('/')
for i in xrange(url.count('/')):
base += '../'
if not base:
base = './'
return base
def expires(delta):
"""
Outputs an `Expires` header for `delta` from now.
`delta` is a `timedelta` object or a number of seconds.
"""
if isinstance(delta, (int, long)):
delta = datetime.timedelta(seconds=delta)
date_obj = datetime.datetime.utcnow() + delta
web.header('Expires', net.httpdate(date_obj))
def lastmodified(date_obj):
"""Outputs a `Last-Modified` header for `datetime`."""
web.header('Last-Modified', net.httpdate(date_obj))
def modified(date=None, etag=None):
"""
Checks to see if the page has been modified since the version in the
requester's cache.
When you publish pages, you can include `Last-Modified` and `ETag`
with the date the page was last modified and an opaque token for
the particular version, respectively. When readers reload the page,
the browser sends along the modification date and etag value for
the version it has in its cache. If the page hasn't changed,
the server can just return `304 Not Modified` and not have to
send the whole page again.
This function takes the last-modified date `date` and the ETag `etag`
and checks the headers to see if they match. If they do, it returns
`True`, or otherwise it raises NotModified error. It also sets
`Last-Modified` and `ETag` output headers.
"""
try:
from __builtin__ import set
except ImportError:
# for python 2.3
from sets import Set as set
n = set([x.strip('" ') for x in web.ctx.env.get('HTTP_IF_NONE_MATCH', '').split(',')])
m = net.parsehttpdate(web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
validate = False
if etag:
if '*' in n or etag in n:
validate = True
if date and m:
# we subtract a second because
# HTTP dates don't have sub-second precision
if date-datetime.timedelta(seconds=1) <= m:
validate = True
if date: lastmodified(date)
if etag: web.header('ETag', '"' + etag + '"')
if validate:
raise web.notmodified()
else:
return True
def urlencode(query, doseq=0):
"""
Same as urllib.urlencode, but supports unicode strings.
>>> urlencode({'text':'foo bar'})
'text=foo+bar'
>>> urlencode({'x': [1, 2]}, doseq=True)
'x=1&x=2'
"""
def convert(value, doseq=False):
if doseq and isinstance(value, list):
return [convert(v) for v in value]
else:
return utils.safestr(value)
query = dict([(k, convert(v, doseq)) for k, v in query.items()])
return urllib.urlencode(query, doseq=doseq)
def changequery(query=None, **kw):
"""
Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
`/foo?a=3&b=2` -- the same URL but with the arguments you requested
changed.
"""
if query is None:
query = web.rawinput(method='get')
for k, v in kw.iteritems():
if v is None:
query.pop(k, None)
else:
query[k] = v
out = web.ctx.path
if query:
out += '?' + urlencode(query, doseq=True)
return out
def url(path=None, doseq=False, **kw):
"""
Makes url by concatenating web.ctx.homepath and path and the
query string created using the arguments.
"""
if path is None:
path = web.ctx.path
if path.startswith("/"):
out = web.ctx.homepath + path
else:
out = path
if kw:
out += '?' + urlencode(kw, doseq=doseq)
return out
def profiler(app):
"""Outputs basic profiling information at the bottom of each response."""
from utils import profile
def profile_internal(e, o):
out, result = profile(app)(e, o)
return list(out) + ['<pre>' + net.websafe(result) + '</pre>']
return profile_internal
if __name__ == "__main__":
import doctest
doctest.testmod()

BIN
web/http.pyc Normal file

Binary file not shown.

319
web/httpserver.py Normal file
View File

@ -0,0 +1,319 @@
__all__ = ["runsimple"]
import sys, os
from SimpleHTTPServer import SimpleHTTPRequestHandler
import urllib
import posixpath
import webapi as web
import net
import utils
def runbasic(func, server_address=("0.0.0.0", 8080)):
"""
Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
is hosted statically.
Based on [WsgiServer][ws] from [Colin Stewart][cs].
[ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
[cs]: http://www.owlfish.com/
"""
# Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
# Modified somewhat for simplicity
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse
import socket, errno
import traceback
class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def run_wsgi_app(self):
protocol, host, path, parameters, query, fragment = \
urlparse.urlparse('http://dummyhost%s' % self.path)
# we only use path, query
env = {'wsgi.version': (1, 0)
,'wsgi.url_scheme': 'http'
,'wsgi.input': self.rfile
,'wsgi.errors': sys.stderr
,'wsgi.multithread': 1
,'wsgi.multiprocess': 0
,'wsgi.run_once': 0
,'REQUEST_METHOD': self.command
,'REQUEST_URI': self.path
,'PATH_INFO': path
,'QUERY_STRING': query
,'CONTENT_TYPE': self.headers.get('Content-Type', '')
,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
,'REMOTE_ADDR': self.client_address[0]
,'SERVER_NAME': self.server.server_address[0]
,'SERVER_PORT': str(self.server.server_address[1])
,'SERVER_PROTOCOL': self.request_version
}
for http_header, http_value in self.headers.items():
env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \
http_value
# Setup the state
self.wsgi_sent_headers = 0
self.wsgi_headers = []
try:
# We have there environment, now invoke the application
result = self.server.app(env, self.wsgi_start_response)
try:
try:
for data in result:
if data:
self.wsgi_write_data(data)
finally:
if hasattr(result, 'close'):
result.close()
except socket.error, socket_err:
# Catch common network errors and suppress them
if (socket_err.args[0] in \
(errno.ECONNABORTED, errno.EPIPE)):
return
except socket.timeout, socket_timeout:
return
except:
print >> web.debug, traceback.format_exc(),
if (not self.wsgi_sent_headers):
# We must write out something!
self.wsgi_write_data(" ")
return
do_POST = run_wsgi_app
do_PUT = run_wsgi_app
do_DELETE = run_wsgi_app
def do_GET(self):
if self.path.startswith('/static/'):
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
else:
self.run_wsgi_app()
def wsgi_start_response(self, response_status, response_headers,
exc_info=None):
if (self.wsgi_sent_headers):
raise Exception \
("Headers already sent and start_response called again!")
# Should really take a copy to avoid changes in the application....
self.wsgi_headers = (response_status, response_headers)
return self.wsgi_write_data
def wsgi_write_data(self, data):
if (not self.wsgi_sent_headers):
status, headers = self.wsgi_headers
# Need to send header prior to data
status_code = status[:status.find(' ')]
status_msg = status[status.find(' ') + 1:]
self.send_response(int(status_code), status_msg)
for header, value in headers:
self.send_header(header, value)
self.end_headers()
self.wsgi_sent_headers = 1
# Send the data
self.wfile.write(data)
class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def __init__(self, func, server_address):
BaseHTTPServer.HTTPServer.__init__(self,
server_address,
WSGIHandler)
self.app = func
self.serverShuttingDown = 0
print "http://%s:%d/" % server_address
WSGIServer(func, server_address).serve_forever()
# The WSGIServer instance.
# Made global so that it can be stopped in embedded mode.
server = None
def runsimple(func, server_address=("0.0.0.0", 8080)):
"""
Runs [CherryPy][cp] WSGI server hosting WSGI app `func`.
The directory `static/` is hosted statically.
[cp]: http://www.cherrypy.org
"""
global server
func = StaticMiddleware(func)
func = LogMiddleware(func)
server = WSGIServer(server_address, func)
if server.ssl_adapter:
print "https://%s:%d/" % server_address
else:
print "http://%s:%d/" % server_address
try:
server.start()
except (KeyboardInterrupt, SystemExit):
server.stop()
server = None
def WSGIServer(server_address, wsgi_app):
"""Creates CherryPy WSGI server listening at `server_address` to serve `wsgi_app`.
This function can be overwritten to customize the webserver or use a different webserver.
"""
import wsgiserver
# Default values of wsgiserver.ssl_adapters uses cherrypy.wsgiserver
# prefix. Overwriting it make it work with web.wsgiserver.
wsgiserver.ssl_adapters = {
'builtin': 'web.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
'pyopenssl': 'web.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter',
}
server = wsgiserver.CherryPyWSGIServer(server_address, wsgi_app, server_name="localhost")
def create_ssl_adapter(cert, key):
# wsgiserver tries to import submodules as cherrypy.wsgiserver.foo.
# That doesn't work as not it is web.wsgiserver.
# Patching sys.modules temporarily to make it work.
import types
cherrypy = types.ModuleType('cherrypy')
cherrypy.wsgiserver = wsgiserver
sys.modules['cherrypy'] = cherrypy
sys.modules['cherrypy.wsgiserver'] = wsgiserver
from wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
adapter = pyOpenSSLAdapter(cert, key)
# We are done with our work. Cleanup the patches.
del sys.modules['cherrypy']
del sys.modules['cherrypy.wsgiserver']
return adapter
# SSL backward compatibility
if (server.ssl_adapter is None and
getattr(server, 'ssl_certificate', None) and
getattr(server, 'ssl_private_key', None)):
server.ssl_adapter = create_ssl_adapter(server.ssl_certificate, server.ssl_private_key)
server.nodelay = not sys.platform.startswith('java') # TCP_NODELAY isn't supported on the JVM
return server
class StaticApp(SimpleHTTPRequestHandler):
"""WSGI application for serving static files."""
def __init__(self, environ, start_response):
self.headers = []
self.environ = environ
self.start_response = start_response
def send_response(self, status, msg=""):
self.status = str(status) + " " + msg
def send_header(self, name, value):
self.headers.append((name, value))
def end_headers(self):
pass
def log_message(*a): pass
def __iter__(self):
environ = self.environ
self.path = environ.get('PATH_INFO', '')
self.client_address = environ.get('REMOTE_ADDR','-'), \
environ.get('REMOTE_PORT','-')
self.command = environ.get('REQUEST_METHOD', '-')
from cStringIO import StringIO
self.wfile = StringIO() # for capturing error
try:
path = self.translate_path(self.path)
etag = '"%s"' % os.path.getmtime(path)
client_etag = environ.get('HTTP_IF_NONE_MATCH')
self.send_header('ETag', etag)
if etag == client_etag:
self.send_response(304, "Not Modified")
self.start_response(self.status, self.headers)
raise StopIteration
except OSError:
pass # Probably a 404
f = self.send_head()
self.start_response(self.status, self.headers)
if f:
block_size = 16 * 1024
while True:
buf = f.read(block_size)
if not buf:
break
yield buf
f.close()
else:
value = self.wfile.getvalue()
yield value
class StaticMiddleware:
"""WSGI middleware for serving static files."""
def __init__(self, app, prefix='/static/'):
self.app = app
self.prefix = prefix
def __call__(self, environ, start_response):
path = environ.get('PATH_INFO', '')
path = self.normpath(path)
if path.startswith(self.prefix):
return StaticApp(environ, start_response)
else:
return self.app(environ, start_response)
def normpath(self, path):
path2 = posixpath.normpath(urllib.unquote(path))
if path.endswith("/"):
path2 += "/"
return path2
class LogMiddleware:
"""WSGI middleware for logging the status."""
def __init__(self, app):
self.app = app
self.format = '%s - - [%s] "%s %s %s" - %s'
from BaseHTTPServer import BaseHTTPRequestHandler
import StringIO
f = StringIO.StringIO()
class FakeSocket:
def makefile(self, *a):
return f
# take log_date_time_string method from BaseHTTPRequestHandler
self.log_date_time_string = BaseHTTPRequestHandler(FakeSocket(), None, None).log_date_time_string
def __call__(self, environ, start_response):
def xstart_response(status, response_headers, *args):
out = start_response(status, response_headers, *args)
self.log(status, environ)
return out
return self.app(environ, xstart_response)
def log(self, status, environ):
outfile = environ.get('wsgi.errors', web.debug)
req = environ.get('PATH_INFO', '_')
protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-')
method = environ.get('REQUEST_METHOD', '-')
host = "%s:%s" % (environ.get('REMOTE_ADDR','-'),
environ.get('REMOTE_PORT','-'))
time = self.log_date_time_string()
msg = self.format % (host, time, protocol, method, req, status)
print >> outfile, utils.safestr(msg)

BIN
web/httpserver.pyc Normal file

Binary file not shown.

193
web/net.py Normal file
View File

@ -0,0 +1,193 @@
"""
Network Utilities
(from web.py)
"""
__all__ = [
"validipaddr", "validipport", "validip", "validaddr",
"urlquote",
"httpdate", "parsehttpdate",
"htmlquote", "htmlunquote", "websafe",
]
import urllib, time
try: import datetime
except ImportError: pass
def validipaddr(address):
"""
Returns True if `address` is a valid IPv4 address.
>>> validipaddr('192.168.1.1')
True
>>> validipaddr('192.168.1.800')
False
>>> validipaddr('192.168.1')
False
"""
try:
octets = address.split('.')
if len(octets) != 4:
return False
for x in octets:
if not (0 <= int(x) <= 255):
return False
except ValueError:
return False
return True
def validipport(port):
"""
Returns True if `port` is a valid IPv4 port.
>>> validipport('9000')
True
>>> validipport('foo')
False
>>> validipport('1000000')
False
"""
try:
if not (0 <= int(port) <= 65535):
return False
except ValueError:
return False
return True
def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
"""Returns `(ip_address, port)` from string `ip_addr_port`"""
addr = defaultaddr
port = defaultport
ip = ip.split(":", 1)
if len(ip) == 1:
if not ip[0]:
pass
elif validipaddr(ip[0]):
addr = ip[0]
elif validipport(ip[0]):
port = int(ip[0])
else:
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
elif len(ip) == 2:
addr, port = ip
if not validipaddr(addr) and validipport(port):
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
port = int(port)
else:
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
return (addr, port)
def validaddr(string_):
"""
Returns either (ip_address, port) or "/path/to/socket" from string_
>>> validaddr('/path/to/socket')
'/path/to/socket'
>>> validaddr('8000')
('0.0.0.0', 8000)
>>> validaddr('127.0.0.1')
('127.0.0.1', 8080)
>>> validaddr('127.0.0.1:8000')
('127.0.0.1', 8000)
>>> validaddr('fff')
Traceback (most recent call last):
...
ValueError: fff is not a valid IP address/port
"""
if '/' in string_:
return string_
else:
return validip(string_)
def urlquote(val):
"""
Quotes a string for use in a URL.
>>> urlquote('://?f=1&j=1')
'%3A//%3Ff%3D1%26j%3D1'
>>> urlquote(None)
''
>>> urlquote(u'\u203d')
'%E2%80%BD'
"""
if val is None: return ''
if not isinstance(val, unicode): val = str(val)
else: val = val.encode('utf-8')
return urllib.quote(val)
def httpdate(date_obj):
"""
Formats a datetime object for use in HTTP headers.
>>> import datetime
>>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1))
'Thu, 01 Jan 1970 01:01:01 GMT'
"""
return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
def parsehttpdate(string_):
"""
Parses an HTTP date into a datetime object.
>>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT')
datetime.datetime(1970, 1, 1, 1, 1, 1)
"""
try:
t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
except ValueError:
return None
return datetime.datetime(*t[:6])
def htmlquote(text):
r"""
Encodes `text` for raw use in HTML.
>>> htmlquote(u"<'&\">")
u'&lt;&#39;&amp;&quot;&gt;'
"""
text = text.replace(u"&", u"&amp;") # Must be done first!
text = text.replace(u"<", u"&lt;")
text = text.replace(u">", u"&gt;")
text = text.replace(u"'", u"&#39;")
text = text.replace(u'"', u"&quot;")
return text
def htmlunquote(text):
r"""
Decodes `text` that's HTML quoted.
>>> htmlunquote(u'&lt;&#39;&amp;&quot;&gt;')
u'<\'&">'
"""
text = text.replace(u"&quot;", u'"')
text = text.replace(u"&#39;", u"'")
text = text.replace(u"&gt;", u">")
text = text.replace(u"&lt;", u"<")
text = text.replace(u"&amp;", u"&") # Must be done last!
return text
def websafe(val):
r"""Converts `val` so that it is safe for use in Unicode HTML.
>>> websafe("<'&\">")
u'&lt;&#39;&amp;&quot;&gt;'
>>> websafe(None)
u''
>>> websafe(u'\u203d')
u'\u203d'
>>> websafe('\xe2\x80\xbd')
u'\u203d'
"""
if val is None:
return u''
elif isinstance(val, str):
val = val.decode('utf-8')
elif not isinstance(val, unicode):
val = unicode(val)
return htmlquote(val)
if __name__ == "__main__":
import doctest
doctest.testmod()

BIN
web/net.pyc Normal file

Binary file not shown.

46
web/python23.py Normal file
View File

@ -0,0 +1,46 @@
"""Python 2.3 compatabilty"""
import threading
class threadlocal(object):
"""Implementation of threading.local for python2.3.
"""
def __getattribute__(self, name):
if name == "__dict__":
return threadlocal._getd(self)
else:
try:
return object.__getattribute__(self, name)
except AttributeError:
try:
return self.__dict__[name]
except KeyError:
raise AttributeError, name
def __setattr__(self, name, value):
self.__dict__[name] = value
def __delattr__(self, name):
try:
del self.__dict__[name]
except KeyError:
raise AttributeError, name
def _getd(self):
t = threading.currentThread()
if not hasattr(t, '_d'):
# using __dict__ of thread as thread local storage
t._d = {}
_id = id(self)
# there could be multiple instances of threadlocal.
# use id(self) as key
if _id not in t._d:
t._d[_id] = {}
return t._d[_id]
if __name__ == '__main__':
d = threadlocal()
d.x = 1
print d.__dict__
print d.x

358
web/session.py Normal file
View File

@ -0,0 +1,358 @@
"""
Session Management
(from web.py)
"""
import os, time, datetime, random, base64
import os.path
from copy import deepcopy
try:
import cPickle as pickle
except ImportError:
import pickle
try:
import hashlib
sha1 = hashlib.sha1
except ImportError:
import sha
sha1 = sha.new
import utils
import webapi as web
__all__ = [
'Session', 'SessionExpired',
'Store', 'DiskStore', 'DBStore',
]
web.config.session_parameters = utils.storage({
'cookie_name': 'webpy_session_id',
'cookie_domain': None,
'cookie_path' : None,
'timeout': 86400, #24 * 60 * 60, # 24 hours in seconds
'ignore_expiry': True,
'ignore_change_ip': True,
'secret_key': 'fLjUfxqXtfNoIldA0A0J',
'expired_message': 'Session expired',
'httponly': True,
'secure': False
})
class SessionExpired(web.HTTPError):
def __init__(self, message):
web.HTTPError.__init__(self, '200 OK', {}, data=message)
class Session(object):
"""Session management for web.py
"""
__slots__ = [
"store", "_initializer", "_last_cleanup_time", "_config", "_data",
"__getitem__", "__setitem__", "__delitem__"
]
def __init__(self, app, store, initializer=None):
self.store = store
self._initializer = initializer
self._last_cleanup_time = 0
self._config = utils.storage(web.config.session_parameters)
self._data = utils.threadeddict()
self.__getitem__ = self._data.__getitem__
self.__setitem__ = self._data.__setitem__
self.__delitem__ = self._data.__delitem__
if app:
app.add_processor(self._processor)
def __contains__(self, name):
return name in self._data
def __getattr__(self, name):
return getattr(self._data, name)
def __setattr__(self, name, value):
if name in self.__slots__:
object.__setattr__(self, name, value)
else:
setattr(self._data, name, value)
def __delattr__(self, name):
delattr(self._data, name)
def _processor(self, handler):
"""Application processor to setup session for every request"""
self._cleanup()
self._load()
try:
return handler()
finally:
self._save()
def _load(self):
"""Load the session from the store, by the id from cookie"""
cookie_name = self._config.cookie_name
cookie_domain = self._config.cookie_domain
cookie_path = self._config.cookie_path
httponly = self._config.httponly
self.session_id = web.cookies().get(cookie_name)
# protection against session_id tampering
if self.session_id and not self._valid_session_id(self.session_id):
self.session_id = None
self._check_expiry()
if self.session_id:
d = self.store[self.session_id]
self.update(d)
self._validate_ip()
if not self.session_id:
self.session_id = self._generate_session_id()
if self._initializer:
if isinstance(self._initializer, dict):
self.update(deepcopy(self._initializer))
elif hasattr(self._initializer, '__call__'):
self._initializer()
self.ip = web.ctx.ip
def _check_expiry(self):
# check for expiry
if self.session_id and self.session_id not in self.store:
if self._config.ignore_expiry:
self.session_id = None
else:
return self.expired()
def _validate_ip(self):
# check for change of IP
if self.session_id and self.get('ip', None) != web.ctx.ip:
if not self._config.ignore_change_ip:
return self.expired()
def _save(self):
if not self.get('_killed'):
self._setcookie(self.session_id)
self.store[self.session_id] = dict(self._data)
else:
self._setcookie(self.session_id, expires=-1)
def _setcookie(self, session_id, expires='', **kw):
cookie_name = self._config.cookie_name
cookie_domain = self._config.cookie_domain
cookie_path = self._config.cookie_path
httponly = self._config.httponly
secure = self._config.secure
web.setcookie(cookie_name, session_id, expires=expires, domain=cookie_domain, httponly=httponly, secure=secure, path=cookie_path)
def _generate_session_id(self):
"""Generate a random id for session"""
while True:
rand = os.urandom(16)
now = time.time()
secret_key = self._config.secret_key
session_id = sha1("%s%s%s%s" %(rand, now, utils.safestr(web.ctx.ip), secret_key))
session_id = session_id.hexdigest()
if session_id not in self.store:
break
return session_id
def _valid_session_id(self, session_id):
rx = utils.re_compile('^[0-9a-fA-F]+$')
return rx.match(session_id)
def _cleanup(self):
"""Cleanup the stored sessions"""
current_time = time.time()
timeout = self._config.timeout
if current_time - self._last_cleanup_time > timeout:
self.store.cleanup(timeout)
self._last_cleanup_time = current_time
def expired(self):
"""Called when an expired session is atime"""
self._killed = True
self._save()
raise SessionExpired(self._config.expired_message)
def kill(self):
"""Kill the session, make it no longer available"""
del self.store[self.session_id]
self._killed = True
class Store:
"""Base class for session stores"""
def __contains__(self, key):
raise NotImplementedError
def __getitem__(self, key):
raise NotImplementedError
def __setitem__(self, key, value):
raise NotImplementedError
def cleanup(self, timeout):
"""removes all the expired sessions"""
raise NotImplementedError
def encode(self, session_dict):
"""encodes session dict as a string"""
pickled = pickle.dumps(session_dict)
return base64.encodestring(pickled)
def decode(self, session_data):
"""decodes the data to get back the session dict """
pickled = base64.decodestring(session_data)
return pickle.loads(pickled)
class DiskStore(Store):
"""
Store for saving a session on disk.
>>> import tempfile
>>> root = tempfile.mkdtemp()
>>> s = DiskStore(root)
>>> s['a'] = 'foo'
>>> s['a']
'foo'
>>> time.sleep(0.01)
>>> s.cleanup(0.01)
>>> s['a']
Traceback (most recent call last):
...
KeyError: 'a'
"""
def __init__(self, root):
# if the storage root doesn't exists, create it.
if not os.path.exists(root):
os.makedirs(
os.path.abspath(root)
)
self.root = root
def _get_path(self, key):
if os.path.sep in key:
raise ValueError, "Bad key: %s" % repr(key)
return os.path.join(self.root, key)
def __contains__(self, key):
path = self._get_path(key)
return os.path.exists(path)
def __getitem__(self, key):
path = self._get_path(key)
if os.path.exists(path):
pickled = open(path).read()
return self.decode(pickled)
else:
raise KeyError, key
def __setitem__(self, key, value):
path = self._get_path(key)
pickled = self.encode(value)
try:
f = open(path, 'w')
try:
f.write(pickled)
finally:
f.close()
except IOError:
pass
def __delitem__(self, key):
path = self._get_path(key)
if os.path.exists(path):
os.remove(path)
def cleanup(self, timeout):
now = time.time()
for f in os.listdir(self.root):
path = self._get_path(f)
atime = os.stat(path).st_atime
if now - atime > timeout :
os.remove(path)
class DBStore(Store):
"""Store for saving a session in database
Needs a table with the following columns:
session_id CHAR(128) UNIQUE NOT NULL,
atime DATETIME NOT NULL default current_timestamp,
data TEXT
"""
def __init__(self, db, table_name):
self.db = db
self.table = table_name
def __contains__(self, key):
data = self.db.select(self.table, where="session_id=$key", vars=locals())
return bool(list(data))
def __getitem__(self, key):
now = datetime.datetime.now()
try:
s = self.db.select(self.table, where="session_id=$key", vars=locals())[0]
self.db.update(self.table, where="session_id=$key", atime=now, vars=locals())
except IndexError:
raise KeyError
else:
return self.decode(s.data)
def __setitem__(self, key, value):
pickled = self.encode(value)
now = datetime.datetime.now()
if key in self:
self.db.update(self.table, where="session_id=$key", data=pickled, vars=locals())
else:
self.db.insert(self.table, False, session_id=key, data=pickled )
def __delitem__(self, key):
self.db.delete(self.table, where="session_id=$key", vars=locals())
def cleanup(self, timeout):
timeout = datetime.timedelta(timeout/(24.0*60*60)) #timedelta takes numdays as arg
last_allowed_time = datetime.datetime.now() - timeout
self.db.delete(self.table, where="$last_allowed_time > atime", vars=locals())
class ShelfStore:
"""Store for saving session using `shelve` module.
import shelve
store = ShelfStore(shelve.open('session.shelf'))
XXX: is shelve thread-safe?
"""
def __init__(self, shelf):
self.shelf = shelf
def __contains__(self, key):
return key in self.shelf
def __getitem__(self, key):
atime, v = self.shelf[key]
self[key] = v # update atime
return v
def __setitem__(self, key, value):
self.shelf[key] = time.time(), value
def __delitem__(self, key):
try:
del self.shelf[key]
except KeyError:
pass
def cleanup(self, timeout):
now = time.time()
for k in self.shelf.keys():
atime, v = self.shelf[k]
if now - atime > timeout :
del self[k]
if __name__ == '__main__' :
import doctest
doctest.testmod()

BIN
web/session.pyc Normal file

Binary file not shown.

1515
web/template.py Normal file

File diff suppressed because it is too large Load Diff

BIN
web/template.pyc Normal file

Binary file not shown.

51
web/test.py Normal file
View File

@ -0,0 +1,51 @@
"""test utilities
(part of web.py)
"""
import unittest
import sys, os
import web
TestCase = unittest.TestCase
TestSuite = unittest.TestSuite
def load_modules(names):
return [__import__(name, None, None, "x") for name in names]
def module_suite(module, classnames=None):
"""Makes a suite from a module."""
if classnames:
return unittest.TestLoader().loadTestsFromNames(classnames, module)
elif hasattr(module, 'suite'):
return module.suite()
else:
return unittest.TestLoader().loadTestsFromModule(module)
def doctest_suite(module_names):
"""Makes a test suite from doctests."""
import doctest
suite = TestSuite()
for mod in load_modules(module_names):
suite.addTest(doctest.DocTestSuite(mod))
return suite
def suite(module_names):
"""Creates a suite from multiple modules."""
suite = TestSuite()
for mod in load_modules(module_names):
suite.addTest(module_suite(mod))
return suite
def runTests(suite):
runner = unittest.TextTestRunner()
return runner.run(suite)
def main(suite=None):
if not suite:
main_module = __import__('__main__')
# allow command line switches
args = [a for a in sys.argv[1:] if not a.startswith('-')]
suite = module_suite(main_module, args or None)
result = runTests(suite)
sys.exit(not result.wasSuccessful())

1526
web/utils.py Normal file

File diff suppressed because it is too large Load Diff

BIN
web/utils.pyc Normal file

Binary file not shown.

525
web/webapi.py Normal file
View File

@ -0,0 +1,525 @@
"""
Web API (wrapper around WSGI)
(from web.py)
"""
__all__ = [
"config",
"header", "debug",
"input", "data",
"setcookie", "cookies",
"ctx",
"HTTPError",
# 200, 201, 202
"OK", "Created", "Accepted",
"ok", "created", "accepted",
# 301, 302, 303, 304, 307
"Redirect", "Found", "SeeOther", "NotModified", "TempRedirect",
"redirect", "found", "seeother", "notmodified", "tempredirect",
# 400, 401, 403, 404, 405, 406, 409, 410, 412, 415
"BadRequest", "Unauthorized", "Forbidden", "NotFound", "NoMethod", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed", "UnsupportedMediaType",
"badrequest", "unauthorized", "forbidden", "notfound", "nomethod", "notacceptable", "conflict", "gone", "preconditionfailed", "unsupportedmediatype",
# 500
"InternalError",
"internalerror",
]
import sys, cgi, Cookie, pprint, urlparse, urllib
from utils import storage, storify, threadeddict, dictadd, intget, safestr
config = storage()
config.__doc__ = """
A configuration object for various aspects of web.py.
`debug`
: when True, enables reloading, disabled template caching and sets internalerror to debugerror.
"""
class HTTPError(Exception):
def __init__(self, status, headers={}, data=""):
ctx.status = status
for k, v in headers.items():
header(k, v)
self.data = data
Exception.__init__(self, status)
def _status_code(status, data=None, classname=None, docstring=None):
if data is None:
data = status.split(" ", 1)[1]
classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified
docstring = docstring or '`%s` status' % status
def __init__(self, data=data, headers={}):
HTTPError.__init__(self, status, headers, data)
# trick to create class dynamically with dynamic docstring.
return type(classname, (HTTPError, object), {
'__doc__': docstring,
'__init__': __init__
})
ok = OK = _status_code("200 OK", data="")
created = Created = _status_code("201 Created")
accepted = Accepted = _status_code("202 Accepted")
class Redirect(HTTPError):
"""A `301 Moved Permanently` redirect."""
def __init__(self, url, status='301 Moved Permanently', absolute=False):
"""
Returns a `status` redirect to the new URL.
`url` is joined with the base URL so that things like
`redirect("about") will work properly.
"""
newloc = urlparse.urljoin(ctx.path, url)
if newloc.startswith('/'):
if absolute:
home = ctx.realhome
else:
home = ctx.home
newloc = home + newloc
headers = {
'Content-Type': 'text/html',
'Location': newloc
}
HTTPError.__init__(self, status, headers, "")
redirect = Redirect
class Found(Redirect):
"""A `302 Found` redirect."""
def __init__(self, url, absolute=False):
Redirect.__init__(self, url, '302 Found', absolute=absolute)
found = Found
class SeeOther(Redirect):
"""A `303 See Other` redirect."""
def __init__(self, url, absolute=False):
Redirect.__init__(self, url, '303 See Other', absolute=absolute)
seeother = SeeOther
class NotModified(HTTPError):
"""A `304 Not Modified` status."""
def __init__(self):
HTTPError.__init__(self, "304 Not Modified")
notmodified = NotModified
class TempRedirect(Redirect):
"""A `307 Temporary Redirect` redirect."""
def __init__(self, url, absolute=False):
Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
tempredirect = TempRedirect
class BadRequest(HTTPError):
"""`400 Bad Request` error."""
message = "bad request"
def __init__(self, message=None):
status = "400 Bad Request"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, message or self.message)
badrequest = BadRequest
class Unauthorized(HTTPError):
"""`401 Unauthorized` error."""
message = "unauthorized"
def __init__(self):
status = "401 Unauthorized"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
unauthorized = Unauthorized
class Forbidden(HTTPError):
"""`403 Forbidden` error."""
message = "forbidden"
def __init__(self):
status = "403 Forbidden"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
forbidden = Forbidden
class _NotFound(HTTPError):
"""`404 Not Found` error."""
message = "not found"
def __init__(self, message=None):
status = '404 Not Found'
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, message or self.message)
def NotFound(message=None):
"""Returns HTTPError with '404 Not Found' error from the active application.
"""
if message:
return _NotFound(message)
elif ctx.get('app_stack'):
return ctx.app_stack[-1].notfound()
else:
return _NotFound()
notfound = NotFound
class NoMethod(HTTPError):
"""A `405 Method Not Allowed` error."""
def __init__(self, cls=None):
status = '405 Method Not Allowed'
headers = {}
headers['Content-Type'] = 'text/html'
methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
if cls:
methods = [method for method in methods if hasattr(cls, method)]
headers['Allow'] = ', '.join(methods)
data = None
HTTPError.__init__(self, status, headers, data)
nomethod = NoMethod
class NotAcceptable(HTTPError):
"""`406 Not Acceptable` error."""
message = "not acceptable"
def __init__(self):
status = "406 Not Acceptable"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
notacceptable = NotAcceptable
class Conflict(HTTPError):
"""`409 Conflict` error."""
message = "conflict"
def __init__(self):
status = "409 Conflict"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
conflict = Conflict
class Gone(HTTPError):
"""`410 Gone` error."""
message = "gone"
def __init__(self):
status = '410 Gone'
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
gone = Gone
class PreconditionFailed(HTTPError):
"""`412 Precondition Failed` error."""
message = "precondition failed"
def __init__(self):
status = "412 Precondition Failed"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
preconditionfailed = PreconditionFailed
class UnsupportedMediaType(HTTPError):
"""`415 Unsupported Media Type` error."""
message = "unsupported media type"
def __init__(self):
status = "415 Unsupported Media Type"
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, self.message)
unsupportedmediatype = UnsupportedMediaType
class _InternalError(HTTPError):
"""500 Internal Server Error`."""
message = "internal server error"
def __init__(self, message=None):
status = '500 Internal Server Error'
headers = {'Content-Type': 'text/html'}
HTTPError.__init__(self, status, headers, message or self.message)
def InternalError(message=None):
"""Returns HTTPError with '500 internal error' error from the active application.
"""
if message:
return _InternalError(message)
elif ctx.get('app_stack'):
return ctx.app_stack[-1].internalerror()
else:
return _InternalError()
internalerror = InternalError
def header(hdr, value, unique=False):
"""
Adds the header `hdr: value` with the response.
If `unique` is True and a header with that name already exists,
it doesn't add a new one.
"""
hdr, value = safestr(hdr), safestr(value)
# protection against HTTP response splitting attack
if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
raise ValueError, 'invalid characters in header'
if unique is True:
for h, v in ctx.headers:
if h.lower() == hdr.lower(): return
ctx.headers.append((hdr, value))
def rawinput(method=None):
"""Returns storage object with GET or POST arguments.
"""
method = method or "both"
from cStringIO import StringIO
def dictify(fs):
# hack to make web.input work with enctype='text/plain.
if fs.list is None:
fs.list = []
return dict([(k, fs[k]) for k in fs.keys()])
e = ctx.env.copy()
a = b = {}
if method.lower() in ['both', 'post', 'put']:
if e['REQUEST_METHOD'] in ['POST', 'PUT']:
if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
# since wsgi.input is directly passed to cgi.FieldStorage,
# it can not be called multiple times. Saving the FieldStorage
# object in ctx to allow calling web.input multiple times.
a = ctx.get('_fieldstorage')
if not a:
fp = e['wsgi.input']
a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
ctx._fieldstorage = a
else:
fp = StringIO(data())
a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
a = dictify(a)
if method.lower() in ['both', 'get']:
e['REQUEST_METHOD'] = 'GET'
b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
def process_fieldstorage(fs):
if isinstance(fs, list):
return [process_fieldstorage(x) for x in fs]
elif fs.filename is None:
return fs.value
else:
return fs
return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()])
def input(*requireds, **defaults):
"""
Returns a `storage` object with the GET and POST arguments.
See `storify` for how `requireds` and `defaults` work.
"""
_method = defaults.pop('_method', 'both')
out = rawinput(_method)
try:
defaults.setdefault('_unicode', True) # force unicode conversion by default.
return storify(out, *requireds, **defaults)
except KeyError:
raise badrequest()
def data():
"""Returns the data sent with the request."""
if 'data' not in ctx:
cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
ctx.data = ctx.env['wsgi.input'].read(cl)
return ctx.data
def setcookie(name, value, expires='', domain=None,
secure=False, httponly=False, path=None):
"""Sets a cookie."""
morsel = Cookie.Morsel()
name, value = safestr(name), safestr(value)
morsel.set(name, value, urllib.quote(value))
if expires < 0:
expires = -1000000000
morsel['expires'] = expires
morsel['path'] = path or ctx.homepath+'/'
if domain:
morsel['domain'] = domain
if secure:
morsel['secure'] = secure
value = morsel.OutputString()
if httponly:
value += '; httponly'
header('Set-Cookie', value)
def decode_cookie(value):
r"""Safely decodes a cookie value to unicode.
Tries us-ascii, utf-8 and io8859 encodings, in that order.
>>> decode_cookie('')
u''
>>> decode_cookie('asdf')
u'asdf'
>>> decode_cookie('foo \xC3\xA9 bar')
u'foo \xe9 bar'
>>> decode_cookie('foo \xE9 bar')
u'foo \xe9 bar'
"""
try:
# First try plain ASCII encoding
return unicode(value, 'us-ascii')
except UnicodeError:
# Then try UTF-8, and if that fails, ISO8859
try:
return unicode(value, 'utf-8')
except UnicodeError:
return unicode(value, 'iso8859', 'ignore')
def parse_cookies(http_cookie):
r"""Parse a HTTP_COOKIE header and return dict of cookie names and decoded values.
>>> sorted(parse_cookies('').items())
[]
>>> sorted(parse_cookies('a=1').items())
[('a', '1')]
>>> sorted(parse_cookies('a=1%202').items())
[('a', '1 2')]
>>> sorted(parse_cookies('a=Z%C3%A9Z').items())
[('a', 'Z\xc3\xa9Z')]
>>> sorted(parse_cookies('a=1; b=2; c=3').items())
[('a', '1'), ('b', '2'), ('c', '3')]
>>> sorted(parse_cookies('a=1; b=w("x")|y=z; c=3').items())
[('a', '1'), ('b', 'w('), ('c', '3')]
>>> sorted(parse_cookies('a=1; b=w(%22x%22)|y=z; c=3').items())
[('a', '1'), ('b', 'w("x")|y=z'), ('c', '3')]
>>> sorted(parse_cookies('keebler=E=mc2').items())
[('keebler', 'E=mc2')]
>>> sorted(parse_cookies(r'keebler="E=mc2; L=\"Loves\"; fudge=\012;"').items())
[('keebler', 'E=mc2; L="Loves"; fudge=\n;')]
"""
#print "parse_cookies"
if '"' in http_cookie:
# HTTP_COOKIE has quotes in it, use slow but correct cookie parsing
cookie = Cookie.SimpleCookie()
try:
cookie.load(http_cookie)
except Cookie.CookieError:
# If HTTP_COOKIE header is malformed, try at least to load the cookies we can by
# first splitting on ';' and loading each attr=value pair separately
cookie = Cookie.SimpleCookie()
for attr_value in http_cookie.split(';'):
try:
cookie.load(attr_value)
except Cookie.CookieError:
pass
cookies = dict((k, urllib.unquote(v.value)) for k, v in cookie.iteritems())
else:
# HTTP_COOKIE doesn't have quotes, use fast cookie parsing
cookies = {}
for key_value in http_cookie.split(';'):
key_value = key_value.split('=', 1)
if len(key_value) == 2:
key, value = key_value
cookies[key.strip()] = urllib.unquote(value.strip())
return cookies
def cookies(*requireds, **defaults):
r"""Returns a `storage` object with all the request cookies in it.
See `storify` for how `requireds` and `defaults` work.
This is forgiving on bad HTTP_COOKIE input, it tries to parse at least
the cookies it can.
The values are converted to unicode if _unicode=True is passed.
"""
# If _unicode=True is specified, use decode_cookie to convert cookie value to unicode
if defaults.get("_unicode") is True:
defaults['_unicode'] = decode_cookie
# parse cookie string and cache the result for next time.
if '_parsed_cookies' not in ctx:
http_cookie = ctx.env.get("HTTP_COOKIE", "")
ctx._parsed_cookies = parse_cookies(http_cookie)
try:
return storify(ctx._parsed_cookies, *requireds, **defaults)
except KeyError:
badrequest()
raise StopIteration
def debug(*args):
"""
Prints a prettyprinted version of `args` to stderr.
"""
try:
out = ctx.environ['wsgi.errors']
except:
out = sys.stderr
for arg in args:
print >> out, pprint.pformat(arg)
return ''
def _debugwrite(x):
try:
out = ctx.environ['wsgi.errors']
except:
out = sys.stderr
out.write(x)
debug.write = _debugwrite
ctx = context = threadeddict()
ctx.__doc__ = """
A `storage` object containing various information about the request:
`environ` (aka `env`)
: A dictionary containing the standard WSGI environment variables.
`host`
: The domain (`Host` header) requested by the user.
`home`
: The base path for the application.
`ip`
: The IP address of the requester.
`method`
: The HTTP method used.
`path`
: The path request.
`query`
: If there are no query arguments, the empty string. Otherwise, a `?` followed
by the query string.
`fullpath`
: The full path requested, including query arguments (`== path + query`).
### Response Data
`status` (default: "200 OK")
: The status code to be used in the response.
`headers`
: A list of 2-tuples to be used in the response.
`output`
: A string to be used as the response.
"""
if __name__ == "__main__":
import doctest
doctest.testmod()

BIN
web/webapi.pyc Normal file

Binary file not shown.

115
web/webopenid.py Normal file
View File

@ -0,0 +1,115 @@
"""openid.py: an openid library for web.py
Notes:
- This will create a file called .openid_secret_key in the
current directory with your secret key in it. If someone
has access to this file they can log in as any user. And
if the app can't find this file for any reason (e.g. you
moved the app somewhere else) then each currently logged
in user will get logged out.
- State must be maintained through the entire auth process
-- this means that if you have multiple web.py processes
serving one set of URLs or if you restart your app often
then log ins will fail. You have to replace sessions and
store for things to work.
- We set cookies starting with "openid_".
"""
import os
import random
import hmac
import __init__ as web
import openid.consumer.consumer
import openid.store.memstore
sessions = {}
store = openid.store.memstore.MemoryStore()
def _secret():
try:
secret = file('.openid_secret_key').read()
except IOError:
# file doesn't exist
secret = os.urandom(20)
file('.openid_secret_key', 'w').write(secret)
return secret
def _hmac(identity_url):
return hmac.new(_secret(), identity_url).hexdigest()
def _random_session():
n = random.random()
while n in sessions:
n = random.random()
n = str(n)
return n
def status():
oid_hash = web.cookies().get('openid_identity_hash', '').split(',', 1)
if len(oid_hash) > 1:
oid_hash, identity_url = oid_hash
if oid_hash == _hmac(identity_url):
return identity_url
return None
def form(openid_loc):
oid = status()
if oid:
return '''
<form method="post" action="%s">
<img src="http://openid.net/login-bg.gif" alt="OpenID" />
<strong>%s</strong>
<input type="hidden" name="action" value="logout" />
<input type="hidden" name="return_to" value="%s" />
<button type="submit">log out</button>
</form>''' % (openid_loc, oid, web.ctx.fullpath)
else:
return '''
<form method="post" action="%s">
<input type="text" name="openid" value=""
style="background: url(http://openid.net/login-bg.gif) no-repeat; padding-left: 18px; background-position: 0 50%%;" />
<input type="hidden" name="return_to" value="%s" />
<button type="submit">log in</button>
</form>''' % (openid_loc, web.ctx.fullpath)
def logout():
web.setcookie('openid_identity_hash', '', expires=-1)
class host:
def POST(self):
# unlike the usual scheme of things, the POST is actually called
# first here
i = web.input(return_to='/')
if i.get('action') == 'logout':
logout()
return web.redirect(i.return_to)
i = web.input('openid', return_to='/')
n = _random_session()
sessions[n] = {'webpy_return_to': i.return_to}
c = openid.consumer.consumer.Consumer(sessions[n], store)
a = c.begin(i.openid)
f = a.redirectURL(web.ctx.home, web.ctx.home + web.ctx.fullpath)
web.setcookie('openid_session_id', n)
return web.redirect(f)
def GET(self):
n = web.cookies('openid_session_id').openid_session_id
web.setcookie('openid_session_id', '', expires=-1)
return_to = sessions[n]['webpy_return_to']
c = openid.consumer.consumer.Consumer(sessions[n], store)
a = c.complete(web.input(), web.ctx.home + web.ctx.fullpath)
if a.status.lower() == 'success':
web.setcookie('openid_identity_hash', _hmac(a.identity_url) + ',' + a.identity_url)
del sessions[n]
return web.redirect(return_to)

BIN
web/webopenid.pyc Normal file

Binary file not shown.

70
web/wsgi.py Normal file
View File

@ -0,0 +1,70 @@
"""
WSGI Utilities
(from web.py)
"""
import os, sys
import http
import webapi as web
from utils import listget
from net import validaddr, validip
import httpserver
def runfcgi(func, addr=('localhost', 8000)):
"""Runs a WSGI function as a FastCGI server."""
import flup.server.fcgi as flups
return flups.WSGIServer(func, multiplexed=True, bindAddress=addr, debug=False).run()
def runscgi(func, addr=('localhost', 4000)):
"""Runs a WSGI function as an SCGI server."""
import flup.server.scgi as flups
return flups.WSGIServer(func, bindAddress=addr, debug=False).run()
def runwsgi(func):
"""
Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server,
as appropriate based on context and `sys.argv`.
"""
if os.environ.has_key('SERVER_SOFTWARE'): # cgi
os.environ['FCGI_FORCE_CGI'] = 'Y'
if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
or os.environ.has_key('SERVER_SOFTWARE')):
return runfcgi(func, None)
if 'fcgi' in sys.argv or 'fastcgi' in sys.argv:
args = sys.argv[1:]
if 'fastcgi' in args: args.remove('fastcgi')
elif 'fcgi' in args: args.remove('fcgi')
if args:
return runfcgi(func, validaddr(args[0]))
else:
return runfcgi(func, None)
if 'scgi' in sys.argv:
args = sys.argv[1:]
args.remove('scgi')
if args:
return runscgi(func, validaddr(args[0]))
else:
return runscgi(func)
return httpserver.runsimple(func, validip(listget(sys.argv, 1, '')))
def _is_dev_mode():
# Some embedded python interpreters won't have sys.arv
# For details, see https://github.com/webpy/webpy/issues/87
argv = getattr(sys, "argv", [])
# quick hack to check if the program is running in dev mode.
if os.environ.has_key('SERVER_SOFTWARE') \
or os.environ.has_key('PHP_FCGI_CHILDREN') \
or 'fcgi' in argv or 'fastcgi' in argv \
or 'mod_wsgi' in argv:
return False
return True
# When running the builtin-server, enable debug mode if not already set.
web.config.setdefault('debug', _is_dev_mode())

BIN
web/wsgi.pyc Normal file

Binary file not shown.

2219
web/wsgiserver/__init__.py Normal file

File diff suppressed because it is too large Load Diff

BIN
web/wsgiserver/__init__.pyc Normal file

Binary file not shown.

View File

@ -0,0 +1,72 @@
"""A library for integrating Python's builtin ``ssl`` library with CherryPy.
The ssl module must be importable for SSL functionality.
To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of
``BuiltinSSLAdapter``.
"""
try:
import ssl
except ImportError:
ssl = None
from cherrypy import wsgiserver
class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating Python's builtin ssl module with CherryPy."""
certificate = None
"""The filename of the server SSL certificate."""
private_key = None
"""The filename of the server's private key file."""
def __init__(self, certificate, private_key, certificate_chain=None):
if ssl is None:
raise ImportError("You must install the ssl module to use HTTPS.")
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
def bind(self, sock):
"""Wrap and return the given socket."""
return sock
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
try:
s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
server_side=True, certfile=self.certificate,
keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23)
except ssl.SSLError, e:
if e.errno == ssl.SSL_ERROR_EOF:
# This is almost certainly due to the cherrypy engine
# 'pinging' the socket to assert it's connectable;
# the 'ping' isn't SSL.
return None, {}
elif e.errno == ssl.SSL_ERROR_SSL:
if e.args[1].endswith('http request'):
# The client is speaking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError
raise
return s, self.get_environ(s)
# TODO: fill this out more with mod ssl env
def get_environ(self, sock):
"""Create WSGI environ entries to be merged into each request."""
cipher = sock.cipher()
ssl_environ = {
"wsgi.url_scheme": "https",
"HTTPS": "on",
'SSL_PROTOCOL': cipher[1],
'SSL_CIPHER': cipher[0]
## SSL_VERSION_INTERFACE string The mod_ssl program version
## SSL_VERSION_LIBRARY string The OpenSSL program version
}
return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1):
return wsgiserver.CP_fileobject(sock, mode, bufsize)

View File

@ -0,0 +1,256 @@
"""A library for integrating pyOpenSSL with CherryPy.
The OpenSSL module must be importable for SSL functionality.
You can obtain it from http://pyopenssl.sourceforge.net/
To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
SSLAdapter. There are two ways to use SSL:
Method One
----------
* ``ssl_adapter.context``: an instance of SSL.Context.
If this is not None, it is assumed to be an SSL.Context instance,
and will be passed to SSL.Connection on bind(). The developer is
responsible for forming a valid Context object. This approach is
to be preferred for more flexibility, e.g. if the cert and key are
streams instead of files, or need decryption, or SSL.SSLv3_METHOD
is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
the pyOpenSSL documentation for complete options.
Method Two (shortcut)
---------------------
* ``ssl_adapter.certificate``: the filename of the server SSL certificate.
* ``ssl_adapter.private_key``: the filename of the server's private key file.
Both are None by default. If ssl_adapter.context is None, but .private_key
and .certificate are both given and valid, they will be read, and the
context will be automatically created from them.
"""
import socket
import threading
import time
from cherrypy import wsgiserver
try:
from OpenSSL import SSL
from OpenSSL import crypto
except ImportError:
SSL = None
class SSL_fileobject(wsgiserver.CP_fileobject):
"""SSL file object attached to a socket object."""
ssl_timeout = 3
ssl_retry = .01
def _safe_call(self, is_reader, call, *args, **kwargs):
"""Wrap the given call with SSL error-trapping.
is_reader: if False EOF errors will be raised. If True, EOF errors
will return "" (to emulate normal sockets).
"""
start = time.time()
while True:
try:
return call(*args, **kwargs)
except SSL.WantReadError:
# Sleep and try again. This is dangerous, because it means
# the rest of the stack has no way of differentiating
# between a "new handshake" error and "client dropped".
# Note this isn't an endless loop: there's a timeout below.
time.sleep(self.ssl_retry)
except SSL.WantWriteError:
time.sleep(self.ssl_retry)
except SSL.SysCallError, e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
errnum = e.args[0]
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return ""
raise socket.error(errnum)
except SSL.Error, e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
thirdarg = None
try:
thirdarg = e.args[0][0][2]
except IndexError:
pass
if thirdarg == 'http request':
# The client is talking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError()
raise wsgiserver.FatalSSLAlert(*e.args)
except:
raise
if time.time() - start > self.ssl_timeout:
raise socket.timeout("timed out")
def recv(self, *args, **kwargs):
buf = []
r = super(SSL_fileobject, self).recv
while True:
data = self._safe_call(True, r, *args, **kwargs)
buf.append(data)
p = self._sock.pending()
if not p:
return "".join(buf)
def sendall(self, *args, **kwargs):
return self._safe_call(False, super(SSL_fileobject, self).sendall,
*args, **kwargs)
def send(self, *args, **kwargs):
return self._safe_call(False, super(SSL_fileobject, self).send,
*args, **kwargs)
class SSLConnection:
"""A thread-safe wrapper for an SSL.Connection.
``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
"""
def __init__(self, *args):
self._ssl_conn = SSL.Connection(*args)
self._lock = threading.RLock()
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
'renegotiate', 'bind', 'listen', 'connect', 'accept',
'setblocking', 'fileno', 'close', 'get_cipher_list',
'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
'makefile', 'get_app_data', 'set_app_data', 'state_string',
'sock_shutdown', 'get_peer_certificate', 'want_read',
'want_write', 'set_connect_state', 'set_accept_state',
'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
exec("""def %s(self, *args):
self._lock.acquire()
try:
return self._ssl_conn.%s(*args)
finally:
self._lock.release()
""" % (f, f))
def shutdown(self, *args):
self._lock.acquire()
try:
# pyOpenSSL.socket.shutdown takes no args
return self._ssl_conn.shutdown()
finally:
self._lock.release()
class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating pyOpenSSL with CherryPy."""
context = None
"""An instance of SSL.Context."""
certificate = None
"""The filename of the server SSL certificate."""
private_key = None
"""The filename of the server's private key file."""
certificate_chain = None
"""Optional. The filename of CA's intermediate certificate bundle.
This is needed for cheaper "chained root" SSL certificates, and should be
left as None if not required."""
def __init__(self, certificate, private_key, certificate_chain=None):
if SSL is None:
raise ImportError("You must install pyOpenSSL to use HTTPS.")
self.context = None
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
self._environ = None
def bind(self, sock):
"""Wrap and return the given socket."""
if self.context is None:
self.context = self.get_context()
conn = SSLConnection(self.context, sock)
self._environ = self.get_environ()
return conn
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
return sock, self._environ.copy()
def get_context(self):
"""Return an SSL.Context from self attributes."""
# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
c = SSL.Context(SSL.SSLv23_METHOD)
c.use_privatekey_file(self.private_key)
if self.certificate_chain:
c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate)
return c
def get_environ(self):
"""Return WSGI environ entries to be merged into each request."""
ssl_environ = {
"HTTPS": "on",
# pyOpenSSL doesn't provide access to any of these AFAICT
## 'SSL_PROTOCOL': 'SSLv2',
## SSL_CIPHER string The cipher specification name
## SSL_VERSION_INTERFACE string The mod_ssl program version
## SSL_VERSION_LIBRARY string The OpenSSL program version
}
if self.certificate:
# Server certificate attributes
cert = open(self.certificate, 'rb').read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
ssl_environ.update({
'SSL_SERVER_M_VERSION': cert.get_version(),
'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
## 'SSL_SERVER_V_START': Validity of server's certificate (start time),
## 'SSL_SERVER_V_END': Validity of server's certificate (end time),
})
for prefix, dn in [("I", cert.get_issuer()),
("S", cert.get_subject())]:
# X509Name objects don't seem to have a way to get the
# complete DN string. Use str() and slice it instead,
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
dnstr = str(dn)[18:-2]
wsgikey = 'SSL_SERVER_%s_DN' % prefix
ssl_environ[wsgikey] = dnstr
# The DN should be of the form: /k1=v1/k2=v2, but we must allow
# for any value to contain slashes itself (in a URL).
while dnstr:
pos = dnstr.rfind("=")
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
pos = dnstr.rfind("/")
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
ssl_environ[wsgikey] = value
return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1):
if SSL and isinstance(sock, SSL.ConnectionType):
timeout = sock.gettimeout()
f = SSL_fileobject(sock, mode, bufsize)
f.ssl_timeout = timeout
return f
else:
return wsgiserver.CP_fileobject(sock, mode, bufsize)