def setUp(self):
        self.private = State('private', is_default=True)
        self.pending = State('pending')
        self.published = State('published')

        self.submit = Transition('private', 'pending')
        self.publish = Transition('pending', 'published')
        self.reject = Transition('pending', 'private')
        self.retract = Retract()

        self.workflow = Workflow([
            self.private, self.pending, self.published
            ], [
            self.submit, self.publish, self.reject, self.retract
        ])
Exemple #2
0
    def test_state_equality(self):
        self.assertEqual(self.private, self.private)
        self.assertEqual(self.private, State('private'))

        self.assertNotEqual(self.private, self.pending)
        self.assertNotEqual(self.pending, None)
        self.assertNotEqual(self.pending, object())
Exemple #3
0
    def setUp(self):
        self.private = State('private', is_default=True)
        self.pending = State('pending')
        self.published = State('published')

        self.submit = Transition('private', 'pending')
        self.publish = Transition('pending', 'published')
        self.reject = Transition('pending', 'private')
        self.retract = Transition('published', 'pending')

        self.workflow = Workflow(
            [self.private, self.pending, self.published],
            [self.submit, self.publish, self.reject, self.retract])
Exemple #4
0
class TestUnitWorkflow(TestCase):
    def setUp(self):
        self.private = State('private', is_default=True)
        self.pending = State('pending')
        self.published = State('published')

        self.submit = Transition('private', 'pending')
        self.publish = Transition('pending', 'published')
        self.reject = Transition('pending', 'private')
        self.retract = Transition('published', 'pending')

        self.workflow = Workflow(
            [self.private, self.pending, self.published],
            [self.submit, self.publish, self.reject, self.retract])

    def test_transition_string_representation(self):
        self.assertEqual('<Transition "private-pending">', str(self.submit))
        self.assertEqual('<Transition "private-pending">', repr(self.submit))

    def test_state_string_representation(self):
        self.assertEqual('<State "pending">', str(self.pending))
        self.assertEqual('<State "pending">', repr(self.pending))

    def test_state_equality(self):
        self.assertEqual(self.private, self.private)
        self.assertEqual(self.private, State('private'))

        self.assertNotEqual(self.private, self.pending)
        self.assertNotEqual(self.pending, None)
        self.assertNotEqual(self.pending, object())

    def test_default_workflow_is_set(self):
        self.assertEqual(self.private, self.workflow.default_state)

    def test_fails_without_default_workflow(self):
        with self.assertRaises(AssertionError):
            Workflow([self.pending], [])

    def test_fails_with_duplicate_state(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.private], [])

    def test_fails_with_duplicate_transition(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.pending], [self.submit, self.submit])

    def test_fails_with_invalid_transition(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.pending],
                     [Transition('private', 'invalid_identifier')])

    def test_get_state_returns_correct_state(self):
        self.assertEqual(self.private, self.workflow.get_state('private'))

    def test_get_state_fails_with_invalid_state(self):
        with self.assertRaises(KeyError):
            self.workflow.get_state('invalid_identifier')

    def test_can_execute_available_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertTrue(
            self.workflow.can_execute_transition(obj, self.submit.name))

    def test_cannot_perform_unavailable_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertFalse(
            self.workflow.can_execute_transition(obj, self.retract.name))

    def test_cannot_perform_invalid_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertFalse(
            self.workflow.can_execute_transition(obj, 'invalid_name'))

    def test_performs_available_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        self.workflow.execute_transition(None, obj, self.publish.name)

        self.assertEqual(self.published.name, obj.workflow_state)

    def test_does_not_perform_unavailable_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        with self.assertRaises(AssertionError):
            self.workflow.execute_transition(None, obj, self.submit.name)

    def test_does_not_perform_invalid_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.published.name)
        with self.assertRaises(AssertionError):
            self.workflow.execute_transition(None, obj, 'invalid_identifier')

    def test_transitions_are_registered_with_their_state(self):
        self.assertEqual([self.submit], self.private.get_transitions())
        self.assertEqual([self.publish, self.reject],
                         self.pending.get_transitions())
        self.assertEqual([self.retract], self.published.get_transitions())
