#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# © Hub People 2024› https://www.hubpeople.be · https://www.geezteem.com/455
# AGPL-3.0-or-later - https://www.gnu.org/licenses/agpl-3.0.txt

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
import time
from DateTime import DateTime

from appy.utils.dates import Month
from appy.model.utils import Object as O

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bn = '\n'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

subGroups = (
  # People in a group can be sub-divided into sub-groups according to what they
  # can do or not in the group. Every sub-group has a corresponding local role
  # that will be used in the workflows for the HP group and its related objects.
  #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  # name          # local role       # description
  #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  'admins',       # GroupAdmin       # A group admin can create, edit and delete
                                     # local users.
  'managers',     # GroupManager     # A group manager can create and edit all
                                     # group sub-objects.
  'editors',      # GroupEditor      # People authorized to edit some group's
                                     # sub-elements, like beneficiaries's data.
  'writers',      # GroupWriter      # People that can write messages in the
                                     # group.
  'readers',      # GroupReader      # People that can only read info from the
                                     # group
  'timetrackers', # GroupTimetracker # People that can manage timetracked info
                                     # in the group.
  'caremanagers', # GroupCaremanager # People defining cares for beneficiaries.
  'caregivers',   # GroupCaregiver   # People who provide care to beneficiaries.
  #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
)
# Note that, in this list (except for role "timetrackers" and below), any role
# is also granted all prerogatives of any role below him.

# Sub-groups allowed to create messages
writerGroups = ('admins', 'managers', 'editors', 'writers')

# Correspondance between sub-groups and User fields used to store them  
subFields = {'admins'       : 'administeredGroups',
             'managers'     : 'managedGroups',
             'editors'      : 'adaptedGroups',
             'writers'      : 'editedGroups',
             'readers'      : 'observedGroups',
             'timetrackers' : 'timetrackedGroups',
             'caremanagers' : 'caremanagedGroups',
             'caregivers'   : 'caredGroups',
}
# Journeyman-relate User back refs
journeymanRefs = ('administeredGroups', 'managedGroups', 'adaptedGroups',
                  'editedGroups', 'observedGroups')

# User fields (HpGroup back refs) indicating roles allowing a user to create
# messages.
creatorFields = ('administeredGroups', 'managedGroups',
                 'adaptedGroups', 'editedGroups')

# Management roles
managerRoles = ('GroupAdmin', 'GroupManager')

# Journeyman roles
journeymanRoles = managerRoles + ('GroupEditor', 'GroupWriter', 'GroupReader')

# Journeyman roles allowed to render pods
journeypodRoles = managerRoles + ('GroupEditor', 'GroupWriter')

# Care-related roles
careRoles = ('GroupCaremanager', 'GroupCaregiver')

currentYear = time.localtime()[0]

# Oldest possible dates for some fields
oldestYears = O(
  contract=currentYear - 90,   # for a worker contract ;
  birthDate=currentYear - 100, # for the birth date of a beneficiary ;
  entryDate=currentYear - 100  # for the entry (or exit) of a beneficiary
                               # into a group.
)

# Default oldest years for the same fields
defaultYears = O(
  contract=currentYear - 25,
  birthDate=currentYear - 30,
  entryDate=currentYear - 20
)

# Values corresponding to the absence of a value
nonValues = ('-', '', {})

# Values for which no color must be applied
noColorValues = nonValues + ('0:00',)

# Letters used to summarize info, in the timeline group planning, about
# timespans at a given day.
timespanLetters = (
  '✖', # At least one missing timespan w.r.t events
  'M', # At least one unforeseen timespan w.r.t events
  '!', # At least one non-validated timespan, or a mismatch between timespans
       # and events
  '✔'  # All timespans are validated
)
# The map converts special chars to actual letter·s
timespanLettersMap = {'✖':'x', 'M':'m', '!':'em', '✔':'v'}

