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_fails_with_invalid_transition(self): with self.assertRaises(AssertionError): Workflow([self.private, self.pending], [Transition('private', 'invalid_identifier')])
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 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()
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()
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()
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 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()
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
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()
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()