Exemple #5
0
class AgendaItem(Base):
    """An item must either have a reference to a proposal or a title.

    """

    __tablename__ = 'agendaitems'

    # workflow definition
    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_DECIDED = State('decided', title=_('decided', default='Decided'))
    STATE_REVISION = State('revision', title=_('revision', default='Revision'))

    workflow = Workflow([STATE_PENDING, STATE_DECIDED, STATE_REVISION], [
        Transition('pending', 'decided', title=_('decide', default='Decide')),
        Transition('decided', 'revision', title=_('reopen', default='Reopen')),
        Transition('revision', 'decided', title=_('revise', default='Revise')),
    ])

    agenda_item_id = Column("id",
                            Integer,
                            Sequence("agendaitems_id_seq"),
                            primary_key=True)
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH),
                            nullable=False,
                            default=workflow.default_state.name)
    proposal_id = Column(Integer, ForeignKey('proposals.id'))
    proposal = relationship("Proposal",
                            uselist=False,
                            backref=backref('agenda_item', uselist=False))

    ad_hoc_document_admin_unit_id = Column(String(UNIT_ID_LENGTH))
    ad_hoc_document_int_id = Column(Integer)
    ad_hoc_document_oguid = composite(Oguid, ad_hoc_document_admin_unit_id,
                                      ad_hoc_document_int_id)

    decision_number = Column(Integer)

    title = Column(UnicodeCoercingText)
    number = Column('item_number', String(16))
    is_paragraph = Column(Boolean, nullable=False, default=False)
    sort_order = Column(Integer, nullable=False, default=0)

    meeting_id = Column(Integer, ForeignKey('meetings.id'), nullable=False)

    discussion = Column(UnicodeCoercingText)
    decision = Column(UnicodeCoercingText)

    def __init__(self, *args, **kwargs):
        """Prefill the decision attributes with proposal's decision_draft.
        """
        proposal = kwargs.get('proposal')
        if proposal and not kwargs.get('decision'):
            submitted_proposal = proposal.resolve_submitted_proposal()
            decision_draft = submitted_proposal.decision_draft
            kwargs.update({'decision': decision_draft})

        document = kwargs.pop('document', None)
        if document:
            assert not proposal, 'must only have one of proposal and document'
            kwargs.update(
                {'ad_hoc_document_oguid': Oguid.for_object(document)})

        super(AgendaItem, self).__init__(*args, **kwargs)

    def update(self, request):
        """Update with changed data."""

        data = request.get(self.name)
        if not data:
            return

        def to_safe_html(markup):
            # keep empty data (whatever it is), it makes transform unhappy
            if not markup:
                return markup

            markup = markup.decode('utf-8')
            markup = trix2sablon.convert(markup)
            return trix_strip_whitespace(markup)

        if self.has_proposal:
            self.submitted_proposal.legal_basis = to_safe_html(
                data.get('legal_basis'))
            self.submitted_proposal.initial_position = to_safe_html(
                data.get('initial_position'))
            self.submitted_proposal.considerations = to_safe_html(
                data.get('considerations'))
            self.submitted_proposal.proposed_action = to_safe_html(
                data.get('proposed_action'))
            self.submitted_proposal.publish_in = to_safe_html(
                data.get('publish_in'))
            self.submitted_proposal.disclose_to = to_safe_html(
                data.get('disclose_to'))
            self.submitted_proposal.copy_for_attention = to_safe_html(
                data.get('copy_for_attention'))

        self.discussion = to_safe_html(data.get('discussion'))
        self.decision = to_safe_html(data.get('decision'))

    def get_field_data(self,
                       include_initial_position=True,
                       include_legal_basis=True,
                       include_considerations=True,
                       include_proposed_action=True,
                       include_discussion=True,
                       include_decision=True,
                       include_publish_in=True,
                       include_disclose_to=True,
                       include_copy_for_attention=True):
        data = {
            'number': self.number,
            'description': self.description,
            'title': self.get_title(),
            'dossier_reference_number': self.get_dossier_reference_number(),
            'repository_folder_title': self.get_repository_folder_title(),
            'is_paragraph': self.is_paragraph,
            'decision_number': self.decision_number,
            'html:decision_draft':
            self._sanitize_text(self.get_decision_draft())
        }

        if include_initial_position:
            data['html:initial_position'] = self._sanitize_text(
                self.initial_position)
        if include_legal_basis:
            data['html:legal_basis'] = self._sanitize_text(self.legal_basis)
        if include_considerations:
            data['html:considerations'] = self._sanitize_text(
                self.considerations)
        if include_proposed_action:
            data['html:proposed_action'] = self._sanitize_text(
                self.proposed_action)
        if include_discussion:
            data['html:discussion'] = self._sanitize_text(self.discussion)
        if include_decision:
            data['html:decision'] = self._sanitize_text(self.decision)
        if include_publish_in:
            data['html:publish_in'] = self._sanitize_text(self.publish_in)
        if include_disclose_to:
            data['html:disclose_to'] = self._sanitize_text(self.disclose_to)
        if include_copy_for_attention:
            data['html:copy_for_attention'] = self._sanitize_text(
                self.copy_for_attention)

        self._add_attachment_data(data)
        return data

    def _add_attachment_data(self, data):
        if not self.has_proposal:
            return

        documents = self.proposal.resolve_submitted_documents()
        if not documents:
            return

        attachment_data = []
        for document in documents:
            attachment = {'title': document.title}
            filename = document.get_filename()
            if filename:
                attachment['filename'] = filename
            attachment_data.append(attachment)
        data['attachments'] = attachment_data

    def _sanitize_text(self, text):
        if not text:
            return None

        return text

    @property
    def submitted_proposal(self):
        if not hasattr(self, '_submitted_proposal'):
            self._submitted_proposal = self.proposal.resolve_submitted_proposal(
            )  # noqa
        return self._submitted_proposal

    def get_title(self, include_number=False):
        title = (self.submitted_proposal.title
                 if self.has_proposal else self.title)
        if include_number and self.number:
            title = u"{} {}".format(self.number, title)

        return title

    def set_title(self, title):
        if self.has_proposal:
            self.submitted_proposal.title = title
            self.submitted_proposal.sync_model()
        else:
            self.title = title

    def get_decision_draft(self):
        if self.has_proposal:
            return self.submitted_proposal.decision_draft

    def get_decision_number(self):
        if not is_word_meeting_implementation_enabled():
            return self.decision_number

        if not self.decision_number:
            return self.decision_number

        period = Period.query.get_current_for_update(self.meeting.committee)
        year = period.date_from.year
        return '{} / {}'.format(year, self.decision_number)

    def get_dossier_reference_number(self):
        if self.has_proposal:
            return self.proposal.dossier_reference_number
        return None

    def get_excerpt_header_template(self):
        return self.meeting.committee.get_excerpt_header_template()

    def get_excerpt_suffix_template(self):
        return self.meeting.committee.get_excerpt_suffix_template()

    def get_repository_folder_title(self):
        if self.has_proposal:
            return self.proposal.repository_folder_title
        return None

    def get_css_class(self):
        css_classes = []
        if self.is_paragraph:
            css_classes.append("paragraph")
        if self.has_submitted_documents():
            css_classes.append("expandable")
        if self.has_proposal:
            css_classes.append("proposal")
        return " ".join(css_classes)

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

    def generate_decision_number(self, period):
        if self.is_paragraph:
            return

        next_decision_number = period.get_next_decision_sequence_number()
        self.decision_number = next_decision_number

    def remove(self):
        assert self.meeting.is_editable()

        # the agenda_item is ad hoc if it has a document but no proposal
        if self.has_document and not self.has_proposal:
            document = self.resolve_document()
            trasher = ITrashable(document)
            trasher.trash()

        session = create_session()
        if self.proposal:
            self.proposal.remove_scheduled(self.meeting)
        session.delete(self)
        self.meeting.reorder_agenda_items()

    def get_document_filename_for_zip(self, document):
        return normalize_path(u'{} {}/{}{}'.format(
            self.number, safe_unicode(self.get_title()),
            safe_unicode(document.Title()),
            os.path.splitext(document.file.filename)[1]))

    def get_proposal_link(self, include_icon=True):
        if not self.has_proposal:
            return self.get_title()

        return self.proposal.get_submitted_link(include_icon=include_icon)

    @require_word_meeting_feature
    def get_data_for_zip_export(self):
        agenda_item_data = {
            'title': safe_unicode(self.get_title()),
        }

        if self.has_document:
            document = self.resolve_document()
            agenda_item_data.update({
                'number': self.number,
                'proposal': {
                    'checksum': (IBumblebeeDocument(document).get_checksum()),
                    'file':
                    self.get_document_filename_for_zip(document),
                    'modified':
                    safe_unicode(get_localzone().localize(
                        document.modified().asdatetime().replace(
                            tzinfo=None)).isoformat()),
                }
            })

        if self.has_submitted_documents():
            agenda_item_data.update({
                'attachments': [{
                    'checksum': (IBumblebeeDocument(document).get_checksum()),
                    'file':
                    self.get_document_filename_for_zip(document),
                    'modified':
                    safe_unicode(get_localzone().localize(
                        document.modified().asdatetime().replace(
                            tzinfo=None)).isoformat()),
                    'title':
                    safe_unicode(document.Title()),
                } for document in self.proposal.resolve_submitted_documents()],
            })

        return agenda_item_data

    def serialize(self):
        return {
            'id': self.agenda_item_id,
            'css_class': self.get_css_class(),
            'title': self.get_title(),
            'number': self.number,
            'has_proposal': self.has_proposal,
            'link': self.get_proposal_link(include_icon=False),
        }

    @property
    def has_proposal(self):
        return self.proposal is not None

    @property
    def has_document(self):
        return self.has_proposal or self.ad_hoc_document_int_id is not None

    def resolve_document(self):
        if not self.has_document:
            return None

        if self.has_proposal:
            proposal = self.proposal.resolve_submitted_proposal()
            return proposal.get_proposal_document()

        return self.ad_hoc_document_oguid.resolve_object()

    @require_word_meeting_feature
    def checkin_document(self):
        document = self.resolve_document()
        if not document:
            return

        checkout_manager = getMultiAdapter((document, document.REQUEST),
                                           ICheckinCheckoutManager)
        checkout_manager.checkin()

    @property
    def legal_basis(self):
        return self.submitted_proposal.legal_basis if self.has_proposal else None

    @property
    def initial_position(self):
        return self.submitted_proposal.initial_position if self.has_proposal else None

    @property
    def considerations(self):
        return self.submitted_proposal.considerations if self.has_proposal else None

    @property
    def proposed_action(self):
        return self.submitted_proposal.proposed_action if self.has_proposal else None

    @property
    def publish_in(self):
        return self.submitted_proposal.publish_in if self.has_proposal else None

    @property
    def disclose_to(self):
        return self.submitted_proposal.disclose_to if self.has_proposal else None

    @property
    def copy_for_attention(self):
        return self.submitted_proposal.copy_for_attention if self.has_proposal else None

    @property
    def name(self):
        """Currently used as name for input tags in html."""

        return "agenda_item-{}".format(self.agenda_item_id)

    @property
    def description(self):
        return self.get_title()

    def has_submitted_documents(self):
        return self.has_proposal and self.proposal.has_submitted_documents()

    def has_submitted_excerpt_document(self):
        return self.has_proposal and self.proposal.has_submitted_excerpt_document(
        )

    def close(self):
        """Close the agenda item.

        Can be called to close an agenda item, this puts the agenda item in
        decided state using the correct transitions. Currently valid states
        are:
        decided: do nothing
        pending: decide
        revision: revise
        """
        if self.is_revise_possible():
            self.revise()
        self.decide()

    def is_decide_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_PENDING
        return False

    def is_decided(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_DECIDED
        return False

    def decide(self):
        if self.get_state() == self.STATE_DECIDED:
            return

        self.meeting.hold()

        if self.has_proposal:
            self.proposal.decide(self)

        self.workflow.execute_transition(None, self, 'pending-decided')

    def reopen(self):
        if self.has_proposal:
            self.proposal.reopen(self)
        self.workflow.execute_transition(None, self, 'decided-revision')

    def is_reopen_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_DECIDED
        return False

    def revise(self):
        if not self.is_revise_possible():
            raise WrongAgendaItemState()

        if self.has_proposal:
            self.proposal.revise(self)
        self.workflow.execute_transition(None, self, 'revision-decided')

    def is_revise_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_REVISION
        return False

    def return_excerpt(self, document):
        self.proposal.return_excerpt(document)

    @require_word_meeting_feature
    def generate_excerpt(self, title):
        """Generate an excerpt from the agenda items document.

        Can either be an excerpt from the proposals document or an excerpt
        from the ad-hoc agenda items document.
        In both cases the excerpt is stored in the meeting dossier.
        """

        from opengever.meeting.command import MergeDocxExcerptCommand

        if not self.can_generate_excerpt():
            raise WrongAgendaItemState()

        meeting_dossier = self.meeting.get_dossier()
        source_document = self.resolve_document()

        if not source_document:
            raise ValueError('The agenda item has no document.')

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

        excerpt_document = MergeDocxExcerptCommand(
            context=meeting_dossier,
            agenda_item=self,
            filename=source_document.file.filename,
            title=title,
        ).execute()

        if self.has_proposal:
            submitted_proposal = self.proposal.resolve_submitted_proposal()
            submitted_proposal.append_excerpt(excerpt_document)
        else:
            self.excerpts.append(
                Excerpt(excerpt_oguid=Oguid.for_object(excerpt_document)))

        return excerpt_document

    def can_generate_excerpt(self):
        """Return whether excerpts can be generated."""

        if not self.meeting.is_editable():
            return False

        return self.get_state() == self.STATE_DECIDED

    @require_word_meeting_feature
    def get_excerpt_documents(self, unrestricted=False):
        """Return a list of excerpt documents.

        If the agenda items has a proposal return the proposals excerpt
        documents. Otherwise return the excerpts stored in the meeting
        dossier.
        """
        if self.has_proposal:
            return self.submitted_proposal.get_excerpts(
                unrestricted=unrestricted)

        checkPermission = getSecurityManager().checkPermission
        documents = [excerpt.resolve_document() for excerpt in self.excerpts]
        documents = filter(None, documents)
        if not unrestricted:
            documents = filter(lambda obj: checkPermission('View', obj),
                               documents)

        return documents

    @require_word_meeting_feature
    def get_source_dossier_excerpt(self):
        if not self.has_proposal:
            return None

        return self.proposal.resolve_submitted_excerpt_document()
class AgendaItem(Base):
    """An item must either have a reference to a proposal or a title.

    """

    __tablename__ = 'agendaitems'

    # workflow definition
    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_DECIDED = State('decided', title=_('decided', default='Decided'))
    STATE_REVISION = State('revision', title=_('revision', default='Revision'))

    workflow = Workflow([STATE_PENDING, STATE_DECIDED, STATE_REVISION], [
        Transition('pending', 'decided', title=_('decide', default='Decide')),
        Transition('decided', 'revision', title=_('reopen', default='Reopen')),
        Transition('revision', 'decided', title=_('revise', default='Revise')),
    ])

    agenda_item_id = Column("id",
                            Integer,
                            Sequence("agendaitems_id_seq"),
                            primary_key=True)
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH),
                            nullable=False,
                            default=workflow.default_state.name)
    proposal_id = Column(Integer, ForeignKey('proposals.id'))
    proposal = relationship("Proposal",
                            uselist=False,
                            backref=backref('agenda_item', uselist=False))

    ad_hoc_document_admin_unit_id = Column(String(UNIT_ID_LENGTH))
    ad_hoc_document_int_id = Column(Integer)
    ad_hoc_document_oguid = composite(Oguid, ad_hoc_document_admin_unit_id,
                                      ad_hoc_document_int_id)

    decision_number = Column(Integer)

    title = Column(UnicodeCoercingText)
    description = Column(UnicodeCoercingText)
    item_number = Column(Integer)
    is_paragraph = Column(Boolean, nullable=False, default=False)
    sort_order = Column(Integer, nullable=False, default=0)

    meeting_id = Column(Integer, ForeignKey('meetings.id'), nullable=False)

    def __init__(self, *args, **kwargs):
        proposal = kwargs.get('proposal')
        document = kwargs.pop('document', None)
        if document:
            assert not proposal, 'must only have one of proposal and document'
            kwargs.update(
                {'ad_hoc_document_oguid': Oguid.for_object(document)})

        super(AgendaItem, self).__init__(*args, **kwargs)

    def get_agenda_item_data(self):
        data = {
            'number': self.formatted_number,
            'number_raw': self.item_number,
            'description': self.get_description(),
            'title': self.get_title(),
            'dossier_reference_number': self.get_dossier_reference_number(),
            'repository_folder_title': self.get_repository_folder_title(),
            'is_paragraph': self.is_paragraph,
            'decision_number': self.decision_number
        }
        self._add_attachment_data(data)
        return data

    def _add_attachment_data(self, data):
        if not self.has_proposal:
            return

        documents = self.proposal.resolve_submitted_documents()
        if not documents:
            return

        attachment_data = []
        for document in documents:
            attachment = {'title': document.title}
            filename = document.get_filename()
            if filename:
                attachment['filename'] = filename
            attachment_data.append(attachment)
        data['attachments'] = attachment_data

    @property
    def submitted_proposal(self):
        if not hasattr(self, '_submitted_proposal'):
            self._submitted_proposal = self.proposal.resolve_submitted_proposal(
            )  # noqa
        return self._submitted_proposal

    @property
    def formatted_number(self):
        if not self.item_number:
            return ""
        return '{}.'.format(self.item_number)

    def get_title(self, include_number=False, formatted=False):
        title = (self.proposal.submitted_title
                 if self.has_proposal else self.title)

        if include_number and self.item_number:
            if formatted:
                title = u"{} {}".format(self.formatted_number, title)
            else:
                title = u"{} {}".format(self.item_number, title)

        return title

    def get_title_html(self, include_number=False):
        return to_html_xweb_intelligent(
            self.get_title(include_number=include_number))

    def set_title(self, title):
        if self.has_proposal:
            self.submitted_proposal.title = title
            self.proposal.sync_with_submitted_proposal(self.submitted_proposal)
        else:
            self.title = title

    def get_description(self):
        return (self.proposal.submitted_description
                if self.has_proposal else self.description) or None

    def get_description_html(self):
        return to_html_xweb_intelligent(self.get_description()) or None

    def set_description(self, description):
        if self.has_proposal:
            self.submitted_proposal.description = description
            self.submitted_proposal.sync_model()
        else:
            self.description = description

    def get_decision_number(self):
        # Before the meeting is held, agendaitems do not have a decision number and
        # in that case we do not want to format it with the period title prefixed
        if not self.decision_number:
            return None

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

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

    def get_dossier_reference_number(self):
        if self.has_proposal:
            return self.proposal.dossier_reference_number
        return None

    def get_excerpt_header_template(self):
        return self.meeting.committee.get_excerpt_header_template()

    def get_excerpt_suffix_template(self):
        return self.meeting.committee.get_excerpt_suffix_template()

    def get_repository_folder_title(self):
        if self.has_proposal:
            return self.proposal.repository_folder_title
        return None

    def get_css_class(self):
        css_classes = []
        if self.is_paragraph:
            css_classes.append("paragraph")
        if self.has_submitted_documents():
            css_classes.append("expandable")
        if self.has_proposal:
            css_classes.append("proposal")
        return " ".join(css_classes)

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

    def generate_decision_number(self, period):
        if self.is_paragraph:
            return

        next_decision_number = period.get_next_decision_sequence_number()
        self.decision_number = next_decision_number

    def remove(self):
        assert self.meeting.is_editable()

        # the agenda_item is ad hoc if it has a document but no proposal
        if self.has_document and not self.has_proposal:
            document = self.resolve_document()
            trasher = ITrashable(document)
            trasher.trash()

        session = create_session()
        if self.proposal:
            self.proposal.remove_scheduled(self.meeting)
        session.delete(self)
        self.meeting.reorder_agenda_items()

    def get_proposal_link(self, include_icon=True):
        if not self.has_proposal:
            return self.get_title_html()

        return self.proposal.get_submitted_link(include_icon=include_icon)

    def serialize(self):
        return {
            'id': self.agenda_item_id,
            'css_class': self.get_css_class(),
            'title': self.get_title_html(),
            'description': self.get_description_html(),
            'number': self.formatted_number,
            'number_raw': self.item_number,
            'has_proposal': self.has_proposal,
            'link': self.get_proposal_link(include_icon=False),
        }

    @property
    def has_proposal(self):
        return self.proposal is not None

    @property
    def has_document(self):
        return self.has_proposal or self.ad_hoc_document_int_id is not None

    def resolve_document(self):
        if not self.has_document:
            return None

        if self.has_proposal:
            proposal = self.proposal.resolve_submitted_proposal()
            return proposal.get_proposal_document()

        return self.ad_hoc_document_oguid.resolve_object()

    def checkin_document(self):
        document = self.resolve_document()
        if not document:
            return

        checkout_manager = getMultiAdapter((document, document.REQUEST),
                                           ICheckinCheckoutManager)
        checkout_manager.checkin()

    @property
    def name(self):
        """Currently used as name for input tags in html."""

        return "agenda_item-{}".format(self.agenda_item_id)

    def has_submitted_documents(self):
        return self.has_proposal and self.proposal.has_submitted_documents()

    def resolve_submitted_documents(self):
        if not self.has_proposal:
            return []

        return self.proposal.resolve_submitted_documents()

    def has_submitted_excerpt_document(self):
        return self.has_proposal and self.proposal.has_submitted_excerpt_document(
        )

    def close(self):
        """Close the agenda item.

        Can be called to close an agenda item, this puts the agenda item in
        decided state using the correct transitions. Currently valid states
        are:
        decided: do nothing
        pending: decide
        revision: revise
        """
        if self.is_revise_possible():
            self.revise()
        self.decide()

    def is_decide_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_PENDING
        return False

    def is_decided(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_DECIDED
        return False

    def decide(self):
        if self.get_state() == self.STATE_DECIDED:
            return

        self.meeting.hold()

        self.workflow.execute_transition(None, self, 'pending-decided')

    def reopen(self):
        """If the excerpt has been sent back so that the proposal is
        decided, we also have to reopen the proposal"""
        if self.has_proposal and self.is_proposal_decided():
            self.proposal.reopen(self)
        self.workflow.execute_transition(None, self, 'decided-revision')

    def is_reopen_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_DECIDED
        return False

    def is_proposal_decided(self):
        if not self.has_proposal:
            return False
        return self.proposal.is_decided()

    def is_completed(self):
        """An ad-hoc agendaitem is completed when it is decided, whereas an
        agendaitem with proposal needs to be decided, the excerpt generated
        and returned to the proposal. A paragraph is always considered completed.
        """
        if self.is_paragraph:
            return True
        if self.has_proposal:
            return self.is_decided() and self.is_proposal_decided()
        return self.is_decided()

    def revise(self):
        """If the excerpt has been sent back so that the proposal is
        decided, we also have to revise the proposal"""
        if not self.is_revise_possible():
            raise WrongAgendaItemState()

        if self.has_proposal and self.is_proposal_decided():
            self.proposal.revise(self)
        self.workflow.execute_transition(None, self, 'revision-decided')

    def is_revise_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_REVISION
        return False

    def return_excerpt(self, document):
        """Returning an excerpt decides the proposal.

        Some legacy proposals can already be decided, the proposal can handle
        that by itself.
        """
        self.proposal.decide(self, document)

    def generate_excerpt(self, title):
        """Generate an excerpt from the agenda items document.

        Can either be an excerpt from the proposals document or an excerpt
        from the ad-hoc agenda items document.
        In both cases the excerpt is stored in the meeting dossier.
        """

        from opengever.meeting.command import MergeDocxExcerptCommand

        if not self.can_generate_excerpt():
            raise WrongAgendaItemState()

        meeting_dossier = self.meeting.get_dossier()
        source_document = self.resolve_document()

        if not source_document:
            raise ValueError('The agenda item has no document.')

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

        excerpt_document = MergeDocxExcerptCommand(
            context=meeting_dossier,
            agenda_item=self,
            filename=source_document.get_file().filename,
            title=title,
        ).execute()

        if self.has_proposal:
            submitted_proposal = self.proposal.resolve_submitted_proposal()
            submitted_proposal.append_excerpt(excerpt_document)
        else:
            self.excerpts.append(
                Excerpt(excerpt_oguid=Oguid.for_object(excerpt_document)))

        return excerpt_document

    def can_generate_excerpt(self):
        """Return whether excerpts can be generated."""

        if not self.meeting.is_editable():
            return False

        return self.get_state() == self.STATE_DECIDED

    def get_excerpt_documents(self, unrestricted=False, include_trashed=False):
        """Return a list of excerpt documents.

        If the agenda items has a proposal return the proposals excerpt
        documents. Otherwise return the excerpts stored in the meeting
        dossier.
        """
        if self.has_proposal:
            return self.submitted_proposal.get_excerpts(
                unrestricted=unrestricted, include_trashed=include_trashed)

        checkPermission = getSecurityManager().checkPermission
        documents = [excerpt.resolve_document() for excerpt in self.excerpts]
        documents = filter(None, documents)
        if not unrestricted:
            documents = filter(lambda obj: checkPermission('View', obj),
                               documents)
        if not include_trashed:
            documents = filter(lambda obj: not ITrashed.providedBy(obj),
                               documents)
        return documents

    def get_source_dossier_excerpt(self):
        if not self.has_proposal:
            return None

        return self.proposal.resolve_submitted_excerpt_document()
Exemple #7
0
class Proposal(Base):
    """Sql representation of a proposal."""

    __tablename__ = 'proposals'
    __table_args__ = (
        UniqueConstraint('admin_unit_id', 'int_id'),
        UniqueConstraint('submitted_admin_unit_id', 'submitted_int_id'),
        {})

    proposal_id = Column("id", Integer, Sequence("proposal_id_seq"),
                         primary_key=True)
    admin_unit_id = Column(String(UNIT_ID_LENGTH), nullable=False)
    int_id = Column(Integer, nullable=False)
    oguid = composite(Oguid, admin_unit_id, int_id)
    physical_path = Column(String(256), nullable=False)
    creator = Column(String(USER_ID_LENGTH), nullable=False)

    submitted_admin_unit_id = Column(String(UNIT_ID_LENGTH))
    submitted_int_id = Column(Integer)
    submitted_oguid = composite(
        Oguid, submitted_admin_unit_id, submitted_int_id)
    submitted_physical_path = Column(String(256))

    excerpt_document_id = Column(Integer, ForeignKey('generateddocuments.id'))
    excerpt_document = relationship(
        'GeneratedExcerpt', uselist=False,
        backref=backref('proposal', uselist=False),
        primaryjoin="GeneratedExcerpt.document_id==Proposal.excerpt_document_id")

    submitted_excerpt_document_id = Column(Integer,
                                           ForeignKey('generateddocuments.id'))
    submitted_excerpt_document = relationship(
        'GeneratedExcerpt', uselist=False,
        backref=backref('submitted_proposal', uselist=False),
        primaryjoin="GeneratedExcerpt.document_id==Proposal.submitted_excerpt_document_id")

    title = Column(String(256), nullable=False)
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH), nullable=False)
    legal_basis = Column(UnicodeCoercingText)
    initial_position = Column(UnicodeCoercingText)
    proposed_action = Column(UnicodeCoercingText)

    considerations = Column(UnicodeCoercingText)
    decision_draft = Column(UnicodeCoercingText)
    publish_in = Column(UnicodeCoercingText)
    disclose_to = Column(UnicodeCoercingText)
    copy_for_attention = Column(UnicodeCoercingText)

    committee_id = Column(Integer, ForeignKey('committees.id'))
    committee = relationship('Committee', backref='proposals')
    dossier_reference_number = Column(UnicodeCoercingText, nullable=False)
    repository_folder_title = Column(UnicodeCoercingText, nullable=False)
    language = Column(String(8), nullable=False)

    history_records = relationship('ProposalHistory',
                                   order_by="desc(ProposalHistory.created)")

    # workflow definition
    STATE_PENDING = State('pending', is_default=True,
                          title=_('pending', default='Pending'))
    STATE_SUBMITTED = State('submitted',
                            title=_('submitted', default='Submitted'))
    STATE_SCHEDULED = State('scheduled',
                            title=_('scheduled', default='Scheduled'))
    STATE_DECIDED = State('decided', title=_('decided', default='Decided'))
    STATE_CANCELLED = State('cancelled',
                            title=_('cancelled', default='Cancelled'))

    workflow = Workflow([
        STATE_PENDING,
        STATE_SUBMITTED,
        STATE_SCHEDULED,
        STATE_DECIDED,
        STATE_CANCELLED,
        ], [
        Submit('pending', 'submitted',
               title=_('submit', default='Submit')),
        Reject('submitted', 'pending',
               title=_('reject', default='Reject')),
        Transition('submitted', 'scheduled',
                   title=_('schedule', default='Schedule')),
        Transition('scheduled', 'submitted',
                   title=_('un-schedule', default='Remove from schedule')),
        Transition('scheduled', 'decided',
                   title=_('decide', default='Decide')),
        Cancel('pending', 'cancelled',
               title=_('cancel', default='Cancel')),
        Reactivate('cancelled', 'pending',
                   title=_('reactivate', default='Reactivate')),
        ])

    def __repr__(self):
        return "<Proposal {}@{}>".format(self.int_id, self.admin_unit_id)

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

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

    def get_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.admin_unit_id)

    def get_submitted_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.submitted_admin_unit_id)

    @property
    def id(self):
        return self.proposal_id

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

    def get_searchable_text(self):
        searchable = filter(None, [self.title, self.initial_position])
        return ' '.join([term.encode('utf-8') for term in searchable])

    def get_decision(self):
        if self.agenda_item:
            return self.agenda_item.decision
        return None

    def get_decision_number(self):
        if self.agenda_item:
            return self.agenda_item.decision_number
        return None

    def get_url(self):
        return self._get_url(self.get_admin_unit(), self.physical_path)

    def get_submitted_url(self):
        return self._get_url(self.get_submitted_admin_unit(),
                             self.submitted_physical_path)

    def _get_url(self, admin_unit, physical_path):
        if not (admin_unit and physical_path):
            return ''
        return '/'.join((admin_unit.public_url, physical_path))

    def get_link(self, include_icon=True):
        return self._get_link(self.get_url(), include_icon=include_icon)

    def get_submitted_link(self, include_icon=True):
        return self._get_link(self.get_submitted_url(),
                              include_icon=include_icon)

    def _get_link(self, url, include_icon=True):
        title = escape_html(self.title)
        if include_icon:
            link = u'<a href="{0}" title="{1}" class="{2}">{1}</a>'.format(
                url, title, self.css_class)
        else:
            link = u'<a href="{0}" title="{1}">{1}</a>'.format(url, title)
        return link

    def getPath(self):
        """This method is required by a tabbedview."""

        return self.physical_path

    def resolve_submitted_proposal(self):
        return self.submitted_oguid.resolve_object()

    def resolve_submitted_documents(self):
        return [doc.resolve_submitted() for doc in self.submitted_documents]

    def has_submitted_documents(self):
        return self.submitted_documents or self.submitted_excerpt_document

    def resolve_excerpt_document(self):
        document = self.excerpt_document
        if document:
            return document.oguid.resolve_object()

    def has_submitted_excerpt_document(self):
        return self.submitted_excerpt_document is not None

    def resolve_submitted_excerpt_document(self):
        document = self.submitted_excerpt_document
        if document:
            return document.oguid.resolve_object()

    def can_be_scheduled(self):
        return self.get_state() == self.STATE_SUBMITTED

    def is_submit_additional_documents_allowed(self):
        return self.get_state() in [self.STATE_SUBMITTED, self.STATE_SCHEDULED]

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

    def is_editable_in_committee(self):
        return self.get_state() in [self.STATE_SUBMITTED, self.STATE_SCHEDULED]

    def schedule(self, meeting):
        assert self.can_be_scheduled()

        self.execute_transition('submitted-scheduled')
        session = create_session()
        meeting.agenda_items.append(AgendaItem(proposal=self))
        session.add(proposalhistory.Scheduled(proposal=self, meeting=meeting))

    def reject(self, text):
        assert self.workflow.can_execute_transition(self, 'submitted-pending')

        self.submitted_physical_path = None
        self.submitted_admin_unit_id = None
        self.submitted_int_id = None

        # kill references to submitted documents (i.e. copies), they will be
        # deleted.
        query = proposalhistory.ProposalHistory.query.filter_by(
            proposal=self)
        for record in query.all():
            record.submitted_document = None

        # set workflow state directly for once, the transition is used to
        # redirect to a form.
        self.workflow_state = self.STATE_PENDING.name
        session = create_session()
        session.add(proposalhistory.Rejected(proposal=self, text=text))

    def remove_scheduled(self, meeting):
        self.execute_transition('scheduled-submitted')
        session = create_session()
        session.add(
            proposalhistory.RemoveScheduled(proposal=self, meeting=meeting))

    def resolve_proposal(self):
        return self.oguid.resolve_object()

    def generate_excerpt(self, agenda_item):
        from opengever.meeting.command import CreateGeneratedDocumentCommand
        from opengever.meeting.command import ExcerptOperations

        proposal_obj = self.resolve_submitted_proposal()
        operations = ExcerptOperations(agenda_item)
        CreateGeneratedDocumentCommand(
            proposal_obj, agenda_item.meeting, operations).execute()

    def revise(self, agenda_item):
        assert self.get_state() == self.STATE_DECIDED
        self.update_excerpt(agenda_item)
        self.session.add(proposalhistory.ProposalRevised(proposal=self))

    def reopen(self, agenda_item):
        assert self.get_state() == self.STATE_DECIDED
        self.session.add(proposalhistory.ProposalReopened(proposal=self))

    def cancel(self):
        self.session.add(proposalhistory.Cancelled(proposal=self))

    def reactivate(self):
        self.session.add(proposalhistory.Reactivated(proposal=self))

    def update_excerpt(self, agenda_item):
        from opengever.meeting.command import ExcerptOperations
        from opengever.meeting.command import UpdateExcerptInDossierCommand
        from opengever.meeting.command import UpdateGeneratedDocumentCommand

        operations = ExcerptOperations(agenda_item)
        UpdateGeneratedDocumentCommand(
            self.submitted_excerpt_document,
            agenda_item.meeting,
            operations).execute()
        UpdateExcerptInDossierCommand(self).execute()

    def decide(self, agenda_item):
        self.generate_excerpt(agenda_item)
        document_intid = self.copy_excerpt_to_proposal_dossier()
        self.register_excerpt(document_intid)
        self.session.add(proposalhistory.ProposalDecided(proposal=self))
        self.execute_transition('scheduled-decided')

    def register_excerpt(self, document_intid):
        """Adds a GeneratedExcerpt database entry and a corresponding
        proposalhistory entry.
        """
        version = self.submitted_excerpt_document.generated_version
        excerpt = GeneratedExcerpt(admin_unit_id=self.admin_unit_id,
                                   int_id=document_intid,
                                   generated_version=version)
        self.session.add(excerpt)
        self.excerpt_document = excerpt

    def copy_excerpt_to_proposal_dossier(self):
        """Copies the submitted excerpt to the source dossier and returns
        the intid of the created document.
        """
        from opengever.meeting.command import CreateExcerptCommand

        dossier = self.resolve_proposal().get_containing_dossier()
        response = CreateExcerptCommand(
            self.resolve_submitted_excerpt_document(),
            self.admin_unit_id,
            '/'.join(dossier.getPhysicalPath())).execute()
        return response['intid']

    def get_meeting_link(self):
        agenda_item = self.agenda_item
        if not agenda_item:
            return u''

        return agenda_item.meeting.get_link()
