@@ -1,10 +1,14 @@
from Acquisition import aq_inner
from collective.quickupload.interfaces import IQuickUploadFileFactory
from opengever.document import _
from opengever.document.document import IDocumentSchema
from opengever.document.interfaces import ICheckinCheckoutManager
from opengever.meeting.proposaltemplate import IProposalTemplate
from zExceptions import Unauthorized
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.globalrequest import getRequest
from zope.i18n import translate
from zope.interface import implementer
import os

@@ -13,7 +17,10 @@
@adapter(IDocumentSchema)
class QuickUploadFileUpdater(object):
"""Specific quick_upload adapter for documents, which only replace the file
with the uploaded one."""
with the uploaded one.
Disallows non-.docx files for proposal documents.
"""

def __init__(self, context):
self.context = aq_inner(context)
@@ -22,6 +29,18 @@ def __call__(self, filename, title, description, content_type, data, portal_type
if not self.is_upload_allowed():
raise Unauthorized

if self.is_proposal_upload() or self.is_proposal_template_upload():
if not os.path.splitext(self.get_file_name(filename))[1].lower() == '.docx':
return {
'error': translate(_(
u'error_proposal_document_type',
default=u"It's not possible to have non-.docx documents as proposal documents.",
),
context=getRequest(),
),
'success': None,
}

self.context.update_file(data,
content_type=content_type,
filename=self.get_file_name(filename))
@@ -33,6 +52,13 @@ def is_upload_allowed(self):
ICheckinCheckoutManager)
return manager.is_file_upload_allowed()

def is_proposal_upload(self):
"""The upload form context can be, for example, a Dossier."""
return getattr(self.context, 'is_inside_a_proposal', lambda: False)()

def is_proposal_template_upload(self):
return IProposalTemplate.providedBy(self.context)

def get_file_name(self, org_filename):
filename, ext = os.path.splitext(org_filename)
if self.context.file:
@@ -79,13 +79,15 @@ def test_force_checkin_clears_lock(self):
class TestCheckin(FunctionalTestCase):
"""Tests for the checkin functionality."""

document_date = date(2014, 1, 1)

def setUp(self):
super(TestCheckin, self).setUp()
self.dossier = create(Builder('dossier'))

self.document = create(Builder('document')
.with_dummy_content()
.having(document_date=date(2014, 1, 1))
.having(document_date=self.document_date)
.within(self.dossier)
.checked_out())

@@ -122,20 +124,21 @@ def test_clear_locks(self):
self.manager.checkin()
self.assertFalse(IRefreshableLockable(self.document).locked())

def test_document_date_is_updated_to_current_date(self):
def test_document_date_is_not_updated_when_checked_in(self):
self.manager.checkin()

self.assertEquals(date.today(), self.document.document_date)
self.assertEquals(self.document_date, self.document.document_date)


class TestReverting(FunctionalTestCase):
"""Tests for reverting documents to older revisions."""

document_date = date(2014, 1, 1)

def setUp(self):
super(TestReverting, self).setUp()
self.dossier = create(Builder('dossier'))
self.document = create(Builder('document')
.having(document_date=date(2014, 1, 1))
.having(document_date=self.document_date)
.within(self.dossier)
.attach_file_containing(
u"INITIAL VERSION DATA", u"somefile.txt"))
@@ -172,13 +175,11 @@ def test_creates_a_new_blob_instance(self):
self.document.file._blob, version2.object.file._blob)
self.assertNotEqual(self.document.file, version2.object.file)

def test_resets_document_date_to_reverted_version(self):
def test_revert_does_not_change_document_date(self):
with freeze(datetime(2015, 01, 28, 12, 00)):
create_document_version(self.document, 3)

self.document.document_date = date(2015, 5, 15)
self.manager.revert_to_version(3)
self.assertEquals(date(2015, 01, 28), self.document.document_date)
self.assertEquals(self.document_date, self.document.document_date)

def test_revert_disallowed_for_unprivileded_user(self):
self.grant('Authenticated')
@@ -1,47 +1,39 @@
from ftw.builder import Builder
from ftw.builder import create
from opengever.testing import FunctionalTestCase
from opengever.testing import IntegrationTestCase
from plone.locking.interfaces import IRefreshableLockable
from zExceptions import Unauthorized


class TestDocumentQuickupload(FunctionalTestCase):

def setUp(self):
super(TestDocumentQuickupload, self).setUp()
self.dossier = create(Builder('dossier'))
self.document = create(Builder('document')
.titled('Anfrage Herr Meier')
.checked_out()
.attach_file_containing('OLD DATA')
.within(self.dossier))
class TestDocumentQuickupload(IntegrationTestCase):

def test_raises_unauthorized_when_document_is_not_checked_out(self):
document = create(Builder('document'))
self.login(self.regular_user)
with self.assertRaises(Unauthorized):
create(Builder('quickuploaded_document')
.within(document)
.within(self.document)
.with_data('text'))

def test_raises_unauthorized_when_document_is_locked(self):
self.login(self.regular_user)
IRefreshableLockable(self.document).lock()

with self.assertRaises(Unauthorized):
create(Builder('quickuploaded_document')
.within(self.document)
.with_data('text'))

def test_file_is_updated(self):
self.login(self.regular_user)
self.checkout_document(self.document)
create(Builder('quickuploaded_document')
.within(self.document)
.with_data('NEW DATA'))

self.assertEquals('NEW DATA', self.document.file.data)

def test_uses_existing_filename_but_new_extension(self):
self.login(self.regular_user)
self.checkout_document(self.document)
create(Builder('quickuploaded_document')
.within(self.document)
.with_data('NEW DATA', filename='test.pdf'))

self.assertEquals('Anfrage Herr Meier.pdf',
self.document.file.filename)
self.assertEquals('Vertraegsentwurf.pdf', self.document.file.filename)
@@ -10,8 +10,9 @@ class CreateDocumentFromTemplateCommand(CreateDocumentCommand):
"""

def __init__(self, context, template_doc, title, recipient_data=tuple()):
data = getattr(template_doc.get_file(), "data", None)
super(CreateDocumentFromTemplateCommand, self).__init__(
context, template_doc.file.filename, template_doc.file.data,
context, template_doc.get_filename(), data,
title=title)
self.recipient_data = recipient_data

@@ -336,4 +336,5 @@ def is_available(self):
"""
return is_dossier_template_feature_enabled() and \
self.context.is_leaf_node() and \
api.user.has_permission('opengever.dossier: Add businesscasedossier', obj=self.context)
api.user.has_permission('opengever.dossier: Add businesscasedossier', obj=self.context) and \
self.context.allow_add_businesscase_dossier
@@ -211,6 +211,12 @@ class IDossierResolveProperties(Interface):
'should be added automatically to the dossier when it gets resolved.',
default=False)

tasks_pdf_enabled = schema.Bool(
title=u'Enable `tasks pdf` option.',
description=u'Select if a pdf representation of the tasks in the dossier '
'should be added automatically to the dossier when it gets resolved.',
default=False)

archival_file_conversion_enabled = schema.Bool(
title=u'Enable automatic archival file conversion with bumblebee.',
description=u'Select if GEVER should trigger the archival file '
@@ -222,3 +228,11 @@ class IDossierResolveProperties(Interface):
vocabulary=u'opengever.dossier.ValidResolverNamesVocabulary',
default='strict'
)


class IDossierTasksPdfMarker(Interface):
"""Marker Interface for dossier tasks list document."""


class IDossierJournalPdfMarker(Interface):
"""Marker Interface for dossier journal document."""
@@ -207,6 +207,10 @@ msgstr "Das Subdossier wurde erfolgreich abgeschlossen."
msgid "This subdossier can't be activated,because the main dossiers is inactive"
msgstr "Dieses Subdossier kann nicht wieder aktiviert werden, da das Hauptdossier storniert ist."

#: ./opengever/dossier/resolve.py
msgid "Updated with a newer generated version from dossier ${title}."
msgstr "Mit einer neuen Version des Dossiers ${title} aktualisiert."

#: ./opengever/dossier/archive.py
msgid "When the Action give filing number is selected, all fields are required."
msgstr "Für die Vergabe der Ablagenummer, werden alle Felder benötigt."
@@ -909,6 +913,11 @@ msgstr "abgelaufen"
msgid "title_dossier_journal"
msgstr "Dossier Journal ${title}, ${timestamp}"

#. Default: "Task list of dossier ${title}, ${timestamp}"
#: ./opengever/dossier/resolve.py
msgid "title_dossier_tasks"
msgstr "Aufgabenliste des Dossiers ${title}, ${timestamp}"

#. Default: "You didn't select any participants."
#: ./opengever/dossier/browser/forms.py
msgid "warning_no_participants_selected"
@@ -205,6 +205,10 @@ msgstr "Le sous-dossier a été clôturé avec succès."
msgid "This subdossier can't be activated,because the main dossiers is inactive"
msgstr "Ce sous-dossier ne peut pas être réactivé, parce que le dossier principal a été annulé."

#: ./opengever/dossier/resolve.py
msgid "Updated with a newer generated version from dossier ${title}."
msgstr "Actualisé par une nouvelle version du dossier ${title}."

#: ./opengever/dossier/archive.py
msgid "When the Action give filing number is selected, all fields are required."
msgstr "Pour l'attribution du numéro d'inventaire, tous les champs sont obligatoires."
@@ -907,6 +911,11 @@ msgstr "Périmé"
msgid "title_dossier_journal"
msgstr "Journal du dossier ${title}, ${timestamp}"

#. Default: "Task list of dossier ${title}, ${timestamp}"
#: ./opengever/dossier/resolve.py
msgid "title_dossier_tasks"
msgstr "Liste des tâches du dossier ${title}, ${timestamp}"

#. Default: "You didn't select any participants."
#: ./opengever/dossier/browser/forms.py
msgid "warning_no_participants_selected"
@@ -206,6 +206,10 @@ msgstr ""
msgid "This subdossier can't be activated,because the main dossiers is inactive"
msgstr ""

#: ./opengever/dossier/resolve.py
msgid "Updated with a newer generated version from dossier ${title}."
msgstr ""

#: ./opengever/dossier/archive.py
msgid "When the Action give filing number is selected, all fields are required."
msgstr ""
@@ -908,6 +912,11 @@ msgstr ""
msgid "title_dossier_journal"
msgstr ""

#. Default: "Task list of dossier ${title}, ${timestamp}"
#: ./opengever/dossier/resolve.py
msgid "title_dossier_tasks"
msgstr ""

#. Default: "You didn't select any participants."
#: ./opengever/dossier/browser/forms.py
msgid "warning_no_participants_selected"
@@ -3,20 +3,24 @@
from opengever.base.security import elevated_privileges
from opengever.document.archival_file import ArchivalFileConverter
from opengever.document.behaviors import IBaseDocument
from opengever.document.versioner import Versioner
from opengever.dossier import _
from opengever.dossier.base import DOSSIER_STATES_OPEN
from opengever.dossier.behaviors.dossier import IDossier
from opengever.dossier.behaviors.dossier import IDossierMarker
from opengever.dossier.behaviors.filing import IFilingNumberMarker
from opengever.dossier.interfaces import IDossierResolveProperties
from opengever.dossier.interfaces import IDossierResolver
from opengever.dossier.interfaces import IDossierJournalPdfMarker
from opengever.dossier.interfaces import IDossierTasksPdfMarker
from plone import api
from Products.CMFCore.utils import getToolByName
from Products.Five.browser import BrowserView
from zope.component import adapter
from zope.component import getAdapter
from zope.component import getSiteManager
from zope.i18n import translate
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.interface import implements
from zope.schema.interfaces import IVocabularyFactory
@@ -180,20 +184,23 @@ def after_resolve(self):
- Remove all trashed documents.
- (Trigger PDF-A conversion).
- Generate a PDF output of the journal.
- For a main dossier, Generate a PDF listing the tasks.
"""

