django-ctct 0.0.1.dev4__tar.gz → 0.0.1.dev6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/PKG-INFO +26 -13
  2. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/README.md +20 -10
  3. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/admin.py +29 -38
  4. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/apps.py +0 -10
  5. django_ctct-0.0.1.dev6/django_ctct/management/commands/__init__.py +0 -0
  6. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/management/commands/import_ctct.py +45 -95
  7. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/managers.py +116 -118
  8. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/migrations/0001_initial.py +24 -6
  9. django_ctct-0.0.1.dev6/django_ctct/migrations/__init__.py +0 -0
  10. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/models.py +137 -112
  11. django_ctct-0.0.1.dev6/django_ctct/signals.py +41 -0
  12. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/utils.py +5 -0
  13. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/views.py +0 -1
  14. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/pyproject.toml +3 -2
  15. django_ctct-0.0.1.dev4/django_ctct/signals.py +0 -93
  16. django_ctct-0.0.1.dev4/django_ctct/vendor.py +0 -108
  17. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/LICENSE +0 -0
  18. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/__init__.py +0 -0
  19. {django_ctct-0.0.1.dev4/django_ctct/migrations → django_ctct-0.0.1.dev6/django_ctct/management}/__init__.py +0 -0
  20. {django_ctct-0.0.1.dev4 → django_ctct-0.0.1.dev6}/django_ctct/urls.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: django-ctct
3
- Version: 0.0.1.dev4
3
+ Version: 0.0.1.dev6
4
4
  Summary: A Django interface for the Constant Contact API
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: Geoffrey Eisenbarth
7
8
  Author-email: geoffrey.eisenbarth@gmail.com
8
9
  Requires-Python: >=3.10
@@ -12,7 +13,9 @@ Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: django (>=3.2.0,<6.0.0)
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: django (>=4.2)
18
+ Requires-Dist: mypy (>=1.19.0,<2.0.0)
16
19
  Requires-Dist: pyjwt[crypto] (>=2.10.1,<3.0.0)
17
20
  Requires-Dist: ratelimit (>=2.2.1,<3.0.0)
18
21
  Requires-Dist: requests (>=2.32.3,<3.0.0)
@@ -25,7 +28,9 @@ Description-Content-Type: text/markdown
25
28
 
26
29
  This Django app provides a seamless interface to the Constant Contact API, allowing you to manage contacts, email campaigns, and other Constant Contact functionalities directly from your Django project.
27
30
 
28
- **Warning:** This package is under active development. While it is our intention to develop with a consistent API going forward, we will not make promises until a later version is released.
31
+ **Warning:** This package is under active development.
32
+ While it is our intention to develop with a consistent API going forward, we will not make promises until a later version is released.
33
+
29
34
 
30
35
  ## Installation
31
36
 
@@ -33,6 +38,7 @@ This Django app provides a seamless interface to the Constant Contact API, allow
33
38
  pip install django-ctct
34
39
  ```
35
40
 
41
+
36
42
  ## Configuration
37
43
 
38
44
  1) **Add to `INSTALLED_APPS`:**
@@ -61,6 +67,7 @@ urlpatterns = [
61
67
  # ... other URL patterns
62
68
  ]
63
69
  ```
70
+
64
71
  3) **ConstantContact API Credentials:**
65
72
 
66
73
  Head to https://app.constantcontact.com/pages/dma/portal/ to set up your application with ConstantContact.
@@ -110,17 +117,19 @@ CTCT_SYNC_ADMIN = False # Django admin CRUD operations will sync with ctct
110
117
  * `CTCT_PREVIEW_MESSAGE` will be blank by default.
111
118
 
112
119
  3) **Run Migrations:**
120
+
113
121
  ```bash
114
122
  > ./manage.py makemigrations
115
123
  > ./manage.py migrate
116
124
  ```
117
125
 
118
- 4) **Create Authenticatin Token:**
126
+ 4) **Create Authentication Token:**
119
127
 
120
128
  After the app has been installed and configured, you must generate your first auth token:
