From 6ea0e71a741dc74bddd31238d61877f5d9156971 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Tue, 28 Jul 2015 14:04:11 +1000 Subject: [PATCH 1/9] Bugfix dddp.accounts forgot_password feature. --- dddp/accounts/ddp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dddp/accounts/ddp.py b/dddp/accounts/ddp.py index 05d328e..d49b0e8 100644 --- a/dddp/accounts/ddp.py +++ b/dddp/accounts/ddp.py @@ -538,7 +538,7 @@ class Auth(APIMixin): except self.user_model.DoesNotExist: self.auth_failed() - days_valid = HASH_DAYS_VALID[HashPurpose.PASSWORD_RESET], + days_valid = HASH_DAYS_VALID[HashPurpose.PASSWORD_RESET] token = get_user_token( user=user, purpose=HashPurpose.PASSWORD_RESET, days_valid=days_valid, From ca2be7fec7b409989147f8d68b5b7d304848c3d4 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Tue, 28 Jul 2015 14:05:25 +1000 Subject: [PATCH 2/9] Update CHANGES.rst, bump version number. --- CHANGES.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 88647f4..0055509 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Change Log ========== +0.10.1 +------ +* Bugfix dddp.accounts forgot_password feature. + 0.10.0 ------ * Stop processing request middleware upon connection - see diff --git a/setup.py b/setup.py index a736cae..8b1c8c9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-ddp', - version='0.10.0', + version='0.10.1', description=__doc__, long_description=open('README.rst').read(), author='Tyson Clugg', From 5ec27b669c9c0712f43cf2b1453f4d82a3a76c8a Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Mon, 10 Aug 2015 16:34:39 +1000 Subject: [PATCH 3/9] Add BackdoorServer support to dddp command, add "Logs" publication. --- dddp/api.py | 2 ++ dddp/ddp.py | 22 ++++++++++++++++++++++ dddp/logging.py | 39 +++++++++++++++++++++++++++++++++++++++ dddp/main.py | 47 ++++++++++++++++++++++++++++++++++++++--------- dddp/websocket.py | 3 +-- 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 dddp/ddp.py create mode 100644 dddp/logging.py diff --git a/dddp/api.py b/dddp/api.py index d61d9aa..1780d27 100644 --- a/dddp/api.py +++ b/dddp/api.py @@ -597,6 +597,7 @@ class DDP(APIMixin): 'params_ejson': ejson.dumps(params), }, ) + this.subs.setdefault(sub.publication, set()).add(sub.pk) if not created: if not silent: this.send({'msg': 'ready', 'subs': [id_]}) @@ -636,6 +637,7 @@ class DDP(APIMixin): for obj in qs: payload = col.obj_change_as_msg(obj, REMOVED, meteor_ids) this.send(payload) + this.subs[sub.publication].remove(sub.pk) sub.delete() if not silent: this.send({'msg': 'nosub', 'id': id_}) diff --git a/dddp/ddp.py b/dddp/ddp.py new file mode 100644 index 0000000..0af0f11 --- /dev/null +++ b/dddp/ddp.py @@ -0,0 +1,22 @@ +from dddp import THREAD_LOCAL as this +from dddp.api import API, Publication +from django.contrib import auth + + +class Logs(Publication): + + users = auth.get_user_model() + + def get_queries(self): + user_pk = getattr(this, 'user_id', False) + if user_pk: + if self.users.objects.filter( + pk=user_pk, + is_active=True, + is_superuser=True, + ).exists(): + return [] + raise ValueError('User not permitted.') + + +API.register([Logs]) diff --git a/dddp/logging.py b/dddp/logging.py new file mode 100644 index 0000000..147873f --- /dev/null +++ b/dddp/logging.py @@ -0,0 +1,39 @@ +"""Django DDP logging helpers.""" +from __future__ import absolute_import, print_function + +import logging + +from dddp import THREAD_LOCAL as this, meteor_random_id, ADDED + + + + +class DDPHandler(logging.Handler): + + """Logging handler that streams log events via DDP to the current client.""" + + def __init__(self, *args, **kwargs): + print(self.__class__, args, kwargs) + self.logger = logging.getLogger('django.db.backends') + self.logger.info('Test') + super(DDPHandler, self).__init__(*args, **kwargs) + + def emit(self, record): + """Emit a formatted log record via DDP.""" + if getattr(this, 'subs', {}).get('Logs', False): + this.send({ + 'msg': ADDED, + 'collection': 'logs', + 'id': meteor_random_id('/collection/logs'), + 'fields': { + # 'name': record.name, + # 'levelno': record.levelno, + 'levelname': record.levelname, + # 'pathname': record.pathname, + # 'lineno': record.lineno, + 'msg': record.msg, + 'args': record.args, + # 'exc_info': record.exc_info, + # 'funcName': record.funcName, + }, + }) diff --git a/dddp/main.py b/dddp/main.py index 2b0f863..e8ab34a 100644 --- a/dddp/main.py +++ b/dddp/main.py @@ -60,7 +60,7 @@ def ddpp_sockjs_info(environ, start_response): ])) -def serve(listen, debug=False, **ssl_args): +def serve(listen, debug=False, verbosity=1, debug_port=0, **ssl_args): """Spawn greenlets for handling websockets and PostgreSQL calls.""" import signal from django.apps import apps @@ -99,7 +99,7 @@ def serve(listen, debug=False, **ssl_args): ) # setup WebSocketServer to dispatch web requests - webservers = [ + servers = [ geventwebsocket.WebSocketServer( (host, port), resource, @@ -113,8 +113,8 @@ def serve(listen, debug=False, **ssl_args): def killall(*args, **kwargs): """Kill all green threads.""" pgworker.stop() - for webserver in webservers: - webserver.stop() + for server in servers: + server.stop() # die gracefully with SIGINT or SIGQUIT gevent.signal(signal.SIGINT, killall) @@ -133,19 +133,38 @@ def serve(listen, debug=False, **ssl_args): ) # start greenlets + if debug_port: + from gevent.backdoor import BackdoorServer + servers.append( + BackdoorServer( + ('127.0.0.1', debug_port), + banner='Django DDP', + locals={ + 'servers': servers, + 'pgworker': pgworker, + 'killall': killall, + 'api': api, + 'resource': resource, + 'settings': settings, + 'wsgi_app': wsgi_app, + 'wsgi_name': wsgi_name, + }, + ) + ) + pgworker.start() print('=> Started PostgresGreenlet.') - web_threads = [ - gevent.spawn(webserver.serve_forever) - for webserver - in webservers + threads = [ + gevent.spawn(server.serve_forever) + for server + in servers ] print('=> Started DDPWebSocketApplication.') print('=> Started your app (%s).' % wsgi_name) print('') for host, port in listen: print('=> App running at: http://%s:%d/' % (host, port)) - gevent.joinall(web_threads) + gevent.joinall(threads) pgworker.stop() gevent.joinall([pgworker]) @@ -190,6 +209,14 @@ def main(): import argparse parser = argparse.ArgumentParser(description=__doc__) django = parser.add_argument_group('Django Options') + django.add_argument( + '--verbosity', '-v', metavar='VERBOSITY', dest='verbosity', type=int, + default=1, + ) + django.add_argument( + '--debug-port', metavar='DEBUG_PORT', dest='debug_port', type=int, + default=0, + ) django.add_argument( '--settings', metavar='SETTINGS', dest='settings', help="The Python path to a settings module, e.g. " @@ -218,8 +245,10 @@ def main(): os.environ['DJANGO_SETTINGS_MODULE'] = namespace.settings serve( namespace.listen or [Addr('localhost', 8000)], + debug_port=namespace.debug_port, keyfile=namespace.keyfile, certfile=namespace.certfile, + verbosity=namespace.verbosity, ) diff --git a/dddp/websocket.py b/dddp/websocket.py index b599e29..0676363 100644 --- a/dddp/websocket.py +++ b/dddp/websocket.py @@ -106,7 +106,6 @@ class DDPWebSocketApplication(geventwebsocket.WebSocketApplication): version = None support = None connection = None - subs = None remote_ids = None base_handler = BaseHandler() @@ -131,7 +130,7 @@ class DDPWebSocketApplication(geventwebsocket.WebSocketApplication): '{0[REMOTE_ADDR]}:{0[REMOTE_PORT]}'.format( self.ws.environ, ) - self.subs = {} + this.subs = {} self.logger.info('+ %s OPEN', self) self.send('o') self.send('a["{\\"server_id\\":\\"0\\"}"]') From 573886d96fb94e3577ee4fe9d94e2a938a49c9f7 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Mon, 10 Aug 2015 16:39:37 +1000 Subject: [PATCH 4/9] Update CHANGES.rst, bump version number. --- CHANGES.rst | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0055509..af1de8f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Change Log ========== +0.10.2 +------ +* Add `Logs` publication that can be configured to emit logs via DDP + through the use of the `dddp.logging.DDPHandler` log handler. +* Add option to dddp daemon to provide a BackdoorServer (telnet) for + interactive debugging (REPL) at runtime. + 0.10.1 ------ * Bugfix dddp.accounts forgot_password feature. diff --git a/setup.py b/setup.py index 8b1c8c9..ce1b417 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-ddp', - version='0.10.1', + version='0.10.2', description=__doc__, long_description=open('README.rst').read(), author='Tyson Clugg', From 7083c5c92f0c8bfd4f00d93e9dc4cc03456dba85 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Mon, 10 Aug 2015 18:47:04 +1000 Subject: [PATCH 5/9] Chunked payload for NOTIFY/LISTEN to get around 8KB limit on payload. --- dddp/api.py | 32 ++++++++++++++++++++++++++------ dddp/postgres.py | 25 ++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/dddp/api.py b/dddp/api.py index 1780d27..b3d8d97 100644 --- a/dddp/api.py +++ b/dddp/api.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals, print_function import collections from copy import deepcopy import traceback +import uuid # requirements import dbarray @@ -888,13 +889,32 @@ class DDP(APIMixin): if my_connection_id in connection_ids: # msg must go to connection that initiated the change payload['_tx_id'] = this.ws.get_tx_id() + # header is sent in every payload + header = { + 'uuid': uuid.uuid1().int, # UUID1 should be unique + 'seq': 1, # increments for each 8KB chunk + 'fin': 0, # zero if more chunks expected, 1 if last chunk. + } + data = ejson.dumps(payload) cursor = connections[using].cursor() - cursor.execute( - 'NOTIFY "ddp", %s', - [ - ejson.dumps(payload), - ], - ) + while data: + hdr = ejson.dumps(header) + # use all available payload space for chunk + max_len = 8000 - len(hdr) - 100 + # take a chunk from data + chunk, data = data[:max_len], data[max_len:] + if not data: + # last chunk, set fin=1. + header['fin'] = 1 + hdr = ejson.dumps(header) + # print('NOTIFY: %s' % hdr) + cursor.execute( + 'NOTIFY "ddp", %s', + [ + '%s|%s' % (hdr, chunk), # pipe separates hdr|chunk. + ], + ) + header['seq'] += 1 # increment sequence. API = DDP() diff --git a/dddp/postgres.py b/dddp/postgres.py index 2c2404c..51844d0 100644 --- a/dddp/postgres.py +++ b/dddp/postgres.py @@ -22,6 +22,7 @@ class PostgresGreenlet(gevent.Greenlet): # queues for processing incoming sub/unsub requests and processing self.connections = {} + self.chunks = {} self._stop_event = gevent.event.Event() # connect to DB in async mode @@ -62,7 +63,29 @@ class PostgresGreenlet(gevent.Greenlet): "Got NOTIFY (pid=%d, payload=%r)", notify.pid, notify.payload, ) - data = ejson.loads(notify.payload) + + # read the header and check seq/fin. + hdr, chunk = notify.payload.split('|', 1) + # print('RECEIVE: %s' % hdr) + header = ejson.loads(hdr) + uuid = header['uuid'] + size, chunks = self.chunks.setdefault(uuid, [0, {}]) + if header['fin']: + size = self.chunks[uuid][0] = header['seq'] + + # stash the chunk + chunks[header['seq']] = chunk + + if len(chunks) != size: + # haven't got all the chunks yet + continue # process next NOTIFY in loop + + # got the last chunk -> process it. + data = ''.join( + chunk for _, chunk in sorted(chunks.items()) + ) + del self.chunks[uuid] # don't forget to cleanup! + data = ejson.loads(data) sender = data.pop('_sender', None) tx_id = data.pop('_tx_id', None) for connection_id in data.pop('_connection_ids'): From edac793c2e8a033733b35bbafc8ec2f98861dacd Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Mon, 10 Aug 2015 18:50:36 +1000 Subject: [PATCH 6/9] Update CHANGES.rst, bump version number. --- CHANGES.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index af1de8f..5a8edb4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Change Log ========== +0.11.0 +------ +* Support more than 8KB of change data by splitting large payloads into + multiple chunks. + 0.10.2 ------ * Add `Logs` publication that can be configured to emit logs via DDP diff --git a/setup.py b/setup.py index ce1b417..50e90a1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-ddp', - version='0.10.2', + version='0.11.0', description=__doc__, long_description=open('README.rst').read(), author='Tyson Clugg', From 00b143297482143b85f819afa3b44d05d2e5da50 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Thu, 23 Jul 2015 11:07:20 +1000 Subject: [PATCH 7/9] Allow support for multiple meteor apps in a single django-ddp enabled project by reading METEOR_STAR_JSON as part of view init instead of app ready. --- dddp/server/apps.py | 171 ------------------------------------------ dddp/server/views.py | 175 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 178 deletions(-) diff --git a/dddp/server/apps.py b/dddp/server/apps.py index 3065be9..4a742e9 100644 --- a/dddp/server/apps.py +++ b/dddp/server/apps.py @@ -1,37 +1,9 @@ """Django DDP Server app config.""" from __future__ import print_function, absolute_import, unicode_literals -import io import mimetypes -import os.path from django.apps import AppConfig -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from ejson import dumps, loads -import pybars - - -STAR_JSON_SETTING_NAME = 'METEOR_STAR_JSON' - - -def read(path, default=None, encoding='utf8'): - """Read encoded contents from specified path or return default.""" - if not path: - return default - try: - with io.open(path, mode='r', encoding=encoding) as contents: - return contents.read() - except IOError: - if default is not None: - return default - raise - - -def read_json(path): - """Read JSON encoded contents from specified path.""" - with open(path, mode='r') as json_file: - return loads(json_file.read()) class ServerConfig(AppConfig): @@ -41,149 +13,6 @@ class ServerConfig(AppConfig): name = 'dddp.server' verbose_name = 'Django DDP Meteor Web Server' - manifest = None - program_json = None - program_json_path = None - runtime_config = None - - star_json = None # top level layout - server_json = None # web server layout - web_browser_json = None # web.browser (client) layout - - url_map = None - internal_map = None - server_load_map = None - template_path = None # web server HTML template path - client_map = None # web.browser (client) URL to path map - html = '\nDDP App' - def ready(self): """Configure Django DDP server app.""" mimetypes.init() # read and process /etc/mime.types - self.url_map = {} - - try: - json_path = getattr(settings, STAR_JSON_SETTING_NAME) - except AttributeError: - raise ImproperlyConfigured( - '%s setting required by dddp.server.view.' % ( - STAR_JSON_SETTING_NAME, - ), - ) - - self.star_json = read_json(json_path) - star_format = self.star_json['format'] - if star_format != 'site-archive-pre1': - raise ValueError( - 'Unknown Meteor star format: %r' % star_format, - ) - programs = { - program['name']: program - for program in self.star_json['programs'] - } - - server_json_path = os.path.join( - os.path.dirname(json_path), - os.path.dirname(programs['server']['path']), - 'program.json', - ) - self.server_json = read_json(server_json_path) - server_format = self.server_json['format'] - if server_format != 'javascript-image-pre1': - raise ValueError( - 'Unknown Meteor server format: %r' % server_format, - ) - self.server_load_map = {} - for item in self.server_json['load']: - item['path_full'] = os.path.join( - os.path.dirname(server_json_path), - item['path'], - ) - self.server_load_map[item['path']] = item - self.url_map[item['path']] = ( - item['path_full'], 'text/javascript' - ) - try: - item['source_map_full'] = os.path.join( - os.path.dirname(server_json_path), - item['sourceMap'], - ) - self.url_map[item['sourceMap']] = ( - item['source_map_full'], 'text/plain' - ) - except KeyError: - pass - self.template_path = os.path.join( - os.path.dirname(server_json_path), - self.server_load_map[ - 'packages/boilerplate-generator.js' - ][ - 'assets' - ][ - 'boilerplate_web.browser.html' - ], - ) - - web_browser_json_path = os.path.join( - os.path.dirname(json_path), - programs['web.browser']['path'], - ) - self.web_browser_json = read_json(web_browser_json_path) - web_browser_format = self.web_browser_json['format'] - if web_browser_format != 'web-program-pre1': - raise ValueError( - 'Unknown Meteor web.browser format: %r' % ( - web_browser_format, - ), - ) - self.client_map = {} - self.internal_map = {} - for item in self.web_browser_json['manifest']: - item['path_full'] = os.path.join( - os.path.dirname(web_browser_json_path), - item['path'], - ) - if item['where'] == 'client': - if '?' in item['url']: - item['url'] = item['url'].split('?', 1)[0] - self.client_map[item['url']] = item - self.url_map[item['url']] = ( - item['path_full'], - mimetypes.guess_type( - item['path_full'], - )[0] or 'application/octet-stream', - ) - elif item['where'] == 'internal': - self.internal_map[item['type']] = item - - config = { - 'css': [ - {'url': item['path']} - for item in self.web_browser_json['manifest'] - if item['type'] == 'css' and item['where'] == 'client' - ], - 'js': [ - {'url': item['path']} - for item in self.web_browser_json['manifest'] - if item['type'] == 'js' and item['where'] == 'client' - ], - 'meteorRuntimeConfig': '"%s"' % ( - dumps(self.runtime_config) - ), - 'rootUrlPathPrefix': '/app', - 'bundledJsCssPrefix': '/app/', - 'inlineScriptsAllowed': False, - 'inline': None, - 'head': read( - self.internal_map.get('head', {}).get('path_full', None), - default=u'', - ), - 'body': read( - self.internal_map.get('body', {}).get('path_full', None), - default=u'', - ), - } - tmpl_raw = read(self.template_path, encoding='utf8') - compiler = pybars.Compiler() - tmpl = compiler.compile(tmpl_raw) - self.html = '\n%s' % tmpl(config) diff --git a/dddp/server/views.py b/dddp/server/views.py index 59e4575..9084fc5 100644 --- a/dddp/server/views.py +++ b/dddp/server/views.py @@ -1,13 +1,35 @@ """Django DDP Server views.""" from __future__ import print_function, absolute_import, unicode_literals -from ejson import dumps -from django.apps import apps + +import io +import mimetypes +import os.path + +from ejson import dumps, loads from django.conf import settings from django.http import HttpResponse from django.views.generic import View +import pybars -STAR_JSON_SETTING_NAME = 'METEOR_STAR_JSON' + +def read(path, default=None, encoding='utf8'): + """Read encoded contents from specified path or return default.""" + if not path: + return default + try: + with io.open(path, mode='r', encoding=encoding) as contents: + return contents.read() + except IOError: + if default is not None: + return default + raise + + +def read_json(path): + """Read JSON encoded contents from specified path.""" + with open(path, mode='r') as json_file: + return loads(json_file.read()) class MeteorView(View): @@ -16,13 +38,152 @@ class MeteorView(View): http_method_names = ['get', 'head'] - app = None + json_path = None runtime_config = None + manifest = None + program_json = None + program_json_path = None + runtime_config = None + + star_json = None # top level layout + + url_map = None + internal_map = None + server_load_map = None + template_path = None # web server HTML template path + client_map = None # web.browser (client) URL to path map + html = '\nDDP App' + + root_url_path_prefix = '' + bundled_js_css_prefix = '/' + def __init__(self, **kwargs): """Initialisation for Django DDP server view.""" + # super(...).__init__ assigns kwargs to instance. super(MeteorView, self).__init__(**kwargs) - self.app = apps.get_app_config('server') + + self.url_map = {} + + # process `star_json` + self.star_json = read_json(self.json_path) + star_format = self.star_json['format'] + if star_format != 'site-archive-pre1': + raise ValueError( + 'Unknown Meteor star format: %r' % star_format, + ) + programs = { + program['name']: program + for program in self.star_json['programs'] + } + + # process `bundle/programs/server/program.json` from build dir + server_json_path = os.path.join( + os.path.dirname(self.json_path), + os.path.dirname(programs['server']['path']), + 'program.json', + ) + server_json = read_json(server_json_path) + server_format = server_json['format'] + if server_format != 'javascript-image-pre1': + raise ValueError( + 'Unknown Meteor server format: %r' % server_format, + ) + self.server_load_map = {} + for item in server_json['load']: + item['path_full'] = os.path.join( + os.path.dirname(server_json_path), + item['path'], + ) + self.server_load_map[item['path']] = item + self.url_map[item['path']] = ( + item['path_full'], 'text/javascript' + ) + try: + item['source_map_full'] = os.path.join( + os.path.dirname(server_json_path), + item['sourceMap'], + ) + self.url_map[item['sourceMap']] = ( + item['source_map_full'], 'text/plain' + ) + except KeyError: + pass + self.template_path = os.path.join( + os.path.dirname(server_json_path), + self.server_load_map[ + 'packages/boilerplate-generator.js' + ][ + 'assets' + ][ + 'boilerplate_web.browser.html' + ], + ) + + # process `bundle/programs/web.browser/program.json` from build dir + web_browser_json_path = os.path.join( + os.path.dirname(self.json_path), + programs['web.browser']['path'], + ) + web_browser_json = read_json(web_browser_json_path) + web_browser_format = web_browser_json['format'] + if web_browser_format != 'web-program-pre1': + raise ValueError( + 'Unknown Meteor web.browser format: %r' % ( + web_browser_format, + ), + ) + self.client_map = {} + self.internal_map = {} + for item in web_browser_json['manifest']: + item['path_full'] = os.path.join( + os.path.dirname(web_browser_json_path), + item['path'], + ) + if item['where'] == 'client': + if '?' in item['url']: + item['url'] = item['url'].split('?', 1)[0] + self.client_map[item['url']] = item + self.url_map[item['url']] = ( + item['path_full'], + mimetypes.guess_type( + item['path_full'], + )[0] or 'application/octet-stream', + ) + elif item['where'] == 'internal': + self.internal_map[item['type']] = item + + config = { + 'css': [ + {'url': item['path']} + for item in web_browser_json['manifest'] + if item['type'] == 'css' and item['where'] == 'client' + ], + 'js': [ + {'url': item['path']} + for item in web_browser_json['manifest'] + if item['type'] == 'js' and item['where'] == 'client' + ], + 'meteorRuntimeConfig': '"%s"' % ( + dumps(self.runtime_config) + ), + 'rootUrlPathPrefix': self.root_url_path_prefix, + 'bundledJsCssPrefix': self.bundled_js_css_prefix, + 'inlineScriptsAllowed': False, + 'inline': None, + 'head': read( + self.internal_map.get('head', {}).get('path_full', None), + default=u'', + ), + 'body': read( + self.internal_map.get('body', {}).get('path_full', None), + default=u'', + ), + } + tmpl_raw = read(self.template_path, encoding='utf8') + compiler = pybars.Compiler() + tmpl = compiler.compile(tmpl_raw) + self.html = '\n%s' % tmpl(config) def get(self, request, path): """Return HTML (or other related content) for Meteor.""" @@ -46,7 +207,7 @@ class MeteorView(View): content_type='text/javascript', ) try: - file_path, content_type = self.app.url_map[path] + file_path, content_type = self.url_map[path] with open(file_path, 'r') as content: return HttpResponse( content.read(), @@ -54,5 +215,5 @@ class MeteorView(View): ) except KeyError: print(path) - return HttpResponse(self.app.html) + return HttpResponse(self.html) # raise Http404 From e5784ac4caee6447258cb801ef425bf2126c0c13 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Tue, 11 Aug 2015 14:55:43 +1000 Subject: [PATCH 8/9] Drop dddp.server app, dddp.server.views moved to dddp.views --- dddp/server/__init__.py | 1 - dddp/server/apps.py | 18 ------------------ dddp/{server => }/views.py | 3 +++ 3 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 dddp/server/__init__.py delete mode 100644 dddp/server/apps.py rename dddp/{server => }/views.py (99%) diff --git a/dddp/server/__init__.py b/dddp/server/__init__.py deleted file mode 100644 index cea26b4..0000000 --- a/dddp/server/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'dddp.server.apps.ServerConfig' diff --git a/dddp/server/apps.py b/dddp/server/apps.py deleted file mode 100644 index 4a742e9..0000000 --- a/dddp/server/apps.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Django DDP Server app config.""" -from __future__ import print_function, absolute_import, unicode_literals - -import mimetypes - -from django.apps import AppConfig - - -class ServerConfig(AppConfig): - - """Django config for dddp.server app.""" - - name = 'dddp.server' - verbose_name = 'Django DDP Meteor Web Server' - - def ready(self): - """Configure Django DDP server app.""" - mimetypes.init() # read and process /etc/mime.types diff --git a/dddp/server/views.py b/dddp/views.py similarity index 99% rename from dddp/server/views.py rename to dddp/views.py index 9084fc5..76ebfac 100644 --- a/dddp/server/views.py +++ b/dddp/views.py @@ -63,6 +63,9 @@ class MeteorView(View): # super(...).__init__ assigns kwargs to instance. super(MeteorView, self).__init__(**kwargs) + # read and process /etc/mime.types + mimetypes.init() + self.url_map = {} # process `star_json` From 66ed01a393db11e16acac22b22c94b96777a2625 Mon Sep 17 00:00:00 2001 From: Tyson Clugg Date: Tue, 11 Aug 2015 14:56:39 +1000 Subject: [PATCH 9/9] Update README.rst, CHANGES.rst and bumped version number. --- CHANGES.rst | 6 +++++ README.rst | 63 ++++++++++++++++++++++++++++++++++++++++++++++------- setup.py | 2 +- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5a8edb4..2eae416 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Change Log ========== +0.12.0 +------ +* Get path to `star.json` from view config (defined in your urls.py) + instead of from settings. +* Dropped `dddp.server.views`, use `dddp.views` instead. + 0.11.0 ------ * Support more than 8KB of change data by splitting large payloads into diff --git a/README.rst b/README.rst index afd6ece..367cc6e 100644 --- a/README.rst +++ b/README.rst @@ -103,6 +103,19 @@ Add ddp.py to your Django application: [Book, Author, AllBooks, BooksByAuthorEmail] ) +Start the Django DDP service: + +.. code:: sh + + DJANGO_SETTINGS_MODULE=myproject.settings dddp + + +Using django-ddp as a secondary DDP connection (RAPID DEVELOPMENT) +------------------------------------------------------------------ + +Running in this manner allows rapid development through use of the hot +code push features provided by Meteor. + Connect your Meteor application to the Django DDP service: .. code:: javascript @@ -118,19 +131,53 @@ Connect your Meteor application to the Django DDP service: Django.subscribe('BooksByAuthorEmail', 'janet@evanovich.com'); } -Start the Django DDP service: - -.. code:: sh - - DJANGO_SETTINGS_MODULE=myproject.settings dddp - -In a separate terminal, start Meteor (from within your meteor -application directory): +Start Meteor (from within your meteor application directory): .. code:: sh meteor +Using django-ddp as the primary DDP connection (RECOMMENDED) +------------------------------------------------------------ + +If you'd prefer to not have two DDP connections (one to Meteor and one +to django-ddp) you can set the `DDP_DEFAULT_CONNECTION_URL` environment +variable to use the specified URL as the primary DDP connection in +Meteor. When doing this, you won't need to use `DDP.connect(...)` or +specify `{connection: Django}` on your collections. Running with +django-ddp as the primary connection is recommended, and indeed required +if you wish to use `dddp.accounts` to provide authentication using +`django.contrib.auth` to your meteor app. + +.. code:: sh + + DDP_DEFAULT_CONNECTION_URL=http://localhost:8000/ meteor + + +Serving your Meteor applications from django-ddp +------------------------------------------------ + +First, you will need to build your meteor app into a directory (examples +below assume target directory named `myapp`): + +.. code:: sh + + meteor build ../myapp + +Then, add a MeteorView to your urls.py: + +.. code:: python + + from dddp.views import MeteorView + + urlpatterns = patterns( + url('^(?P/.*)$', MeteorView.as_view( + json_path=os.path.join( + settings.PROJ_ROOT, 'myapp', 'bundle', 'star.json', + ), + ), + ) + Adding API endpoints (server method definitions) ------------------------------------------------ diff --git a/setup.py b/setup.py index 50e90a1..c84f740 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-ddp', - version='0.11.0', + version='0.12.0', description=__doc__, long_description=open('README.rst').read(), author='Tyson Clugg',