Exemple #8
0
class Committee(Base):

    __tablename__ = 'committees'
    __table_args__ = (UniqueConstraint('admin_unit_id', 'int_id'), {})

    STATE_ACTIVE = State('active', is_default=True,
                         title=_('active', default='Active'))
    STATE_INACTIVE = State('inactive', title=_('inactive', default='Inactive'))

    workflow = Workflow(
        [STATE_ACTIVE, STATE_INACTIVE],
        [Transition(
            'active', 'inactive',
            title=_('label_deactivate', default='Deactivate committee'),
            visible=False),
         Transition(
             'inactive', 'active',
             title=_('label_reactivate', default='Reactivate committee'),
             visible=False)],
    )

    committee_id = Column("id", Integer, Sequence("committee_id_seq"),
                          primary_key=True)

    group_id = Column(String(GROUP_ID_LENGTH),
                      nullable=False)

    admin_unit_id = Column(String(UNIT_ID_LENGTH), nullable=False)
    int_id = Column(Integer, nullable=False)
    oguid = composite(Oguid, admin_unit_id, int_id)
    title = Column(String(256))
    physical_path = Column(String(256), nullable=False)
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH),
                            nullable=False,
                            default=workflow.default_state.name)

    def __repr__(self):
        return '<Committee {}>'.format(repr(self.title))

    def is_active(self):
        return self.get_state() == self.STATE_ACTIVE

    def get_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.admin_unit_id)

    def get_link(self):
        url = self.get_url()
        if not url:
            return ''

        link = u'<a href="{0}" title="{1}">{1}</a>'.format(
            url, escape_html(self.title))
        return link

    def get_url(self, admin_unit=None):
        admin_unit = admin_unit or self.get_admin_unit()
        if not admin_unit:
            return None

        return '/'.join((admin_unit.public_url, self.physical_path))

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

    def resolve_committee(self):
        return self.oguid.resolve_object()

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

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

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

    def get_toc_template(self):
        return self.resolve_committee().get_toc_template()

    def get_active_memberships(self):
        return Membership.query.filter_by(
            committee=self).only_active()

    def deactivate(self):
        if self.has_pending_meetings() or self.has_unscheduled_proposals():
            return False

        return self.workflow.execute_transition(None, self, 'active-inactive')

    def reactivate(self):
        return self.workflow.execute_transition(None, self, 'inactive-active')

    def check_deactivate_conditions(self):
        conditions = [self.all_meetings_closed,
                      self.no_unscheduled_proposals]

        for condition in conditions:
            if not condition():
                return False

        return True

    def has_pending_meetings(self):
        return bool(Meeting.query.pending_meetings(self).count())

    def has_unscheduled_proposals(self):
        query = Proposal.query.filter_by(
            committee=self, workflow_state=Proposal.STATE_SUBMITTED.name)

        return bool(query.count())