self.trash_shadowed_docs()
self.purge_trash()
self.create_journal_pdf()
if not self.context.is_subdossier():
self.create_tasks_listing_pdf()
self.trigger_pdf_conversion()

def trash_shadowed_docs(self):
"""Trash all documents that are in shadow state (recursive).
"""
portal_catalog = api.portal.get_tool('portal_catalog')
query = {'path': {'query': self.context.absolute_url_path(), 'depth': -1},
'object_provides': [IBaseDocument.__identifier__],
'review_state': "document-state-shadow"}
'object_provides': [IBaseDocument.__identifier__],
'review_state': "document-state-shadow"}
shadowed_docs = portal_catalog.unrestrictedSearchResults(query)

if shadowed_docs:
@@ -244,12 +251,74 @@ def create_journal_pdf(self):
if dossier and dossier.end:
kwargs['document_date'] = dossier.end

results = api.content.find(object_provides=IDossierJournalPdfMarker,
depth=1,
context=self.context)

with elevated_privileges():
if len(results) > 0:
document = results[0].getObject()
document.title = translate(title, context=self.context.REQUEST)
comment = _(u'Updated with a newer generated version from dossier ${title}.',
mapping=dict(title=self.context.title))
document.update_file(view.get_data(), create_version=True,
comment=comment)
return

document = CreateDocumentCommand(
self.context, filename, view.get_data(),
title=translate(title, context=self.context.REQUEST),
content_type='application/pdf',
**kwargs).execute()
alsoProvides(document, IDossierJournalPdfMarker)

def create_tasks_listing_pdf(self):
"""Creates a pdf representation of the dossier tasks, and add it to
the dossier as a normal document.
If the dossiers has an end date use that date as the document date.
This prevents the dossier from entering an invalid state with a
document date outside the dossiers start-end range.
"""
if not self.get_property('tasks_pdf_enabled'):
return

view = self.context.unrestrictedTraverse('pdf-dossier-tasks')

today = api.portal.get_localized_time(
datetime=datetime.today(), long_format=True)
filename = u'Tasks {}.pdf'.format(today)
title = _(u'title_dossier_tasks',
default=u'Task list of dossier ${title}, ${timestamp}',
mapping={'title': self.context.title,
'timestamp': today})
kwargs = {
'preserved_as_paper': False,
}
dossier = IDossier(self.context)
if dossier and dossier.end:
kwargs['document_date'] = dossier.end

results = api.content.find(object_provides=IDossierTasksPdfMarker,
depth=1,
context=self.context)

with elevated_privileges():
CreateDocumentCommand(
self.context, filename, view.get_data(),
title=translate(title, context=self.context.REQUEST),
content_type='application/pdf',
**kwargs).execute()
if len(results) > 0:
document = results[0].getObject()
document.title = translate(title, context=self.context.REQUEST)
comment = _(u'Updated with a newer generated version from dossier ${title}.',
mapping=dict(title=self.context.title))
document.update_file(view.get_data(), create_version=True,
comment=comment)
return

document = CreateDocumentCommand(
self.context, filename, view.get_data(),
title=translate(title, context=self.context.REQUEST),
content_type='application/pdf',
**kwargs).execute()
alsoProvides(document, IDossierTasksPdfMarker)

def trigger_pdf_conversion(self):
if not self.get_property('archival_file_conversion_enabled'):
@@ -352,9 +421,9 @@ def check_preconditions(self):

errors = []

if (self.strict and
not self.context.is_all_supplied() and
not self.context.is_subdossier()):
if (self.strict
and not self.context.is_all_supplied()
and not self.context.is_subdossier()):
errors.append(NOT_SUPPLIED_OBJECTS)
if not self.context.is_all_checked_in():
errors.append(NOT_CHECKED_IN_DOCS)
@@ -32,18 +32,25 @@

@provider(IContextSourceBinder)
def get_templates(context):
template_folder = get_template_folder()
"""We only want to document templates that are directly contained
in a templatefolder, and not those contained in dossiertemplate
"""
results = api.content.find(portal_type="opengever.dossier.templatefolder")
template_folders = [brain.getObject() for brain in results]

if template_folder is None:
if not template_folders:
# this may happen when the user does not have permissions to
# view templates and/or during ++widget++ traversal
return SimpleVocabulary([])

templates = api.content.find(
context=template_folder,
depth=-1,
portal_type="opengever.document.document",
sort_on='sortable_title', sort_order='ascending')
templates = []
for template_folder in template_folders:
templates.extend(api.content.find(
context=template_folder,
depth=1,
portal_type="opengever.document.document",
sort_on='sortable_title', sort_order='ascending'))
templates.sort(key=lambda template: template.Title.lower())

intids = getUtility(IIntIds)
terms = []
@@ -0,0 +1,38 @@
from ftw.builder import Builder
from ftw.builder import create
from opengever.testing import IntegrationTestCase
from opengever.dossier.behaviors.dossier import IDossierMarker
from opengever.dossier.command import CreateDossierFromTemplateCommand
from opengever.dossier.command import CreateDocumentFromTemplateCommand


class TestCreateDocumentFromTemplateCommand(IntegrationTestCase):

def test_create_document_from_template(self):
expected_title = 'My title'
expected_data = 'Test data'
self.login(self.regular_user)
template = create(Builder('document').within(self.dossiertemplate).with_dummy_content())
command = CreateDocumentFromTemplateCommand(self.dossier, template, expected_title)
document = command.execute()
self.assertEqual(expected_title, document.title)
self.assertEqual(expected_data, document.file.data)

def test_create_document_from_template_without_file(self):
expected_title = 'My title'
self.login(self.regular_user)
template = create(Builder('document').within(self.dossiertemplate))
command = CreateDocumentFromTemplateCommand(self.dossier, template, expected_title)
document = command.execute()
self.assertEqual(expected_title, document.title)
self.assertIsNone(document.file)


class TestCreateDossierFromTemplateCommand(IntegrationTestCase):

def test_create_dossier_from_template(self):
self.login(self.regular_user)
command = CreateDossierFromTemplateCommand(self.dossier, self.dossiertemplate)
dossier = command.execute()
self.assertEqual(self.dossiertemplate.title, dossier.title)
self.assertTrue(IDossierMarker.providedBy(dossier))
@@ -197,7 +197,102 @@ def test_sets_journal_pdf_document_date_to_dossier_end_date(self, browser):
"Document date should be earliest possible date")

@browsing
def test_journal_pdf_is_disabled_by_default(self, browser):
def test_journal_pdf_gets_updated_when_dossier_is_closed_again(self, browser):
self.activate_feature('journal-pdf')
self.login(self.secretariat_user, browser)

with self.observe_children(self.empty_dossier) as children:
resolve_dossier(self.empty_dossier, browser)
self.assertEquals(1, len(children['added']))
journal_pdf, = children['added']
journal_pdf.reindexObject()
self.assertEquals(0, journal_pdf.get_current_version_id(missing_as_zero=True))

browser.open(self.empty_dossier, view='transition-reactivate', data={'_authenticator': createToken()})
with self.observe_children(self.empty_dossier) as children:
resolve_dossier(self.empty_dossier, browser)
self.assertEquals(0, len(children['added']))
self.assertEquals(1, journal_pdf.get_current_version_id(missing_as_zero=True))

@browsing
def test_adds_tasks_pdf_only_to_main_dossier(self, browser):
self.activate_feature('tasks-pdf')
self.login(self.secretariat_user, browser)

subdossier = create(Builder('dossier')
.within(self.empty_dossier)
.titled(u'Sub'))

with self.observe_children(self.empty_dossier) as main_children:
with self.observe_children(subdossier) as sub_children:
with freeze(datetime(2016, 4, 25)):
resolve_dossier(self.empty_dossier, browser)

self.assertEquals(1, len(main_children['added']))
main_tasks_pdf, = main_children['added']
self.assertEquals(u'Task list of dossier An empty dossier, Apr 25, 2016 12:00 AM',
main_tasks_pdf.title)
self.assertEquals(u'Task list of dossier An empty dossier, Apr 25, 2016 12 00 AM.pdf',
main_tasks_pdf.file.filename)
self.assertEquals(u'application/pdf',
main_tasks_pdf.file.contentType)
self.assertFalse(main_tasks_pdf.preserved_as_paper)

self.assertEquals(0, len(sub_children['added']))

@browsing
def test_sets_tasks_pdf_document_date_to_dossier_end_date(self, browser):
"""When the document date is not set to the dossiers end date the
subdossier will be left in an inconsistent state. this will make
resolving the main dossier impossible.
"""
self.activate_feature('tasks-pdf')
self.login(self.secretariat_user, browser)

subdossier = create(Builder('dossier')
.within(self.empty_dossier)
.having(
start=date(2016, 1, 1),
end=date(2016, 3, 15))
.titled(u'Sub'))

with self.observe_children(subdossier) as sub_children:
with freeze(datetime(2016, 4, 25)):
resolve_dossier(subdossier, browser)

self.assertEquals(0, len(sub_children['added']))

with self.observe_children(self.empty_dossier) as main_children:
with freeze(datetime(2016, 9, 1)):
resolve_dossier(self.empty_dossier, browser)

self.assertEquals(1, len(main_children['added']))
main_tasks_pdf, = main_children['added']
self.assertEqual(date(2016, 3, 15), IDossier(self.empty_dossier).end,
"End should be earliest possible date")
self.assertEqual(date(2016, 3, 15), main_tasks_pdf.document_date,
"Document date should be earliest possible date")

@browsing
def test_tasks_pdf_gets_updated_when_dossier_is_closed_again(self, browser):
self.activate_feature('tasks-pdf')
self.login(self.secretariat_user, browser)

with self.observe_children(self.empty_dossier) as children:
resolve_dossier(self.empty_dossier, browser)
self.assertEquals(1, len(children['added']))
tasks_pdf, = children['added']
tasks_pdf.reindexObject()
self.assertEquals(0, tasks_pdf.get_current_version_id(missing_as_zero=True))

browser.open(self.empty_dossier, view='transition-reactivate', data={'_authenticator': createToken()})
with self.observe_children(self.empty_dossier) as children:
resolve_dossier(self.empty_dossier, browser)
self.assertEquals(0, len(children['added']))
self.assertEquals(1, tasks_pdf.get_current_version_id(missing_as_zero=True))

