Python Pedigree Database
Artifact [0faa98b5e0]
Not logged in

Artifact 0faa98b5e0bcba0f2f435daa72ac184baf527233:


""" 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')