121
- * Open up the `CTCT_REDIRECT_URI` address in your browser
122
- * Use your ConstantContact credentials to log in
123
- * From this point forward `django-ctct` should use refresh tokens, so no need to manually log in again
129
+
130
+ * Open up the `CTCT_REDIRECT_URI` address in your browser
131
+ * Use your ConstantContact credentials to log in
132
+ * From this point forward `django-ctct` should use refresh tokens, so no need to manually log in again
124
133
 
125
134
 
126
135
  ## Usage
@@ -130,12 +139,16 @@ If you wish to import data from ConstantContact.com into your local database (re
130
139
  > ./manage.py import_ctct
131
140
  ```
132
141
 
133
- You will be asked before each model type is imported. **Note** ConstantContact does not provide a bulk API endpoint for fetching Campaign Activities, so depending on the size of your account, this might take some time and possible put you over their 10,000 request per day limit if you run it regularly.
142
+ You will be asked before each model type is imported.
143
+ **Note** ConstantContact does not provide a bulk API endpoint for fetching Campaign Activities, so depending on the size of your account, this might take some time and possible put you over their 10,000 request per day limit if you run it regularly.
134
144
 
135
- Since ConstantContact does not offer any webhooks, you will need to set up a cron job if you want your account to remain syncronized with ConstantContact's database. You can use the `--no-input` flag to bypass the interactive questions. The `--stats-only` flag is useful for running a cron job to keep EmailCampaign statistics updated.
145
+ Since ConstantContact does not offer any webhooks, you will need to set up a cron job if you want your account to remain syncronized with ConstantContact's database.
146
+ You can use the `--no-input` flag to bypass the interactive questions.
147
+ The `--stats-only` flag is useful for running a cron job to keep EmailCampaign statistics updated.
136
148
 
137
149
  If you wish to use the Django admin to interact with ConstantContact, you must explicitly set the `CTCT_USE_ADMIN` and `CTCT_SYNC_ADMIN` settings to `True`.
138
150
 
151
+
139
152
  ## Testing
140
153
 
141
154
  To install dev dependencies:
@@ -155,13 +168,13 @@ To run tests:
155
168
  > poetry run coverage report
156
169
  ```
157
170
 
171
+
158
172
  ## Contributing
159
173
 
160
174
  Once version 0.0.1 is released on PyPI, we hope to implement the following new features (in no particular order):
161
175
 
162
- * Support for API syncing using signals (`post_save`, `pre_delete`, `m2m_changed`, etc). This will be controlled by the `CTCT_SYNC_SIGNALS` setting. **Update** This probably won't work as desired since the primary object will be saved before related objects are.
163
- * Background task support using `django-tasks` (which hopefully will merge into Django). This will be controlled by the `CTCT_ENQUEUE_DEFAULT` setting.
164
- * Add `models.CheckConstraint` and `models.UniqueConstraint` constraints that are currently commented out.
176
+ * Background task support using `django-tasks` (which hopefully will merge into Django). This will be controlled by the `CTCT_ENQUEUE_DEFAULT` setting.
177
+ * Add `models.CheckConstraint` and `models.UniqueConstraint` constraints that are currently commented out.
165
178
 
166
179
 
167
180
  I'm always open to new suggestions, so please reach out on GitHub: https://github.com/geoffrey-eisenbarth/django-ctct/
@@ -4,7 +4,9 @@
4
4
 
5
5
  This Django app provides a seamless interface to the Constant Contact API, allowing you to manage contacts, email campaigns, and other Constant Contact functionalities directly from your Django project.
6
6
 
7
- **Warning:** This package is under active development. While it is our intention to develop with a consistent API going forward, we will not make promises until a later version is released.
7
+ **Warning:** This package is under active development.
8
+ While it is our intention to develop with a consistent API going forward, we will not make promises until a later version is released.
9
+
8
10
 
9
11
  ## Installation
10
12
 
@@ -12,6 +14,7 @@ This Django app provides a seamless interface to the Constant Contact API, allow
12
14
  pip install django-ctct
13
15
  ```
14
16
 
17
+
15
18
  ## Configuration
16
19
 
17
20
  1) **Add to `INSTALLED_APPS`:**
@@ -40,6 +43,7 @@ urlpatterns = [
40
43
  # ... other URL patterns
41
44
  ]
42
45
  ```
46
+
43
47
  3) **ConstantContact API Credentials:**
