Skip to content

Commit 0aac47e

Browse files
authored
Migrate to pyairtable 2.x (#76)
* Migrate AirtableMixin to pyairtable 2.x * Migrate AirtableModelImporter to pyairtable 2.x * Update mock_airtable and test_import for pyairtable 2 * Update test_views and test_models for pyairtable 2
1 parent ded5674 commit 0aac47e

File tree

7 files changed

+169
-146
lines changed

7 files changed

+169
-146
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ requires-python = ">=3.8"
3434
dependencies = [
3535
"Wagtail>=5.2",
3636
"Django>=4.2",
37-
"airtable-python-wrapper>=0.13.0,<0.14",
37+
"pyairtable>=2.3,<3",
3838
"djangorestframework>=3.11.0"
3939
]
4040

tests/mock_airtable.py

Lines changed: 93 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
"""A mocked Airtable API wrapper."""
22
from unittest import mock
3+
from pyairtable.formulas import match
34
from requests.exceptions import HTTPError
45

56
def get_mock_airtable():
67
"""
78
Wrap it in a function, so it's pure
89
"""
910

10-
class MockAirtable(mock.Mock):
11-
def get_iter(self):
12-
return [self.get_all()]
11+
class MockTable(mock.Mock):
12+
def iterate(self):
13+
return [self.all()]
1314

1415

15-
MockAirtable.table_name = "app_airtable_advert_base_key"
16+
MockTable.table_name = "app_airtable_advert_base_key"
1617

17-
MockAirtable.get = mock.MagicMock("get")
18+
MockTable.get = mock.MagicMock("get")
1819

1920
def get_fn(record_id):
2021
if record_id == "recNewRecordId":
@@ -34,11 +35,11 @@ def get_fn(record_id):
3435
else:
3536
raise HTTPError("404 Client Error: Not Found")
3637

37-
MockAirtable.get.side_effect = get_fn
38+
MockTable.get.side_effect = get_fn
3839

39-
MockAirtable.insert = mock.MagicMock("insert")
40+
MockTable.create = mock.MagicMock("create")
4041

41-
MockAirtable.insert.return_value = {
42+
MockTable.create.return_value = {
4243
"id": "recNewRecordId",
4344
"fields": {
4445
"title": "Red! It's the new blue!",
@@ -52,8 +53,8 @@ def get_fn(record_id):
5253
},
5354
}
5455

55-
MockAirtable.update = mock.MagicMock("update")
56-
MockAirtable.update.return_value = {
56+
MockTable.update = mock.MagicMock("update")
57+
MockTable.update.return_value = {
5758
"id": "recNewRecordId",
5859
"fields": {
5960
"title": "Red! It's the new blue!",
@@ -67,12 +68,77 @@ def get_fn(record_id):
6768
},
6869
}
6970

70-
MockAirtable.delete = mock.MagicMock("delete")
71-
MockAirtable.delete.return_value = {"deleted": True, "record": "recNewRecordId"}
71+
MockTable.delete = mock.MagicMock("delete")
72+
MockTable.delete.return_value = {"deleted": True, "record": "recNewRecordId"}
7273

73-
MockAirtable.search = mock.MagicMock("search")
74-
def search_fn(field, value):
75-
if field == "slug" and value == "red-its-new-blue":
74+
MockTable.all = mock.MagicMock("all")
75+
def all_fn(formula=None):
76+
if formula is None:
77+
return [
78+
{
79+
"id": "recNewRecordId",
80+
"fields": {
81+
"title": "Red! It's the new blue!",
82+
"description": "Red is a scientifically proven color that moves faster than all other colors.",
83+
"external_link": "https://example.com/",
84+
"is_active": True,
85+
"rating": "1.5",
86+
"long_description": "<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Veniam laboriosam consequatur saepe. Repellat itaque dolores neque, impedit reprehenderit eum culpa voluptates harum sapiente nesciunt ratione.</p>",
87+
"points": 95,
88+
"slug": "delete-me",
89+
"publications": [
90+
{"title": "Record 1 publication 1"},
91+
{"title": "Record 1 publication 2"},
92+
{"title": "Record 1 publication 3"},
93+
]
94+
},
95+
},
96+
{
97+
"id": "Different record",
98+
"fields": {
99+
"title": "Not the used record.",
100+
"description": "This is only used for multiple responses from MockAirtable",
101+
"external_link": "https://example.com/",
102+
"is_active": False,
103+
"rating": "5.5",
104+
"long_description": "",
105+
"points": 1,
106+
"slug": "not-the-used-record",
107+
},
108+
},
109+
{
110+
"id": "recRecordThree",
111+
"fields": {
112+
"title": "A third record.",
113+
"description": "This is only used for multiple responses from MockAirtable",
114+
"external_link": "https://example.com/",
115+
"is_active": False,
116+
"rating": "5.5",
117+
"long_description": "",
118+
"points": 1,
119+
"slug": "record-3",
120+
},
121+
},
122+
{
123+
"id": "recRecordFour",
124+
"fields": {
125+
"title": "A fourth record.",
126+
"description": "This is only used for multiple responses from MockAirtable",
127+
"external_link": "https://example.com/",
128+
"is_active": False,
129+
"rating": "5.5",
130+
"long_description": "",
131+
"points": 1,
132+
"slug": "record-4",
133+
"publications": [
134+
{"title": "Record 4 publication 1"},
135+
{"title": "Record 4 publication 2"},
136+
{"title": "Record 4 publication 3"},
137+
]
138+
},
139+
},
140+
]
141+
elif formula == match({"slug": "red-its-new-blue"}):
76142
return [
77143
{
78144
"id": "recNewRecordId",
@@ -101,7 +167,7 @@ def search_fn(field, value):
101167
},
102168
},
103169
]
104-
elif field == "slug" and value == "a-matching-slug":
170+
elif formula == match({"slug": "a-matching-slug"}):
105171
return [
106172
{
107173
"id": "recMatchedRecordId",
@@ -117,7 +183,7 @@ def search_fn(field, value):
117183
},
118184
},
119185
]
120-
elif field == "Page Slug" and value == "home":
186+
elif formula == match({"Page Slug": "home"}):
121187
return [
122188
{
123189
"id": "recHomePageId",
@@ -131,72 +197,14 @@ def search_fn(field, value):
131197
else:
132198
return []
133199

134-
MockAirtable.search.side_effect = search_fn
135-
136-
MockAirtable.get_all = mock.MagicMock("get_all")
137-
MockAirtable.get_all.return_value = [
138-
{
139-
"id": "recNewRecordId",
140-
"fields": {
141-
"title": "Red! It's the new blue!",
142-
"description": "Red is a scientifically proven color that moves faster than all other colors.",
143-
"external_link": "https://example.com/",
144-
"is_active": True,
145-
"rating": "1.5",
146-
"long_description": "<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Veniam laboriosam consequatur saepe. Repellat itaque dolores neque, impedit reprehenderit eum culpa voluptates harum sapiente nesciunt ratione.</p>",
147-
"points": 95,
148-
"slug": "delete-me",
149-
"publications": [
150-
{"title": "Record 1 publication 1"},
151-
{"title": "Record 1 publication 2"},
152-
{"title": "Record 1 publication 3"},
153-
]
154-
},
155-
},
156-
{
157-
"id": "Different record",
158-
"fields": {
159-
"title": "Not the used record.",
160-
"description": "This is only used for multiple responses from MockAirtable",
161-
"external_link": "https://example.com/",
162-
"is_active": False,
163-
"rating": "5.5",
164-
"long_description": "",
165-
"points": 1,
166-
"slug": "not-the-used-record",
167-
},
168-
},
169-
{
170-
"id": "recRecordThree",
171-
"fields": {
172-
"title": "A third record.",
173-
"description": "This is only used for multiple responses from MockAirtable",
174-
"external_link": "https://example.com/",
175-
"is_active": False,
176-
"rating": "5.5",
177-
"long_description": "",
178-
"points": 1,
179-
"slug": "record-3",
180-
},
181-
},
182-
{
183-
"id": "recRecordFour",
184-
"fields": {
185-
"title": "A fourth record.",
186-
"description": "This is only used for multiple responses from MockAirtable",
187-
"external_link": "https://example.com/",
188-
"is_active": False,
189-
"rating": "5.5",
190-
"long_description": "",
191-
"points": 1,
192-
"slug": "record-4",
193-
"publications": [
194-
{"title": "Record 4 publication 1"},
195-
{"title": "Record 4 publication 2"},
196-
{"title": "Record 4 publication 3"},
197-
]
198-
},
199-
},
200-
]
200+
MockTable.all.side_effect = all_fn
201+
202+
class MockApi(mock.Mock):
203+
def __init__(self, *args, **kwargs):
204+
super().__init__(*args, **kwargs)
205+
self._table = MockTable()
206+
207+
def table(self, base_id, table_name):
208+
return self._table
201209

202-
return MockAirtable
210+
return MockApi

tests/test_import.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class TestImportClass(TestCase):
1616
fixtures = ['test.json']
1717

1818
def setUp(self):
19-
airtable_patcher = patch("wagtail_airtable.importer.Airtable", new_callable=get_mock_airtable())
19+
airtable_patcher = patch("wagtail_airtable.importer.Api", new_callable=get_mock_airtable())
2020
self.mock_airtable = airtable_patcher.start()
2121
self.addCleanup(airtable_patcher.stop)
2222

@@ -114,12 +114,13 @@ def test_update_object(self):
114114
self.assertEqual(updated_result.record_id, "recNewRecordId")
115115
self.assertEqual(len(advert.publications.all()), 3)
116116

117-
@patch('wagtail_airtable.mixins.Airtable')
117+
@patch('wagtail_airtable.mixins.Api')
118118
def test_create_object(self, mixin_airtable):
119119
importer = AirtableModelImporter(model=Advert)
120120
self.assertFalse(Advert.objects.filter(slug="test-created").exists())
121121
self.assertFalse(Advert.objects.filter(airtable_record_id="test-created-id").exists())
122-
self.mock_airtable.get_all.return_value = [{
122+
self.mock_airtable._table.all.side_effect = None
123+
self.mock_airtable._table.all.return_value = [{
123124
"id": "test-created-id",
124125
"fields": {
125126
"title": "The created one",
@@ -155,12 +156,19 @@ def test_update_object_with_invalid_serialized_data(self):
155156
advert = Advert.objects.get(airtable_record_id="recNewRecordId")
156157
importer = AirtableModelImporter(model=Advert)
157158
self.assertNotEqual(advert.description, "Red is a scientifically proven..")
158-
self.mock_airtable.get_all()[0]['fields'] = {
159-
"SEO Description": "Red is a scientifically proven...",
160-
"External Link": "https://example.com/",
161-
"slug": "red-its-new-blue",
162-
"Rating": "2.5",
163-
}
159+
self.mock_airtable._table.all.side_effect = None
160+
self.mock_airtable._table.all.return_value = [
161+
{
162+
"id": "recNewRecordId",
163+
"fields": {
164+
"SEO Description": "Red is a scientifically proven...",
165+
"External Link": "https://example.com/",
166+
"slug": "red-its-new-blue",
167+
"Rating": "2.5",
168+
}
169+
},
170+
]
171+
164172
result = next(importer.run())
165173
self.assertEqual(result.errors, {'title': ['This field is required.']})
166174
advert.refresh_from_db()
@@ -191,12 +199,13 @@ def test_get_data_for_new_model(self):
191199
self.assertIsNone(data_for_new_model.get('id'))
192200
self.assertIsNone(data_for_new_model.get('pk'))
193201

194-
@patch('wagtail_airtable.mixins.Airtable')
202+
@patch('wagtail_airtable.mixins.Api')
195203
def test_create_page(self, mixin_airtable):
196204
importer = AirtableModelImporter(model=SimplePage)
197205
self.assertEqual(Page.objects.get(slug="home").get_children().count(), 0)
198206

199-
self.mock_airtable.get_all.return_value = [{
207+
self.mock_airtable._table.all.side_effect = None
208+
self.mock_airtable._table.all.return_value = [{
200209
"id": "test-created-page-id",
201210
"fields": {
202211
"title": "A simple page",
@@ -218,15 +227,16 @@ def test_create_page(self, mixin_airtable):
218227
self.assertEqual(page.intro, "How much more simple can it get? And the answer is none. None more simple.")
219228
self.assertFalse(page.live)
220229

221-
@patch('wagtail_airtable.mixins.Airtable')
230+
@patch('wagtail_airtable.mixins.Api')
222231
def test_create_and_publish_page(self, mixin_airtable):
223232
new_settings = copy.deepcopy(settings.AIRTABLE_IMPORT_SETTINGS)
224233
new_settings['tests.SimplePage']['AUTO_PUBLISH_NEW_PAGES'] = True
225234
with override_settings(AIRTABLE_IMPORT_SETTINGS=new_settings):
226235
importer = AirtableModelImporter(model=SimplePage)
227236
self.assertEqual(Page.objects.get(slug="home").get_children().count(), 0)
228237

229-
self.mock_airtable.get_all.return_value = [{
238+
self.mock_airtable._table.all.side_effect = None
239+
self.mock_airtable._table.all.return_value = [{
230240
"id": "test-created-page-id",
231241
"fields": {
232242
"title": "A simple page",
@@ -248,7 +258,7 @@ def test_create_and_publish_page(self, mixin_airtable):
248258
self.assertEqual(page.intro, "How much more simple can it get? And the answer is none. None more simple.")
249259
self.assertTrue(page.live)
250260

251-
@patch('wagtail_airtable.mixins.Airtable')
261+
@patch('wagtail_airtable.mixins.Api')
252262
def test_update_page(self, mixin_airtable):
253263
importer = AirtableModelImporter(model=SimplePage)
254264
parent_page = Page.objects.get(slug="home")
@@ -261,7 +271,8 @@ def test_update_page(self, mixin_airtable):
261271
parent_page.add_child(instance=page)
262272
self.assertEqual(page.revisions.count(), 0)
263273

264-
self.mock_airtable.get_all.return_value = [{
274+
self.mock_airtable._table.all.side_effect = None
275+
self.mock_airtable._table.all.return_value = [{
265276
"id": "test-created-page-id",
266277
"fields": {
267278
"title": "A simple page",
@@ -282,7 +293,7 @@ def test_update_page(self, mixin_airtable):
282293
self.assertEqual(page.intro, "How much more simple can it get? Oh, actually it can get more simple.")
283294
self.assertEqual(page.revisions.count(), 1)
284295

285-
@patch('wagtail_airtable.mixins.Airtable')
296+
@patch('wagtail_airtable.mixins.Api')
286297
def test_skip_update_page_if_unchanged(self, mixin_airtable):
287298
importer = AirtableModelImporter(model=SimplePage)
288299
parent_page = Page.objects.get(slug="home")
@@ -295,7 +306,8 @@ def test_skip_update_page_if_unchanged(self, mixin_airtable):
295306
parent_page.add_child(instance=page)
296307
self.assertEqual(page.revisions.count(), 0)
297308

298-
self.mock_airtable.get_all.return_value = [{
309+
self.mock_airtable._table.all.side_effect = None
310+
self.mock_airtable._table.all.return_value = [{
299311
"id": "test-created-page-id",
300312
"fields": {
301313
"title": "A simple page",
@@ -312,7 +324,7 @@ def test_skip_update_page_if_unchanged(self, mixin_airtable):
312324

313325
self.assertEqual(page.revisions.count(), 0)
314326

315-
@patch('wagtail_airtable.mixins.Airtable')
327+
@patch('wagtail_airtable.mixins.Api')
316328
def test_skip_update_page_if_locked(self, mixin_airtable):
317329
importer = AirtableModelImporter(model=SimplePage)
318330
parent_page = Page.objects.get(slug="home")
@@ -326,7 +338,8 @@ def test_skip_update_page_if_locked(self, mixin_airtable):
326338
parent_page.add_child(instance=page)
327339
self.assertEqual(page.revisions.count(), 0)
328340

329-
self.mock_airtable.get_all.return_value = [{
341+
self.mock_airtable._table.all.side_effect = None
342+
self.mock_airtable._table.all.return_value = [{
330343
"id": "test-created-page-id",
331344
"fields": {
332345
"title": "A simple page",

0 commit comments

Comments
 (0)