Skip to content

Commit

Permalink
GCM/FCM device uuid
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewh committed Apr 18, 2017
1 parent 129e599 commit 1e82f84
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 80 deletions.
24 changes: 8 additions & 16 deletions push_notifications/api/rest_framework.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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):
Expand Down
61 changes: 10 additions & 51 deletions push_notifications/fields.py
Original file line number Diff line number Diff line change
@@ -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 _
Expand Down Expand Up @@ -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
Expand All @@ -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",
}
19 changes: 19 additions & 0 deletions push_notifications/migrations/0006_gcmdevice_device_uuid.py
Original file line number Diff line number Diff line change
@@ -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'),
)
]
25 changes: 25 additions & 0 deletions push_notifications/migrations/0007_gcmdevice_migrate_data.py
Original file line number Diff line number Diff line change
@@ -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),
]
24 changes: 24 additions & 0 deletions push_notifications/migrations/0008_gcmdevice_rename_device_uuid.py
Original file line number Diff line number Diff line change
@@ -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',
),
]
6 changes: 3 additions & 3 deletions push_notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
10 changes: 0 additions & 10 deletions tests/test_rest_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1e82f84

Please sign in to comment.