From 1e82f84cc40c18e98221ad458ece62f508ed9b8c Mon Sep 17 00:00:00 2001 From: Matthew Hershberger Date: Tue, 11 Apr 2017 14:45:12 -0400 Subject: [PATCH] GCM/FCM device uuid --- push_notifications/api/rest_framework.py | 24 +++----- push_notifications/fields.py | 61 +++---------------- .../migrations/0006_gcmdevice_device_uuid.py | 19 ++++++ .../migrations/0007_gcmdevice_migrate_data.py | 25 ++++++++ .../0008_gcmdevice_rename_device_uuid.py | 24 ++++++++ push_notifications/models.py | 6 +- tests/test_rest_framework.py | 10 --- 7 files changed, 89 insertions(+), 80 deletions(-) create mode 100644 push_notifications/migrations/0006_gcmdevice_device_uuid.py create mode 100644 push_notifications/migrations/0007_gcmdevice_migrate_data.py create mode 100644 push_notifications/migrations/0008_gcmdevice_rename_device_uuid.py diff --git a/push_notifications/api/rest_framework.py b/push_notifications/api/rest_framework.py index 8fec5562..d84ccce5 100644 --- a/push_notifications/api/rest_framework.py +++ b/push_notifications/api/rest_framework.py @@ -1,30 +1,29 @@ from __future__ import absolute_import from rest_framework import permissions, status -from rest_framework.fields import IntegerField +from rest_framework.fields import UUIDField from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, Serializer, ValidationError from rest_framework.viewsets import ModelViewSet -from ..fields import hex_re, UNSIGNED_64BIT_INT_MAX_VALUE +from ..fields import hex_re from ..models import APNSDevice, GCMDevice, WNSDevice from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS -# Fields -class HexIntegerField(IntegerField): +class UUIDIntegerField(UUIDField): """ - Store an integer represented as a hex string of form "0x01". + Store an integer represented as a UUID for backwards compatibiltiy. Also + allows device_ids to be express as UUIDs. """ def to_internal_value(self, data): - # validate hex string and convert it to the unsigned - # integer representation for internal use try: + # maintain some semblence of backwards compatibility data = int(data, 16) if type(data) != int else data except ValueError: raise ValidationError("Device ID is not a valid hex number") - return super(HexIntegerField, self).to_internal_value(data) + return super(UUIDIntegerField, self).to_internal_value(data) def to_representation(self, value): return value @@ -90,9 +89,8 @@ def validate(self, attrs): class GCMDeviceSerializer(UniqueRegistrationSerializerMixin, ModelSerializer): - device_id = HexIntegerField( + device_id = UUIDIntegerField( help_text="ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)", - style={"input_type": "text"}, required=False, allow_null=True ) @@ -105,12 +103,6 @@ class Meta(DeviceSerializerMixin.Meta): ) extra_kwargs = {"id": {"read_only": False, "required": False}} - def validate_device_id(self, value): - # device ids are 64 bit unsigned values - if value > UNSIGNED_64BIT_INT_MAX_VALUE: - raise ValidationError("Device ID is out of range") - return value - class WNSDeviceSerializer(UniqueRegistrationSerializerMixin, ModelSerializer): class Meta(DeviceSerializerMixin.Meta): diff --git a/push_notifications/fields.py b/push_notifications/fields.py index 481d30f4..6a0c91e7 100644 --- a/push_notifications/fields.py +++ b/push_notifications/fields.py @@ -1,7 +1,7 @@ import re import struct from django import forms -from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator +from django.core.validators import RegexValidator from django.db import connection, models from django.utils import six from django.utils.translation import ugettext_lazy as _ @@ -60,6 +60,7 @@ def prepare_value(self, value): return super(forms.CharField, self).prepare_value(value) +# https://docs.djangoproject.com/en/1.11/topics/migrations/#considerations-when-removing-model-fields class HexIntegerField(models.BigIntegerField): """ This field stores a hexadecimal *string* of up to 64 bits as an unsigned integer @@ -73,53 +74,11 @@ class HexIntegerField(models.BigIntegerField): value we deal with in python is always in hex. """ - validators = [ - MinValueValidator(UNSIGNED_64BIT_INT_MIN_VALUE), - MaxValueValidator(UNSIGNED_64BIT_INT_MAX_VALUE) - ] - - def db_type(self, connection): - engine = connection.settings_dict["ENGINE"] - if "mysql" in engine: - return "bigint unsigned" - elif "sqlite" in engine: - return "UNSIGNED BIG INT" - else: - return super(HexIntegerField, self).db_type(connection=connection) - - def get_prep_value(self, value): - """ Return the integer value to be stored from the hex string """ - if value is None or value == "": - return None - if isinstance(value, six.string_types): - value = _hex_string_to_unsigned_integer(value) - if _using_signed_storage(): - value = _unsigned_to_signed_integer(value) - return value - - def from_db_value(self, value, expression, connection, context): - """ Return an unsigned int representation from all db backends """ - if value is None: - return value - if _using_signed_storage(): - value = _signed_to_unsigned_integer(value) - return value - - def to_python(self, value): - """ Return a str representation of the hexadecimal """ - if isinstance(value, six.string_types): - return value - if value is None: - return value - return _unsigned_integer_to_hex_string(value) - - def formfield(self, **kwargs): - defaults = {"form_class": HexadecimalField} - defaults.update(kwargs) - # yes, that super call is right - return super(models.IntegerField, self).formfield(**defaults) - - def run_validators(self, value): - # make sure validation is performed on integer value not string value - value = _hex_string_to_unsigned_integer(value) - return super(models.BigIntegerField, self).run_validators(value) + system_check_removed_details = { + "msg": ( + "HexIntegerField has been removed except for support in " + "historical migrations." + ), + "hint": "Use UUIDField instead.", + "id": "fields.E001", + } diff --git a/push_notifications/migrations/0006_gcmdevice_device_uuid.py b/push_notifications/migrations/0006_gcmdevice_device_uuid.py new file mode 100644 index 00000000..fc7f6bfb --- /dev/null +++ b/push_notifications/migrations/0006_gcmdevice_device_uuid.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-08 19:17 +from __future__ import unicode_literals + +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ('push_notifications', '0005_applicationid'), + ] + + operations = [ + migrations.AddField( + model_name='gcmdevice', + name='device_uuid', + field=models.UUIDField(blank=True, db_index=True, help_text='ANDROID_ID / TelephonyManager.getDeviceId()', null=True, verbose_name='Device ID'), + ) + ] diff --git a/push_notifications/migrations/0007_gcmdevice_migrate_data.py b/push_notifications/migrations/0007_gcmdevice_migrate_data.py new file mode 100644 index 00000000..19e3924b --- /dev/null +++ b/push_notifications/migrations/0007_gcmdevice_migrate_data.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-08 19:17 +from __future__ import unicode_literals + +from django.db import migrations, models +import uuid + + +def migrate_device_id(apps, schema_editor): + GCMDevice = apps.get_model("push_notifications", "GCMDevice") + for device in GCMDevice.objects.filter(device_id__isnull=False): + # previously stored as an unsigned integer + device.device_uuid = uuid.UUID(int=device.device_id) + device.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('push_notifications', '0006_gcmdevice_device_uuid'), + ] + + operations = [ + migrations.RunPython(migrate_device_id), + ] diff --git a/push_notifications/migrations/0008_gcmdevice_rename_device_uuid.py b/push_notifications/migrations/0008_gcmdevice_rename_device_uuid.py new file mode 100644 index 00000000..1016feef --- /dev/null +++ b/push_notifications/migrations/0008_gcmdevice_rename_device_uuid.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-08 19:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('push_notifications', '0007_gcmdevice_migrate_data'), + ] + + operations = [ + migrations.RemoveField( + model_name='gcmdevice', + name='device_id', + ), + migrations.RenameField( + model_name='gcmdevice', + old_name='device_uuid', + new_name='device_id', + ), + ] diff --git a/push_notifications/models.py b/push_notifications/models.py index 9e05b23b..c7059f26 100644 --- a/push_notifications/models.py +++ b/push_notifications/models.py @@ -3,7 +3,6 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from .fields import HexIntegerField from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS @@ -80,10 +79,11 @@ class GCMDevice(Device): # device_id cannot be a reliable primary key as fragmentation between different devices # can make it turn out to be null and such: # http://android-developers.blogspot.co.uk/2011/03/identifying-app-installations.html - device_id = HexIntegerField( + device_id = models.UUIDField( verbose_name=_("Device ID"), blank=True, null=True, db_index=True, - help_text=_("ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)") + help_text=_("ANDROID_ID / TelephonyManager.getDeviceId()") ) + registration_id = models.TextField(verbose_name=_("Registration ID")) cloud_message_type = models.CharField( verbose_name=_("Cloud Message Type"), max_length=3, diff --git a/tests/test_rest_framework.py b/tests/test_rest_framework.py index 61fd3f43..77b6eed3 100644 --- a/tests/test_rest_framework.py +++ b/tests/test_rest_framework.py @@ -108,16 +108,6 @@ def test_device_id_validation_fail_bad_hex(self): self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, GCM_DRF_INVALID_HEX_ERROR) - def test_device_id_validation_fail_out_of_range(self): - serializer = GCMDeviceSerializer(data={ - "registration_id": "foobar", - "name": "Galaxy Note 3", - "device_id": "10000000000000000", # 2**64 - "application_id": "XXXXXXXXXXXXXXXXXXXX", - }) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, GCM_DRF_OUT_OF_RANGE_ERROR) - def test_device_id_validation_value_between_signed_unsigned_64b_int_maximums(self): """ 2**63 < 0xe87a4e72d634997c < 2**64