Python Pedigree Database
Artifact [26df394689]
Not logged in

Artifact 26df394689f4144d1e390018fb231b5be26f2e87:


""" 
    flockuri.py

    URI method handler classes for the flock branch of the URI tree.

    Copyright PR Hardman 2009 - 2023.
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details./
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""

import cherrypy

from ppdb import const
from ppdb.lib import util, memlib, perslib, flocklib, fbklib, transfer, userlib
from ppdb.handlers import uribase

xhrc = const.JSConsts()

#~ DEBUG = True
DEBUG = False

class FlockBase(uribase.UriBase):
    """ 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. """
        super().__init__(config, path_conf)
        self.disclaimer = config.get('disclaimer', '')
        self.flock_sort_params = list(flocklib.FLOCK_SORT_TRANSLATE.keys())
      
    def check_id(self, arg, srchby='flock', one_only=True):
        """ Test that the id is good. 
            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 []
            
        if srchby == 'owner':
            flocklist = flocklib.find_flock_by_owner(arg)
        else:
            flocklist = flocklib.find_flock(arg)                         
       
        if not flocklist:
            return f"Nothing found matching '{arg}'"
        
        if one_only and len(flocklist) > 1:
            return f"More than one flock found matching '{arg}'"
        return flocklist
        

@cherrypy.expose
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!
        
    """
    
    def GET(self, rid=None):
        """ 
            Return the Flock Data page
            - If 'rid' is empty' or resolves to a single flock:
                
                - If an 'officer' is logged in the data for the flock(s) specified by 
                    'rid' or data for the most recently created flock if 'rid' is empty.
                
                - If a 'breeder' is logged in the data for one of the breeder's flocks as 
                    specified by 'rid' or the 'first of the breeder's flocks if 'rid' is 
                    empty. 

        """
        if DEBUG:
            print(f"rid: {rid}")
            
        brdr_flocks = []
        # This uri is only available to logged in users - no need to check 'login'
        # brdr_flocks is a list of flock_no, flock_name tuples
        if ('breeder' in cherrypy.request.loginroles and 
                'officer' not in cherrypy.request.logingroups):
            userpers = userlib.get_user_login(cherrypy.request.login)['person_id']
            brdr_flocks = [[row[0], row[1]] for row in 
                                                flocklib.get_persons_flocks(userpers)]
        if not rid:
            if brdr_flocks:
                rid = brdr_flocks[0][0]
            else:
                rid = flocklib.last_flock()[0]
                
        # Get a list of tuples of flock numbers matching 'rid'
        flock_list = self.check_id(rid)
        
        if DEBUG:
            print(f"flock_list: {flock_list}")
          
        if not isinstance(flock_list, list):
            # Some sort of error or unexpected case.
            raise cherrypy.HTTPError('400 Bad request', flock_list)
                
        flock = flock_list[0]
        if brdr_flocks:
            if flock not in brdr_flocks:
                raise cherrypy.HTTPError('400 Bad request', 
                                    "You may only view data for flock(s) that you own")
                                    
        flock_no = flock[0]                    
        page_data = util.row2dict(flocklib.get_flock_data(flock_no))
        page_data['flocks'] = brdr_flocks                                                            
        page_data["pagetitle"] = f"{page_data['flock_no']} - {page_data['flock_name']}"
        page_data["titlebar"] = page_data["pagetitle"]  
        page_data["curr_owner"] = \
                                perslib.make_person_string(flocklib.get_flock_owner(flock_no))
        page_data["owners"] = []
        owners = util.rows2dicts(flocklib.get_flock_owners(flock_no))
        for owner in owners:
            current = memlib.is_membership_current(owner['member_no'])
            # Set the text colour
            if current:
                owner['colour'] = ''
            else:
                owner['colour'] = 'grayed'
            owner['pers_string'] = perslib.make_person_string(owner)
            owner['current'] = current
            owner['changed'] = owner['changed'][:19]

        page_data['owners'] = owners
        page_data["flk_stats"] = fbklib.get_flock_stats(flock_no)
        prefixes = util.rows2dicts(flocklib.get_prefixes_by_flock_no(flock_no)[1])
        for pfx in prefixes:
            pfx['date_assigned'] = pfx['date_assigned'][:19] 
        page_data["prefixes"] = prefixes
        page_data["health_schemes"] = flocklib.get_health_schemes()
        page_data["flock_schemes"] = flocklib.get_flock_health(flock_no)
        
        page_info = self.html_appconf_items()
        page_info.update({'requestpath': cherrypy.request.path_info,
                        'resource': 'flock',
                        'rid': flock_no})
        page_info.update(page_data)
        template = const.JINJA_ENV.get_template('templates/flockdata.tmpl') 
        return template.render(page_info)
        
    @cherrypy.tools.json_out()
    def PUT(self): 
        """ Change flock data """
        req_body = self.check_request('action', ("memsec",))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
            
        if req_body['action'] == 'health':
            print(req_body)
            flocklib.update_flock_health(req_body)
            cherrypy.response.status = 200
            return {"rcode": xhrc.XHR_UPDATED, "data": "Updated"}
            
        if req_body['action'] == 'rename':
            cherrypy.response.status = 200
            resp = flocklib.update_flock_name(req_body)
            if resp["rcode"] != xhrc.XHR_UPDATED:
                cherrypy.response.status = 400
            
            return resp
            
        cherrypy.response.status = 400
        return {"rcode": xhrc.XHR_ERROR, "data": "Invalid PUT request"}
            
    @cherrypy.tools.json_out()
    def POST(self):
        """ Create a new flock or a new owner. """
        req_body = self.check_request('action', ("memsec",))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
        
        if req_body['action'] == 'flock':
            # Create a new flock
            breeder = perslib.get_pers_breeder(req_body['owner_id'])
            if not breeder[0]:
                return {"rcode": xhrc.XHR_WARN, 
                        "data": f"Member {breeder[1]} membership class does not permit "
                                                                            "flock ownership"}
            result = flocklib.add_flock(req_body)  
            if result["rcode"] == xhrc.XHR_NOERR:    
                cherrypy.response.status = 201
                return {"rcode": xhrc.XHR_CREATED, "data": result["data"]}
            # result is an error message
            return result
                
        if req_body['action'] == 'owner':
            # Add a new owner for a flock
            if not 'person_id' in req_body or not req_body['person_id']:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                        "data": "A new owner person must be specified."}
                        
            if not 'date' in req_body or not req_body['date']:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                        "data": "An owner change date must be specified."}
                        
            try:
                flocklib.change_owner(req_body)
            except (flocklib.ValidationError, util.PPDError, util.PPDWarning) as err:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, "data": str(err)}
                
            cherrypy.response.status = 201
            return {"rcode": xhrc.XHR_CREATED, "data": "New owner created"}
        
        cherrypy.response.status = 400
        return {"rcode": xhrc.XHR_ERROR, "data": "Invalid POST request"}
    
@cherrypy.expose
class Data(FlockBase):
    """ Methods to handle data requests """

    @cherrypy.tools.json_out()
    def GET(self, vpath=None, rid=None):
        """ Return flock and owner data for the flock owner in 'rid' """
        if vpath == 'owner':
            # Return owner data
            if not rid:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                                        "data": "You must enter a value to search for"}
            
            flock_list = flocklib.find_flock_by_owner(rid)
            if not flock_list:
                flock_list = f"Nothing found matching '{rid}'"
                
            if not isinstance(flock_list, list):
                # Some sort of error or unexpected case.
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, "data": flock_list}
                
            data = util.rows2dicts(flock_list)
            
            for item in data:
                item["owner_name"] = perslib.make_person_string(item, forename=False)
                item["paid_up"] = memlib.is_membership_current(item['member_no'])
            return {"rcode": xhrc.XHR_NOERR, "data": data}
        
        if vpath == 'flock':
            # rid is a flock search argument - may contain wildcard
            if not rid:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                                        "data": "You must enter a value to search for"}
                                        
            if ('breeder' in cherrypy.request.loginroles and 
                'officer' not in cherrypy.request.logingroups):
                userpers = userlib.get_user_login(cherrypy.request.login)['person_id']
                brdr_flocks = [[row[0], row[1]] for row in 
                                                    flocklib.get_persons_flocks(userpers)]
            else:
                brdr_flocks = []
                                    
            flocklist = flocklib.find_flock(rid)
            if not flocklist:
                # Some sort of error or unexpected case.
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                                        "data": f"Nothing found matching '{rid}'"}
            if brdr_flocks:
                for flock in flocklist:
                    if flock[0] not in brdr_flocks:
                        cherrypy.response.status = 400
                        return {"rcode": xhrc.XHR_ERROR, 
                                "data": "You can only view data for flock(s) that you own"}
                    
            data = util.rows2dicts(flocklib.get_flocks_current_owners(flocklist))
            for item in data:
                item["owner_name"] = perslib.make_person_string(item, forename=False)
            return {"rcode": xhrc.XHR_NOERR, "data": data}
            
        cherrypy.response.status = 400
        return {"rcode": xhrc.XHR_ERROR, "data": "Invalid vpath"}
   
    
@cherrypy.expose
class Health(FlockBase):
    """ Health root class and methods.
        
        This class is the root of the 'flock/health' 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!
    """
    @cherrypy.tools.json_out()
    def GET(self, findval=None):
        """ Return the 'Health Schemes' data """
        if not 'json' in cherrypy.request.headers.get("Accept", ''):
            cherrypy.response.status = 415
            return {"rcode": xhrc.XHR_ERROR, "data": "Program Error: Unsupported Media Type"}
            
        if findval:
            try:
                scheme_id = int(findval)
            except ValueError as e:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, "data": f"Invalid scheme_id: {e}"}
            data = data = util.row2dict(flocklib.get_health_schemes(scheme_id)[0])
        else:
            data = util.rows2dicts(flocklib.get_health_schemes())
        cherrypy.response.status = 200
        return {"rcode": xhrc.XHR_NOERR, "data": data}
    
    @cherrypy.tools.json_out()
    def PUT(self):
        """ Update an existing health scheme """
        req_body = self.check_request('scheme_id', ("memsec", "admin"))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
            
        # Update the specified health scheme
        if len(req_body) == 2:
            if not 'active' in req_body or not 'scheme_id' in req_body:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, "data": "Missing required field(s)"}
                
            flocklib.update_health_scheme(req_body)
            cherrypy.response.status = 200
            return {"rcode": xhrc.XHR_UPDATED, "data": "Updated"}

        if flocklib.is_health_scheme_used(req_body['scheme_id']):
            scheme = flocklib.get_health_schemes(req_body['scheme_id'])[0]
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_WARN, 
                    "data": (f"The Health Scheme '{scheme['health_scheme']}' has "
                            "been assigned to one or more flocks and can only have "
                            "the 'Active' flag changed")}
        flocklib.update_health_scheme(req_body)
        cherrypy.response.status = 200
        return {"rcode": xhrc.XHR_UPDATED, "data": "Updated"}
    
    @cherrypy.tools.json_out()
    def POST(self):
        """ Create a new health scheme """
        req_body = self.check_request('scheme', ("memsec", "admin"))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
        
        if not 'short_name' in req_body or req_body['short_name'] == "":
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, "data": "Missing 'short_name'"}
        
        scheme_id = flocklib.add_health_scheme(req_body)    
        cherrypy.response.status = 201
        return {"rcode": xhrc.XHR_CREATED, "data": scheme_id}
                
    

@cherrypy.expose
class Lookup(FlockBase):
    """ Flock Lookup class. This class has only the GET method. """
    
      
    def GET(self):
        """ Return the HTML lookup page """
       
        # Set the page_info items 
        page_info = self.html_appconf_items()
        page_info['pagetitle'] = 'Flock Lookup'
        page_info['titlebar'] = page_info['pagetitle'] 
        page_info.update({'resource': "flock/lookup",
                        'loginroles': cherrypy.request.loginroles,  
                        'logingroups': cherrypy.request.logingroups})
        template = const.JINJA_ENV.get_template('templates/flocklookup.tmpl') 
        return template.render(page_info)
        
        
        
@cherrypy.expose
class Maint(FlockBase):
    """ A class to handle the 'Flock Maintenance page """
    
    def GET(self):
        """ Return the 'Flock Maintenance' page """
        page_info = self.html_appconf_items()
        roles = cherrypy.request.loginroles
        if 'memsec' in roles or 'admin' in roles:
            page_info["health_schemes"] = flocklib.get_health_schemes()
            
        page_info.update({'titlebar': "Flock Administration",
                        'requestpath': cherrypy.request.path_info,
                        })
        template = const.JINJA_ENV.get_template('templates/flockmaint.tmpl') 
        return template.render(page_info)
        
