Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion syncano/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os

__title__ = 'Syncano Python'
__version__ = '5.0.1'
__version__ = '5.0.2'
__author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski"
__credits__ = ["Daniel Kopka",
"Michal Kobus",
Expand Down
16 changes: 15 additions & 1 deletion syncano/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def make_request(self, method_name, path, **kwargs):
# remove 'data' and 'content-type' to avoid "ValueError: Data must not be a string."
params.pop('data')
params['headers'].pop('content-type')
params['files'] = files
params['files'] = self._process_apns_cert_files(files)

if response.status_code == 201:
url = '{}{}/'.format(url, content['id'])
Expand Down Expand Up @@ -402,6 +402,20 @@ def get_user_info(self, api_key=None, user_key=None):
return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={
'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key})

def _process_apns_cert_files(self, files):
files = files.copy()
for key in [file_name for file_name in files.keys()]:
# remove certificates files (which are bool - True if cert exist, False otherwise)
value = files[key]
if isinstance(value, bool):
files.pop(key)
continue

if key in ['production_certificate', 'development_certificate']:
value = (value.name, value, 'application/x-pkcs12', {'Expires': '0'})
files[key] = value
return files


class ConnectionMixin(object):
"""Injects connection attribute with support of basic validation."""
Expand Down
18 changes: 12 additions & 6 deletions syncano/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,15 @@ def __getattribute__(self, item):
try:
return super(LinksWrapper, self).__getattribute__(item)
except AttributeError:
item = item.replace('_', '-')
if item not in self.links_dict or item in self.ignored_links:
value = self.links_dict.get(item)
if not value:
item = item.replace('_', '-')
value = self.links_dict.get(item)

if not value:
raise
return self.links_dict[item]

return value

def to_native(self):
return self.links_dict
Expand Down Expand Up @@ -672,9 +677,10 @@ def to_native(self, value):
return

if not isinstance(value, six.string_types):
value.update({
'environment': PUSH_ENV,
})
if 'environment' not in value:
value.update({
'environment': PUSH_ENV,
})
value = json.dumps(value)
return value

Expand Down
114 changes: 102 additions & 12 deletions syncano/models/push_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,28 @@ class DeviceBase(object):
label = fields.StringField(max_length=80)
user = fields.IntegerField(required=False)

links = fields.LinksField()
created_at = fields.DateTimeField(read_only=True, required=False)
updated_at = fields.DateTimeField(read_only=True, required=False)

class Meta:
abstract = True

def is_new(self):
return self.created_at is None
def send_message(self, content):
"""
A method which allows to send message directly to the device;
:param contet: Message content structure - object like;
:return:
"""
print(self.links.links_dict)
send_message_path = self.links.send_message
data = {
'content': content
}
connection = self._get_connection()
response = connection.request('POST', send_message_path, data=data)
self.to_python(response)
return self


class GCMDevice(DeviceBase, Model):
Expand All @@ -51,21 +65,21 @@ class GCMDevice(DeviceBase, Model):
Delete:
gcm_device.delete()

.. note::

another save on the same object will always fail (altering the Device data is currently not possible);
Update:
gcm_device.label = 'some new label'
gcm_device.save()

"""

class Meta:
parent = Instance
endpoints = {
'detail': {
'methods': ['delete', 'get'],
'methods': ['delete', 'get', 'put', 'patch'],
'path': '/push_notifications/gcm/devices/{registration_id}/',
},
'list': {
'methods': ['get'],
'methods': ['post', 'get'],
'path': '/push_notifications/gcm/devices/',
}
}
Expand Down Expand Up @@ -94,9 +108,9 @@ class APNSDevice(DeviceBase, Model):
Delete:
apns_device.delete()

.. note::

another save on the same object will always fail (altering the Device data is currently not possible);
Update:
apns_device.label = 'some new label'
apns_device.save()

.. note::

Expand All @@ -108,11 +122,11 @@ class Meta:
parent = Instance
endpoints = {
'detail': {
'methods': ['delete', 'get'],
'methods': ['delete', 'get', 'put', 'patch'],
'path': '/push_notifications/apns/devices/{registration_id}/',
},
'list': {
'methods': ['get'],
'methods': ['post', 'get'],
'path': '/push_notifications/apns/devices/',
}
}
Expand Down Expand Up @@ -235,3 +249,79 @@ class Meta:
'path': '/push_notifications/apns/messages/',
}
}


class GCMConfig(Model):
"""
A model which stores information with GCM Push keys;

Usage::

Add (modify) new keys:
gcm_config = GCMConfig(production_api_key='ccc', development_api_key='ddd')
gcm_config.save()

or:
gcm_config = GCMConfig().please.get()
gcm_config.production_api_key = 'ccc'
gcm_config.development_api_key = 'ddd'
gcm_config.save()

"""
production_api_key = fields.StringField(required=False)
development_api_key = fields.StringField(required=False)

def is_new(self):
return False # this is predefined - never will be new

class Meta:
parent = Instance
endpoints = {
'list': {
'methods': ['get', 'put'],
'path': '/push_notifications/gcm/config/',
},
'detail': {
'methods': ['get', 'put'],
'path': '/push_notifications/gcm/config/',
},
}


class APNSConfig(Model):
"""
A model which stores information with APNS Push certificates;

Usage::

Add (modify) new keys:
cert_file = open('cert_file.p12', 'rb')
apns_config = APNSConfig(development_certificate=cert_file)
apns_config.save()
cert_file.close()

"""
production_certificate_name = fields.StringField(required=False)
production_certificate = fields.FileField(required=False)
production_bundle_identifier = fields.StringField(required=False)
production_expiration_date = fields.DateField(read_only=True)
development_certificate_name = fields.StringField(required=False)
development_certificate = fields.FileField(required=False)
development_bundle_identifier = fields.StringField(required=False)
development_expiration_date = fields.DateField(read_only=True)

def is_new(self):
return False # this is predefined - never will be new

class Meta:
parent = Instance
endpoints = {
'list': {
'methods': ['get', 'put'],
'path': '/push_notifications/apns/config/',
},
'detail': {
'methods': ['get', 'put'],
'path': '/push_notifications/apns/config/',
},
}
Binary file added tests/certificates/ApplePushDevelopment.p12
Binary file not shown.
103 changes: 92 additions & 11 deletions tests/integration_test_push.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,75 @@
# -*- coding: utf-8 -*-
import uuid

from syncano.exceptions import SyncanoRequestError
from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage
from syncano.models import APNSConfig, APNSDevice, APNSMessage, GCMConfig, GCMDevice, GCMMessage
from tests.integration_test import InstanceMixin, IntegrationTest


class PushNotificationTest(InstanceMixin, IntegrationTest):
class PushIntegrationTest(InstanceMixin, IntegrationTest):

@classmethod
def setUpClass(cls):
super(PushIntegrationTest, cls).setUpClass()

cls.gcm_config = GCMConfig(
development_api_key=uuid.uuid4().hex,
instance_name=cls.instance.name
)
cls.gcm_config.save()

with open('tests/certificates/ApplePushDevelopment.p12', 'rb') as cert:
cls.apns_config = APNSConfig(
development_certificate=cert,
development_certificate_name='test',
development_bundle_identifier='test1234',
instance_name=cls.instance.name
)
cls.apns_config.save()

cls.environment = 'development'
cls.gcm_device = GCMDevice(
instance_name=cls.instance.name,
label='example label',
registration_id=86152312314401555,
device_id='10000000001',
)
cls.gcm_device.save()

cls.apns_device = APNSDevice(
instance_name=cls.instance.name,
label='example label',
registration_id='4719084371920471208947120984731208947910827409128470912847120894',
device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a',
)
cls.apns_device.save()


class PushNotificationTest(PushIntegrationTest):

def test_gcm_config_update(self):
gcm_config = GCMConfig.please.get()
new_key = uuid.uuid4().hex
gcm_config.development_api_key = new_key
gcm_config.save()

gcm_config_ = GCMConfig.please.get()
self.assertEqual(gcm_config_.development_api_key, new_key)

def test_apns_config_update(self):
apns_config = APNSConfig.please.get()
new_cert_name = 'new cert name'
apns_config.development_certificate_name = new_cert_name
apns_config.save()

apns_config_ = APNSConfig.please.get()
self.assertEqual(apns_config_.development_certificate_name, new_cert_name)

def test_gcm_device(self):
device = GCMDevice(
instance_name=self.instance.name,
label='example label',
registration_id=86152312314401555,
registration_id=86152312314401666,
device_id='10000000001',
)
self._test_device(device, GCMDevice.please)
Expand All @@ -18,43 +78,58 @@ def test_apns_device(self):
device = APNSDevice(
instance_name=self.instance.name,
label='example label',
registration_id='4719084371920471208947120984731208947910827409128470912847120894',
registration_id='4719084371920471208947120984731208947910827409128470912847120222',
device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a',
)

self._test_device(device, APNSDevice.please)

def test_send_message_gcm(self):

self.assertEqual(0, len(list(GCMMessage.please.all())))

self.gcm_device.send_message(content={'environment': self.environment, 'data': {'c': 'more_c'}})

self.assertEqual(1, len(list(GCMMessage.please.all())))

def test_send_message_apns(self):
self.assertEqual(0, len(list(APNSMessage.please.all())))

self.apns_device.send_message(content={'environment': 'development', 'aps': {'alert': 'alert test'}})

self.assertEqual(1, len(list(APNSMessage.please.all())))

def test_gcm_message(self):
message = GCMMessage(
instance_name=self.instance.name,
content={
'registration_ids': ['TESTIDREGISRATION', ],
'environment': 'production',
'data': {
'param1': 'test'
}
}
)

self._test_message(message, GCMMessage.please)
self._test_message(message, GCMMessage.please) # we want this to fail; no productions keys;

def test_apns_message(self):
message = APNSMessage(
instance_name=self.instance.name,
content={
'registration_ids': ['TESTIDREGISRATION', ],
'environment': 'production',
'aps': {'alert': 'semo example label'}
}
)

self._test_message(message, APNSMessage.please)
self._test_message(message, APNSMessage.please) # we want this to fail; no productions certs;

def _test_device(self, device, manager):

self.assertFalse(manager.all(instance_name=self.instance.name))

device.save()

self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 1)
self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 2)

# test get:
device_ = manager.get(instance_name=self.instance.name, registration_id=device.registration_id)
Expand All @@ -63,9 +138,15 @@ def _test_device(self, device, manager):
self.assertEqual(device_.registration_id, device.registration_id)
self.assertEqual(device_.device_id, device.device_id)

device.delete()
# test update:
new_label = 'totally new label'
device.label = new_label
device.save()

self.assertFalse(manager.all(instance_name=self.instance.name))
device_ = manager.get(instance_name=self.instance.name, registration_id=device.registration_id)
self.assertEqual(new_label, device_.label)

device.delete()

def _test_message(self, message, manager):
self.assertFalse(manager.all(instance_name=self.instance.name))
Expand Down
Loading