class TestUnitWorkflow(TestCase):

    def setUp(self):
        self.private = State('private', is_default=True)
        self.pending = State('pending')
        self.published = State('published')

        self.submit = Transition('private', 'pending')
        self.publish = Transition('pending', 'published')
        self.reject = Transition('pending', 'private')
        self.retract = Retract()

        self.workflow = Workflow([
            self.private, self.pending, self.published
            ], [
            self.submit, self.publish, self.reject, self.retract
        ])

    def test_transition_string_representation(self):
        self.assertEqual('<Transition "private-pending">', str(self.submit))
        self.assertEqual('<Transition "private-pending">', repr(self.submit))

    def test_state_string_representation(self):
        self.assertEqual('<State "pending">', str(self.pending))
        self.assertEqual('<State "pending">', repr(self.pending))

    def test_state_equality(self):
        self.assertEqual(self.private, self.private)
        self.assertEqual(self.private, State('private'))

        self.assertNotEqual(self.private, self.pending)
        self.assertNotEqual(self.pending, None)
        self.assertNotEqual(self.pending, object())

    def test_default_workflow_is_set(self):
        self.assertEqual(self.private, self.workflow.default_state)

    def test_fails_without_default_workflow(self):
        with self.assertRaises(AssertionError):
            Workflow([self.pending], [])

    def test_fails_with_duplicate_state(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.private], [])

    def test_fails_with_duplicate_transition(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.pending], [self.submit, self.submit])

    def test_fails_with_invalid_transition(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.pending],
                     [Transition('private', 'invalid_identifier')])

    def test_get_state_returns_correct_state(self):
        self.assertEqual(self.private, self.workflow.get_state('private'))

    def test_get_state_fails_with_invalid_state(self):
        with self.assertRaises(KeyError):
            self.workflow.get_state('invalid_identifier')

    def test_can_execute_available_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertTrue(
            self.workflow.can_execute_transition(obj, self.submit.name))

    def test_cannot_perform_unavailable_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertFalse(
            self.workflow.can_execute_transition(obj, self.retract.name))

    def test_cannot_perform_invalid_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertFalse(
            self.workflow.can_execute_transition(obj, 'invalid_name'))

    def test_performs_available_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        self.workflow.execute_transition(None, obj, self.publish.name)

        self.assertEqual(self.published.name, obj.workflow_state)

    def test_transitions_handle_kwargs(self):
        """ As the workflow passes kwargs to the transition execute method,
        these whould handle kwargs
        """
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        self.workflow.execute_transition(None, obj, self.publish.name, useless=True)

        self.assertEqual(self.published.name, obj.workflow_state)

    def test_workflow_handles_kwargs(self):
        """ Workflows needs to handle kwargs as some transitions have
        take additional arguments when executed (e.g. Proposal transitions
        also take a comment that is added to the history)
        """
        obj = SomethingWithWorkflow(initial_state=self.published.name)
        self.workflow.execute_transition(None, obj, self.retract.name, useless=True, reason="A comment")
        self.assertEqual(self.pending.name, obj.workflow_state)
        self.assertListEqual(["A comment"], obj.history)

    def test_does_not_perform_unavailable_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        with self.assertRaises(CannotExecuteTransition):
            self.workflow.execute_transition(None, obj, self.submit.name)

    def test_does_not_perform_invalid_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.published.name)
        with self.assertRaises(AssertionError):
            self.workflow.execute_transition(None, obj, 'invalid_identifier')

    def test_transitions_are_registered_with_their_state(self):
        self.assertEqual([self.submit], self.private.get_transitions())
        self.assertEqual([self.publish, self.reject],
                         self.pending.get_transitions())
        self.assertEqual([self.retract], self.published.get_transitions())