@browsing
def test_tasks_and_journal_pdf_are_disabled_by_default(self, browser):
self.login(self.secretariat_user, browser)

with self.observe_children(self.empty_dossier) as children:
@@ -50,6 +50,24 @@ def test_form_lists_all_templates_alphabetically_including_nested(self, browser)

self.assertEquals(expected_listing, browser.css('table.listing').first.dicts())

@browsing
def test_form_does_not_list_templates_from_dossiertemplates(self, browser):
self.login(self.regular_user, browser)
create(Builder('document')
.titled(u'T\xc3\xb6mpl\xc3\xb6te in dossiertemplate')
.with_dummy_content()
.within(self.dossiertemplate))

browser.open(self.dossier, view='document_with_template')
expected_listing = [
{'': '', 'Creator': 'nicole.kohler', 'Modified': '28.12.2010', 'Title': u'T\xc3\xb6mpl\xc3\xb6te Mit'},
{'': '', 'Creator': 'nicole.kohler', 'Modified': '31.08.2016', 'Title': u'T\xc3\xb6mpl\xc3\xb6te Normal'},
{'': '', 'Creator': 'nicole.kohler', 'Modified': '31.08.2016', 'Title': u'T\xc3\xb6mpl\xc3\xb6te Ohne'},
{'': '', 'Creator': 'nicole.kohler', 'Modified': '29.02.2020', 'Title': u'T\xc3\xb6mpl\xc3\xb6te Sub'},
]

self.assertEquals(expected_listing, browser.css('table.listing').first.dicts())

@browsing
def test_form_does_not_inlcude_participants_with_disabled_feature(self, browser):
self.login(self.regular_user, browser)
@@ -196,15 +214,15 @@ def test_properties_are_added_when_created_from_template_with_doc_properties(sel
self.assertEquals(u'Test Docx.docx', document.file.filename)

expected_doc_properties = {
'Document.ReferenceNumber': 'Client1 1.1 / 1 / 35',
'Document.SequenceNumber': '35',
'Document.ReferenceNumber': 'Client1 1.1 / 1 / 37',
'Document.SequenceNumber': '37',
'Dossier.ReferenceNumber': 'Client1 1.1 / 1',
'Dossier.Title': u'Vertr\xe4ge mit der kantonalen Finanzverwaltung',
'User.FullName': u'B\xe4rfuss K\xe4thi',
'User.ID': 'kathi.barfuss',
'ogg.document.document_date': datetime(2020, 9, 28, 0, 0),
'ogg.document.reference_number': 'Client1 1.1 / 1 / 35',
'ogg.document.sequence_number': '35',
'ogg.document.reference_number': 'Client1 1.1 / 1 / 37',
'ogg.document.sequence_number': '37',
'ogg.document.title': 'Test Docx',
'ogg.document.version_number': 0,
'ogg.dossier.reference_number': 'Client1 1.1 / 1',
@@ -254,15 +272,15 @@ def test_properties_are_added_when_created_from_template_without_doc_properties(
self.assertEquals(u'Test Docx.docx', document.file.filename)

expected_doc_properties = {
'Document.ReferenceNumber': 'Client1 1.1 / 1 / 35',
'Document.SequenceNumber': '35',
'Document.ReferenceNumber': 'Client1 1.1 / 1 / 37',
'Document.SequenceNumber': '37',
'Dossier.ReferenceNumber': 'Client1 1.1 / 1',
'Dossier.Title': u'Vertr\xe4ge mit der kantonalen Finanzverwaltung',
'User.FullName': u'B\xe4rfuss K\xe4thi',
'User.ID': 'kathi.barfuss',
'ogg.document.document_date': datetime(2020, 10, 28, 0, 0),
'ogg.document.reference_number': 'Client1 1.1 / 1 / 35',
'ogg.document.sequence_number': '35',
'ogg.document.reference_number': 'Client1 1.1 / 1 / 37',
'ogg.document.sequence_number': '37',
'ogg.document.title': 'Test Docx',
'ogg.document.version_number': 0,
'ogg.dossier.reference_number': 'Client1 1.1 / 1',
@@ -771,6 +789,8 @@ def test_receipt_delivery_and_subdossier_column_are_hidden_in_document_tab(self,
'Title',
'Document Author',
'Document Date',
'Modification Date',
'Creation Date',
'Checked out by',
'Public Trial',
'Reference Number',
@@ -789,6 +809,8 @@ def test_receipt_delivery_and_subdossier_column_are_hidden_in_sablon_template_ta
'Title',
'Document Author',
'Document Date',
'Modification Date',
'Creation Date',
'Checked out by',
'Public Trial',
'Reference Number',
@@ -807,6 +829,8 @@ def test_receipt_delivery_and_subdossier_column_are_hidden_in_proposal_templates
'Title',
'Document Author',
'Document Date',
'Modification Date',
'Creation Date',
'Checked out by',
'Public Trial',
'Reference Number',
@@ -73,6 +73,7 @@ def test_adding_to_zip(self):
u'files/dossier-1/Vertraegsentwurf.docx',
u'files/dossier-1/Die Buergschaft.eml',
u'files/dossier-1/testm\xe4il.msg',
u'files/dossier-1/Initialvertrag fuer Bearbeitung.docx',
u'files/Vertraegsentwurf.docx'],
zipfile.arcnames)

@@ -1,14 +1,22 @@
from opengever.base.model import is_oracle
from opengever.base.oguid import Oguid
from opengever.globalindex.model.task import Task
from opengever.ogds.base.utils import get_current_admin_unit
from opengever.ogds.models.query import BaseQuery
from plone import api
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy import or_


class TaskQuery(BaseQuery):

def _extend_with_physical_path(self, query, field, path):
if is_oracle():
return query.filter(func.to_char(field) == path)

return query.filter(field == path)

def users_tasks(self, userid):
"""Returns query which List all tasks where the given user,
his userid, is responsible. It queries all admin units.
@@ -58,8 +66,9 @@ def by_path(self, path, admin_unit_id):
"""Returns a task on the specified client identified by its physical
path (which is relative to the site root!) or None.
"""
return self.filter_by(
admin_unit_id=admin_unit_id, physical_path=path).first()
query = self.filter_by(admin_unit_id=admin_unit_id)
return self._extend_with_physical_path(
query, Task.physical_path, path).first()

def by_ids(self, task_ids):
"""Returns all tasks whos task_ids are listed in `task_ids`.
@@ -76,8 +85,9 @@ def by_container(self, container, admin_unit):

def by_brain(self, brain):
relative_content_path = '/'.join(brain.getPath().split('/')[2:])
return self.by_admin_unit(get_current_admin_unit())\
.filter(Task.physical_path == relative_content_path).one()
query = self.by_admin_unit(get_current_admin_unit())
return self._extend_with_physical_path(
query, Task.physical_path, relative_content_path).one()

def subtasks_by_tasks(self, tasks):
"""Queries all subtask of the given tasks sql object."""
@@ -64,7 +64,7 @@ def add_response(self, response_text):
add_simple_response(
self.context,
text=response_text,
transition=u'forwarding-transition-refuse')
transition=u'forwarding-transition-refuse', supress_events=True)

def change_workflow_sate(self):
wf_tool = getToolByName(self.context, 'portal_workflow')
@@ -3,7 +3,6 @@
from datetime import datetime
from ftw.keywordwidget.widget import KeywordFieldWidget
from opengever.inbox import _
from opengever.inbox.activities import ForwardingAddedActivity
from opengever.ogds.base.sources import AllUsersInboxesAndTeamsSourceBinder
from opengever.ogds.base.utils import get_current_org_unit
from opengever.ogds.base.utils import get_ou_selector
@@ -174,15 +173,7 @@ def updateFieldsFromSchemata(self):
def createAndAdd(self, data):
update_reponsible_field_data(data)

forwarding = super(ForwardingAddForm, self).createAndAdd(data=data)

ForwardingAddedActivity(
forwarding,
self.request,
self.context,
).record()

return forwarding
return super(ForwardingAddForm, self).createAndAdd(data=data)


class ForwardingAddView(add.DefaultAddView):
@@ -79,8 +79,6 @@ def test_accepting_forwarding_with_successor_updated_responsibles(self, browser)
.having(responsible=TEST_USER_ID,
issuer='hugo.boss')
.within(inbox))
self.center.add_task_responsible(forwarding, TEST_USER_ID)
self.center.add_task_issuer(forwarding, 'hugo.boss')

successor = accept_forwarding_with_successor(
self.portal, forwarding.oguid.id,
@@ -97,8 +95,7 @@ def test_accepting_forwarding_with_successor_updated_responsibles(self, browser)
self.assertItemsEqual(
[(u'test_user_1_', u'task_responsible'),
(u'inbox:org-unit-1', u'task_issuer')],
[(subscription.watcher.actorid, subscription.role)
for subscription in successor_resource.subscriptions])
[(subscription.watcher.actorid, subscription.role) for subscription in successor_resource.subscriptions])

@browsing
def test_accepting_and_assign_forwarding_with_successor_and__updated_responsibles(self, browser):
@@ -7,6 +7,7 @@
from opengever.task.adapters import IResponseContainer
from opengever.testing import FunctionalTestCase
from Products.CMFCore.utils import getToolByName
from sqlalchemy import desc


class TestAssingForwarding(FunctionalTestCase):
@@ -83,9 +84,9 @@ def test_activity_is_recorded_correctly(self, browser):
self.assertEquals('inbox:org-unit-2',
self.forwarding.get_sql_object().responsible)

self.assertEquals(1, len(Activity.query.all()))
self.assertEquals('forwarding-transition-reassign-refused',
Activity.query.all()[0].kind)
self.assertEquals(
'forwarding-transition-reassign-refused',
Activity.query.order_by(desc(Activity.id)).first().kind)

def assign_forwarding(self, new_client, response, browser=default_browser):
browser.login().open(self.forwarding)
@@ -17,7 +17,8 @@ def test_trash_listing_does_not_contain_subdossier_and_checked_out_column(self,

self.assertEquals(
['', 'Sequence Number', 'Title', 'Document Author',
'Document Date', 'Receipt Date', 'Delivery Date', 'Public Trial',
'Document Date', 'Modification Date', 'Creation Date',
'Receipt Date', 'Delivery Date', 'Public Trial',
'Reference Number'],
browser.css('.listing th').text)

@@ -29,8 +30,8 @@ def test_document_listings_does_not_contain_subdossier_checked_out_and_reference

self.assertEquals(
['', 'Sequence Number', 'Title', 'Document Author',
'Document Date', 'Receipt Date', 'Delivery Date',
'Public Trial'],
'Document Date', 'Modification Date', 'Creation Date',
'Receipt Date', 'Delivery Date', 'Public Trial'],
browser.css('.listing th').text)

@browsing
@@ -40,6 +40,11 @@
name="tasks"
/>

<adapter
factory=".listing.TaskHistoryLaTeXListing"
name="task-history"
/>

<adapter
factory=".listing.JournalLaTeXListing"
name="journal"
@@ -81,6 +86,18 @@
permission="zope2.View"
/>

<adapter
factory=".dossiertasks.DossierTasksLaTeXView"
provides="ftw.pdfgenerator.interfaces.ILaTeXView"
/>

<browser:page
for="*"
name="pdf-dossier-tasks"
class=".dossiertasks.DossierTasksPDFView"
permission="zope2.View"
/>

<adapter
factory=".dossierlisting.DossierListingLaTeXView"
provides="ftw.pdfgenerator.interfaces.ILaTeXView"
@@ -264,7 +264,7 @@ def get_sorting(self, tab_name):
return sort_on, sort_order

def convert_list(self, items):
"""Returns a new list, containing all values in `items` convertend
"""Returns a new list, containing all values in `items` converted
into LaTeX.
"""
data = []
@@ -1,3 +1,4 @@
from contextlib import contextmanager
from copy import deepcopy
from ftw.journal.config import JOURNAL_ENTRIES_ANNOTATIONS_KEY
from ftw.journal.interfaces import IAnnotationsJournalizable
@@ -14,6 +15,7 @@
from zope.component import getMultiAdapter
from zope.i18n import translate
from zope.interface import Interface
from zope.interface import noLongerProvides


class IDossierJournalLayer(ILandscapeLayer):
@@ -23,24 +25,29 @@ class IDossierJournalLayer(ILandscapeLayer):
"""


class DossierJournalPDFView(ExportPDFView):
@contextmanager
def provide_dossier_journal_layer(request):
try:
provide_request_layer(request, IDossierJournalLayer)
yield
finally:
noLongerProvides(request, IDossierJournalLayer)


request_layer = IDossierJournalLayer
class DossierJournalPDFView(ExportPDFView):

def __call__(self):
# use the landscape layout
# let the request provide IDossierJournalLayer
provide_request_layer(self.request, self.request_layer)

return super(DossierJournalPDFView, self).__call__()
with provide_dossier_journal_layer(self.request):
return super(DossierJournalPDFView, self).__call__()

def get_data(self):
# let the request provide IDossierJournalLayer
provide_request_layer(self.request, self.request_layer)

assembler = getMultiAdapter((self.context, self.request),
IPDFAssembler)
return assembler.build_pdf()
with provide_dossier_journal_layer(self.request):
assembler = getMultiAdapter((self.context, self.request),
IPDFAssembler)
return assembler.build_pdf()


@adapter(Interface, IDossierJournalLayer, ILaTeXLayout)
@@ -0,0 +1,97 @@
from contextlib import contextmanager
from ftw.pdfgenerator.browser.views import ExportPDFView
from ftw.pdfgenerator.interfaces import ILaTeXLayout
from ftw.pdfgenerator.interfaces import IPDFAssembler
from ftw.pdfgenerator.utils import provide_request_layer
from ftw.pdfgenerator.view import MakoLaTeXView
from opengever.latex import _
from opengever.latex.listing import ILaTexListing
from opengever.task.adapters import IResponseContainer
from opengever.task.task import ITask
from plone import api
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.i18n import translate
from zope.interface import Interface
from zope.interface import noLongerProvides


class IDossierTasksLayer(Interface):
"""Request layer for selecting the dossier tasks view.
"""


@contextmanager
def provide_dossier_task_layer(request):
try:
provide_request_layer(request, IDossierTasksLayer)
yield
finally:
noLongerProvides(request, IDossierTasksLayer)


class DossierTasksPDFView(ExportPDFView):

def __call__(self):
# Enable IDossierTasksLayer
with provide_dossier_task_layer(self.request):
return super(DossierTasksPDFView, self).__call__()

def get_data(self):
# let the request provide IDossierTasksLayer
with provide_dossier_task_layer(self.request):
assembler = getMultiAdapter((self.context, self.request),
IPDFAssembler)
return assembler.build_pdf()


@adapter(Interface, IDossierTasksLayer, ILaTeXLayout)
class DossierTasksLaTeXView(MakoLaTeXView):

template_directories = ['templates']
template_name = 'dossiertasks.tex'
strftimestring = '%d.%m.%Y %H:%M'

def get_render_arguments(self):
self.layout.show_organisation = True

tasks = self.get_tasks()
task_data_list = []
title = translate(_('label_dossier_tasks',
default=u'Task list for dossier ${title} (${reference_number})',
mapping={'title': self.context.title,
'reference_number': self.context.get_reference_number()}),
context=self.request)

for task in tasks:
task_history = getMultiAdapter((task, self.request, self),
ILaTexListing, name='task-history')

response_container = IResponseContainer(task)

completion_date = task.date_of_completion
if completion_date:
completion_date = completion_date.strftime(self.strftimestring)

deadline = task.deadline
if deadline:
deadline = deadline.strftime(self.strftimestring)

task_data_list.append({'title': task.title,
'description': task.text,
'sequence_number': task.get_sequence_number(),
'type': task.get_task_type_label(),
'completion_date': completion_date,
'deadline': deadline,
'responsible': task.responsible,
'responsible_client': task.responsible_client,
'history': task_history.get_listing(response_container)})

return {'task_data_list': task_data_list,
'label': title}

def get_tasks(self):
task_brains = api.content.find(context=self.context, object_provides=ITask)
tasks = [el.getObject() for el in task_brains]
tasks.sort(key=lambda task: task.get_sequence_number())
return tasks
@@ -1,5 +1,6 @@
from collections import OrderedDict
from ftw.table import helper
from opengever.activity import _ as activity_mf
from opengever.journal import _ as journal_mf
from opengever.journal.handlers import DOCUMENT_SENT
from opengever.journal.tab import title_helper
@@ -40,14 +41,19 @@ def get_value(self, item):

return value

def get_width(self):
if not self.width.endswith("%"):
raise NotImplementedError("Only implemented for width in percentage")
return float(self.width.split("%")[0])


class ILaTexListing(Interface):

def get_labels():
""""Returns a LaTEx string with the labels of the listing"""

def get_widths():
""""Returns a LaTEx string with the labels of the listing,
""""Returns a LaTEx string with the widths of the listing,
which are calculated."""

def get_rows():
@@ -86,20 +92,20 @@ def update_column_dict(self, columns):
return columns

def get_widths(self):
""""Returns a LaTEx string with the labels of the listing,
which are calculated."""
""""Returns a list of the column widths as strings"""
return [col.width for col in self.columns.values()]

def get_labels(self):
""""Returns a LaTEx string with the labels of the listing"""
""""Returns a list of the column labels of the listing"""
return [col.label for col in self.columns.values()]

def get_rows(self):
""""Returns a LaTEx string with all the rows of the listing"""
""""Returns a list of the data for each row of the listing"""
return [self.get_row_for_item(item) for item in self.items]

def get_listing(self, items=[]):
self.items += items
def get_listing(self, items=None):
if items is not None:
self.items += items

if len(self.items) == 0:
return None
@@ -133,6 +139,16 @@ def get_repository_title(self, brain):

return obj.Title()

@staticmethod
def reset_column_widths(columns):
"""recalculates the column widths so that they span the whole textwidth.
"""
total_width = reduce(lambda total_width, column: total_width + column.get_width(),
columns.values(), 0)
for column in columns.values():
column.width = str(column.get_width() / total_width * 100) + "%"
return columns


@implementer(ILaTexListing)
@adapter(Interface, Interface, Interface)
@@ -192,6 +208,7 @@ class SubDossiersLaTeXListing(DossiersLaTeXListing):
def update_column_dict(self, columns):
del columns['reference']
del columns['repository_title']
self.reset_column_widths(columns)
return columns


@@ -273,6 +290,32 @@ def get_columns(self):
]


