#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# HubSessions © GeezTeem 2010› https://www.geezteem.com/253
# AGPL-3.0-or-later - https://www.gnu.org/licenses/agpl-3.0.txt

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
from appy.all import *

from HubSessions.Item import Item
from HubSessions.User import User
from HubSessions.participant import Status
from HubSessions.Mail import Mail, MailToGuest
from HubSessions import Config, utils, workflows
from HubSessions.Remunerations import Remunerations

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PART_ED   = 'Participant "%s" edited in meeting "%s".'
PART_DEL  = 'Participant "%s" deleted in meeting "%s".'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class ParticipantColumns(utils.Columns):
    '''Manages columns for lists of participants'''

    width = '120px|'
    bwidth = '70px|'
    c = utils.Columns

    # Base columns
    base = [
      Col.Checkbox('_checkbox*16px|', td=f'padding-top:0.8em;{c.borderR_}'),
      c.getTitle(first=False),
      Col(f'duty*{width}', td=c.mid),
      Col(f'signerCb*{bwidth}', td=c.mid),
      Col(f'guestCb*{bwidth}', td=c.mid),
    ]

    # Optional columns
    voter   = Col(f'voterCb*{bwidth}', td=c.mid)
    role    = Col(f'role*{width}', td=c.mid)
    company = Col(f'company*{width}', td=c.mid)

    # The final column
    status  = Col(f'statusUi*{width}', td=c.end)

    @classmethod
    def get(class_, tool):
        '''Returns p_class_.base columns, with style'''
        r = list(class_.base)
        # Add column "voter" if votes are enabled
        config = tool.config
        if config.votes.enabled:
            r.insert(-1, class_.voter)
        # Add optional columns, if used
        unused = config.unusedParticipantFields
        if 'role' not in unused:
            r.insert(2, class_.role)
        if 'company' not in unused:
            r.insert(2, class_.company)
        # Add the final column
        r.append(class_.status)
        # Add styles
        class_.addHeaderStyles(tool, r)
        return r

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Participant:
    '''A participant represents a user participating in a meeting'''

    popup = ('600px', '550px')
    workflow = workflows.TooPermissive
    Columns = ParticipantColumns

    creators = ['Authenticated'] # More precise permissions will come from Ref
                                 # Meeting.participants and the meeting workflow

    # ~~~ Participant's statuses (see field "status" below) ~~~

    # Statuses for participants whose presence was foreseen but who aren't there
    absentStatuses = ('excused', 'absent')

    # Statuses for participants being attendees
    attendeeStatuses = ('attendee', 'lateAttendee')

    # Status "unattendee" refers to someone that has a link to some item but
    # whose presence in the meeting is not foreseen. For example, someone may be
    # referred as being some item's author. But among all authors, only a few of
    # them (or a single one) may be expected to be present while presenting it.
    allAbsentStatuses = absentStatuses + ('unattendee',)

    # All possible statuses for participants
    allStatuses = ['attendee', 'lateAttendee'] + list(allAbsentStatuses)
    baseColumns = ['title', 'duty*250px', 'statusUi*150px|', 'signerCb*40px|',
                   'guestCb*40px|']

    def _getContentLanguages(self):
        '''Some fields may need to be multilingual'''
        return Config.configLanguages

    listColumns = Columns.get

    def uses(self, name):
        '''Is field named p_name in use ?'''
        return name not in Config.unusedParticipantFields

    @staticmethod
    def update(class_):
        '''Hide the title'''
        class_.fields['title'].show = 'xml'

    # Field "title with replacement" displays the participant's name and first
    # and the name of its replacement when relevant.

    def getTitleWithReplacement(self, short=True, sep='<br/>'):
        '''Displays the participant's title together with its replacement's
           title when relevant.'''
        r = self.title or '?'
        if not self.isEmpty('replacement'):
            replacement = self.replacement.title
            if short:
                r = f'{r} ({replacement})'
            else:
                textR = self.translate('replaced_by')
                r = f'{r}{sep}{textR} {replacement}'
        return r

    titleWithReplacement = Computed(method=getTitleWithReplacement, show=False,
                                    label=(None, 'title'))

    def getTitle(self, transform=None):
        '''Returns the participant's title or the replacement user's title if
           any. p_transform can be "upper" or "lower".'''
        target = 'tiedUser' if self.isEmpty('replacement') else 'replacement'
        r = getattr(self, target).getTitle()
        # Apply p_transform when relevant
        return getattr(r, transform)() if transform else r

    def getSupTitle(self, nav):
        '''Display an icon for a remunerable participant'''
        if not self.remunerable: return
        iconUrl = self.buildSvg('remunerable')
        text = Escape.xhtml(self.translate('is_remunerable'))
        return f'<img src="{iconUrl}" class="help iconSUP" title="{text}"/>'

    def getSubTitle(self):
        '''Displays important info in the sub-title'''
        r = ''
        # The replacement person
        if not self.isEmpty('replacement'):
            repText = self.translate('replaced_by')
            r = f'{r}<br/><i>{repText} {self.replacement.title}</i>'
        # Remunerations-related info
        if self.remunerable:
            r = f'{r}<br/>{Remunerations.asText(self)}'
        # A link to the User object
        if self.allows('write'):
            user = self.tiedUser
            if user:
                r = f'{self.tiedUser.getLink(page="assembly")}{r}'
            else:
                # This should not happen
                text = self.translate('user_missing')
                r = f'{r}<div class="focus">{text}</div>'
        return r or None

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                  Creation or selection of a tied user
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    def showUserField(self):
        '''Once the participant has been created and tied to a (possibly created
           on-the-fly) user, it is not possible to change this link.'''
        return self.isTemp() and 'edit' or None

    # Select an existing user (member or non member) or create a new one ?
    userAction = Select(validator=('getMember', 'getNonMember', 'create'),
                        default='getMember', render='radio', show=showUserField)

    # The link to the related user
    def listMeetingUsers(self):
        '''Lists the meeting users that can be added as participants in this
           meeting.'''
        # Try first to get them from the cache
        cache = self.cache
        if 'listedMeetingUsers' in cache: return cache.listedMeetingUsers
        # Compute them else
        meeting = self.initiator.o
        r = meeting.getAvailableUsers()
        # Cache and return the result
        cache.listedMeetingUsers = r
        return r

    def showTiedUser(self):
        '''Show field "tiedUser" only when creating a Participant instance, or
           on the XML layout.'''
        if self.isTemp():
            # Show it on "edit", unless there is no more tied user to choose
            r = 'edit' if self.listMeetingUsers() else None
        else:
            r = 'xml'
        return r

    tiedUser = Ref(User, add=False, link=True, multiplicity=(1,1),
                   back=Ref(attribute='participants', multiplicity=(0,None),
                            show=False),
                   show=showTiedUser, select=listMeetingUsers,
                   master=userAction, masterValue='getMember',
                   xml=lambda o, value: value[0].login,
                   backSecurity=False)

    def getTiedUser(self):
        '''Return this participant's tied user or replacement user if he is
           replaced.'''
        target = 'tiedUser' if self.isEmpty('replacement') else 'replacement'
        return getattr(self, target)

    # A message to display if there is no meeting user to add (all available
    # users already being included as meeting participants).

    def showInfoNoUser(self):
        '''Show a message if the user want to link a new user but it is not
           possible, because all available users are already defined as meeting
           participants.'''
        # This has sense only when creating a new participant
        if not self.isTemp(): return
        if not self.listMeetingUsers(): return 'edit'

    infoNoUser = Info(focus=True, show=showInfoNoUser,
                      master=userAction, masterValue='getMember')

    # A temporary field used to select, via a popup, a non-meeting user, when
    # userAction is "getNonMember".
    selectedUser = Ref(User, add=False, link='dropdown', multiplicity=(1,1),
      back=Ref(attribute='selectedParticipants', show=False),
      show=showUserField, master=userAction, masterValue='getNonMember',
      select=Search(maxPerPage=20, sortBy='title', cid='1_toTool'),
      label=(None, 'tiedUser'), backSecurity=False)

    # Temporary fields whose sole purpose is to carry info in order to create a
    # new user and link it to this participant.

    sp = {'multiplicity': (1, 1), 'show': showUserField,
          'master': userAction, 'masterValue': 'create'}

    userGender = Select(label=('User', 'gender'), validator=('m', 'f'), **sp)

    sp['width'] = 40
    userName      = String(label=('User', 'name')     , **sp)
    userFirstName = String(label=('User', 'firstName'), **sp)
    userEmail     = String(label=('User', 'email')    , **sp)

    sp['languages'] = _getContentLanguages
    sp['masterValue'] = ['create', 'getNonMember']
    sp['layouts'] = Layouts.d
    userDuty = String(label=('User', 'duty'), **sp)

    del sp['multiplicity']
    sp['show'] = lambda o: (o.isTemp() and o.uses('company')) and 'edit' or None
    userCompany = String(label=('User', 'company'), **sp)

    sp['show'] = lambda o: (o.isTemp() and o.uses('role')) and 'edit' or None
    userRole = String(label=('User', 'role'), **sp)

    # Names of all temporary fields for creating a User instance
    createUserFields = ('userName', 'userFirstName', 'userEmail',
                        'userGender', 'userDuty', 'userCompany', 'userRole')
    # Names of all temporary fields used for updating a User instance
    updateUserFields = ('userDuty', 'userCompany', 'userRole')

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                           Participant's status
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Special layout with top space on edit, but not on view
    layoutsT = Layouts.t.clone()
    layoutsT['view'].css = ''

    # Nice grid group to visualize participant data in its popup, in view mode
    viewGroup = Group('viewP', ['30%','70%'], style='grid', hasLabel=False,
                      css='topSpace')

    p = {'group': {'edit': None, 'view':viewGroup}}

    def listStatuses(self):
        '''Lists the possible statuses for a participant'''
        r = []
        _ = self.translate
        for status in self.allStatuses:
            if status in Config.unusedParticipantStatuses: continue
            r.append((status, _(f'participant_status_{status}')))
        return r

    status = Select(validator=Selection(listStatuses), default='attendee',
                    layouts=layoutsT, width=150, multiplicity=(1,1), **p)

    def getSimpleStatus(self):
        '''Returns p_self.status, simplified: "lateAttendee" is simplified to
           "attendee".'''
        r =  self.status
        return 'attendee' if r == 'lateAttendee' else r

    def isAttendee(self):
        '''Return True if p_self is globally present: p_self's status must be
           among attendee statuses.'''
        return self.status in Participant.attendeeStatuses

    def computeStatus(self, ui=True):
        '''Display detailed status about a participant, including, for a late
           attendee, info about his "arrival item", and, for someone having
           definitely left the meeting, info about his "departure item".

           If p_ui is True, a chunk of XHTML code is returned, including icons
           and links. Else, textual information is returned.
        '''
        status = Status(self)
        return ui and status.asHtml() or status.asText()

    # "statusUi" is the graphical version of "status"
    statusUi = Computed(method=computeStatus, show='result',
                        label=(None, 'status'))

    # When a participant is absent, the reason for this absence can be speficied
    # in the following field.
    sfa = {'width': 40, 'languages': _getContentLanguages}
    sfa.update(p)

    def showAbsenceReason(self):
        '''Show absence reaon on "view" only if not empty'''
        return Show.V_ if self.isEmpty('absenceReason') else True

    absenceReason = String(master=status, masterValue=absentStatuses,
                           show=showAbsenceReason, **sfa)

    # An attendee can have a virtual presence only
    def historizeVirtual(self):
        '''Historize field "virtual" from the moment attendances are frozen on
           the tied meeting.'''
        return self.getMeeting().attendanceIsFrozen()

    virtual = Boolean(master=status, masterValue=attendeeStatuses,
                      historized=historizeVirtual,
                      layouts=Boolean.Layouts.d, **p)

    def notIfTemp(self):
        '''Returns True if p_self is not a temp object'''
        return not self.isTemp()

    signer = Boolean(show=notIfTemp, **p)
    toNotify = Boolean(show=notIfTemp, label='User', **p)
    voter = Boolean(show=lambda o: o.config.votes.enabled and not o.isTemp(),
                    **p)
    guest = Boolean(show=notIfTemp, **p)

    # Fields "xCb" below are the graphical versions of fields "x" hereabove

    def computeChecked(self, name):
        '''Returns symbol "checked" if attribute p_name is True for this
           participant.'''
        return '✔' if getattr(self, name) else ''

    signerCb = Computed(method=lambda o: o.computeChecked('signer'),
                        show='result', label=(None, 'signer'))
    voterCb = Computed(method=lambda o: o.computeChecked('voter'),
                        show='result', label=(None, 'voter'))
    guestCb = Computed(method=lambda o: o.computeChecked('guest'),
                        show='result', label=(None, 'guest'))

    def isSigner(self):
        '''Is this participant a signer (at the whole meeting level) ?'''
        if self.isEmpty('replacement'):
            return self.signer
        else:
            return self.getReplacementParticipant().signer

    def isSignerFor(self, item):
        '''Is this participant a signer for p_item? If p_item-specific signers
           are defined, this method checks if this participant is among them.
           Else, it simply checks if the participant is flagged as signer at the
           meeting level.'''
        if item.isEmpty('signers'): return self.signer
        return self in item.signers

    def isReporterFor(self, item):
        '''Is this participant a reporter for p_item ?'''
        return self in item.reporters

    # Fields "duty", "replacementDuty" and "replacementDutyF" are duplicated
    # here. Indeed, people may have different duties over time.

    def showField(self, name='duty'):
        '''Do not show the field having this p_name while creating a new
           participant: this info has no sense at this step, or will be copied
           from the tied user.'''
        if self.isTemp(): return
        # Avoid rendering an empty field on partipant/view
        return Show.V_ if self.isEmpty(name) else True

    sfa['label'] = 'User'; sfa['viewSingle'] = True

    # Special layout with top space on edit, but not on view
    layoutsTD = Layouts.td.clone()
    layoutsTD['view'].css = ''

    duty = String(multiplicity=(1,1), layouts=layoutsTD, show=showField,
                  cell=User.pxDutyOnCell, **sfa)

    replacementDuty =String(show=lambda o:o.showField('replacementDuty'), **sfa)
    replacementDutyF=String(show=lambda o:o.showField('replacementDutyF'),**sfa)

    def showOptional(self, name):
        '''Show optional field having this p_name only on a non-temp
           participant, if the field is in use.'''
        if self.isTemp() or not self.uses(name): return
        return Show.V_ if self.isEmpty(name) else True

    sfa['layouts'] = Layouts.d
    company = String(show=lambda o: o.showOptional('company'), **sfa)

    # Role the participant plays within this meeting (ie, "president",
    # "secretary"). It can be different from its non-meeting-specific duty.
    role = String(show=lambda o: o.showOptional('role'), **sfa)

    def listReplacements(self):
        '''Lists the persons that can act as a replacement for this
           participant.'''
        # Any participant can replace this user, excepted himself
        meeting = self.meeting
        r = [p.tiedUser for p in meeting.participants if p != self]
        # Any person noted as "assemblySubstitute" in the config/committee can
        # also become a replacement for this user.
        com = meeting.getCommittee() # Or the tool
        # Get replacements
        for user in com.meetingUsers:
            # Get user's usages
            usages = user.getComInfo('usages', com, default=())
            if 'assemblySubstitute' in usages and user not in r:
                r.append(user)
        return r

    def xmlReplacement(self, value):
        '''The XML version of this field must simply return the user login'''
        return value[0].login if value else None

    replacement = Ref(User, add=False, link=True, multiplicity=(0,1),
      back=Ref(attribute='replacements', show=False, multiplicity=(0,None)),
      show=lambda o:o.showField('replacement'), select=listReplacements,
      render='minimal', xml=xmlReplacement, **p)

    def getReplacementParticipant(self):
        '''Field "replacement" stores a User instance. Sometimes it is needed
           to get the Participant instance instead. You should avoid using this
           method, it is not performant.'''
        # Get the User instance
        replacement = self.replacement
        if not replacement: return
        # Fint the participant whose tied user is this user
        for participant in self.meeting.participants:
            if replacement == participant.tiedUser:
                return participant

    def getDuty(self, part=None, sep=' / ', original=False):
        '''Returns the participant's duty (or replacement duty if he is
           replaced and if p_original is False).'''
        # 2 duties can be stored in this field, separated by p_sep. Indeed,
        # before field "role" was added (= a person's duty relative to a
        # meeting, like "Meeting president"), it could be encoded within field
        # "duty", after the standard duty and preceded by some separator.
        if self.isEmpty('replacement') or original:
            r = self.duty
        else:
            # Get the male or female version of the replacement duty depending
            # on the replacement gender.
            suffix = 'F' if self.replacement.gender == 'f' else ''
            r = getattr(self, f'replacementDuty{suffix}')
            if suffix and not r:
                # The female version may not be defined: default to the male
                # version.
                r = self.replacementDuty
            # If no replacement duty can be computed, return the replacement's
            # own duty (this should be exceptional).
            if not r: r = self.getReplacementParticipant().duty
        # Manage p_part and p_sep when appropriate
        if part is not None and r and sep in r: r = r.split(sep)[part]
        return r

    def getSignaturePath(self, original=False):
        '''Returns the absolute path to the scanned signature, on disk,
           belonging to the tied user.'''
        if self.isEmpty('replacement') or original:
            target = 'tiedUser'
        else:
            target = 'replacement'
        return getattr(self, target).getSignaturePath()

    def getAbsentMention(self, sep='***', blank=None):
        '''Returns some text if this participant is absent. p_sep will surround
           the mention; if p_blank is "before" or "after", a blank will be
           inserted accordingly.'''
        status = self.status
        if status not in self.absentStatuses: return ''
        text = self.getShownValue('status')
        r = f'{sep}{text}{sep}'
        if not blank: return r
        return f' {r}' if blank == 'before' else f'{r} '

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                       Remuneration-related fields
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Is this participant remunerable ?
    remunerable = Boolean(show=Remunerations.showRemunerable,
                          layouts=Boolean.Layouts.d, **p)

    # Parameters being common to the following fields
    pf = {'layouts': Layouts.d, 'master': remunerable, 'masterValue': True,
          'alignOnEdit': 'center', 'historized': True}
    pf.update(p)

    # The number of times the remuneration amount must be paid to the
    # participant (=the number of "attendance tokens") depends on the meeting
    # duration and is normally automatically computed (based on parameter
    # tool::remunParams::durationUnit). That being said, the number of tokens
    # can be forced via the following field. For example, if the meeting lasts
    # the whole day and corresponds to 2 tokens, but a given participant has
    # joined the meeting in the afternoon, you may want to grant him a single
    # token.
    tokens = Integer(show=Remunerations.showTokens,
                     validator=Remunerations.validTokens, **pf)

    # Distance, in km, from the participant's home to the meeting place
    route = Float(show=Remunerations.showRoute, **pf)

    def routeRequired(self, checkVirtual=True):
        '''Must p_self.route be computed on p_self ?'''
        # This method focuses on participant-related conditions, not meeting-
        # related conditions (see meeting::m_computableDistances).
        r = self.remunerable and self.isAttendee()
        if checkVirtual:
            r = r and not self.virtual
        return r

    def computeRoute(self, place):
        '''Computes the distance, in km, between self's home to this (meeting)
           p_place. Records the result (OK or KO) in p_self's history and return
           True if the distance has been successfully computed.'''
        user = self.tiedUser
        _ = self.translate
        if user.geolocalized:
            # Compute the distance between the participants' home and the
            # meeting place.
            self.route = place.distanceBetween(place, user)
            # Add in workflow history
            text = _('compute_distance_descr')
            summary = place.distanceSummary(place, user, self.route)
            comment = f'<div>{text}</div>{summary}'
            self.history.add('Custom', label='compute_distance',
                             comment=comment)
            r = True
        else:
            # The distance cannot be computed. Record this info in the
            # participant's history.
            self.history.add('Custom', label='part_not_geoloc',
                             comment=_('part_not_geoloc_descr'))
            r = False
        return r

    def updateRoute(self, say=False):
        '''Try to compute p_self.route if it is empty and attendances have
           already been frozen.'''
        route = self.route
        if route is None and self.routeRequired():
            meeting = self.getMeeting()
            if meeting.computableDistances():
                # Try to compute the distance
                success = self.computeRoute(meeting.placeObject)
                if not success and say:
                    # Return a message to the UI
                    self.say(self.translate('part_route_ko_one'),fleeting=False)

    # Amount (€) for reading allowances (in french: indemnités de lecture)
    reading = Float(show=Remunerations.showReading, **pf)

    # Remuneration-related fields
    remunerationFields = ('tokens', 'route', 'reading')

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                        Entrances and departures
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # If this attendee is a late attendee, at what item did he enter ? (the
    # participant entered juste before discussing "entranceItem").
    def xmlEntranceItem(self, value):
        '''Do not dump the item URL: dump the item ID and number instead'''
        if not value: return
        item = value[0]
        return f'{item.iid}:{item.nb()}'

    entranceItem = Ref(Item, add=False, link=True, multiplicity=(0,1),
      back=Ref(attribute='entrances', show='xml', multiplicity=(0,None)),
      xml=xmlEntranceItem, show='xml')

    def hasEntered(self, meeting, item):
        '''During p_meeting, was this participant already present when
           discussing p_item?'''
        if self.status != 'lateAttendee':
            raise Exception('Participant::hasEntered can be called on late ' \
                            'attendees only.')
        if self.isEmpty('entranceItem'): return
        items = meeting.items
        return items.index(item) >= items.index(self.entranceItem)

    def mayEnter(self):
        '''May this participant enter the meeting ?'''
        if self.status != 'lateAttendee': return
        return self.isEmpty('entranceItem')

    # If this attendee left completely the meeting before its end, at what item
    # did he leave? ("departureItem" is the last item for which he attended).
    def xmlDepartureItem(self, value):
        '''Do not dump the item URL: dump the item ID and number instead'''
        if not value: return
        item = value[0]
        return f'{item.iid}:{item.nb()}'

    departureItem = Ref(Item, add=False, link=True, multiplicity=(0,1),
      back=Ref(attribute='departures', show='xml', multiplicity=(0,None)),
      xml=xmlDepartureItem, show='xml')

    def hasLeft(self, meeting, item, justAfter=False):
        '''Has this participant already left the p_meeting when discussing
           p_item? If p_justAfter is True, the question becomes: has this
           participant left the meeting just after p_item?'''
        if self.isEmpty('departureItem'): return
        items = meeting.items
        i = items.index(item)
        j = items.index(self.departureItem)
        return i == j if justAfter else i > j

    def attends(self, meeting, item, could=False):
        '''Is this participant present when discussing p_item during
           p_meeting ? If p_could is True, the method also returns True if the
           participant is absent but p_could be present:
           * a late attendee that has not entered the meeting yet;
           * someone flagged as absent for this particular p_item (in field
             "item.absents"): this flag can be changed.'''
        status = self.status
        # Is she globally absent ?
        if status in self.absentStatuses: return
        # Has she completely left the meeting before p_item ?
        if self.hasLeft(meeting, item): return
        # Is she a late attendee not being arrived yet ?
        if not could and status == 'lateAttendee' and \
           not self.hasEntered(meeting, item): return
        # Has she temporarily left the meeting during p_item ?
        if not could and self in item.absents: return
        return True

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                           Action "refresh"
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    def doRefresh(self):
        '''Reload data from the user and overwrites it on the participant
           instance. This action is the only way to update the participant's
           name and first name based following a change to the corresponding
           user.'''
        self.initialiseData(self.getMeeting())

    def showRefresh(self):
        '''Show button "refresh" to people allowed to update p_self'''
        if self.mayEdit(): return Show.BS

    refresh = Action(action=doRefresh, show=showRefresh, render='icon',
                     confirm=True, icon='reload.svg', iconSize='27px')

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                       Send a mail to a guest
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    mailToGuest = Action(action=lambda o, options: options.send(o),
                         icon='HubSessions/mail', render='icon',
                         show=MailToGuest.show, options=MailToGuest)

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                                  PXs
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Render a participant, on layout "view", within field Item::talk

    viewInTalk = Px('''
     <x var="curr=o.getValue(name)" if="curr">
      <div>:curr.title</div>
      <div class="discreet" if="not curr.isEmpty('company')">:curr.company</div>
     </x>''')

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                            Main methods
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    def getMeeting(self):
        '''Gets the tied meeting. Get it on the request object when available,
           for performance.'''
        return self.container or self.initiator.o

    def validate(self, new, errors):
        '''Participant inter-field validation'''
        # If a user needs to be created, ensure it does not already exist
        _ = self.translate
        userAction = new.userAction or 'getMember'
        if userAction == 'create':
            # Ensure the user to create does not already exist
            email = new.userEmail.strip()
            com = self.container.getCommittee()
            # If we are at the global level, the number of users may be high:
            # check only among meeting participants: this is the most important.
            name = 'meetingUsers' if com.isTool else 'users'
            for user in getattr(com, name):
                if user.email == email:
                    msg = _('email_exists')
                    errors.userEmail = msg
        elif userAction == 'getNonMember':
            # Ensure the user is not a special user
            selected = new.selectedUser
            if selected.isSpecial():
                msg = _('special_participant')
                errors.selectedUser = msg
            else:
                # Ensure the selected user is not already among participants
                for participant in self.container.participants:
                    if participant.tiedUser == selected:
                        msg = _('already_participates')
                        errors.selectedUser = msg
        elif userAction == 'getMember' and self.isTemp():
            # A member must be chosen: it is not possible if every member is
            # already in the meeting.
            tiedUser = new.tiedUser
            if not tiedUser:
                errors.userAction = _('Participant_infoNoUser')
        # Stop here if errors have already been detected
        if errors: return
        # Manage replacements. When a user has a replacement, he cannot be
        # present; when a user is a replacement for someone else, he cannot be
        # absent.
        if new.replacement and new.status not in self.absentStatuses:
            msg = _('replacee_must_be_absent')
            errors.status = msg
            errors.replacement = msg
        if new.status in self.absentStatuses and not self.isEmpty('tiedUser') \
           and not self.tiedUser.isEmpty('replacements'):
            # Check if the user is not already a replacement for a participant
            # of *this* meeting.
            participants = self.meeting.participants
            for p in self.tiedUser.replacements:
                if p in participants:
                    msg = _('replacement_must_be_present')
                    errors.status = msg
                    break
        if new.virtual and 'route' in new and new.route != 0:
            errors.virtual = errors.route = _('route_part_virtual')

    def setTitle(self, user):
        '''Computes the participant's title based on the tied p_user's title'''
        self.title = user.getTitle(nameFirst=Config.participantNamesFirst,
                                 nameTransform=Config.participantNamesTransform)

    def initialiseData(self, meeting):
        '''Copy some fields from the tied user. Returns True if no tied user is
           found.'''
        if self.isEmpty('tiedUser'): return True
        tied = self.tiedUser
        # Copy the user title
        self.setTitle(tied)
        # Copy "meeting-specific" field values. Duplicate them if dicts.
        com = meeting.getCommittee()
        for name in User.meetingFields:
            value = tied.getComInfo(name, com)
            if value and not isinstance(value, str): value = value.copy()
            setattr(self, name, value)
        # Initialise boolean attributes
        self.signer = tied.getComInfo('signatureIsDefault', com)
        toNotify = tied.getComInfo('toNotify', com)
        if toNotify is None: toNotify = True
        self.toNotify = toNotify
        if meeting.config.remunerations.enabled:
            self.remunerable = bool(tied.getComInfo('remunerable', com))
        usages = tied.getComInfo('usages', com, ())
        if self.config.votes.enabled:
            self.voter = 'voter' in usages
        self.guest = 'guest' in usages

    def getUserParams(self, fields):
        '''Returns a dict containing all values from user* fields, used to
           create or update a User as tied user for this participant.'''
        # Depending on the fact that a user must be created or updated, the list
        # of p_fields to retrieve is different.
        # ~
        # The method returns a dict of values and cleans the temporary user*
        # fields on this Participant instance.
        r = {}
        for name in fields:
            aname = f'{name[4].lower()}{name[5:]}'
            value = getattr(self, name)
            if isinstance(value, dict):
                for key in value:
                    value[key] = (value[key] or '').strip()
            elif isinstance(value, str):
                value = value.strip()
            r[aname] = value
            # Clean the temp field on the Participant instance
            setattr(self, name, None)
        return r

    def onEditEarly(self, o):
        '''Create the tied user when relevant'''
        # Managing the "user action" must be done before m_onEdit, because
        # m_onEdit is executed after the participant is tied to its meeting
        # (=the initiator object). A requirement for performing this link is
        # that the tied user must already be tied to the participant (it is used
        # to order the participant in the Ref), which is not the case if the
        # user instance must be created (user action "create") or is not linked
        # yet (user action "getNonMember").
        if self.userAction == 'create':
            com = self.initiator.o.getCommittee()
            # Generate a unique login
            login = self.tool.getIncrementalLogin()
            # Create the User instance
            params = self.getUserParams(self.createUserFields)
            tiedUser = com.create('users', login=login, usages=['guest'],
                                  **params)
            # Link it to this participant
            self.tiedUser = tiedUser
            # When working in a committee, transfer single-valued fields to the
            # corresponding entry in dict "byCommittee". Do it before linking to
            # the committee: that way, usage "guest" is moved to its correct
            # place and mails settings can be disabled for him.
            if not com.isTool:
                tiedUser.updateDictField('byCommittee')
            # Link it to the committee or tool, as meeting user
            com.link('meetingUsers', tiedUser)

        elif self.userAction == 'getNonMember':
            com = self.initiator.o.getCommittee()
            # Set the selected user as tied user
            selected = self.selectedUser
            self.tiedUser = selected
            self.selectedUser = None
            # Manage data about this user
            usages = selected.getComInfo('usages', com, ())
            if not usages:
                usages = ['guest']
            elif 'guest' not in usages:
                usages.append('guest')
            selected.usages = usages
            params = self.getUserParams(self.updateUserFields)
            for name, value in params.items():
                if value is not None:
                    setattr(selected, name, value)
            # When working in a committee, transfer single-valued fields to the
            # corresponding entry in dict "byCommittee".
            if not com.isTool:
                selected.updateDictField('byCommittee')
            # Put this selected user among meeting users (could already be
            # done). This action can't be done before calling m_updateDictField,
            # see comment if the previous "if".
            com.link('meetingUsers', selected)
        # Clean field "userAction"
        self.userAction = None

    def cleanUnused(self):
        '''Ensure unused fields are cleaned'''
        status = self.status
        if status not in Participant.absentStatuses:
            # Ensure field "absenceReason" is empty if p_self's status is not an
            # absence status.
            self.absenceReason = None
        if status not in Participant.attendeeStatuses:
            # Ensure field "virtual" is empty if p_self's status is not an
            # attendance status.
            self.virtual = None
        # Ensure remuneration-based fields are empty for a non-remunerable
        # participant.
        if not self.remunerable:
            for name in self.remunerationFields:
                setattr(self, name, None)

    def onEdit(self, created):
        meeting = self.meeting
        if created:
            # Initialise participant data based on info from the tied user
            self.initialiseData(meeting)
            # Notify policies of this new participant
            Config.policies.apply(meeting, 'onParticipantAdd', participant=self)
        # Ensure unused fields are cleaned
        self.cleanUnused()
        # The replacement user can be a substitute coming from the config and
        # not among the participants yet.
        if not self.isEmpty('replacement'):
            users = [p.tiedUser.id for p in meeting.participants if p != self]
            replacement = self.replacement
            if replacement.id not in users:
                participant = meeting.create('participants',
                                             tiedUser=replacement)
        # Compute p_self.route when relevant
        self.updateRoute(say=True)
        if not created:
            self.log(PART_ED % (self.title, meeting.getDate(withHour=True)))

    # Security follows meeting security
    def mayView(self): return self.getMeeting().allows(r)
    def mayEdit(self): return self.getMeeting().allows(w)
    def mayDelete(self): return self.getMeeting().allows(d)

    def onDelete(self, minimal=False):
        '''Prevents participant deletion if he is used as a replacement. We do
           not check it in m_mayDelete for performance reasons. Indeed, we
           browse here all meeting participants.'''
        # If p_minimal is True, deleting this participant is done in a special
        # context, like deleting the whole meeting or reloading all its
        # participants. In that case, we don't care about logging the operation
        # or preventing a user to be deleted if used is a replacement for
        # another.
        meeting = self.meeting
        # A participant used as a replacement can't be deleted
        if not minimal:
            user = self.tiedUser
            for participant in meeting.participants:
                if (participant != self) and \
                   not participant.isEmpty('replacement') and \
                   (participant.replacement == user):
                    msg = self.translate('participant_is_replacement')
                    self.raiseUnauthorized(msg)
        # Notify policies
        Config.policies.apply(meeting, 'onParticipantRemove', participant=self)
        # Log the deletion
        if not minimal:
            self.log(PART_DEL % (self.title, meeting.getDate(withHour=True)))

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
autoref(Participant, Item.instigators)
autoref(Participant, Item.authors)
autoref(Participant, Item.addressees)
autoref(Participant, Item.absents)
autoref(Participant, Item.signers)
autoref(Participant, Item.reporters)
autoref(Participant, Item.questioners)
autoref(Participant, Item.answerers)
# There is no back Ref for a Ref within a List
speaker = Item.talk.getField('speaker')
speaker.class_ = Participant
speaker.view  = Participant.viewInTalk
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
