diff --git a/.travis.yml b/.travis.yml index eba3223..a61f73b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,27 @@ language: python python: - "2.7" - - "3.4" + - "3.5" + - "3.7" + - "3.8" env: - DJANGO_VERSION=1.7.8 - DJANGO_VERSION=1.8.1 + - DJANGO_VERSION=2.2.16 + - DJANGO_VERSION=3.1.1 + - DJANGO_VERSION=1.8.19 install: + # Install node 6 (current LTS as of January 2017) + - "rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 6" + # Install latest npm - "npm install -g npm" + # Install node deps for render server - "cd tests" - "npm install" - "cd .." + # Setup python packages - "pip install Django==$DJANGO_VERSION" - "pip install -r requirements.txt" diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c631c..2d309cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,57 @@ Changelog ========= + +### 4.3.0 (8/10/2018) + +- Add option to pass a URL explicitly instead of reading from settings. + ([jchen-eb](https://github.com/jchen-eb)) + https://github.com/markfinger/python-react/pull/88 + + +### 4.2.1 (17/6/2018) + +- Fix for a missing argument bug that could occur with rendering deactivated. + + +### 4.2.0 (17/6/2018) + +- API for returning extra items returned by render server. + ([Sassan Haradji](https://github.com/sassanh)) + https://github.com/markfinger/python-react/pull/87 + + +### 4.1.1 (9/8/2017) + +- Update timeout in render_server.py. + ([Mike Plis](https://github.com/mplis)) + https://github.com/markfinger/python-react/pull/81 + + +### 4.1.0 (1/3/2017) + +- `render_component` now accepts a `timeout` keyword argument which is passed to `RenderServer.render`. + ([Corey Burmeister](https://github.com/cburmeister)) + https://github.com/markfinger/python-react/pull/74 + + +### 4.0.0 (28/2/2017) + +- **Possibly breaking change** `RenderServer.render` now accepts a `timeout` keyword argument. There are some + edge-cases where this may break down-stream code. + ([Corey Burmeister](https://github.com/cburmeister)) + https://github.com/markfinger/python-react/pull/73 +- Documentation updates regarding production environments. The key takeaway is to ensure that you are using + the `NODE_ENV=production` environment variable so that React runs without debugging helpers which slow down + rendering. +- Documentation updates regarding `RenderServer` API. + + +### 3.0.1 (6/4/2016) + +- Documentation updates. + + ### 3.0.0 (6/4/2016) - **Possibly breaking change** render_component now accepts a `request_headers` keyword argument. @@ -21,6 +72,7 @@ Changelog ([Pringels](https://github.com/Pringels)) https://github.com/markfinger/python-react/pull/55 + ### 2.0.0 (22/9/2015) - **Breaking change** The base renderer's __init__ no longer accepts the RENDER_URL setting as an argument. @@ -45,6 +97,7 @@ Changelog - Fixed a potential path issue in config files - Replaced the webpack-service dependency with webpack-wrapper. + ### 0.8.0 (26/1/2015) - Boosting render performance by using a dedicated render server. @@ -53,6 +106,7 @@ Changelog render server, 'django_react.render_server.ReactRenderServer'. The legacy renderer is useable by setting DJANGO_REACT['RENDERER'] = 'django_react.renderer.ReactRenderer'. + ### 0.7.0 (2/1/2015) - Changed `django_react.exceptions.ReactComponentMissingSourceAttribute` to `django_react.exceptions.ReactComponentMissingSource` @@ -63,10 +117,12 @@ Changelog - The Python<->JS bridge used to render components now relies on a `--serialized-props-file` argument, formerly it was `--serialized-props`. - Switched the JSX loader to a fork which improves the debug information provided during error handling + ### 0.6.0 (24/12/2014) - The NODE_ENV environment setting is now controlled by the `DJANGO_REACT['DEBUG']` setting. Activating it will provides some improvements to the rendering performance. + ### 0.5.0 (14/12/2014) - Renamed `django_react.exceptions.PropSerialisationError` to `django_react.exceptions.PropSerializationError`. @@ -88,6 +144,7 @@ Changelog - Added a test suite and harness. - Added basic documentation. + ### 0.4.0 (11/12/2014) - Fixed a bug where errors caused during a component's prop serialization could silently fail. @@ -100,6 +157,7 @@ Changelog - `ReactComponent.get_component_variable` is now `ReactComponent.get_library`. - Moved the Webpack configuration into the ReactComponent class. + ### 0.3.0 (3/12/2014) - `django_react.exceptions.ReactComponentSourceFileNotFound` is now `django_react.exceptions.SourceFileNotFound` @@ -110,10 +168,12 @@ Changelog - `django_react.utils.render` no longer accepts a `ReactComponent` as an argument, it now takes `path_to_source`, `serialised_props`, and `to_static_markup`. - `django_react/render.js` no longer accepts the `--path-to-component` argument, instead it takes `--path-to-source`. + ### 0.2.0 (3/12/2014) - Replaced the post-install step in setup.py with django-node's dependency and package resolver. + ### 0.1.0 (2/12/2014) - Initial release diff --git a/README.md b/README.md index 0ab5e57..b73e30c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Documentation - [Usage in development](#usage-in-development) - [Usage in production](#usage-in-production) - [Overriding the renderer](#overriding-the-renderer) -- [Django integration](#django-integration) - [Settings](#settings) +- [Frequently Asked Questions](#frequently-asked-questions) - [Running the tests](#running-the-tests) @@ -65,10 +65,11 @@ rendered = render_component('path/to/component.jsx', {'foo': 'bar'}) print(rendered) ``` -The object returned has two properties: +The object returned has three properties: - `markup` - the rendered markup - `props` - the JSON-serialized props + - `data` - the data returned by the render server If the object is coerced to a string, it will emit the value of the `markup` attribute. @@ -153,6 +154,12 @@ render_component( request_headers={ 'Accept-Language': 'da, en-gb;q=0.8, en;q=0.7' }, + + # An optional timeout that is used when handling communications with the render server. + # Can be an integer, a float, or a tuple containing two numeric values (the two values + # represent the individual timeouts on the send & receive phases of the request). + # Note that if not defined, this value will default to (5, 5) + timeout=None ) ``` @@ -196,6 +203,11 @@ In production environments, you should ensure that `RENDER` is set to True. You will want to run the render server under whatever supervisor process suits your need. Depending on your setup, you may need to change the `RENDER_URL` setting to reflect your environment. +The render server should be run with the `NODE_ENV` environment variable set to `production`, +eg: `NODE_ENV=production node render_server.js`. React defaults to development mode and relies on the +`NODE_ENV` variable to deactivate dev-oriented code (types and constraint checking) that slows down renders. +Defining this variable will ensure that your code is rendered much faster. + Depending on your load, you may want to use a worker farm to handle rendering. Node's [cluster module](https://nodejs.org/api/cluster.html) provides an easy way to fork a process and serve multiple instances from a single network address. @@ -222,7 +234,7 @@ you can consistently define the `renderer` argument to `render_component`. For e from react.render import render_component class MyRenderer(object): - def render(self, path, props=None, to_static_markup=False): + def render(self, path, props=None, to_static_markup=False, request_headers=None, timeout=None): # ... def my_render_function(*args, **kwargs): @@ -244,7 +256,7 @@ from react.conf import settings DEBUG = True settings.configure( - RENDER=not DEBUG, + RENDER=not DEBUG, RENDER_URL='http://127.0.0.1:9009/render', ) ``` @@ -259,7 +271,7 @@ INSTALLED_APPS = ( ) REACT = { - 'RENDER': not DEBUG, + 'RENDER': not DEBUG, 'RENDER_URL': 'http://127.0.0.1:8001/render', } ``` @@ -286,6 +298,46 @@ A complete url to an endpoint which accepts POST requests conforming to Default: `'http://127.0.0.1:9009/render'` +Frequently Asked Questions +-------------------------- + +### How do I return extra data from the render server? + +You can edit the render server's code and annotate the returned payload with whatever data +that you like. The payload provided by the render server is available under the `data` attribute +of the response object. + +For example: + +```python +from react.render import render_component + +rendered = render_component('path/to/component.js') + +print(rendered.data) +``` + +### Can python-react integrate with Django? + +python-react can integrate with Django's settings and the renderer integration can +resolve relative paths to components via django's static file finders. + +### How do I handle Django's translation and gettext with React components? + +[sillygod](https://github.com/sillygod) sparked a discussion of this at issue +[#69](https://github.com/markfinger/python-react/issues/69). + +### Can python-react integrate with Web2Py? + +[Anima-t3d](https://github.com/Anima-t3d) has a write-up of their experience +in [#70](https://github.com/markfinger/python-react/issues/70#issuecomment-254396083). + +### How do I pass child components to the root component? + +[Anima-t3d](https://github.com/Anima-t3d) sparked a discussion of this at issue +[#71](https://github.com/markfinger/python-react/issues/71). + + Running the tests ----------------- diff --git a/examples/frontend-rendering-with-webpack/README.md b/examples/frontend-rendering-with-webpack/README.md index f7b9908..05f9421 100644 --- a/examples/frontend-rendering-with-webpack/README.md +++ b/examples/frontend-rendering-with-webpack/README.md @@ -1,7 +1,7 @@ Running the example =================== -As mentioned in the ***Using React on the front-end*** section, [Webpack](https://webpack.github.io/) is used to bundle the respective js files into `dist.js` and included in `index.html`. To make React attributes like `onClick` etc. work, the app has to be re-rendered (along with all the props passed down) when it loads on the browswer. React is intelligent enough to not re-paint the browser and only update the changes, thus adding all the component properties. +As mentioned in the ***Using React on the front-end*** section, [Webpack](https://webpack.github.io/) is used to bundle the respective js files into `dist.js` and included in `index.html`. To make React attributes like `onClick` etc. work, the app has to be re-rendered (along with all the props passed down) when it loads on the browser. React is intelligent enough to not re-paint the browser and only update the changes, thus adding all the component properties. In this example, the basic_rendering example is modified to submit the Comment Form through ajax and update the Comment List by fetching the updated comments and rendering the application with new props. diff --git a/react/__init__.py b/react/__init__.py index 90a0993..66cf931 100644 --- a/react/__init__.py +++ b/react/__init__.py @@ -1,3 +1,3 @@ -__version__ = '3.0.1' +__version__ = '4.3.0' default_app_config = 'react.apps.ReactConfig' \ No newline at end of file diff --git a/react/render.py b/react/render.py index 83173d0..447c340 100644 --- a/react/render.py +++ b/react/render.py @@ -4,7 +4,7 @@ from .render_server import render_server -def render_component(path, props=None, to_static_markup=False, renderer=render_server, request_headers=None): +def render_component(path, props=None, to_static_markup=False, renderer=render_server, request_headers=None, timeout=None): if not os.path.isabs(path): abs_path = staticfiles.find(path) if not abs_path: @@ -14,4 +14,4 @@ def render_component(path, props=None, to_static_markup=False, renderer=render_s if not os.path.exists(path): raise ComponentSourceFileNotFound(path) - return renderer.render(path, props, to_static_markup, request_headers) + return renderer.render(path, props, to_static_markup, request_headers, timeout=timeout) diff --git a/react/render_server.py b/react/render_server.py index 63b07be..b7d7d48 100644 --- a/react/render_server.py +++ b/react/render_server.py @@ -8,9 +8,10 @@ class RenderedComponent(object): - def __init__(self, markup, props): + def __init__(self, markup, props, data): self.markup = markup self.props = props + self.data = data def __str__(self): return self.markup @@ -20,8 +21,9 @@ def __unicode__(self): class RenderServer(object): - def render(self, path, props=None, to_static_markup=False, request_headers=None): - url = conf.settings.RENDER_URL + def render(self, path, props=None, to_static_markup=False, request_headers=None, timeout=None, url=None): + if not url: + url = conf.settings.RENDER_URL if props is not None: serialized_props = json.dumps(props, cls=JSONEncoder) @@ -29,7 +31,7 @@ def render(self, path, props=None, to_static_markup=False, request_headers=None) serialized_props = None if not conf.settings.RENDER: - return RenderedComponent('', serialized_props) + return RenderedComponent('', serialized_props, {}) options = { 'path': path, @@ -45,12 +47,17 @@ def render(self, path, props=None, to_static_markup=False, request_headers=None) if request_headers is not None: all_request_headers.update(request_headers) + # Add a send/receive timeout with the request if not specified + if not isinstance(timeout, (tuple, int, float)): + timeout = 5.0 + try: res = requests.post( url, data=serialized_options, headers=all_request_headers, - params={'hash': options_hash} + params={'hash': options_hash}, + timeout=timeout ) except requests.ConnectionError: raise RenderServerError('Could not connect to render server at {}'.format(url)) @@ -62,8 +69,9 @@ def render(self, path, props=None, to_static_markup=False, request_headers=None) obj = res.json() - markup = obj.get('markup', None) - err = obj.get('error', None) + markup = obj.pop('markup', None) + err = obj.pop('error', None) + data = obj if err: if 'message' in err and 'stack' in err: @@ -75,7 +83,7 @@ def render(self, path, props=None, to_static_markup=False, request_headers=None) if markup is None: raise ReactRenderingError('Render server failed to return markup. Returned: {}'.format(obj)) - return RenderedComponent(markup, serialized_props) + return RenderedComponent(markup, serialized_props, data) render_server = RenderServer() diff --git a/tests/__init__.py b/tests/__init__.py index e2ef88f..b8d4847 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,10 +11,17 @@ # Ensure the process is killed on exit atexit.register(lambda _process: _process.kill(), process) -output = process.stdout.readline().decode('utf-8') +def read_line(): + return process.stdout.readline().decode('utf-8') +output = read_line() if output.strip() == '': - output += process.stdout.readline().decode('utf-8') + output += read_line() if 'React render server' not in output: - raise Exception('Unexpected output: "{}"'.format(output)) + if 'module.js' in output: + line = read_line() + while line: + output += line + os.linesep + line = read_line() + raise Exception('Unexpected output from render server subprocess...' + os.linesep + os.linesep + output)