Skip to content

Commit 431fcfa

Browse files
authored
Merge pull request #305 from edx/schen/ECOM-5402
ECOM-5402 Delete the drupal node when program is hard deleted
2 parents faef420 + cfe5ad4 commit 431fcfa

File tree

7 files changed

+170
-50
lines changed

7 files changed

+170
-50
lines changed

course_discovery/apps/course_metadata/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@
44
class CourseMetadataConfig(AppConfig):
55
name = 'course_discovery.apps.course_metadata'
66
verbose_name = 'Course Metadata'
7+
8+
def ready(self):
9+
super(CourseMetadataConfig, self).ready()
10+
# noinspection PyUnresolvedReferences
11+
import course_discovery.apps.course_metadata.signals # pylint: disable=unused-variable

course_discovery/apps/course_metadata/publishers.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ def __init__(self, program_before=None):
8989
'title': program_before.title,
9090
}
9191

92+
def _get_api_client(self, program):
93+
if not program.partner.has_marketing_site:
94+
return
95+
96+
if not (program.partner.marketing_site_api_username and program.partner.marketing_site_api_password):
97+
msg = 'Marketing Site API credentials are not properly configured for Partner [{partner}]!'.format(
98+
partner=program.partner.short_code)
99+
raise ProgramPublisherException(msg)
100+
101+
if self.data_before and \
102+
all(self.data_before[key] == getattr(program, key) for key in ['title', 'status', 'type']):
103+
# We don't need to publish to marketing site because
104+
# nothing we care about has changed. This would save at least 4 network calls
105+
return
106+
107+
return MarketingSiteAPIClient(
108+
program.partner.marketing_site_api_username,
109+
program.partner.marketing_site_api_password,
110+
program.partner.marketing_site_url_root
111+
)
112+
92113
def _get_node_data(self, program, user_id):
93114
return {
94115
'type': str(program.type).lower(),
@@ -128,34 +149,25 @@ def _create_node(self, api_client, node_data):
128149
else:
129150
raise ProgramPublisherException("Marketing site page creation failed!")
130151

131-
def publish_program(self, program):
132-
if not program.partner.has_marketing_site:
133-
return
134-
135-
if not (program.partner.marketing_site_api_username and program.partner.marketing_site_api_password):
136-
msg = 'Marketing Site API credentials are not properly configured for Partner [{partner}]!'.format(
137-
partner=program.partner.short_code)
138-
raise ProgramPublisherException(msg)
139-
140-
if self.data_before and \
141-
self.data_before.get('title') == program.title and \
142-
self.data_before.get('status') == program.status and \
143-
self.data_before.get('type') == program.type:
144-
# We don't need to publish to marketing site because
145-
# nothing we care about has changed. This would save at least 4 network calls
146-
return
147-
148-
api_client = MarketingSiteAPIClient(
149-
program.partner.marketing_site_api_username,
150-
program.partner.marketing_site_api_password,
151-
program.partner.marketing_site_url_root
152-
)
152+
def _delete_node(self, api_client, nid):
153+
node_url = '{root}/node.json/{nid}'.format(root=api_client.api_url, nid=nid)
154+
api_client.api_session.delete(node_url)
153155

154-
node_data = self._get_node_data(program, api_client.user_id)
155-
nid = self._get_node_id(api_client, program.uuid)
156-
if nid:
157-
# We would like to edit the existing node
158-
self._edit_node(api_client, nid, node_data)
159-
else:
160-
# We should create a new node
161-
self._create_node(api_client, node_data)
156+
def publish_program(self, program):
157+
api_client = self._get_api_client(program)
158+
if api_client:
159+
node_data = self._get_node_data(program, api_client.user_id)
160+
nid = self._get_node_id(api_client, program.uuid)
161+
if nid:
162+
# We would like to edit the existing node
163+
self._edit_node(api_client, nid, node_data)
164+
else:
165+
# We should create a new node
166+
self._create_node(api_client, node_data)
167+
168+
def delete_program(self, program):
169+
api_client = self._get_api_client(program)
170+
if api_client:
171+
nid = self._get_node_id(api_client, program.uuid)
172+
if nid:
173+
self._delete_node(api_client, nid)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from django.db.models.signals import pre_delete
2+
from django.dispatch import receiver
3+
import waffle
4+
5+
from course_discovery.apps.course_metadata.models import Program
6+
from course_discovery.apps.course_metadata.publishers import MarketingSitePublisher
7+
8+
9+
@receiver(pre_delete, sender=Program)
10+
def delete_program(sender, instance, **kwargs): # pylint: disable=unused-argument
11+
if waffle.switch_is_active('publish_program_to_marketing_site') and \
12+
instance.partner.has_marketing_site:
13+
publisher = MarketingSitePublisher()
14+
publisher.delete_program(instance)

course_discovery/apps/course_metadata/tests/mixins.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import json
22

3+
from django.test import TestCase
34
from factory.fuzzy import FuzzyText, FuzzyInteger
45
import responses
56

67
from course_discovery.apps.core.tests.utils import FuzzyUrlRoot
78

89

9-
class MarketingSiteAPIClientTestMixin(object):
10+
class MarketingSiteAPIClientTestMixin(TestCase):
1011
"""
1112
The mixing to help mock the responses for marketing site API Client
1213
"""
@@ -70,6 +71,9 @@ def mock_user_id_response(self, status):
7071
match_querystring=True
7172
)
7273

