Beispiel #1
0
    def migrate(self):
        self.op.create_table(
            'favorites',
            Column('id',
                   Integer,
                   Sequence("favorites_id_seq"),
                   primary_key=True),
            Column('admin_unit_id',
                   String(UNIT_ID_LENGTH),
                   index=True,
                   nullable=False),
            Column('int_id', Integer, index=True, nullable=False),
            Column('userid', String(USER_ID_LENGTH), index=True),
            Column('position', Integer, index=True),
            Column('title', String(CONTENT_TITLE_LENGTH), nullable=False),
            Column('is_title_personalized',
                   Boolean,
                   default=False,
                   nullable=False),
            Column('portal_type', String(PORTAL_TYPE_LENGTH)),
            Column('icon_class', String(CSS_CLASS_LENGTH)),
            Column('plone_uid', String(UID_LENGTH)),
            Column('created',
                   UTCDateTime(timezone=True),
                   default=utcnow_tz_aware),
            Column('modified',
                   UTCDateTime(timezone=True),
                   default=utcnow_tz_aware,
                   onupdate=utcnow_tz_aware))

        self.ensure_sequence_exists('favorites_id_seq')
Beispiel #2
0
class Favorite(Base):

    __tablename__ = 'favorites'

    favorite_id = Column('id',
                         Integer,
                         Sequence("favorites_id_seq"),
                         primary_key=True)

    admin_unit_id = Column(String(UNIT_ID_LENGTH), index=True, nullable=False)
    int_id = Column(Integer, index=True, nullable=False)
    oguid = composite(Oguid, admin_unit_id, int_id)

    userid = Column(String(USER_ID_LENGTH), index=True)
    position = Column(Integer, index=True)

    title = Column(String(CONTENT_TITLE_LENGTH), nullable=False)
    is_title_personalized = Column(Boolean, default=False, nullable=False)
    portal_type = Column(String(PORTAL_TYPE_LENGTH))
    icon_class = Column(String(CSS_CLASS_LENGTH))

    plone_uid = Column(String(UID_LENGTH))
    created = Column(UTCDateTime(timezone=True), default=utcnow_tz_aware)
    modified = Column(UTCDateTime(timezone=True),
                      default=utcnow_tz_aware,
                      onupdate=utcnow_tz_aware)
    def migrate(self):
        self.op.create_table(
            'digests',
            Column("id", Integer, Sequence('digest_id_seq'), primary_key=True),
            Column("userid", String(255), nullable=False),
            Column("last_dispatch", UTCDateTime(timezone=True)))

        self.ensure_sequence_exists('digest_id_seq')
Beispiel #4
0
class ArchiveMixin(object):

    @declared_attr
    def contact_id(self):
        return Column(Integer, ForeignKey('contacts.id'), nullable=False)

    actor_id = Column(String(USER_ID_LENGTH), nullable=False)
    created = Column(UTCDateTime(timezone=True), default=utcnow_tz_aware)
class Activity(Base, Translatable):

    __tablename__ = 'activities'
    __translatable__ = {
        'locales': SUPPORTED_LOCALES,
        'fallback_locale': DEFAULT_LOCALE
    }

    locale = DEFAULT_LOCALE

    id = Column('id', Integer, Sequence("activities_id_seq"), primary_key=True)
    kind = Column(String(255), nullable=False)
    actor_id = Column(String(USER_ID_LENGTH), nullable=False)
    created = Column(UTCDateTime(timezone=True), default=utcnow_tz_aware)
    resource_id = Column(Integer, ForeignKey('resources.id'), nullable=False)
    resource = relationship("Resource", backref="activities")

    def __repr__(self):
        return u'<Activity {} on {} >'.format(self.kind, repr(self.resource))

    def create_notifications(self):
        """Create a notification for every resource watcher.
        For the activity's actor, who has created the activity, a notification
        is usually unnecessary or disruptive. We therefore only create a
        notification for the activity's actor if he has enabled the
        notify_own_actions'setting.
        """

        notifications = []
        for userid in self.get_users_for_watchers():
            if (self.is_current_user(userid)
                    and not self.user_wants_own_action_notifications(userid)):
                continue

            notifications.append(Notification(userid=userid, activity=self))

        return notifications

    def get_users_for_watchers(self):
        users = []
        for watcher in self.resource.watchers:
            users += watcher.get_user_ids()

        return set(users)

    def get_notifications_for_watcher_roles(self, roles):
        """Returns a list of activities notifications, but only those
        where the watchers watch the resource in one of the given roles.
        """
        return Notification.query.by_subscription_roles(roles, self).all()

    def is_current_user(self, user_id):
        return user_id == self.actor_id

    def user_wants_own_action_notifications(self, userid):
        return UserSettings.get_setting_for_user(userid, 'notify_own_actions')