Exemple #10
0
class TestUnitWorkflow(TestCase):

    def setUp(self):
        self.private = State('private', is_default=True)
        self.pending = State('pending')
        self.published = State('published')

        self.submit = Transition('private', 'pending')
        self.publish = Transition('pending', 'published')
        self.reject = Transition('pending', 'private')
        self.retract = Transition('published', 'pending')

        self.workflow = Workflow([
            self.private, self.pending, self.published
            ], [
            self.submit, self.publish, self.reject, self.retract
        ])

    def test_transition_string_representation(self):
        self.assertEqual('<Transition "private-pending">', str(self.submit))
        self.assertEqual('<Transition "private-pending">', repr(self.submit))

    def test_state_string_representation(self):
        self.assertEqual('<State "pending">', str(self.pending))
        self.assertEqual('<State "pending">', repr(self.pending))

    def test_state_equality(self):
        self.assertEqual(self.private, self.private)
        self.assertEqual(self.private, State('private'))

        self.assertNotEqual(self.private, self.pending)
        self.assertNotEqual(self.pending, None)
        self.assertNotEqual(self.pending, object())

    def test_default_workflow_is_set(self):
        self.assertEqual(self.private, self.workflow.default_state)

    def test_fails_without_default_workflow(self):
        with self.assertRaises(AssertionError):
            Workflow([self.pending], [])

    def test_fails_with_duplicate_state(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.private], [])

    def test_fails_with_duplicate_transition(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.pending], [self.submit, self.submit])

    def test_fails_with_invalid_transition(self):
        with self.assertRaises(AssertionError):
            Workflow([self.private, self.pending],
                     [Transition('private', 'invalid_identifier')])

    def test_get_state_returns_correct_state(self):
        self.assertEqual(self.private, self.workflow.get_state('private'))

    def test_get_state_fails_with_invalid_state(self):
        with self.assertRaises(KeyError):
            self.workflow.get_state('invalid_identifier')

    def test_can_execute_available_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertTrue(
            self.workflow.can_execute_transition(obj, self.submit.name))

    def test_cannot_perform_unavailable_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertFalse(
            self.workflow.can_execute_transition(obj, self.retract.name))

    def test_cannot_perform_invalid_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.private.name)
        self.assertFalse(
            self.workflow.can_execute_transition(obj, 'invalid_name'))

    def test_performs_available_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        self.workflow.execute_transition(None, obj, self.publish.name)

        self.assertEqual(self.published.name, obj.workflow_state)

    def test_does_not_perform_unavailable_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.pending.name)
        with self.assertRaises(AssertionError):
            self.workflow.execute_transition(None, obj, self.submit.name)

    def test_does_not_perform_invalid_transition(self):
        obj = SomethingWithWorkflow(initial_state=self.published.name)
        with self.assertRaises(AssertionError):
            self.workflow.execute_transition(None, obj, 'invalid_identifier')

    def test_transitions_are_registered_with_their_state(self):
        self.assertEqual([self.submit], self.private.get_transitions())
        self.assertEqual([self.publish, self.reject],
                         self.pending.get_transitions())
        self.assertEqual([self.retract], self.published.get_transitions())
class Period(Base, SQLFormSupport):

    STATE_ACTIVE = State('active', is_default=True,
                         title=_('active', default='Active'))
    STATE_CLOSED = State('closed', title=_('closed', default='Closed'))

    workflow = Workflow([
        STATE_ACTIVE,
        STATE_CLOSED,
        ], [
        Transition('active', 'closed',
                   title=_('close_period', default='Close period')),
        ])

    __tablename__ = 'periods'

    period_id = Column('id', Integer, Sequence('periods_id_seq'),
                       primary_key=True)
    committee_id = Column('committee_id', Integer, ForeignKey('committees.id'),
                          nullable=False)
    committee = relationship('Committee', backref='periods')
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH), nullable=False,
                            default=workflow.default_state.name)
    title = Column(String(256), nullable=False)
    date_from = Column(Date, nullable=False)
    date_to = Column(Date, nullable=False)
    decision_sequence_number = Column(Integer, nullable=False, default=0)
    meeting_sequence_number = Column(Integer, nullable=False, default=0)

    def __repr__(self):
        return '<Period {}>'.format(repr(self.title))

    @property
    def wrapper_id(self):
        return 'period-{}'.format(self.period_id)

    def is_removable(self):
        return False

    def get_title(self):
        return u'{} ({} - {})'.format(
            self.title, self.get_date_from(), self.get_date_to())

    def get_url(self, context, view=None):
        elements = [context.absolute_url(), self.wrapper_id]
        if view:
            elements.append(view)

        return '/'.join(elements)

    def get_date_from(self):
        """Return a localized date."""

        return api.portal.get_localized_time(datetime=self.date_from)

    def get_date_to(self):
        """Return a localized date."""

        return api.portal.get_localized_time(datetime=self.date_to)

    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_next_decision_sequence_number(self):
        self.decision_sequence_number += 1
        return self.decision_sequence_number

    def get_next_meeting_sequence_number(self):
        self.meeting_sequence_number += 1
        return self.meeting_sequence_number

    def get_toc_template(self):
        return self.committee.get_toc_template()