# Addition or substraction of DateTime instances produces a number of days. In
# order to get minutes from these days, the following factor must be used
minutesFactor = 24 * 60 # hours * minutes

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def fullAddress(o, includeCountry=False, line=None, empty='-',
                numberAttr='number', lineSep=', '):
    '''Compute the full address of some p_obj as a one-line string if p_line is
       None, or return only the line numbered p_line else. p_line can be:
            1     Address, number, box
            2     Postal code, city[, country]
    '''
    if o.isEmpty('address'): return empty
    # Compute line 1
    if line != 2:
        nb = getattr(o, numberAttr)
        number = f', {nb}' if nb else ''
        line1 = f'{o.address}{number}'
        box = o.box
        if box: line1 = f'{line1} {box}'
        if line == 1: return line1
    # Compute line 2
    line2 = o.postalCode or ''
    city = o.city
    if city:
        line2 = f'{line2}, {city}' if line2 else city
    if includeCountry:
        country = o.country
        if country:
            line2 = f'{line2}, {country}' if line2 else country
    if line == 2: return line2
    # If we are here, return both
    return f'{line1}{lineSep}{line2}' if line2 else line1

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def getNow(o):
    '''Gets a DateTime object representing "now", that is then cached'''
    cache = o.cache
    if 'now' in cache: return cache.now
    r = cache.now = DateTime()
    return r

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def getCurrentPeriod(splitDay=10):
    '''Gets the current period, as a tuple (i_year, i_month)'''
    # The "current" period is defined as follows.
    #
    # We suppose that, at the start of the month, the user is mainly interested
    # in what happened during the previous month. If we are later in the month,
    # we suppose he will be more concerned with the current month. "If we are
    # later in the month" means: if the current day is later than the
    # p_splitDay.
    now = DateTime()
    if now.day() > splitDay:
        # Get the current month
        r = now.year(), now.month()
    else:
        # Get the last month
        previous = Month.getSibling(now, next=False)
        r = previous.year(), previous.month()
    return r

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def getStartEnd(date):
    '''Returns a tuple of DateTime instance (start, end) representing the first
       and last day of the month into which p_date is.'''
    start = DateTime(date.strftime('%Y/%m/01'))
    end = Month.getLastDay(date, hour='23:59')
    return start, end

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def getColored(value):
    '''Returns p_this value, wrapped around a span defining a color, depending
       on the fact that the value is positive or negative.'''
    if value in noColorValues: return value
    css = 'negT' if value.startswith('-') else 'posT'
    return f'<span class="{css}">{value}</span>'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def getTTName(tool, tt, sep=' · '):
    '''Return the nice name for this p_tt entry'''
    # A TT entry is a tuple (timespan *t*ype, timespan *t*emplate), encoded in a
    # string of the form:
    #
    #              <timespanTypeID>*<timespanTemplateID>
    #
    # It is used as several places within HubPeople, ie, for encoding events in
    # calendars or choosing entries in fixed contracts.
    #
    # If the event type is not a validated one, but a wish, its ends with letter
    # "W".
    if tt.endswith('W'):
        wish = True
        tt = tt[:-1]
    else:
        wish = False
    typeId, templateId = tt.split('*', 1)
    o_ = tool.getObject
    if wish:
        # For a wish, suffix the name by translated term "Wish"
        prefix = ''
        suffix = tool.translate('event_wish')
    else:
        # For a non-wish, suffix the name with info about the template
        template = o_(templateId)
        if not template:
            prefix = ''
            suffix = tool.translate('unknown_template')
        elif template.isStandard():
            prefix = f'{template.getStartEnd()}{sep}'
            suffix = template.title
        else:
            prefix = ''
            suffix = template.title
    # Get info about the type
    typE = o_(typeId)
    typeTitle = typE.title if typE else tool.translate('unknown_type')
    return f'{prefix}{typeTitle}{sep}{suffix}'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Counts(O):
    '''This utility class allows to perform counts of objects and, finally,
       render them as a XHTML table.'''

    def getDetails(self, textFun):
        '''Returns an XHTML table giving details about attributes as stored on
           p_self. p_textFun is a function that will be called for every
           attribute stored on p_self, in order to get the corresponding
           translated text.'''
        rows = []
        for name, count in self.items():
            # Get the label to use for this attribute
            row = f'<tr><th>{textFun(name)}</th>' \
                  f'<td style="text-align:right">{count}</td></tr>'
            rows.append(row)
        return f'<table class="small">{bn.join(rows)}</table>'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Inherited:
    '''Bunch of utility methods for managing the link between objects inherited
       from one parent group to its children groups.'''

    @classmethod
    def show(class_, group, name):
        '''Show, on this HP p_group, the list of inherited objects via the field
           having this p_name, only on a main group and if the ref is not
           empty.'''
        if not group.isMain and not group.isEmpty(name):
            return 'view'

    @classmethod
    def showPageBens(class_, group):
        '''Must page p_group::inheritedBeneficiaries be shown ?'''
        return class_.show(group, 'inheritedBeneficiaries')

    @classmethod
    def showPageContacts(class_, group):
        '''Must page p_group::inheritedContacts be shown ?'''
        return class_.show(group, 'inheritedContacts')

    @classmethod
    def showTags(class_, group):
        '''Must inherited tags be shown on this p_group ?'''
        return class_.show(group, 'inheritedTags')

    @classmethod
    def showCategories(class_, group):
        '''Must inherited categories be shown on this p_group ?'''
        return class_.show(group, 'inheritedCategories')

    @classmethod
    def showClassifiers(class_, group):
        '''Must inherited classifiers be shown on this p_group ?'''
        return class_.show(group, 'inheritedClassifiers')

    @classmethod
    def showRelTypes(class_, group):
        '''Must inherited relation types be shown on this p_group ?'''
        return class_.show(group, 'inheritedRelationTypes')

    @classmethod
    def showAllowedChildren(class_, o):
        '''Show field o::allowedChildren only if the container group has at
           least one child group.'''
        group = o.container
        return group and not group.isEmpty('children')

    @classmethod
    def listChildGroups(class_, o):
        '''Lists the groups being children from the one where this p_o(bject) is
           defined.'''
        return o.container.children

    @classmethod
    def supTitle(class_, o, ref='allowedChildren'):
        '''Display, in the sup-title zone for this p_o(bject), an icon if this
           latter is available to at least one child group.'''
        if not o.isEmpty(ref):
            imgUrl = o.buildUrl('inherited')
            text = o.translate('element_inherited')
            return f'<img src="{imgUrl}" title="{text}" class="help"/>'

    @classmethod
    def computeGroups(class_, o, ac='allowedChildren'):
        '''Computes the content of index "groups" on p_o'''
        # Index "groups" will contain iids of any group that can access this
        # p_o(bject): its container group and all allowed child groups.
        r = [o.container.iid]
        children = getattr(o, ac)
        if children:
            for child in children:
                r.append(child.iid)
        return r

    @classmethod
    def computeCategoryGroups(class_, cat):
        '''Computes the content of index "groups" on this p_cat(egory)'''
        # Depending on the type of category, there is a specific
        # "allowedChildren*" field to use.
        return class_.computeGroups(cat, ac=cat.getAllowedChildrenAttribute())

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class ToParent:
    '''Move an element (tag or beneficiary) from a child to a parent group'''

    # Messages
    MOVED_OK = '%s %d (%s) moved from group %d (%s) to %d (%s)(ac field:"%s").'

    @classmethod
    def show(class_, o, isGroup=False):
        '''Only a manager can perform this action, triggerable on a child group
           only.'''
        group = o if isGroup else o.container
        if not group.isMain and o.user.isManagerOn(o):
            # Moreover, we must not be displaying inherited elements
            page = o.req.page or 'main'
            if not page.startswith('inherited'):
                return 'buttons'

    @classmethod
    def finalize(class_, o, parent, child, name, map, many):
        '''After the m_move or m_moveMany action was performed (depending on
           p_many), returns a nice message to the user and redirects him to the
           target page on the p_parent group.'''
        # If p_many is True, p_o is one of the selected objects
        _  = o.translate
        # Complete the p_mapping with common entries
        map['child'] = child.getShownValue()
        map['parent'] = parent.getShownValue()
        part = 's' if many else ''
        text = o.translate(f'to_parent{part}_done', mapping=map)
        # If p_many is True, we are called from an Ajax request: the return page
        # is already defined.
        if many: return text
        # Redirect the user to the p_parent group
        backUrl = f'{parent.url}?page={name}'
        o.goto(backUrl, message=text, fleeting=False)

    @classmethod
    def move(class_, o, name, alsoForChildren, fromMany=False,
             ac='allowedChildren'):
        '''Moves this p_o(bject) to its container's parent group, via the Ref
           having this p_name.'''
        # Get the current and parent groups
        child = o.container
        parent = child.parent
        # Switch groups
        child.unlink(name, o)
        parent.link(name, o)
        # Historize the action in p_o
        maP = {'parent': parent.getShownValue(), 'child': child.getShownValue()}
        comment = o.translate('to_parent_descr', mapping=maP)
        o.history.add('Custom', label='to_parent', comment=comment)
        # Add the child group among p_o's allowed children
        isIndexed = bool(o.getField(ac).indexed)
        o.link(ac, child, reindex=isIndexed)
        # Recompute impacted indexes
        o.reindex(fields=('cid', 'groups'))
        # Recompute local roles on p_o
        parent.setLocalRoles(o, alsoForChildren=alsoForChildren,
                             reindex=True, ac=ac)
        # Compute a nice message and redirect the user (excepted if we are
        # called from m_moveMany (p_fromMany is True): this latter deals it by
        # itself).
        if not fromMany:
            mapping = {
              'title': o.getPrefixedTitle(),
              'classNamePlural': o.translate(f'{o.class_.name}_plural').lower()
            }
            class_.finalize(o, parent, child, name, mapping, False)
        # Log
        o.log(class_.MOVED_OK % (o.class_.name, o.iid, o.getShownValue(),
                                 child.iid, child.getShownValue(), parent.iid,
                                 parent.getShownValue(), ac))

    @classmethod
    def moveCategory(class_, cat, fromMany=False):
        '''Moving a category to a parent group is a little bit trickier than for
           any other obejct. So, it deserves this specific method, that will, in
           the end, call p_move.'''
        # Get the name of the Ref that links a category to its container group
        group, name = cat.getContainer(forward=True)
        # Get the name of the used "allowedChildren*" attribute on this p_cat
        ac = cat.getAllowedChildrenAttribute()
        return class_.move(cat, name, True, fromMany=fromMany, ac=ac)

    @classmethod
    def moveMany(class_, group, objects, name, alsoForChildren, isCat=False):
        '''Moves these p_objects at once, by repeatedly calling m_move'''
        group.resp.fleetingMessage = False
        if not objects:
            return False, group.translate('no_effect_no_object')
        # Execute m_move (or an alternate method, depending on p_single) for
        # every object from p_objects.
        for o in objects:
            if isCat:
                # A specific method exists for moving a single category
                class_.moveCategory(o, fromMany=True)
            else:
                class_.move(o, name, alsoForChildren, fromMany=True)
        # Redirect the user to the page listing v_parent's objects
        mapping = {'count': len(objects)}
        message = class_.finalize(o, group.parent, group, name, mapping, True)
        return True, message
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