74+
def assert_responses_call_count(self, count):
75+
self.assertEqual(len(responses.calls), count)
76+
7377

7478
class MarketingSitePublisherTestMixin(MarketingSiteAPIClientTestMixin):
7579
"""
@@ -121,3 +125,12 @@ def mock_node_create(self, response_data, status):
121125
content_type='application/json',
122126
status=status
123127
)
128+
129+
def mock_node_delete(self, status):
130+
responses.add(
131+
responses.DELETE,
132+
'{root}/node.json/{nid}'.format(root=self.api_root, nid=self.nid),
133+
body='',
134+
content_type='text/html',
135+
status=status
136+
)

course_discovery/apps/course_metadata/tests/test_models.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ class TestAbstractValueModel(AbstractValueModel):
268268

269269

270270
@ddt.ddt
271-
class ProgramTests(MarketingSitePublisherTestMixin, TestCase):
271+
class ProgramTests(MarketingSitePublisherTestMixin):
272272
"""Tests of the Program model."""
273273

274274
def setUp(self):
@@ -395,7 +395,12 @@ def test_is_active(self, status):
395395
def test_save_without_publish(self):
396396
self.program.title = FuzzyText().fuzz()
397397
self.program.save()
398-
self.assertEqual(len(responses.calls), 0)
398+
self.assert_responses_call_count(0)
399+
400+
@responses.activate
401+
def test_delete_without_publish(self):
402+
self.program.delete()
403+
self.assert_responses_call_count(0)
399404

400405
@responses.activate
401406
def test_save_and_publish_success(self):
@@ -409,8 +414,7 @@ def test_save_and_publish_success(self):
409414
toggle_switch('publish_program_to_marketing_site', True)
410415
self.program.title = FuzzyText().fuzz()
411416
self.program.save()
412-
self.assertEqual(len(responses.calls), 6)
413-
toggle_switch('publish_program_to_marketing_site', False)
417+
self.assert_responses_call_count(6)
414418

415419
@responses.activate
416420
def test_save_and_no_marketing_site(self):
@@ -419,8 +423,28 @@ def test_save_and_no_marketing_site(self):
419423
toggle_switch('publish_program_to_marketing_site', True)
420424
self.program.title = FuzzyText().fuzz()
421425
self.program.save()
422-
self.assertEqual(len(responses.calls), 0)
423-
toggle_switch('publish_program_to_marketing_site', False)
426+
self.assert_responses_call_count(0)
427+
428+
@responses.activate
429+
def test_delete_and_publish_success(self):
430+
self.program.partner.marketing_site_url_root = self.api_root
431+
self.program.partner.marketing_site_api_username = self.username
432+
self.program.partner.marketing_site_api_password = self.password
433+
self.program.save()
434+
self.mock_api_client(200)
435+
self.mock_node_retrieval(self.program.uuid)
436+
self.mock_node_delete(204)
437+
toggle_switch('publish_program_to_marketing_site', True)
438+
self.program.delete()
439+
self.assert_responses_call_count(5)
440+
441+
@responses.activate
442+
def test_delete_and_no_marketing_site(self):
443+
self.program.partner.marketing_site_url_root = None
444+
self.program.save()
445+
toggle_switch('publish_program_to_marketing_site', True)
446+
self.program.delete()
447+
self.assert_responses_call_count(0)
424448

425449
def test_course_update_caught_exception(self):
426450
""" Test that the index update process failing will not cause the program save to error """

course_discovery/apps/course_metadata/tests/test_publishers.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.test import TestCase
21
import responses
32

43
from course_discovery.apps.course_metadata.publishers import (
@@ -14,7 +13,7 @@
1413
from course_discovery.apps.course_metadata.models import Program
1514

1615

17-
class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin, TestCase):
16+
class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin):
1817
"""
1918
Unit test cases for MarketinSiteAPIClient
2019
"""
@@ -30,7 +29,7 @@ def setUp(self):
3029
def test_init_session(self):
3130
self.mock_login_response(200)
3231
session = self.api_client.init_session
33-
self.assertEqual(len(responses.calls), 2)
32+
self.assert_responses_call_count(2)
3433
self.assertIsNotNone(session)
3534

3635
@responses.activate
@@ -44,7 +43,7 @@ def test_csrf_token(self):
4443
self.mock_login_response(200)
4544
self.mock_csrf_token_response(200)
4645
csrf_token = self.api_client.csrf_token
47-
self.assertEqual(len(responses.calls), 3)
46+
self.assert_responses_call_count(3)
4847
self.assertEqual(self.csrf_token, csrf_token)
4948

5049
@responses.activate
@@ -59,7 +58,7 @@ def test_user_id(self):
5958
self.mock_login_response(200)
6059
self.mock_user_id_response(200)
6160
user_id = self.api_client.user_id
62-
self.assertEqual(len(responses.calls), 3)
61+
self.assert_responses_call_count(3)
6362
self.assertEqual(self.user_id, user_id)
6463

6564
@responses.activate
@@ -74,7 +73,7 @@ def test_api_session(self):
7473
self.mock_login_response(200)
7574
self.mock_csrf_token_response(200)
7675
api_session = self.api_client.api_session
77-
self.assertEqual(len(responses.calls), 3)
76+
self.assert_responses_call_count(3)
7877
self.assertIsNotNone(api_session)
7978
self.assertEqual(api_session.headers.get('Content-Type'), 'application/json')
8079
self.assertEqual(api_session.headers.get('X-CSRF-Token'), self.csrf_token)
@@ -87,7 +86,7 @@ def test_api_session_failed(self):
8786
self.api_client.api_session # pylint: disable=pointless-statement
8887

8988

90-
class MarketingSitePublisherTests(MarketingSitePublisherTestMixin, TestCase):
89+
class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
9190
"""
9291
Unit test cases for the MarketingSitePublisher
9392
"""
@@ -125,7 +124,7 @@ def test_get_node_id(self):
125124
self.mock_node_retrieval(self.program.uuid)
126125
publisher = MarketingSitePublisher()
127126
node_id = publisher._get_node_id(self.api_client, self.program.uuid) # pylint: disable=protected-access
128-
self.assertEqual(len(responses.calls), 4)
127+
self.assert_responses_call_count(4)
129128
self.assertEqual(node_id, self.nid)
130129

131130
@responses.activate
@@ -143,7 +142,7 @@ def test_edit_node(self):
143142
publisher = MarketingSitePublisher()
144143
publish_data = publisher._get_node_data(self.program, self.user_id) # pylint: disable=protected-access
145144
publisher._edit_node(self.api_client, self.nid, publish_data) # pylint: disable=protected-access
146-
self.assertEqual(len(responses.calls), 4)
145+
self.assert_responses_call_count(4)
147146

148147
@responses.activate
149148
def test_edit_node_failed(self):
@@ -189,7 +188,7 @@ def test_publish_program_create(self):
189188
self.mock_node_create(expected, 201)
190189
publisher = MarketingSitePublisher()
191190
publisher.publish_program(self.program)
192-
self.assertEqual(len(responses.calls), 6)
191+
self.assert_responses_call_count(6)
193192

194193
@responses.activate
195194
def test_publish_program_edit(self):
@@ -198,7 +197,7 @@ def test_publish_program_edit(self):
198197
self.mock_node_edit(200)
199198
publisher = MarketingSitePublisher()
200199
publisher.publish_program(self.program)
201-
self.assertEqual(len(responses.calls), 6)
200+
self.assert_responses_call_count(6)
202201

203202
@responses.activate
204203
def test_publish_modified_program(self):
@@ -208,11 +207,38 @@ def test_publish_modified_program(self):
208207
program_before = ProgramFactory()
209208
publisher = MarketingSitePublisher(program_before)
210209
publisher.publish_program(self.program)
211-
self.assertEqual(len(responses.calls), 6)
210+
self.assert_responses_call_count(6)
212211

213212
@responses.activate
214213
def test_publish_unmodified_program(self):
215214
self.mock_api_client(200)
216215
publisher = MarketingSitePublisher(self.program)
217216
publisher.publish_program(self.program)
218-
self.assertEqual(len(responses.calls), 0)
217+
self.assert_responses_call_count(0)
218+
219+
@responses.activate
220+
def test_publish_program_no_credential(self):
221+
self.program.partner.marketing_site_api_password = None
222+
self.program.partner.marketing_site_api_username = None
223+
self.program.save() # pylint: disable=no-member
224+
publisher = MarketingSitePublisher()
225+
with self.assertRaises(ProgramPublisherException):
226+
publisher.publish_program(self.program)
227+
self.assert_responses_call_count(0)
228+
229+
@responses.activate
230+
def test_publish_delete_program(self):
231+
self.mock_api_client(200)
232+
self.mock_node_retrieval(self.program.uuid)
233+
self.mock_node_delete(204)
234+
publisher = MarketingSitePublisher()
235+
publisher.delete_program(self.program)
236+
self.assert_responses_call_count(5)
237+
238+
@responses.activate
239+
def test_publish_delete_non_existent_program(self):
240+
self.mock_api_client(200)
241+
self.mock_node_retrieval(self.program.uuid, exists=False)
242+
publisher = MarketingSitePublisher()
243+
publisher.delete_program(self.program)
244+
self.assert_responses_call_count(4)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# pylint: disable=no-member
2+
from unittest.mock import patch
3+
4+
from django.test import TestCase
5+
6+
from course_discovery.apps.course_metadata.tests import factories, toggle_switch
7+
8+
9+
MARKETING_SITE_PUBLISHERS_MODULE = 'course_discovery.apps.course_metadata.publishers.MarketingSitePublisher'
10+
11+
12+
@patch(MARKETING_SITE_PUBLISHERS_MODULE + '.delete_program')
13+
class SignalsTest(TestCase):
14+
def setUp(self):
15+
super(SignalsTest, self).setUp()
16+
self.program = factories.ProgramFactory()
17+
18+
def test_delete_program_signal_no_publish(self, delete_program_mock):
19+
toggle_switch('publish_program_to_marketing_site', False)
20+
self.program.delete()
21+
self.assertFalse(delete_program_mock.called)
22+
23+
def test_delete_program_signal_with_publish(self, delete_program_mock):
24+
toggle_switch('publish_program_to_marketing_site', True)
25+
self.program.delete()
26+
delete_program_mock.assert_called_once_with(self.program)

0 commit comments

Comments
 (0)