Exemple #12
0
class Proposal(Base):
    """Sql representation of a proposal."""

    __tablename__ = 'proposals'
    __table_args__ = (UniqueConstraint('admin_unit_id', 'int_id'),
                      UniqueConstraint('submitted_admin_unit_id',
                                       'submitted_int_id'), {})

    proposal_id = Column("id",
                         Integer,
                         Sequence("proposal_id_seq"),
                         primary_key=True)
    admin_unit_id = Column(String(UNIT_ID_LENGTH), nullable=False)
    int_id = Column(Integer, nullable=False)
    oguid = composite(Oguid, admin_unit_id, int_id)
    physical_path = Column(UnicodeCoercingText, nullable=False)
    creator = Column(String(USER_ID_LENGTH), nullable=False)

    title = Column(String(MAX_TITLE_LENGTH), index=True)
    submitted_title = Column(String(MAX_TITLE_LENGTH), index=True)

    date_of_submission = Column(Date, index=True)

    submitted_admin_unit_id = Column(String(UNIT_ID_LENGTH))
    submitted_int_id = Column(Integer)
    submitted_oguid = composite(Oguid, submitted_admin_unit_id,
                                submitted_int_id)
    submitted_physical_path = Column(UnicodeCoercingText)

    excerpt_document_id = Column(Integer, ForeignKey('generateddocuments.id'))
    excerpt_document = relationship(
        'GeneratedExcerpt',
        uselist=False,
        backref=backref('proposal', uselist=False),
        primaryjoin="GeneratedExcerpt.document_id==Proposal.excerpt_document_id"
    )

    submitted_excerpt_document_id = Column(Integer,
                                           ForeignKey('generateddocuments.id'))
    submitted_excerpt_document = relationship(
        'GeneratedExcerpt',
        uselist=False,
        backref=backref('submitted_proposal', uselist=False),
        primaryjoin=
        "GeneratedExcerpt.document_id==Proposal.submitted_excerpt_document_id")

    workflow_state = Column(String(WORKFLOW_STATE_LENGTH), nullable=False)

    committee_id = Column(Integer, ForeignKey('committees.id'))
    committee = relationship('Committee', backref='proposals')
    dossier_reference_number = Column(UnicodeCoercingText, nullable=False)
    repository_folder_title = Column(UnicodeCoercingText, nullable=False)
    language = Column(String(8), nullable=False)

    __mapper_args__ = {"order_by": proposal_id}

    # workflow definition
    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_SUBMITTED = State('submitted',
                            title=_('submitted', default='Submitted'))
    STATE_SCHEDULED = State('scheduled',
                            title=_('scheduled', default='Scheduled'))
    STATE_DECIDED = State('decided', title=_('decided', default='Decided'))
    STATE_CANCELLED = State('cancelled',
                            title=_('cancelled', default='Cancelled'))

    workflow = Workflow([
        STATE_PENDING,
        STATE_SUBMITTED,
        STATE_SCHEDULED,
        STATE_DECIDED,
        STATE_CANCELLED,
    ], [
        Submit('pending', 'submitted', title=_('submit', default='Submit')),
        Reject('submitted', 'pending', title=_('reject', default='Reject')),
        Transition(
            'submitted', 'scheduled', title=_('schedule', default='Schedule')),
        Transition('scheduled',
                   'submitted',
                   title=_('un-schedule', default='Remove from schedule')),
        Transition('scheduled', 'decided', title=_('decide',
                                                   default='Decide')),
        Cancel('pending', 'cancelled', title=_('cancel', default='Cancel')),
        Reactivate('cancelled',
                   'pending',
                   title=_('reactivate', default='Reactivate')),
    ])

    def __repr__(self):
        return "<Proposal {}@{}>".format(self.int_id, self.admin_unit_id)

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

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

    def get_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.admin_unit_id)

    def get_submitted_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.submitted_admin_unit_id)

    @property
    def id(self):
        return self.proposal_id

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

    def get_decision_number(self):
        if self.agenda_item:
            return self.agenda_item.get_decision_number()
        return None

    def get_url(self):
        return self._get_url(self.get_admin_unit(), self.physical_path)

    def get_submitted_url(self):
        return self._get_url(self.get_submitted_admin_unit(),
                             self.submitted_physical_path)

    def _get_url(self, admin_unit, physical_path):
        if not (admin_unit and physical_path):
            return ''
        return '/'.join((admin_unit.public_url, physical_path))

    def get_link(self, include_icon=True):
        proposal_ = self.resolve_proposal()
        as_link = proposal_ is None or api.user.has_permission('View',
                                                               obj=proposal_)
        return self._get_link(self.get_url(),
                              self.title,
                              include_icon=include_icon,
                              as_link=as_link)

    def get_submitted_link(self, include_icon=True):
        proposal_ = self.resolve_submitted_proposal()
        as_link = proposal_ is None or api.user.has_permission('View',
                                                               obj=proposal_)
        return self._get_link(self.get_submitted_url(),
                              proposal_.title,
                              include_icon=include_icon,
                              as_link=as_link)

    def _get_link(self, url, title, include_icon=True, as_link=True):
        title = escape_html(title)
        if as_link:
            if include_icon:
                link = u'<a href="{0}" title="{1}" class="{2}">{1}</a>'.format(
                    url, title, self.css_class)
            else:
                link = u'<a href="{0}" title="{1}">{1}</a>'.format(url, title)
            return link

        if include_icon:
            link = u'<span title="{0}" class="{1}">{0}</span>'.format(
                title, self.css_class)
        else:
            link = u'<span title="{0}">{0}</a>'.format(title)
        return link

    def getPath(self):
        """This method is required by a tabbedview."""

        return self.physical_path

    def resolve_submitted_proposal(self):
        return self.submitted_oguid.resolve_object()

    def resolve_submitted_documents(self):
        return [doc.resolve_submitted() for doc in self.submitted_documents]

    def has_submitted_documents(self):
        return bool(self.submitted_documents)

    def resolve_excerpt_document(self):
        document = self.excerpt_document
        if document:
            return document.oguid.resolve_object()

    def has_submitted_excerpt_document(self):
        return self.submitted_excerpt_document is not None

    def resolve_submitted_excerpt_document(self):
        document = self.submitted_excerpt_document
        if document:
            return document.oguid.resolve_object()

    def can_be_scheduled(self):
        return self.get_state() == self.STATE_SUBMITTED

    def is_submit_additional_documents_allowed(self):
        return self.get_state() in [self.STATE_SUBMITTED, self.STATE_SCHEDULED]

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

    def is_editable_in_committee(self):
        return self.get_state() in [self.STATE_SUBMITTED, self.STATE_SCHEDULED]

    def schedule(self, meeting):
        assert self.can_be_scheduled()

        self.execute_transition('submitted-scheduled')
        meeting.agenda_items.append(AgendaItem(proposal=self))

        IHistory(self.resolve_submitted_proposal()).append_record(
            u'scheduled', meeting_id=meeting.meeting_id)

    def reject(self, text):
        assert self.workflow.can_execute_transition(self, 'submitted-pending')

        self.submitted_physical_path = None
        self.submitted_admin_unit_id = None
        self.submitted_int_id = None
        self.date_of_submission = None

        # set workflow state directly for once, the transition is used to
        # redirect to a form.
        self.workflow_state = self.STATE_PENDING.name
        IHistory(self.resolve_proposal()).append_record(u'rejected', text=text)

    def remove_scheduled(self, meeting):
        self.execute_transition('scheduled-submitted')
        IHistory(self.resolve_submitted_proposal()).append_record(
            u'remove_scheduled', meeting_id=meeting.meeting_id)

    def resolve_proposal(self):
        return self.oguid.resolve_object()

    def revise(self, agenda_item):
        assert self.get_state() == self.STATE_DECIDED

        document = self.resolve_submitted_proposal().get_proposal_document()
        checkout_manager = getMultiAdapter((document, document.REQUEST),
                                           ICheckinCheckoutManager)
        if checkout_manager.get_checked_out_by() is not None:
            raise ValueError(
                'Cannot revise proposal when proposal document is checked out.'
            )

        IHistory(self.resolve_submitted_proposal()).append_record(u'revised')

    def reopen(self, agenda_item):
        assert self.get_state() == self.STATE_DECIDED
        IHistory(self.resolve_submitted_proposal()).append_record(u'reopened')

    def cancel(self):
        IHistory(self.resolve_proposal()).append_record(u'cancelled')

    def reactivate(self):
        IHistory(self.resolve_proposal()).append_record(u'reactivated')

    def decide(self, agenda_item):
        document = self.resolve_submitted_proposal().get_proposal_document()
        checkout_manager = getMultiAdapter((document, document.REQUEST),
                                           ICheckinCheckoutManager)
        if checkout_manager.get_checked_out_by() is not None:
            raise ValueError(
                'Cannot decide proposal when proposal document is checked out.'
            )

        IHistory(self.resolve_submitted_proposal()).append_record(u'decided')
        self.execute_transition('scheduled-decided')

    def register_excerpt(self, document_intid):
        """Adds a GeneratedExcerpt database entry and a corresponding
        proposalhistory entry.
        """
        version = self.submitted_excerpt_document.generated_version
        excerpt = GeneratedExcerpt(admin_unit_id=self.admin_unit_id,
                                   int_id=document_intid,
                                   generated_version=version)
        self.session.add(excerpt)
        self.excerpt_document = excerpt

    def return_excerpt(self, document):
        """Return the selected excerpt to the proposals originating dossier.

        The document is registered as official excerpt for this proposal and
        copied to the dossier. Future edits in the excerpt document will
        be synced to the proposals dossier.

        """
        assert document in self.resolve_submitted_proposal().get_excerpts()

        version = document.get_current_version_id(missing_as_zero=True)
        excerpt = GeneratedExcerpt(oguid=Oguid.for_object(document),
                                   generated_version=version)
        self.submitted_excerpt_document = excerpt

        document_intid = self.copy_excerpt_to_proposal_dossier()
        self.register_excerpt(document_intid)

    def copy_excerpt_to_proposal_dossier(self):
        """Copies the submitted excerpt to the source dossier and returns
        the intid of the created document.
        """
        from opengever.meeting.command import CreateExcerptCommand

        dossier = self.resolve_proposal().get_containing_dossier()
        response = CreateExcerptCommand(
            self.resolve_submitted_excerpt_document(), self.admin_unit_id,
            '/'.join(dossier.getPhysicalPath())).execute()
        return response['intid']

    def get_meeting_link(self):
        agenda_item = self.agenda_item
        if not agenda_item:
            return u''

        return agenda_item.meeting.get_link()