44
48
 
45
49
  Head to https://app.constantcontact.com/pages/dma/portal/ to set up your application with ConstantContact.
@@ -89,17 +93,19 @@ CTCT_SYNC_ADMIN = False # Django admin CRUD operations will sync with ctct
89
93
  * `CTCT_PREVIEW_MESSAGE` will be blank by default.
90
94
 
91
95
  3) **Run Migrations:**
96
+
92
97
  ```bash
93
98
  > ./manage.py makemigrations
94
99
  > ./manage.py migrate
95
100
  ```
96
101
 
97
- 4) **Create Authenticatin Token:**
102
+ 4) **Create Authentication Token:**
98
103
 
99
104
  After the app has been installed and configured, you must generate your first auth token:
100
- * Open up the `CTCT_REDIRECT_URI` address in your browser
101
- * Use your ConstantContact credentials to log in
102
- * From this point forward `django-ctct` should use refresh tokens, so no need to manually log in again
105
+
106
+ * Open up the `CTCT_REDIRECT_URI` address in your browser
107
+ * Use your ConstantContact credentials to log in
108
+ * From this point forward `django-ctct` should use refresh tokens, so no need to manually log in again
103
109
 
104
110
 
105
111
  ## Usage
@@ -109,12 +115,16 @@ If you wish to import data from ConstantContact.com into your local database (re
109
115
  > ./manage.py import_ctct
110
116
  ```
111
117
 
112
- You will be asked before each model type is imported. **Note** ConstantContact does not provide a bulk API endpoint for fetching Campaign Activities, so depending on the size of your account, this might take some time and possible put you over their 10,000 request per day limit if you run it regularly.
118
+ You will be asked before each model type is imported.
119
+ **Note** ConstantContact does not provide a bulk API endpoint for fetching Campaign Activities, so depending on the size of your account, this might take some time and possible put you over their 10,000 request per day limit if you run it regularly.
113
120
 
114
- Since ConstantContact does not offer any webhooks, you will need to set up a cron job if you want your account to remain syncronized with ConstantContact's database. You can use the `--no-input` flag to bypass the interactive questions. The `--stats-only` flag is useful for running a cron job to keep EmailCampaign statistics updated.
121
+ Since ConstantContact does not offer any webhooks, you will need to set up a cron job if you want your account to remain syncronized with ConstantContact's database.
122
+ You can use the `--no-input` flag to bypass the interactive questions.
123
+ The `--stats-only` flag is useful for running a cron job to keep EmailCampaign statistics updated.
115
124
 
116
125
  If you wish to use the Django admin to interact with ConstantContact, you must explicitly set the `CTCT_USE_ADMIN` and `CTCT_SYNC_ADMIN` settings to `True`.
117
126
 
127
+
118
128
  ## Testing
119
129
 
120
130
  To install dev dependencies:
@@ -134,13 +144,13 @@ To run tests:
134
144
  > poetry run coverage report
135
145
  ```
136
146
 
147
+
137
148
  ## Contributing
138
149
 
139
150
  Once version 0.0.1 is released on PyPI, we hope to implement the following new features (in no particular order):
140
151
 
141
- * Support for API syncing using signals (`post_save`, `pre_delete`, `m2m_changed`, etc). This will be controlled by the `CTCT_SYNC_SIGNALS` setting. **Update** This probably won't work as desired since the primary object will be saved before related objects are.
142
- * Background task support using `django-tasks` (which hopefully will merge into Django). This will be controlled by the `CTCT_ENQUEUE_DEFAULT` setting.
143
- * Add `models.CheckConstraint` and `models.UniqueConstraint` constraints that are currently commented out.
152
+ * Background task support using `django-tasks` (which hopefully will merge into Django). This will be controlled by the `CTCT_ENQUEUE_DEFAULT` setting.
153
+ * Add `models.CheckConstraint` and `models.UniqueConstraint` constraints that are currently commented out.
144
154
 
145
155
 
146
156
  I'm always open to new suggestions, so please reach out on GitHub: https://github.com/geoffrey-eisenbarth/django-ctct/
@@ -1,12 +1,11 @@
1
1
  import functools
2
- from typing import TypeVar, ParamSpec, Generic, Optional, Callable, Iterable
2
+ from typing import TypeVar, ParamSpec, Generic, Callable, Iterable
3
3
  from requests.exceptions import HTTPError
4
4
 
5
5
  from django import forms
6
6
  from django.conf import settings
7
7
  from django.contrib import admin, messages
8
8
  from django.contrib.auth import get_user_model
9
- from django.db.models import signals
10
9
  from django.db.models import Model, QuerySet, When, Case, F, FloatField
11
10
  from django.db.models.functions import Cast
12
11
  from django.forms import ModelForm, BaseFormSet
@@ -24,7 +23,6 @@ from django_ctct.models import (
24
23
  EmailCampaign, CampaignActivity, CampaignSummary,
25
24
  )
26
25
  from django_ctct.signals import remote_save, remote_delete
27
- from django_ctct.vendor import mute_signals
28
26
 
29
27
 
30
28
  P = ParamSpec('P')
@@ -51,7 +49,7 @@ def catch_api_errors(func: Callable[P, None]) -> Callable[P, None]:
51
49
  if getattr(settings, 'CTCT_RAISE_FOR_API', False):
52
50
  raise e
53
51
  else:
54
- self, request, *_ = args
52
+ self, request, *x = args
55
53
  assert isinstance(self, admin.ModelAdmin)
56
54
  assert isinstance(request, HttpRequest)
57
55
  self.message_user(
@@ -81,7 +79,7 @@ class ViewModelAdmin(admin.ModelAdmin[Model]):
81
79
  def has_add_permission(
82
80
  self,
83
81
  request: HttpRequest,
84
- obj: Optional[Model] = None,
82
+ obj: Model | None = None,
85
83
  ) -> bool:
86
84
  """Prevent creation in the Django admin."""
87
85
  return False
@@ -89,7 +87,7 @@ class ViewModelAdmin(admin.ModelAdmin[Model]):
89
87
  def has_change_permission(
90
88
  self,
91
89
  request: HttpRequest,
92
- obj: Optional[Model] = None,
90
+ obj: Model | None = None,
93
91
  ) -> bool:
94
92
  """Prevent updates in the Django admin."""
95
93
  return False
@@ -97,7 +95,7 @@ class ViewModelAdmin(admin.ModelAdmin[Model]):
97
95
  def get_readonly_fields(
98
96
  self,
99
97
  request: HttpRequest,
100
- obj: Optional[Model] = None,
98
+ obj: Model | None = None,
101
99
  ) -> tuple[str, ...]:
102
100
  """Prevent updates in the Django admin."""
103
101
  if obj is not None:
@@ -113,7 +111,7 @@ class ViewModelAdmin(admin.ModelAdmin[Model]):
113
111
  def has_delete_permission(
114
112
  self,
115
113
  request: HttpRequest,
116
- obj: Optional[Model] = None,
114
+ obj: Model | None = None,
117
115
  ) -> bool:
118
116
  """Allow superusers to delete objects."""
119
117
  return request.user.is_superuser
@@ -124,35 +122,30 @@ class RemoteModelAdmin(
124
122
  ):
125
123
  """Facilitate remote saving and deleting."""
126
124
 
127
- # ChangeView
128
- @property
129
- def remote_sync(self) -> bool:
130
- sync_admin = getattr(settings, 'CTCT_SYNC_ADMIN', False)
131
- sync_signals = getattr(settings, 'CTCT_SYNC_SIGNALS', False)
132
- return sync_admin and not sync_signals
125
+ sync_admin: bool = getattr(settings, 'CTCT_SYNC_ADMIN', False)
133
126
 
127
+ # ChangeView
134
128
  @catch_api_errors
135
129
  def delete_model(self, request: HttpRequest, obj: Model) -> None:
136
130
  obj.delete()
137
- if self.remote_sync:
131
+ if self.sync_admin:
138
132
  remote_delete(sender=self.model, instance=obj)
139
133
 
140
134
  @catch_api_errors
141
135
  def delete_queryset(
142
136
  self,
143
137
  request: HttpRequest,
144
- queryset: QuerySet[CTCTEndpointModel],
138
+ queryset: QuerySet[E],
145
139
  ) -> None:
146
- if self.remote_sync:
140
+ if self.sync_admin:
147
141
  queryset.model.remote.bulk_delete(queryset)
148
- with mute_signals(signals.pre_delete):
149
- queryset.delete()
142
+ queryset.delete()
150
143
 
151
144
  @catch_api_errors
152
145
  def save_related(
153
146
  self,
154
147
  request: HttpRequest,
155
- form: ModelForm[CTCTEndpointModel],
148
+ form: ModelForm[E],
156
149
  formsets: list[BaseFormSet[ModelForm[Model]]],
157
150
  change: bool,
158
151
  ) -> None:
@@ -164,9 +157,8 @@ class RemoteModelAdmin(
164
157
  for saving objects remotely.
165
158
 
166
159
  """
167
- with mute_signals(signals.m2m_changed):
168
- # ManyToMany information is sent to CTCT in PUT call
169
- form.save_m2m()
160
+ # ManyToMany information is sent to CTCT in PUT call
161
+ form.save_m2m()
170
162
  for formset in formsets:
171
163
  self.save_formset(request, form, formset, change=change)
172
164
  self.save_remotely(request, form, formsets, change)
@@ -175,11 +167,11 @@ class RemoteModelAdmin(
175
167
  def save_remotely(
176
168
  self,
177
169
  request: HttpRequest,
178
- form: ModelForm[CTCTEndpointModel],
170
+ form: ModelForm[E],
179
171
  formsets: list[BaseFormSet[ModelForm[Model]]],
180
172
  change: bool,
181
173
  ) -> None:
182
- if self.remote_sync:
174
+ if self.sync_admin:
183
175
  # Remote save the primary object after related objects have been saved
184
176
  remote_save(
185
177
  sender=self.model,
@@ -285,7 +277,7 @@ class ContactNoteInline(admin.TabularInline[ContactNote, Contact]):
285
277
  def has_change_permission(
286
278
  self,
287
279
  request: HttpRequest,
288
- obj: Optional[Contact] = None, # type: ignore[override]
280
+ obj: Contact | None = None, # type: ignore[override]
289
281
  ) -> bool:
290
282
  return False
291
283
 
@@ -295,7 +287,7 @@ class ContactCustomFieldInline(
295
287
  ):
296
288
 
297
289
  model = ContactCustomField
298
- excldue = ('api_id', )
290
+ exclude = ('api_id', )
299
291
 
300
292
  extra = 0
301
293
 
@@ -370,7 +362,7 @@ class ContactAdmin(RemoteModelAdmin[Contact]):
370
362
  def get_readonly_fields(
371
363
  self,
372
364
  request: HttpRequest,
373
- obj: Optional[Contact] = None,
365
+ obj: Contact | None = None,
374
366
  ) -> list[str]:
375
367
  readonly_fields = list(Contact.API_READONLY_FIELDS)
376
368
  if obj and obj.opt_out_source and not request.user.is_superuser:
@@ -401,9 +393,8 @@ class ContactAdmin(RemoteModelAdmin[Contact]):
401
393
  instance.author = request.user
402
394
  instance.save()
403
395
 
404
- with mute_signals(signals.m2m_changed):
405
- # ManyToMany information is sent to CTCT in PUT call
406
- formset.save_m2m()
396
+ # ManyToMany information is sent to CTCT in PUT call
397
+ formset.save_m2m()
407
398
 
408
399
 
409
400
  class ContactNoteAuthorFilter(admin.SimpleListFilter):
@@ -416,7 +407,7 @@ class ContactNoteAuthorFilter(admin.SimpleListFilter):
416
407
  self,
417
408
  request: HttpRequest,
418
409
  model_admin: admin.ModelAdmin[Model],
419
- ) -> Optional[Iterable[tuple[str, str]]]:
410
+ ) -> Iterable[tuple[str, str]] | None:
420
411
  authors = get_user_model().objects.exclude(notes__isnull=True)
421
412
  return [(str(obj.id), str(obj)) for obj in authors]
422
413
 
@@ -473,7 +464,7 @@ class ContactNoteAdmin(RemoteSyncMixin, ViewModelAdmin):
473
464
  def has_delete_permission(
474
465
  self,
475
466
  request: HttpRequest,
476
- obj: Optional[Model] = None,
467
+ obj: Model | None = None,
477
468
  ) -> bool:
478
469
  """Allow superusers to delete Notes."""
479
470
  return request.user.is_superuser
@@ -516,7 +507,7 @@ class CampaignActivityInline(
516
507
  def get_readonly_fields(
517
508
  self,
518
509
  request: HttpRequest,
519
- obj: Optional[CampaignActivity] = None,
510
+ obj: CampaignActivity | None = None,
520
511
  ) -> list[str]:
521
512
  readonly_fields = list(CampaignActivity.API_READONLY_FIELDS)
522
513
  if obj and obj.current_status == 'DONE':
@@ -559,7 +550,7 @@ class EmailCampaignAdmin(RemoteModelAdmin[EmailCampaign]):
559
550
  def get_readonly_fields(
560
551
  self,
561
552
  request: HttpRequest,
562
- obj: Optional[EmailCampaign] = None,
553
+ obj: EmailCampaign | None = None,
563
554
  ) -> list[str]:
564
555
  readonly_fields = list(EmailCampaign.API_READONLY_FIELDS)
565
556
  if obj and obj.current_status == 'DONE':
@@ -570,20 +561,20 @@ class EmailCampaignAdmin(RemoteModelAdmin[EmailCampaign]):
570
561
  def save_remotely(
571
562
  self,
572
563
  request: HttpRequest,
573
- form: ModelForm[EmailCampaign], # type: ignore[override]
564
+ form: ModelForm[EmailCampaign],
574
565
  formsets: list[BaseFormSet[ModelForm[Model]]],
575
566
  change: bool,
576
567
  ) -> None:
577
- if self.remote_sync:
568
+ if self.sync_admin:
578
569
 
579
570
  campaign = form.instance
580
571
  activity = formsets[0][0].instance
581
572
 
582
573
  # Handle remote saving the EmailCampaign
574
+ # NOTE: The only EmailCampaign field that can be updated is 'name'
583
575
  campaign_created = not change
584
576
  campaign_updated = change and ('name' in form.changed_data)
585
577
  if campaign_created or campaign_updated:
586
- # The only EmailCampaign field that can be updated is 'name'
587
578
  remote_save(
588
579
  sender=self.model,
589
580
  instance=campaign,
@@ -1,5 +1,4 @@
1
1
  from django.apps import AppConfig
2
- from django.db.models.signals import post_save, m2m_changed, pre_delete
3
2
  from django.conf import settings
4
3
  from django.core.exceptions import ImproperlyConfigured
5
4
  from django.utils.translation import gettext_lazy as _
@@ -24,12 +23,3 @@ class CTCTConfig(AppConfig):
24
23
  f"[django-ctct] {value} must be defined in settings.py."
25
24
  )
26
25
  raise ImproperlyConfigured(message)
27
-
28
- # Hook up the signals
29
- from django_ctct.signals import (
30
- remote_save, remote_delete, remote_update_m2m
31
- )
32
- if getattr(settings, 'CTCT_SYNC_SIGNALS', False):
33
- post_save.connect(remote_save)
34
- pre_delete.connect(remote_delete)
35
- m2m_changed.connect(remote_update_m2m)