Skip to content

Commit

Permalink
Add tcpserver backend mode
Browse files Browse the repository at this point in the history
  • Loading branch information
liminspace committed Jul 24, 2016
1 parent 8466764 commit eaae776
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ target/

# db
tests/db.sqlite3

# node
node_modules/
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ language: python

python:
- "2.7"
- "3.4"
- "3.5"

env:
- DJANGO_VERSION=1.9.5
- DJANGO_VERSION=1.9.8

before_install:
- . $HOME/.nvm/nvm.sh
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
0.2.0 (2016-07-24)
==================
* Add backend mode TPCServer
* Remove Python 3.4 from tests
* Upgrade Django to 1.9.8 in tests


0.1.2 (2016-05-01)
==================
* Fix release tools and setup.py


0.1.0 (2016-04-30)
==================
* Migrate to MJML 2.x
Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ Load ``mjml`` in your django template and use ``mjml`` tag that will compile mjm
</mj-body>
</mjml>
{% endmjml %}

2 changes: 1 addition & 1 deletion mjml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '0.1.2'
__version__ = '0.2.0'

default_app_config = 'mjml.apps.MJMLConfig'
51 changes: 51 additions & 0 deletions mjml/node/tcpserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';


var host = '127.0.0.1',
port = '42201',
argv = process.argv.slice(2);


switch (argv.length) {
case 0:
break;
case 1:
port = argv[0];
break;
case 2:
port = argv[0];
host = argv[1];
break;
default:
console.log('Run command: NODE_PATH=node_modules node tcpserver.js 42201 127.0.0.1');
}


var mjml = require('mjml'),
net = require('net'),
server = net.createServer();


function handleConnection(conn) {
conn.setEncoding('utf8');
conn.on('data', function(d) {
var result;
try {
result = mjml.mjml2html(d.toString());
conn.write('0');
} catch (err) {
result = err.message;
conn.write('1');
}
conn.write(('000000000' + Buffer.byteLength(result).toString()).slice(-9));
conn.write(result);
});
conn.once('close', function() {});
conn.on('error', function(err) {});
}


server.on('connection', handleConnection);
server.listen(port, host, function () {
console.log('RUN SERVER %s:%s', host, port);
});
9 changes: 9 additions & 0 deletions mjml/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
from django.conf import settings

MJML_BACKEND_MODE = getattr(settings, 'MJML_BACKEND_MODE', 'cmd')
assert MJML_BACKEND_MODE in ('cmd', 'tcpserver')

# cmd backend mode configs
MJML_EXEC_CMD = getattr(settings, 'MJML_EXEC_CMD', 'mjml')

# tcpserver backend mode configs
MJML_TCPSERVERS = getattr(settings, 'MJML_TCPSERVERS', [('127.0.0.1', 42201)])
assert isinstance(MJML_TCPSERVERS, (list, tuple))
for t in MJML_TCPSERVERS:
assert isinstance(t, (list, tuple)) and len(t) == 2 and isinstance(t[0], str) and isinstance(t[1], int)
39 changes: 38 additions & 1 deletion mjml/tools.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import socket
import random
import subprocess
from . import settings as mjml_settings


def mjml_render(mjml_code):
def _mjml_render_by_cmd(mjml_code):
cmd_args = mjml_settings.MJML_EXEC_CMD
if not isinstance(cmd_args, list):
cmd_args = [cmd_args]
Expand All @@ -19,3 +21,38 @@ def mjml_render(mjml_code):
'See https://github.com/mjmlio/mjml#installation'
)
return html


def _mjml_render_by_tcpserver(mjml_code):
servers = list(mjml_settings.MJML_TCPSERVERS)[:]
random.shuffle(servers)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
for host, port in servers:
try:
s.connect((host, port))
except socket.error:
continue
try:
s.send(mjml_code.encode('utf8'))
ok = s.recv(1) == '0'
result_len = int(s.recv(9))
result = s.recv(result_len)
if ok:
return result
else:
raise RuntimeError('MJML compile error (via MJML TCP server): {}'.format(result))
finally:
s.close()
raise RuntimeError('MJML compile error (via MJML TCP server): no working server')


def mjml_render(mjml_code):
if mjml_code is '':
return mjml_code

if mjml_settings.MJML_BACKEND_MODE == 'cmd':
return _mjml_render_by_cmd(mjml_code)
elif mjml_settings.MJML_BACKEND_MODE == 'tcpserver':
return _mjml_render_by_tcpserver(mjml_code)
else:
raise RuntimeError('Invalid settings.MJML_BACKEND_MODE "{}"'.format(mjml_settings.MJML_BACKEND_MODE))
8 changes: 8 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@
},
},
]

MJML_BACKEND = 'cmd'
MJML_EXEC_CMD = os.path.join(os.path.dirname(BASE_DIR), 'node_modules', '.bin', 'mjml')
MJML_TCPSERVERS = (
('127.0.0.1', 42201),
('127.0.0.1', 42202),
('127.0.0.1', 42203),
)
69 changes: 69 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# coding=utf-8
import copy
import os
import subprocess
import time
from contextlib import contextmanager
from django.test import TestCase
from django.template import Template, Context
from django.template.exceptions import TemplateSyntaxError
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from mjml.apps import check_mjml_command
from mjml import settings as mjml_settings

Expand Down Expand Up @@ -176,3 +180,68 @@ def test_unicode(self):
self.assertIn('<html ', html)
self.assertIn('<body', html)
self.assertIn(u'Український текст', html)


class TestMJMLTCPServer(TestCase):
processes = []

@classmethod
def setUpClass(cls):
super(TestMJMLTCPServer, cls).setUpClass()
root_dir = os.path.dirname(settings.BASE_DIR)
tcpserver_path = os.path.join(root_dir, 'mjml', 'node', 'tcpserver.js')
env = os.environ.copy()
env['NODE_PATH'] = root_dir
for host, port in mjml_settings.MJML_TCPSERVERS:
p = subprocess.Popen(['node', tcpserver_path, str(port), host],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
cls.processes.append(p)
time.sleep(5)

@classmethod
def tearDownClass(cls):
super(TestMJMLTCPServer, cls).tearDownClass()
while cls.processes:
p = cls.processes.pop()
p.terminate()

def render_tpl(self, tpl, context=None):
return Template('{% load mjml %}' + tpl).render(Context(context))

def test_simple(self):
with safe_change_mjml_settings():
mjml_settings.MJML_BACKEND = 'tcpserver'

html = self.render_tpl("""
{% mjml %}
<mjml>
<mj-body>
<mj-container>
<mj-section>
<mj-column>
<mj-image src="img/test.png"></mj-image>
<mj-text font-size="20px" align="center">Test title</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-button background-color="#ffcc00" font-size="15px">Test button</mj-button>
</mj-column>
</mj-section>
</mj-container>
</mj-body>
</mjml>
{% endmjml %}
""")
self.assertIn('<html ', html)
self.assertIn('<body', html)
self.assertIn('20px ', html)
self.assertIn('Test title', html)
self.assertIn('Test button', html)

with self.assertRaises(RuntimeError):
self.render_tpl("""
{% mjml %}
123
{% endmjml %}
""")

0 comments on commit eaae776

Please sign in to comment.