fcm-django 3.2.0__py3-none-any.whl
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.
- fcm_django/__init__.py +16 -0
- fcm_django/admin.py +278 -0
- fcm_django/api/__init__.py +10 -0
- fcm_django/api/rest_framework.py +190 -0
- fcm_django/api/tastypie.py +26 -0
- fcm_django/apps.py +8 -0
- fcm_django/fields.py +127 -0
- fcm_django/migrations/0001_initial.py +81 -0
- fcm_django/migrations/0002_auto_20160808_1645.py +25 -0
- fcm_django/migrations/0003_auto_20170313_1314.py +21 -0
- fcm_django/migrations/0004_auto_20181128_1642.py +20 -0
- fcm_django/migrations/0005_auto_20170808_1145.py +28 -0
- fcm_django/migrations/0006_auto_20210802_1140.py +25 -0
- fcm_django/migrations/0007_auto_20211001_1440.py +16 -0
- fcm_django/migrations/0008_auto_20211224_1205.py +20 -0
- fcm_django/migrations/0009_alter_fcmdevice_user.py +27 -0
- fcm_django/migrations/0010_unique_registration_id.py +51 -0
- fcm_django/migrations/0011_fcmdevice_fcm_django_registration_id_user_id_idx.py +50 -0
- fcm_django/migrations/__init__.py +0 -0
- fcm_django/models.py +732 -0
- fcm_django/settings.py +66 -0
- fcm_django/signals.py +3 -0
- fcm_django/types.py +90 -0
- fcm_django-3.2.0.dist-info/METADATA +592 -0
- fcm_django-3.2.0.dist-info/RECORD +28 -0
- fcm_django-3.2.0.dist-info/WHEEL +5 -0
- fcm_django-3.2.0.dist-info/licenses/LICENSE.txt +20 -0
- fcm_django-3.2.0.dist-info/top_level.txt +1 -0
fcm_django/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__author__ = "xTrinch"
|
|
2
|
+
__email__ = "mojca.rojko@gmail.com"
|
|
3
|
+
__version__ = "3.2.0"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NotificationError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from django import VERSION as DJANGO_VERSION
|
|
12
|
+
|
|
13
|
+
if DJANGO_VERSION < (3, 2):
|
|
14
|
+
default_app_config = "fcm_django.apps.FcmDjangoConfig"
|
|
15
|
+
except ImportError:
|
|
16
|
+
pass
|
fcm_django/admin.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import swapper
|
|
4
|
+
from django.apps import apps
|
|
5
|
+
from django.contrib import admin, messages
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
from django.utils.translation import ngettext_lazy
|
|
8
|
+
from firebase_admin.exceptions import FirebaseError
|
|
9
|
+
from firebase_admin.messaging import (
|
|
10
|
+
ErrorInfo,
|
|
11
|
+
Message,
|
|
12
|
+
Notification,
|
|
13
|
+
SendResponse,
|
|
14
|
+
TopicManagementResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from fcm_django.models import fcm_error_list
|
|
18
|
+
from fcm_django.settings import FCM_DJANGO_SETTINGS as SETTINGS
|
|
19
|
+
from fcm_django.types import FirebaseResponseDict
|
|
20
|
+
|
|
21
|
+
User = apps.get_model(*SETTINGS["USER_MODEL"].split("."))
|
|
22
|
+
|
|
23
|
+
FCMDevice = swapper.load_model("fcm_django", "fcmdevice")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DeviceAdmin(admin.ModelAdmin):
|
|
27
|
+
list_display = (
|
|
28
|
+
"__str__",
|
|
29
|
+
"device_id",
|
|
30
|
+
"name",
|
|
31
|
+
"type",
|
|
32
|
+
"user",
|
|
33
|
+
"active",
|
|
34
|
+
"date_created",
|
|
35
|
+
)
|
|
36
|
+
list_filter = (
|
|
37
|
+
"active",
|
|
38
|
+
"type",
|
|
39
|
+
)
|
|
40
|
+
actions = (
|
|
41
|
+
"send_message",
|
|
42
|
+
"send_bulk_message",
|
|
43
|
+
"subscribe_to_topic",
|
|
44
|
+
"bulk_subscribe_to_topic",
|
|
45
|
+
"unsubscribe_to_topic",
|
|
46
|
+
"bulk_unsubscribe_to_topic",
|
|
47
|
+
"send_topic_message",
|
|
48
|
+
"enable",
|
|
49
|
+
"disable",
|
|
50
|
+
)
|
|
51
|
+
raw_id_fields = ("user",)
|
|
52
|
+
list_select_related = ("user",)
|
|
53
|
+
|
|
54
|
+
def get_search_fields(self, request):
|
|
55
|
+
if hasattr(User, "USERNAME_FIELD"):
|
|
56
|
+
return "name", "device_id", f"user__{User.USERNAME_FIELD}"
|
|
57
|
+
else:
|
|
58
|
+
return "name", "device_id"
|
|
59
|
+
|
|
60
|
+
def _send_deactivated_message(
|
|
61
|
+
self,
|
|
62
|
+
request,
|
|
63
|
+
response: Union[
|
|
64
|
+
FirebaseResponseDict,
|
|
65
|
+
list[FirebaseResponseDict],
|
|
66
|
+
list[tuple[SendResponse, str]],
|
|
67
|
+
],
|
|
68
|
+
total_failure: int,
|
|
69
|
+
is_topic: bool,
|
|
70
|
+
):
|
|
71
|
+
if total_failure == 0:
|
|
72
|
+
return
|
|
73
|
+
if is_topic:
|
|
74
|
+
message = ngettext_lazy(
|
|
75
|
+
"A device failed to un/subscribe to topic. %(count)d device was "
|
|
76
|
+
"marked as inactive.",
|
|
77
|
+
"Some devices failed to un/subscribe to topic. %(count)d devices "
|
|
78
|
+
"were marked as inactive.",
|
|
79
|
+
total_failure,
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
message = ngettext_lazy(
|
|
83
|
+
"A message failed to send. %(count)d device was marked as " "inactive.",
|
|
84
|
+
"Some messages failed to send. %(count)d devices were marked as "
|
|
85
|
+
"inactive.",
|
|
86
|
+
total_failure,
|
|
87
|
+
)
|
|
88
|
+
self.message_user(
|
|
89
|
+
request,
|
|
90
|
+
message % {"count": total_failure},
|
|
91
|
+
level=messages.WARNING,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _get_to_str_obj(obj):
|
|
95
|
+
if isinstance(obj, SendResponse):
|
|
96
|
+
return obj.exception
|
|
97
|
+
elif isinstance(obj, TopicManagementResponse):
|
|
98
|
+
return obj.errors
|
|
99
|
+
return obj
|
|
100
|
+
|
|
101
|
+
def _print_responses(_response):
|
|
102
|
+
__error_list = fcm_error_list + [ErrorInfo]
|
|
103
|
+
# TODO Aggregate error response text. Each firebase error
|
|
104
|
+
# has multiple response texts too
|
|
105
|
+
[
|
|
106
|
+
self.message_user(
|
|
107
|
+
request,
|
|
108
|
+
(
|
|
109
|
+
_("%(response)s (Registration ID/Tokens: %(reg_id)s)")
|
|
110
|
+
% {"response": _get_to_str_obj(x), "reg_id": reg_id}
|
|
111
|
+
),
|
|
112
|
+
level=messages.WARNING,
|
|
113
|
+
)
|
|
114
|
+
for x, reg_id in _response
|
|
115
|
+
if type(_get_to_str_obj(x)) in __error_list
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
if isinstance(response, list):
|
|
119
|
+
# Our custom list of single responses
|
|
120
|
+
_print_responses(response)
|
|
121
|
+
elif isinstance(response, FirebaseResponseDict):
|
|
122
|
+
# technically, type should be: FirebaseResponseDict not just dict
|
|
123
|
+
_print_responses(
|
|
124
|
+
zip(
|
|
125
|
+
response.response.responses,
|
|
126
|
+
response.deactivated_registration_ids,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
raise NotImplementedError
|
|
131
|
+
|
|
132
|
+
def send_messages(self, request, queryset, bulk=False):
|
|
133
|
+
"""
|
|
134
|
+
Provides error handling for DeviceAdmin send_message and
|
|
135
|
+
send_bulk_message methods.
|
|
136
|
+
"""
|
|
137
|
+
total_failure = 0
|
|
138
|
+
single_responses: list[tuple[SendResponse, str]] = []
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
for device in queryset:
|
|
142
|
+
device: "FCMDevice"
|
|
143
|
+
if bulk:
|
|
144
|
+
response = queryset.send_message(
|
|
145
|
+
Message(
|
|
146
|
+
notification=Notification(
|
|
147
|
+
title="Test notification", body="Test bulk notification"
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
total_failure = len(response.deactivated_registration_ids)
|
|
152
|
+
return self._send_deactivated_message(
|
|
153
|
+
request, response, total_failure, False
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
response = device.send_message(
|
|
157
|
+
Message(
|
|
158
|
+
notification=Notification(
|
|
159
|
+
title="Test notification",
|
|
160
|
+
body="Test single notification",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
single_responses.append((response, device.registration_id))
|
|
165
|
+
if type(response) != SendResponse:
|
|
166
|
+
total_failure += 1
|
|
167
|
+
except FirebaseError as exc:
|
|
168
|
+
self.message_user(request, str(exc), level=messages.ERROR)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
self._send_deactivated_message(request, single_responses, total_failure, False)
|
|
172
|
+
|
|
173
|
+
def send_message(self, request, queryset):
|
|
174
|
+
self.send_messages(request, queryset)
|
|
175
|
+
|
|
176
|
+
send_message.short_description = _("Send test notification")
|
|
177
|
+
|
|
178
|
+
def send_bulk_message(self, request, queryset):
|
|
179
|
+
self.send_messages(request, queryset, True)
|
|
180
|
+
|
|
181
|
+
send_bulk_message.short_description = _("Send test notification in bulk")
|
|
182
|
+
|
|
183
|
+
def handle_topic_subscription(
|
|
184
|
+
self, request, queryset, should_subscribe: bool, bulk: bool = False
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
Provides error handling for DeviceAdmin bulk_un/subscribe_to_topic and
|
|
188
|
+
un/subscribe_to_topic methods.
|
|
189
|
+
"""
|
|
190
|
+
total_failure = 0
|
|
191
|
+
single_responses = []
|
|
192
|
+
|
|
193
|
+
for device in queryset:
|
|
194
|
+
device: "FCMDevice"
|
|
195
|
+
if bulk:
|
|
196
|
+
response: "FirebaseResponseDict" = queryset.handle_topic_subscription(
|
|
197
|
+
should_subscribe,
|
|
198
|
+
"test-topic",
|
|
199
|
+
)
|
|
200
|
+
total_failure = response.response.failure_count
|
|
201
|
+
single_responses = [
|
|
202
|
+
(x, response.registration_ids_sent[x.index])
|
|
203
|
+
for x in response.response.errors
|
|
204
|
+
]
|
|
205
|
+
break
|
|
206
|
+
else:
|
|
207
|
+
response = device.handle_topic_subscription(
|
|
208
|
+
should_subscribe,
|
|
209
|
+
"test-topic",
|
|
210
|
+
)
|
|
211
|
+
single_responses.append(
|
|
212
|
+
(
|
|
213
|
+
(
|
|
214
|
+
response.response.errors[0]
|
|
215
|
+
if len(response.response.errors) > 0
|
|
216
|
+
else "Success"
|
|
217
|
+
),
|
|
218
|
+
device.registration_id,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
total_failure += len(response.deactivated_registration_ids)
|
|
222
|
+
|
|
223
|
+
self._send_deactivated_message(request, single_responses, total_failure, True)
|
|
224
|
+
|
|
225
|
+
def subscribe_to_topic(self, request, queryset):
|
|
226
|
+
self.handle_topic_subscription(request, queryset, True)
|
|
227
|
+
|
|
228
|
+
subscribe_to_topic.short_description = _("Subscribe to test topic")
|
|
229
|
+
|
|
230
|
+
def bulk_subscribe_to_topic(self, request, queryset):
|
|
231
|
+
self.handle_topic_subscription(request, queryset, True, True)
|
|
232
|
+
|
|
233
|
+
bulk_subscribe_to_topic.short_description = _("Subscribe to test topic in bulk")
|
|
234
|
+
|
|
235
|
+
def unsubscribe_to_topic(self, request, queryset):
|
|
236
|
+
self.handle_topic_subscription(request, queryset, False)
|
|
237
|
+
|
|
238
|
+
unsubscribe_to_topic.short_description = _("Unsubscribe to test topic")
|
|
239
|
+
|
|
240
|
+
def bulk_unsubscribe_to_topic(self, request, queryset):
|
|
241
|
+
self.handle_topic_subscription(request, queryset, False, True)
|
|
242
|
+
|
|
243
|
+
bulk_unsubscribe_to_topic.short_description = _("Unsubscribe to test topic in bulk")
|
|
244
|
+
|
|
245
|
+
def handle_send_topic_message(self, request, queryset):
|
|
246
|
+
try:
|
|
247
|
+
FCMDevice.send_topic_message(
|
|
248
|
+
Message(
|
|
249
|
+
notification=Notification(
|
|
250
|
+
title="Test notification", body="Test single notification"
|
|
251
|
+
)
|
|
252
|
+
),
|
|
253
|
+
"test-topic",
|
|
254
|
+
)
|
|
255
|
+
except FirebaseError as exc:
|
|
256
|
+
self.message_user(request, str(exc), level=messages.ERROR)
|
|
257
|
+
|
|
258
|
+
def send_topic_message(self, request, queryset):
|
|
259
|
+
self.handle_send_topic_message(request, queryset)
|
|
260
|
+
|
|
261
|
+
send_topic_message.short_description = _("Send message test topic")
|
|
262
|
+
|
|
263
|
+
def enable(self, request, queryset):
|
|
264
|
+
queryset.update(active=True)
|
|
265
|
+
|
|
266
|
+
enable.short_description = _("Enable selected devices")
|
|
267
|
+
|
|
268
|
+
def disable(self, request, queryset):
|
|
269
|
+
queryset.deactivate(
|
|
270
|
+
reason="manual_disable",
|
|
271
|
+
source="admin_action",
|
|
272
|
+
metadata={},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
disable.short_description = _("Disable selected devices")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
admin.site.register(FCMDevice, DeviceAdmin)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
if "tastypie" in settings.INSTALLED_APPS:
|
|
4
|
+
# Tastypie resources are importable from the api package level (backwards compatibility)
|
|
5
|
+
from .tastypie import APNSDeviceAuthenticatedResource, FCMDeviceResource
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"APNSDeviceAuthenticatedResource",
|
|
9
|
+
"FCMDeviceResource",
|
|
10
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import swapper
|
|
2
|
+
from django.db.models import Q
|
|
3
|
+
from rest_framework import permissions, status
|
|
4
|
+
from rest_framework.mixins import CreateModelMixin
|
|
5
|
+
from rest_framework.response import Response
|
|
6
|
+
from rest_framework.serializers import ModelSerializer, Serializer, ValidationError
|
|
7
|
+
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
|
8
|
+
|
|
9
|
+
from fcm_django.settings import FCM_DJANGO_SETTINGS as SETTINGS
|
|
10
|
+
|
|
11
|
+
FCMDevice = swapper.load_model("fcm_django", "fcmdevice")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Serializers
|
|
15
|
+
class DeviceSerializerMixin(ModelSerializer):
|
|
16
|
+
class Meta:
|
|
17
|
+
fields = (
|
|
18
|
+
"id",
|
|
19
|
+
"name",
|
|
20
|
+
"registration_id",
|
|
21
|
+
"device_id",
|
|
22
|
+
"active",
|
|
23
|
+
"date_created",
|
|
24
|
+
"type",
|
|
25
|
+
)
|
|
26
|
+
read_only_fields = ("date_created",)
|
|
27
|
+
|
|
28
|
+
extra_kwargs = {"active": {"default": True}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UniqueRegistrationSerializerMixin(Serializer):
|
|
32
|
+
def validate(self, attrs):
|
|
33
|
+
devices = None
|
|
34
|
+
primary_key = None
|
|
35
|
+
request_method = None
|
|
36
|
+
|
|
37
|
+
if self.initial_data.get("registration_id", None):
|
|
38
|
+
if self.instance:
|
|
39
|
+
request_method = "update"
|
|
40
|
+
primary_key = self.instance.id
|
|
41
|
+
else:
|
|
42
|
+
request_method = "create"
|
|
43
|
+
else:
|
|
44
|
+
if self.context["request"].method in ["PUT", "PATCH"]:
|
|
45
|
+
request_method = "update"
|
|
46
|
+
primary_key = self.instance.id
|
|
47
|
+
elif self.context["request"].method == "POST":
|
|
48
|
+
request_method = "create"
|
|
49
|
+
|
|
50
|
+
Device = self.Meta.model
|
|
51
|
+
# if request authenticated, unique together with registration_id and
|
|
52
|
+
# user
|
|
53
|
+
user = self.context["request"].user
|
|
54
|
+
registration_id = attrs.get("registration_id")
|
|
55
|
+
|
|
56
|
+
if request_method == "update":
|
|
57
|
+
if registration_id:
|
|
58
|
+
if user is not None and user.is_authenticated:
|
|
59
|
+
devices = Device.objects.filter(
|
|
60
|
+
registration_id=registration_id
|
|
61
|
+
).exclude(id=primary_key)
|
|
62
|
+
if attrs.get("active", False):
|
|
63
|
+
devices.filter(~Q(user=user)).deactivate(
|
|
64
|
+
reason="duplicate_registration_id",
|
|
65
|
+
source="serializer_update",
|
|
66
|
+
metadata={
|
|
67
|
+
"request_method": request_method,
|
|
68
|
+
"target_user_id": user.id,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
devices = devices.filter(user=user)
|
|
72
|
+
else:
|
|
73
|
+
devices = Device.objects.filter(
|
|
74
|
+
registration_id=registration_id
|
|
75
|
+
).exclude(id=primary_key)
|
|
76
|
+
elif request_method == "create":
|
|
77
|
+
if user is not None and user.is_authenticated:
|
|
78
|
+
devices = Device.objects.filter(registration_id=registration_id)
|
|
79
|
+
devices.filter(~Q(user=user)).deactivate(
|
|
80
|
+
reason="duplicate_registration_id",
|
|
81
|
+
source="serializer_create",
|
|
82
|
+
metadata={
|
|
83
|
+
"request_method": request_method,
|
|
84
|
+
"target_user_id": user.id,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
devices = devices.filter(user=user, active=True)
|
|
88
|
+
else:
|
|
89
|
+
devices = Device.objects.filter(registration_id=registration_id)
|
|
90
|
+
|
|
91
|
+
if devices:
|
|
92
|
+
raise ValidationError({"registration_id": "This field must be unique."})
|
|
93
|
+
return attrs
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FCMDeviceSerializer(ModelSerializer, UniqueRegistrationSerializerMixin):
|
|
97
|
+
class Meta(DeviceSerializerMixin.Meta):
|
|
98
|
+
model = FCMDevice
|
|
99
|
+
|
|
100
|
+
extra_kwargs = {"id": {"read_only": True, "required": False}}
|
|
101
|
+
extra_kwargs.update(DeviceSerializerMixin.Meta.extra_kwargs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Permissions
|
|
105
|
+
class IsOwner(permissions.BasePermission):
|
|
106
|
+
def has_object_permission(self, request, view, obj):
|
|
107
|
+
# must be the owner to view the object
|
|
108
|
+
return obj.user == request.user
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Mixins
|
|
112
|
+
class DeviceViewSetMixin:
|
|
113
|
+
lookup_field = "registration_id"
|
|
114
|
+
|
|
115
|
+
def create(self, request, *args, **kwargs):
|
|
116
|
+
serializer = None
|
|
117
|
+
is_update = False
|
|
118
|
+
if (
|
|
119
|
+
SETTINGS.get("UPDATE_ON_DUPLICATE_REG_ID")
|
|
120
|
+
and "registration_id" in request.data
|
|
121
|
+
):
|
|
122
|
+
instance = self.queryset.model.objects.filter(
|
|
123
|
+
registration_id=request.data["registration_id"]
|
|
124
|
+
).first()
|
|
125
|
+
if instance:
|
|
126
|
+
serializer = self.get_serializer(instance, data=request.data)
|
|
127
|
+
is_update = True
|
|
128
|
+
if not serializer:
|
|
129
|
+
serializer = self.get_serializer(data=request.data)
|
|
130
|
+
|
|
131
|
+
serializer.is_valid(raise_exception=True)
|
|
132
|
+
if is_update:
|
|
133
|
+
self.perform_update(serializer)
|
|
134
|
+
return Response(serializer.data)
|
|
135
|
+
else:
|
|
136
|
+
self.perform_create(serializer)
|
|
137
|
+
headers = self.get_success_headers(serializer.data)
|
|
138
|
+
return Response(
|
|
139
|
+
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def perform_create(self, serializer):
|
|
143
|
+
if self.request.user.is_authenticated:
|
|
144
|
+
if SETTINGS["ONE_DEVICE_PER_USER"] and self.request.data.get(
|
|
145
|
+
"active", True
|
|
146
|
+
):
|
|
147
|
+
FCMDevice.objects.filter(user=self.request.user).deactivate(
|
|
148
|
+
reason="one_device_per_user",
|
|
149
|
+
source="perform_create",
|
|
150
|
+
metadata={"user_id": self.request.user.id},
|
|
151
|
+
)
|
|
152
|
+
return serializer.save(user=self.request.user)
|
|
153
|
+
return serializer.save()
|
|
154
|
+
|
|
155
|
+
def perform_update(self, serializer):
|
|
156
|
+
if self.request.user.is_authenticated:
|
|
157
|
+
if SETTINGS["ONE_DEVICE_PER_USER"] and self.request.data.get(
|
|
158
|
+
"active", False
|
|
159
|
+
):
|
|
160
|
+
FCMDevice.objects.filter(user=self.request.user).deactivate(
|
|
161
|
+
reason="one_device_per_user",
|
|
162
|
+
source="perform_update",
|
|
163
|
+
metadata={"user_id": self.request.user.id},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return serializer.save(user=self.request.user)
|
|
167
|
+
return serializer.save()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class AuthorizedMixin:
|
|
171
|
+
permission_classes = (permissions.IsAuthenticated, IsOwner)
|
|
172
|
+
|
|
173
|
+
def get_queryset(self):
|
|
174
|
+
# filter all devices to only those belonging to the current user
|
|
175
|
+
return self.queryset.filter(user=self.request.user)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ViewSets
|
|
179
|
+
class FCMDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
|
|
180
|
+
queryset = FCMDevice.objects.order_by("-id")
|
|
181
|
+
serializer_class = FCMDeviceSerializer
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class FCMDeviceCreateOnlyViewSet(DeviceViewSetMixin, CreateModelMixin, GenericViewSet):
|
|
185
|
+
queryset = FCMDevice.objects.all()
|
|
186
|
+
serializer_class = FCMDeviceSerializer
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class FCMDeviceAuthorizedViewSet(AuthorizedMixin, FCMDeviceViewSet):
|
|
190
|
+
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import swapper
|
|
2
|
+
from tastypie.authentication import BasicAuthentication
|
|
3
|
+
from tastypie.authorization import Authorization
|
|
4
|
+
from tastypie.resources import ModelResource
|
|
5
|
+
|
|
6
|
+
FCMDevice = swapper.load_model("fcm_django", "fcmdevice")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FCMDeviceResource(ModelResource):
|
|
10
|
+
class Meta:
|
|
11
|
+
authorization = Authorization()
|
|
12
|
+
queryset = FCMDevice.objects.all()
|
|
13
|
+
resource_name = "device/apns"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APNSDeviceAuthenticatedResource(FCMDeviceResource):
|
|
17
|
+
# user = ForeignKey(UserResource, "user")
|
|
18
|
+
|
|
19
|
+
class Meta(FCMDeviceResource.Meta):
|
|
20
|
+
authentication = BasicAuthentication()
|
|
21
|
+
|
|
22
|
+
# authorization = SameUserAuthorization()
|
|
23
|
+
|
|
24
|
+
def obj_create(self, bundle, **kwargs):
|
|
25
|
+
# See https://github.com/toastdriven/django-tastypie/issues/854
|
|
26
|
+
return super().obj_create(bundle, user=bundle.request.user, **kwargs)
|
fcm_django/apps.py
ADDED
fcm_django/fields.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
from django import forms
|
|
5
|
+
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
|
6
|
+
from django.db import connection, models
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
|
|
9
|
+
UNSIGNED_64BIT_INT_MIN_VALUE = 0
|
|
10
|
+
UNSIGNED_64BIT_INT_MAX_VALUE = 2**64 - 1
|
|
11
|
+
|
|
12
|
+
__all__ = ["HexadecimalField", "HexIntegerField"]
|
|
13
|
+
|
|
14
|
+
hex_re = re.compile(r"^(([0-9A-f])|(0x[0-9A-f]))+$")
|
|
15
|
+
signed_integer_engines = [
|
|
16
|
+
"django.db.backends.postgresql",
|
|
17
|
+
"django.db.backends.postgresql_psycopg2",
|
|
18
|
+
"django.contrib.gis.db.backends.postgis",
|
|
19
|
+
"django.db.backends.sqlite3",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _using_signed_storage():
|
|
24
|
+
return connection.settings_dict["ENGINE"] in signed_integer_engines
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _signed_to_unsigned_integer(value):
|
|
28
|
+
return struct.unpack("Q", struct.pack("q", value))[0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _unsigned_to_signed_integer(value):
|
|
32
|
+
return struct.unpack("q", struct.pack("Q", value))[0]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _hex_string_to_unsigned_integer(value):
|
|
36
|
+
return int(value, 16)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _unsigned_integer_to_hex_string(value):
|
|
40
|
+
return hex(value).rstrip("L")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HexadecimalField(forms.CharField):
|
|
44
|
+
"""
|
|
45
|
+
A form field that accepts only hexadecimal numbers
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args, **kwargs):
|
|
49
|
+
self.default_validators = [
|
|
50
|
+
RegexValidator(hex_re, _("Enter a valid hexadecimal number"), "invalid")
|
|
51
|
+
]
|
|
52
|
+
super().__init__(*args, **kwargs)
|
|
53
|
+
|
|
54
|
+
def prepare_value(self, value):
|
|
55
|
+
# converts bigint from db to hex before it is displayed in admin
|
|
56
|
+
if (
|
|
57
|
+
value
|
|
58
|
+
and not isinstance(value, str)
|
|
59
|
+
and connection.vendor in ("mysql", "sqlite")
|
|
60
|
+
):
|
|
61
|
+
value = _unsigned_integer_to_hex_string(value)
|
|
62
|
+
return super(forms.CharField, self).prepare_value(value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class HexIntegerField(models.BigIntegerField):
|
|
66
|
+
"""
|
|
67
|
+
This field stores a hexadecimal *string* of up to 64 bits as an unsigned integer
|
|
68
|
+
on *all* backends including postgres.
|
|
69
|
+
|
|
70
|
+
Reasoning: Postgres only supports signed bigints. Since we don't care about
|
|
71
|
+
signedness, we store it as signed, and cast it to unsigned when we deal with
|
|
72
|
+
the actual value (with struct)
|
|
73
|
+
|
|
74
|
+
On sqlite and mysql, native unsigned bigint types are used. In all cases, the
|
|
75
|
+
value we deal with in python is always in hex.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
validators = [
|
|
79
|
+
MinValueValidator(UNSIGNED_64BIT_INT_MIN_VALUE),
|
|
80
|
+
MaxValueValidator(UNSIGNED_64BIT_INT_MAX_VALUE),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
def db_type(self, connection):
|
|
84
|
+
engine = connection.settings_dict["ENGINE"]
|
|
85
|
+
if "mysql" in engine:
|
|
86
|
+
return "bigint unsigned"
|
|
87
|
+
elif "sqlite" in engine:
|
|
88
|
+
return "UNSIGNED BIG INT"
|
|
89
|
+
else:
|
|
90
|
+
return super().db_type(connection=connection)
|
|
91
|
+
|
|
92
|
+
def get_prep_value(self, value):
|
|
93
|
+
"""Return the integer value to be stored from the hex string"""
|
|
94
|
+
if value is None or value == "":
|
|
95
|
+
return None
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
value = _hex_string_to_unsigned_integer(value)
|
|
98
|
+
if _using_signed_storage():
|
|
99
|
+
value = _unsigned_to_signed_integer(value)
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
def from_db_value(self, value, expression, connection, context):
|
|
103
|
+
"""Return an unsigned int representation from all db backends"""
|
|
104
|
+
if value is None:
|
|
105
|
+
return value
|
|
106
|
+
if _using_signed_storage():
|
|
107
|
+
value = _signed_to_unsigned_integer(value)
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
def to_python(self, value):
|
|
111
|
+
"""Return a str representation of the hexadecimal"""
|
|
112
|
+
if isinstance(value, str):
|
|
113
|
+
return value
|
|
114
|
+
if value is None:
|
|
115
|
+
return value
|
|
116
|
+
return _unsigned_integer_to_hex_string(value)
|
|
117
|
+
|
|
118
|
+
def formfield(self, **kwargs):
|
|
119
|
+
defaults = {"form_class": HexadecimalField}
|
|
120
|
+
defaults.update(kwargs)
|
|
121
|
+
# yes, that super call is right
|
|
122
|
+
return super(models.IntegerField, self).formfield(**defaults)
|
|
123
|
+
|
|
124
|
+
def run_validators(self, value):
|
|
125
|
+
# make sure validation is performed on integer value not string value
|
|
126
|
+
value = _hex_string_to_unsigned_integer(value)
|
|
127
|
+
return super(models.BigIntegerField, self).run_validators(value)
|