@implementer(ILaTexListing)
@adapter(Interface, Interface, Interface)
class TaskHistoryLaTeXListing(LaTexListing):

def get_columns(self):
return [
Column('date',
_('label_time', default=u'Time'),
'10%',
lambda item: helper.readable_date_time(item, item.date)),

Column('creator',
_('label_creator', default='Changed by'),
'20%'),

Column('transition',
_('label_transition', default='Transition'),
'20%',
lambda item: activity_mf(item.transition)),

Column('text',
_('label_description', default='Description'),
'50%'),
]


@implementer(ILaTexListing)
@adapter(Interface, Interface, Interface)
class JournalLaTeXListing(LaTexListing):
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2017-10-02 12:51+0000\n"
"POT-Creation-Date: 2018-07-30 13:38+0000\n"
"PO-Revision-Date: 2013-11-19 16:25+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -14,6 +14,11 @@ msgstr ""
"Preferred-Encodings: utf-8 latin1\n"
"Domain: DOMAIN\n"

#. Default: "Changed by"
#: ./opengever/latex/listing.py
msgid "label_creator"
msgstr "Geändert von"

#. Default: "Deadline"
#: ./opengever/latex/listing.py
msgid "label_deadline"
@@ -24,6 +29,11 @@ msgstr "Zu erledigen bis"
msgid "label_delivery_date"
msgstr "Ausgangsdatum"

#. Default: "Description"
#: ./opengever/latex/listing.py
msgid "label_description"
msgstr "Beschreibung"

#. Default: "Document author"
#: ./opengever/latex/listing.py
msgid "label_document_author"
@@ -44,6 +54,11 @@ msgstr "Dokumente"
msgid "label_dossier_journal"
msgstr "Journal zu Dossier ${title} (${reference_number})"

#. Default: "Task list for dossier ${title} (${reference_number})"
#: ./opengever/latex/dossiertasks.py
msgid "label_dossier_tasks"
msgstr "Aufgabenliste zu Dossier ${title} (${reference_number})"

#. Default: "End"
#: ./opengever/latex/dossierdetails.py
#: ./opengever/latex/listing.py
@@ -129,14 +144,23 @@ msgstr "Art"
msgid "label_tasks"
msgstr "Aufgaben"

#. Default: "Time"
#: ./opengever/latex/listing.py
msgid "label_time"
msgstr "Zeitpunkt"

#. Default: "Title"
#: ./opengever/latex/dossierdetails.py
#: ./opengever/latex/listing.py
msgid "label_title"
msgstr "Titel"

#. Default: "Transition"
#: ./opengever/latex/listing.py
msgid "label_transition"
msgstr "Aktion"

#. Default: "No."
#: ./opengever/latex/listing.py
msgid "short_label_sequence_number"
msgstr "Nr."

@@ -1,21 +1,25 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2017-10-02 12:51+0000\n"
"POT-Creation-Date: 2018-07-30 13:38+0000\n"
"PO-Revision-Date: 2017-12-03 09:48+0000\n"
"Last-Translator: Jacqueline Sposato <jacqueline.sposato@gmail.com>\n"
"Language-Team: French <https://translations.onegovgever.ch/projects/onegov-"
"gever/opengever-latex/fr/>\n"
"Language: fr\n"
"Language-Team: French <https://translations.onegovgever.ch/projects/onegov-gever/opengever-latex/fr/>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 2.13.1\n"
"Language-Code: en\n"
"Language-Name: English\n"
"Preferred-Encodings: utf-8 latin1\n"
"Domain: DOMAIN\n"
"Language: fr\n"
"X-Generator: Weblate 2.13.1\n"

#. Default: "Changed by"
#: ./opengever/latex/listing.py
msgid "label_creator"
msgstr "Modifié par"

