Skip to content

Commit c0361e5

Browse files
committed
Merge pull request #189 from Syncano/LIB-660
[LIB-660][HOTFIX] PUSH Config and extension in Messages
2 parents 76ba552 + 68bc102 commit c0361e5

File tree

7 files changed

+224
-34
lines changed

7 files changed

+224
-34
lines changed

syncano/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33

44
__title__ = 'Syncano Python'
5-
__version__ = '5.0.1'
5+
__version__ = '5.0.2'
66
__author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski"
77
__credits__ = ["Daniel Kopka",
88
"Michal Kobus",

syncano/connection.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def make_request(self, method_name, path, **kwargs):
271271
# remove 'data' and 'content-type' to avoid "ValueError: Data must not be a string."
272272
params.pop('data')
273273
params['headers'].pop('content-type')
274-
params['files'] = files
274+
params['files'] = self._process_apns_cert_files(files)
275275

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

405+
def _process_apns_cert_files(self, files):
406+
files = files.copy()
407+
for key in [file_name for file_name in files.keys()]:
408+
# remove certificates files (which are bool - True if cert exist, False otherwise)
409+
value = files[key]
410+
if isinstance(value, bool):
411+
files.pop(key)
412+
continue
413+
414+
if key in ['production_certificate', 'development_certificate']:
415+
value = (value.name, value, 'application/x-pkcs12', {'Expires': '0'})
416+
files[key] = value
417+
return files
418+
405419

406420
class ConnectionMixin(object):
407421
"""Injects connection attribute with support of basic validation."""

syncano/models/fields.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,15 @@ def __getattribute__(self, item):
435435
try:
436436
return super(LinksWrapper, self).__getattribute__(item)
437437
except AttributeError:
438-
item = item.replace('_', '-')
439-
if item not in self.links_dict or item in self.ignored_links:
438+
value = self.links_dict.get(item)
439+
if not value:
440+
item = item.replace('_', '-')
441+
value = self.links_dict.get(item)
442+
443+
if not value:
440444
raise
441-
return self.links_dict[item]
445+
446+
return value
442447

443448
def to_native(self):
444449
return self.links_dict
@@ -672,9 +677,10 @@ def to_native(self, value):
672677
return
673678

674679
if not isinstance(value, six.string_types):
675-
value.update({
676-
'environment': PUSH_ENV,
677-
})
680+
if 'environment' not in value:
681+
value.update({
682+
'environment': PUSH_ENV,
683+
})
678684
value = json.dumps(value)
679685
return value
680686

syncano/models/push_notification.py

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,28 @@ class DeviceBase(object):
1818
label = fields.StringField(max_length=80)
1919
user = fields.IntegerField(required=False)
2020

21+
links = fields.LinksField()
2122
created_at = fields.DateTimeField(read_only=True, required=False)
2223
updated_at = fields.DateTimeField(read_only=True, required=False)
2324

2425
class Meta:
2526
abstract = True
2627

27-
def is_new(self):
28-
return self.created_at is None
28+
def send_message(self, content):
29+
"""
30+
A method which allows to send message directly to the device;
31+
:param contet: Message content structure - object like;
32+
:return:
33+
"""
34+
print(self.links.links_dict)
35+
send_message_path = self.links.send_message
36+
data = {
37+
'content': content
38+
}
39+
connection = self._get_connection()
40+
response = connection.request('POST', send_message_path, data=data)
41+
self.to_python(response)
42+
return self
2943

3044

3145
class GCMDevice(DeviceBase, Model):
@@ -51,21 +65,21 @@ class GCMDevice(DeviceBase, Model):
5165
Delete:
5266
gcm_device.delete()
5367
54-
.. note::
55-
56-
another save on the same object will always fail (altering the Device data is currently not possible);
68+
Update:
69+
gcm_device.label = 'some new label'
70+
gcm_device.save()
5771
5872
"""
5973

6074
class Meta:
6175
parent = Instance
6276
endpoints = {
6377
'detail': {
64-
'methods': ['delete', 'get'],
78+
'methods': ['delete', 'get', 'put', 'patch'],
6579
'path': '/push_notifications/gcm/devices/{registration_id}/',
6680
},
6781
'list': {
68-
'methods': ['get'],
82+
'methods': ['post', 'get'],
6983
'path': '/push_notifications/gcm/devices/',
7084
}
7185
}
@@ -94,9 +108,9 @@ class APNSDevice(DeviceBase, Model):
94108
Delete:
95109
apns_device.delete()
96110
97-
.. note::
98-
99-
another save on the same object will always fail (altering the Device data is currently not possible);
111+
Update:
112+
apns_device.label = 'some new label'
113+
apns_device.save()
100114
101115
.. note::
102116
@@ -108,11 +122,11 @@ class Meta:
108122
parent = Instance
109123
endpoints = {
110124
'detail': {
111-
'methods': ['delete', 'get'],
125+
'methods': ['delete', 'get', 'put', 'patch'],
112126
'path': '/push_notifications/apns/devices/{registration_id}/',
113127
},
114128
'list': {
115-
'methods': ['get'],
129+
'methods': ['post', 'get'],
116130
'path': '/push_notifications/apns/devices/',
117131
}
118132
}
@@ -235,3 +249,79 @@ class Meta:
235249
'path': '/push_notifications/apns/messages/',
236250
}
237251
}
252+
253+
254+
class GCMConfig(Model):
255+
"""
256+
A model which stores information with GCM Push keys;
257+
258+
Usage::
259+
260+
Add (modify) new keys:
261+
gcm_config = GCMConfig(production_api_key='ccc', development_api_key='ddd')
262+
gcm_config.save()
263+
264+
or:
265+
gcm_config = GCMConfig().please.get()
266+
gcm_config.production_api_key = 'ccc'
267+
gcm_config.development_api_key = 'ddd'
268+
gcm_config.save()
269+
270+
"""
271+
production_api_key = fields.StringField(required=False)
272+
development_api_key = fields.StringField(required=False)
273+
274+
def is_new(self):
275+
return False # this is predefined - never will be new
276+
277+
class Meta:
278+
parent = Instance
279+
endpoints = {
280+
'list': {
281+
'methods': ['get', 'put'],
282+
'path': '/push_notifications/gcm/config/',
283+
},
284+
'detail': {
285+
'methods': ['get', 'put'],
286+
'path': '/push_notifications/gcm/config/',
287+
},
288+
}
289+
290+
291+
class APNSConfig(Model):
292+
"""
293+
A model which stores information with APNS Push certificates;
294+
295+
Usage::
296+
297+
Add (modify) new keys:
298+
cert_file = open('cert_file.p12', 'rb')
299+
apns_config = APNSConfig(development_certificate=cert_file)
300+
apns_config.save()
301+
cert_file.close()
302+
303+
"""
304+
production_certificate_name = fields.StringField(required=False)
305+
production_certificate = fields.FileField(required=False)
306+
production_bundle_identifier = fields.StringField(required=False)
307+
production_expiration_date = fields.DateField(read_only=True)
308+
development_certificate_name = fields.StringField(required=False)
309+
development_certificate = fields.FileField(required=False)
310+
development_bundle_identifier = fields.StringField(required=False)
311+
development_expiration_date = fields.DateField(read_only=True)
312+
313+
def is_new(self):
314+
return False # this is predefined - never will be new
315+
316+
class Meta:
317+
parent = Instance
318+
endpoints = {
319+
'list': {
320+
'methods': ['get', 'put'],
321+
'path': '/push_notifications/apns/config/',
322+
},
323+
'detail': {
324+
'methods': ['get', 'put'],
325+
'path': '/push_notifications/apns/config/',
326+
},
327+
}
3.13 KB
Binary file not shown.

tests/integration_test_push.py

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,75 @@
11
# -*- coding: utf-8 -*-
2+
import uuid
3+
24
from syncano.exceptions import SyncanoRequestError
3-
from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage
5+
from syncano.models import APNSConfig, APNSDevice, APNSMessage, GCMConfig, GCMDevice, GCMMessage
46
from tests.integration_test import InstanceMixin, IntegrationTest
57

68

7-
class PushNotificationTest(InstanceMixin, IntegrationTest):
9+
class PushIntegrationTest(InstanceMixin, IntegrationTest):
10+
11+
@classmethod
12+
def setUpClass(cls):
13+
super(PushIntegrationTest, cls).setUpClass()
14+
15+
cls.gcm_config = GCMConfig(
16+
development_api_key=uuid.uuid4().hex,
17+
instance_name=cls.instance.name
18+
)
19+
cls.gcm_config.save()
20+
21+
with open('tests/certificates/ApplePushDevelopment.p12', 'rb') as cert:
22+
cls.apns_config = APNSConfig(
23+
development_certificate=cert,
24+
development_certificate_name='test',
25+
development_bundle_identifier='test1234',
26+
instance_name=cls.instance.name
27+
)
28+
cls.apns_config.save()
29+
30+
cls.environment = 'development'
31+
cls.gcm_device = GCMDevice(
32+
instance_name=cls.instance.name,
33+
label='example label',
34+
registration_id=86152312314401555,
35+
device_id='10000000001',
36+
)
37+
cls.gcm_device.save()
38+
39+
cls.apns_device = APNSDevice(
40+
instance_name=cls.instance.name,
41+
label='example label',
42+
registration_id='4719084371920471208947120984731208947910827409128470912847120894',
43+
device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a',
44+
)
45+
cls.apns_device.save()
46+
47+
48+
class PushNotificationTest(PushIntegrationTest):
49+
50+
def test_gcm_config_update(self):
51+
gcm_config = GCMConfig.please.get()
52+
new_key = uuid.uuid4().hex
53+
gcm_config.development_api_key = new_key
54+
gcm_config.save()
55+
56+
gcm_config_ = GCMConfig.please.get()
57+
self.assertEqual(gcm_config_.development_api_key, new_key)
58+
59+
def test_apns_config_update(self):
60+
apns_config = APNSConfig.please.get()
61+
new_cert_name = 'new cert name'
62+
apns_config.development_certificate_name = new_cert_name
63+
apns_config.save()
64+
65+
apns_config_ = APNSConfig.please.get()
66+
self.assertEqual(apns_config_.development_certificate_name, new_cert_name)
67+
868
def test_gcm_device(self):
969
device = GCMDevice(
1070
instance_name=self.instance.name,
1171
label='example label',
12-
registration_id=86152312314401555,
72+
registration_id=86152312314401666,
1373
device_id='10000000001',
1474
)
1575
self._test_device(device, GCMDevice.please)
@@ -18,43 +78,58 @@ def test_apns_device(self):
1878
device = APNSDevice(
1979
instance_name=self.instance.name,
2080
label='example label',
21-
registration_id='4719084371920471208947120984731208947910827409128470912847120894',
81+
registration_id='4719084371920471208947120984731208947910827409128470912847120222',
2282
device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a',
2383
)
2484

2585
self._test_device(device, APNSDevice.please)
2686

87+
def test_send_message_gcm(self):
88+
89+
self.assertEqual(0, len(list(GCMMessage.please.all())))
90+
91+
self.gcm_device.send_message(content={'environment': self.environment, 'data': {'c': 'more_c'}})
92+
93+
self.assertEqual(1, len(list(GCMMessage.please.all())))
94+
95+
def test_send_message_apns(self):
96+
self.assertEqual(0, len(list(APNSMessage.please.all())))
97+
98+
self.apns_device.send_message(content={'environment': 'development', 'aps': {'alert': 'alert test'}})
99+
100+
self.assertEqual(1, len(list(APNSMessage.please.all())))
101+
27102
def test_gcm_message(self):
28103
message = GCMMessage(
29104
instance_name=self.instance.name,
30105
content={
31106
'registration_ids': ['TESTIDREGISRATION', ],
107+
'environment': 'production',
32108
'data': {
33109
'param1': 'test'
34110
}
35111
}
36112
)
37113

38-
self._test_message(message, GCMMessage.please)
114+
self._test_message(message, GCMMessage.please) # we want this to fail; no productions keys;
39115

40116
def test_apns_message(self):
41117
message = APNSMessage(
42118
instance_name=self.instance.name,
43119
content={
44120
'registration_ids': ['TESTIDREGISRATION', ],
121+
'environment': 'production',
45122
'aps': {'alert': 'semo example label'}
46123
}
47124
)
48125

49-
self._test_message(message, APNSMessage.please)
126+
self._test_message(message, APNSMessage.please) # we want this to fail; no productions certs;
50127

51128
def _test_device(self, device, manager):
52129

53-
self.assertFalse(manager.all(instance_name=self.instance.name))
54-
55130
device.save()
56131

57-
self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 1)
132+
self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 2)
58133

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

66-
device.delete()
141+
# test update:
142+
new_label = 'totally new label'
143+
device.label = new_label
144+
device.save()
67145

68-
self.assertFalse(manager.all(instance_name=self.instance.name))
146+
device_ = manager.get(instance_name=self.instance.name, registration_id=device.registration_id)
147+
self.assertEqual(new_label, device_.label)
148+
149+
device.delete()
69150

70151
def _test_message(self, message, manager):
71152
self.assertFalse(manager.all(instance_name=self.instance.name))

0 commit comments

Comments
 (0)