Beispiel #6
0
class Digest(Base):

    __tablename__ = 'digests'

    digest_id = Column('id',
                       Integer,
                       Sequence('digest_id_seq'),
                       primary_key=True)
    userid = Column(String(USER_ID_LENGTH), nullable=False)
    last_dispatch = Column(UTCDateTime(timezone=True))
Beispiel #7
0
class Lock(Base):

    __tablename__ = 'locks'
    __table_args__ = (UniqueConstraint('object_id', 'object_type',
                                       'lock_type'), {})

    lock_id = Column("id", Integer, Sequence("locks_id_seq"), primary_key=True)
    object_id = Column(Integer)
    object_type = Column(String(100), index=True)
    creator = Column(String(USER_ID_LENGTH), index=True)
    time = Column(UTCDateTime(timezone=True),
                  default=utcnow_tz_aware,
                  index=True)
    lock_type = Column(String(100))

    @property
    def token(self):
        return '{}:{}'.format(self.object_type, self.object_id)

    def is_valid(self):
        return self.time >= lowest_valid()
class ReminderSetting(Base):

    __tablename__ = 'reminder_settings'

    reminder_setting_id = Column('id',
                                 Integer,
                                 Sequence('reminder_setting_id_seq'),
                                 primary_key=True)

    task_id = Column(Integer, ForeignKey('tasks.id'), nullable=False)
    task = relationship("Task", backref="reminder_settings")

    actor_id = Column(String(USER_ID_LENGTH), index=True, nullable=False)

    option_type = Column(String(255), nullable=False)
    remind_day = Column(Date, nullable=False)
    created = Column(UTCDateTime(timezone=True), default=utcnow_tz_aware)

    def __repr__(self):
        return u'<ReminderSetting {} for {} for {} on {} >'.format(
            self.reminder_setting_id, self.actor_id, repr(self.task),
            self.remind_day)
Beispiel #9
0
class Favorite(Base):

    __tablename__ = 'favorites'
    __table_args__ = (
        UniqueConstraint('admin_unit_id', 'int_id', 'userid',
                         name='ix_favorites_unique'),
        {})

    favorite_id = Column('id', Integer, Sequence("favorites_id_seq"),
                primary_key=True)

    admin_unit_id = Column(String(UNIT_ID_LENGTH), index=True, nullable=False)
    int_id = Column(Integer, index=True, nullable=False)
    oguid = composite(Oguid, admin_unit_id, int_id)

    userid = Column(String(USER_ID_LENGTH), index=True)
    position = Column(Integer, index=True)

    title = Column(String(CONTENT_TITLE_LENGTH), nullable=False)
    is_title_personalized = Column(Boolean, default=False, nullable=False)
    portal_type = Column(String(PORTAL_TYPE_LENGTH))
    icon_class = Column(String(CSS_CLASS_LENGTH))

    plone_uid = Column(String(UID_LENGTH))
    created = Column(UTCDateTime(timezone=True), default=utcnow_tz_aware)
    modified = Column(UTCDateTime(timezone=True),
                      default=utcnow_tz_aware,
                      onupdate=utcnow_tz_aware)

    def serialize(self, portal_url):
        return {
            '@id': self.api_url(portal_url),
            'portal_type': self.portal_type,
            'favorite_id': self.favorite_id,
            'oguid': self.oguid.id,
            'uid': self.plone_uid,
            'title': self.title,
            'icon_class': self.icon_class,
            'target_url': self.get_target_url(),
            'tooltip_url': self.get_tooltip_url(),
            'position': self.position,
            'admin_unit': AdminUnit.query.get(self.admin_unit_id).title}

    def api_url(self, portal_url):
        return '{}/@favorites/{}/{}'.format(
            portal_url, self.userid, self.favorite_id)

    @property
    def tooltip_view(self):
        if is_bumblebeeable(self):
            return 'tooltip'

    def get_tooltip_url(self):
        url = self.get_target_url()
        if self.tooltip_view:
            return u'{}/{}'.format(url, self.tooltip_view)

        return None

    def get_target_url(self):
        admin_unit = AdminUnit.query.get(self.admin_unit_id)
        return u'{}/resolve_oguid/{}'.format(admin_unit.public_url, self.oguid)

    @staticmethod
    def truncate_title(title):
        return safe_unicode(title)[:CONTENT_TITLE_LENGTH]