#. Default: "Deadline"
#: ./opengever/latex/listing.py
@@ -27,6 +31,11 @@ msgstr "A réaliser jusqu'au"
msgid "label_delivery_date"
msgstr "Date de remise"

#. Default: "Description"
#: ./opengever/latex/listing.py
msgid "label_description"
msgstr "Description"

#. Default: "Document author"
#: ./opengever/latex/listing.py
msgid "label_document_author"
@@ -47,6 +56,11 @@ msgstr "Documents"
msgid "label_dossier_journal"
msgstr "Journal du dossier ${title} (${reference_number})"

#. Default: "Task list for dossier ${title} (${reference_number})"
#: ./opengever/latex/dossiertasks.py
msgid "label_dossier_tasks"
msgstr "Liste des tâches du dossier ${title} (${reference_number})"

#. Default: "End"
#: ./opengever/latex/dossierdetails.py
#: ./opengever/latex/listing.py
@@ -132,12 +146,22 @@ msgstr "Type de mandat"
msgid "label_tasks"
msgstr "Tâches"

#. Default: "Time"
#: ./opengever/latex/listing.py
msgid "label_time"
msgstr "Date"

#. Default: "Title"
#: ./opengever/latex/dossierdetails.py
#: ./opengever/latex/listing.py
msgid "label_title"
msgstr "Titre"

#. Default: "Transition"
#: ./opengever/latex/listing.py
msgid "label_transition"
msgstr "Action"

#. Default: "No."
#: ./opengever/latex/listing.py
msgid "short_label_sequence_number"
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2017-10-02 12:51+0000\n"
"POT-Creation-Date: 2018-07-30 13:38+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,6 +17,11 @@ msgstr ""
"Preferred-Encodings: utf-8 latin1\n"
"Domain: opengever.latex\n"

#. Default: "Changed by"
#: ./opengever/latex/listing.py
msgid "label_creator"
msgstr ""

#. Default: "Deadline"
#: ./opengever/latex/listing.py
msgid "label_deadline"
@@ -27,6 +32,11 @@ msgstr ""
msgid "label_delivery_date"
msgstr ""

#. Default: "Description"
#: ./opengever/latex/listing.py
msgid "label_description"
msgstr ""

#. Default: "Document author"
#: ./opengever/latex/listing.py
msgid "label_document_author"
@@ -47,6 +57,11 @@ msgstr ""
msgid "label_dossier_journal"
msgstr ""

#. Default: "Task list for dossier ${title} (${reference_number})"
#: ./opengever/latex/dossiertasks.py
msgid "label_dossier_tasks"
msgstr ""

#. Default: "End"
#: ./opengever/latex/dossierdetails.py
#: ./opengever/latex/listing.py
@@ -132,14 +147,23 @@ msgstr ""
msgid "label_tasks"
msgstr ""

#. Default: "Time"
#: ./opengever/latex/listing.py
msgid "label_time"
msgstr ""

#. Default: "Title"
#: ./opengever/latex/dossierdetails.py
#: ./opengever/latex/listing.py
msgid "label_title"
msgstr ""

#. Default: "Transition"
#: ./opengever/latex/listing.py
msgid "label_transition"
msgstr ""

#. Default: "No."
#: ./opengever/latex/listing.py
msgid "short_label_sequence_number"
msgstr ""

@@ -0,0 +1,18 @@
\section*{${label}}\\%%
\vspace{\baselineskip}
%for task_data in task_data_list:
\vspace{2\baselineskip}
\normalsize
{\bf Title: ${task_data.get("title")} }\\%%
{\bf Beschreibung}: ${task_data.get("description")}\\%%
{\bf Auftragstyp}: ${task_data.get("type")}\\%%
{\bf Frist}: ${task_data.get("deadline")}\\%%
{\bf Auftraggeber}: ${task_data.get("responsible")}\\%%
{\bf Auftragnehmer}: ${task_data.get("responsible_client")}\\%%
{\bf Erledigungsdatum}: ${task_data.get("completion_date")}\\%%
\small
\vspace{\baselineskip}
${task_data.get("history")}
%endfor
@@ -30,21 +30,6 @@ def test_is_registered(self):
view = getMultiAdapter((context, request), name='pdf-dossier-journal')
self.assertTrue(isinstance(view, dossierjournal.DossierJournalPDFView))

def test_render_adds_browser_layer(self):
context = request = self.create_dummy()

view = self.mocker.patch(
dossierjournal.DossierJournalPDFView(context, request))

self.expect(view.allow_alternate_output()).result(False)
self.expect(view.export())

self.replay()

view()
self.assertTrue(
dossierjournal.IDossierJournalLayer.providedBy(request))


class TestJournalListingLaTeXView(FunctionalTestCase):

@@ -0,0 +1,140 @@
from datetime import datetime
from datetime import date
from ftw.builder import Builder
from ftw.builder import create
from ftw.pdfgenerator.builder import Builder as PDFBuilder
from ftw.pdfgenerator.interfaces import ILaTeXView
from ftw.pdfgenerator.utils import provide_request_layer
from ftw.testbrowser import browsing
from ftw.testbrowser.pages import factoriesmenu
from ftw.testing import freeze
from ftw.testing import MockTestCase
from opengever.latex import dossiertasks
from opengever.latex.dossiertasks import provide_dossier_task_layer
from opengever.latex.layouts.default import DefaultLayout
from opengever.latex.testing import LATEX_ZCML_LAYER
from opengever.testing import FunctionalTestCase
from plone import api
from plone.app.testing import TEST_USER_ID
from zope.component import getMultiAdapter
from zope.publisher.interfaces.browser import IDefaultBrowserLayer


class TestDossierTasksPDFView(MockTestCase):

layer = LATEX_ZCML_LAYER

def test_is_registered(self):
context = self.create_dummy()
request = self.providing_stub([IDefaultBrowserLayer])

self.replay()
view = getMultiAdapter((context, request), name='pdf-dossier-tasks')
self.assertTrue(isinstance(view, dossiertasks.DossierTasksPDFView))


class TestDossierTasksLaTeXView(FunctionalTestCase):

@browsing
def test_dossier_tasks_label(self, browser):
dossier = create(Builder('dossier').titled(u'Anfr\xf6gen 2015'))

with provide_dossier_task_layer(dossier.REQUEST):
layout = DefaultLayout(dossier, dossier.REQUEST, PDFBuilder())
dossier_tasks = getMultiAdapter(
(dossier, dossier.REQUEST, layout), ILaTeXView)

self.assertEquals(
u'Task list for dossier Anfr\xf6gen 2015 (Client1 / 1)',
dossier_tasks.get_render_arguments().get('label'))

@browsing
def test_dossier_tasks_data(self, browser):
repository = create(Builder('repository_root')
.titled(u'Repository'))
dossier = create(Builder('dossier')
.titled(u'Anfr\xf6gen 2015')
.within(repository)
.having(responsible=self.user.userid))

subdossier = create(Builder('dossier')
.within(dossier)
.titled(u'Subdossier'))

with freeze(datetime(2016, 4, 12, 10, 35)):
task1 = create(Builder('task')
.within(dossier)
.having(responsible=self.user.userid,
responsible_client=self.org_unit.id(),
title="task 1"))

task2 = create(Builder('task')
.within(subdossier)
.having(responsible=self.user.userid,
responsible_client=self.org_unit.id(),
title="task 2"))

expected_deadline = datetime(2016, 4, 17, 0, 0)

with freeze(datetime(2016, 10, 12, 13, 20)):
api.content.transition(task1, to_state='task-state-resolved')
completion_date = date.today()

with provide_dossier_task_layer(dossier.REQUEST):
layout = DefaultLayout(dossier, dossier.REQUEST, PDFBuilder())
dossiertasks = getMultiAdapter((dossier, dossier.REQUEST, layout),
ILaTeXView)
self.assertEquals([task1, task2], dossiertasks.get_tasks())

expected = {'label': u'Task list for dossier Anfr\xf6gen 2015 (Client1 / 1)',
'task_data_list': [{'completion_date': completion_date.strftime('%d.%m.%Y %H:%M'),
'deadline': expected_deadline.strftime('%d.%m.%Y %H:%M'),
'description': None,
'history': None,
'responsible': u'test_user_1_',
'responsible_client': u'org-unit-1',
'sequence_number': 1,
'title': 'task 1',
'type': ''},
{'completion_date': None,
'deadline': expected_deadline.strftime('%d.%m.%Y %H:%M'),
'description': None,
'history': None,
'responsible': u'test_user_1_',
'responsible_client': u'org-unit-1',
'sequence_number': 2,
'title': 'task 2',
'type': ''}]}

self.assertEquals(expected, dossiertasks.get_render_arguments())

@browsing
def test_dossier_tasks_history(self, browser):
repository = create(Builder('repository_root')
.titled(u'Repository'))
dossier = create(Builder('dossier')
.titled(u'Anfr\xf6gen 2015')
.within(repository)
.having(responsible=self.user.userid))

browser.login().visit(dossier)
factoriesmenu.add('Task')
browser.fill({'Title': 'Task title',
'Task Type': 'To comment'})

form = browser.find_form_by_field('Responsible')
form.find_widget('Responsible').fill(self.get_org_unit().id() + ':' + TEST_USER_ID)
browser.find('Save').click()

browser.open('http://nohost/plone/repository/dossier-1/task-1')
browser.click_on("task-transition-open-resolved")
browser.fill({'Response': 'response text'})
browser.click_on("Save")

with provide_dossier_task_layer(dossier.REQUEST):
layout = DefaultLayout(dossier, dossier.REQUEST, PDFBuilder())
dossiertasks = getMultiAdapter((dossier, dossier.REQUEST, layout),
ILaTeXView)
tasks_data = dossiertasks.get_render_arguments()['task_data_list']
self.assertEqual(1, len(tasks_data))
self.assertIn("response text", tasks_data[0]['history'])
@@ -93,6 +93,18 @@ def get_meeting_title(self, meeting_id):
return meeting.get_title() if meeting else u''


class ProposalRemovedFromScheduleActivity(ProposalScheduledActivity):
kind = 'proposal-transition-pull'

@property
def summary(self):
return self.translate_to_all_languages(
_('proposal_history_label_remove_scheduled',
u'Removed from schedule of meeting ${meeting} by ${user}',
mapping={'meeting': self.get_meeting_title(self.meeting_id),
'user': actor_link()}))


class ProposalDecideActivity(ProposalTransitionActivity):
kind = 'proposal-transition-decide'

@@ -262,6 +262,7 @@ def _get_agenda_items(self):
view='agenda_items/{}/edit'.format(item.agenda_item_id))
data['decision_number'] = item.get_decision_number()
data['is_decided'] = item.is_decided()
data['is_completed'] = item.is_completed()
if item.is_decide_possible():
data['decide_link'] = meeting.get_url(
view='agenda_items/{}/decide'.format(item.agenda_item_id))
@@ -271,6 +272,9 @@ def _get_agenda_items(self):
if item.is_revise_possible():
data['revise_link'] = meeting.get_url(
view='agenda_items/{}/revise'.format(item.agenda_item_id))
if self.is_manager():
data['debug_excerpt_docxcompose_link'] = meeting.get_url(
view='agenda_items/{}/debug_excerpt_docxcompose'.format(item.agenda_item_id))
if item.is_paragraph:
data['paragraph'] = True

