You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by jo...@apache.org on 2013/08/17 00:21:46 UTC
[10/16] git commit: [#6464] Fixed issue with custom fields not
persisting and added tests
[#6464] Fixed issue with custom fields not persisting and added tests
Signed-off-by: Cory Johns <cj...@slashdotmedia.com>
Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/36cf1a28
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/36cf1a28
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/36cf1a28
Branch: refs/heads/cj/6464
Commit: 36cf1a281b1e0d88e384a18922e0edbb5a27d7b0
Parents: ea2fc78
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Mon Aug 12 22:25:23 2013 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Fri Aug 16 22:21:00 2013 +0000
----------------------------------------------------------------------
Allura/allura/lib/helpers.py | 4 +-
.../forgeimporters/google/__init__.py | 19 ++
ForgeImporters/forgeimporters/google/tracker.py | 65 ++---
.../tests/google/functional/__init__.py | 17 ++
.../tests/google/functional/test_tracker.py | 260 +++++++++++++++++++
.../forgeimporters/tests/google/test_tracker.py | 25 +-
6 files changed, 341 insertions(+), 49 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/36cf1a28/Allura/allura/lib/helpers.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 99a5d30..01586c2 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -98,7 +98,7 @@ re_preserve_spaces = re.compile(r'''
''', re.VERBOSE)
re_angle_bracket_open = re.compile('<')
re_angle_bracket_close = re.compile('>')
-md_chars_matcher_all = re.compile(r"([`\*_{}\[\]\(\)#!\\.+-])")
+md_chars_matcher_all = re.compile(r"([`\*_{}\[\]\(\)#!\\\.+-])")
def make_safe_path_portion(ustr, relaxed=True):
"""Return an ascii representation of ``ustr`` that conforms to mount point
@@ -962,7 +962,7 @@ def plain2markdown(text, preserve_multiple_spaces=False, has_html_entities=False
text = html2text.escape_md_section(text, snob=True)
except ImportError:
# fall back to just escaping any MD-special chars
- text = md_chars_matcher.sub(r"\\\\1", text)
+ text = md_chars_matcher_all.sub(r"\\\1", text)
# prevent < and > from becoming tags
text = re_angle_bracket_open.sub('<', text)
text = re_angle_bracket_close.sub('>', text)
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/36cf1a28/ForgeImporters/forgeimporters/google/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/__init__.py b/ForgeImporters/forgeimporters/google/__init__.py
index 88a1449..660853f 100644
--- a/ForgeImporters/forgeimporters/google/__init__.py
+++ b/ForgeImporters/forgeimporters/google/__init__.py
@@ -28,6 +28,7 @@ import logging
from BeautifulSoup import BeautifulSoup
+from allura.lib import helpers as h
from allura import model as M
from forgeimporters.base import ProjectExtractor
@@ -261,6 +262,24 @@ class Comment(object):
else:
self.attachments = []
+ @property
+ def annotated_text(self):
+ text = (
+ u'*Originally posted by:* [{author.name}]({author.link})\n'
+ u'\n'
+ u'{body}\n'
+ u'\n'
+ u'{updates}'
+ ).format(
+ author=self.author,
+ body=h.plain2markdown(self.body, True),
+ updates='\n'.join(
+ '**%s** %s' % (k,v)
+ for k,v in self.updates.items()
+ ),
+ )
+ return text
+
class Attachment(object):
def __init__(self, tag):
self.filename = _as_text(tag).strip().split()[0]
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/36cf1a28/ForgeImporters/forgeimporters/google/tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tracker.py b/ForgeImporters/forgeimporters/google/tracker.py
index bd3a57f..fefd695 100644
--- a/ForgeImporters/forgeimporters/google/tracker.py
+++ b/ForgeImporters/forgeimporters/google/tracker.py
@@ -19,6 +19,7 @@ from collections import defaultdict
from datetime import datetime
from pylons import tmpl_context as c
+from pylons import app_globals as g
from ming.orm import session, ThreadLocalORMSession
from allura import model as M
@@ -44,22 +45,27 @@ class GoogleCodeTrackerImporter(ToolImporter):
def import_tool(self, project, user, project_name, mount_point=None,
mount_label=None, **kw):
- c.app = project.install_app('tickets', mount_point, mount_label)
+ app = project.install_app('tickets', mount_point, mount_label)
+ app.globals.open_status_names = 'New Accepted Started'
+ app.globals.closed_status_names = 'Fixed Verified Invalid Duplicate WontFix Done'
ThreadLocalORMSession.flush_all()
- c.app.globals.open_status_names = 'New Accepted Started'
- c.app.globals.closed_status_names = 'Fixed Verified Invalid Duplicate WontFix Done'
self.custom_fields = {}
try:
M.session.artifact_orm_session._get().skip_mod_date = True
- for issue in GoogleCodeProjectExtractor.iter_issues(project_name):
- ticket = TM.Ticket.new()
- self.process_fields(ticket, issue)
- self.process_labels(ticket, issue)
- self.process_comments(ticket, issue)
- session(ticket).flush(ticket)
- session(ticket).expunge(ticket)
- self.postprocess_custom_fields()
- ThreadLocalORMSession.flush_all()
+ with h.push_config(c, user=M.User.anonymous(), app=app):
+ for issue in GoogleCodeProjectExtractor.iter_issues(project_name):
+ ticket = TM.Ticket.new()
+ self.process_fields(ticket, issue)
+ self.process_labels(ticket, issue)
+ self.process_comments(ticket, issue)
+ session(ticket).flush(ticket)
+ session(ticket).expunge(ticket)
+ # app.globals gets expunged every time Ticket.new() is called :-(
+ app.globals = TM.Globals.query.get(app_config_id=app.config._id)
+ app.globals.custom_fields = self.postprocess_custom_fields()
+ ThreadLocalORMSession.flush_all()
+ g.post_event('project_updated')
+ return app
finally:
M.session.artifact_orm_session._get().skip_mod_date = False
@@ -78,13 +84,18 @@ class GoogleCodeTrackerImporter(ToolImporter):
ticket.status = issue.get_issue_status()
ticket.created_date = datetime.strptime(issue.get_issue_created_date(), '%c')
ticket.mod_date = datetime.strptime(issue.get_issue_mod_date(), '%c')
+ owner = issue.get_issue_owner()
+ if owner:
+ owner_line = '*Originally owned by:* [{owner.name}]({owner.link})\n'.format(owner=owner)
+ else:
+ owner_line = ''
ticket.description = (
u'*Originally created by:* [{creator.name}]({creator.link})\n'
- '*Originally owned by:* [{owner.name}]({owner.link})\n'
- '\n'
- '{body}').format(
+ u'{owner}'
+ u'\n'
+ u'{body}').format(
creator=issue.get_issue_creator(),
- owner=issue.get_issue_owner(),
+ owner=owner_line,
body=h.plain2markdown(issue.get_issue_description(), True),
)
ticket.add_multiple_attachments(issue.get_issue_attachments())
@@ -106,25 +117,14 @@ class GoogleCodeTrackerImporter(ToolImporter):
def process_comments(self, ticket, issue):
for comment in issue.iter_comments():
p = ticket.discussion_thread.add_post(
- text = (
- u'*Originally posted by:* [{author.name}]({author.link})\n'
- '\n'
- '{body}\n'
- '\n'
- '{updates}').format(
- author=comment.author,
- body=h.plain2markdown(comment.body, True),
- updates='\n'.join(
- '**%s** %s' % (k,v)
- for k,v in comment.updates.items()
- ),
- )
+ text = comment.annotated_text,
+ ignore_security = True,
+ timestamp = datetime.strptime(comment.created_date, '%c'),
)
- p.created_date = p.timestamp = datetime.strptime(comment.created_date, '%c')
p.add_multiple_attachments(comment.attachments)
def postprocess_custom_fields(self):
- c.app.globals.custom_fields = []
+ custom_fields = []
for name, field in self.custom_fields.iteritems():
if field['name'] == '_milestone':
field['milestones'] = [{
@@ -137,4 +137,5 @@ class GoogleCodeTrackerImporter(ToolImporter):
field['options'] = ' '.join(field['options'])
else:
field['options'] = ''
- c.app.globals.custom_fields.append(field)
+ custom_fields.append(field)
+ return custom_fields
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/36cf1a28/ForgeImporters/forgeimporters/tests/google/functional/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/google/functional/__init__.py b/ForgeImporters/forgeimporters/tests/google/functional/__init__.py
new file mode 100644
index 0000000..77505f1
--- /dev/null
+++ b/ForgeImporters/forgeimporters/tests/google/functional/__init__.py
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/36cf1a28/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py b/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py
new file mode 100644
index 0000000..206ac7c
--- /dev/null
+++ b/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py
@@ -0,0 +1,260 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from unittest import TestCase
+import pkg_resources
+from functools import wraps
+from datetime import datetime
+
+from BeautifulSoup import BeautifulSoup
+import mock
+from ming.orm import ThreadLocalORMSession
+from pylons import tmpl_context as c
+from IPython.testing.decorators import module_not_available, skipif
+
+from alluratest.controller import setup_basic_test
+from allura import model as M
+from forgetracker import model as TM
+from .... import google
+from ....google import tracker
+
+
+def without_html2text(func):
+ @wraps(func)
+ def wrapped(*args, **kw):
+ try:
+ import html2text
+ except ImportError:
+ return func(*args, **kw)
+ else:
+ with mock.patch.object(html2text, 'escape_md_section') as ems:
+ ems.side_effect = ImportError
+ return func(*args, **kw)
+ return wrapped
+
+class TestGCTrackerImporter(TestCase):
+ def _make_extractor(self, html):
+ with mock.patch.object(google, 'urllib2') as urllib2:
+ urllib2.urlopen.return_value = ''
+ extractor = google.GoogleCodeProjectExtractor('my-project', 'project_info')
+ extractor.page = BeautifulSoup(html)
+ extractor.url = "http://test/issue/?id=1"
+ return extractor
+
+ def _make_ticket(self, issue):
+ self.assertIsNone(self.project.app_instance('test-issue'))
+ with mock.patch.object(google, 'urllib2') as urllib2,\
+ mock.patch.object(google.tracker, 'GoogleCodeProjectExtractor') as GPE:
+ urllib2.urlopen = lambda url: mock.Mock(read=lambda: url)
+ GPE.iter_issues.return_value = [issue]
+ gti = google.tracker.GoogleCodeTrackerImporter()
+ gti.import_tool(self.project, self.user, 'test-issue-project', mount_point='test-issue')
+ c.app = self.project.app_instance('test-issue')
+ query = TM.Ticket.query.find({'app_config_id': c.app.config._id})
+ self.assertEqual(query.count(), 1)
+ ticket = query.all()[0]
+ return ticket
+
+ def setUp(self, *a, **kw):
+ super(TestGCTrackerImporter, self).setUp(*a, **kw)
+ setup_basic_test()
+ self.empty_issue = self._make_extractor(open(pkg_resources.resource_filename('forgeimporters', 'tests/data/google/empty-issue.html')).read())
+ self.test_issue = self._make_extractor(open(pkg_resources.resource_filename('forgeimporters', 'tests/data/google/test-issue.html')).read())
+ c.project = self.project = M.Project.query.get(shortname='test')
+ c.user = self.user = M.User.query.get(username='test-admin')
+
+ def test_empty_issue(self):
+ ticket = self._make_ticket(self.empty_issue)
+ self.assertEqual(ticket.summary, 'Empty Issue')
+ self.assertEqual(ticket.description, '*Originally created by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n\nEmpty')
+ self.assertEqual(ticket.status, '')
+ self.assertEqual(ticket.milestone, '')
+ self.assertEqual(ticket.custom_fields, {})
+
+ @without_html2text
+ def test_issue_basic_fields(self):
+ anon = M.User.anonymous()
+ ticket = self._make_ticket(self.test_issue)
+ self.assertEqual(ticket.reported_by, anon)
+ self.assertIsNone(ticket.assigned_to_id)
+ self.assertEqual(ticket.summary, 'Test Issue')
+ self.assertEqual(ticket.description,
+ '*Originally created by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '*Originally owned by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '\n'
+ 'Test \\*Issue\\* for testing\n'
+ '\n'
+ ' 1\\. Test List\n'
+ ' 2\\. Item\n'
+ '\n'
+ '\\*\\*Testing\\*\\*\n'
+ '\n'
+ ' \\* Test list 2\n'
+ ' \\* Item\n'
+ '\n'
+ '\\# Test Section\n'
+ '\n'
+ ' p = source\\.test\\_issue\\.post\\(\\)\n'
+ ' p\\.count = p\\.count \\*5 \\#\\* 6\n'
+ '\n'
+ 'That\'s all'
+ )
+ self.assertEqual(ticket.status, 'Started')
+ self.assertEqual(ticket.created_date, datetime(2013, 8, 8, 15, 33, 52))
+ self.assertEqual(ticket.mod_date, datetime(2013, 8, 8, 15, 36, 57))
+ self.assertEqual(ticket.custom_fields, {
+ '_priority': 'Medium',
+ '_opsys': 'All, OSX, Windows',
+ '_component': 'Logic',
+ '_type': 'Defect',
+ '_milestone': 'Release1.0'
+ })
+ self.assertEqual(ticket.labels, ['Performance', 'Security'])
+
+ @skipif(module_not_available('html2text'))
+ def test_html2text_escaping(self):
+ ticket = self._make_ticket(self.test_issue)
+ self.assertEqual(ticket.description,
+ '*Originally created by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '*Originally owned by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '\n'
+ 'Test \\*Issue\\* for testing\n'
+ '\n'
+ ' 1. Test List\n'
+ ' 2. Item\n'
+ '\n'
+ '\\*\\*Testing\\*\\*\n'
+ '\n'
+ ' \\* Test list 2\n'
+ ' \\* Item\n'
+ '\n'
+ '\\# Test Section\n'
+ '\n'
+ ' p = source.test\\_issue.post\\(\\)\n'
+ ' p.count = p.count \\*5 \\#\\* 6\n'
+ '\n'
+ 'That\'s all'
+ )
+
+ def _assert_attachments(self, actual, *expected):
+ self.assertEqual(actual.count(), len(expected))
+ atts = set((a.filename, a.content_type, a.rfile().read()) for a in actual)
+ self.assertEqual(atts, set(expected))
+
+ def test_attachements(self):
+ ticket = self._make_ticket(self.test_issue)
+ self._assert_attachments(ticket.attachments,
+ ('at1.txt', 'text/plain', 'http://allura-google-importer.googlecode.com/issues/attachment?aid=70000000&name=at1.txt&token=3REU1M3JUUMt0rJUg7ldcELt6LA%3A1376059941255'),
+ ('at2.txt', 'text/plain', 'http://allura-google-importer.googlecode.com/issues/attachment?aid=70000001&name=at2.txt&token=C9Hn4s1-g38hlSggRGo65VZM1ys%3A1376059941255'),
+ )
+
+ @without_html2text
+ def test_comments(self):
+ anon = M.User.anonymous()
+ ticket = self._make_ticket(self.test_issue)
+ actual_comments = ticket.discussion_thread.find_posts()
+ expected_comments = [
+ {
+ 'timestamp': datetime(2013, 8, 8, 15, 35, 15),
+ 'text': (
+ '*Originally posted by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '\n'
+ 'Test \\*comment\\* is a comment\n'
+ '\n'
+ '**Labels:** -OpSys-Linux OpSys-Windows\n'
+ '**Status:** Started'
+ ),
+ 'attachments': [
+ ('at2.txt', 'text/plain', 'http://allura-google-importer.googlecode.com/issues/attachment?aid=60001000&name=at2.txt&token=JOSo4duwaN2FCKZrwYOQ-nx9r7U%3A1376001446667'),
+ ],
+ },
+ {
+ 'timestamp': datetime(2013, 8, 8, 15, 35, 34),
+ 'text': (
+ '*Originally posted by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '\n'
+ 'Another comment\n\n'
+ ),
+ },
+ {
+ 'timestamp': datetime(2013, 8, 8, 15, 36, 39),
+ 'text': (
+ '*Originally posted by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '\n'
+ 'Last comment\n\n'
+ ),
+ 'attachments': [
+ ('at4.txt', 'text/plain', 'http://allura-google-importer.googlecode.com/issues/attachment?aid=60003000&name=at4.txt&token=6Ny2zYHmV6b82dqxyoiH6HUYoC4%3A1376001446667'),
+ ('at1.txt', 'text/plain', 'http://allura-google-importer.googlecode.com/issues/attachment?aid=60003001&name=at1.txt&token=NS8aMvWsKzTAPuY2kniJG5aLzPg%3A1376001446667'),
+ ],
+ },
+ {
+ 'timestamp': datetime(2013, 8, 8, 15, 36, 57),
+ 'text': (
+ '*Originally posted by:* [john...@gmail.com](http://code.google.com/u/101557263855536553789/)\n'
+ '\n'
+ 'Oh, I forgot one\n'
+ '\n'
+ '**Labels:** OpSys-OSX'
+ ),
+ },
+ ]
+ self.assertEqual(len(actual_comments), len(expected_comments))
+ for actual, expected in zip(actual_comments, expected_comments):
+ self.assertEqual(actual.author(), anon)
+ self.assertEqual(actual.timestamp, expected['timestamp'])
+ self.assertEqual(actual.text, expected['text'])
+ if 'attachments' in expected:
+ self._assert_attachments(actual.attachments, *expected['attachments'])
+
+ def test_globals(self):
+ globals = self._make_ticket(self.test_issue).globals
+ self.assertItemsEqual(globals.custom_fields, [
+ {
+ 'label': 'Milestone',
+ 'name': '_milestone',
+ 'type': 'milestone',
+ 'options': '',
+ 'milestones': [
+ {'name': 'Release1.0', 'due_date': None, 'complete': False},
+ ],
+ },
+ {
+ 'label': 'Type',
+ 'name': '_type',
+ 'type': 'select',
+ 'options': 'Defect',
+ },
+ {
+ 'label': 'Priority',
+ 'name': '_priority',
+ 'type': 'select',
+ 'options': 'Medium',
+ },
+ {
+ 'label': 'OpSys',
+ 'name': '_opsys',
+ 'type': 'string',
+ 'options': '',
+ },
+ {
+ 'label': 'Component',
+ 'name': '_component',
+ 'type': 'string',
+ 'options': '',
+ },
+ ])
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/36cf1a28/ForgeImporters/forgeimporters/tests/google/test_tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/google/test_tracker.py b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
index a1f0a28..3efd97d 100644
--- a/ForgeImporters/forgeimporters/tests/google/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
@@ -27,9 +27,10 @@ class TestTrackerImporter(TestCase):
@mock.patch.object(tracker, 'c')
@mock.patch.object(tracker, 'ThreadLocalORMSession')
@mock.patch.object(tracker, 'session')
+ @mock.patch.object(tracker, 'M')
@mock.patch.object(tracker, 'TM')
@mock.patch.object(tracker, 'GoogleCodeProjectExtractor')
- def test_import_tool(self, gpe, TM, session, tlos, c):
+ def test_import_tool(self, gpe, TM, M, session, tlos, c):
importer = tracker.GoogleCodeTrackerImporter()
importer.process_fields = mock.Mock()
importer.process_labels = mock.Mock()
@@ -158,12 +159,14 @@ class TestTrackerImporter(TestCase):
mock.Mock(
author=_author(1),
body='text1',
+ annotated_text='annotated1',
attachments='attachments1',
created_date='Mon Jul 15 00:00:00 2013',
),
mock.Mock(
author=_author(2),
body='text2',
+ annotated_text='annotated2',
attachments='attachments2',
created_date='Mon Jul 16 00:00:00 2013',
),
@@ -177,24 +180,16 @@ class TestTrackerImporter(TestCase):
importer = tracker.GoogleCodeTrackerImporter()
importer.process_comments(ticket, issue)
self.assertEqual(ticket.discussion_thread.add_post.call_args_list[0], mock.call(
- text='*Originally posted by:* [author1](author1_link)\n'
- '\n'
- 'text1\n'
- '\n'
- '**Foo:** Bar\n'
- '**Baz:** Qux'
+ text='annotated1',
+ timestamp=datetime(2013, 7, 15),
+ ignore_security=True,
))
- self.assertEqual(posts[0].created_date, datetime(2013, 7, 15))
- self.assertEqual(posts[0].timestamp, datetime(2013, 7, 15))
posts[0].add_multiple_attachments.assert_called_once_with('attachments1')
self.assertEqual(ticket.discussion_thread.add_post.call_args_list[1], mock.call(
- text='*Originally posted by:* [author2](author2_link)\n'
- '\n'
- 'text2\n'
- '\n'
+ text='annotated2',
+ timestamp=datetime(2013, 7, 16),
+ ignore_security=True,
))
- self.assertEqual(posts[1].created_date, datetime(2013, 7, 16))
- self.assertEqual(posts[1].timestamp, datetime(2013, 7, 16))
posts[1].add_multiple_attachments.assert_called_once_with('attachments2')
@mock.patch.object(tracker, 'c')