def paragraph(content, context, before=1, after=1, editable=False, style=None): try: status = context[u"_paragraph_status"] except KeyError: status = Bunch(opened=False, after=0) context[u"_paragraph_status"] = status html = [] if status.opened and before > 0: html.append(format_html(u"</div>")) status.opened = False if not status.opened: spacing = max(before, status.after) attrs = defaultdict(list) if editable: attrs[u"class"].append(u"editable-container") if spacing > 1: attrs[u"style"].append(u"margin-top: {0}ex;".format(spacing)) if style: attrs[u"style"].append(style) html.append(format_html_tag(u"div", attrs)) html.append(u" ") status.opened = True html.append(format_html(u"{0}", content)) if after > 0: html.append(format_html(u"</div>")) status.opened = False status.after = after html = format_html_join(u"", u"{0}", zip(html)) return html
def _create_scenario(self, **kwargs): res = Bunch() inforequest_args = kwargs.pop(u'inforequest_args', []) res.inforequest, res.branch, res.actions = self._create_inforequest_scenario(*inforequest_args) email_args = kwargs.pop(u'email_args', {}) email_args.setdefault(u'inforequest', res.inforequest) res.email, res.rel = self._create_inforequest_email(**email_args) self.assertEqual(kwargs, {}) return res
def _create_scenario(self, **kwargs): res = Bunch() inforequest_args = kwargs.pop(u'inforequest_args', []) res.inforequest, res.branch, res.actions = self._create_inforequest_scenario( *inforequest_args) email_args = kwargs.pop(u'email_args', {}) email_args.setdefault(u'inforequest', res.inforequest) res.email, res.rel = self._create_inforequest_email(**email_args) self.assertEqual(kwargs, {}) return res
def _create_scenario(self, **kwargs): res = Bunch() inforequest_args = kwargs.pop(u'inforequest_args', []) inforequest_scenario = kwargs.pop(u'inforequest_scenario', self.good_scenario) inforequest_args = list(inforequest_args) + list(inforequest_scenario) res.inforequest, res.branch, res.actions = self._create_inforequest_scenario(*inforequest_args) draft_args = kwargs.pop(u'draft_args', None) if draft_args is not None: draft_args.setdefault(u'inforequest', res.inforequest) draft_args.setdefault(u'type', self.action_type) res.draft = self._create_action_draft(**draft_args) self.assertEqual(kwargs, {}) return res
def _run_mail_cron_job(self, status_code=200, response={}, delete_settings=(), **override_settings): overrides = { u'EMAIL_OUTBOUND_TRANSPORT': u'poleno.mail.transports.mandrill.MandrillTransport', u'EMAIL_INBOUND_TRANSPORT': None, u'MANDRILL_API_KEY': u'default_testing_api_key', u'MANDRILL_API_URL': u'https://defaulttestinghost/api' } overrides.update(override_settings) requests = mock.Mock() requests.post.return_value.status_code = status_code requests.post.return_value.text = u'Response text' requests.post.return_value.json.return_value = response with self.settings(**overrides): for name in delete_settings: delattr(settings, name) with mock.patch( u'poleno.mail.transports.mandrill.transport.requests', requests): with override_signals(message_sent, message_received): mail_cron_job().do() posts = [ Bunch(url=call[0][0], data=json.loads(call[1][u'data'])) for call in requests.post.call_args_list ] return posts
def _create_scenario(self, **kwargs): res = Bunch() created = kwargs.pop(u'created', u'2010-03-07 10:33:00') timewarp.jump(local_datetime_from_local(created)) inforequest_args = kwargs.pop(u'inforequest_args', []) inforequest_scenario = kwargs.pop(u'inforequest_scenario', [u'request']) inforequest_args = list(inforequest_args) + list(inforequest_scenario) res.inforequest, res.branch, res.actions = self._create_inforequest_scenario(*inforequest_args) res.action = res.branch.last_action now = kwargs.pop(u'now', u'2010-07-08 10:33:00') timewarp.jump(local_datetime_from_local(now)) self.assertEqual(kwargs, {}) self._scenario = res return res
def _create_scenario(self, **kwargs): res = Bunch() created = kwargs.pop(u'created', u'2010-03-07 10:33:00') timewarp.jump(local_datetime_from_local(created)) inforequest_args = kwargs.pop(u'inforequest_args', []) inforequest_scenario = kwargs.pop(u'inforequest_scenario', [u'request']) inforequest_args = list(inforequest_args) + list(inforequest_scenario) res.inforequest, res.branch, res.actions = self._create_inforequest_scenario( *inforequest_args) res.action = res.branch.last_action now = kwargs.pop(u'now', u'2010-07-08 10:33:00') timewarp.jump(local_datetime_from_local(now)) self.assertEqual(kwargs, {}) self._scenario = res return res
def paragraph(content, context, before=1, after=1, editable=False, style=None): try: status = context[u'_paragraph_status'] except KeyError: status = Bunch(opened=False, after=0) context[u'_paragraph_status'] = status html = [] if status.opened and before > 0: html.append(format_html(u'</div>')) status.opened = False if not status.opened: spacing = max(before, status.after) attrs = defaultdict(list) if editable: attrs[u'class'].append(u'editable-container') if spacing > 1: attrs[u'style'].append(u'margin-top: {0}ex;'.format(spacing)) if style: attrs[u'style'].append(style) html.append(format_html_tag(u'div', attrs)) html.append(u' ') status.opened = True html.append(format_html(u'{0}', content)) if after > 0: html.append(format_html(u'</div>')) status.opened = False status.after = after html = format_html_join(u'', u'{0}', zip(html)) return html
def test_call_method_filters(self): u""" Tests ``obj|method:"name"|arg:arg|call`` and ``obj|method:"name"|call_with:arg`` filters for calling object methods with arguments. """ a = Bunch(plus=(lambda a, b: a + b)) b = {'minus': (lambda a, b: a - b)} rendered = self._render( u'{% load method call_with call with from poleno.utils %}' u'({{ a|method:"plus"|with:10|with:11|call }})' u'({{ b|method:"minus"|with:5|call_with:10 }})' u'({{ a|method:"nop"|call_with:3 }})' # invalid method u'({{ b|method:"nop"|with:a|call }})' # invalid method u'', a=a, b=b) self.assertEqual(rendered, u'(21)(-5)([not callable])([not callable])')
def _run_mail_cron_job(self): connection = mock.Mock() with self.settings(EMAIL_OUTBOUND_TRANSPORT= u'poleno.mail.transports.smtp.SmtpTransport', EMAIL_INBOUND_TRANSPORT=None): with mock.patch(u'poleno.mail.transports.smtp.get_connection', return_value=connection): with override_signals(message_sent, message_received): mail_cron_job().do() res = [] for call in connection.send_messages.call_args_list: for mail in call[0][0]: as_bytes = mail.message().as_bytes() headers, body = as_bytes.split(u'\n\n', 1) headers = self._parse_headers(headers) res.append(Bunch(headers=headers, body=body, as_bytes=as_bytes)) return res
def __init__(self, page, file, create, *args, **kwargs): super(FileEditForm, self).__init__(*args, **kwargs) self.page = page self.file = file self.create = create self.fieldsets = [ Bunch(label=None, collapse=False, fields=[]), ] self.fieldsets[0].fields.append( FakeField(u'Page', LivePath.render(page.lang, page.path))) self.fields[u'name'] = forms.CharField( label=u'File Name', validators=[ RegexValidator( pages.file_regex, squeeze(u""" Enter a valid file name. Only letters, numbers, dashes and dots are allowed. """)), ], ) if not create: self.initial[u'name'] = file.name self.fieldsets[0].fields.append(self[u'name']) if not create: self.fieldsets[0].fields.append( FakeField(u'Content Type', file.content_type)) self.fieldsets[0].fields.append( FakeField( u'Modified', date_format(datetime.datetime.fromtimestamp(file.mtime), u'DATETIME_FORMAT'))) self.fields[u'content'] = forms.FileField( label=u'Content', widget=PreviewFileInput, ) if not create: self.initial[u'content'] = file self.fieldsets[0].fields.append(self[u'content'])
def save_objects(self, rows): errors = 0 stats = Bunch(created=0, changed=0, unchanged=0, deleted=0) originals = {o.pk: o for o in self.model.objects.all()} for row_idx, values in rows.items(): try: pk = values[self.columns.pk.label] original = originals.pop(pk, None) fields = self.process_fields(row_idx, values, original) assert fields[u'pk'] == pk obj, has_changed = self.save_object(fields, original) if has_changed and original: self.importer.write(2, u'Changed {}', self.obj_repr(obj)) stats.changed += 1 elif has_changed: self.importer.write(2, u'Created {}', self.obj_repr(obj)) stats.created += 1 else: stats.unchanged += 1 except RollingError as e: errors += e.count # Mark omitted instances for deletion or add an error if delete is not permitted. Don't # delete anything if there were any errors as we don't know which instances are missing for # real and which are missing because of errors. The instances will be deleted only after # all sheets are imported to prevent unintentional cascades. if errors: raise RollingError(errors) for obj in originals.values(): if self.delete_omitted: self.book.marked_for_deletion[obj] = self stats.deleted += 1 else: self.error(u'omitted', u'Omitted {}', self.obj_repr(obj)) errors += 1 if errors: raise RollingError(errors) return stats
def test_undefined_attribute(self): bunch = Bunch(first=1, second=u'second') with self.assertRaises(AttributeError): bunch.nop
def test_can_add_x_properties(self): tests = ( (Action.TYPES.REQUEST, Bunch( scenario=[], allowed=[ u'confirmation', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action' ], expired=[ u'appeal', u'confirmation', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action', u'applicant_action' ], )), (Action.TYPES.CLARIFICATION_RESPONSE, Bunch( scenario=[ u'clarification_request', u'clarification_response' ], allowed=[ u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action' ], expired=[ u'appeal', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action', u'applicant_action' ], )), (Action.TYPES.APPEAL, Bunch( scenario=[u'expiration', u'appeal'], allowed=[ u'affirmation', u'reversion', u'remandment', u'obligee_action' ], expired=[ u'affirmation', u'reversion', u'remandment', u'obligee_action' ], )), (Action.TYPES.CONFIRMATION, Bunch( scenario=[u'confirmation'], allowed=[ u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action' ], expired=[ u'appeal', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action', u'applicant_action' ], )), (Action.TYPES.EXTENSION, Bunch( scenario=[u'extension'], allowed=[ u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action' ], expired=[ u'appeal', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action', u'applicant_action' ], )), (Action.TYPES.ADVANCEMENT, Bunch( scenario=[u'advancement'], allowed=[u'appeal', u'applicant_action'], expired=[u'appeal', u'applicant_action'], )), (Action.TYPES.CLARIFICATION_REQUEST, Bunch( scenario=[u'clarification_request'], allowed=[ u'clarification_response', u'clarification_request', u'obligee_action', u'obligee_email_action', u'applicant_action', u'applicant_email_action' ], expired=[ u'clarification_response', u'clarification_request', u'obligee_action', u'obligee_email_action', u'applicant_action', u'applicant_email_action' ], )), (Action.TYPES.DISCLOSURE, Bunch( scenario=[ (u'disclosure', dict(disclosure_level=Action.DISCLOSURE_LEVELS.NONE)) ], allowed=[u'appeal', u'applicant_action'], expired=[u'appeal', u'applicant_action'], )), (Action.TYPES.DISCLOSURE, Bunch( scenario=[ (u'disclosure', dict(disclosure_level=Action.DISCLOSURE_LEVELS.PARTIAL)) ], allowed=[u'appeal', u'applicant_action'], expired=[u'appeal', u'applicant_action'], )), (Action.TYPES.DISCLOSURE, Bunch( scenario=[ (u'disclosure', dict(disclosure_level=Action.DISCLOSURE_LEVELS.FULL)) ], allowed=[], expired=[], )), (Action.TYPES.REFUSAL, Bunch( scenario=[u'refusal'], allowed=[u'appeal', u'applicant_action'], expired=[u'appeal', u'applicant_action'], )), (Action.TYPES.AFFIRMATION, Bunch( scenario=[u'refusal', u'appeal', u'affirmation'], allowed=[], expired=[], )), (Action.TYPES.REVERSION, Bunch( scenario=[u'refusal', u'appeal', u'reversion'], allowed=[], expired=[], )), (Action.TYPES.REMANDMENT, Bunch( scenario=[u'refusal', u'appeal', u'remandment'], allowed=[ u'extension', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action' ], expired=[ u'appeal', u'extension', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action', u'applicant_action' ], )), (Action.TYPES.ADVANCED_REQUEST, Bunch( branch=1, scenario=[u'advancement'], allowed=[ u'confirmation', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action' ], expired=[ u'appeal', u'confirmation', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'obligee_action', u'obligee_email_action', u'applicant_action' ], )), (Action.TYPES.EXPIRATION, Bunch( scenario=[u'expiration'], allowed=[u'appeal', u'applicant_action'], expired=[u'appeal', u'applicant_action'], )), (Action.TYPES.APPEAL_EXPIRATION, Bunch( scenario=[u'refusal', u'appeal', u'appeal_expiration'], allowed=[], expired=[], )), ) # Make sure we are testing all defined action types tested_action_types = set(a for a, _ in tests) defined_action_types = Action.TYPES._inverse.keys() self.assertItemsEqual(tested_action_types, defined_action_types) can_add_properties = ( u'request', u'clarification_response', u'appeal', u'confirmation', u'extension', u'advancement', u'clarification_request', u'disclosure', u'refusal', u'affirmation', u'reversion', u'remandment', u'applicant_action', u'applicant_email_action', u'obligee_action', u'obligee_email_action', ) for action_type, test in tests: timewarp.jump(local_datetime_from_local(u'2010-07-05 10:33:00')) objs = self._create_inforequest_scenario(*test.scenario) branch = getattr(test, u'branch', 0) branch = [o for o in flatten(objs) if isinstance(o, Branch)][branch] self.assertEqual(branch.last_action.type, action_type) # Check actions allowed when the last action deadline is not expired yet for can_add_property in can_add_properties: value = getattr(branch, u'can_add_%s' % can_add_property) expected = can_add_property in test.allowed self.assertEqual( value, expected, u'can_add_%s is %s after %r' % (can_add_property, value, test.scenario)) # Check actions allowed when the last action deadline is expired timewarp.jump(local_datetime_from_local(u'2010-10-05 10:33:00')) branch = Branch.objects.get(pk=branch.pk) for can_add_property in can_add_properties: value = getattr(branch, u'can_add_%s' % can_add_property) expected = can_add_property in test.expired self.assertEqual( value, expected, u'can_add_%s is %s after expired %r' % (can_add_property, value, test.scenario))
def test_add_attribute(self): bunch = Bunch(first=1, second=u'second') with self.assertRaises(AttributeError): bunch.third bunch.third = [1, 2, 3] self.assertEqual(bunch.third, [1, 2, 3])
def __init__(self, page, create, *args, **kwargs): super(PageEditForm, self).__init__(*args, **kwargs) self.page = page self.create = create edit_root = not create and page.is_root edit_redirect = not create and page.is_redirect edit_regular = not create and not page.is_root and not page.is_redirect assert bool(create) + bool(edit_root) + bool(edit_redirect) + bool( edit_regular) == 1 self.fieldsets = [ Bunch(label=None, collapse=False, fields=[]), Bunch(label=u'Raw Config', collapse=True, fields=[]), Bunch(label=u'Page Content', collapse=False, fields=[]), ] if edit_regular: self.fields[u'parent'] = forms.CharField( label=u'URL Path', validators=[ RegexValidator( pages.path_regex, squeeze(u""" Enter a valid path. It must be an absolute path with respect to the root page starting and ending with a slash. """)), ], widget=forms.TextInput( attrs={ u'class': u'popup-path', u'style': u'width: 50em;', u'data-popup-url': reverse(u'admin:pages_index', args=[page.lang]), u'data-icon': staticfiles_storage.url( u'admin/img/selector-search.gif'), }), ) self.initial[u'parent'] = page.ppath self.fieldsets[0].fields.append(self[u'parent']) self.fieldsets[0].fields.append( LivePath(page.lang, self[u'parent'])) else: self.fieldsets[0].fields.append(FakeField(u'URL Path', page.path)) if create or edit_regular: self.fields[u'name'] = forms.CharField( label=u'URL Name', validators=[ RegexValidator( pages.slug_regex, u'Enter a valid slug. Only letters, numbers and dashes are allowed.' ), ], ) if not create: self.initial[u'name'] = page.name self.fieldsets[0].fields.append(self[u'name']) if create or edit_root or edit_regular: self.fields[u'title'] = forms.CharField( label=u'Page Title', required=False, widget=forms.TextInput(attrs={ u'style': u'width: 50em;', }), ) if not create: self.initial[u'title'] = page._config.get(u'title') self.fieldsets[0].fields.append(self[u'title']) if create or edit_root or edit_regular: self.fields[u'label'] = forms.CharField( label=u'Menu Label', required=False, ) if not create: self.initial[u'label'] = page._config.get(u'label') self.fieldsets[0].fields.append(self[u'label']) if create or edit_regular: self.fields[u'order'] = forms.CharField( label=u'Sort Key', required=False, ) if not create: self.initial[u'order'] = page._config.get(u'order') self.fieldsets[0].fields.append(self[u'order']) if create or edit_regular: for lang, _ in settings.LANGUAGES: if lang != page.lang: key = u'lang_{}'.format(lang) self.fields[key] = forms.CharField( label=u'Translation {}'.format(lang.upper()), required=False, validators=[ RegexValidator( pages.path_regex, squeeze(u""" Enter a valid path. It must be an absolute path with respect to the root page starting and ending with a slash. """)), ], widget=forms.TextInput( attrs={ u'class': u'popup-path', u'style': u'width: 50em;', u'data-popup-url': reverse(u'admin:pages_index', args=[lang]), u'data-icon': staticfiles_storage.url( u'admin/img/selector-search.gif'), }), ) if not create: self.initial[key] = page._config.get(key) self.fieldsets[0].fields.append(self[key]) self.fieldsets[0].fields.append(LivePath(lang, self[key])) if edit_redirect: self.fields[u'redirect'] = forms.CharField( label=u'Redirect', validators=[ RegexValidator( pages.path_regex, squeeze(u""" Enter a valid path. It must be an absolute path with respect to the root page starting and ending with a slash. """)), ], widget=forms.TextInput( attrs={ u'class': u'popup-path', u'style': u'width: 50em;', u'data-popup-url': reverse(u'admin:pages_index', args=[page.lang]), u'data-icon': staticfiles_storage.url( u'admin/img/selector-search.gif'), }), ) self.initial[u'redirect'] = page.redirect_path self.fieldsets[0].fields.append(self[u'redirect']) self.fieldsets[0].fields.append( LivePath(page.lang, self[u'redirect'])) if create or edit_root or edit_regular: self.fields[u'disabled'] = forms.BooleanField( label=u'Disabled', required=False, ) if not create: self.initial[u'disabled'] = bool(page._config.get(u'disabled')) self.fieldsets[0].fields.append(self[u'disabled']) if create or edit_root or edit_regular: self.fields[u'raw'] = forms.CharField( label=u'', required=False, widget=forms.Textarea(attrs={ u'style': u'width: 100%; height: 10em;', }), ) if not create: self.initial[u'raw'] = page.raw_config self.fieldsets[1].fields.append(self[u'raw']) if create or edit_root or edit_regular: with translation(page.lang): url = reverse(u'admin:pages_preview') self.fields[u'template'] = forms.CharField( label=u'', required=False, widget=forms.Textarea(attrs={ u'class': u'template-widget', u'data-url': url, }), ) if not create: self.initial[u'template'] = page.template self.fieldsets[2].fields.append(self[u'template'])
def test_change_attribute(self): bunch = Bunch(first=1, second=u'second') self.assertEqual(bunch.first, 1) bunch.first = u'changed' self.assertEqual(bunch.first, u'changed')
class Deadline(FormatMixin, object): TYPES = Bunch( OBLIGEE_DEADLINE = 1, APPLICANT_DEADLINE = 2, ) UNITS = Bunch( CALENDAR_DAYS = 1, WORKDAYS = 2, ) def __init__(self, type, base_date, value, unit, snooze): self.type = type self.base_date = base_date self.value = value self.unit = unit self._snooze = snooze self._today = local_today() @property def is_obligee_deadline(self): return self.type == self.TYPES.OBLIGEE_DEADLINE @property def is_applicant_deadline(self): return self.type == self.TYPES.APPLICANT_DEADLINE @property def is_in_calendar_days(self): return self.unit == self.UNITS.CALENDAR_DAYS @property def is_in_workdays(self): return self.unit == self.UNITS.WORKDAYS @cached_property def deadline_date(self): if self.is_in_calendar_days: return self.base_date + datetime.timedelta(days=self.value) else: return workdays.advance(self.base_date, self.value) @cached_property def calendar_days_passed(self): return self.calendar_days_passed_at(self._today) def calendar_days_passed_at(self, at): return (at - self.base_date).days @cached_property def workdays_passed(self): return self.workdays_passed(self._today) def workdays_passed_at(self, at): return workdays.between(self.base_date, at) @cached_property def calendar_days_remaining(self): return self.calendar_days_remaining_at(self._today) def calendar_days_remaining_at(self, at): return (self.deadline_date - at).days @cached_property def workdays_remaining(self): return self.workdays_remaining_at(self._today) def workdays_remaining_at(self, at): return workdays.between(at, self.deadline_date) @cached_property def calendar_days_behind(self): return -self.calendar_days_remaining def calendar_days_behind_at(self, at): return -self.calendar_days_remaining_at(at) @cached_property def workdays_behind(self): return -self.workdays_remaining def workdays_behind_at(self, at): return -self.workdays_remaining_at(at) @cached_property def is_deadline_missed(self): return self.is_deadline_missed_at(self._today) def is_deadline_missed_at(self, at): return self.deadline_date < at @cached_property def snooze_date(self): res = self._snooze or self.deadline_date res = max(res, self.deadline_date) res = min(res, self.deadline_date + datetime.timedelta(days=8)) return res @cached_property def is_snoozed(self): return self.snooze_date > self.deadline_date @cached_property def snooze_in_calendar_days(self): return self.calendar_days_behind_at(self.snooze_date) @cached_property def snooze_in_workdays(self): return self.workdays_behind_at(self.snooze_date) @cached_property def snooze_calendar_days_remaining(self): return self.snooze_calendar_days_remaining_at(self._today) def snooze_calendar_days_remaining_at(self, at): return (self.snooze_date - at).days @cached_property def snooze_workdays_remaining(self): return self.snooze_workdays_remaining_at(self._today) def snooze_workdays_remaining_at(self, at): return workdays.between(at, self.snooze_date) @cached_property def snooze_calendar_days_behind(self): return -self.snooze_calendar_days_remaining def snooze_calendar_days_behind_at(self, at): return -self.snooze_calendar_days_remaining_at(at) @cached_property def snooze_workdays_behind(self): return -self.snooze_workdays_remaining def snooze_workdays_behind_at(self, at): return -self.snooze_workdays_remaining_at(at) @cached_property def is_snooze_missed(self): return self.is_snooze_missed_at(self._today) def is_snooze_missed_at(self, at): return self.snooze_date < at def __unicode__(self): return u'{} {} for {} since {}{}'.format( self.value, u'CD' if self.is_in_calendar_days else u'WD', u'Applicant' if self.is_obligee_deadline else u'Obligee', self.base_date, u' +{0} CD'.format(self.snooze_in_calendar_days) if self.snooze_date != self.deadline_date else u'', )
def test_defined_attribute(self): bunch = Bunch(first=1, second=u'second') self.assertEqual(bunch.first, 1) self.assertEqual(bunch.second, u'second')
def test_delete_attribute(self): bunch = Bunch(first=1, second=u'second') self.assertEqual(bunch.first, 1) del bunch.first with self.assertRaises(AttributeError): bunch.first
class Action(models.Model): # May NOT be NULL branch = models.ForeignKey(u'Branch') # NOT NULL for actions sent or received by email; NULL otherwise email = models.OneToOneField(u'mail.Message', blank=True, null=True, on_delete=models.SET_NULL) # May NOT be NULL TYPES = FieldChoices( # Applicant actions (u'REQUEST', 1, _(u'inforequests:Action:type:REQUEST')), (u'CLARIFICATION_RESPONSE', 12, _(u'inforequests:Action:type:CLARIFICATION_RESPONSE')), (u'APPEAL', 13, _(u'inforequests:Action:type:APPEAL')), # Obligee actions (u'CONFIRMATION', 2, _(u'inforequests:Action:type:CONFIRMATION')), (u'EXTENSION', 3, _(u'inforequests:Action:type:EXTENSION')), (u'ADVANCEMENT', 4, _(u'inforequests:Action:type:ADVANCEMENT')), (u'CLARIFICATION_REQUEST', 5, _(u'inforequests:Action:type:CLARIFICATION_REQUEST')), (u'DISCLOSURE', 6, _(u'inforequests:Action:type:DISCLOSURE')), (u'REFUSAL', 7, _(u'inforequests:Action:type:REFUSAL')), (u'AFFIRMATION', 8, _(u'inforequests:Action:type:AFFIRMATION')), (u'REVERSION', 9, _(u'inforequests:Action:type:REVERSION')), (u'REMANDMENT', 10, _(u'inforequests:Action:type:REMANDMENT')), # Implicit actions (u'ADVANCED_REQUEST', 11, _(u'inforequests:Action:type:ADVANCED_REQUEST')), (u'EXPIRATION', 14, _(u'inforequests:Action:type:EXPIRATION')), (u'APPEAL_EXPIRATION', 15, _(u'inforequests:Action:type:APPEAL_EXPIRATION')), ) APPLICANT_ACTION_TYPES = ( TYPES.REQUEST, TYPES.CLARIFICATION_RESPONSE, TYPES.APPEAL, ) APPLICANT_EMAIL_ACTION_TYPES = ( TYPES.REQUEST, TYPES.CLARIFICATION_RESPONSE, ) OBLIGEE_ACTION_TYPES = ( TYPES.CONFIRMATION, TYPES.EXTENSION, TYPES.ADVANCEMENT, TYPES.CLARIFICATION_REQUEST, TYPES.DISCLOSURE, TYPES.REFUSAL, TYPES.AFFIRMATION, TYPES.REVERSION, TYPES.REMANDMENT, ) OBLIGEE_EMAIL_ACTION_TYPES = ( TYPES.CONFIRMATION, TYPES.EXTENSION, TYPES.ADVANCEMENT, TYPES.CLARIFICATION_REQUEST, TYPES.DISCLOSURE, TYPES.REFUSAL, ) IMPLICIT_ACTION_TYPES = ( TYPES.ADVANCED_REQUEST, TYPES.EXPIRATION, TYPES.APPEAL_EXPIRATION, ) type = models.SmallIntegerField(choices=TYPES._choices) # May be empty for implicit actions; Should NOT be empty for other actions subject = models.CharField(blank=True, max_length=255) content = models.TextField(blank=True) # NOT NULL CONTENT_TYPES = FieldChoices( (u'PLAIN_TEXT', 1, u'Plain Text'), (u'HTML', 2, u'HTML'), ) content_type = models.SmallIntegerField(choices=CONTENT_TYPES._choices, default=CONTENT_TYPES.PLAIN_TEXT, help_text=squeeze(u""" Mandatory choice action content type. Supported formats are plain text and html code. The html code is assumed to be safe. It is passed to the client without sanitizing. It must be sanitized before saving it here. """)) # May be empty attachment_set = generic.GenericRelation( u'attachments.Attachment', content_type_field=u'generic_type', object_id_field=u'generic_id') # May NOT be NULL effective_date = models.DateField(help_text=squeeze(u""" The date at which the action was sent or received. If the action was sent/received by e‑mail it's set automatically. If it was sent/received by s‑mail it's filled by the applicant. """)) # May be empty for obligee actions; Should be empty for other actions file_number = models.CharField(blank=True, max_length=255, help_text=squeeze(u""" A file number assigned to the action by the obligee. Usually only obligee actions have it. However, if we know tha obligee assigned a file number to an applicant action, we should keep it here as well. The file number is not mandatory. """)) # May NOT be NULL for actions that set deadline; Must be NULL otherwise. Default value is # determined and automaticly set in save() when creating a new object. All actions that set # deadlines except CLARIFICATION_REQUEST, DISCLOSURE and REFUSAL set the deadline for the # obligee. CLARIFICATION_REQUEST, DISCLOSURE and REFUSAL set the deadline for the applicant. # DISCLOSURE sets the deadline only if not FULL. DEFAULT_DEADLINES = Bunch( # Applicant actions REQUEST=8, CLARIFICATION_RESPONSE=8, APPEAL=30, # Obligee actions CONFIRMATION=8, EXTENSION=10, ADVANCEMENT=30, CLARIFICATION_REQUEST=7, # Deadline for the applicant DISCLOSURE=( lambda a: 15 # Deadline for the applicant if not full disclosure if a.disclosure_level != a.DISCLOSURE_LEVELS.FULL else None), REFUSAL=15, # Deadline for the applicant AFFIRMATION=None, REVERSION=None, REMANDMENT=13, # Implicit actions ADVANCED_REQUEST=13, EXPIRATION=30, APPEAL_EXPIRATION=None, ) SETTING_APPLICANT_DEADLINE_TYPES = ( # Applicant actions # Obligee actions TYPES.ADVANCEMENT, TYPES.CLARIFICATION_REQUEST, TYPES.DISCLOSURE, TYPES.REFUSAL, # Implicit actions TYPES.EXPIRATION, ) SETTING_OBLIGEE_DEADLINE_TYPES = ( # Applicant actions TYPES.REQUEST, TYPES.CLARIFICATION_RESPONSE, TYPES.APPEAL, # Obligee actions TYPES.CONFIRMATION, TYPES.EXTENSION, TYPES.REMANDMENT, # Implicit actions TYPES.ADVANCED_REQUEST, ) deadline = models.IntegerField(blank=True, null=True, help_text=squeeze(u""" The deadline that apply after the action, if the action sets a deadline, NULL otherwise. The deadline is expressed in a number of working days (WD) counting since the effective date. It may apply to the applicant or to the obligee, depending on the action type. """)) # May be NULL extension = models.IntegerField(blank=True, null=True, help_text=squeeze(u""" Applicant extension to the deadline, if the action sets an obligee deadline. The applicant may extend the deadline after it is missed in order to be patient and wait a little longer. He may extend it multiple times. Applicant deadlines may not be extended. """)) # NOT NULL for ADVANCEMENT, DISCLOSURE, REVERSION and REMANDMENT; NULL otherwise DISCLOSURE_LEVELS = FieldChoices( (u'NONE', 1, _(u'inforequests:Action:disclosure_level:NONE')), (u'PARTIAL', 2, _(u'inforequests:Action:disclosure_level:PARTIAL')), (u'FULL', 3, _(u'inforequests:Action:disclosure_level:FULL')), ) disclosure_level = models.SmallIntegerField( choices=DISCLOSURE_LEVELS._choices, blank=True, null=True, help_text=squeeze(u""" Mandatory choice for advancement, disclosure, reversion and remandment actions, NULL otherwise. Specifies if the obligee disclosed any requested information by this action. """)) # NOT NULL for REFUSAL and AFFIRMATION; NULL otherwise REFUSAL_REASONS = FieldChoices( (u'DOES_NOT_HAVE', u'3', _(u'inforequests:Action:refusal_reason:DOES_NOT_HAVE')), (u'DOES_NOT_PROVIDE', u'4', _(u'inforequests:Action:refusal_reason:DOES_NOT_PROVIDE')), (u'DOES_NOT_CREATE', u'5', _(u'inforequests:Action:refusal_reason:DOES_NOT_CREATE')), (u'COPYRIGHT', u'6', _(u'inforequests:Action:refusal_reason:COPYRIGHT')), (u'BUSINESS_SECRET', u'7', _(u'inforequests:Action:refusal_reason:BUSINESS_SECRET')), (u'PERSONAL', u'8', _(u'inforequests:Action:refusal_reason:PERSONAL')), (u'CONFIDENTIAL', u'9', _(u'inforequests:Action:refusal_reason:CONFIDENTIAL')), (u'OTHER_REASON', u'-2', _(u'inforequests:Action:refusal_reason:OTHER_REASON')), ) refusal_reason = MultiSelectField(choices=REFUSAL_REASONS._choices, blank=True, help_text=squeeze(u""" Optional multichoice for refusal and affirmation actions, NULL otherwise. Specifies the reason why the obligee refused to disclose the information. Empty value means that the obligee refused to disclose it with no reason. """)) # May be NULL; Used by ``cron.obligee_deadline_reminder`` and ``cron.applicant_deadline_reminder`` last_deadline_reminder = models.DateTimeField(blank=True, null=True) # Backward relations: # # -- advanced_to_set: by Branch.advanced_by # May NOT be empty for ADVANCEMENT; Must be empty otherwise # Backward relations added to other models: # # -- Branch.action_set # Should NOT be empty # # -- Message.action # May be undefined objects = ActionQuerySet.as_manager() class Meta: index_together = [ [u'effective_date', u'id'], # [u'branch'] -- ForeignKey defines index by default # [u'email'] -- OneToOneField defines index by default ] @staticmethod def prefetch_attachments(path=None, queryset=None): u""" Use to prefetch ``Action.attachments``. """ if queryset is None: queryset = Attachment.objects.get_queryset() queryset = queryset.order_by_pk() return Prefetch(join_lookup(path, u'attachment_set'), queryset, to_attr=u'attachments') @cached_property def attachments(self): u""" Cached list of all action attachments ordered by ``pk``. May be prefetched with ``prefetch_related(Action.prefetch_attachments())`` queryset method. """ return list(self.attachment_set.order_by_pk()) @cached_property def is_applicant_action(self): return self.type in self.APPLICANT_ACTION_TYPES @cached_property def is_obligee_action(self): return self.type in self.OBLIGEE_ACTION_TYPES @cached_property def is_implicit_action(self): return self.type in self.IMPLICIT_ACTION_TYPES @cached_property def is_by_email(self): return self.email_id is not None @cached_property def days_passed(self): return self.days_passed_at(local_today()) @cached_property def deadline_remaining(self): return self.deadline_remaining_at(local_today()) @cached_property def deadline_missed(self): return self.deadline_missed_at(local_today()) @cached_property def deadline_date(self): if self.deadline is None: return None deadline = self.deadline + (self.extension or 0) return workdays.advance(self.effective_date, deadline) @cached_property def has_deadline(self): return self.deadline is not None @cached_property def has_applicant_deadline(self): return self.deadline is not None and self.type in self.SETTING_APPLICANT_DEADLINE_TYPES @cached_property def has_obligee_deadline(self): return self.deadline is not None and self.type in self.SETTING_OBLIGEE_DEADLINE_TYPES @decorate(prevent_bulk_create=True) def save(self, *args, **kwargs): if self.pk is None: # Creating a new object assert self.type is not None, u'%s.type is mandatory' % self.__class__.__name__ if self.deadline is None: type_name = self.TYPES._inverse[self.type] deadline = getattr(self.DEFAULT_DEADLINES, type_name) self.deadline = deadline(self) if callable( deadline) else deadline super(Action, self).save(*args, **kwargs) def get_absolute_url(self): return self.branch.inforequest.get_absolute_url(u'#action-%d' % self.pk) def days_passed_at(self, at): return workdays.between(self.effective_date, at) def deadline_remaining_at(self, at): if self.deadline is None: return None deadline = self.deadline + (self.extension or 0) return deadline - self.days_passed_at(at) def deadline_missed_at(self, at): # self.deadline_remaining is 0 on the last day of deadline remaining = self.deadline_remaining_at(at) return remaining is not None and remaining < 0 def send_by_email(self): if not self.is_applicant_action: raise TypeError(u'%s is not applicant action' % self.get_type_display()) sender_name = self.branch.inforequest.applicant_name sender_address = self.branch.inforequest.unique_email sender_formatted = formataddr((squeeze(sender_name), sender_address)) recipients = (formataddr(r) for r in self.branch.collect_obligee_emails()) # FIXME: Attachment name and content type are set by client and not to be trusted. The name # must be sanitized and the content type white listed for known content types. Any unknown # content type should be replaced with 'application/octet-stream'. msg = EmailMessage(self.subject, self.content, sender_formatted, recipients) for attachment in self.attachments: msg.attach(attachment.name, attachment.content, attachment.content_type) msg.send() inforequestemail = InforequestEmail( inforequest=self.branch.inforequest, email=msg.instance, type=InforequestEmail.TYPES.APPLICANT_ACTION, ) inforequestemail.save() self.email = msg.instance self.save(update_fields=[u'email']) def __unicode__(self): return u'%s' % self.pk
def test_empty_bunch(self): bunch = Bunch() with self.assertRaises(AttributeError): bunch.first
def paragraphs(context): context.push() context[u'_paragraph_status'] = Bunch(opened=False, after=0) return u''