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

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
from persistent.mapping import PersistentMapping

from appy.all import *
from appy.utils import formatNumber

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IND_VOTE_OV  = 'Individual vote overwrite: %s'
VOTE_ANN_IKO = 'Annex %s not found on item %s.'
VOTE_DONE    = 'Vote encoded: %s'
SECRET       = '<secret>'
SECRET_KO    = 'Method "%s" is unusable when votes are secret.'
VOTE_STR     = '"%s" voted on %s - item %s%s'
ANNEX_PART   = ' - annex %s (#%s)'
REMOVED      = 'Votes removed on %s (%s).'
UPDATED      = '%s: %ssecret votes saved%s.'
SWITCHED     = '%s: votes switched (secret=%s).'
DELETED      = '%s: %ssecret votes deleted.'
SW_VOTES_KO  = 'Switch votes: old and new modes are identical.'
VOTE_TYPE_MM = 'Vote type mismatch.'
SV_EXCESS    = '%s: too much secret votes, impossible to repair automatically.'
V_REMOVED    = '%s: vote(s) removed for user(s) %s not being voter(s) anymore.'
V_MISSING    = '%s: incomplete vote: %d vote(s) for %d voters.'
HV_ADDED     = '%s: voter "%s" added in dict "havingVoted".'
HV_REMOVED   = '%s: voter "%s" removed from dict "havingVoted".'
V_CLOSED     = 'Closed votes for meeting %s: %d missing vote(s) on %d ' \
               'target(s), involving %d voter(s).'
NO_V_DICT    = 'Vote dict "%s" does not exist on %s %s.'

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Config:
    '''Configuration about the HubSessions voting system'''

    # In HubSessions, a vote may be applied on the following elements.
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # An item  | The item must be included in a meeting. Voters are meeting
    #          | participants being defined as voters at the meeting level, via
    #          | attribute Participant.voter. Moreover, they must attend the
    #          | item in order to vote on it.
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # An annex | Some items are further refined into logical "parts"; distinct
    #          | votes may be applied to these parts. For example, an item may
    #          | represent a law, made of several law articles. Imagine a law
    #          | made of 7 articles. The most controversial parts of these
    #          | articles are articles 3 and 7. It may be legitimate to make 3
    #          | votes:
    #          |
    #          | (1) a first "global" vote on articles 1, 2, 4, 5 and 6 ;
    #          | (2) a specific vote on article 3 ;
    #          | (3) a specific vote on article 7.
    #          |
    #          | In order to handle such cases, HubSessions has defined the
    #          | concept of "votable annex". Because several annexes can be
    #          | associated to an item, and annex types can be defined,
    #          | annexes have been "misused" to be able to apply votes on item
    #          | parts. The Annex config (see HubSessions/Annex.py) allows to
    #          | determine which annex types are considered "votable". On every
    #          | annex being of this type, a vote may be applied. On such
    #          | annexes, uploading a file is not mandatory, but may be useful
    #          | for uploading a file as produced by an external hardware or
    #          | software voting system (in XML or Excel format).
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # As soon as you enable votes on annexes, it becomes impossible to directly
    # vote on the items themselves. Votable annexes are enabled if you define at
    # least one votable annex, in config.annexes.votableTypes.
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    def __init__(self):
        # Are votes enabled ?
        self.enabled = False

        # 2 main voting modes can be enabled. When attribute "individual" is...
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # False | (the default) votes are done outside HubSessions, are
        #       | collected via various external means (by email, via a physical
        #       | urn, by counting up raised hands...) and are encoded by
        #       | meeting managers within HubSessions.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # True  | Votes are done within HubSessions, by the voters themselves
        #       | being duly authenticated via HubSessions.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        self.individual = False

        # On every votable element (item or votable annex), votes can be secret
        # or not secret. Choose here if votes will be secret or not by default.
        # This is only a default setting: element per element, an authorized
        # user will be able to switch to the other mode.
        self.secret = False

        # When individual votes are enabled, 2 more meeting-related events make
        # sense: opening and closing votes. These events define the period of
        # time during which individuals are allowed to vote. Currently, the
        # mentioned values are the only possible values and correspond to
        # meeting transitions: voting is enabled as soon as the meeting is
        # frozen, until it is decided. With one limitation: if the opening event
        # is earlier than the meeting's start date (which is the case here), the
        # effective opening date is this latter. When HS managers encode votes
        # themselves, the "voting" period is implicit and spans from the meeting
        # start hour to the triggering of meeting transition "decide".
        self.opening = 'freeze'
        self.closing = 'decide'

        # Activated vote values. Values can be:
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #    "not_yet"    | The vote has not been encoded yet
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #      "yes"      | self-explanatory
        #      "no"       |
        #    "abstain"    |
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # "does_not_vote" | The voter does not vote for the specified element
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # "has_not_voted" | When individual votes are enabled, this value is
        #                 | encoded when a voter has missed the deadline and has
        #                 | not voted. This value must not be mentioned among
        #                 | activated values: it will be automatically set by
        #                 | HubSessions when appropriate.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #   "not_found"   | When performing a paper vote, the ballot was not
        #                 | found in the urn)
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #    "invalid"    | When performing a paper vote, the ballot doesn't
        #                 | meet the conditions to be considered as a valid
        #                 | vote.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #     "blank"     | self-explanatory
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        self.values = ['yes', 'no', 'abstain', 'does_not_vote']

        # For some outputs, vote values must be represented by single chars.
        # The following dict must have an entry for every vote value as listed
        # in p_self.values.
        self.chars = {'yes': 'Y', 'no': 'N', 'abstain': '-',
                      'does_not_vote': 'x'}

        # The default vote value. Can be None or one of the values from
        # p_self.values. This default value will not be used when encoding
        # individual or secret votes. It will only be used as a timesaver for a
        # HS manager that must encode votes in the name of every voter.
        self.defaultValue = 'yes'

        # You can define here a Python expression that will determine if, on a
        # given item, votes need to be given or not. The expression receives the
        # item as variable "item". By default, when votes are enabled on items,
        # a vote must be applied on every item from every meeting.
        self.condition = 'True'

        # Must we show the button allowing meeting managers to set all votes to
        # value "yes" ?
        self.allYes = True

        # At the meeting level, it is possible, via the following attribute, to
        # enable or disable the upload of an external Excel file defining votes
        # for every applicable participant and inner item.
        self.meetingFile = False

        # By default, once a meeting is decided, anyone allowed to consult it
        # will be allowed to consult votes within it. If attribute "preview" is
        # True, people will be allowed to consult votes earlier, as soon as
        # votes are closed.
        self.preview = False

    def prepareOnFreeze(self):
        '''Returns True if vote-related data structures must be prepared when a
           meeting is frozen.'''
        return self.enabled and self.individual and self.opening == 'freeze'

    def meetingFileEnabled(self):
        '''Is the upload of a file on the meeting enabled ?'''
        return self.enabled and self.meetingFile

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Quorum:
    '''Represents the percentage of voters that must be present in order to
       produce a valid vote.'''

    default = 50

    @classmethod
    def show(class_, meeting):
        '''Show field "quorum" on this m_meeting if votes are enabled and field
           "quorum" is in use.'''
        if meeting.config.votes.enabled and meeting.uses('quorum'):
            return Show.V_B

    @classmethod
    def validate(class_, meeting, value):
        '''The quorum is a percentage: it must be between 5 and 100'''
        if value < 5 or value > 100:
            return meeting.translate('range_ko', mapping={'min':5, 'max':100})
        return True

    def __init__(self, o):
        '''A Quorum instance collects data for computing the quorum on a given
           votable p_o(bject).'''
        self.o = o
        self.isMeeting = self.o.class_.name == 'Meeting'
        self.total = 0 # The total number of voters
        self.absents = 0 # Those being absents or excused
        # % of voters being attendees (will be computed by m_computeOn)
        self.attendance = None
        self.reached = False # Was the quorum reached ?

    def getMeeting(self):
        '''Returns the meeting for which this quorum is computed'''
        return self.o if self.isMeeting else self.o.meeting

    def compute(self):
        '''Compute this quorum'''
        isMeeting = self.isMeeting
        meeting = self.getMeeting()
        # Browse meeting participants being voters
        for p in meeting.participants:
            if not p.voter: continue
            self.total += 1
            # Count the number of voters being absent
            if isMeeting:
                condition = 'ttendee' in p.status
            else:
                condition = p.attends(meeting, self.o)
            if not condition:
                self.absents += 1
        # Compute the attendance rate (as a percentage betweeen 0 and 100)
        try:
            self.attendance = ((self.total - self.absents) / self.total) * 100
        except ZeroDivisionError:
            # There is no voter. This should not occur
            self.attendance = 100.0
        self.reached = self.attendance >= meeting.quorum

    @classmethod
    def computeOn(class_, o):
        '''Compute a quorum on this votable p_o(bject)'''
        r = Quorum(o)
        r.compute()
        return r

    @classmethod
    def getText(class_, o, ifNotReached=False):
        '''Returns a translated sentence about the quorum on p_o'''
        quorum = class_.computeOn(o)
        if quorum.reached:
            # If p_ifNotReached is True, do not return anything if the quorum is
            # reached.
            if ifNotReached: return
            suffix = 'ok'
            map = None
        else:
            suffix = 'ko'
            fmt = formatNumber
            map = {'percent': formatNumber(quorum.attendance),
                   'quorum' : formatNumber(quorum.getMeeting().quorum),
                   'total'  : quorum.total,
                   'attends': quorum.total - quorum.absents}
        return o.translate(f'quorum_{suffix}', mapping=map)