Beispiel #10
0
class Meeting(Base, SQLFormSupport):

    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_HELD = State('held', title=_('held', default='Held'))
    STATE_CLOSED = State('closed', title=_('closed', default='Closed'))

    workflow = Workflow(
        [STATE_PENDING, STATE_HELD, STATE_CLOSED],
        [
            CloseTransition('pending',
                            'closed',
                            title=_('close_meeting', default='Close meeting')),
            Transition('pending',
                       'held',
                       title=_('hold', default='Hold meeting'),
                       visible=False),
            CloseTransition('held',
                            'closed',
                            title=_('close_meeting', default='Close meeting')),
            Transition('closed',
                       'held',
                       title=_('reopen', default='Reopen'),
                       condition=is_word_meeting_implementation_enabled)
        ],
        show_in_actions_menu=True,
        transition_controller=MeetingTransitionController,
    )

    __tablename__ = 'meetings'

    meeting_id = Column("id",
                        Integer,
                        Sequence("meeting_id_seq"),
                        primary_key=True)
    committee_id = Column(Integer, ForeignKey('committees.id'), nullable=False)
    committee = relationship("Committee", backref='meetings')
    location = Column(String(256))
    title = Column(UnicodeCoercingText)
    start = Column('start_datetime',
                   UTCDateTime(timezone=True),
                   nullable=False)
    end = Column('end_datetime', UTCDateTime(timezone=True))
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH),
                            nullable=False,
                            default=workflow.default_state.name)
    modified = Column(UTCDateTime(timezone=True),
                      nullable=False,
                      default=utcnow_tz_aware)
    meeting_number = Column(Integer)

    presidency = relationship(
        'Member', primaryjoin="Member.member_id==Meeting.presidency_id")
    presidency_id = Column(Integer, ForeignKey('members.id'))
    secretary = relationship(
        'Member', primaryjoin="Member.member_id==Meeting.secretary_id")
    secretary_id = Column(Integer, ForeignKey('members.id'))
    other_participants = Column(UnicodeCoercingText)
    participants = relationship('Member',
                                secondary=meeting_participants,
                                order_by='Member.lastname, Member.firstname',
                                backref='meetings')

    dossier_admin_unit_id = Column(String(UNIT_ID_LENGTH), nullable=False)
    dossier_int_id = Column(Integer, nullable=False)
    dossier_oguid = composite(Oguid, dossier_admin_unit_id, dossier_int_id)

    agenda_items = relationship("AgendaItem",
                                order_by='AgendaItem.sort_order',
                                backref='meeting')

    protocol_document_id = Column(Integer, ForeignKey('generateddocuments.id'))
    protocol_document = relationship(
        'GeneratedProtocol',
        uselist=False,
        backref=backref('meeting', uselist=False),
        primaryjoin=
        "GeneratedProtocol.document_id==Meeting.protocol_document_id")
    protocol_start_page_number = Column(Integer)

    agendaitem_list_document_id = Column(Integer,
                                         ForeignKey('generateddocuments.id'))
    agendaitem_list_document = relationship(
        'GeneratedAgendaItemList',
        uselist=False,
        backref=backref('meeting', uselist=False),
        primaryjoin=
        "GeneratedAgendaItemList.document_id==Meeting.agendaitem_list_document_id"
    )

    # define relationship here using a secondary table to keep
    # GeneratedDocument as simple as possible and avoid that it actively
    # knows about all its relationships
    excerpt_documents = relationship(
        'GeneratedExcerpt',
        secondary=meeting_excerpts,
    )

    def initialize_participants(self):
        """Set all active members of our committee as participants of
        this meeting.

        """
        self.participants = [
            membership.member
            for membership in Membership.query.for_meeting(self)
        ]

    def __repr__(self):
        return '<Meeting at "{}">'.format(self.start)

    def generate_meeting_number(self):
        """Generate meeting number for self.

        This method locks the current period of this meeting to protect its
        meeting_sequence_number against concurrent updates.

        """
        period = Period.query.get_current_for_update(self.committee)

        self.meeting_number = period.get_next_meeting_sequence_number()

    def generate_decision_numbers(self):
        """Generate decision numbers for each agenda item of this meeting.

        This method locks the current period of this meeting to protect its
        decision_sequence_number against concurrent updates.

        """
        period = Period.query.get_current_for_update(self.committee)

        for agenda_item in self.agenda_items:
            agenda_item.generate_decision_number(period)

    def update_protocol_document(self):
        """Update or create meeting's protocol."""
        from opengever.meeting.command import CreateGeneratedDocumentCommand
        from opengever.meeting.command import MergeDocxProtocolCommand
        from opengever.meeting.command import ProtocolOperations
        from opengever.meeting.command import UpdateGeneratedDocumentCommand

        if self.has_protocol_document(
        ) and not self.protocol_document.is_locked():
            # The protocol should never be changed when it is no longer locked:
            # the user probably has made changes manually.
            return

        operations = ProtocolOperations()

        if is_word_meeting_implementation_enabled():
            command = MergeDocxProtocolCommand(
                self.get_dossier(),
                self,
                operations,
                lock_document_after_creation=True)
        else:
            if self.has_protocol_document():
                command = UpdateGeneratedDocumentCommand(
                    self.protocol_document, self, operations)
            else:
                command = CreateGeneratedDocumentCommand(
                    self.get_dossier(),
                    self,
                    operations,
                    lock_document_after_creation=True)

        command.execute()

    def unlock_protocol_document(self):
        if not self.protocol_document:
            return

        self.protocol_document.unlock_document()

    def hold(self):
        if self.workflow_state == 'held':
            return

        self.generate_meeting_number()
        self.generate_decision_numbers()
        self.workflow_state = 'held'

    def close(self):
        """Closes a meeting means set the meeting in the closed state.

        - generate and set the meeting number
        - generate decision numbers for each agenda_item
        - close each agenda item (generates proposal excerpt and change workflow state)
        - update and unlock the protocol document
        """
        self.hold()
        for agenda_item in self.agenda_items:
            agenda_item.close()

        self.update_protocol_document()
        self.unlock_protocol_document()
        self.workflow_state = 'closed'

    @property
    def css_class(self):
        return 'contenttype-opengever-meeting-meeting'

    def is_editable(self):
        return self.get_state() in [self.STATE_PENDING, self.STATE_HELD]

    def is_agendalist_editable(self):
        return self.get_state() == self.STATE_PENDING

    def has_protocol_document(self):
        return self.protocol_document is not None

    def has_agendaitem_list_document(self):
        return self.agendaitem_list_document is not None

    @property
    def wrapper_id(self):
        return 'meeting-{}'.format(self.meeting_id)

    def _get_title(self, prefix):
        return u"{}-{}".format(translate(prefix, context=getRequest()),
                               self.get_title())

    def _get_filename(self, prefix):
        normalizer = getUtility(IIDNormalizer)
        return u"{}-{}.docx".format(translate(prefix, context=getRequest()),
                                    normalizer.normalize(self.get_title()))

    def get_protocol_title(self):
        return self._get_title(_("Protocol"))

    def get_excerpt_title(self):
        return self._get_title(_("Protocol Excerpt"))

    def get_agendaitem_list_title(self):
        return self._get_title(
            _(u'label_agendaitem_list', default=u'Agendaitem list'))

    def get_protocol_filename(self):
        return self._get_filename(_("Protocol"))

    def get_excerpt_filename(self):
        return self._get_filename(_("Protocol Excerpt"))

    def get_agendaitem_list_filename(self):
        return self._get_filename(
            _(u'label_agendaitem_list', default=u'Agendaitem list'))

    def get_protocol_template(self):
        return self.committee.get_protocol_template()

    def get_excerpt_template(self):
        return self.committee.get_excerpt_template()

    def get_agendaitem_list_template(self):
        return self.committee.get_agendaitem_list_template()

    @property
    def physical_path(self):
        return '/'.join((self.committee.physical_path, self.wrapper_id))

    def execute_transition(self, name):
        self.workflow.execute_transition(self, self, name)

    def can_execute_transition(self, name):
        return self.workflow.can_execute_transition(self, name)

    def get_state(self):
        return self.workflow.get_state(self.workflow_state)

    def update_model(self, data):
        """Manually set the modified timestamp when updating meetings."""
        super(Meeting, self).update_model(data)
        self.modified = utcnow_tz_aware()

    def get_title(self):
        return self.title

    def get_date(self):
        return api.portal.get_localized_time(datetime=self.start)

    def get_start(self):
        """Returns the start datetime in localized format.
        """
        return api.portal.get_localized_time(datetime=self.start,
                                             long_format=True)

    def get_end(self):
        """Returns the end datetime in localized format.
        """
        if self.end:
            return api.portal.get_localized_time(datetime=self.end,
                                                 long_format=True)

        return None

    def get_start_time(self):
        return self._get_localized_time(self.start)

    def get_end_time(self):
        if not self.end:
            return ''

        return self._get_localized_time(self.end)

    def _get_localized_time(self, date):
        if not date:
            return ''

        return api.portal.get_localized_time(datetime=date, time_only=True)

    def schedule_proposal(self, proposal):
        assert proposal.committee == self.committee

        proposal.schedule(self)
        self.reorder_agenda_items()

    def schedule_text(self, title, is_paragraph=False):
        self.agenda_items.append(
            AgendaItem(title=title, is_paragraph=is_paragraph))
        self.reorder_agenda_items()

    @require_word_meeting_feature
    def schedule_ad_hoc(self, title):
        committee = self.committee.resolve_committee()
        ad_hoc_template = committee.get_ad_hoc_template()
        if not ad_hoc_template:
            raise MissingAdHocTemplate

        meeting_dossier = self.get_dossier()
        if not api.user.get_current().checkPermission(
                'opengever.document: Add document', meeting_dossier):
            raise MissingMeetingDossierPermissions

        document_title = _(u'title_ad_hoc_document',
                           default=u'Ad hoc agenda item ${title}',
                           mapping={u'title': title})

        ad_hoc_document = CreateDocumentCommand(
            context=meeting_dossier,
            filename=ad_hoc_template.file.filename,
            data=ad_hoc_template.file.data,
            content_type=ad_hoc_template.file.contentType,
            title=translate(document_title, context=getRequest())).execute()
        agenda_item = AgendaItem(title=title,
                                 document=ad_hoc_document,
                                 is_paragraph=False)

        self.agenda_items.append(agenda_item)
        self.reorder_agenda_items()
        return agenda_item

    def _set_agenda_item_order(self, new_order):
        agenda_items_by_id = OrderedDict(
            (item.agenda_item_id, item) for item in self.agenda_items)
        agenda_items = []

        for agenda_item_id in new_order:
            agenda_item = agenda_items_by_id.pop(agenda_item_id, None)
            if agenda_item:
                agenda_items.append(agenda_item)
        agenda_items.extend(agenda_items_by_id.values())
        self.agenda_items = agenda_items

    def reorder_agenda_items(self, new_order=None):
        if new_order:
            self._set_agenda_item_order(new_order)

        sort_order = 1
        number = 1
        for agenda_item in self.agenda_items:
            agenda_item.sort_order = sort_order
            sort_order += 1
            if not agenda_item.is_paragraph:
                agenda_item.number = '{}.'.format(number)
                number += 1

    def get_submitted_link(self):
        return self._get_link(self.get_submitted_admin_unit(),
                              self.submitted_physical_path)

    def get_link(self):
        url = self.get_url()
        link = u'<a href="{0}" title="{1}" class="{2}">{1}</a>'.format(
            url, escape_html(self.get_title()), self.css_class)
        return link

    def get_url(self, context=None, view='view'):
        elements = [
            self.committee.get_admin_unit().public_url, self.physical_path
        ]
        if view:
            elements.append(view)

        return '/'.join(elements)

    def get_dossier_url(self):
        return self.dossier_oguid.get_url()

    def get_dossier(self):
        return self.dossier_oguid.resolve_object()