Exemple #13
0
class AgendaItem(Base):
    """An item must either have a reference to a proposal or a title.

    """

    __tablename__ = 'agendaitems'

    # workflow definition
    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_DECIDED = State('decided', title=_('decided', default='Decided'))
    STATE_REVISION = State('revision', title=_('revision', default='Revision'))

    workflow = Workflow([STATE_PENDING, STATE_DECIDED, STATE_REVISION], [
        Transition('pending', 'decided', title=_('decide', default='Decide')),
        Transition('decided', 'revision', title=_('reopen', default='Reopen')),
        Transition('revision', 'decided', title=_('revise', default='Revise')),
    ])

    agenda_item_id = Column("id",
                            Integer,
                            Sequence("agendaitems_id_seq"),
                            primary_key=True)
    workflow_state = Column(String(WORKFLOW_STATE_LENGTH),
                            nullable=False,
                            default=workflow.default_state.name)
    proposal_id = Column(Integer, ForeignKey('proposals.id'))
    proposal = relationship("Proposal",
                            uselist=False,
                            backref=backref('agenda_item', uselist=False))
    decision_number = Column(Integer)

    title = Column(UnicodeCoercingText)
    number = Column('item_number', String(16))
    is_paragraph = Column(Boolean, nullable=False, default=False)
    sort_order = Column(Integer, nullable=False, default=0)

    meeting_id = Column(Integer, ForeignKey('meetings.id'), nullable=False)

    discussion = Column(UnicodeCoercingText)
    decision = Column(UnicodeCoercingText)

    def __init__(self, *args, **kwargs):
        """Prefill the decision attributes with proposal's decision_draft.
        """
        proposal = kwargs.get('proposal')
        if proposal and not kwargs.get('decision'):
            kwargs.update({'decision': proposal.decision_draft})

        super(AgendaItem, self).__init__(*args, **kwargs)

    def update(self, request):
        """Update with changed data."""

        data = request.get(self.name)
        if not data:
            return

        def to_safe_html(markup):
            # keep empty data (whatever it is), it makes transform unhappy
            if not markup:
                return markup

            markup = markup.decode('utf-8')
            markup = trix2sablon.convert(markup)
            return trix_strip_whitespace(markup)

        if self.has_proposal:
            self.proposal.legal_basis = to_safe_html(data.get('legal_basis'))
            self.proposal.initial_position = to_safe_html(
                data.get('initial_position'))
            self.proposal.considerations = to_safe_html(
                data.get('considerations'))
            self.proposal.proposed_action = to_safe_html(
                data.get('proposed_action'))
            self.proposal.publish_in = to_safe_html(data.get('publish_in'))
            self.proposal.disclose_to = to_safe_html(data.get('disclose_to'))
            self.proposal.copy_for_attention = to_safe_html(
                data.get('copy_for_attention'))

        self.discussion = to_safe_html(data.get('discussion'))
        self.decision = to_safe_html(data.get('decision'))

    def get_field_data(self,
                       include_initial_position=True,
                       include_legal_basis=True,
                       include_considerations=True,
                       include_proposed_action=True,
                       include_discussion=True,
                       include_decision=True,
                       include_publish_in=True,
                       include_disclose_to=True,
                       include_copy_for_attention=True):
        data = {
            'number': self.number,
            'description': self.description,
            'title': self.get_title(),
            'dossier_reference_number': self.get_dossier_reference_number(),
            'repository_folder_title': self.get_repository_folder_title(),
            'is_paragraph': self.is_paragraph,
            'decision_number': self.decision_number,
            'html:decision_draft':
            self._sanitize_text(self.get_decision_draft())
        }

        if include_initial_position:
            data['html:initial_position'] = self._sanitize_text(
                self.initial_position)
        if include_legal_basis:
            data['html:legal_basis'] = self._sanitize_text(self.legal_basis)
        if include_considerations:
            data['html:considerations'] = self._sanitize_text(
                self.considerations)
        if include_proposed_action:
            data['html:proposed_action'] = self._sanitize_text(
                self.proposed_action)
        if include_discussion:
            data['html:discussion'] = self._sanitize_text(self.discussion)
        if include_decision:
            data['html:decision'] = self._sanitize_text(self.decision)
        if include_publish_in:
            data['html:publish_in'] = self._sanitize_text(self.publish_in)
        if include_disclose_to:
            data['html:disclose_to'] = self._sanitize_text(self.disclose_to)
        if include_copy_for_attention:
            data['html:copy_for_attention'] = self._sanitize_text(
                self.copy_for_attention)

        self._add_attachment_data(data)
        return data

    def _add_attachment_data(self, data):
        if not self.has_proposal:
            return

        documents = self.proposal.resolve_submitted_documents()
        if not documents:
            return

        attachment_data = []
        for document in documents:
            attachment = {'title': document.title}
            filename = document.get_filename()
            if filename:
                attachment['filename'] = filename
            attachment_data.append(attachment)
        data['attachments'] = attachment_data

    def _sanitize_text(self, text):
        if not text:
            return None

        return text

    def get_title(self, include_number=False):
        title = self.proposal.title if self.has_proposal else self.title
        if include_number and self.number:
            title = u"{} {}".format(self.number, title)

        return title

    def set_title(self, title):
        if self.has_proposal:
            self.proposal.title = title
        else:
            self.title = title

    def get_decision_draft(self):
        if self.has_proposal:
            return self.proposal.decision_draft

    def get_dossier_reference_number(self):
        if self.has_proposal:
            return self.proposal.dossier_reference_number
        return None

    def get_repository_folder_title(self):
        if self.has_proposal:
            return self.proposal.repository_folder_title
        return None

    def get_css_class(self):
        css_classes = []
        if self.is_paragraph:
            css_classes.append("paragraph")
        if self.has_submitted_documents():
            css_classes.append("expandable")
        if self.has_proposal:
            css_classes.append("proposal")
        return " ".join(css_classes)

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

    def generate_decision_number(self, period):
        if self.is_paragraph:
            return

        next_decision_number = period.get_next_decision_sequence_number()
        self.decision_number = next_decision_number

    def remove(self):
        assert self.meeting.is_editable()

        session = create_session()
        if self.proposal:
            self.proposal.remove_scheduled(self.meeting)
        session.delete(self)
        self.meeting.reorder_agenda_items()

    def get_proposal_link(self, include_icon=True):
        if not self.has_proposal:
            return self.get_title()

        return self.proposal.get_submitted_link(include_icon=include_icon)

    def serialize(self):
        return {
            'id': self.agenda_item_id,
            'css_class': self.get_css_class(),
            'title': self.get_title(),
            'number': self.number,
            'has_proposal': self.has_proposal,
            'link': self.get_proposal_link(include_icon=False),
        }

    @property
    def has_proposal(self):
        return self.proposal is not None

    @property
    def legal_basis(self):
        return self.proposal.legal_basis if self.has_proposal else None

    @property
    def initial_position(self):
        return self.proposal.initial_position if self.has_proposal else None

    @property
    def considerations(self):
        return self.proposal.considerations if self.has_proposal else None

    @property
    def proposed_action(self):
        return self.proposal.proposed_action if self.has_proposal else None

    @property
    def publish_in(self):
        return self.proposal.publish_in if self.has_proposal else None

    @property
    def disclose_to(self):
        return self.proposal.disclose_to if self.has_proposal else None

    @property
    def copy_for_attention(self):
        return self.proposal.copy_for_attention if self.has_proposal else None

    @property
    def name(self):
        """Currently used as name for input tags in html."""

        return "agenda_item-{}".format(self.agenda_item_id)

    @property
    def description(self):
        return self.get_title()

    def has_submitted_documents(self):
        return self.has_proposal and self.proposal.has_submitted_documents()

    def has_submitted_excerpt_document(self):
        return self.has_proposal and self.proposal.has_submitted_excerpt_document(
        )

    def close(self):
        """Close the agenda item.

        Can be called to close an agenda item, this puts the agenda item in
        decided state using the correct transitions. Currently valid states
        are:
        decided: do nothing
        pending: decide
        revision: revise
        """
        if self.is_revise_possible():
            self.revise()
        self.decide()

    def is_decide_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_PENDING
        return False

    def is_decided(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_DECIDED
        return False

    def decide(self):
        if self.get_state() == self.STATE_DECIDED:
            return

        self.meeting.hold()

        if self.has_proposal:
            self.proposal.decide(self)

        self.workflow.execute_transition(None, self, 'pending-decided')

    def reopen(self):
        if self.has_proposal:
            self.proposal.reopen(self)
        self.workflow.execute_transition(None, self, 'decided-revision')

    def is_reopen_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_DECIDED
        return False

    def revise(self):
        assert self.is_revise_possible()

        if self.has_proposal:
            self.proposal.revise(self)
        self.workflow.execute_transition(None, self, 'revision-decided')

    def is_revise_possible(self):
        if not self.is_paragraph:
            return self.get_state() == self.STATE_REVISION
        return False
Exemple #14
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()
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()
Exemple #16
0
class Proposal(Base):
    """Sql representation of a proposal."""

    __tablename__ = 'proposals'
    __table_args__ = (UniqueConstraint('admin_unit_id', 'int_id'),
                      UniqueConstraint('submitted_admin_unit_id',
                                       'submitted_int_id'), {})

    proposal_id = Column("id",
                         Integer,
                         Sequence("proposal_id_seq"),
                         primary_key=True)
    admin_unit_id = Column(String(UNIT_ID_LENGTH), nullable=False)
    int_id = Column(Integer, nullable=False)
    oguid = composite(Oguid, admin_unit_id, int_id)
    physical_path = Column(UnicodeCoercingText, nullable=False)
    issuer = Column(String(USER_ID_LENGTH), nullable=False)

    title = Column(String(MAX_TITLE_LENGTH), index=True)
    submitted_title = Column(String(MAX_TITLE_LENGTH), index=True)

    description = Column(UnicodeCoercingText)
    submitted_description = Column(UnicodeCoercingText)

    date_of_submission = Column(Date, index=True)

    submitted_admin_unit_id = Column(String(UNIT_ID_LENGTH))
    submitted_int_id = Column(Integer)
    submitted_oguid = composite(Oguid, submitted_admin_unit_id,
                                submitted_int_id)
    submitted_physical_path = Column(UnicodeCoercingText)

    excerpt_document_id = Column(Integer, ForeignKey('generateddocuments.id'))
    excerpt_document = relationship(
        'GeneratedExcerpt',
        uselist=False,
        backref=backref('proposal', uselist=False),
        primaryjoin="GeneratedExcerpt.document_id==Proposal.excerpt_document_id"
    )

    submitted_excerpt_document_id = Column(Integer,
                                           ForeignKey('generateddocuments.id'))
    submitted_excerpt_document = relationship(
        'GeneratedExcerpt',
        uselist=False,
        backref=backref('submitted_proposal', uselist=False),
        primaryjoin=
        "GeneratedExcerpt.document_id==Proposal.submitted_excerpt_document_id")

    workflow_state = Column(String(WORKFLOW_STATE_LENGTH), nullable=False)

    committee_id = Column(Integer, ForeignKey('committees.id'))
    committee = relationship('Committee', backref='proposals')
    dossier_reference_number = Column(UnicodeCoercingText, nullable=False)
    repository_folder_title = Column(UnicodeCoercingText, nullable=False)
    language = Column(String(8), nullable=False)

    __mapper_args__ = {"order_by": proposal_id}

    # workflow definition
    STATE_PENDING = State('pending',
                          is_default=True,
                          title=_('pending', default='Pending'))
    STATE_SUBMITTED = State('submitted',
                            title=_('submitted', default='Submitted'))
    STATE_SCHEDULED = State('scheduled',
                            title=_('scheduled', default='Scheduled'))
    STATE_DECIDED = State('decided', title=_('decided', default='Decided'))
    STATE_CANCELLED = State('cancelled',
                            title=_('cancelled', default='Cancelled'))

    workflow = Workflow([
        STATE_PENDING,
        STATE_SUBMITTED,
        STATE_SCHEDULED,
        STATE_DECIDED,
        STATE_CANCELLED,
    ], [
        Reject('submitted', 'pending', title=_('reject', default='Reject')),
        Transition(
            'submitted', 'scheduled', title=_('schedule', default='Schedule')),
        Transition('scheduled',
                   'submitted',
                   title=_('un-schedule', default='Remove from schedule')),
        Transition('scheduled', 'decided', title=_('decide',
                                                   default='Decide')),
    ])

    # temporary mapping for plone workflow state to model workflow state
    WORKFLOW_STATE_TO_SQL_STATE = {
        'proposal-state-active': 'pending',
        'proposal-state-cancelled': 'cancelled',
        'proposal-state-decided': 'decided',
        'proposal-state-scheduled': 'scheduled',
        'proposal-state-submitted': 'submitted',
    }

    def __repr__(self):
        return "<Proposal {}@{}>".format(self.int_id, self.admin_unit_id)

    @classmethod
    def create_from(cls, proposal):
        model = cls(oguid=Oguid.for_object(proposal),
                    workflow_state='pending',
                    physical_path=proposal.get_physical_path())
        model.sync_with_proposal(proposal)
        return model

    def sync_with_proposal(self, proposal):
        """Sync self with a plone proposal instance."""

        from opengever.meeting.model.committee import Committee

        reference_number = proposal.get_main_dossier_reference_number()
        repository_folder_title = safe_unicode(
            proposal.get_repository_folder_title())
        committee = Committee.get_one(
            oguid=Oguid.parse(proposal.committee_oguid))

        # temporarily use mapping from plone workflow state to model workflow
        # state
        workflow_state = api.content.get_state(proposal)
        new_sql_state = self.WORKFLOW_STATE_TO_SQL_STATE.get(workflow_state)
        if new_sql_state:
            self.workflow_state = new_sql_state

        self.committee = committee
        self.language = proposal.language
        self.physical_path = proposal.get_physical_path()
        self.dossier_reference_number = reference_number
        self.repository_folder_title = repository_folder_title
        self.title = proposal.title
        self.issuer = proposal.issuer
        self.description = proposal.description
        self.date_of_submission = proposal.date_of_submission

    def sync_with_submitted_proposal(self, submitted_proposal):
        """Sync self with a plone submitted proposal instance."""

        self.submitted_oguid = Oguid.for_object(submitted_proposal)
        self.submitted_physical_path = submitted_proposal.get_physical_path()
        self.submitted_admin_unit_id = get_current_admin_unit().id()
        self.submitted_title = submitted_proposal.title
        self.submitted_description = submitted_proposal.description
        self.date_of_submission = submitted_proposal.date_of_submission

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

    def execute_transition(self, name, text=None):
        self.workflow.execute_transition(None, self, name, text=text)

    def get_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.admin_unit_id)

    def get_submitted_admin_unit(self):
        return ogds_service().fetch_admin_unit(self.submitted_admin_unit_id)

    @property
    def id(self):
        return self.proposal_id

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

    def get_decision_number(self):
        if self.agenda_item:
            return self.agenda_item.get_decision_number()
        return None

    def get_url(self):
        return self._get_url(self.get_admin_unit(), self.physical_path)

    def get_submitted_url(self):
        return self._get_url(self.get_submitted_admin_unit(),
                             self.submitted_physical_path)

    def _get_url(self, admin_unit, physical_path):
        if not (admin_unit and physical_path):
            return ''
        return '/'.join((admin_unit.public_url, physical_path))

    def get_link(self, include_icon=True):
        proposal_ = self.resolve_proposal()
        as_link = proposal_ is None or api.user.has_permission('View',
                                                               obj=proposal_)
        return self._get_link(self.get_url(),
                              self.title,
                              include_icon=include_icon,
                              as_link=as_link)

    def get_description(self):
        proposal_ = self.resolve_proposal()
        return proposal_.get_description()

    def get_submitted_description(self):
        proposal_ = self.resolve_submitted_proposal()
        return proposal_.get_description()

    def get_submitted_link(self, include_icon=True):
        proposal_ = self.resolve_submitted_proposal()
        as_link = proposal_ is None or api.user.has_permission('View',
                                                               obj=proposal_)
        return self._get_link(self.get_submitted_url(),
                              proposal_.title,
                              include_icon=include_icon,
                              as_link=as_link)

    def _get_link(self, url, title, include_icon=True, as_link=True):
        title = escape_html(title)
        if as_link:
            if include_icon:
                link = u'<a href="{0}" title="{1}" class="{2}">{1}</a>'.format(
                    url, title, self.css_class)
            else:
                link = u'<a href="{0}" title="{1}">{1}</a>'.format(url, title)
            return link

        if include_icon:
            link = u'<span title="{0}" class="{1}">{0}</span>'.format(
                title, self.css_class)
        else:
            link = u'<span title="{0}">{0}</a>'.format(title)
        return link

    def getPath(self):
        """This method is required by a tabbedview."""

        return self.physical_path

    def resolve_submitted_proposal(self):
        return self.submitted_oguid.resolve_object()

    def resolve_submitted_documents(self):
        return [doc.resolve_submitted() for doc in self.submitted_documents]

    def has_submitted_documents(self):
        return bool(self.submitted_documents)

    def resolve_excerpt_document(self):
        document = self.excerpt_document
        if document:
            return document.oguid.resolve_object()

    def has_submitted_excerpt_document(self):
        return self.submitted_excerpt_document is not None

    def resolve_submitted_excerpt_document(self):
        document = self.submitted_excerpt_document
        if document:
            return document.oguid.resolve_object()

    def can_be_scheduled(self):
        return self.get_state() == self.STATE_SUBMITTED

    def is_submit_additional_documents_allowed(self):
        return self.get_state() in [self.STATE_SUBMITTED, self.STATE_SCHEDULED]

    def is_editable_in_committee(self):
        return self.get_state() in [self.STATE_SUBMITTED, self.STATE_SCHEDULED]

    def schedule(self, meeting):
        assert self.can_be_scheduled()

        self.execute_transition('submitted-scheduled')
        meeting.agenda_items.append(AgendaItem(proposal=self))
        meeting.reorder_agenda_items()

        submitted_proposal = self.resolve_submitted_proposal()
        ProposalScheduledActivity(submitted_proposal, getRequest(),
                                  meeting.meeting_id).record()
        IHistory(self.resolve_submitted_proposal()).append_record(
            u'scheduled', meeting_id=meeting.meeting_id)

        request_data = {
            'data': advancedjson.dumps({
                'meeting_id': meeting.meeting_id,
            })
        }
        expect_ok_response(
            dispatch_request(self.admin_unit_id,
                             '@@receive-proposal-scheduled',
                             path=self.physical_path,
                             data=request_data),
            'Unexpected response {!r} when scheduling proposal.')

    def reject(self, text):
        assert self.workflow.can_execute_transition(self, 'submitted-pending')

        self.submitted_physical_path = None
        self.submitted_admin_unit_id = None
        self.submitted_int_id = None
        self.date_of_submission = None

        # set workflow state directly for once, the transition is used to
        # redirect to a form.
        self.workflow_state = self.STATE_PENDING.name

        request_data = {
            'data': advancedjson.dumps({
                'text': text,
            })
        }
        expect_ok_response(
            dispatch_request(self.admin_unit_id,
                             '@@receive-proposal-rejected',
                             path=self.physical_path,
                             data=request_data),
            'Unexpected response {!r} when rejecting proposal.')

    def remove_scheduled(self, meeting):
        self.execute_transition('scheduled-submitted')
        IHistory(self.resolve_submitted_proposal()).append_record(
            u'remove_scheduled', meeting_id=meeting.meeting_id)

        request_data = {
            'data': advancedjson.dumps({
                'meeting_id': meeting.meeting_id,
            })
        }
        expect_ok_response(
            dispatch_request(self.admin_unit_id,
                             '@@receive-proposal-unscheduled',
                             path=self.physical_path,
                             data=request_data),
            'Unexpected response {!r} when unscheduling proposal.')

    def resolve_proposal(self):
        return self.oguid.resolve_object()

    def revise(self, agenda_item):
        assert self.get_state() == self.STATE_DECIDED

        document = self.resolve_submitted_proposal().get_proposal_document()
        checkout_manager = getMultiAdapter((document, document.REQUEST),
                                           ICheckinCheckoutManager)
        if checkout_manager.get_checked_out_by() is not None:
            raise ValueError(
                'Cannot revise proposal when proposal document is checked out.'
            )

        IHistory(self.resolve_submitted_proposal()).append_record(u'revised')

    def is_decided(self):
        return self.get_state() == self.STATE_DECIDED

    def reopen(self, agenda_item):
        assert self.is_decided()
        IHistory(self.resolve_submitted_proposal()).append_record(u'reopened')

    def decide(self, agenda_item, excerpt_document):
        # Proposals for AgendaItems that were decided before we introduced that
        # Proposals get decided only when the excerpt is returned can be
        # already decided even if the proposal has not been returned. Thus we
        # only decide the proposal if it has not been decided yet.
        if not self.is_decided():
            proposal_document = self.resolve_submitted_proposal(
            ).get_proposal_document()
            checkout_manager = getMultiAdapter(
                (proposal_document, proposal_document.REQUEST),
                ICheckinCheckoutManager)
            if checkout_manager.get_checked_out_by() is not None:
                raise ValueError(
                    'Cannot decide proposal when proposal document is checked out.'
                )

            submitted_proposal = self.resolve_submitted_proposal()
            ProposalDecideActivity(submitted_proposal, getRequest()).record()
            IHistory(submitted_proposal).append_record(u'decided')
            self.execute_transition('scheduled-decided')

        self._return_excerpt(excerpt_document)

    def register_excerpt(self, document_intid):
        """Adds a GeneratedExcerpt database entry and a corresponding
        proposalhistory entry.
        """
        version = self.submitted_excerpt_document.generated_version
        excerpt = GeneratedExcerpt(admin_unit_id=self.admin_unit_id,
                                   int_id=document_intid,
                                   generated_version=version)
        self.session.add(excerpt)
        self.excerpt_document = excerpt

    def _return_excerpt(self, document):
        """Return the selected excerpt to the proposals originating dossier.

        The document is registered as official excerpt for this proposal and
        copied to the dossier. Future edits in the excerpt document will
        be synced to the proposals dossier.

        """
        assert document in self.resolve_submitted_proposal().get_excerpts()

        version = document.get_current_version_id(missing_as_zero=True)
        excerpt = GeneratedExcerpt(oguid=Oguid.for_object(document),
                                   generated_version=version)
        self.submitted_excerpt_document = excerpt

        document_intid = self._copy_excerpt_to_proposal_dossier()
        self.register_excerpt(document_intid)

    def _copy_excerpt_to_proposal_dossier(self):
        """Copies the submitted excerpt to the source dossier and returns
        the intid of the created document.
        """
        from opengever.meeting.command import CreateExcerptCommand

        response = CreateExcerptCommand(
            self.resolve_submitted_excerpt_document(), self.admin_unit_id,
            self.physical_path).execute()
        return response['intid']

    def get_meeting_link(self):
        agenda_item = self.agenda_item
        if not agenda_item:
            return u''

        return agenda_item.meeting.get_link()