#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class Vote:
    '''Option class allowing to encode an individual vote'''

    # This option class allows to encode an individual vote and is only used if
    # config attribute "individual" (see hereabove) is set to True.

    # Some elements will be traversable
    traverse = {}

    # Make some classes available here
    Quorum = Quorum

    # Most vote-related functionality is tied to meeting participants and is
    # found in module HubSessions/participant.

    # The default vote values
    defaultValues = ('yes', 'no', 'abstain')

    # Ony these vote values are selectable by a voter
    selectableValues = {'yes':None, 'no':None, 'abstain':None, 'blank':None}

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

    creators = True # Let anyone "create" options
    popup = ('400px', '300px')
    indexable = False
    confirmPopup = 300

    def getVoteObjects(self, initiator):
        '''Returns the tuple of objects concerned by the vote'''
        # The tuple has the form (meeting, item, annex). If the vote does not
        # concerns an annex, the "annex" part is None.
        if initiator.class_.name == 'Annex':
            annex = initiator
            item = annex.container
        else:
            annex = None
            item = initiator
        return item.meeting, item, annex

    # Explanatory message
    def computeInfo(self):
        '''Produces a message explaining that the user is about to vote on a
           specific target. The message incorporates the target's title in order
           to avoid any ambiguity.'''
        _ = self.translate
        # Get the relevant objects for defining the voting context
        meeting, item, annex = self.getVoteObjects(self.initiator.o)
        # Produce a table with the voting context
        rows = [('HubSessions', _('app_name')),
                (_('Meeting'), meeting.title.capitalize()),
                (_('Item'), item.title)]
        if annex: rows.append((_('item_part'), annex.title))
        # Create HTML rows
        i = len(rows) - 1
        while i >= 0:
            row = rows[i]
            rows[i] = '<tr><th>%s</th><td>%s</td></tr>' % (row[0], row[1])
            i -= 1
        # Produce an introductory text containing the name and login of the
        # person that is going to ncode its vote.
        user = self.user
        data = {'name': user.getTitle(), 'login': user.login}
        text = self.translate('vote_warning', mapping=data)
        return '<div>%s</div><table class="small" width="100%%">%s</table>' % \
               (text, '\n'.join(rows))

    info = Computed(method=computeInfo, show='edit')

    def listVoteValues(self):
        '''Lists the values a user may select when voting'''
        r = []
        for value in self.config.votes.values:
            # Ignore values that cannot be selected by a voter
            if value not in Vote.selectableValues: continue
            r.append((value, self.translate('vote_%s' % value)))
        return r

    voteValue = Select(validator=Selection('listVoteValues'),
                       multiplicity=(1,1), render='radio')

    def confirm(self, new):
        '''Displays a confirmation message that repeats the encoded vote
           value.'''
        translatedValue = self.translate('vote_%s' % new.voteValue)
        return self.translate('vote_confirm', mapping={'value':translatedValue})

    def asString(self, target):
        '''Returns a string summary about the vote, ready to be logged'''
        value = SECRET if target.votesAreSecret else self.voteValue
        meeting, item, annex = self.getVoteObjects(target)
        # Define the "meeting" part
        show = target.config.showMeetingNumber
        prefix = '%d. ' % meeting.number if show else ''
        meetingPart = '%s%s' % (prefix, meeting.computeDateShort(withHour=True))
        # Define the "item" part
        itemPart = '%s. %s' % (item.nb(meeting), item.id)
        # Define the "annex" part
        if annex:
            # Compute the index of the annex within item's annexes
            i = item.getIndexOf('annexes', annex, raiseError=False)
            if i is None:
                target.log(VOTE_ANN_IKO % (annex.id, item.id), type='warning')
                i = '?'
            else:
                i = str(i)
            annexPart = ANNEX_PART % (annex.id, i)
        else:
            annexPart = ''
        return VOTE_STR % (value, meetingPart, itemPart, annexPart)

    def storeOn(self, target):
        '''Stores the selected vote value on this p_target element'''
        # Security check
        isItem = target.class_.name == 'Item'
        if not Vote.showOn(target, isItem, blockIfPopup=False):
            target.raiseUnauthorized(self.translate('vote_unallowed'))
        # Get the login of the voter
        login = self.user.login
        # Store the vote value on the p_target
        overwritten = Vote.storeValueOn(target, self.voteValue, login)
        # Log the operation and return a message to the UI
        voteString = self.asString(target)
        if overwritten:
            self.log(IND_VOTE_OV % voteString, type='warning')
        self.log(VOTE_DONE % voteString)
        self.say(self.translate('vote_saved'))

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                  Visibility of vote-related actions
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    @classmethod
    def showOn(class_, target, isItem, blockIfPopup=True):
        '''Determine when to show the button allowing an individual to vote on a
           p_target element.'''
        config = target.config
        # Don't show it if individual votes are not enabled
        votes = config.votes
        if not votes.enabled or not votes.individual: return
        # Block if we are in the popup and p_blockIfPopup is True. For example,
        # m_showOn is re-executed within m_storeOn, as a preliminary security
        # check. In that case, we are in the popup, so checking this fact must
        # be disabled.
        if blockIfPopup and target.req.popup == 'True': return
        # Do not show the button if we must vote on annexes and p_target is an
        # item, or conversely.
        onAnnexes = bool(config.annexes.votableTypes)
        if (onAnnexes and isItem) or (not onAnnexes and not isItem): return
        # Don't show the button on a non-votable annex
        if not isItem and not target.isVotable(): return
        # Don't show it if the meeting has not started yet
        item = isItem and target or target.container
        if item.isEmpty('meeting') or not item.meeting.hasStarted(): return
        # Show it if individual votes are opened, the logged user is among
        # voters for p_target and she hasn't voted yet.
        login = target.user.login
        havingVoted = Vote.getDict(target, 'havingVoted',
                                   create=False, raiseError=False)
        if havingVoted and login in havingVoted and not havingVoted[login]:
            return 'buttons'

    @classmethod
    def showNoVote(class_, item):
        '''Show field p_item.noVote if in use'''
        if not item.uses('noVote'): return
        # Do not show it on "view" when there is no vote value
        return item.noVote and True or Show.V_

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                             Class methods
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    @classmethod
    def storeValueOn(class_, target, value, login):
        '''Stores, on p_target, this vote p_value for the voter having this
           p_login. Returns True if this p_value overwrote an existing value for
           the same p_voter on the same p_target.'''
        # Vote storage differs, depending on the votes being secret or not
        votes = target.votes
        havingVoted = target.havingVoted
        if target.votesAreSecret:
            # Store a secret vote
            if value in votes:
                votes[value] += 1
            else:
                votes[value] = 1
        else:
            # Store a non-secret vote
            votes[login] = value
        # Remember that this person has voted. Check if we overwrite an existing
        # vote, which should not occur.
        overwritten = bool(havingVoted.get(login))
        havingVoted[login] = True
        return overwritten

    @classmethod
    def enabledOn(class_, target, level, checkDate=True, meeting=None):
        '''Are votes enabled for p_target at some p_level ?'''
        # If level is...
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #   "meeting"  | We check that votes are globally enabled on this
        #              | HubSessions and if they are enabled on this specific
        #              | meeting (p_target). Indeed, votes can be seen and
        #              | encoded only once the meeting has started. If
        #              | p_checkDate is False, we do perform this last check and
        #              | simply check if votes are globally enabled.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #    "item"    | We check if votes are to be encoded for a specific item
        #              | (p_target). Indeed, even when votes are enabled at the
        #              | meeting level, a specific condition (in
        #              | config.votes.condition) as well as field item.noVote,
        #              | if in use, may prevent votes to be encoded on some of
        #              | its inner items.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # "itemstrict" | We also check if votes are to be encoded for a specific
        #              | item (p_target). But if annex-level votes are enabled,
        #              | the method returns False, meaning that there will be no
        #              | vote directly at the item level.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # If level is not "meeting", the meeting must be given in p_meeting
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        config = target.config
        if level == 'meeting':
            # Are votes globally enabled ?
            if not config.votes.enabled: return
            # Check if the meeting has started
            if checkDate and not target.hasStarted(): return
            return True
        else:
            # Check first if votes are globally enabled
            if not Vote.enabledOn(meeting or target.meeting, 'meeting', \
                                  checkDate=checkDate): return
            # Check if votes are required on this item
            if not Vote.requiredOn(target): return
            # If we are here, votes are enabled for this item
            if level == 'item': return True
            else: # level is "itemstrict"
                # We return True only if votes are to be encoded on the item and
                # not on its annexes.
                return not config.annexes.votableTypes

    @classmethod
    def prepareOn(class_, target, item):
        '''Prepares the vote-related data structures on this p_target'''
        # Do not prepare anything if votes have already been encoded on p_target
        if class_.hasOn(target): return
        # If p_target is an annex, its item is p_item. If p_target is an item,
        # p_item == p_target.
        # ~
        # Retrieve participants being present voters on this p_item
        voters = []
        item.getAttendees(voter=True, voters=voters)
        # Initialise dict "havingVoted" on the p_target
        havingVoted = class_.getDict(target, 'havingVoted')
        for voter in voters:
            havingVoted[voter.tiedUser.login] = False

    @classmethod
    def initialiseOn(class_, target, votes=None, havingVoted=None,
                     setSecret=True):
        '''Initialise vote-related data structures on this p_target'''
        # Initialise field "votesAreSecret" if required
        config = target.config.votes
        if setSecret:
            target.votesAreSecret = config.secret
        # Within dict "votes", keys are user logins, values are vote values
        # (strings). If votes are secret (target.votesAreSecret is True), the
        # structure is different: keys are vote values and values are numbers of
        # times the vote value has been chosen.
        target.votes = votes
        # This second data structure is solely used when votes are encoded by
        # the voters themselves. Named "havingVoted", it stores, for every
        # voter, its login (key) and a boolean (value) indicating if he already
        # voted or not (~{s_login: p_hasVoted}~). Without that information, when
        # secret votes are enabled, it would be impossible to prevent a voter to
        # vote several times. Moreover, this dict is used as condition for
        # showing the button allowing an individual to vote.
        if target.config.votes.individual:
            target.havingVoted = havingVoted
            # Add one entry per participant if participants are already known
            item = target if target.class_.name == 'Item' else target.container
            if not item.isEmpty('meeting'):
                Vote.prepareOn(target, item)

    @classmethod
    def hasOn(class_, target):
        '''Return True if vote values are defined for p_target'''
        return bool(target.votes)

    @classmethod
    def hasFor(class_, target, login):
        '''Returns True if a vote has already been encoded on this p_target for
           this p_login.'''
        if target.config.votes.individual:
            d = Vote.getDict(target, 'havingVoted',
                             create=False, raiseError=False)
            r = d and d.get(login)
        else:
            # Votes are all encoded at once by a HS manager. Simply check that
            # votes are present.
            r = Vote.hasOn(target)
        return r

    @classmethod
    def getDict(class_, target, name, create=True, raiseError=True):
        '''Gets the vote-related dict named p_name on this p_target. It p_create
           is True and the dict does not exist, it is created.'''
        r = getattr(target, name, None)
        if r is None:
            if create:
                setattr(target, name, PersistentMapping())
                r = class_.getDict(target, name, create=False)
            else:
                if raiseError:
                    message = NO_V_DICT % (name, target.class_.name, target.id)
                    raise Exception(message)
                else:
                    r = None
        return r

    @classmethod
    def getValue(class_, target, login, returnDefault=False,
                 raiseErrorIfSecret=True, asChar=False):
        '''What is the vote value defined on p_target for user with p_login ?'''

        # If no vote value is found, and p_returnDefault is True, the default
        # value as defined in the config is returned.

        # If p_raiseErrorIfSecret is True, the method will raise an exception if
        # this item is secret. Else, an empty string will be returned. If
        # p_asChar is True, instead of returning the vote value, it returns its
        # single-char-representation, as found in config.votes.chars.
        if target.votesAreSecret:
            if raiseErrorIfSecret:
                raise Exception(SECRET_KO % 'getVoteValue')
            else: return ''
        config = target.config.votes
        if Vote.hasOn(target):
            votes = target.votes
            if login in votes:
                if not asChar: return votes[login]
                return config.chars[votes[login]]
        # No value is defined for p_login
        if returnDefault:
            if not asChar: return config.defaultValue
            return config.chars[config.defaultValue]

    @classmethod
    def copy(class_, source, target):
        '''Copy votes from a p_source to a p_target object, if votes are found
           on p_source.'''
        if not Vote.hasOn(source): return
        votes = source.votes
        # Copy dict "havingVoted" as well if present
        havingVoted = Vote.getDict(source, 'havingVoted',
                                   create=False, raiseError=False)
        if havingVoted: havingVoted = havingVoted.copy()
        Vote.initialiseOn(target, votes=votes.copy(), havingVoted=havingVoted,
                          setSecret=False)

    @classmethod
    def removeOn(class_, target):
        '''Removes any vote-related data structure on this p_target'''
        target.votes = None
        target.havingVoted = None
        target.log(REMOVED % (target.class_.name.lower(), target.id))

    @classmethod
    def requiredOn(class_, item):
        '''Returns True if a vote is (or will be, or has been) requested on this
           p_item.'''
        return not item.noVote and eval(item.config.votes.condition)

    @classmethod
    def getTargetsIn(class_, item, checkRequired=True):
        '''Gets the target votable elements related to this p_item: either the
           item itself, or its inner votable annexes.'''
        votableTypes = item.config.annexes.votableTypes
        if votableTypes:
            # Votes are applied on annexes only: return all found votable
            # annexes. In this case, any votable annex is considered implicitly
            # votable: it has no sense to check, on a votable annex, if votes
            # are enabled or not.
            r = [annex for annex in item.annexes \
                 if annex.annexType.annexTypeId in votableTypes]
        else:
            # Votes are directly applied on items: return the item itself. In
            # that case, it has sense to check whether votes are effectively
            # required on this p_item. That being said, if p_checkRequired is
            # False, this check is not performed, which suits cases where we
            # could search for undue votes on items for which votes were
            # required in the past but are not now. Such a search is performed
            # by m_repairOn.
            r = []
            if not checkRequired or Vote.requiredOn(target):
                r.append(item)
        return r

    @classmethod
    def countOn(class_, target):
        '''Gets the total number of votes on p_target'''
        votes = target.votes
        # When votes are not secret...
        if not target.votesAreSecret: return len(votes)
        # When votes are secret...
        r = 0
        for count in votes.values(): r += count
        return r

    @classmethod
    def countValue(class_, target, voteValue):
        '''Gets the number of votes for p_voteValue on this p_target'''
        if not Vote.hasOn(target): return 0
        votes = target.votes
        # When votes are secret...
        if target.votesAreSecret:
            if voteValue in votes: return votes[voteValue]
            return 0
        # When votes are individual...
        r = 0
        for aValue in votes.values():
            if aValue == voteValue: r += 1
        return r

    @classmethod
    def manageOn(class_, annex):
        '''Creates or removes votes on this p_annex every time it is created or
           updated.'''
        if annex.isVotable():
            if not Vote.hasOn(annex):
                Vote.initialiseOn(annex)
            # Do we need to parse the uploaded file ?
            fun = annex.config.annexes.votableFunction
            req = annex.req
            inReq = 'file_file' in req or 'file_delete' in req
            if fun and not annex.isEmpty('file') and inReq and \
               req.file_delete != 'nochange':
                # A file was uploaded, parse it
                success, message = fun(annex, annex.file.getFilePath(annex))
                label = 'votes_saved' if success else 'action_ko'
                # Create the message to return
                text = '<div><b>%s</b></div>' % annex.translate(label)
                if message:
                    text += '<br/><div>%s</div><br/>' % message
                annex.say(text)
        else:
            # The annex is not votable (anymore): ensure no vote is stored on it
            if Vote.hasOn(annex):
                Vote.removeOn(annex)

    @classmethod
    def statusOn(class_, participants):
        '''Produce, as a chunk of XHTML, a status message about votes as
           currently encoded (or not) on this p_participants.target.'''

        # p_participants is an instance of class
        # HubSessions.participants.inPart.Participants.
        p = participants
        target = p.target

        # The status is customized: it varies depending on the user being a HS
        # manager or a voter.

        # The first status lines are for everyone
        part = not target.votesAreSecret and 'not_' or ''
        _  = target.translate
        votersCount = len(p.voters)
        map = {'nb': votersCount, 'type': _('vote_%ssecret' % part)}
        line = '<div>%s</div>'
        status = line % _('voters_count', mapping=map)
        r = [status]

        # The second line gives info about the number of votes
        map = None
        if not p.votesEncoded:
            label = 'not_encoded'
        else:
            voteCount = Vote.countOn(target)
            if voteCount == votersCount:
                label = 'complete'
            elif voteCount < votersCount:
                label = 'incomplete'
                map = {'votes': voteCount, 'missing': votersCount-voteCount}
            else:
                label = 'excess'
                map = {'votes': voteCount, 'excess': voteCount-votersCount}
        r.append(line % _('vote_%s' % label, mapping=map))

        # The third line is shown for the voter only
        if target.config.votes.individual:
            login = target.user.login 
            havingVoted = Vote.getDict(target, 'havingVoted',
                                       create=False, raiseError=False)
            if havingVoted and (login in havingVoted):
                hasVoted = havingVoted[login]
                label = havingVoted[login] and 'encoded' or 'to_encode'
                r.append(line % _('vote_%s' % label))
        # Return the complete result
        return '<div class="voteStatus">%s</div>' % '\n'.join(r)

    @classmethod
    def repairHavingVotedOn(class_, target, voters):
        '''Repairs dict "havingVoted" for this p_target. Logins of voters for
           this target are passed in p_voters. Returns True if the dict has been
           repaired.'''
        r = False
        d = Vote.getDict(target, 'havingVoted')
        # Add an entry for every voter that would be missing
        for login in voters:
            if login not in d:
                d[login] = False
                target.log(HV_ADDED % (target.id, login))
                r = True
        # Remove an entry for every voter that would be in excess
        for login in d:
            if login not in voters:
                del d[login]
                target.log(HV_REMOVED % (target.id, login))
                r = True
        return r

    @classmethod
    def closedOn(class_, meeting):
        '''Are votes closed on this p_meeting ?'''
        # Votes are considered to be closed when a meeting has been decided
        if meeting.state != 'frozen': return meeting.isDecided()
        return bool(meeting.history.getLast('closeVotes', notBefore='freeze'))

    @classmethod
    def viewableOn(class_, meeting):
        '''Are votes viewable on this p_meeting ?'''
        # View condition varies, depending on the config
        if meeting.config.votes.preview:
            view = Vote.closedOn(meeting)
        else:
            view = meeting.isDecided()
        if view: return True
        user = meeting.user
        return user.hasRole('Transcriber') or user.isHsManager(meeting)

    @classmethod
    def repairOn(class_, target, meeting, counts):
        '''See docstring of m_repairAllOn'''
        isItem = target.class_.name == 'Item'
        votes = target.votes
        if isItem:
            # Are votes required ? If yes, are votes encoded ?
            votesRequired = Vote.enabledOn(target, 'itemstrict',
                                           checkDate=False, meeting=meeting)
            if votesRequired and not votes:
                counts.no += 1
            elif not votesRequired and votes:
                # Votes are not required but are present. Remove them.
                Vote.removeOn(target)
                counts.deleted += 1
                return
            elif not votesRequired and not votes:
                return # Everything is fine
        else:
            # For an annex, simply check if votes are present
            votesRequired = True
            if not votes:
                counts.no += 1
        # Get voters
        item = isItem and target or target.container
        voters = {}
        for p in item.getAttendees(voter=True):
            voters[p.tiedUser.login] = None
        # Repair dict "havingVoted" when relevant
        repaired = False
        if votesRequired and target.config.votes.individual:
            repaired = Vote.repairHavingVotedOn(target, voters)
            if repaired:
                counts.repaired += 1
        # Stop here if there is no vote
        if not votes: return
        # Compute the number of votes and the number of voters
        nbOfVotes = target.getVotesCount()
        nbOfVoters = len(voters)
        if nbOfVotes > nbOfVoters:
            # Too much votes
            if target.votesAreSecret:
                target.log(SV_EXCESS % target.id)
                counts.warning += 1
            else:
                # Remove votes performed by people not being voters anymore
                removed = []
                for login in votes.keys():
                    if login in voters: continue
                    # Remove this vote value
                    removed.append(login)
                    del votes[login]
                target.log(V_REMOVED % (target.id, ', '.join(removed)),
                           type='warning')
                # Count a repaired vote, if it was not repaired yet
                if not repaired:
                    counts.repaired += 1
        elif nbOfVotes < nbOfVoters:
            target.log(V_MISSING % (target.id, nbOfVotes, nbOfVoters),
                       type='warning')
            counts.incomplete += 1

    @classmethod
    def getVotersFor(class_, target, voteValue, isItem=True):
        '''Gets the list of voters having voted p_voteValue on p_target'''
        if target.votesAreSecret:
            raise Exception(SECRET_KO % 'getVotersFor')
        r = []
        item = target if isItem else target.item
        for participant in item.getAttendees(voter=True):
            if target.getVoteValue(participant.tiedUser.login) == voteValue:
                r.append(participant)
        return r

    @classmethod
    def getPrint(class_, target, voteValues=defaultValues):
        '''Returns the "voteprint" for this p_target. A "voteprint" is a string
           integrating all votes with vote values in p_voteValues. Useful for
           grouping items having the same vote value.'''
        if target.votesAreSecret:
            raise Exception(SECRET_KO % 'getPrint')
        votes = target.votes
        if not votes: return ''
        voters = list(votes.keys())
        voters.sort()
        r = []
        for voter in voters:
            if votes[voter] in voteValues:
                # Reduce the vote value to a single letter
                value = votes[voter]
                if value == 'not_yet': v = 't'
                elif value == 'not_found': v = 'f'
                else: v = value[0]
                r.append('%s.%s' % (voter, v))
        return ''.join(r)

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #               Actions applying on all votes of some target
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    @classmethod
    def getItem(class_, target):
        '''Returns the item tied to this p_target'''
        return target if target.class_.name == 'Item' else target.item

    traverse['updateOn'] = 'perm:read'
    @classmethod
    def updateOn(class_, target):
        '''Called when the user saves votes from the table of participants'''
        req = target.req
        item = class_.getItem(target)
        meeting = item.meeting
        config = target.config.votes
        # Security check
        if not config.enabled or config.individual or \
           not item.user.mayEditAllVotes(meeting, item):
            target.raiseUnauthorized(target.translate('vote_unallowed'))
        # If votes are secret, we get vote counts. Else, we get vote values.
        secret = True # could be updated while scanning request elements
        requestVotes = {}
        numberOfVotes = 0
        numberOfVoters = len(item.getAttendees(voter=True))
        req.error = True # If everything OK, we'll set "False" in the end
        # If allYes is True, we must set vote value "yes" for every voter
        allYes = req.allYes == 'true'
        for key in req.keys():
            if key.startswith('vote_value_'):
                voterLogin = key[11:]
                requestVotes[voterLogin] = 'yes' if allYes else req[key]
                secret = False
            elif key.startswith('vote_count_'):
                voteValue = key[11:]
                # If allYes, we cheat
                if allYes:
                    if voteValue == 'yes':
                        v = numberOfVoters
                    else:
                        v = 0
                else:
                    # Check that the entered value is positive integer
                    inError = False
                    v = 0
                    try:
                        v = int(req[key])
                        if v < 0: inError = True
                    except ValueError:
                        inError = True
                    if inError:
                        return item.translate('invalid_vote_count')
                numberOfVotes += v
                requestVotes[voteValue] = v
        # Check that computed value "secret" does correspond to field
        # "votesAreSecret".
        if secret != target.votesAreSecret: raise Exception(VOTE_TYPE_MM)
        # Check the total number of votes
        if secret:
            if numberOfVotes != numberOfVoters:
                return item.translate('wrong_vote_count')
        # Save votes
        req.error = False
        votes = target.votes or PersistentMapping()
        for k, v in requestVotes.items(): votes[k] = v
        target.votes = votes
        prefix = '' if secret else 'non '
        suffix = ' (all yes)' if allYes else ''
        target.log(UPDATED % (target.id, prefix, suffix))
        # Return a different message depending on the fact that an individual
        # has voted for himself or a HS manager has encoded votes for everybody.
        return target.translate('votes_saved')

    traverse['switchOn'] = 'perm:read'
    @classmethod
    def switchOn(class_, target):
        '''Switch votes on p_target by updating field "votesAreSecret"'''
        # Security check
        item = class_.getItem(target)
        if not item.allows('Write item'): item.raiseUnauthorized()
        switchTo = eval(target.req.switchTo)
        currentMode = target.votesAreSecret
        if switchTo == currentMode: raise Exception(SW_VOTES_KO)
        target.votesAreSecret = switchTo
        # Reinitialise votes
        Vote.initialiseOn(target, setSecret=False)
        target.log(SWITCHED % (target.id, switchTo))
        return target.translate('object_saved')

    traverse['deleteOn'] = 'perm:read'
    @classmethod
    def deleteOn(class_, target):
        '''Deletes all votes defined on p_target'''
        # Security check
        item = class_.getItem(target)
        if not item.user.mayEditAllVotes(item.meeting, item):
            item.raiseUnauthorized()
        # Reinitialise votes
        Vote.initialiseOn(target)
        prefix = '' if target.votesAreSecret else 'non '
        target.log(DELETED % (target.id, prefix))
        return target.translate('object_saved')

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #                    Methods defined at the meeting level
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    @classmethod
    def getMeetingVoters(class_, meeting, result='participants',
                         participants=None):
        '''Returns the participants being voters in p_meeting'''

        # Note that even absents are returned. If p_participants are given, they
        # are used instead of p_meeting.participants. Indeed, a plug-in may
        # compute them in some order that may be slighly different from their
        # standard order in p_meeting.participants.
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        # If p_result is... | r_ is...
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        #  "participants"   | A list of Participant instances
        #     "users"       | A list of User instances
        #      "all"        | A list of tuples (participant, user) 
        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        r = []
        participants = participants or meeting.participants
        for participant in participants:
            if not participant.voter: continue
            user = participant.tiedUser
            if result == 'participants':
                r.append(participant)
            elif result == 'users':
                r.append(user)
            elif result == 'all':
                r.append( (participant, user) )
        return r

    @classmethod
    def showRepair(class_, meeting):
        '''When and where must we show the action for repairing votes ?'''
        if meeting.isEmpty('items') or not meeting.user.isHsManager(): return
        if Vote.enabledOn(meeting, 'meeting'):
            return 'buttons'

    @classmethod
    def repairAllOn(class_, meeting):
        '''Repairs (or tries to repair) incoherences detected between all
           votable elements found within this m_meeting and its voters.'''

        # If, after votes have been encoded, the user decides to perform changes
        # in the absents/presents, be it at the meeting or item level, or other
        # manipulations such as adding or removing a meeting participant, the
        # number of encoded votes may be higher than the number of voters. While
        # this should never occur, it does. So this method checks, on every
        # votable element of within p_meeting, if the number of votes
        # corresponds to the number of voters. Encoded votes that correspond to
        # users that were not voters anymore are removed, excepted if votes were
        # encoded by the voters themselves.

        # Votes can be found on items or on votable annexes. Within the
        # remaining of this method, the object onto which a vote may be found
        # will be called a "target".

        # Create an object storing details about the process
        counts = O(
          # "repaired" counts the number of votes having been repaired
          repaired = 0,
          # "warning" counts the number of targets that need to be repaired but
          # that could not be, because of secret and/or individual votes.
          warning = 0,
          # Count the number of targets for which votes are required but no vote
          # has been found.
          no = 0,
          # Count the number of targets for which votes, normally not required,
          # were nevertheless found.
          deleted = 0,
          # Count the number of targets for which votes are incomplete
          incomplete = 0
        )
        for item in meeting.items:
            # Get all the vote targets related to this item = the item itself or
            # its inner votable annexes.
            for target in Vote.getTargetsIn(item, checkRequired=False):
                Vote.repairOn(target, meeting, counts)
        # Display (a) message(s) to the user
        msg = None
        prefix = 'repair_votes'
        _ = meeting.translate
        for value in counts.d().values():
            if value > 0:
                msg = _('%s_ko' % prefix, mapping=counts.d())
                break
        return True, msg or _('%s_ok' % prefix)

    @classmethod
    def showClose(class_, meeting):
        '''Closing votes has sense when individual votes are enabled'''
        if not meeting.config.votes.individual or Vote.closedOn(meeting):
            return # Votes can't be closed twice
        return bool(class_.showRepair(meeting))

    @classmethod
    def closeAllOn(class_, meeting):
        '''Closing votes for a p_meeting means setting, for every missing vote
           on every vote target within p_meeting, value "has_not_voted".'''
        # As a preamble, repair all votes (may have no effect if no problem is
        # detected regarding votes within this p_meeting).
        ok, repairDetails = class_.repairAllOn(meeting)
        # This technique disables vote buttons for individuals.
        # ~
        # Collect some info while walking votable targets:
        # - the total number of found targets ;
        total = 0
        # - the total number of missing vote values, on all targets ;
        missing = 0
        # - the IDs of the targets onto which missing votes have been found ;
        targets = {}
        # - the logins of the voters for which at least one vote was missing.
        voters = {}
        # Browse all votable targets
        for item in meeting.items:
            for target in Vote.getTargetsIn(item):
                total += 1
                havingVoted = Vote.getDict(target, 'havingVoted')
                for login in havingVoted:
                    # Ignore voters that have voted
                    if havingVoted[login]: continue
                    # Manage this missing vote
                    Vote.storeValueOn(target, 'has_not_voted', login)
                    # Update counts
                    missing += 1
                    targets[target.id] = None
                    voters[login] = None
        # Log the operation and return a nice message to the UI
        _ = meeting.translate
        if total == 0: return _('votes_closed_no')
        voters = len(voters)
        targets = len(targets)
        meeting.log(V_CLOSED % (meeting.id, missing, targets, voters))
        if missing:
            part = 'missing'
            map = {'missing': missing, 'voters': voters, 'targets': targets}
        else:
            part = 'complete'
            map = None
        return '<div class="focus"><i>%s</i><br/>%s</div><div>%s</div>' % \
               (_('repair_votes_preamble'), repairDetails,
                _('votes_closed_%s' % part, mapping=map))

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

    # These PXs concern vote-related functions as incorporated in the table of
    # participants, as managed by HubSessions/participant/inPart.py, being shown
    # on a "votable" element, like an item or an annex.

    # Within these PX, "self" is an instance of class Participants as defined in
    # the hereabove-mentiond file.

    # ~~~ PX showing a vote (editable or not) for a given participant (used
    #     within the main Participants PX). Via this PX, it is not allowed to
    #     perform an individual vote. So, even if an individual is allowed to
    #     edit a vote, he won't get the editable version of the widget. Only a
    #     HS manager, if allowed to edit all votes, will get "edit" widgets.
    # ~~~

    pxSingle = Px('''
     <x var="doesVote=participant.voter and not absent;
             userLogin=tiedUser.login">
      <!-- Does this participant vote for this item ? -->
      <td align="center"><span if="doesVote">✔</span></td>
      <!-- Show the individual vote -->
      <td if="not secret" align="center">
       <x if="doesVote"
          var2="voteValue=self.target.getVoteValue(userLogin);
                mayConsultVote=self.mayConsultVotes or user.login == userLogin;
                mayEditVote=self.mayEditVotes">
        <!-- For users that can't consult neither edit the vote value -->
        <x if="not mayConsultVote and not mayEditVote">-</x>
        <!-- For users that can consult the vote value but not edit it -->
        <x if="mayConsultVote and not mayEditVote">
         <span if="voteValue"
               id=":'vote-%s'%voteValue">:_('vote_%s' % voteValue)</span>
         <x if="not voteValue">-</x>
        </x>
        <!-- For users that can consult and edit the vote -->
        <x if="mayConsultVote and mayEditVote"
           var2="widgetName='vote_value_%s' % userLogin;
                 voteValue=self.target.getVoteValue(userLogin, \
                                                    returnDefault=True)">
         <x for="aVoteValue in config.votes.values"
            var2="widgetId='vote_value_%s_%s' % (aVoteValue, userLogin)">
          <input type="radio" name=":widgetName" id=":widgetId"
                 value=":aVoteValue" checked=":voteValue == aVoteValue"/>
          <label lfor=":widgetId"
           id=":'vote-%s'% aVoteValue">:_('vote_%s' % aVoteValue)</label>
         </x>
        </x>
       </x>
      </td>
     </x>''')

    # ~~~ PX showing vote counts. If votes are secret and the user is
    #     authorized, those vote counts will be editable.
    # ~~~

    pxCounts = Px('''
     <td colspan="10" class="discreet odd">
      <div class="voteCounts">
       <x for="voteValue in config.votes.values"
          var2="voteCount=self.target.getVoteCount(voteValue)">
        <span var="id='vote-%s' % voteValue"
              id=":id">:_('vote_%s' % voteValue)</span> : 
        <!-- Vote counts, editable version -->
        <input if="self.mayEditCounts" type="text" size="2"
               var2="widgetName='vote_count_%s' % voteValue" name=":widgetName"
               value=":(req.widgetName or 0) if error else voteCount"/>
        <!-- Vote counts, read-only version -->
        <span if="self.mayConsultVotes and not self.mayEditCounts"
              style="margin-right: 8px">:voteCount</span>
        <!-- If the vote is not consultable -->
        <x if="not self.mayConsultVotes and not self.mayEditCounts">-</x>
       </x>
       <!-- Also show not-encoded votes, if any -->
       <x var="neVotes=self.target.getVoteCount('has_not_voted')"
          if="neVotes and self.mayConsultVotes">
        <x>:_('vote_have_not_voted')</x> : <x>:neVotes</x>
       </x>
      </div>
      <!-- Status lines -->
      <x>::Vote.statusOn(self)</x>
     </td>''')

    # ~~~ The series of vote-related buttons at the bottom of the table of
    #     participants.
    # ~~~

    pxButtons = Px('''
     <!-- Delete votes -->
     <img if="self.votesEncoded and self.mayEditVotes" class="clickable iconS"
          var2="jsCall='deleteVotes(%s)' % jsParams" title=":_('delete_votes')"
          onclick=":'askConfirm(%s,%s)' % (q('script'), q(jsCall, False))"
          src=":svg('deleteMany')"/>

     <!-- Switch votes -->
     <img if="self.showSwitch" class="clickable"
          src=":url('switch', base='HubSessions')" title=":_('switch_votes')"
          var2="jsCall='switchVotes(%s,%s)' % (jsParams, q(not secret));
                msg=_(self.switchConfirmLabel)"
          onclick=":'askConfirm(%s,%s,%s)' % \
                     (q('script'), q(jsCall, False), q(msg))"/>

     <!-- Save votes: all 'yes' -->
     <img if="self.showSave and config.votes.allYes"
          class="clickable" src=":url('saveYes', base='HubSessions')"
          title=":_('save_votes_all_yes')"
          var2="jsCall='saveVotes(%s, true)' % jsParams"
          onclick=":'askConfirm(%s,%s)' % (q('script'), q(jsCall, False))"/>

     <!-- Save votes -->
     <img if="self.showSave" class="clickable"
          src=":url('save', base='HubSessions')" title=":_('save_votes')"
          onclick=":'saveVotes(%s, false)' % jsParams"/>''',

     js='''
      function deleteVotes(hookId, objectUrl){
        let action = hookId.split('_')[0] + '*Vote*deleteOn';
        askField(hookId, objectUrl, 'view', {'action':action}, false);
      }

      function switchVotes(hookId, objectUrl, secret){
        let action = hookId.split('_')[0] + '*Vote*switchOn',
            params = {'action':action, 'switchTo': secret};
        askField(hookId, objectUrl, 'view', params, false);
      }

      function saveVotes(hookId, objectUrl, allYes) {
        // If "allYes" is true, all vote values must be set to "yes"
        let f = document.forms['participantsForm'],
            action = hookId.split('_')[0] + '*Vote*updateOn',
            // Collect params to send via the AJAX request
            params = {'action':action, 'allYes':allYes}, widget;
        for (let i=0; i<f.elements.length; i++) {
          widget = f.elements[i];
          if ((widget.type == "text") || widget.checked) {
            params[widget.name] = widget.value;
          }
        }
        askField(hookId, objectUrl, 'view', params, false);
      }''')
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