Beispiel #11
0
class Meeting(Base, SQLFormSupport):

    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_HELD = State('held', title=_('held', default='Held'))
    STATE_CLOSED = State('closed', title=_('closed', default='Closed'))
    STATE_CANCELLED = State('cancelled',
                            title=_('cancelled', default='Cancelled'))

    workflow = Workflow(
        [STATE_PENDING, STATE_HELD, STATE_CLOSED, STATE_CANCELLED],
        [
            CloseTransition('pending',
                            'closed',
                            title=_('close_meeting', default='Close meeting')),
            Transition('pending',
                       'held',
                       title=_('hold', default='Hold meeting'),
                       visible=False),
            CloseTransition('held',
                            'closed',
                            title=_('close_meeting', default='Close meeting')),
            Transition('closed', 'held', title=_('reopen', default='Reopen')),
            CancelTransition(
                'pending', 'cancelled', title=_('cancel', default='Cancel')),
        ],
        show_in_actions_menu=True,
        transition_controller=MeetingTransitionController,
    )

    __tablename__ = 'meetings'

    meeting_id = Column("id",
                        Integer,
                        Sequence("meeting_id_seq"),
                        primary_key=True)
    committee_id = Column(Integer, ForeignKey('committees.id'), nullable=False)
    committee = relationship("Committee", backref='meetings')
    location = Column(String(256))
    title = Column(UnicodeCoercingText)
    start = Column('start_datetime',
                   UTCDateTime(timezone=True),
                   nullable=False)
    end = Column('end_datetime', UTCDateTime(timezone=True))
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH),
                            nullable=False,
                            default=workflow.default_state.name)
    modified = Column(UTCDateTime(timezone=True),
                      nullable=False,
                      default=utcnow_tz_aware)
    meeting_number = Column(Integer)

    presidency = relationship(
        'Member', primaryjoin="Member.member_id==Meeting.presidency_id")
    presidency_id = Column(Integer, ForeignKey('members.id'))
    secretary_id = Column(String(USER_ID_LENGTH), ForeignKey(User.userid))
    secretary = relationship(User, primaryjoin=User.userid == secretary_id)
    other_participants = Column(UnicodeCoercingText)
    participants = relationship('Member',
                                secondary=meeting_participants,
                                order_by='Member.lastname, Member.firstname',
                                backref='meetings')

    dossier_admin_unit_id = Column(String(UNIT_ID_LENGTH), nullable=False)
    dossier_int_id = Column(Integer, nullable=False)
    dossier_oguid = composite(Oguid, dossier_admin_unit_id, dossier_int_id)

    agenda_items = relationship("AgendaItem",
                                order_by='AgendaItem.sort_order',
                                backref='meeting')

    protocol_document_id = Column(Integer, ForeignKey('generateddocuments.id'))
    protocol_document = relationship(
        'GeneratedProtocol',
        uselist=False,
        backref=backref('meeting', uselist=False),
        primaryjoin=
        "GeneratedProtocol.document_id==Meeting.protocol_document_id")
    protocol_start_page_number = Column(Integer)

    agendaitem_list_document_id = Column(Integer,
                                         ForeignKey('generateddocuments.id'))
    agendaitem_list_document = relationship(
        'GeneratedAgendaItemList',
        uselist=False,
        backref=backref('meeting', uselist=False),
        primaryjoin=
        "GeneratedAgendaItemList.document_id==Meeting.agendaitem_list_document_id"
    )

    def was_protocol_manually_edited(self):
        """checks whether the protocol has been manually edited or not"""
        if not self.has_protocol_document():
            return False
        document = self.protocol_document.resolve_document()
        return not self.protocol_document.is_up_to_date(document)

    def get_other_participants_list(self):
        if self.other_participants is not None:
            return filter(
                len,
                map(lambda value: value.strip(),
                    self.other_participants.split('\n')))
        else:
            return []

    def initialize_participants(self):
        """Set all active members of our committee as participants of
        this meeting.

        """
        self.participants = [
            membership.member
            for membership in Membership.query.for_meeting(self)
        ]

    @property
    def absentees(self):
        return [
            membership.member
            for membership in Membership.query.for_meeting(self)
            if membership.member not in set(self.participants)
        ]

    def __repr__(self):
        return '<Meeting at "{}">'.format(self.start)

    def generate_meeting_number(self):
        """Generate meeting number for self.

        This method locks the current period of this meeting to protect its
        meeting_sequence_number against concurrent updates.

        """
        period = Period.query.get_current_for_update(self.committee)

        self.meeting_number = period.get_next_meeting_sequence_number()

    def get_meeting_number(self):
        # Before the meeting is held, it will not have a meeting number.
        # In that case we do not want to format it with the period title prefixed
        if not self.meeting_number:
            return None

        period = Period.query.get_for_meeting(self)
        if not period:
            return str(self.meeting_number)

        title = period.title
        return '{} / {}'.format(title, self.meeting_number)

    def generate_decision_numbers(self):
        """Generate decision numbers for each agenda item of this meeting.

        This method locks the current period of this meeting to protect its
        decision_sequence_number against concurrent updates.

        """
        period = Period.query.get_current_for_update(self.committee)

        for agenda_item in self.agenda_items:
            agenda_item.generate_decision_number(period)

    def update_protocol_document(self, overwrite=False):
        """Update or create meeting's protocol."""
        from opengever.meeting.command import MergeDocxProtocolCommand
        from opengever.meeting.command import ProtocolOperations

        operations = ProtocolOperations()
        command = MergeDocxProtocolCommand(self.get_dossier(), self,
                                           operations)
        command.execute(overwrite=overwrite)

        return command

    def hold(self):
        if self.workflow_state == 'held':
            return

        self.generate_meeting_number()
        self.generate_decision_numbers()
        self.workflow_state = 'held'

    def close(self):
        """Closes a meeting means set the meeting in the closed state.

        - generate and set the meeting number
        - generate decision numbers for each agenda_item
        - close each agenda item (generates proposal excerpt and change workflow state)
        - generate or update the protocol if necessary
        """
        self.hold()
        assert not self.get_undecided_agenda_items(), \
            'All agenda items must be decided before a meeting is closed.'

        try:
            self.update_protocol_document()
        except SablonProcessingFailed:
            msg = _(u'Error while processing Sablon template')
            api.portal.show_message(msg,
                                    api.portal.get().REQUEST,
                                    type='error')
            return False

        self.workflow_state = 'closed'
        return True

    @property
    def css_class(self):
        return 'contenttype-opengever-meeting-meeting'

    def is_editable(self):
        committee = self.committee.resolve_committee()
        if not api.user.has_permission('Modify portal content', obj=committee):
            return False

        return self.is_active()

    def is_agendalist_editable(self):
        if not self.is_editable():
            return False
        return self.is_pending()

    def is_pending(self):
        return self.get_state() == self.STATE_PENDING

    def is_active(self):
        return self.get_state() in [self.STATE_HELD, self.STATE_PENDING]

    def is_closed(self):
        return self.get_state() == self.STATE_CLOSED

    def has_protocol_document(self):
        return self.protocol_document is not None

    def has_agendaitem_list_document(self):
        return self.agendaitem_list_document is not None

    @property
    def wrapper_id(self):
        return 'meeting-{}'.format(self.meeting_id)

    def _get_title(self, prefix):
        return u"{}-{}".format(translate(prefix, context=getRequest()),
                               self.get_title())

    def _get_filename(self, prefix):
        normalizer = getUtility(IFileNameNormalizer,
                                name='gever_filename_normalizer')
        return u"{}-{}.docx".format(translate(prefix, context=getRequest()),
                                    normalizer.normalize(self.get_title()))

    def get_protocol_title(self):
        return self._get_title(_("Protocol"))

    def get_excerpt_title(self):
        return self._get_title(_("Protocol Excerpt"))

    def get_agendaitem_list_title(self):
        return self._get_title(
            _(u'label_agendaitem_list', default=u'Agendaitem list'))

    def get_protocol_filename(self):
        return self._get_filename(_("Protocol"))

    def get_excerpt_filename(self):
        return self._get_filename(_("Protocol Excerpt"))

    def get_agendaitem_list_filename(self):
        return self._get_filename(
            _(u'label_agendaitem_list', default=u'Agendaitem list'))

    def get_protocol_header_template(self):
        return self.committee.get_protocol_header_template()

    def get_protocol_suffix_template(self):
        return self.committee.get_protocol_suffix_template()

    def get_agenda_item_header_template(self):
        return self.committee.get_agenda_item_header_template()

    def get_agenda_item_suffix_template(self):
        return self.committee.get_agenda_item_suffix_template()

    def get_agendaitem_list_template(self):
        return self.committee.get_agendaitem_list_template()

    @property
    def physical_path(self):
        return '/'.join((self.committee.physical_path, self.wrapper_id))

    def execute_transition(self, name):
        self.workflow.execute_transition(self, self, name)

    def can_execute_transition(self, name):
        return self.workflow.can_execute_transition(self, name)

    def get_state(self):
        return self.workflow.get_state(self.workflow_state)

    def update_model(self, data):
        """Manually set the modified timestamp when updating meetings."""

        super(Meeting, self).update_model(data)
        self.modified = utcnow_tz_aware()

        meeting_dossier = self.get_dossier()
        title = data.get('title')
        if meeting_dossier and title:
            meeting_dossier.title = title
            meeting_dossier.reindexObject()

    def get_title(self):
        return self.title

    def get_date(self):
        return api.portal.get_localized_time(datetime=self.start)

    def get_start(self):
        """Returns the start datetime in localized format.
        """
        return api.portal.get_localized_time(datetime=self.start,
                                             long_format=True)

    def get_end(self):
        """Returns the end datetime in localized format.
        """
        if self.end:
            return api.portal.get_localized_time(datetime=self.end,
                                                 long_format=True)

        return None

    def get_start_time(self):
        return self._get_localized_time(self.start)

    def get_end_time(self):
        if not self.end:
            return None

        return self._get_localized_time(self.end)

    def get_undecided_agenda_items(self):
        """Return a filtered list of this meetings agenda items,
        containing only the items which are not in a "decided" workflow state.
        """
        def is_not_paragraph(agenda_item):
            return not agenda_item.is_paragraph

        def is_not_decided(agenda_item):
            return not agenda_item.is_completed()

        return filter(is_not_decided,
                      filter(is_not_paragraph, self.agenda_items))

    def _get_localized_time(self, date):
        if not date:
            return None

        return api.portal.get_localized_time(datetime=date, time_only=True)

    def schedule_proposal(self, proposal):
        assert proposal.committee == self.committee

        proposal.schedule(self)

    def schedule_text(self, title, is_paragraph=False, description=None):
        self.agenda_items.append(
            AgendaItem(title=title,
                       description=description,
                       is_paragraph=is_paragraph))
        self.reorder_agenda_items()

    def schedule_ad_hoc(self, title, template_id=None, description=None):
        committee = self.committee.resolve_committee()

        if template_id is None:
            ad_hoc_template = committee.get_ad_hoc_template()
        else:
            from opengever.meeting.vocabulary import ProposalTemplatesForCommitteeVocabulary
            vocabulary_factory = ProposalTemplatesForCommitteeVocabulary()
            vocabulary = vocabulary_factory(committee)
            templates = [
                term.value for term in vocabulary
                if term.value.getId() == template_id
            ]
            assert 1 == len(templates)
            ad_hoc_template = templates[0]

        if not ad_hoc_template:
            raise MissingAdHocTemplate

        meeting_dossier = self.get_dossier()
        if not api.user.get_current().checkPermission(
                'opengever.document: Add document', meeting_dossier):
            raise MissingMeetingDossierPermissions

        ad_hoc_document = CreateDocumentCommand(
            context=meeting_dossier,
            filename=ad_hoc_template.file.filename,
            data=ad_hoc_template.file.data,
            content_type=ad_hoc_template.file.contentType,
            title=title).execute()
        agenda_item = AgendaItem(title=title,
                                 description=description,
                                 document=ad_hoc_document,
                                 is_paragraph=False)

        self.agenda_items.append(agenda_item)
        self.reorder_agenda_items()
        return agenda_item

    def _set_agenda_item_order(self, new_order):
        agenda_items_by_id = OrderedDict(
            (item.agenda_item_id, item) for item in self.agenda_items)
        agenda_items = []

        for agenda_item_id in new_order:
            agenda_item = agenda_items_by_id.pop(agenda_item_id, None)
            if agenda_item:
                agenda_items.append(agenda_item)
        agenda_items.extend(agenda_items_by_id.values())
        self.agenda_items = agenda_items

    def reorder_agenda_items(self, new_order=None):
        if new_order:
            self._set_agenda_item_order(new_order)

        sort_order = 1
        number = 1
        for agenda_item in self.agenda_items:
            agenda_item.sort_order = sort_order
            sort_order += 1
            if not agenda_item.is_paragraph:
                agenda_item.item_number = number
                number += 1

    def get_submitted_link(self):
        return self._get_link(self.get_submitted_admin_unit(),
                              self.submitted_physical_path)

    def get_link(self):
        url = self.get_url()
        if api.user.has_permission('View',
                                   obj=self.committee.resolve_committee()):
            link = u'<a href="{0}" title="{1}" class="{2}">{1}</a>'.format(
                url, escape_html(self.get_title()), self.css_class)
        else:
            link = u'<span title="{0}" class="{1}">{0}</a>'.format(
                escape_html(self.get_title()), self.css_class)
        return link

    def get_url(self, context=None, view='view'):
        elements = [
            self.committee.get_admin_unit().public_url, self.physical_path
        ]
        if view:
            elements.append(view)

        return '/'.join(elements)

    def get_dossier_url(self):
        return self.dossier_oguid.get_url()

    def get_dossier(self):
        return self.dossier_oguid.resolve_object()