""" URL handler for the flock branch of the URI tree.
Note that these classes are instantiated once only per application when the
application is initialised.
An application's instance is shared by all requests.
As a result instance variables must not be used to store data which is
dependant on the request.
In addition CherryPy is a multi-threaded framework, so that instance
variables are shared by all threads. Only the cherrypy thread-local
request and response objects and method variables are per-thread.
Copyright PR Hardman 2008-2019. All rights reserved.
Licence - see licence.txt
"""
import time
import json
import cherrypy
from ppdb.lib import util, memlib, flocklib, fbklib
from ppdb.html import flockhtml
DEBUG = True
#~ DEBUG = False
class FlockBase():
""" Provides generic methods for the 'flock' branch of the URI tree.
Note that because there is only one instance of the subclass per
application then instance variables must only be those variables
which are truly per-instance and not per-method.
"""
def __init__(self, config, path_conf=None):
""" Default constructor. """
if path_conf:
self._cp_config = path_conf
self.appconf = config
self.uri_root = config.get('uri_root', '')
self.grouping = config.get('group', '')
self.tc_grouping = self.grouping.title()
self.anitype = config.get('sheep', '')
self.breed = self.appconf.get('breedclass', None)
self.disclaimer = config.get('disclaimer', '')
self.flock_sort_params = list(flocklib.FLOCK_SORT_TRANSLATE.keys())
def find_page(self, message='', status=200, requestpath=''):
"""
Return a Find Flock html page.
The page generated here is also used as an error page.
"""
if not requestpath:
requestpath = cherrypy.request.path_info
cherrypy.response.status = status
if not message:
if status == 400:
message = "%s Bad Request" % status
elif status == 404:
message = "%s Not Found" % status
page_info = {'title': self.tc_grouping,
'requestpath': requestpath,
'resource': self.grouping,
'class': self.__class__.__name__,
'crumb': '',
'crumbpath': '',
'message': message,}
page_info.update(util.html_appconf_items(self.appconf))
return flockhtml.find_page(page_info)
def check_id(self, arg, one_only=True):
""" Test that the id is good. If the rid starts with 'b/' then it is assumeed to
be a breder name or number.
Returns a list, empty if no rid was supplied, tuples of flock_no, flock_name if
the rid is good, or a message string if not.
"""
if not arg:
return []
try:
if arg[0:2] == 'b/':
arg = arg[2:]
flocklist = flocklib.find_flock_by_breeder(arg)
else:
flocklist = flocklib.find_flock(arg)
except util.PPDWarning as err:
return "Nothing found matching '%s': %s" % (arg, err.msg)
if not flocklist:
return "Nothing found matching '%s'" % arg
if one_only and len(flocklist) > 1:
return "More than one flock found matching '%s'" % arg
return flocklist
def summary_page(self, rid, flocklist, start, step, sortby, sortdir):
"""
Return a summary page for the flocks in 'result', using 'start', 'step',
'sortby' and 'sortdir' to control the flocks displayed.
"""
begin = time.time()
# Now collect the entire data set - necessary because without all
# the data items the list cannot be sorted as specified by 'sortby'
# get_flock_data_for_list() takes a list of flock numbers.
full_set = flocklib.get_flock_data_for_list([row[0] for row in flocklist], sortby,
sortdir)
display_set = util.get_display_set(full_set, start, step)
if isinstance(display_set, tuple):
display_nos, resultcount, start, step = display_set
else:
return self.find_page("Invalid parameter: %s" % display_set, 404)
page_data = {'flock_data': display_nos}
# Now set the other page_data items
page_info = {'count':resultcount,
'start':start, 'step':step,
'sortby': sortby, 'sortdir': sortdir,
'requestpath': cherrypy.request.path_info,
'rid': rid,
'crumb': 'Find Flock',
'crumbpath': 'flock',
'time': "%.3f" % (time.time() - begin),}
page_info.update(util.html_appconf_items(self.appconf))
return flockhtml.flock_summary_page(page_info, page_data)
class Flock(FlockBase):
""" Flock root class and methods.
This class is the root of the 'flock' branch of the tree
so any configuration applied here will also apply to all subsidiary
branches unless explicitly overriden. So take care when modifying
the config for this class!
"""
exposed = True
def __init__(self, config, path_conf=None):
super(Flock, self).__init__(config, path_conf)
self.apptype = self.appconf['apptype']
popargs = ('rid',)
def GET(self, rid=None, start=0, step=20, sortby='flkno', sortdir='asc'):
"""
1) A GET with no rid will return an HTML search page for the OFB, or the data form
displaying the most recent flock for the Database.
2) For the OFB a GET with a rid that resolves to one or more sheep will return
a list of summary data for those sheep.
If specified, 'start' must be numeric and is the position in the
list of the next sheep to be shown. Defaults to 0
If specified, 'step' is the number of sheeps to be shown per page.
Defaults to 20. A value of 0 indicates that all results should be
shown on one page.
If specified, 'sortby' must be in self.sheep_sort_params
If specified, 'sortdir' must be 'asc' (ascending) or 'desc' (descending).
3) For the Database:
- if the Accept header is 'application/json' a GET with a rid that
resolves to one or more flocks, or to one or more flock owners if the rid is
prefixed with 'b/', will return the data for the rid(s) as a JSON object.
- if the Accept header is not 'application/json' then if the rid resolves to
multiple sheep an HTML summary page will be returned as for the OFB,
otherwise a flock data form will be returned for the single flock.
"""
if DEBUG:
print('rid: %s' % rid)
# Get a list of tuples of flock numbers matching 'rid'
accept = cherrypy.request.headers.get("Accept", '')
if DEBUG:
print('accept: %s' % accept)
if 'json' in accept:
cherrypy.response.headers['content-type'] = 'application/json'
flocklist = self.check_id(rid, False)
else:
flocklist = self.check_id(rid, False)
if DEBUG:
print("check_rid flocklist: %s" % flocklist)
if not isinstance(flocklist, list):
# Some sort of error or unexpected case.
if 'json' in accept:
cherrypy.response.status = 404
return json.dumps({"message": flocklist}).encode('utf-8')
return self.find_page(flocklist, 404)
if self.apptype == 'ofb':
if not flocklist:
return self.find_page()
return self.summary_page(rid, flocklist, start, step, sortby, sortdir)
if len(flocklist) > 1:
if 'json' in accept:
if rid[0:2] == "b/":
# A breeder search
data = util.rows2dicts(flocklist)
else:
data = util.rows2dicts(flocklib.get_flocks_current_owners(flocklist))
for item in data:
item["owner_name"] = memlib.make_person_string(item, forename=False)
return json.dumps({"status": 'OK', "message": data}).encode('utf-8')
else:
return self.summary_page(rid, flocklist, start, step, sortby, sortdir)
else:
if not flocklist:
flocklist = [flocklib.last_flock()]
if DEBUG:
print("Last flock flocklist: %s" % flocklist)
if 'json' in accept:
data = {"status": 'OK', 'message': flocklist[0]['flock_no']}
return json.dumps(data).encode('utf-8')
flock_no = flocklist[0]['flock_no']
page_data = util.row2dict(flocklib.get_flock_data_for_list((flock_no,),
"flkno", "asc")[0])
page_data["curr_owner"] = memlib.make_person_string(page_data)
page_data["owners"] = []
owners = util.rows2dicts(flocklib.get_flock_owners(flock_no))
for owner in owners:
owner["full_name"] = memlib.make_person_string(owner)
page_data["owners"].append(owner)
page_data["flk_stats"] = flocklib.get_flock_stats(flock_no)
prefixes = flocklib.get_flock_prefixes(flock_no)
curr_prefix = flocklib.get_current_prefix(flock_no)
prefix_list = []
for prefix in prefixes:
if prefix == curr_prefix:
prefix_list.append((prefix, 'yes'))
else:
prefix_list.append((prefix, 'no'))
page_data["prefixes"] = prefix_list
page_data["health_schemes"] = flocklib.get_health_schemes()
print(page_data["health_schemes"])
page_data["flock_schemes"] = flocklib.get_flock_health(flock_no)
print(page_data['flock_schemes'])
page_info = {'requestpath': cherrypy.request.path_info,
'resource': self.grouping,
'rid': flock_no,}
try:
page_info['loginroles'] = cherrypy.request.loginroles
except AttributeError:
# loginroles isn't set by URL tests
print("Setting empty 'loginroles'")
page_info['loginroles'] = ()
page_info.update(util.html_appconf_items(self.appconf))
return flockhtml.flock_data_page(page_info, page_data)
return self.summary_page(rid, flocklist, start, step, sortby, sortdir)
def PUT(self):
""" Change Health Schemes or change the flock owner.
The flock name and number are immutable by design.
"""
req_body = util.check_request('flock_no', ("memsec",))
if 'rtn_status' in req_body:
return json.dumps(req_body).encode('utf-8')
if 'health' in req_body:
del req_body['health']
flocklib.update_flock_health(req_body)
else:
flocklib.change_owner(req_body)
cherrypy.response.status = 200
return json.dumps({'status': "OK", 'message': "Updated"}).encode('utf-8')
def POST(self):
""" Create a new flock. """
req_body = util.check_request('owner_id', ("memsec",))
if 'rtn_status' in req_body:
return json.dumps(req_body).encode('utf-8')
flocklib.change_owner(req_body)
cherrypy.response.status = 200
return json.dumps({'rtn_status': "OK", 'message': "Updated"}).encode('utf-8')
class New(FlockBase):
""" Class to provide a form for creating a new flock """
exposed = True
def __init__(self, config, path_conf=None):
super(New, self).__init__(config, path_conf)
def GET(self):
""" Return an HTML form allowin the user to create a new flock """
page_info = {'requestpath': cherrypy.request.path_info,
'resource': self.grouping,}
page_info.update(util.html_appconf_items(self.appconf))
return flockhtml.flock_new_page(page_info)
class Validators(FlockBase):
""" Flock validation methods """
exposed = True
def __init__(self, config, path_conf=None):
super(Validators, self).__init__(config, path_conf)
def GET(self, vpath=None, param=None):
""" Return flock validation data - member info """
if DEBUG:
print("vpath: %s, param: %s" % (vpath, param))
cherrypy.response.headers['content-type'] = 'application/json'
if vpath not in ('regflk', 'flock'):
cherrypy.response.status = 404
return json.dumps({"message": "Bad vpath"}).encode('utf-8')
if not param:
cherrypy.response.status = 400
return json.dumps({"message": "Missing parameter"}).encode('utf-8')
flocklist = self.check_id(param, True)
if not isinstance(flocklist, list):
# Some sort of error or unexpected case.
cherrypy.response.status = 400
return json.dumps({"message": flocklist}).encode('utf-8')
data = {'flock_no': flocklist[0][0], 'flock_name': flocklist[0][1]}
if vpath == 'regflk':
# Add the flock owner data
owner = flocklib.get_current_owner(data['flock_no'])
data['mem_info'] = util.row2dict(memlib.get_person_member(owner))
data['mem_info']['name'] = memlib.make_person_string(data['mem_info'])
if owner:
data['mem_info']['paidup'] = bool(data['mem_info']['expires'] >=
util.isodate_now())
else:
data['mem_info']['paidup'] = True
curr_vol = fbklib.get_current_fbkvol()
# javaScript has no simple test for an empty object...
flk_stats = util.row2dict(fbklib.get_flock_stats(data['flock_no'], curr_vol))
if flk_stats:
data['flock_stats'] = flk_stats
data['fbk_order'] = fbklib.get_fbk_orders(data['mem_info']['flock_no'],
curr_vol)
return json.dumps(data).encode('utf-8')
class Prefixes(FlockBase):
""" Flock Prefixes sub-resource handler """
exposed = True
popargs = ('rid',)
def __init__(self, config, path_conf=None):
super(Prefixes, self).__init__(config, path_conf)
def GET(self, rid=None):
"""
Return a list of assigned tag prefixes from current to oldest as an
HTML page or as json.
"""
# Validate the flock number ('rid')
flocklist = self.check_id(rid, True)
accept = cherrypy.request.headers.get("Accept", '')
if not isinstance(flocklist, list):
# Some sort of error or unexpected case.
if 'json' in accept:
cherrypy.response.status = 404
cherrypy.response.headers['content-type'] = 'application/json'
return json.dumps({"message": flocklist}).encode('utf-8')
return self.find_page(flocklist, 404)
if 'json' in accept:
cherrypy.response.headers['content-type'] = 'application/json'
return json.dumps(flocklib.get_flock_prefixes(flocklist[0][0])).encode('utf-8')
return 'Prefixes list under Construction!'
class Nav(FlockBase):
""" Class and method to return the flock number as JSON after applying the navigation
request. This URL is assumed to be called by an AJAX call from the client.
"""
popargs = ('rid', 'oldflk')
exposed = True
def __init__(self, config, path_conf=None):
super(Nav, self).__init__(config, path_conf)
def GET(self, rid=None, oldflk=None):
""" Return the flock located in reponse to the navigation request """
cherrypy.response.headers['content-type'] = 'application/json'
if rid:
if rid == 'first':
flock_no = flocklib.first_flock()[0]
elif rid == 'last':
flock_no = flocklib.last_flock()[0]
elif oldflk and flocklib.exists_flock(oldflk):
if rid == 'fleft':
flock_no = flocklib.prev_flock(oldflk, recs=10)
elif rid == 'left':
flock_no = flocklib.prev_flock(oldflk)
elif rid == 'right':
flock_no = flocklib.next_flock(oldflk)
elif rid == 'fright':
flock_no = flocklib.next_flock(oldflk, recs=10)
else:
cherrypy.response.status = 404
return json.dumps({"status": "Warning",
"message": "Invalid request {}".format(rid)}).encode('utf-8')
cherrypy.response.status = 200
return json.dumps({"status": "OK", "message": flock_no}).encode('utf-8')
else:
cherrypy.response.status = 404
return json.dumps({"status": "Error", "message": "Invalid rid {} or "
"previous flock no {}".format(rid, oldflk)}).encode('utf-8')