From 2ad5e284526b2b2f55e50d5cf2cf293e990c5e80 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Fri, 12 Jun 2015 23:08:27 +1000 Subject: [PATCH 1/7] WIP on Django 1.8 compatibility. --- dddp/api.py | 20 +++++++++++++------- dddp/msg.py | 18 ++++++++++++------ setup.py | 4 ++-- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/dddp/api.py b/dddp/api.py index 603748d..dfb4c81 100644 --- a/dddp/api.py +++ b/dddp/api.py @@ -11,8 +11,11 @@ import dbarray from django.conf import settings from django.contrib.auth import get_user_model from django.db import connection, connections -from django.db.models import aggregates, Q -from django.db.models.expressions import ExpressionNode +from django.db.models import aggregates, Q, Expression +try: + from django.db.models.expressions import ExpressionNode +except ImportError: + ExpressionNode = None from django.db.models.sql import aggregates as sql_aggregates from django.utils.encoding import force_text from django.db import DatabaseError @@ -353,11 +356,14 @@ class Collection(APIMixin): def serialize(self, obj, meteor_ids): """Generate a DDP msg for obj with specified msg type.""" - # check for F expressions - exps = [ - name for name, val in vars(obj).items() - if isinstance(val, ExpressionNode) - ] + if ExpressionNode is None: + exps = False + else: + # check for F expressions + exps = [ + name for name, val in vars(obj).items() + if isinstance(val, ExpressionNode) + ] if exps: # clone/update obj with values but only for the expression fields obj = deepcopy(obj) diff --git a/dddp/msg.py b/dddp/msg.py index 892f49f..4029b5b 100644 --- a/dddp/msg.py +++ b/dddp/msg.py @@ -1,16 +1,22 @@ """Django DDP utils for DDP messaging.""" from copy import deepcopy from dddp import THREAD_LOCAL as this, REMOVED -from django.db.models.expressions import ExpressionNode +try: + from django.db.models.expressions import ExpressionNode +except AttributeError: + ExpressionNode = None def obj_change_as_msg(obj, msg): """Generate a DDP msg for obj with specified msg type.""" - # check for F expressions - exps = [ - name for name, val in vars(obj).items() - if isinstance(val, ExpressionNode) - ] + if ExpressionNode is None: + exps = False + else: + # check for F expressions + exps = [ + name for name, val in vars(obj).items() + if isinstance(val, ExpressionNode) + ] if exps: # clone and update obj with values but only for the expression fields obj = deepcopy(obj) diff --git a/setup.py b/setup.py index 99d473e..32e4432 100644 --- a/setup.py +++ b/setup.py @@ -14,10 +14,10 @@ setup( packages=find_packages(), include_package_data=True, install_requires=[ - 'Django>=1.7,<1.8', + 'Django>=1.7', 'psycopg2>=2.5.4', 'gevent>=1.0', - 'gevent-websocket>=0.9', + 'gevent-websocket>=0.9,!=0.9.4', 'meteor-ejson>=1.0', 'psycogreen>=1.0', 'django-dbarray>=0.2', From d60ce9479a6c937fcb6e24594bb8438249304e36 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Sun, 14 Jun 2015 00:02:29 +1000 Subject: [PATCH 2/7] Don't barf on multiple invocations of greenify(). --- dddp/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dddp/__init__.py b/dddp/__init__.py index a2d4dcd..7c3c404 100644 --- a/dddp/__init__.py +++ b/dddp/__init__.py @@ -26,10 +26,9 @@ REMOVED = 'removed' def greenify(): """Patch threading and psycopg2 modules for green threads.""" - if 'threading' in sys.modules: + from gevent.monkey import patch_all, saved + if ('threading' in sys.modules) and ('threading' not in saved): raise Exception('threading module loaded before patching!') - - from gevent.monkey import patch_all patch_all() from psycogreen.gevent import patch_psycopg From 16e48e50bef20578d433ad1ae6d81141e1fb14a9 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Sun, 14 Jun 2015 00:02:29 +1000 Subject: [PATCH 3/7] Don't barf on multiple invocations of greenify(). --- dddp/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dddp/__init__.py b/dddp/__init__.py index a2d4dcd..7c3c404 100644 --- a/dddp/__init__.py +++ b/dddp/__init__.py @@ -26,10 +26,9 @@ REMOVED = 'removed' def greenify(): """Patch threading and psycopg2 modules for green threads.""" - if 'threading' in sys.modules: + from gevent.monkey import patch_all, saved + if ('threading' in sys.modules) and ('threading' not in saved): raise Exception('threading module loaded before patching!') - - from gevent.monkey import patch_all patch_all() from psycogreen.gevent import patch_psycopg From 71324420df350bba5423006a444927e33c1a5ae2 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Sun, 14 Jun 2015 00:19:49 +1000 Subject: [PATCH 4/7] Remove unused imports from AppConfig module. --- dddp/apps.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dddp/apps.py b/dddp/apps.py index 9cbd8bc..6692e6b 100644 --- a/dddp/apps.py +++ b/dddp/apps.py @@ -4,11 +4,8 @@ from __future__ import print_function from django.apps import AppConfig from django.conf import settings, ImproperlyConfigured -from django.db import DatabaseError -from django.db.models import signals from dddp import autodiscover -from dddp.models import Connection class DjangoDDPConfig(AppConfig): From 9b18e4833a3550d424aa18fddbf5373d142ce3f1 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Sun, 14 Jun 2015 00:19:49 +1000 Subject: [PATCH 5/7] Remove unused imports from AppConfig module. --- dddp/apps.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dddp/apps.py b/dddp/apps.py index 9cbd8bc..6692e6b 100644 --- a/dddp/apps.py +++ b/dddp/apps.py @@ -4,11 +4,8 @@ from __future__ import print_function from django.apps import AppConfig from django.conf import settings, ImproperlyConfigured -from django.db import DatabaseError -from django.db.models import signals from dddp import autodiscover -from dddp.models import Connection class DjangoDDPConfig(AppConfig): From 07ccc014bc362e857abef5fabaeb97608e71b68e Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Sun, 14 Jun 2015 03:31:27 +1000 Subject: [PATCH 6/7] Fix array aggregate for Django 1.8 --- dddp/api.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/dddp/api.py b/dddp/api.py index dfb4c81..cdce7b7 100644 --- a/dddp/api.py +++ b/dddp/api.py @@ -11,11 +11,11 @@ import dbarray from django.conf import settings from django.contrib.auth import get_user_model from django.db import connection, connections -from django.db.models import aggregates, Q, Expression +from django.db.models import aggregates, Q try: from django.db.models.expressions import ExpressionNode except ImportError: - ExpressionNode = None + from django.db.models import Expression as ExpressionNode from django.db.models.sql import aggregates as sql_aggregates from django.utils.encoding import force_text from django.db import DatabaseError @@ -51,6 +51,7 @@ class Array(aggregates.Aggregate): """Array aggregate function.""" func = 'ARRAY' + function = 'array_agg' name = 'Array' def add_to_query(self, query, alias, col, source, is_summary): @@ -67,9 +68,17 @@ class Array(aggregates.Aggregate): return 'ArrayType' new_source = ArrayField() - super(Array, self).add_to_query( - query, alias, col, new_source, is_summary, - ) + try: + super(Array, self).add_to_query( + query, alias, col, new_source, is_summary, + ) + except AttributeError: + query.aggregates[alias] = new_source + + def convert_value(self, value, expression, connection, context): + if not value: + return [] + return value def api_endpoint(path_or_func): @@ -356,14 +365,11 @@ class Collection(APIMixin): def serialize(self, obj, meteor_ids): """Generate a DDP msg for obj with specified msg type.""" - if ExpressionNode is None: - exps = False - else: - # check for F expressions - exps = [ - name for name, val in vars(obj).items() - if isinstance(val, ExpressionNode) - ] + # check for F expressions + exps = [ + name for name, val in vars(obj).items() + if isinstance(val, ExpressionNode) + ] if exps: # clone/update obj with values but only for the expression fields obj = deepcopy(obj) From 4911b94f4de5c357108f6a7b9003e8023792188d Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Sun, 14 Jun 2015 04:56:05 +1000 Subject: [PATCH 7/7] Bump version number, update CHANGES and README. --- CHANGES.rst | 21 +++++++++++++++------ README.rst | 9 +++++---- setup.py | 2 +- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e97de57..adf4092 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,18 @@ Change Log ========== +0.9.0 +----- +* Added Django 1.8 compatibility. The current implementation has a + hackish (but functional) implementation to use PostgreSQL's + `array_agg` function. Pull requests are welcome. +* Retained compatibility with Django 1.7, though we still depend on the + `dbarray` package for this even though not strictly required with + Django 1.8. Once again, pull requests are welcome. + 0.8.1 ----- -* Add missing dependency on `pybars3` used to render boilerplate HTML +* Add missing dependency on `pybars3` used to render boilerplate HTML template when serving Meteor application files. 0.8.0 @@ -14,7 +23,7 @@ Change Log 0.7.0 ----- -* Refactor serialization to improve performance through reduced number +* Refactor serialization to improve performance through reduced number of database queries, especially on sub/unsub. * Fix login/logout user subscription, now emitting user `added`/ `removed` upon `login`/`logout` respectively. @@ -28,9 +37,9 @@ Change Log ----- * Send `removed` messages when client unsubscribes from publications. * Add support for SSL options and --settings=SETTINGS args in dddp tool. -* Add `optional` and `label` attributes to ManyToManyField simple +* Add `optional` and `label` attributes to ManyToManyField simple schema. -* Check order of added/changed when emitting WebSocket frames rather +* Check order of added/changed when emitting WebSocket frames rather than when queuing messages. * Move test projects into path that can be imported post install. @@ -40,7 +49,7 @@ Change Log 0.6.2 ----- -* Bugfix issue where DDP connection thread stops sending messages after +* Bugfix issue where DDP connection thread stops sending messages after changing item that has subscribers for other connections but not self. 0.6.1 @@ -49,7 +58,7 @@ Change Log * Dump stack trace to console on error for easier debugging DDP apps. * Fix handing of F expressions in object change handler. * Send `nosub` in response to invalid subscription request. -* Per connection tracking of sent objects so changed/added sent +* Per connection tracking of sent objects so changed/added sent appropriately. 0.6.0 diff --git a/README.rst b/README.rst index 1e87a46..9fd100c 100644 --- a/README.rst +++ b/README.rst @@ -18,11 +18,12 @@ Peer servers subscribe to aggregate broadcast events which are de-multiplexed an Limitations ----------- -The current release series only supports DDP via WebSockets_. Future -development may resolve this by using SockJS, to support browsers that -don't have WebSockets. +* No support for the SockJS protocol to support browsers that + don't have WebSockets_ (see http://caniuse.com/websockets for + supported browsers). -Changes must be made via the Django ORM as django-ddp uses `Django signals`_ to receive model save/update signals. +* Changes must be made via the Django ORM as django-ddp uses `Django + signals`_ to receive model save/update signals. Installation diff --git a/setup.py b/setup.py index 32e4432..c768106 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-ddp', - version='0.8.1', + version='0.9.0', description=__doc__, long_description=open('README.rst').read(), author='Tyson Clugg',