@@ -379,13 +383,8 @@ def decide(self):
self.agenda_item.decide()

response = JSONResponse(self.request)
if self.agenda_item.has_proposal:
response.info(
_(u'agenda_item_proposal_decided',
default=u'Agenda Item decided and excerpt generated.'))
else:
response.info(_(u'agenda_item_decided',
default=u'Agenda Item decided.'))
response.info(_(u'agenda_item_decided',
default=u'Agenda Item decided.'))

if meeting_state != self.meeting.get_state():
response.redirect(self.context.absolute_url())
@@ -554,8 +553,11 @@ def _get_excerpt_doc_by_uuid(self, doc_uuid):
return doc
return None

def is_manager(self):
return api.user.has_permission('cmf.ManagePortal')

def debug_excerpt_docxcompose(self):
if not api.user.has_permission('cmf.ManagePortal'):
if not self.is_manager():
raise Forbidden

if self.agenda_item.is_paragraph:
@@ -336,6 +336,7 @@ def render_handlebars_agendaitems_template(self):
_('label_decision_number', default=u'Decision number'),
_('label_agenda_item_decided', default='Decided'),
_('action_edit_document', default='Edit with word'),
_('action_debug_excerpt_docxcompose', default='Debug excerpt docxcompose'),
_('action_decide', default='Decide'),
_('action_generate_excerpt', default='Generate excerpt'),
_('action_rename_agenda_item', default='Rename agenda item'),
@@ -1,6 +1,6 @@
<script id="agendaitemsTemplate" type="text/x-handlebars-template">
{{#each agendaitems}}
<tr class="{{css_class}} word-feature agenda_item" data-uid="{{id}}" id="ai{{id}}">
<tr class="{{css_class}} word-feature agenda_item" data-uid="{{id}}" id="ai{{id}}" completed="{{is_completed}}">
{{#if ../agendalist_editable}}<td class="sortable-handle"></td>{{/if}}
<td class="title">

@@ -31,6 +31,7 @@
<a href="#" class="button editing-menu">&#133;</a>
{{/if}}
{{/if}}
</div>

{{#if (or ../agendalist_editable reopen_link)}}
@@ -42,6 +43,9 @@
{{else}}
<li><a href="{{edit_link}}" class="rename-agenda-item">%(action_rename_agenda_item)s</a></li>
<li><a href="{{delete_link}}" class="delete-agenda-item">%(action_remove_agenda_item)s</a></li>
{{#if debug_excerpt_docxcompose_link}}
<li><a href="{{debug_excerpt_docxcompose_link}}" class="debug-excerpt-docxcompose">%(action_debug_excerpt_docxcompose)s</a></li>
{{/if}}
{{/if}}
{{/if}}
{{#if reopen_link}}
@@ -4,7 +4,7 @@
<li>
<a href="#ai{{id}}" class="{{css_class}}">
<span class="number">{{number}}</span>
<span class="title">{{title}}</span>
<span class="title">{{{title}}}</span>
</a>
</li>
{{/each}}
@@ -639,7 +639,7 @@
};

this.updateCloseTransitionActionState = function() {
if($('.decide-agenda-item, .revise-agenda-item').length > 0) {
if($("[completed='false']").length > 0) {
$('.close-meeting').addClass('disabled');
$('.cancel-meeting').hide();
} else {
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-07-24 11:50+0000\n"
"POT-Creation-Date: 2018-09-06 07:57+0000\n"
"PO-Revision-Date: 2017-10-23 18:40+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -271,6 +271,11 @@ msgstr "Sitzung abschliessen"
msgid "action_create_task"
msgstr "Aufgabe erstellen"

#. Default: "Debug excerpt docxcompose"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_debug_excerpt_docxcompose"
msgstr "Dateien für docxcompose herunterladen"

#. Default: "Decide"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_decide"
@@ -327,7 +332,7 @@ msgstr "Sitzung wieder öffnen"
#. Default: "Return to proposal"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_return_excerpt"
msgstr "Protokollauszug zurücksenden"
msgstr "Protokollauszug ablegen"

#. Default: "Active"
#: ./opengever/meeting/browser/documents/proposalstab.py
@@ -361,11 +366,6 @@ msgstr "Das Traktandum wurde abgeschlossen und die Sitzung wurde durchgeführt."
msgid "agenda_item_order_updated"
msgstr "Die Reihenfolge der Traktanden wurde aktualisiert."

#. Default: "Agenda Item decided and excerpt generated."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "agenda_item_proposal_decided"
msgstr "Traktandum wurde abgeschlossen, der Protokollauszug generiert und zurückgespielt."

#. Default: "Agenda Item successfully reopened."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "agenda_item_reopened"
@@ -652,11 +652,6 @@ msgstr "Nur .docx Dateien sind als Antragsdokumente erlaubt."
msgid "error_prosal_template_not_docx"
msgstr "Nur Word-Dateien (.docx) sind erlaubt."

#. Default: "Either a proposal template or a proposal document, but not both, is required."
#: ./opengever/meeting/browser/proposalforms.py
msgid "error_template_or_document_but_not_both_required_for_creation"
msgstr "Bitte wählen sie entweder eine Antragsvorlage oder ein Antragsdokument aus."

#. Default: "Either a proposal template or a proposal document is required."
#: ./opengever/meeting/browser/proposalforms.py
msgid "error_template_or_document_required_for_creation"
@@ -811,7 +806,7 @@ msgstr "Sitzung stornieren"
#. Default: "The meeting cannot be closed because it has undecided agenda items."
#: ./opengever/meeting/model/meeting.py
msgid "label_close_error_has_undecided_agenda_items"
msgstr "Die Sitzung kann nicht abgeschlossen werden solange noch nicht alle Traktanden abgeschlossen sind."
msgstr "Die Sitzung kann nicht abgeschlossen werden solange noch nicht alle Traktanden abgeschlossen sind und die Protokollauszüge generiert und zurückgesendet wurden."

#. Default: "Closed meetings"
#: ./opengever/meeting/browser/committee.py
@@ -856,6 +851,7 @@ msgid "label_create_excerpt"
msgstr "Protokollauszug generieren"

#. Default: "Creator"
#: ./opengever/meeting/browser/meetings/meeting.py
#: ./opengever/meeting/browser/proposalforms.py
msgid "label_creator"
msgstr "Ersteller"
@@ -1093,6 +1089,7 @@ msgid "label_member"
msgstr "Mitglied"

#. Default: "Modified"
#: ./opengever/meeting/browser/meetings/meeting.py
#: ./opengever/meeting/browser/proposalforms.py
msgid "label_modified"
msgstr "Zuletzt bearbeitet"
@@ -1179,12 +1176,12 @@ msgstr "Löschen"
#. Default: "Return Excerpt"
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
msgid "label_return_excerpt"
msgstr "Protokollauszug zurücksenden"
msgstr "Protokollauszug ablegen"

#. Default: "Returned"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "label_returned_excerpt"
msgstr "An Antrag zurückgesendet."
msgstr "In Dossier abgelegt."

#. Default: "Role"
#: ./opengever/meeting/browser/memberships.py
@@ -1442,7 +1439,7 @@ msgstr "Antrag erfolgreich eingereicht."
#. Default: "The meeting can only be closed when all agenda items are decided."
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
msgid "msg_require_all_agenda_items_decided_for_closing"
msgstr "Die Sitzung kann erst abgeschlossen werden wenn alle Traktanden abgeschlossen sind."
msgstr "Die Sitzung kann erst abgeschlossen werden wenn alle Traktanden abgeschlossen sind und die Protokollauszüge generiert und zurückgesendet wurden."

#. Default: "The object was deleted successfully."
#: ./opengever/meeting/browser/views.py
@@ -1467,7 +1464,7 @@ msgstr "Übersicht"
#. Default: "Paragraph successfully added."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "paragraph_added"
msgstr "Paragraph erfolgreich hinzugefügt."
msgstr "Zwischentitel erfolgreich hinzugefügt."

#. Default: "Pending"
#: ./opengever/meeting/model/agendaitem.py
@@ -1540,6 +1537,7 @@ msgid "proposal_history_label_rejected"
msgstr "Zurückgewiesen von ${user}"

#. Default: "Removed from schedule of meeting ${meeting} by ${user}"
#: ./opengever/meeting/activity/activities.py
#: ./opengever/meeting/proposalhistory.py
msgid "proposal_history_label_remove_scheduled"
msgstr "Von der Traktandenliste der Sitzung ${meeting} entfernt durch ${user}"
@@ -1600,7 +1598,7 @@ msgstr "Wieder eröffnen"
#. Default: "Return Excerpt"
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
msgid "return_excerpt"
msgstr "Protokollauszug zurücksenden"
msgstr "Protokollauszug ablegen"

#. Default: "Revise"
#: ./opengever/meeting/model/agendaitem.py
@@ -1666,10 +1664,10 @@ msgstr "${count} Teilnehmende"
msgid "text_added"
msgstr "Traktandum erfolgreich hinzugefügt"

#. Default: "Ad hoc agenda item ${title}"
#: ./opengever/meeting/model/meeting.py
msgid "title_ad_hoc_document"
msgstr "Traktandum ${title}"
#. Default: "Agenda item ${number}"
#: ./opengever/meeting/model/agendaitem.py
msgid "title_agenda_item"
msgstr "Traktandum ${number}"

#. Default: "Delete proposal"
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-07-24 11:50+0000\n"
"POT-Creation-Date: 2018-09-06 07:57+0000\n"
"PO-Revision-Date: 2018-05-22 10:15+0000\n"
"Last-Translator: Niklaus Johner <Niklaus.johner@4teamwork.ch>\n"
"Language-Team: French <https://translations.onegovgever.ch/projects/onegov-gever/opengever-meeting/fr/>\n"
@@ -273,6 +273,11 @@ msgstr "Clôturer la réunion"
msgid "action_create_task"
msgstr "Créer une tâche"

#. Default: "Debug excerpt docxcompose"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_debug_excerpt_docxcompose"
msgstr "Télécharger les fichiers docxcompose"

#. Default: "Decide"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_decide"
@@ -329,7 +334,7 @@ msgstr "Rouvrir la réunion"
#. Default: "Return to proposal"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_return_excerpt"
msgstr "Renvoyer l'extrait du protocole"
msgstr "Classer l'extrait de protocole"

#. Default: "Active"
#: ./opengever/meeting/browser/documents/proposalstab.py
@@ -363,11 +368,6 @@ msgstr "Le point de l'ordre du jour a été clôturé et la réunion a eu lieu."
msgid "agenda_item_order_updated"
msgstr "Les points de l'ordre du jour ont été réarrangés."

#. Default: "Agenda Item decided and excerpt generated."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "agenda_item_proposal_decided"
msgstr "Le point de l'ordre du jour a été clôturé, l'extrait de protocole a été généré et renvoyé."

#. Default: "Agenda Item successfully reopened."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "agenda_item_reopened"
@@ -654,11 +654,6 @@ msgstr "Seuls les fichiers .docx sont autorisés comme documents de proposition.
msgid "error_prosal_template_not_docx"
msgstr "Seuls les fichiers Word (.docx) sont acceptés."

#. Default: "Either a proposal template or a proposal document, but not both, is required."
#: ./opengever/meeting/browser/proposalforms.py
msgid "error_template_or_document_but_not_both_required_for_creation"
msgstr "Veuillez choisir soit un modèle de proposition, soit un document de proposition."

#. Default: "Either a proposal template or a proposal document is required."
#: ./opengever/meeting/browser/proposalforms.py
msgid "error_template_or_document_required_for_creation"
@@ -813,7 +808,7 @@ msgstr "Annuler la réunion"
#. Default: "The meeting cannot be closed because it has undecided agenda items."
#: ./opengever/meeting/model/meeting.py
msgid "label_close_error_has_undecided_agenda_items"
msgstr "La réunion ne peut pas être clôturée avant que tous les points de l'ordre du jour ne soient clôturés."
msgstr "La réunion ne peut pas être clôturée avant que tous les points de l'ordre du jour ne soient clôturés et les extraits de protocol générés et renvoyés comme réponse."

#. Default: "Closed meetings"
#: ./opengever/meeting/browser/committee.py
@@ -858,6 +853,7 @@ msgid "label_create_excerpt"
msgstr "Générer un extrait de protocole"

#. Default: "Creator"
#: ./opengever/meeting/browser/meetings/meeting.py
#: ./opengever/meeting/browser/proposalforms.py
msgid "label_creator"
msgstr "Créé par"
@@ -1095,6 +1091,7 @@ msgid "label_member"
msgstr "Membre"

#. Default: "Modified"
#: ./opengever/meeting/browser/meetings/meeting.py
#: ./opengever/meeting/browser/proposalforms.py
msgid "label_modified"
msgstr "Dernière modification"
@@ -1181,12 +1178,12 @@ msgstr "Supprimer"
#. Default: "Return Excerpt"
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
msgid "label_return_excerpt"
msgstr "Renvoyer l'extrait de protocole"
msgstr "Classer l'extrait de protocole"

#. Default: "Returned"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "label_returned_excerpt"
msgstr "Renvoyé à la demande."
msgstr "Classé dans le dossier."

#. Default: "Role"
#: ./opengever/meeting/browser/memberships.py
@@ -1444,7 +1441,7 @@ msgstr "Proposition soumise avec succès."
#. Default: "The meeting can only be closed when all agenda items are decided."
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
msgid "msg_require_all_agenda_items_decided_for_closing"
msgstr "La réunion pourra seulement être clôturée quand tous les points de l'ordre du jour seront clôturés."
msgstr "La réunion pourra seulement être clôturée quand tous les points de l'ordre du jour seront clôturés et les extraits de protocol générés et renvoyés comme réponse."

#. Default: "The object was deleted successfully."
#: ./opengever/meeting/browser/views.py
@@ -1469,7 +1466,7 @@ msgstr "Sommaire"
#. Default: "Paragraph successfully added."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "paragraph_added"
msgstr "Paragraphe ajouté avec succès."
msgstr "Intertitre ajouté avec succès."

#. Default: "Pending"
#: ./opengever/meeting/model/agendaitem.py
@@ -1542,6 +1539,7 @@ msgid "proposal_history_label_rejected"
msgstr "Rejeté par ${user}"

#. Default: "Removed from schedule of meeting ${meeting} by ${user}"
#: ./opengever/meeting/activity/activities.py
#: ./opengever/meeting/proposalhistory.py
msgid "proposal_history_label_remove_scheduled"
msgstr "Enlevé de l'ordre du jour de la réunion ${meeting} par ${user}"
@@ -1602,7 +1600,7 @@ msgstr "Réactiver"
#. Default: "Return Excerpt"
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
msgid "return_excerpt"
msgstr "Renvoyer l'extrait de protocole"
msgstr "Classer l'extrait de protocole"

#. Default: "Revise"
#: ./opengever/meeting/model/agendaitem.py
@@ -1668,10 +1666,10 @@ msgstr "${count} participants"
msgid "text_added"
msgstr "Texte libre ajouté à l'ordre du jour avec succès."

#. Default: "Ad hoc agenda item ${title}"
#: ./opengever/meeting/model/meeting.py
msgid "title_ad_hoc_document"
msgstr "Point ${title} de l'ordre du jour"
#. Default: "Agenda item ${number}"
#: ./opengever/meeting/model/agendaitem.py
msgid "title_agenda_item"
msgstr ""

#. Default: "Delete proposal"
#: ./opengever/meeting/browser/meetings/templates/meeting.pt
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-07-24 11:50+0000\n"
"POT-Creation-Date: 2018-09-06 07:57+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -270,6 +270,11 @@ msgstr ""
msgid "action_create_task"
msgstr ""

#. Default: "Debug excerpt docxcompose"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_debug_excerpt_docxcompose"
msgstr ""

#. Default: "Decide"
#: ./opengever/meeting/browser/meetings/meeting.py
msgid "action_decide"
@@ -360,11 +365,6 @@ msgstr ""
msgid "agenda_item_order_updated"
msgstr ""

#. Default: "Agenda Item decided and excerpt generated."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "agenda_item_proposal_decided"
msgstr ""

#. Default: "Agenda Item successfully reopened."
#: ./opengever/meeting/browser/meetings/agendaitem.py
msgid "agenda_item_reopened"
@@ -651,11 +651,6 @@ msgstr ""
msgid "error_prosal_template_not_docx"
msgstr ""

#. Default: "Either a proposal template or a proposal document, but not both, is required."
#: ./opengever/meeting/browser/proposalforms.py
msgid "error_template_or_document_but_not_both_required_for_creation"
msgstr ""

#. Default: "Either a proposal template or a proposal document is required."
#: ./opengever/meeting/browser/proposalforms.py
msgid "error_template_or_document_required_for_creation"
@@ -855,6 +850,7 @@ msgid "label_create_excerpt"
msgstr ""

#. Default: "Creator"
#: ./opengever/meeting/browser/meetings/meeting.py
#: ./opengever/meeting/browser/proposalforms.py
msgid "label_creator"
msgstr ""
@@ -1092,6 +1088,7 @@ msgid "label_member"
msgstr ""

#. Default: "Modified"
#: ./opengever/meeting/browser/meetings/meeting.py
#: ./opengever/meeting/browser/proposalforms.py
msgid "label_modified"
msgstr ""
@@ -1539,6 +1536,7 @@ msgid "proposal_history_label_rejected"
msgstr ""

#. Default: "Removed from schedule of meeting ${meeting} by ${user}"
#: ./opengever/meeting/activity/activities.py
#: ./opengever/meeting/proposalhistory.py
msgid "proposal_history_label_remove_scheduled"
msgstr ""
@@ -1665,9 +1663,9 @@ msgstr ""
msgid "text_added"
msgstr ""

#. Default: "Ad hoc agenda item ${title}"
#: ./opengever/meeting/model/meeting.py
msgid "title_ad_hoc_document"
#. Default: "Agenda item ${number}"
#: ./opengever/meeting/model/agendaitem.py
msgid "title_agenda_item"
msgstr ""

#. Default: "Delete proposal"
@@ -31,6 +31,8 @@
from sqlalchemy.schema import Sequence
from tzlocal import get_localzone
from zope.component import getMultiAdapter
from zope.globalrequest import getRequest
from zope.i18n import translate
import os


@@ -218,9 +220,11 @@ def remove(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()),
return normalize_path(u'{}/{}{}'.format(
translate(
_(u'title_agenda_item', default=u'Agenda item ${number}', mapping={u'number': self.number}),
context=getRequest(),
),
safe_unicode(document.Title()),
os.path.splitext(document.get_file().filename)[1]))

@@ -232,7 +236,9 @@ def get_proposal_link(self, include_icon=True):

def get_data_for_zip_export(self):
agenda_item_data = {
'opengever_id': self.agenda_item_id,
'title': safe_unicode(self.get_title()),
'sort_order': self.sort_order,
}

if self.has_document:
@@ -349,13 +355,12 @@ def decide(self):

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:
"""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')

@@ -364,11 +369,29 @@ def is_reopen_possible(self):
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:
if self.has_proposal and self.is_proposal_decided():
self.proposal.revise(self)
self.workflow.execute_transition(None, self, 'revision-decided')

@@ -378,6 +401,7 @@ def is_revise_possible(self):
return False

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

def generate_excerpt(self, title):
@@ -398,7 +398,7 @@ def is_not_paragraph(agenda_item):
return not agenda_item.is_paragraph

def is_not_decided(agenda_item):
return agenda_item.workflow_state != 'decided'
return not agenda_item.is_completed()

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

@@ -410,6 +410,7 @@ def _get_localized_time(self, date):

def get_data_for_zip_export(self):
meeting_data = {
'opengever_id': self.meeting_id,
'title': safe_unicode(self.title),
'start': safe_unicode(self.start.isoformat()),
'end': safe_unicode(self.end and self.end.isoformat() or ''),
@@ -472,16 +473,12 @@ def schedule_ad_hoc(self, title, template_id=None, description=None):
'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()
title=title).execute()
agenda_item = AgendaItem(
title=title, description=description,
document=ad_hoc_document, is_paragraph=False)
@@ -345,8 +345,11 @@ def revise(self, agenda_item):

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.get_state() == self.STATE_DECIDED
assert self.is_decided()
IHistory(self.resolve_submitted_proposal()).append_record(u'reopened')

def cancel(self, text=None):
@@ -329,6 +329,10 @@ def comment(self, text, uuid=None):
ProposalCommentedActivitiy(self, self.REQUEST).record()
return IHistory(self).append_record(u'commented', uuid=uuid, text=text)

def is_submitted(self):
model = self.load_model()
return model.is_submitted()


class SubmittedProposal(ProposalBase):
"""Proxy for a proposal in queue with a committee."""
@@ -701,7 +705,3 @@ def reject(self):
self.date_of_submission = None
api.content.transition(obj=self,
transition='proposal-transition-reject')

def is_submitted(self):
model = self.load_model()
return model.is_submitted()
@@ -8,10 +8,9 @@
from opengever.meeting import _
from opengever.meeting.activity.activities import ProposalCommentedActivitiy
from opengever.meeting.activity.activities import ProposalDecideActivity
from opengever.meeting.activity.activities import ProposalRemovedFromScheduleActivity
from opengever.meeting.activity.activities import ProposalScheduledActivity
from opengever.meeting.model import Meeting
from opengever.meeting.proposal import Proposal
from opengever.meeting.proposal import SubmittedProposal
from opengever.ogds.base.actor import Actor
from persistent.mapping import PersistentMapping
from plone import api
@@ -68,7 +67,7 @@ def append_record(self, history_type, timestamp=None, **kwargs):
record = clazz(self.context, timestamp=timestamp, **kwargs)
record.append_to(history)

if record.needs_syncing:
if record.needs_syncing and self.context.is_submitted():
path = self.context.get_sync_target_path()
admin_unit_id = self.context.get_sync_admin_unit_id()

@@ -122,9 +121,8 @@ class BaseHistoryRecord(object):
Each record must have a unique `history_type` from which it can be built
with IHistory.append_record.
If `needs_syncing` is `True` a records that is created on the
`SubmittedProposal` side is automatically added to its corresponding
`Proposal`.
If `needs_syncing` is `True` a records is created on one side is
automatically added to its corresponding `Proposal` or `SubmittedProposal`.
"""

history_type = None
@@ -377,6 +375,11 @@ def message(self):
mapping={'user': self.get_actor_link(),
'meeting': self.meeting_title})

@classmethod
def receive(cls, context, request, data):
ProposalRemovedFromScheduleActivity(
context, request, data.get('meeting_id')).record()


@ProposalHistory.register
class DocumentUpdated(DocumentSubmitted):
@@ -152,7 +152,7 @@ def test_update_agenda_item_raise_forbidden_when_meeting_is_not_editable(self, b

agenda_item = self.schedule_proposal(
self.meeting, self.submitted_proposal)
agenda_item.decide()
self.decide_agendaitem_generate_and_return_excerpt(agenda_item)
self.meeting.model.close()

with browser.expect_http_error(code=403):
@@ -257,20 +257,6 @@ def test_redirect_to_current_view_when_meeting_has_to_be_decided_as_well(self, b
data={'_authenticator': createToken()})
self.assertEquals(None, browser.json.get('redirectUrl'))

@browsing
def test_closing_proposal_adds_proposalhistory(self, browser):
self.login(self.committee_responsible, browser)
agenda_item = self.schedule_proposal(
self.meeting, self.submitted_word_proposal)
agenda_item.close()

browser.open(self.submitted_word_proposal, view=u'tabbedview_view-overview')
entry = browser.css('.answer').first

self.assertEquals(u'Proposal decided by M\xfcller Fr\xe4nzi (franzi.muller)',
entry.css('h3').first.text)
self.assertEquals('answer decided', entry.get('class'))


class TestReopenAgendaItem(IntegrationTestCase):

@@ -281,7 +267,7 @@ def test_reopen_agenda_item(self, browser):
self.login(self.committee_responsible, browser)
agenda_item = self.schedule_proposal(
self.meeting, self.submitted_proposal)
agenda_item.decide()
self.decide_agendaitem_generate_and_return_excerpt(agenda_item)

browser.open(self.agenda_item_url(agenda_item, 'reopen'),
data={'_authenticator': createToken()})
@@ -308,7 +294,7 @@ def test_raise_forbidden_when_meeting_is_not_editable(self, browser):
self.login(self.committee_responsible, browser)
agenda_item = self.schedule_proposal(
self.meeting, self.submitted_proposal)
agenda_item.decide()
self.decide_agendaitem_generate_and_return_excerpt(agenda_item)
self.meeting.model.close()

with browser.expect_http_error(code=403):
@@ -325,7 +311,7 @@ def test_revise_agenda_item(self, browser):
self.login(self.committee_responsible, browser)
agenda_item = self.schedule_proposal(
self.meeting, self.submitted_proposal)
agenda_item.decide()
self.decide_agendaitem_generate_and_return_excerpt(agenda_item)
agenda_item.reopen()

browser.open(self.agenda_item_url(agenda_item, 'revise'),
@@ -353,7 +339,7 @@ def test_raise_forbidden_when_meeting_is_not_editable(self, browser):
self.login(self.committee_responsible, browser)
agenda_item = self.schedule_proposal(
self.meeting, self.submitted_proposal)
agenda_item.decide()
self.decide_agendaitem_generate_and_return_excerpt(agenda_item)
self.meeting.model.close()

with browser.expect_http_error(code=403):
@@ -56,7 +56,7 @@ def test_ad_hoc_document_is_created_from_template(self, browser):

document_link_html = item_data.get('document_link')
self.assertIn(
u'Ad hoc agenda item R\xfccktritt',
u'R\xfccktritt',
document_link_html)
self.assertIn(
ad_hoc_document.absolute_url() + '/tooltip',
@@ -1,6 +1,7 @@
from ftw.testbrowser import browsing
from opengever.base.role_assignments import RoleAssignmentManager
from opengever.base.role_assignments import SharingRoleAssignment
from opengever.meeting.model.proposal import Proposal
from opengever.testing import IntegrationTestCase
from opengever.trash.trash import ITrashed
from plone import api
@@ -23,8 +24,7 @@ def test_decide_proposal_agenda_item(self, browser):
data={'_authenticator': createToken()})

self.assertEquals('decided', agenda_item.workflow_state)
self.assertEquals([{u'message': u'Agenda Item decided and excerpt '
u'generated.',
self.assertEquals([{u'message': u'Agenda Item decided.',
u'messageClass': u'info',
u'messageTitle': u'Information'}],
browser.json.get('messages'))
@@ -41,6 +41,30 @@ def test_decide_proposal_agenda_item_generates_numbers(self, browser):
self.assertEqual(2, agenda_item.decision_number)
self.assertEqual(2, self.meeting.model.meeting_number)

@browsing
def test_proposal_is_decided_only_when_excerpt_is_returned(self, browser):
self.login(self.committee_responsible, browser)
self.assertEqual(Proposal.STATE_SUBMITTED, self.submitted_proposal.get_state())

agenda_item = self.schedule_proposal(
self.meeting, self.submitted_proposal)
self.assertEqual(Proposal.STATE_SCHEDULED, self.submitted_proposal.get_state())

browser.open(self.agenda_item_url(agenda_item, 'decide'),
data={'_authenticator': createToken()})
self.assertEqual(Proposal.STATE_SCHEDULED, self.submitted_proposal.get_state())

browser.open(self.meeting, view='agenda_items/list')
generate_excerpt_link = browser.json['items'][0]['generate_excerpt_link']
browser.open(generate_excerpt_link, send_authenticator=True,
data={'excerpt_title': 'Excerption \xc3\x84nderungen'})
self.assertEqual(Proposal.STATE_SCHEDULED, self.submitted_proposal.get_state())

browser.open(self.meeting, view='agenda_items/list')
return_excerpt_link = browser.json[u"items"][0]['excerpts'][0]['return_link']
browser.open(return_excerpt_link, send_authenticator=True)
self.assertEqual(Proposal.STATE_DECIDED, self.submitted_proposal.get_state())

@browsing
def test_delete_agenda_item_does_not_trash_proposal_document(self, browser):
self.login(self.committee_responsible, browser)
@@ -198,11 +222,11 @@ def test_decide_agenda_item_checks_in_documents(self, browser):
'/opengever-meeting-committeecontainer/committee-1/meeting-1',
u'messages': [
{u'messageTitle': u'Information',
u'message': u'Agenda Item decided and excerpt generated.',
u'message': u'Agenda Item decided.',
u'messageClass': u'info'}]},
browser.json)

self.assertEquals(proposal_model.STATE_DECIDED, proposal_model.get_state())
self.assertEquals(proposal_model.STATE_SCHEDULED, proposal_model.get_state())
self.assertIsNone(self.get_checkout_manager(document).get_checked_out_by())

@browsing
@@ -295,7 +319,7 @@ def test_cannot_create_excerpt_when_meeting_closed(self, browser):
self.login(self.committee_responsible, browser)
agenda_item = self.schedule_proposal(self.meeting,
self.submitted_word_proposal)
agenda_item.decide()
self.decide_agendaitem_generate_and_return_excerpt(agenda_item)
self.meeting.model.execute_transition('held-closed')
self.assertEquals(self.meeting.model.STATE_CLOSED,
self.meeting.model.get_state())
@@ -72,7 +72,8 @@ def test_scheduled_proposal_document(self):

def test_decided_proposal_document(self):
with self.login(self.committee_responsible):
self.schedule_proposal(self.meeting, self.submitted_word_proposal).decide()
agendaitem = self.schedule_proposal(self.meeting, self.submitted_word_proposal)
self.decide_agendaitem_generate_and_return_excerpt(agendaitem)

with self.login(self.dossier_responsible):
self.assertEquals(
@@ -107,12 +108,11 @@ def test_excerpt_document(self):
'ogg.meeting.agenda_item_number': '1.',
'ogg.meeting.proposal_title': '\xc3\x84nderungen am Personalreglement',
'ogg.meeting.proposal_description': '',
'ogg.meeting.proposal_state': 'Decided'},
'ogg.meeting.proposal_state': 'Scheduled'},
get_doc_properties(meeting_dossier_excerpt))

with self.observe_children(self.dossier) as children:
self.submitted_word_proposal.load_model().return_excerpt(
meeting_dossier_excerpt)
agenda_item.return_excerpt(meeting_dossier_excerpt)

case_dossier_excerpt, = children['added']

@@ -253,7 +253,7 @@ def test_closing_meeting_with_undecided_items_is_not_allowed(self, browser):
"""The user must decide all agenda items before the meeting can be closed.
"""
self.login(self.committee_responsible, browser)
self.schedule_proposal(self.meeting, self.submitted_word_proposal)
self.schedule_ad_hoc(self.meeting, "Ad hoc proposal")
self.assertEquals(u'pending', self.meeting.model.workflow_state)

browser.open(self.meeting)
@@ -269,6 +269,35 @@ def test_closing_meeting_with_undecided_items_is_not_allowed(self, browser):

self.assertEquals(u'pending', self.meeting.model.workflow_state)

@browsing
def test_closing_meeting_with_unreturned_excerpts_is_not_allowed(self, browser):
"""The user must decide all agenda items before the meeting can be closed.
"""
self.login(self.committee_responsible, browser)
agendaitem = self.schedule_proposal(self.meeting, self.submitted_word_proposal)
agendaitem.decide()
self.assertEquals(u'held', self.meeting.model.workflow_state)

browser.open(self.meeting)
editbar.menu_option('Actions', 'Close meeting').click()
self.assertEquals(
{u'messages': [
{u'messageTitle': u'Error',
u'message': u'The meeting cannot be closed because it'
u' has undecided agenda items.',
u'messageClass': u'error'}],
u'proceed': False},
browser.json)

self.assertEquals(u'held', self.meeting.model.workflow_state)

excerpt = agendaitem.generate_excerpt("Foo")
agendaitem.return_excerpt(excerpt)

browser.open(self.meeting)
editbar.menu_option('Actions', 'Close meeting').click()
self.assertEquals(u'closed', self.meeting.model.workflow_state)

def test_is_editable_for_pending_meeting(self):
with self.login(self.administrator):
meeting = self.meeting.model
@@ -302,7 +331,7 @@ def test_get_undecided_agenda_items(self):
item2 = self.schedule_ad_hoc(meeting, u'Ad-Hoc Agenda Item')

self.assertEquals([item1, item2], meeting.get_undecided_agenda_items())
item1.decide()
self.decide_agendaitem_generate_and_return_excerpt(item1)
self.assertEquals([item2], meeting.get_undecided_agenda_items())
item2.decide()
self.assertEquals([], meeting.get_undecided_agenda_items())