@cherrypy.expose
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.
    """
    
    @cherrypy.tools.json_out()
    def GET(self, rid=None, oldflk=None):
        """ Return the flock located in reponse to the navigation request """
        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 == 'fprev':
                    flock_no = flocklib.prev_flock(oldflk, recs=10)
                elif rid == 'prev':
                    flock_no = flocklib.prev_flock(oldflk)
                elif rid == 'next':
                    flock_no = flocklib.next_flock(oldflk)
                elif rid == 'fnext':
                    flock_no = flocklib.next_flock(oldflk, recs=10)
                else:
                    cherrypy.response.status = 400
                    return {"rcode": xhrc.XHR_WARN,
                            "data": f"Invalid request {rid}"}
            else:
                cherrypy.response.status = 400
                return {"rcode": "Warning",  "data": "Missing 'oldflk' parameter"}
            cherrypy.response.status = 200
            return {"rcode": xhrc.XHR_NOERR, "data": flock_no}
            
        cherrypy.response.status = 400
        return {"rcode": xhrc.XHR_ERROR, 
                        "data": f"Invalid rid {rid} or previous flock no {oldflk}"}
                            
@cherrypy.expose
class Prefixes(FlockBase):
    """ Flock Prefixes sub-resource handler """
    
    @cherrypy.tools.json_out()
    def GET(self, vpath=None, findval=None,):
        """ Return a list of assigned tag prefixes from current to oldest as a JSON object """
        if not findval:
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, "data": xhrc.NO_FINDVAL}
        if vpath == 'flkpfx':
            # This is called from JS sheep.update_prefixes - sort prefixes by 'current' 
            flocklist = self.check_id(findval)
            if not isinstance(flocklist, list):
                # Some sort of error or unexpected case.
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, "data": flocklist}

            if not flocklist:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                            "data": "A resource id (rid) is required"}
            if flocklist[0][0] == 'SSB000':
                pfx_data = flocklib.get_island_prefixes()
            else:
                pfx_data = flocklib.get_flock_prefixes(flocklist[0][0], 
                            sortby="current desc, date_assigned desc")                
               
            return {"rcode": xhrc.XHR_NOERR, "data": pfx_data}
                    
        if vpath in ('prefix', 'flock', 'year', 'name'):            
            # Select or sort the prefixes
            arg = findval.replace('*', '%')
            arg = arg.replace('?', '_')
            rows = None
            if vpath == 'name':
                if findval and findval.lower() == "island":
                    sortby, rows = flocklib.get_island_prefixes()
                else:
                    sortby, rows = flocklib.get_prefixes_by_flock_name(arg)
            elif vpath == 'flock': 
                sortby, rows = flocklib.get_prefixes_by_flock_no(arg)
            elif vpath == 'year':
                sortby, rows = flocklib.get_prefixes_by_year(arg)
            else:
                sortby, rows = flocklib.get_prefixes_by_prefix(arg)
                
            if not rows:
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                                    "data": f"No prefixes found for search on {findval}"}
            
            prefixes = util.rows2lists(rows)                                
            for pfx in prefixes:
                pfx[5] = pfx[5][:19]

            cherrypy.response.status = 200
            return {"rcode": xhrc.XHR_NOERR, "data": (sortby, prefixes)}
        
        cherrypy.response.status = 400
        return {"rcode": xhrc.XHR_ERROR, "data": "You must specify a valid vpath"}
        

    @cherrypy.tools.json_out()
    def PUT(self):
        """ Change current prefix """
        req_body = self.check_request('flock_no', ("regsec", "admin"))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
        
        prefixes = util.rows2lists(flocklib.change_current_prefix(req_body)[1])
        for pfx in prefixes:
            pfx[5] = pfx[5][:19]             
            
        cherrypy.response.status = 200
        return {"rcode": xhrc.XHR_UPDATED, "data": prefixes}
        
    @cherrypy.tools.json_out()
    def POST(self):
        """ Add a prefix. 
            If no flock is specified then allocate it to either the Island flock or
            the Unknown flock depending on the first two digits of the prefix
        """
        req_body = self.check_request('prefix', ("regsec", "admin"))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
            
        if not 'flock_no' in req_body or not req_body['flock_no']:
            if req_body['prefix'][2:5] == '052' or req_body['prefix'][2:4] == '52': 
                req_body['flock_no'] = flocklib.FLOCK_ISLAND
            else:
                req_body['flock_no'] = flocklib.FLOCK_UNRECORDED
        rtn = flocklib.add_prefix(req_body['flock_no'], req_body['prefix'])
        if isinstance(rtn, dict):
            cherrypy.response.status = 400
            return rtn
        
        prefixes = util.rows2lists(rtn[1])
        for pfx in prefixes:
            pfx[5] = pfx[5][:19]
        cherrypy.response.status = 201
        return {"rcode": xhrc.XHR_CREATED, "data": prefixes}
        
    @cherrypy.tools.json_out()
    def DELETE(self):
        """ Verify the  prefix is unused then delete it """
        req_body = self.check_request('tag_prefix', ("regsec", "admin"))
        if not isinstance(req_body, dict):
            # Some sort of error or unexpected case.
            return {"rcode": xhrc.XHR_ERROR, "data": req_body}
            
        rows = flocklib. get_prefixes_by_flock_no(req_body['flock_no'])[1]
        if not rows:
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, 
                    "data": f"Flock {req_body['flock_no']} has no assigned prefixes"}
        pfx_row = []
        for row in rows:
            if row[2] == req_body['flock_no'] and row[1] == req_body['tag_prefix']:
                pfx_row = row
                break;
        if not pfx_row:
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, 
                    "data": f"Prefix {req_body['tag_prefix']} is not assigned to flock "
                            f"{req_body['flock_no']}"}
                            
        if pfx_row[4] == 'Yes':
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_USED, 
                    "data": f"Prefix {req_body['tag_prefix']} is the 'Current' prefix for "
                            f"flock {req_body['flock_no']} and cannot be deleted while "
                            "'Current'.<br><br>"
                            "Make a different prefix 'Current' then retry the Delete"}
                            
        if pfx_row[6] == 'Yes':
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_USED, 
                    "data": f"Prefix {req_body['tag_prefix']} has been used on one or "
                                           "more sheep and cannot be deleted"}
            
        rtn = flocklib.delete_prefix(req_body['tag_prefix'], req_body['flock_no'])
        if rtn:
            prefixes = util.rows2lists(rtn[1])
            for pfx in prefixes:
                pfx[5] = pfx[5][:19]
            return {"rcode": xhrc.XHR_DELETED, "data": prefixes}
        return {"rcode": xhrc.XHR_ERROR, "data": req_body}
            
            

@cherrypy.expose
class RegFlk(FlockBase):
    """ Data for the 'New Flock Session' dialog """
    
    @cherrypy.tools.json_out()
    def GET(self, rid=None):
        """ GET the dialog data """
        # Get data for a flock in which sheep are to be registered 
        flocklist = self.check_id(rid)
        if not isinstance(flocklist, list):
            # Some sort of error or unexpected case.
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, "data": flocklist}
            
        if not flocklist:
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, 
                        "data": "A flock number or name is required"}
            
        data = {'flock_no': flocklist[0][0], 'flock_name': flocklist[0][1]}
        
        # Add the flock owner data   
        owner = flocklib.get_flock_owner(data['flock_no'])
        data['owner_id'] = owner['person_id']
        data['mem_info'] = util.row2dict(perslib.get_person_member(owner['person_id']))
        
        breeder = perslib.get_pers_breeder(owner['person_id'])
        if not breeder[0]:
            return {"rcode": xhrc.XHR_WARN, 
            "data": f"Member {breeder[1]} membership class does not permit "
                                "sheep registrations"}  
        
        data['mem_info']['name'] = perslib.make_person_string(data['mem_info'])
        data['mem_info']['active'] = owner['active']
        if owner:
            data['mem_info']['paidup'] = bool(data['mem_info']['expires'] >= 
                                                                util.isodate_now())
        else:
            # Owner = 0 - a breed special flock
            data['mem_info']['paidup'] = True  
            
        curr_yr = util.isodate_now()[:4]
        # javaScript has no simple test for an empty object...
        flk_stats = util.row2dict(fbklib.get_flock_stats(data['flock_no'], curr_yr))
        if flk_stats:
            data['flock_stats'] = flk_stats
        else:
            data['flock_stats'] = {'pure_ewes': 0, 'crossing_ewes': 0, 'other_females': 0,
                                    'breeding_males': 0, 'other_males': 0}
        data['fbk_orders'] = fbklib.get_members_orders(data['mem_info']['member_no'], 
                                                                                    curr_yr)
        
        return {"rcode": xhrc.XHR_NOERR, "data": data}
        
@cherrypy.expose
class Stats(FlockBase):
    """ Flock Statistics. This class supports only the GET method """
    
    def GET(self):
        """ GET the flock statistics. """
        page_info = self.html_appconf_items()
        page_info.update({'pagetitle': 'Flock Statistics',
                        'requestpath': cherrypy.request.path_info})
        page_info['titlebar'] = page_info['pagetitle']
        template = const.JINJA_ENV.get_template('templates/underconst.tmpl') 
        return template.render(page_info)
    

@cherrypy.expose
class Validators(FlockBase):
    """ Flock validation methods. Unless there is an error in the vpath or parameters
        all validators return HTTP status 200
    """
    
    @cherrypy.tools.json_out()
    def GET(self, vpath=None, param=None):
        """ Validate the data according to vpath. """
        if DEBUG:
            print(f"vpath: {vpath}, param: {param}")
            
        if not param:
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, 
                    "data": "You must specify a flock number or name"}
                        
        if vpath == 'ownerchng':
            # Check that the owner of the flock in 'param' is allowed to be changed
            param = param.upper()
            if not flocklib.exists_flock(param):
                return {"rcode": xhrc.XHR_WARN, 
                        "data": f"The specified flock {param} does not exist"}
                    
            if param[0:3] == 'SSB':
                return {"rcode": xhrc.XHR_WARN, 
                         "data": "You can't change the owner of a 'Society' flock"}
                        
            return {"rcode": xhrc.XHR_NOERR, "data": xhrc.XHR_NOERR}
          
        if vpath == 'exists':
            # Check if a flock name exists and if so is it a synonym of the 'of ' name 
            sname = param
            if sname[:3] == 'of ':
                sname = param[3:]
            row = flocklib.exists_flock_name(sname)
            if not row:
                return {"rcode": xhrc.XHR_UNUSED, "data": xhrc.XHR_UNUSED}
                
            owner_id = flocklib.get_flock_owner(row['flock_no'])['person_id']
            owner_name = perslib.make_person_string(
                                            perslib.get_person_data(owner_id), False)
            data = {'person_id': owner_id, 'person_name': owner_name, 
                                'flock_name': row['flock_name']}     
            if param != row['flock_name']:
                data['synonym'] = 'true'
                    
            return {"rcode": xhrc.XHR_EXISTS, "data": data}
                
        if vpath == 'name': 
            # Check a flock name. parms are new_name[:old_name]
            # old_name will not include 'old_name' in any matchesis o
            parms = param.split(':')
            if len(parms) == 1:
                return flocklib.check_flock_name(parms[0])
            if len(parms) == 2:
                return flocklib.check_flock_name(parms[0], parms[1])
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, 
                "data": "Must supply colon separated flock_name and flock_no (may be empty)"}
            
        if vpath == 'prefix':
            parms = param.split(':')
            if parms[0] not in ('new', 'used'):
                cherrypy.response.status = 400
                return {"rcode": xhrc.XHR_ERROR, 
                    "data": "First parameter must be 'new' or 'used'"}
            if parms[0] == 'new':
                return flocklib.check_flock_prefix(parms[1], parms[2])
                
            if parms[0] == 'used':
                rows = flocklib.is_prefix_used(parms[1], parms[2])
                if rows:
                    return {"rcode": xhrc.XHR_USED, 
                            "data": f"Prefix is used on {len(rows)} sheep originating "
                                f"in flock {parms[2]} and cannot be deleted from that flock"}
                return {"rcode": xhrc.XHR_UNUSED, "data": "Prefix is assigned but unused"} 
                
            cherrypy.response.status = 400
            return {"rcode": xhrc.XHR_ERROR, "data": "Invalid action parameter"}
            
            
        if vpath == 'owns':
            # Is the person allowed to own flocks, and if so do they already own flock(s)
            breeder = perslib.get_pers_breeder(param)
            if not breeder[0]:
                return {"rcode": xhrc.XHR_WARN, 
                        "data": f"Member {breeder[1]} membership class does not permit "
                                                                            "flock ownership"}
            flocks = flocklib.get_persons_flocks(param)
            return {"rcode": xhrc.XHR_NOERR, "data": flocks}
            
        if vpath == 'regns':
            # See if any sheep have been registered in the flock
            return {"rcode": xhrc.XHR_NOERR, 
                    "data": len(transfer.get_sheep_registered_in_flock(param))}
                    
        cherrypy.response.status = 400
        return {"rcode": xhrc.XHR_ERROR, "data": "You must specify a valid vpath"}