diff --git a/.travis.yml b/.travis.yml index f6beff2d7..85379c4c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,12 +24,13 @@ install: - pip install pipenv - pipenv install --dev --ignore-pipfile --python=python$TRAVIS_PYTHON_VERSION - pipenv run python --version - - pipenv run pip install flake8-strict # not installable with pipenv, move ther when repaired + - pipenv run pip install flake8-strict # not installable with pipenv, move ther when repaired - pipenv run pip install -q $DJANGO_VERSION before_script: - - npm install -g bower + - npm install -g bower sass - pipenv run flake8 - pipenv run python manage.py bower install + - pipenv run python manage.py build_contenttools --npm $(which npm) - pipenv run python manage.py collectstatic -v0 --noinput - cd apps/aklub/ - DJANGO_SETTINGS_MODULE="" pipenv run django-admin compilemessages @@ -42,6 +43,6 @@ after_script: addons: postgresql: "9.6" matrix: - allow_failures: - - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' - - env: DJANGO_VERSION="Django>=3.0,<3.1" + allow_failures: + - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' + - env: DJANGO_VERSION="Django>=3.0,<3.1" diff --git a/Dockerfile b/Dockerfile index f5600bc98..07a5706d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ WORKDIR "/home/aplikace" run pip3 install pipenv -RUN npm install -g less bower +RUN npm install -g less bower sass RUN pip3 install pipenv==2018.11.14 RUN useradd test RUN chsh test -s /bin/bash diff --git a/Pipfile b/Pipfile index 795be1682..5d6b812f3 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ django-stdimage = "*" reportlab = "*" django-extensions = "*" "psycopg2" = "*" -django-wysiwyg = "*" +django-wysiwyg = {editable = true,git = "https://github.com/tmszi/django-wysiwyg.git",ref = "enable_editor_for_more_fields"} django-tinymce = "*" django-admin-tools = "*" feedparser = "*" @@ -48,7 +48,6 @@ django-sesame = "==1.4" django-ses = "*" drf-yasg = "*" vokativ = "*" -Django = ">=2,<3" "PyPDF2" = "*" django-post_office = {ref = "jsonfield2",git = "https://github.com/Mogost/django-post_office",editable = true} # for django-jsonfield2 transition raven = "*" @@ -92,7 +91,14 @@ faker = "*" # TODO: fix computedfields error in version 0.1.0 (syntax changed in models) django-computedfields = "==0.0.23" django-oauth-toolkit = "*" - +django-rest-swagger = "*" +django-filter = "*" +django-js-urls = "*" +mkdocs = "*" +cairosvg = "*" +django = "<3,>=2" +django-phonenumber-field = "*" +phonenumbers = "*" [dev-packages] coverage = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 13c3ad966..cc2bae8ab 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -30,6 +30,13 @@ ], "version": "==2.6.1" }, + "babel": { + "hashes": [ + "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", + "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" + ], + "version": "==2.8.0" + }, "beautifulsoup4": { "hashes": [ "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35", @@ -68,13 +75,26 @@ ], "version": "==1.18.16" }, - "celery": { + "cairocffi": { + "hashes": [ + "sha256:f1c0c5878f74ac9ccb5d48b2601fcc75390c881ce476e79f4cfedd288b1b05db" + ], + "version": "==1.1.0" + }, + "cairosvg": { "hashes": [ - "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", - "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" + "sha256:4e668f96653326780036ebb0a9ff2bb59a8443d7bcfc51a14aab77b57a8e67ad", + "sha256:9cb1df7e9bc60f75fb87f67940a8fb805aad544337a67a40b67c05cfe33711a2" ], "index": "pypi", - "version": "==4.3.0" + "version": "==2.4.2" + }, + "celery": { + "hashes": [ + "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916", + "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da" + ], + "version": "==4.4.6" }, "certifi": { "hashes": [ @@ -83,6 +103,39 @@ ], "version": "==2020.6.20" }, + "cffi": { + "hashes": [ + "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", + "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", + "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", + "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", + "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", + "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", + "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", + "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", + "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", + "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", + "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", + "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", + "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", + "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", + "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", + "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", + "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", + "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", + "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", + "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", + "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", + "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", + "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", + "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", + "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", + "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", + "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", + "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" + ], + "version": "==1.14.0" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -111,6 +164,13 @@ ], "version": "==0.0.4" }, + "cssselect2": { + "hashes": [ + "sha256:5c2716f06b5de93f701d5755a9666f2ee22cbcd8b4da8adddfc30095ffea3abc", + "sha256:97d7d4234f846f9996d838964d38e13b45541c18143bc55cf00e4bc1281ace76" + ], + "version": "==0.3.0" + }, "defusedxml": { "hashes": [ "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", @@ -227,7 +287,6 @@ "hashes": [ "sha256:eac59448965ebacae426a86bc572ec8f6683094a20fe4574179856ba0005b19f" ], - "index": "pypi", "version": "==4.0.1" }, "django-bower": { @@ -258,7 +317,6 @@ "sha256:fdf1255eecfbeb770c6521fe3e69989dfc6373cd5a7f0fe62038d37f80f47e48", "sha256:fe0b2a1b31d4a6234fea4b31986ddfd4644a48fab216ce1843f3ed0ddd2e9097" ], - "index": "pypi", "version": "==2.0.0" }, "django-celery-email": { @@ -333,6 +391,14 @@ "index": "pypi", "version": "==0.13.0" }, + "django-filter": { + "hashes": [ + "sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af", + "sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75" + ], + "index": "pypi", + "version": "==2.3.0" + }, "django-flexible-filter-conditions": { "hashes": [ "sha256:662a0705cd6ea47778cab702de33bf40d6b1932f1c645a62aacf8a7c067d9f3d" @@ -386,6 +452,14 @@ ], "version": "==1.2.2" }, + "django-js-urls": { + "hashes": [ + "sha256:9e578a1dae530968ea1c3d609a9177c7fe7bbc2774e5e61dcb2da4b311527cce", + "sha256:c51660f5f7ab22037e91ce161e4e74405bc2ee714573ce762899ef73c5d818a8" + ], + "index": "pypi", + "version": "==0.1.1" + }, "django-jsonfield": { "hashes": [ "sha256:7bdd0ea75ad842b9e33decdf343398c91fbb7bd664fde0648ef83e78b0453b6e", @@ -413,7 +487,6 @@ "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c", "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061" ], - "index": "pypi", "version": "==4.0.0" }, "django-mptt": { @@ -451,6 +524,14 @@ "index": "pypi", "version": "==1.3.2" }, + "django-phonenumber-field": { + "hashes": [ + "sha256:2ca3bb0ada0ebc164bd903a981a34f1202a4294006e520b0da961bd7ce9f20a4", + "sha256:d4580cc3352f4433962825f9927e6669852c1b40ec484fcb5a74064dabc1201a" + ], + "index": "pypi", + "version": "==4.0.0" + }, "django-polymorphic": { "editable": true, "git": "https://github.com/tmszi/django-polymorphic", @@ -497,6 +578,14 @@ ], "version": "==2.0.0" }, + "django-rest-swagger": { + "hashes": [ + "sha256:48f6aded9937e90ae7cbe9e6c932b9744b8af80cc4e010088b3278c700e0685b", + "sha256:b039b0288bab4665cd45dc5d16f94b13911bc4ad0ed55f74ad3b90aa31c87c17" + ], + "index": "pypi", + "version": "==2.2.0" + }, "django-ses": { "hashes": [ "sha256:45f041acb9f2f8df85f3fddd63b04f1d381982bdc7730a3dc1f88e5795758bdd", @@ -566,12 +655,9 @@ "version": "==3.0.2" }, "django-wysiwyg": { - "hashes": [ - "sha256:4de7214c0da50807cf1bef32d1ad16976048006d187a1b8daf0380ed326c424d", - "sha256:7c37c685379fa23402f415ba7f8f6bce3552d3836ed2cf99e37591e1cc0dffc3" - ], - "index": "pypi", - "version": "==0.8.0" + "editable": true, + "git": "https://github.com/tmszi/django-wysiwyg.git", + "ref": "def387340646697e49f17423a7534b4ba964bc24" }, "djangorestframework": { "hashes": [ @@ -758,6 +844,13 @@ ], "version": "==0.10.0" }, + "joblib": { + "hashes": [ + "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6", + "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49" + ], + "version": "==0.16.0" + }, "jsonfield2": { "hashes": [ "sha256:39610a8345b6fc71b45825d177b678b3d7fa9140c60cc488cf82bd1a35367a2b" @@ -771,6 +864,22 @@ ], "version": "==4.6.11" }, + "livereload": { + "hashes": [ + "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be" + ], + "version": "==2.6.2" + }, + "lunr": { + "extras": [ + "languages" + ], + "hashes": [ + "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca", + "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e" + ], + "version": "==0.5.8" + }, "lxml": { "hashes": [ "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f", @@ -805,7 +914,6 @@ "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd", "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee" ], - "index": "pypi", "version": "==4.5.2" }, "markdown": { @@ -866,6 +974,14 @@ ], "version": "==1.1.1" }, + "mkdocs": { + "hashes": [ + "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9", + "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39" + ], + "index": "pypi", + "version": "==1.1.2" + }, "newrelic": { "hashes": [ "sha256:0eaab77f295f6efa16887ef8594ec8601b1f1071fef8fc78818d71334e5545d4", @@ -885,6 +1001,12 @@ "index": "pypi", "version": "==5.20.1.150" }, + "nltk": { + "hashes": [ + "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35" + ], + "version": "==3.5" + }, "oauthlib": { "hashes": [ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", @@ -898,6 +1020,12 @@ ], "version": "==1.4.1" }, + "openapi-codec": { + "hashes": [ + "sha256:1bce63289edf53c601ea3683120641407ff6b708803b8954c8a876fe778d2145" + ], + "version": "==1.3.2" + }, "openpyxl": { "hashes": [ "sha256:18e11f9a650128a12580a58e3daba14e00a11d9e907c554a17ea016bf1a2c71b", @@ -912,6 +1040,14 @@ ], "version": "==20.4" }, + "phonenumbers": { + "hashes": [ + "sha256:d332078fe71c6153b5a263ac87283618b2afe514a248a14f06a0d39ce1f5ce0b", + "sha256:e49b8e21c557f0dafee966ddd55fb2bd3d6db155451999b75fb1b012e8d2016c" + ], + "index": "pypi", + "version": "==8.12.6" + }, "pillow": { "hashes": [ "sha256:04d984e45a0b9815f4b407e8aadb50f25fbb82a605d89db927376e94c3adf371", @@ -986,6 +1122,13 @@ "index": "pypi", "version": "==2.8.6" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", @@ -1011,7 +1154,6 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "index": "pypi", "version": "==2.8.1" }, "python-dotenv": { @@ -1068,7 +1210,6 @@ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" ], - "index": "pypi", "version": "==2020.1" }, "pyyaml": { @@ -1103,6 +1244,32 @@ "index": "pypi", "version": "==3.5.3" }, + "regex": { + "hashes": [ + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "version": "==2020.7.14" + }, "reportlab": { "hashes": [ "sha256:0145233d3596fa5828972eb474b5a9f3fd5dea45d6f196fe006a7a7a461fcd03", @@ -1261,7 +1428,6 @@ "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06", "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb" ], - "index": "pypi", "version": "==3.17.2" }, "single-beat": { @@ -1277,7 +1443,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "index": "pypi", "version": "==1.15.0" }, "sorl-thumbnail": { @@ -1311,11 +1476,10 @@ "yaml" ], "hashes": [ - "sha256:00654241e5beee437ba544e4fa4abef70ccec3668503aa95406c1250bb660770", - "sha256:6336e7aa3f0e5894b47270a3dc639cc2e78eb823c7ccb8c4512fb408cdc18d08" + "sha256:8cc2fa10bc37219ac5e76850eb7fbd50de313c7a1a7895c44af2a8dd206b7be7", + "sha256:c5a77c96ad306d5b380def1309d521ad5e98b4f83726f84857ad87e142d13279" ], - "index": "pypi", - "version": "==0.14.0" + "version": "==2.0.0" }, "text-unidecode": { "hashes": [ @@ -1324,6 +1488,13 @@ ], "version": "==1.3" }, + "tinycss2": { + "hashes": [ + "sha256:6427d0e3faa0a5e0e8c9f6437e2de26148a7a197a8b0992789f23d9a802788cf", + "sha256:9fdacc0e22d344ddd2ca053837c133900fe820ae1222f63b79617490a498507a" + ], + "version": "==1.0.2" + }, "tornadis": { "hashes": [ "sha256:690a38728ae1867253c69c8c824beda84d6b8752536d8c5619c8ea5f86ab3239" @@ -1342,6 +1513,13 @@ ], "version": "==5.1.1" }, + "tqdm": { + "hashes": [ + "sha256:6baa75a88582b1db6d34ce4690da5501d2a1cb65c34664840a456b2c9f794d29", + "sha256:fcb7cb5b729b60a27f300b15c1ffd4744f080fb483b88f31dc8654b082cc8ea5" + ], + "version": "==4.48.0" + }, "unicodecsv": { "hashes": [ "sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc" @@ -1406,7 +1584,6 @@ "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2", "sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde" ], - "index": "pypi", "version": "==1.2.0" }, "xlwt": { @@ -1455,11 +1632,10 @@ }, "celery": { "hashes": [ - "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", - "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" + "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916", + "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da" ], - "index": "pypi", - "version": "==4.3.0" + "version": "==4.4.6" }, "certifi": { "hashes": [ @@ -1718,7 +1894,6 @@ "sha256:3d332afce941c57f1990f45b083ba13252ba74fcd1ae43fd047e5af7a70fb312", "sha256:40d6e740aad7509e696a324b94cf2b0a104da93c3d4a7924cea1be3d0eb95b4f" ], - "index": "pypi", "version": "==2.0.0" }, "parso": { @@ -1797,7 +1972,6 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "index": "pypi", "version": "==2.8.1" }, "pytz": { @@ -1805,7 +1979,6 @@ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" ], - "index": "pypi", "version": "==2020.1" }, "requests": { @@ -1820,7 +1993,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "index": "pypi", "version": "==1.15.0" }, "sqlparse": { diff --git a/Procfile b/Procfile index 7893c0384..7c473a9c1 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ -release: python manage.py migrate +release: ./release.sh web: ./launch_on_heroku celery: ./launch_celery diff --git a/apps/aklub/admin.py b/apps/aklub/admin.py index b4c28ce3a..685c09672 100644 --- a/apps/aklub/admin.py +++ b/apps/aklub/admin.py @@ -80,6 +80,7 @@ from . import darujme, filters, mailing, tasks +from .custom_form_widgets import HtmlTemplateWidget from .filters import ProfileTypeFilter, unit_admin_mixin_generator from .forms import ( CompanyProfileAddForm, CompanyProfileChangeForm, EventForm, TaxConfirmationForm, UnitUserProfileAddForm, @@ -96,7 +97,10 @@ ProfileModelResource, get_polymorphic_parent_child_fields, ) from .profile_model_resources_mixin import ProfileModelResourceMixin -from .utils import check_annotate_filters, edit_donor_annotate_filter, sweet_text +from .utils import ( + check_annotate_filters, edit_donor_annotate_filter, + get_email_templates_names, sweet_text, +) def admin_links(args_generator): @@ -1536,9 +1540,58 @@ def save_form(self, request, form, change): class MassCommunicationForm(forms.ModelForm): + hidden_template = forms.CharField(widget=forms.HiddenInput(), required=False) + hidden_template_en = forms.CharField(widget=forms.HiddenInput(), required=False) + template_textarea = forms.CharField(widget=forms.Textarea(), required=False) + template_en_textarea = forms.CharField(widget=forms.Textarea(), required=False) + class Meta: model = MassCommunication fields = '__all__' + widgets = { + 'template': HtmlTemplateWidget( + attrs={ + 'class': 'template', + }, + ), + 'template_en': HtmlTemplateWidget( + attrs={ + 'class': 'template', + }, + ), + } + + class Media: + js = ( + 'jquery/dist/jquery.min.js', + 'webui-popover/dist/jquery.webui-popover.min.js', + 'aklub/js/csrf_token.js', + 'aklub/js/template_widget.min.js', + 'jquery-ui/jquery-ui.min.js', + 'jquery-mdl/dist/mdl-min.js', + 'jquery.inlineStyler/jquery.inlineStyler.min.js', + ) + css = { + 'all': ( + 'jquery-ui/themes/base/jquery-ui.min.css', + 'webui-popover/dist/jquery.webui-popover.min.css', + 'jquery-mdl/dist/mdl.css', + 'aklub/css/template.css', + 'aklub/css/template_name_dialog.css', + ), + } + + def __init__(self, *args, **kwargs): + super(MassCommunicationForm, self).__init__(*args, **kwargs) + self.fields['template_type'].empty_label = None + self.fields['template_name'] = forms.ChoiceField( + choices=get_email_templates_names(), + required=False, + ) + self.fields['template_en_name'] = forms.ChoiceField( + choices=get_email_templates_names(), + required=False, + ) def clean_send_to_users(self): v = EmailValidator() @@ -1557,6 +1610,16 @@ def clean_send_to_users(self): ) return self.cleaned_data['send_to_users'] + def clean(self): + super().clean() + cleaned_data = self.cleaned_data + if self.cleaned_data.get('hidden_template'): + cleaned_data['template'] = self.cleaned_data.get('hidden_template') + if self.cleaned_data.get('hidden_template_en'): + cleaned_data['template_en'] = self.cleaned_data.get('hidden_template_en') + + return cleaned_data + class MassCommunicationAdmin(unit_admin_mixin_generator('administrative_unit'), large_initial.LargeInitialMixin, admin.ModelAdmin): save_as = True @@ -1623,6 +1686,14 @@ def save_form(self, request, form, change): obj.save() return obj + def add_view(self, request, form_url='', extra_context=None): + request.POST = request.POST.copy() + if request.POST.get('hidden_template'): + request.POST['template'] = request.POST.get('hidden_template') + if request.POST.get('hidden_template_en'): + request.POST['template_en'] = request.POST.get('hidden_template_en') + return super(MassCommunicationAdmin, self).add_view(request, form_url=form_url, extra_context=extra_context) + def pair_payment_with_dpch(self, request, queryset): for account_statement in queryset: diff --git a/apps/aklub/custom_form_widgets.py b/apps/aklub/custom_form_widgets.py new file mode 100644 index 000000000..96bb80ff6 --- /dev/null +++ b/apps/aklub/custom_form_widgets.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from django.forms.widgets import Widget +from django.template import loader +from django.utils.safestring import mark_safe + + +class HtmlTemplateWidget(Widget): + template_name = "form_field_html_template_widget.html" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def render(self, name, value, attrs=None, renderer=None, **kwargs): + context = super().get_context(name, value, attrs) + template = loader.get_template(self.template_name).render(context) + + return mark_safe(template) diff --git a/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.mo b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.mo new file mode 100644 index 000000000..c36a15237 Binary files /dev/null and b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.mo differ diff --git a/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..25ea51384 --- /dev/null +++ b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-11 09:34+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/aklub/js/image_uploader.js:80 +msgid "Max allowed image width is 600 px." +msgstr "Maximální povolená šířka obrázku je 600 px." + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:16 +#: static/template_widget.js:525 +msgid "Edit template" +msgstr "Upravit šablonu" + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:124 +#: static/template_widget.js:166 +msgid "Template name" +msgstr "Název šablony" + +#: static/aklub/js/template_widget.min.js:2 +msgid "" +"Template name may consist of a-z, 0-9, underscores, not spaces and must " +"begin with a letter, and be lowercase." +msgstr "Název šablony může obsahovat a-z, 0-9, podtržítka, nikoli mezery " +"a musí začínat písmenem a musí být malá." + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:228 +msgid "Template name exist." +msgstr "Název šablony existuje." + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:262 +msgid "Ok" +msgstr "Potvrdit" + +#: static/template_widget.js:226 +msgid "" +"Template name may consist of a-z, 0-9, underscores, not spaces and must " +"begin with a letter,and be lowercase." +msgstr "Název šablony může obsahovat a-z, 0-9, podtržítka, nikoli mezery " +"a musí začínat písmenem a musí být malá." diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py new file mode 100644 index 000000000..2b30f8f42 --- /dev/null +++ b/apps/aklub/management/commands/build_contenttools.py @@ -0,0 +1,299 @@ +import json +import os +import pathlib +import re +import shutil +import subprocess + +from django.conf import settings +from django.contrib.staticfiles import finders +from django.core.management import BaseCommand, CommandError + + +class Command(BaseCommand): + help = 'Build js ContentTools library' # noqa + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument( + '--npm', + dest='npm', + default='/usr/bin/npm', + help='Npm js package manager path', + ) + + def handle(self, *args, **options): + self.npm_path = options.get('npm') + _finders = finders.AppDirectoriesFinder() + + self.npm_path = pathlib.Path(self.npm_path) + if not self.npm_path.exists(): + raise CommandError(f"Npm path '{self.npm_path}' doesn't exist.") + + # Aklub app assets dirs + self.js_src_dir = _finders.find_in_app( + app='aklub', + path='aklub/js', + ) + self.styles_src_dir = _finders.find_in_app( + app='aklub', + path='aklub/css', + ) + self.fonts_src_dir = _finders.find_in_app( + app='aklub', + path='aklub/fonts', + ) + + self.cttools_coffee_src_scripts_dir = ( + pathlib.Path(self.js_src_dir) / 'contenttools' / 'src' / + 'scripts' + ) + self.cttools_src_styles_dir = ( + pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / + 'styles' / 'ui' + ) + self.cttools_src_fonts_dir = ( + pathlib.Path(self.fonts_src_dir) / 'contenttools' + ) + self.cttools_src_sandbox_styles_dir = ( + pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / + 'sandbox' / 'styles' + ) + self.cttools_src_trans_dir = ( + pathlib.Path(self.js_src_dir) / 'contenttools' / 'src' / + 'translations' + ) + + # Target dirs + self.cttools_target_dir = ( + pathlib.Path(settings.BASE_DIR) / 'bower_components' / + 'ContentTools' + ) + + self.cttools_target_scripts_dir = ( + self.cttools_target_dir / 'src' / 'scripts' + ) + self.cttools_target_styles_dir = ( + self.cttools_target_dir / 'src' / 'styles' / 'ui' + ) + self.cttools_target_fonts_dir = ( + self.cttools_target_dir / 'build' / 'images' + ) + self.cttools_target_sandbox_dir = ( + self.cttools_target_dir / 'src' / 'sandbox' + ) + self.cttools_target_sandbox__build_dir = ( + self.cttools_target_dir / 'sandbox' + ) + self.cttools_target_trans_dir = ( + self.cttools_target_dir / 'translations' + ) + + self.__copy_coffee_scripts() + self.__copy_sass_styles() + self.__copy_fonts() + self.__copy_gruntfile() + self.__copy_localizations() + + os.chdir(self.cttools_target_dir) + + # Install npm packages + self.__install( + [self.npm_path, 'install'], + self.npm_path, + ) + # Install grunt-cli + self.__install( + [self.npm_path, 'install', 'grunt-cli'], + self.npm_path, + 'install grunt-cli', + ) + # Build ContentTools + grunt = ( + self.cttools_target_dir / 'node_modules' / 'grunt-cli' / + 'bin' / 'grunt' + ) + self.__install( + [grunt, 'build'], + grunt, + 'build', + ) + # Build ContentTools sandbox + self.__install( + [grunt, 'sandbox'], + grunt, + 'sandbox', + ) + + shutil.rmtree('node_modules/') + + def __copy_coffee_scripts(self): + # Copy coffee scripts + scripts = self.cttools_coffee_src_scripts_dir.glob('*.coffee') + for script_file in scripts: + shutil.copy( + script_file, + self.cttools_target_scripts_dir / script_file.name, + ) + + def __copy_sass_styles(self): + # Copy sass styles + styles = self.cttools_src_styles_dir.glob('*.scss') + for style_file in styles: + shutil.copy( + style_file, + self.cttools_target_styles_dir / style_file.name, + ) + + _ui_scss_file = self.cttools_target_styles_dir / '_ui.scss' + with open(_ui_scss_file, 'a+') as f: + f.seek(0) + content = f.read() + if '@import "custom-toolbox";' not in content: + f.write('\n@import "custom-toolbox";') + + # Copy sandbox append file + sandbox_append_scss_file = ( + self.cttools_src_sandbox_styles_dir / 'sandbox-append.scss' + ) + shutil.copy(sandbox_append_scss_file, self.cttools_target_sandbox_dir) + + # Apply sandbox replace file + _src_sandbox_scss_file = ( + self.cttools_src_sandbox_styles_dir / 'sandbox-replace.scss' + ) + _target_sandbox_scss_file = ( + self.cttools_target_sandbox_dir / 'sandbox.scss' + ) + + with open(_src_sandbox_scss_file, 'r') as f_read, \ + open(_target_sandbox_scss_file, 'r+') as f_write: + + new_content = None + source_content = f_read.readlines() + + max_width_base_text = '$max-width: 920px;' + + for line in source_content: + if '$max-width:' in line: + max_width_replace_text = line + + target_content = f_write.read() + + if max_width_base_text in target_content: + new_content = target_content.replace( + max_width_base_text, + max_width_replace_text, + ) + + if not new_content: + new_content = target_content + + new_content = self.__replace_css_import(new_content) + + if new_content: + f_write.seek(0) + f_write.truncate(0) + f_write.write(new_content) + + def __replace_css_import(self, content): + """Replace default css import path with correct path""" + + css_imports = re.findall('@import.*.;', content) + + for css_import in css_imports: + css_import_split = css_import.split(' ') + + try: + css_import_path = pathlib.PurePath( + css_import_split[1][:-1].replace('"', ''), + ) + except IndexError: + continue + + css_import_path_parts = list(css_import_path.parts) + if css_import_path_parts.count('..') < 2: + continue + + css_import_path_parts.pop(0) + + css_import_new_path = pathlib.PurePath(*css_import_path_parts) + new_css_import_text = ( + f"{css_import_split[0]} \"{css_import_new_path.as_posix()}\";" + ) + content = content.replace(css_import, new_css_import_text) + + return content + + def __copy_fonts(self): + """Copy font icons""" + + font_icon_file = self.cttools_src_fonts_dir / 'icons.woff' + shutil.copy( + font_icon_file, + self.cttools_target_fonts_dir / font_icon_file.name, + ) + + def __copy_gruntfile(self): + """Copy Gruntfile""" + + grunt_file = ( + pathlib.Path(self.js_src_dir) / 'contenttools' / 'Gruntfile.coffee' + ) + shutil.copy(grunt_file, self.cttools_target_dir / grunt_file.name) + + def __copy_localizations(self): + """Copy localization json files""" + + trans_src_files = self.cttools_src_trans_dir.glob('*.json') + + for trans_src_file in trans_src_files: + + trans_target_file = (self.cttools_target_trans_dir / + trans_src_file.name) + + with open(trans_src_file, 'r') as src_read_trans_f, \ + open(trans_target_file, 'r+', encoding='utf-8') as \ + target_write_trans_f: + + src_trans = json.load(src_read_trans_f) + target_trans = json.load(target_write_trans_f) + target_trans_keys = target_trans.keys() + update_trans = {} + + for trans_string in src_trans.keys(): + if trans_string not in target_trans_keys: + update_trans[trans_string] = src_trans[trans_string] + + if update_trans: + target_trans.update(update_trans) + + target_write_trans_f.seek(0) + target_write_trans_f.truncate(0) + json.dump( + target_trans, target_write_trans_f, + indent=4, ensure_ascii=False, sort_keys=True, + ) + + def __install(self, command, program, package='install'): + """Install/build package""" + + def raise_command_error(stream): + raise CommandError( + f"Error executing command {program} " + f"{package}\n{stream}" + ) + + # Install package + p = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + + if p.returncode != 0: + if stderr: + raise_command_error(stderr.decode('utf-8')) + if stdout: + raise_command_error(stdout.decode('utf-8')) diff --git a/apps/aklub/management/commands/convert_newsletter_image.py b/apps/aklub/management/commands/convert_newsletter_image.py new file mode 100644 index 000000000..e29361eba --- /dev/null +++ b/apps/aklub/management/commands/convert_newsletter_image.py @@ -0,0 +1,113 @@ + +import os +import pathlib +import re + +from PIL import Image + +import cairosvg + +# import cssutils + + +from django.contrib.staticfiles import finders +from django.core.management import BaseCommand # CommandError + + +class Command(BaseCommand): + help = 'Convert automat newsletter background vector (svg) image -> raster (jpg|png)' # noqa + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + # parser.add_argument( + # '--width', + # dest='width', + # type=int, + # help='Image width (px)', + # ) + # parser.add_argument( + # '--height', + # dest='height', + # type=int, + # help='Image height (px)', + # ) + parser.add_argument( + '--dpi', + dest='dpi', + type=int, + default=96, + help='Image dpi', + ) + + parser.add_argument( + '--format', + dest='format', + choices=['jpeg', 'png'], + default='png', + help='Image format (png|jpeg)', + ) + + # def __parse_css_file(self, css): + # result = {} + # sheet = cssutils.parseString(css) + + # for rule in sheet: + # selector = rule.selectorText + # styles = rule.style.cssText + # result[selector] = styles + + # return result + + def handle(self, *args, **options): + # width = options.get('width') + # height = options.get('height') + image_format = options.get('format') + dpi = options.get('dpi') + + _finders = finders.AppDirectoriesFinder() + images_dir = _finders.find_in_app(app='aklub', path='aklub/images') + # css_dir = _finders.find_in_app(app='aklub', path='aklub/css') + # css_file = pathlib.PurePath(css_dir) / 'automat_newsletter.css' + + # if not width and height: + # raise CommandError('set image --width please.') + # if not height and width: + # raise CommandError('set image --height please.') + # if not width and not height: + # raise CommandError('set image --height and --width please.') + # if not width and not height: + # with open(css_file, 'r') as f: + # css = f.read() + # css_rules = self.__parse_css_file(css=css) + # css_image_size = css_rules['.newsletter-section'] + + # width_match = re.search('width: [0-9]*', css_image_size) + # if width_match: + # width = int(width_match.group(0).split(':')[1]) + + # height_match = re.search('height: [0-9]*', css_image_size) + # if height_match: + # height = int(height_match.group(0).split(':')[1]) + + image_name = '^automat_.*._newsletter.svg$' + for image in [i for i in os.listdir(images_dir) if re.match(image_name, i)]: + png_image = image.replace('svg', 'png') + svg_image_path = pathlib.PurePath(images_dir) / image + png_image_path = pathlib.PurePath(images_dir) / png_image + svg_image_path = svg_image_path.as_posix() + png_image_path = png_image_path.as_posix() + cairosvg.svg2png( + url=svg_image_path, + write_to=png_image_path, + # output_width=width, + # output_height=height, + dpi=dpi, + ) + if image_format == 'jpeg': + jpeg_image = image.replace('svg', 'jpeg') + jpeg_image_path = pathlib.PurePath(images_dir) / jpeg_image + jpeg_image_path = jpeg_image_path.as_posix() + im = Image.open(png_image_path) + rgb_im = im.convert('RGB') + rgb_im.save(jpeg_image_path, quality=95) + os.remove(png_image_path) diff --git a/apps/aklub/migrations/0048_auto_20191014_0818.py b/apps/aklub/migrations/0048_auto_20191014_0818.py new file mode 100644 index 000000000..d6d2b7f00 --- /dev/null +++ b/apps/aklub/migrations/0048_auto_20191014_0818.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.6 on 2019-10-14 06:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0047_auto_20190919_1514'), + ] + + operations = [ + migrations.AddField( + model_name='masscommunication', + name='template_en_name', + field=models.CharField(blank=True, max_length=350, null=True, verbose_name='Choose template'), + ), + migrations.AddField( + model_name='masscommunication', + name='template_en_type', + field=models.CharField(blank=True, choices=[('new', 'Create new email body'), ('existed', 'Choose email body template')], default='new', max_length=350, verbose_name='Choose how to create an email body'), + ), + migrations.AddField( + model_name='masscommunication', + name='template_name', + field=models.CharField(blank=True, max_length=350, null=True, verbose_name='Choose template'), + ), + migrations.AddField( + model_name='masscommunication', + name='template_type', + field=models.CharField(blank=True, choices=[('new', 'Create new email body'), ('existed', 'Choose email body template')], default='new', max_length=350, verbose_name='Choose how to create an email body'), + ), + migrations.AlterField( + model_name='accountstatements', + name='type', + field=models.CharField(choices=[('account', 'Account statement - Fio Banka'), ('account_cs', 'Account statement - Česká spořitelna'), ('account_kb', 'Account statement - Komerční Banka'), ('account_csob', 'Account statement - ČSOB'), ('account_sberbank', 'Account statement - Sberbank'), ('darujme', 'Darujme.cz')], max_length=20), + ), + ] diff --git a/apps/aklub/migrations/0049_auto_20191014_1320.py b/apps/aklub/migrations/0049_auto_20191014_1320.py new file mode 100644 index 000000000..44c70e325 --- /dev/null +++ b/apps/aklub/migrations/0049_auto_20191014_1320.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.6 on 2019-10-14 11:20 + +import aklub.autocom +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0048_auto_20191014_0818'), + ] + + operations = [ + migrations.AlterField( + model_name='masscommunication', + name='template', + field=models.TextField(blank=True, help_text='Šablona může obsahovat následující proměnné:
{mr|mrs} or {mr/mrs}, $addressment, $name, $firstname, $surname, $street, $city, $zipcode, $email, $telephone, $regular_amount, $regular_frequency, $var_symbol, $last_payment_amount, $auth_token', max_length=50000, null=True, validators=[aklub.autocom.gendrify_text, django.core.validators.RegexValidator('^([^$]*(\\$(addressment|name|firstname|surname|street|city|zipcode|email|telephone|regular_amount|regular_frequency|var_symbol|last_payment_amount|auth_token)\\b)?)*$', 'Unknown variable')], verbose_name='Template'), + ), + ] diff --git a/apps/aklub/migrations/0050_merge_20191024_0859.py b/apps/aklub/migrations/0050_merge_20191024_0859.py new file mode 100644 index 000000000..b7e18b835 --- /dev/null +++ b/apps/aklub/migrations/0050_merge_20191024_0859.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.6 on 2019-10-24 06:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0049_auto_20191014_1320'), + ('aklub', '0049_auto_20191022_1547'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0056_merge_20191117_0649.py b/apps/aklub/migrations/0056_merge_20191117_0649.py new file mode 100644 index 000000000..3d9a1ef67 --- /dev/null +++ b/apps/aklub/migrations/0056_merge_20191117_0649.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.7 on 2019-11-17 05:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0050_merge_20191024_0859'), + ('aklub', '0055_auto_20191114_1203'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0057_merge_20191120_1732.py b/apps/aklub/migrations/0057_merge_20191120_1732.py new file mode 100644 index 000000000..fc3c623b4 --- /dev/null +++ b/apps/aklub/migrations/0057_merge_20191120_1732.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.8.dev20191120135045 on 2019-11-20 16:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0056_merge_20191117_0649'), + ('aklub', '0056_auto_20191119_1234'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0063_merge_20200114_2108.py b/apps/aklub/migrations/0063_merge_20200114_2108.py new file mode 100644 index 000000000..7c31c8256 --- /dev/null +++ b/apps/aklub/migrations/0063_merge_20200114_2108.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.8.dev20191120135045 on 2020-01-14 20:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0057_merge_20191120_1732'), + ('aklub', '0062_auto_20200109_09110'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0064_merge_20200115_1345.py b/apps/aklub/migrations/0064_merge_20200115_1345.py new file mode 100644 index 000000000..b2827ae1e --- /dev/null +++ b/apps/aklub/migrations/0064_merge_20200115_1345.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.9 on 2020-01-15 12:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0063_auto_20200114_1732'), + ('aklub', '0063_merge_20200114_2108'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0067_merge_20200203_0833.py b/apps/aklub/migrations/0067_merge_20200203_0833.py new file mode 100644 index 000000000..82644671d --- /dev/null +++ b/apps/aklub/migrations/0067_merge_20200203_0833.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.9 on 2020-02-03 07:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0066_auto_20200128_1502'), + ('aklub', '0064_merge_20200115_1345'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0069_merge_20200406_0617.py b/apps/aklub/migrations/0069_merge_20200406_0617.py new file mode 100644 index 000000000..f4880f232 --- /dev/null +++ b/apps/aklub/migrations/0069_merge_20200406_0617.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.10 on 2020-04-06 04:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0067_merge_20200203_0833'), + ('aklub', '0068_auto_20200319_1414'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0074_merge_20200414_0547.py b/apps/aklub/migrations/0074_merge_20200414_0547.py new file mode 100644 index 000000000..3f867e2a9 --- /dev/null +++ b/apps/aklub/migrations/0074_merge_20200414_0547.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.10 on 2020-04-14 03:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0073_auto_20200206_1440'), + ('aklub', '0069_merge_20200406_0617'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0075_merge_20200421_0651.py b/apps/aklub/migrations/0075_merge_20200421_0651.py new file mode 100644 index 000000000..30bf1d892 --- /dev/null +++ b/apps/aklub/migrations/0075_merge_20200421_0651.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-04-21 04:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0074_auto_20200417_1112'), + ('aklub', '0074_merge_20200414_0547'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0076_merge_20200511_0622.py b/apps/aklub/migrations/0076_merge_20200511_0622.py new file mode 100644 index 000000000..4b2d5c9c3 --- /dev/null +++ b/apps/aklub/migrations/0076_merge_20200511_0622.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-05-11 04:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0075_delete_useryearpayments'), + ('aklub', '0075_merge_20200421_0651'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0079_merge_20200514_0604.py b/apps/aklub/migrations/0079_merge_20200514_0604.py new file mode 100644 index 000000000..5d528aa40 --- /dev/null +++ b/apps/aklub/migrations/0079_merge_20200514_0604.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-05-14 04:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0078_remove_taxconfirmation_administrative_unit'), + ('aklub', '0076_merge_20200511_0622'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0080_merge_20200701_1411.py b/apps/aklub/migrations/0080_merge_20200701_1411.py new file mode 100644 index 000000000..960f5fe30 --- /dev/null +++ b/apps/aklub/migrations/0080_merge_20200701_1411.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-07-01 12:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0079_merge_20200514_0604'), + ('aklub', '0079_accountstatements_pair_log'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0081_merge_20200706_0437.py b/apps/aklub/migrations/0081_merge_20200706_0437.py new file mode 100644 index 000000000..98c47ae98 --- /dev/null +++ b/apps/aklub/migrations/0081_merge_20200706_0437.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-07-06 02:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0080_merge_20200701_1411'), + ('aklub', '0080_auto_20200625_1216'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0088_merge_20201010_1407.py b/apps/aklub/migrations/0088_merge_20201010_1407.py new file mode 100644 index 000000000..5b1937c77 --- /dev/null +++ b/apps/aklub/migrations/0088_merge_20201010_1407.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.16 on 2020-10-10 12:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0081_merge_20200706_0437'), + ('aklub', '0087_auto_20200918_1159'), + ] + + operations = [ + ] diff --git a/apps/aklub/models.py b/apps/aklub/models.py index cd7e5122c..44e494026 100644 --- a/apps/aklub/models.py +++ b/apps/aklub/models.py @@ -2644,6 +2644,11 @@ class Meta: verbose_name = _("Mass Communication") verbose_name_plural = _("Mass Communications") + EMAIL_BODY_CREATION_CHOICES = ( + ("new", _("Create new email body")), + ('existed', _("Choose email body template")), + ) + name = models.CharField( verbose_name=_("Name"), max_length=50, @@ -2680,15 +2685,43 @@ class Meta: null=True, validators=[gender_strings_validator, variable_validator], ) + template_type = models.CharField( + verbose_name=_("Choose how to create an email body"), + max_length=350, + null=False, + blank=True, + default="new", + choices=EMAIL_BODY_CREATION_CHOICES, + ) + template_name = models.CharField( + verbose_name=_("Choose template"), + max_length=350, + null=True, + blank=True, + ) template = models.TextField( verbose_name=_("Template"), help_text=_("Template can contain following variable substitutions:
") + ("{mr|mrs} or {mr/mrs}, $" + ", $" .join(autocom.KNOWN_VARIABLES)), max_length=50000, - blank=False, + blank=True, null=True, validators=[gender_strings_validator, variable_validator], ) + template_en_type = models.CharField( + verbose_name=_("Choose how to create an email body"), + max_length=350, + null=False, + blank=True, + default="new", + choices=EMAIL_BODY_CREATION_CHOICES, + ) + template_en_name = models.CharField( + verbose_name=_("Choose template"), + max_length=350, + null=True, + blank=True, + ) template_en = models.TextField( verbose_name=_("English template"), max_length=50000, diff --git a/apps/aklub/static/aklub/css/base_template.css b/apps/aklub/static/aklub/css/base_template.css new file mode 100644 index 000000000..498197ecb --- /dev/null +++ b/apps/aklub/static/aklub/css/base_template.css @@ -0,0 +1,13 @@ +.content-table { + background-repeat: no-repeat; + background-size: contain; +} + +.content-table section { + width: 100%; + height: 100%; +} + +.content-table section .top-vertical-space { + margin-top: 0; +} diff --git a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-append.scss b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-append.scss new file mode 100644 index 000000000..a980e46a3 --- /dev/null +++ b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-append.scss @@ -0,0 +1,51 @@ +.text-justify { + text-align: justify; +} + +.article { + &__content { + p { + padding: 0px 35px; + } + } + &__automat-poppins-light-font { + font-family: AutoMat-Poppins-Light; + } + &__automat-poppins-thin-font { + font-family: AutoMat-Poppins-Thin; + } + &__automat-poppins-extralight-font { + font-family: AutoMat-Poppins-ExtraLight; + } + &__automat-poppins-bold-font { + font-family: AutoMat-Poppins-Bold; + } + &__automat-poppins-regular-font { + font-family: AutoMat-Poppins-Regular; + } + &__automat-formulacondesed-font { + font-family: AutoMat-FormulaCondensed-AutoMat; + } + &__automat-poppins-semibold-font { + font-family: AutoMat-Poppins-SemiBold; + } + &__automat-poppins-medium-font { + font-family: AutoMat-Poppins-Medium; + } +} + +.article { + &__footer { + &__info { + font-size: 10px; + border-spacing: 5px; + border-collapse: separate; + & a:link { + text-decoration: none; + color: white; + background-color: red; + font-weight: bolder; + } + } + } +} diff --git a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss new file mode 100644 index 000000000..eaa86ce13 --- /dev/null +++ b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss @@ -0,0 +1 @@ +$max-width: 600px; diff --git a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss new file mode 100644 index 000000000..d84ec67b7 --- /dev/null +++ b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss @@ -0,0 +1,21 @@ +/** + * The custom toolbox widget displays a set of tools the user can use to edit the + * content of the page. + */ + +.ct-widget { + .ct-tool { + &--button:before { content: "\ea1e"; } + &--align-justify:before { content: "\ea7a"; } + &--background-image:before { content: "\e90e"; } + &--vertical-space:before { content: "\ea68"; } + &--font-size:before { content: "\ea5f"; } + &--increase-font-size:before { content: "\ea0a"; } + &--decrease-font-size:before { content: "\ea0b"; } + &--line-spacing:before { content: "\ea72"; } + } +} +// Color picker dialog +.cp-app { + z-index: 10010; +} diff --git a/apps/aklub/static/aklub/css/template.css b/apps/aklub/static/aklub/css/template.css new file mode 100644 index 000000000..52417be21 --- /dev/null +++ b/apps/aklub/static/aklub/css/template.css @@ -0,0 +1,4 @@ +.template { + float: left; +} + diff --git a/apps/aklub/static/aklub/css/template_name_dialog.css b/apps/aklub/static/aklub/css/template_name_dialog.css new file mode 100644 index 000000000..e26291ed7 --- /dev/null +++ b/apps/aklub/static/aklub/css/template_name_dialog.css @@ -0,0 +1,3 @@ +#template_name_form { + padding: 10px; +} diff --git a/apps/aklub/static/aklub/fonts/automat/FormulaCondensed-Automat.otf b/apps/aklub/static/aklub/fonts/automat/FormulaCondensed-Automat.otf new file mode 100644 index 000000000..5f814fa42 Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/FormulaCondensed-Automat.otf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-Bold.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-Bold.ttf new file mode 100644 index 000000000..d1c3f992c Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-Bold.ttf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-ExtraLight.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-ExtraLight.ttf new file mode 100644 index 000000000..20fe902c4 Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-ExtraLight.ttf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-Light.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-Light.ttf new file mode 100644 index 000000000..1bbe7d898 Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-Light.ttf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-Medium.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-Medium.ttf new file mode 100644 index 000000000..b3640d5e8 Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-Medium.ttf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-Regular.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-Regular.ttf new file mode 100644 index 000000000..3461a5bec Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-Regular.ttf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-SemiBold.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-SemiBold.ttf new file mode 100644 index 000000000..06b594bd5 Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-SemiBold.ttf differ diff --git a/apps/aklub/static/aklub/fonts/automat/Poppins-Thin.ttf b/apps/aklub/static/aklub/fonts/automat/Poppins-Thin.ttf new file mode 100644 index 000000000..20a6950e7 Binary files /dev/null and b/apps/aklub/static/aklub/fonts/automat/Poppins-Thin.ttf differ diff --git a/apps/aklub/static/aklub/fonts/contenttools/icons.woff b/apps/aklub/static/aklub/fonts/contenttools/icons.woff new file mode 100644 index 000000000..762aeb1ea Binary files /dev/null and b/apps/aklub/static/aklub/fonts/contenttools/icons.woff differ diff --git a/apps/aklub/static/aklub/images/automat_green_newsletter.jpeg b/apps/aklub/static/aklub/images/automat_green_newsletter.jpeg new file mode 100644 index 000000000..78bd7ad35 Binary files /dev/null and b/apps/aklub/static/aklub/images/automat_green_newsletter.jpeg differ diff --git a/apps/aklub/static/aklub/images/automat_green_newsletter.svg b/apps/aklub/static/aklub/images/automat_green_newsletter.svg new file mode 100644 index 000000000..5644f8f5f --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_green_newsletter.svg @@ -0,0 +1,406 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_green_newsletter_orig_size.svg b/apps/aklub/static/aklub/images/automat_green_newsletter_orig_size.svg new file mode 100644 index 000000000..2df8ec4e6 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_green_newsletter_orig_size.svg @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_purple_newsletter.jpeg b/apps/aklub/static/aklub/images/automat_purple_newsletter.jpeg new file mode 100644 index 000000000..ca3e1d0a3 Binary files /dev/null and b/apps/aklub/static/aklub/images/automat_purple_newsletter.jpeg differ diff --git a/apps/aklub/static/aklub/images/automat_purple_newsletter.svg b/apps/aklub/static/aklub/images/automat_purple_newsletter.svg new file mode 100644 index 000000000..8cf0cf406 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_purple_newsletter.svg @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_purple_newsletter_orig_size.svg b/apps/aklub/static/aklub/images/automat_purple_newsletter_orig_size.svg new file mode 100644 index 000000000..d3c92b306 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_purple_newsletter_orig_size.svg @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_red_newsletter.jpeg b/apps/aklub/static/aklub/images/automat_red_newsletter.jpeg new file mode 100644 index 000000000..38e5c5bf6 Binary files /dev/null and b/apps/aklub/static/aklub/images/automat_red_newsletter.jpeg differ diff --git a/apps/aklub/static/aklub/images/automat_red_newsletter.svg b/apps/aklub/static/aklub/images/automat_red_newsletter.svg new file mode 100644 index 000000000..7d385d78b --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_red_newsletter.svg @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_red_newsletter_orig_size.svg b/apps/aklub/static/aklub/images/automat_red_newsletter_orig_size.svg new file mode 100644 index 000000000..cec23aac7 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_red_newsletter_orig_size.svg @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/newsletter_template_sample_pic.png b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.png new file mode 100644 index 000000000..c740dd1c1 Binary files /dev/null and b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.png differ diff --git a/apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg new file mode 100644 index 000000000..8591953db --- /dev/null +++ b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg @@ -0,0 +1,641 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee new file mode 100644 index 000000000..ff2677a91 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee @@ -0,0 +1,216 @@ +module.exports = (grunt) -> + + require('es6-promise').polyfill() + + # Project configuration + grunt.initConfig({ + + pkg: grunt.file.readJSON('package.json') + + coffee: + options: + join: true + + build: + # options: + # sourceMap: true + files: + 'src/tmp/content-tools.js': [ + 'src/scripts/namespace.coffee', + + # UI + 'src/scripts/ui/ui.coffee' + 'src/scripts/ui/events.coffee' + 'src/scripts/ui/flashes.coffee' + 'src/scripts/ui/ignition.coffee' + 'src/scripts/ui/inspector.coffee' + 'src/scripts/ui/modal.coffee' + 'src/scripts/ui/toolbox.coffee' + + # UI - Dialogs + 'src/scripts/ui/dialogs/dialogs.coffee' + 'src/scripts/ui/dialogs/image.coffee' + 'src/scripts/ui/dialogs/link.coffee' + 'src/scripts/ui/dialogs/properties.coffee' + 'src/scripts/ui/dialogs/table.coffee' + 'src/scripts/ui/dialogs/video.coffee' + + # Other + 'src/scripts/clean-html.coffee' + 'src/scripts/editor.coffee' + 'src/scripts/history.coffee' + 'src/scripts/styles.coffee' + 'src/scripts/tools.coffee' + + # Custom tools + 'src/scripts/superclass.coffee' + 'src/scripts/custom-align-left-tool.coffee' + 'src/scripts/custom-align-right-tool.coffee' + 'src/scripts/custom-align-center-tool.coffee' + 'src/scripts/align-justify-tool.coffee' + 'src/scripts/button-tool.coffee' + 'src/scripts/background-image-tool.coffee' + 'src/scripts/vertical-space-tool.coffee' + 'src/scripts/font-size-tool.coffee' + 'src/scripts/increase-font-size-tool.coffee' + 'src/scripts/decrease-font-size-tool.coffee' + 'src/scripts/line-spacing-tool.coffee' + 'src/scripts/custom-editor.coffee' + ] + + sandbox: + files: + 'sandbox/sandbox.js': [ + 'src/sandbox/image-uploader.coffee' + 'src/sandbox/sandbox.coffee' + ] + + spec: + files: + 'spec/spec-helper.js': 'src/spec/spec-helper.coffee' + 'spec/content-tools-spec.js': [ + 'src/spec/namespace.coffee' + + # UI + 'src/spec/ui/ui.coffee' + 'src/spec/ui/events.coffee' + 'src/spec/ui/flashes.coffee' + 'src/spec/ui/ignition.coffee' + 'src/spec/ui/inspector.coffee' + 'src/spec/ui/modal.coffee' + 'src/spec/ui/toolbox.coffee' + + # UI - Dialogs + 'src/spec/ui/dialogs/dialogs.coffee' + 'src/spec/ui/dialogs/image.coffee' + 'src/spec/ui/dialogs/link.coffee' + 'src/spec/ui/dialogs/properties.coffee' + 'src/spec/ui/dialogs/table.coffee' + 'src/spec/ui/dialogs/video.coffee' + + # Other + 'src/spec/editor.coffee' + 'src/spec/history.coffee' + 'src/spec/tools.coffee' + 'src/spec/styles.coffee' + ] + + sass: + options: + banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */' + sourceMap: false + + build: + files: + 'build/content-tools.min.css': + 'src/styles/content-tools.scss' + + sandbox: + files: + 'sandbox/sandbox.css': 'sandbox/sandbox-concat.scss' + + cssnano: + options: + sourcemap: false + + build: + files: + 'build/content-tools.min.css': + 'build/content-tools.min.css' + + sandbox: + files: + 'sandbox/sandbox.min.css': + 'sandbox/sandbox.css' + + uglify: + options: + banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */\n' + mangle: true + # sourceMap: true + # sourceMapIn: './src/tmp/content-tools.js.map' + + build: + src: 'build/content-tools.js' + dest: 'build/content-tools.min.js' + + concat: + build: + src: [ + 'external/scripts/content-edit.js' + 'src/tmp/content-tools.js' + ] + dest: 'build/content-tools.js' + + sandbox: + src: [ + 'src/sandbox/sandbox.scss' + 'src/sandbox/sandbox-append.scss' + ] + dest: 'sandbox/sandbox-concat.scss' + + + clean: + build: ['src/tmp'] + + jasmine: + build: + src: ['build/content-tools.js'] + options: + specs: 'spec/content-tools-spec.js' + helpers: 'spec/spec-helper.js' + + watch: + build: + files: [ + 'src/scripts/**/*.coffee', + 'src/styles/**/*.scss' + ] + tasks: ['build'] + + sandbox: + files: [ + 'src/sandbox/*.coffee', + 'src/sandbox/*.scss' + ] + tasks: ['sandbox'] + + spec: + files: ['src/spec/**/*.coffee'] + tasks: ['spec'] + }) + + # Plug-ins + grunt.loadNpmTasks 'grunt-contrib-clean' + grunt.loadNpmTasks 'grunt-contrib-coffee' + grunt.loadNpmTasks 'grunt-contrib-concat' + grunt.loadNpmTasks 'grunt-contrib-jasmine' + grunt.loadNpmTasks 'grunt-contrib-sass' + grunt.loadNpmTasks 'grunt-contrib-uglify' + grunt.loadNpmTasks 'grunt-contrib-watch' + grunt.loadNpmTasks 'grunt-cssnano' + + # Tasks + grunt.registerTask 'build', [ + 'coffee:build' + 'sass:build' + 'cssnano:build' + 'concat:build' + 'uglify:build' + 'clean:build' + ] + + grunt.registerTask 'sandbox', [ + 'coffee:sandbox' + 'concat:sandbox' + 'sass:sandbox' + 'cssnano:sandbox' + ] + + grunt.registerTask 'spec', [ + 'coffee:spec' + ] + + grunt.registerTask 'watch-build', ['watch:build'] + grunt.registerTask 'watch-sandbox', ['watch:sandbox'] + grunt.registerTask 'watch-spec', ['watch:spec'] diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/align-justify-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/align-justify-tool.coffee new file mode 100644 index 000000000..d67af7430 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/align-justify-tool.coffee @@ -0,0 +1,14 @@ +class AlignJustifyTool extends CustomAlignLeftTool + + # Apply a class to justify align the contents of the current text block. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'align-justify') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Align justify' + @icon = 'align-justify' + @className = 'text-justify' + + +ContentTools.DEFAULT_TOOLS[0].push('align-justify') \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee new file mode 100644 index 000000000..20dab3971 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee @@ -0,0 +1,288 @@ +class BackgroundImageDialog extends SuperClass + + @include ContentTools.DialogUI + @include ContentTools.ImageDialog + + constructor: () -> + ContentTools.ImageDialog.call @ + + mount: () -> + ContentTools.DialogUI::mount.call @ + + # Update dialog class + ContentEdit.addCSSClass(@_domElement, 'ct-image-dialog') + ContentEdit.addCSSClass(@_domElement, 'ct-image-dialog--empty') + + # Update view class + ContentEdit.addCSSClass(@_domView, 'ct-image-dialog__view') + + # Add controls + + # Image tools & progress bar + domTools = @constructor.createDiv( + ['ct-control-group', 'ct-control-group--left']) + @_domControls.appendChild(domTools) + + # Rotate CCW + @_domRotateCCW = @constructor.createDiv([ + 'ct-control', + 'ct-control--icon', + 'ct-control--rotate-ccw' + ]) + @_domRotateCCW.setAttribute( + 'data-ct-tooltip', + ContentEdit._('Rotate') + ' -90°' + ) + domTools.appendChild(@_domRotateCCW) + + # Rotate CW + @_domRotateCW = @constructor.createDiv([ + 'ct-control', + 'ct-control--icon', + 'ct-control--rotate-cw' + ]) + @_domRotateCW.setAttribute( + 'data-ct-tooltip', + ContentEdit._('Rotate') + ' 90°' + ) + domTools.appendChild(@_domRotateCW) + + # Rotate CW + @_domCrop = @constructor.createDiv([ + 'ct-control', + 'ct-control--icon', + 'ct-control--crop' + ]) + @_domCrop.setAttribute('data-ct-tooltip', ContentEdit._('Crop marks')) + domTools.appendChild(@_domCrop) + + # Progress bar + domProgressBar = @constructor.createDiv(['ct-progress-bar']) + domTools.appendChild(domProgressBar) + + @_domProgress = @constructor.createDiv(['ct-progress-bar__progress']) + domProgressBar.appendChild(@_domProgress) + + # Actions + domActions = @constructor.createDiv( + ['ct-control-group', 'ct-control-group--right']) + @_domControls.appendChild(domActions) + + # Upload button + @_domUpload = @constructor.createDiv([ + 'ct-control', + 'ct-control--text', + 'ct-control--upload' + ]) + @_domUpload.textContent = ContentEdit._('Upload') + domActions.appendChild(@_domUpload) + + # File input for upload + @_domInput = document.createElement('input') + @_domInput.setAttribute('class', 'ct-image-dialog__file-upload') + @_domInput.setAttribute('name', 'file') + @_domInput.setAttribute('type', 'file') + @_domInput.setAttribute('accept', 'image/*') + @_domUpload.appendChild(@_domInput) + + # Insert + @_domInsert = @constructor.createDiv([ + 'ct-control', + 'ct-control--text', + 'ct-control--insert' + ]) + @_domInsert.textContent = ContentEdit._('Insert') + domActions.appendChild(@_domInsert) + + # Cancel + @_domCancelUpload = @constructor.createDiv([ + 'ct-control', + 'ct-control--text', + 'ct-control--cancel' + ]) + @_domCancelUpload.textContent = ContentEdit._('Cancel') + domActions.appendChild(@_domCancelUpload) + + # Clear + @_domClear = @constructor.createDiv([ + 'ct-control', + 'ct-control--text', + 'ct-control--clear' + ]) + @_domClear.textContent = ContentEdit._('Clear') + domActions.appendChild(@_domClear) + + # Add interaction handlers + @_addDOMEventListeners() + + @dispatchEvent(@createEvent('imageuploader.mount')) + + save: (imageURL, imageSize, imageAttrs) -> + # Save and insert the current image + # Set correct container height according uploaded image + document.getElementsByClassName('content-table')[0].setAttribute( + 'height', "#{ imageSize[1] }" + ) + @dispatchEvent( + @createEvent( + 'save', + { + 'imageURL': imageURL, + 'imageSize': imageSize, + 'imageAttrs': imageAttrs + }) + ) + + _addDOMEventListeners: () -> + # Add event listeners for the widget + + # Call add base class event listener + ContentTools.DialogUI::_addDOMEventListeners.call @ + + # File ready for upload + @_domInput.addEventListener 'change', (ev) => + + # Get the file uploaded + file = ev.target.files[0] + + # Ignore empty file changes (this may occur when we change the + # value of the input field to '', see issue: + # https://github.com/GetmeUK/ContentTools/issues/385 + unless file + return + + # Clear the file inputs value so that the same file can be uploaded + # again if the user cancels the upload or clears it and starts then + # changes their mind. + ev.target.value = '' + if ev.target.value + # Hack for clearing the file field value in IE + ev.target.type = 'text' + ev.target.type = 'file' + + ### + # Set backgroundImage property + ### + @dispatchEvent( + @createEvent( + 'imageuploader.fileready', + {file: file, backgroundImage: true} + ) + ) + + # Cancel upload + @_domCancelUpload.addEventListener 'click', (ev) => + @dispatchEvent(@createEvent('imageuploader.cancelupload')) + + # Clear image + @_domClear.addEventListener 'click', (ev) => + @removeCropMarks() + @dispatchEvent(@createEvent('imageuploader.clear')) + + # Rotate the image + @_domRotateCCW.addEventListener 'click', (ev) => + @removeCropMarks() + @dispatchEvent(@createEvent('imageuploader.rotateccw')) + + @_domRotateCW.addEventListener 'click', (ev) => + @removeCropMarks() + @dispatchEvent(@createEvent('imageuploader.rotatecw')) + + @_domCrop.addEventListener 'click', (ev) => + if @_cropMarks + @removeCropMarks() + + else + @addCropMarks() + + @_domInsert.addEventListener 'click', (ev) => + @_setEmailTemplateBgImage('content-table') + @dispatchEvent(@createEvent('imageuploader.save')) + + _setEmailTemplateBgImage: (containerClassName) -> + ### + # Set html email template container background image + ### + container = document.getElementsByClassName(containerClassName)[0] + container.style.backgroundImage = "url(#{ @_imageURL })" + + +class BackgroundImage extends ContentTools.Tools.Image + + # Insert an image. + + ContentTools.ToolShelf.stow(@, 'background-image') + + @label = 'Background image' + @icon = 'background-image' + + @canApply: (element, selection) -> + # Return true if the tool can be applied to the current + # element/selection. + if element.isFixed() + unless element.type() is 'ImageFixture' + return false + return true + + @apply: (element, selection, callback) -> + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + # If supported allow store the state for restoring once the dialog is + # cancelled. + if element.storeState + element.storeState() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + dialog = new BackgroundImageDialog() + + # Support cancelling the dialog + dialog.addEventListener 'cancel', () -> + + modal.hide() + dialog.hide() + + if element.restoreState + element.restoreState() + + callback(false) + + # Support saving the dialog + dialog.addEventListener 'save', (ev) => + detail = ev.detail() + imageURL = detail.imageURL + + if element.type() is 'ImageFixture' + # Configure the image source against the fixture + element.src(imageURL) + + modal.hide() + dialog.hide() + + callback(true) + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + # Show the dialog + app.attach(modal) + app.attach(dialog) + modal.show() + dialog.show() + + +index = ContentTools.DEFAULT_TOOLS[2].indexOf('image') + +ContentTools.DEFAULT_TOOLS[2].splice(index + 1, 0, 'background-image') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee new file mode 100644 index 000000000..9d14dfb41 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee @@ -0,0 +1,563 @@ +class ButtonTool extends ContentTools.Tools.Bold + # Insert/Remove a tag. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'button') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Button' + @icon = 'button' + + # The Bold provides a tagName attribute we can override to make inheriting + # from the class cleaner. + @tagName = 'button' + @wrapperParentTagName = 'a' + + @_btnCSSProp = {} + + @_btnHrefParentAttr = {target: '_blank'} + @_btnHrefParentAttrDef = @_btnHrefParentAttr + + @_btnAttr = {onclick: 'btnClickListener(this)'} + @_btnAttrDef = @_btnAttr + + @_openDialogDelay = 1000 + + @apply: (element, selection, callback, btnHrefParentAttr, btnCSSStyle, + btnCreate = true) -> + # Apply the tool to the specified element and selection + + # Store the selection state of the element so we can restore it once done + element.storeState() + + # Add a fake selection wrapper to the selected text so that it appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + # Add btn position + @_btnAttr.position = "#{ from },#{ to }" + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Add an invisible modal that we'll use to detect if the user clicks away + # from the dialog to close it. + modal = new ContentTools.ModalUI transparent=true, allowScrolling=true + + # modal.addEventListener 'click', () -> + # Close the dialog + # @unmount() + # dialog.hide() + + # Remove the fake selection from the element + # element.content = element.content.unformat(from, to, selectTag) + # element.updateInnerHTML() + + # Restore the real selection + # element.restoreState() + + # Trigger the callback + # callback(false) + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + # Create background color dialog + bgColorDialog = new GetBgColorDialog @getBgColor btnCSSStyle, \ + element, selection + bgColorDialog.position @getDialogPosition rect + + # Create text color dialog + textColorDialog = new GetTextColorDialog @getTextColor btnCSSStyle, \ + element, selection + textColorDialog.position @getDialogPosition rect + + # Create btn url link dialog + hrefAttr = @getHrefAttr btnHrefParentAttr, element, selection + targetAttr = @getTargetAttr btnHrefParentAttr, element, selection + btnLinkDialog = new ContentTools.LinkDialog hrefAttr, targetAttr + btnLinkDialog.position @getDialogPosition rect + + # Create padding dialog + paddingDialog = new GetPaddindDialog @parsePaddingToInput \ + @getPadding btnCSSStyle, element, selection + paddingDialog.position @getDialogPosition rect + + # Listen for save events against the dialog + paddingDialog.addEventListener 'save', (ev) => + + # Get link value + padding = ev.detail().padding + + if padding + cssProp = {'padding': @parsePaddingToCSS padding} + @_btnCSSProp = @extendObj false, cssProp, @_btnCSSProp + + # Hide dialog + paddingDialog.hide() + + # Show get btn link dialog + @delay @_openDialogDelay, => @showDialog btnLinkDialog, false + + # Trigger the callback + callback(true) + + # Listen for save events against the dialog + btnLinkDialog.addEventListener 'save', (ev) => + + # Get link value + link = ev.detail().href + + if link + @_btnHrefParentAttr = @extendObj false, {'href': link}, \ + @_btnHrefParentAttr + + # Insert btn element + applyBtn() + + # Trigger the callback + callback(true) + + # Listen for save events against the dialog + textColorDialog.addEventListener 'save', (ev) => + + # Get color value + color = ev.detail().color + + if color + @_btnCSSProp = @extendObj false, {'color': color}, @_btnCSSProp + + # Hide dialog + textColorDialog.hide() + + # Show get btn link dialog + @delay @_openDialogDelay, => @showDialog paddingDialog, false + + # Trigger the callback + callback(true) + + # Listen for save events against the dialog + bgColorDialog.addEventListener 'save', (ev) => + + # Get color value + color = ev.detail().color + + if color + @_btnCSSProp = @extendObj false, {'background-color': color}, \ + @_btnCSSProp + + # Hide dialog + bgColorDialog.hide() + + # Show get text color dialog + @delay @_openDialogDelay, => @showDialog textColorDialog + + # Trigger the callback + callback(true) + + applyBtn = () => + # Clear any existing link + element.content = element.content.unformat from, to, @tagName + element.content = element.content.unformat from, to, @wrapperParentTagName + + if Object.keys(@_btnCSSProp).length > 0 + + # Btn href parent elelement + btnHrefParent = new HTMLString.Tag @wrapperParentTagName, \ + @_btnHrefParentAttr + element.content = element.content.format from, to, btnHrefParent + + # Btn element + btn = new HTMLString.Tag @tagName, @extendObj(false, \ + @_btnAttr, @getCCCStr(@_btnCSSProp)) + element.content = element.content.format from, to, btn + + element.updateInnerHTML() + element.taint() + + # Close the modal and dialog + modal.unmount() + btnLinkDialog.hide() + + # Remove the fake selection from the element + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Restore the real selection + element.restoreState() + + # Reset btn prop + @_btnCSSProp = {} + @_btnAttr = {} + @_btnHrefParentAttr = @_btnHrefParentAttrDef + @_btnAttr = @_btnAttrDef + + # Add btn listener + # @addBtnListener(element) + + selection = window.getSelection() + selection.removeAllRanges() + + app.attach modal + app.attach bgColorDialog + app.attach textColorDialog + app.attach paddingDialog + app.attach btnLinkDialog + + modal.show() + bgColorDialog.show() + + # Initialize color picker + @initColorPicker 'input.color' + + window.btnClickListener = (evt) => + # Click on btn element + + editor = ContentTools.EditorApp.get() + + # Prevent deafult if editor is not in editing mode + if editor.isEditing() is true + # Get evt target + btn = evt + btnText = btn.innerText + + # Create btn text node + btnToText = document.createTextNode btnText + + # Replace btn node with text + btnParent = btn.parentElement + btnHrefParent = btnParent.parentElement + btnParentEditableContainer = document.getElementsByClassName( + 'ce-element--focused' + )[0] + + # Get position + [from, to] = btn.getAttribute('position').split ',' + + # Get btn css prop + btnCSSStyle = @getBtnCSSProp btn + + # Get btn attr + link = btnParent.getAttribute 'href' + btnHrefParentAttr = @extendObj false, {'href': link}, \ + @_btnHrefParentAttrDef + + # Replace btn with btn text node + btnHrefParent.replaceChild btnToText, btnParent + + # Create selection range + range = new ContentSelect.Range parseInt(from), parseInt(to) + range.select btnParentEditableContainer + + # Get selected text element + element = ContentEdit.Root.get().focused() + + selection = null + if element and element.selection + selection = element.selection() + [from, to] = selection.get() + + callback = () -> + + # Apply tool + @apply element, selection, callback, btnHrefParentAttr, \ + btnCSSStyle, false + + # @addBtnListener: (element) -> + # btns = element.domElement().getElementsByTagName('button') + + # for btn in btns + # btn.addEventListener 'click', @btnClickListener + + @getBgColor: (btnCSSStyle, element, selection, + cssPropName = 'background-color') -> + # Get CSS bg color prop value + if btnCSSStyle? + btnCSSStyle[cssPropName] + else @getCSSStyle(cssPropName, element, selection, @tagName) + + @getTextColor: (btnCSSStyle, element, selection, cssPropName = 'color') -> + # Get CSS color prop value + if btnCSSStyle? + btnCSSStyle[cssPropName] + else @getCSSStyle(cssPropName, element, selection, @tagName) + + @getHrefAttr: (tnHrefParentAttr, element, selection, attrName = 'href') -> + # Get href attr value + if btnHrefParentAttr? + btnHrefParentAttr[attrName] + else @getAttr(attrName, element, selection, 'a') + + @getTargetAttr: (btnHrefParentAttr, element, selection, + attrName = 'target') -> + # Get target attr value + if btnHrefParentAttr? + btnHrefParentAttr[attrName] + else @getAttr(attrName, element, selection, 'a') + + @getPadding: (btnCSSStyle, element, selection, cssPropName = 'padding') -> + # Get CSS padding prop value + if btnCSSStyle? + btnCSSStyle[cssPropName] + else @getCSSStyle(cssPropName, element, selection, @tagName) + + @getCCCStr: (cssProp) -> + prop = [] + + for k, v of cssProp + prop.push "#{ k }:#{ v }" + + {'style': prop.join ';'} + + @getBtnCSSProp: (element) -> + # Get btn css prop + cssStyle = + 'color': @getColorCSSProp 'color', element + 'background-color': @getColorCSSProp 'background-color', element + 'padding': @getPaddingCSSProp @getCSSProp 'padding', element + + cssStyle + + @getColorCSSProp: (propName, element) -> + # Convert rgb(255, 255, 255) to the #ffffff format + regExp = /\(([^)]+)\)/ + color = element.style[propName] + + if color.length > 0 + [r, g, b] = regExp.exec(color)[1].split ',' + return @rgbToHex parseInt(r), parseInt(g), parseInt(b) + + return '' + + @getCSSProp: (propName, element) -> + element.style[propName] + + @getPaddingCSSProp: (prop) -> + # If padding is same in every direction + # set two same padding value in return string + propSplit = prop.split ' ' + return if propSplit.length > 1 then prop else [prop, prop].join ' ' + + @parsePaddingToCSS: (value) -> + # Append padding unit + paddingUnit = [] + + padding = value.replace(/\s/g, '').split ',' + addUnit = (value, unit='px') -> paddingUnit.push "#{ value }#{ unit }" + addUnit value for value in padding + + paddingUnit.join(' ') + + @parsePaddingToInput: (value) -> + # Remove padding unit + padding = value.replace(/px/g, '').split ' ' + padding.join ', ' + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @delay = (ms, func) -> setTimeout func, ms + + @showDialog: (dialog, colorPicker = true) -> + # Show dialog + dialog.show() + + # Initialize color picker + if colorPicker + @initColorPicker 'input.color' + + @initColorPicker: (selector) -> + # Initialize color picker + initial = (elm, colors) -> + elm.style.backgroundColor = elm.value + elm.style.color = if colors.rgbaMixBG.luminance > 0.22 \ + then '#222' else '#ddd' + + jsColorPicker(selector, { + memoryColors: @getAutomatColors(), + init: initial + }) + + @getAutomatColors: () -> + # Default Automat colors + red = {r: 233, g: 76, b: 77, a: 1.0} + green = {r: 174, g: 216, b: 198, a: 1.0} + purple = {r: 69, g: 56, b: 142, a: 1.0} + [red, purple, green] + + @rgbToHex: (r, g, b) -> + "##{ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }" + + @getHexFormat: (rgba) -> + @rgbToHex(rgba.r, rgba.g, rgba.b) + + @getCSSStyle: (cssPropName, element, selection, childElement) -> + # Return any existing style value for the element and selection + # Find the first character in the selected text that has a + # `childElement` tag and + # return its `cssPropName` value. + + [from, to] = selection.get() + selectedContent = element.content.slice from, to + + for c in selectedContent.characters + # Does this character have a button tag applied? + if not c.hasTags childElement + continue + + # Find the button tag and return the attribute value + for tag in c.tags() + if tag.name() == childElement + return tag.style cssPropName + + return '' + + @getAttr: (attrName, element, selection, childElement) -> + # Return any existing attribute value for the element and selection + # Find the first character in the selected text that has a + # `childElement` tag and + # return its `attrName` value. + + [from, to] = selection.get() + selectedContent = element.content.slice from, to + + for c in selectedContent.characters + # Does this character have a time tag applied? + if not c.hasTags childElement + continue + + # Find the time tag and return the datetime attribute value + for tag in c.tags() + if tag.name() == childElement + return tag.attr attrName + + return '' + + @extendObj: (deep, objs...) -> + extended = {} + + # Merge the object into the extended object + merge = (obj) -> + + for prop in Object.keys obj + + if Object.prototype.hasOwnProperty.call obj, prop + + # If deep merge and property is an object, merge properties + if ( + deep and + Object.prototype.toString.call obj[prop] is '[object Object]' + ) + extended[prop] = @extendObj true, extended[prop], obj[prop] + else + extended[prop] = obj[prop] + + # Loop through each object and conduct a merge + for obj in objs + merge obj + + return extended + + +class GetBgColorDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a + # background css color prop value + + constructor: (color) -> + @_color = color + + super() + + mount: () -> + super() + + # Update the name and placeholder for the input field provided by the + # link dialog. + cssClass = @_domInput.getAttribute 'class' + colorPickerCssClass = 'color' + color = if @_color then @_color else \ + ButtonTool.getHexFormat ButtonTool.getAutomatColors()[1] + # purpleColor = ButtonTool.getHexFormat ButtonTool.getAutomatColors()[1] + + @_domInput.setAttribute 'name', 'color' + @_domInput.setAttribute 'class', "#{ cssClass } #{ colorPickerCssClass }" + @_domInput.setAttribute 'value', color + @_domInput.setAttribute 'placeholder', ContentEdit._ \ + 'Enter a background color' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the bg color. + detail = { + color: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + show: () -> + super() + # Once visible automatically unfocus to the color input + @_domInput.blur() + + +class GetTextColorDialog extends GetBgColorDialog + + # An anchored dialog to support inserting/modifying a + # text-color css color prop value + + mount: () -> + super() + + defTextColor = '#ffffff' + + color = if @_color then @_color else defTextColor + @_domInput.setAttribute 'value', color + + @_domInput.setAttribute 'placeholder', ContentEdit._ 'Enter a text color' + + +class GetPaddindDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a padding css prop value + + constructor: (@padding) -> + super() + + @_defPadding = '20, 20' + + mount: () -> + super() + + # Update the name and placeholder for the input field provided by the + # link dialog. + + @padding = if @padding then @padding else @_defPadding + @_domInput.setAttribute 'name', 'padding' + @_domInput.setAttribute 'value', @padding + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a button padding space' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + padding: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('button') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-center-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-center-tool.coffee new file mode 100644 index 000000000..0af36996e --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-center-tool.coffee @@ -0,0 +1,13 @@ +class CustomAlignCentertTool extends CustomAlignLeftTool + + ContentTools.ToolShelf.stow(@, 'custom-align-center') + + @label = 'Align center' + @icon = 'align-center' + @className = 'text-center' + + +index = ContentTools.DEFAULT_TOOLS[0].indexOf('align-center') + +# Override default align-center tool +ContentTools.DEFAULT_TOOLS[0][index] = 'custom-align-center' diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-left-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-left-tool.coffee new file mode 100644 index 000000000..e9c18a574 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-left-tool.coffee @@ -0,0 +1,50 @@ +class CustomAlignLeftTool extends ContentTools.Tools.AlignLeft + + ContentTools.ToolShelf.stow(@, 'custom-align-left') + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + # List items and table cells use child nodes to manage their content + # which don't support classes, so we need to use the parent. + if element.type() in ['ListItemText', 'TableCellText'] + element = element.parent() + + # Remove any existing text alignment classes applied + alignmentClassNames = [ + ContentTools.Tools.AlignLeft.className, + ContentTools.Tools.AlignCenter.className, + ContentTools.Tools.AlignRight.className, + AlignJustifyTool.className + ] + + for className in alignmentClassNames + if element.hasCSSClass(className) + element.removeCSSClass(className) + + # If we're removing the class associated with the tool then we + # can return early (this allows the tool to be toggled on/off). + if className == @className + return callback(true) + + # Add the alignment class to the element + element.addCSSClass(@className) + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + +index = ContentTools.DEFAULT_TOOLS[0].indexOf('align-left') + +# Override default align-left tool +ContentTools.DEFAULT_TOOLS[0][index] = 'custom-align-left' diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-right-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-right-tool.coffee new file mode 100644 index 000000000..2f4442261 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-align-right-tool.coffee @@ -0,0 +1,13 @@ +class CustomAlignRightTool extends CustomAlignLeftTool + + ContentTools.ToolShelf.stow(@, 'custom-align-right') + + @label = 'Align right' + @icon = 'align-right' + @className = 'text-right' + + +index = ContentTools.DEFAULT_TOOLS[0].indexOf('align-right') + +# Override default align-right tool +ContentTools.DEFAULT_TOOLS[0][index] = 'custom-align-right' diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee new file mode 100644 index 000000000..de79cdfab --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee @@ -0,0 +1,103 @@ +class CustomEditorApp extends _EditorApp + + # The custom editor application + + getStyles: () -> + _styles = [] + styles = document.querySelectorAll('[id^="font_size_"]') + for item, index in styles + _styles.push({id: item.id, style: item.innerHTML}) + return _styles + + save: (passive) -> + # Save changes to the current page + if not @dispatchEvent(@createEvent('save', {passive: passive})) + return + + # Blur any active element to ensure empty elements are not retained + root = ContentEdit.Root.get() + if root.focused() and not passive + root.focused().blur() + + # Check the document has changed, if not we don't need do anything + if root.lastModified() == @_rootLastModified and passive + # Trigger the saved event early with no modified regions, + @dispatchEvent( + @createEvent('saved', {regions: {}, passive: passive}) + ) + return + + # Build a map of the modified regions + domRegions = [] + modifiedRegions = {} + for name, region of @_regions + # Check for regions that contain only a place holder + html = region.html() + if region.children.length == 1 and not region.type() is 'Fixture' + child = region.children[0] + if child.content and not child.content.html() + html = '' + + # Apply the changes made to the DOM (affectively resetting the DOM + # to a non-editable state). + unless passive + # Unmount all children + for child in region.children + child.unmount() + + # Handle fixtures vs. standard regions + if region.children.length is 1 and region.children[0].isFixed() + wrapper = @constructor.createDiv() + wrapper.innerHTML = html + domRegions.push(wrapper.firstElementChild) + region.domElement().parentNode.replaceChild( + wrapper.firstElementChild, + region.domElement() + ) + else + domRegions.push(region.domElement()) + region.domElement().innerHTML = html + + # Check the region has been modified, if not we don't include it in + # the output. + if region.lastModified() == @_regionsLastModified[name] + continue + + modifiedRegions[name] = html + + # Set the region back to not modified + @_regionsLastModified[name] = region.lastModified() + + # Resync the DOM regions, this is required as fixture will replace the + # existing DOM region element (regions wont). + @_domRegions = domRegions + + # Trigger the saved event with a region HTML map for the changed + # content. + @dispatchEvent( + @createEvent( + 'saved', + { + regions: modifiedRegions, + passive: passive, + styles: @getStyles() + } + ) + ) + + +class ContentTools.EditorApp + + # The `ContentTools.EditorApp` class is a singleton, this code provides + # access to the singleton instance of the protected `_EditorApp` class which + # is initialized the first time the class method `get` is called. + + # Storage for the singleton instance that will be created for the editor app + instance = null + + @get: () -> + cls = ContentTools.EditorApp.getCls() + instance ?= new cls() + + @getCls: () -> + return CustomEditorApp diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee new file mode 100644 index 000000000..7df69c04c --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee @@ -0,0 +1,207 @@ +class DecreaseFontSizeTool extends ContentTools.Tools.Bold + + # Decrease font size of the current selection of text. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'decrease-font-size') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Decrease font size' + @icon = 'decrease-font-size' + @tagName = 'span' + @showOnce = false + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + element.storeState() + + IncreaseFontSizeTool.showOnce = false + + if not @showOnce + # Add a fake selection wrapper to the selected text so that it + # appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + size = if IncreaseFontSizeTool._fontSizeFactor? then \ + IncreaseFontSizeTool._fontSizeFactor else null + dialog = new GetDecreaseFontSizeValueDialog(size) + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get color value + @_fontSizeFactor = ev.detail().size + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + else + @_apply element, selection + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @getNewFontSize: (styleClass) => + defaultFontSize = styleClass.getPropertyValue 'font-size' + defaultFontSize = parseInt \ + defaultFontSize.slice 0, defaultFontSize.length - 2 + newFontSize = defaultFontSize - parseInt(@_fontSizeFactor) + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + fontSizeCSSClassName = "font-size-#{ elementIndex }-#{ from }-#{ to }" + fontSizeId = "font_size_#{ elementIndex }_#{ from }_#{ to }" + + styleClass = window.getComputedStyle element.domElement() + @_newFontSize = @getNewFontSize(styleClass) + + if @isApplied element, selection + style = document.getElementById(fontSizeId) + + styleClass = window.getComputedStyle \ + document.getElementsByClassName(fontSizeCSSClassName)[0] + @_newFontSize = @getNewFontSize(styleClass) + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + style.innerHTML = fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + @_newFontSize -= parseInt @_fontSizeFactor + + @showOnce = true + + else + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + + style = document.getElementById fontSizeId + if style? + style.innerHTML = fontSizeCSSClass + else + @setNewCSS fontSizeId, fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + + @showOnce = true + + element.content.optimize() + element.updateInnerHTML() + element.taint() + + element.restoreState() + + +class GetDecreaseFontSizeValueDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a decrease font + # size value + + constructor: (@size) -> + super() + + @_defDecreaseFontSizeValue = '1' + + mount: () -> + super() + + # Update the name and placeholder for the input field provided by the + # dialog. + + @size = if @size? then @size else @_defDecreaseFontSizeValue + @_domInput.setAttribute 'name', 'size' + @_domInput.setAttribute 'value', @size + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a font size decrease factor (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the decrease font size value. + detail = { + size: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('decrease-font-size') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee new file mode 100644 index 000000000..d9bafbc56 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee @@ -0,0 +1,170 @@ +class FontSizeTool extends ContentTools.Tools.Bold + + # Set font size of the current selection of text. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'font-size') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Font size' + @icon = 'font-size' + @tagName = 'span' + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + element.storeState() + + # Add a fake selection wrapper to the selected text so that it + # appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + if not @isApplied element, selection + styleClass = window.getComputedStyle element.domElement() + fontSize = @getFontSize styleClass + else + fontSize = @_fontSize + + dialog = new GetIncreaseFontSizeValueDialog(fontSize) + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get color value + @_fontSize = ev.detail().size + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @getFontSize: (styleClass) -> + defaultFontSize = styleClass.getPropertyValue 'font-size' + defaultFontSize = parseInt \ + defaultFontSize.slice 0, defaultFontSize.length - 2 + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + fontSizeCSSClassName = "font-size-#{ elementIndex }-#{ from }-#{ to }" + fontSizeId = "font_size_#{ elementIndex }_#{ from }_#{ to }" + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_fontSize }px; + } + """ + + style = document.getElementById fontSizeId + if style? + style.innerHTML = fontSizeCSSClass + else + @setNewCSS fontSizeId, fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + + element.content.optimize() + element.updateInnerHTML() + element.taint() + + element.restoreState() + + +class FontSizeValueDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a font size value + + constructor: (@size) -> + super() + + @_defFontSizeValue = '14' + + mount: () -> + super() + + @size = if @size? then @size else @_defFontSizeValue + @_domInput.setAttribute 'name', 'size' + @_domInput.setAttribute 'value', @size + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a font size (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + size: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('font-size') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee new file mode 100644 index 000000000..1c88beecf --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee @@ -0,0 +1,205 @@ +class IncreaseFontSizeTool extends ContentTools.Tools.Bold + + # Increase font size of the current selection of text. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'increase-font-size') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Increase font size' + @icon = 'increase-font-size' + @tagName = 'span' + @showOnce = false + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + element.storeState() + + DecreaseFontSizeTool.showOnce = false + + if not @showOnce + # Add a fake selection wrapper to the selected text so that it + # appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + size = if DecreaseFontSizeTool._fontSizeFactor? \ + then DecreaseFontSizeTool._fontSizeFactor else null + dialog = new GetIncreaseFontSizeValueDialog(size) + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get color value + @_fontSizeFactor = ev.detail().size + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + else + @_apply element, selection + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @getNewFontSize: (styleClass) => + defaultFontSize = styleClass.getPropertyValue 'font-size' + defaultFontSize = parseInt \ + defaultFontSize.slice 0, defaultFontSize.length - 2 + newFontSize = defaultFontSize + parseInt @_fontSizeFactor + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + fontSizeCSSClassName = "font-size-#{ elementIndex }-#{ from }-#{ to }" + fontSizeId = "font_size_#{ elementIndex }_#{ from }_#{ to }" + + styleClass = window.getComputedStyle element.domElement() + @_newFontSize = @getNewFontSize styleClass + + if @isApplied element, selection + + style = document.getElementById fontSizeId + + styleClass = window.getComputedStyle \ + document.getElementsByClassName(fontSizeCSSClassName)[0] + @_newFontSize = @getNewFontSize styleClass + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + style.innerHTML = fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + @_newFontSize += parseInt @_fontSizeFactor + + @showOnce = true + + else + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + + style = document.getElementById fontSizeId + if style? + style.innerHTML = fontSizeCSSClass + else + @setNewCSS fontSizeId, fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + + @showOnce = true + + element.content.optimize() + element.updateInnerHTML() + element.taint() + + element.restoreState() + + +class GetIncreaseFontSizeValueDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a increase font + # size value + + constructor: (@size) -> + super() + + @_defIncreaseFontSizeValue = '1' + + mount: () -> + super() + + @size = if @size? then @size else @_defIncreaseFontSizeValue + @_domInput.setAttribute 'name', 'size' + @_domInput.setAttribute 'value', @size + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a font size increase factor (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the increase font size value. + detail = { + size: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('increase-font-size') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee new file mode 100644 index 000000000..a8aeebb80 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee @@ -0,0 +1,143 @@ +class LineSpacingTool extends CustomAlignLeftTool + + # Apply a class to justify align the contents of the current text block. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'line-spacing') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Line spacing' + @icon = 'line-spacing' + @className = 'line-spacing' + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + # # Measure a rectangle of the content selected so we can position the + # # dialog centrally to it. + domElement = element.domElement() + rect = domElement.getBoundingClientRect() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # List items and table cells use child nodes to manage their content + # which don't support classes, so we need to use the parent. + if element.type() in ['ListItemText', 'TableCellText'] + element = element.parent() + + dialog = new LineSpacingDialog() + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get line spacing number value + @_lineSpacing = ev.detail().lineSpacing + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + + callback(true) + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + lineSpacingCSSClassName = "line-spacing-#{ elementIndex }" + lineSpacingId = "line_spacing_#{ elementIndex }" + + lineSpacingCSSClass = """ + .article__content .#{ lineSpacingCSSClassName } { + line-height: #{ @_lineSpacing }; + } + """ + + style = document.getElementById lineSpacingId + if style? + style.innerHTML = lineSpacingCSSClass + else + @setNewCSS lineSpacingId, lineSpacingCSSClass + + # Add the alignment class to the element + element.addCSSClass(lineSpacingCSSClassName) + + +class LineSpacingDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a line spacing + # css prop value + + constructor: (@lineSpacing) -> + super() + + @_defLineSpacingValue = 1.0 + + mount: () -> + super() + + @lineSpacing = if @lineSpacing? then @lineSpacing else \ + @_defLineSpacingValue + @_domInput.setAttribute 'name', 'lineSpacing' + @_domInput.setAttribute 'value', @lineSpacing + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter number to set the line height' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the line spacing value. + detail = { + lineSpacing: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('line-spacing') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee new file mode 100644 index 000000000..8a69a3745 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee @@ -0,0 +1,11 @@ +moduleKeywords = ['included', 'extended'] + +class SuperClass + @include: (obj) -> + throw('include(obj) requires obj') unless obj + for key, value of obj.prototype when key not in moduleKeywords + @::[key] = value + + included = obj.included + included.apply(this) if included + @ diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee new file mode 100644 index 000000000..c094e1a4c --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee @@ -0,0 +1,156 @@ +class VerticalSpace extends ContentTools.Tool + + # Insert a vertical space in to the current element at the specified + # selection. + + ContentTools.ToolShelf.stow(@, 'vertical-space') + + @label = 'Add vertical space' + @icon = 'vertical-space' + + @canApply: (element, selection) -> + # Return true if the tool can be applied to the current + # element/selection. + return element.content + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Add an invisible modal that we'll use to detect if the user clicks away + # from the dialog to close it. + modal = new ContentTools.ModalUI transparent=true, allowScrolling=true + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + rect = domElement.getBoundingClientRect() + + # Create vertical space dialog + verticalSpaceDialog = new GetVerticalSpaceDialog() + verticalSpaceDialog.position @getDialogPosition rect + + # Listen for save events against the dialog + verticalSpaceDialog.addEventListener 'save', (ev) => + + # Get link value + verticalSpace = ev.detail().verticalSpace + + if verticalSpace + @_insertSpace(verticalSpace, element, selection) + + # Hide modal/dialog + modal.unmount() + verticalSpaceDialog.hide() + + # Trigger the callback + callback(true) + + app.attach modal + app.attach verticalSpaceDialog + + modal.show() + verticalSpaceDialog.show() + + callback(true) + + @_insertSpace: (verticalSpace, element, selection, toolDetail) -> + # Insert a table at the current in index + cursor = selection.get()[0] + 1 + + tip = element.content.substring(0, selection.get()[0]) + tail = element.content.substring(selection.get()[1]) + + tdContent="vertical space: #{ verticalSpace } px" + + tableHtmlString = "" + + "
" + + "
vertical space
" + + table = new HTMLString.String( + tableHtmlString, + element.content.preserveWhitespace() + ) + element.content = tip.concat(table, tail) + element.updateInnerHTML() + element.taint() + + # Restore the selection + selection.set(cursor, cursor) + element.selection(selection) + + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + element = ContentEdit.Root.get().focused() + + # If the element isn't a text element find the nearest top level + # node and insert a new paragraph element after it. + if element.parent().type() != 'Region' + element = element.closest (node) -> + return node.parent().type() is 'Region' + + region = element.parent() + paragraph = new ContentEdit.Text('p') + region.attach(paragraph, region.children.indexOf(element) + 1, false) + + # Give the newely inserted paragraph focus + paragraph.focus() + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + +class GetVerticalSpaceDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a vertical space + # px unit + + constructor: (@verticalSpace) -> + super() + + @_defVerticalSpace = '10' + + mount: () -> + super() + + # Update the name and placeholder for the input field provided by the + # link dialog. + + @verticalSpace = if @verticalSpace then @verticalSpace else \ + @_defVerticalSpace + @_domInput.setAttribute 'name', 'verticalSpace' + @_domInput.setAttribute 'value', @verticalSpace + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a vertical space (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + verticalSpace: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +index = ContentTools.DEFAULT_TOOLS[2].indexOf('line-break') + +ContentTools.DEFAULT_TOOLS[2].splice(index + 1, 0, 'vertical-space') diff --git a/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json b/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json new file mode 100644 index 000000000..f7afc5ea9 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json @@ -0,0 +1,18 @@ +{ + "Align justify": "Zarovnat do bloku", + "Background image": "Obrázek na pozadí", + "Button": "Tlačidlo", + "Decrease font size": "Zmenšit velikost písma", + "Font size": "Velikost písma", + "Increase font size": "Zvětšit velikost písma", + "Line spacing": "Řádkování", + "Add vertical space": "Přidejte svislú mezeru", + "Enter number to set the line height": "Zadejte číslo pro nastavení výšky řádku", + "Enter a text color": "Zadejte barvu textu", + "Enter a button padding space": "Zadejte hodnotu vzdálenosti textu tlačidla od okraje", + "Enter a background color": "Zadejte barvu pozadí", + "Enter a font size deacrease factor (px)": "Zadejte faktor snížení velikosti písma (px)", + "Enter a font size increase factor (px)": "Zadejte faktor zvětšení velikosti písma (px)", + "Enter a font size (px)": "Zadejte velikost písma (px)", + "Enter a vertical space (px)": "Zadejte svislú mezeru (px)" +} diff --git a/apps/aklub/static/aklub/js/csrf_token.js b/apps/aklub/static/aklub/js/csrf_token.js new file mode 100644 index 000000000..d5d8d1399 --- /dev/null +++ b/apps/aklub/static/aklub/js/csrf_token.js @@ -0,0 +1,16 @@ +// Cross Site Request Forgery protection +function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = (cookies[i]).replace(/^\s+|\s+$/g, ''); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} diff --git a/apps/aklub/static/aklub/js/edit_html_template.js b/apps/aklub/static/aklub/js/edit_html_template.js new file mode 100644 index 000000000..0c2a63cdd --- /dev/null +++ b/apps/aklub/static/aklub/js/edit_html_template.js @@ -0,0 +1,54 @@ +var csrftoken = getCookie('csrftoken') + +var editHtmlTemplate = {} + +editHtmlTemplate.domain = window.origin + +editHtmlTemplate.call = function (type, url, data, auth, onSuccess, onError) { + xhr = new XMLHttpRequest() + xhr.addEventListener('readystatechange', onSuccess) + + if (type == null) { + type = 'get' + } + if (url == null) { + url = '/' + } + if (data == null) { + data = null + } + if (auth == null) { + auth = true + } + if (onSuccess == null) { + onSuccess = null + } + if (onError == null) { + onError = null + } + url = '' + this.domain + url + switch (type) { + case 'get': + xhr.open('GET', url) + break + case 'post': + xhr.open('POST', url) + break + case 'put': + xhr.open('PUT', url) + break + case 'patch': + xhr.open('PATH', url) + break + case 'delete': + xhr.open('DELETE', url) + break + default: + console.log("Request type " + type + " is not supported"); + } + xhr.setRequestHeader('X-CSRFToken', csrftoken) + + if (data) { + xhr.send(data) + } +} diff --git a/apps/aklub/static/aklub/js/editor.js b/apps/aklub/static/aklub/js/editor.js new file mode 100644 index 000000000..d9cd58ef1 --- /dev/null +++ b/apps/aklub/static/aklub/js/editor.js @@ -0,0 +1,122 @@ +(function() { + window.onload = function() { + var editor; + ContentTools.StylePalette.add([ + new ContentTools.Style("By-line", "article__by-line", ["p"]), + new ContentTools.Style("Caption", "article__caption", ["p"]), + new ContentTools.Style("Example", "example", ["pre"]), + new ContentTools.Style("Example + Good", "example--good", ["pre"]), + new ContentTools.Style("Example + Bad", "example--bad", ["pre"]), + new ContentTools.Style( + "AutoMat-Poppins-Light-Font", + "article__automat-poppins-light-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-Poppins-Thin-Font", + "article__automat-poppins-thin-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-Poppins-ExtraLight-Font", + "article__automat-poppins-extralight-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-Poppins-Bold-Font", + "article__automat-poppins-bold-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-Poppins-Regular-Font", + "article__automat-poppins-regular-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-FormulaCondensed-AutoMat-Font", + "article__automat-formulacondesed-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-Poppins-SemiBold-Font", + "article__automat-poppins-semibold-font", + ["p"] + ), + new ContentTools.Style( + "AutoMat-Poppins-Medium-Font", + "article__automat-poppins-medium-font", + ["p"] + ) + ]); + + editor = ContentTools.EditorApp.get(); + editor.init(".editable", "data-name"); + + function getImages() { + // Return an object containing image URLs and widths for all regions + var descendants, i, images; + + images = {}; + for (name in editor.regions) { + // Search each region for images + descendants = editor.regions[name].descendants(); + for (i = 0; i < descendants.length; i++) { + // Filter out elements that are not images + if (descendants[i].constructor.name !== "Image") { + continue; + } + images[descendants[i].attr("src")] = descendants[i].size()[0]; + } + } + + return images; + } + editor.addEventListener("saved", function(ev) { + var onStateChange, + payload, + regions, + element, + pageId, + successSaveFlash, + failSaveFlash, + styles; + // Collect the contents of each region into a FormData instance + payload = new FormData(); + // Check that something changed + regions = ev.detail().regions; + if (Object.keys(regions).length == 0) { + return; + } + styles = ev.detail().styles; + payload.append("page", getTemplatePageUrl()); + payload.append("images", JSON.stringify(getImages())); + payload.append("regions", JSON.stringify(regions)); + payload.append("styles", JSON.stringify(styles)); + // Send the updated content to the server to be saved + onStateChange = function(ev) { + // Check if the request is finished + if (ev.target.readyState == 4) { + editor.busy(false); + if (ev.target.status == "201") { + // Save was successful, notify the user with a flash + successSaveFlash = new ContentTools.FlashUI("ok"); + } else { + // Save failed, notify the user with a flash + failSaveFlash = new ContentTools.FlashUI("no"); + } + } + }; + element = document.querySelector('meta[name="page-id"]'); + if (element != null) { + pageId = element.getAttribute("content"); + } + editHtmlTemplate.call( + "post", + window.reverse("html_template_editor:add"), + payload, + true, + onStateChange + ); + }); + }; +}.call(this, editHtmlTemplate)); diff --git a/apps/aklub/static/aklub/js/image_uploader.js b/apps/aklub/static/aklub/js/image_uploader.js new file mode 100644 index 000000000..b723f7deb --- /dev/null +++ b/apps/aklub/static/aklub/js/image_uploader.js @@ -0,0 +1,219 @@ +function getTemplatePageUrl () { + var newEmptyTemplateName = sessionStorage.getItem('newEmptyTemplateName') + var url = newEmptyTemplateName ? window.location.pathname + newEmptyTemplateName + '/' : window.location.pathname + var dbTemplateNameUrl = sessionStorage.getItem('dbTemplateName') + + return dbTemplateNameUrl ? dbTemplateNameUrl : url +} + + +var imageUploader = function (dialog) { + var image, xhr, xhrComplete, xhrProgress, evtFuncsNames + evtFuncsNames = { + cancelupload: 'imageuploader.cancelupload', + clear: 'imageuploader.clear', + fileready: 'imageuploader.fileready', + save: 'imageuploader.save', + rotateccw: 'imageuploader.rotateccw', + rotatecw: 'imageuploader.rotatecw' + } + + // Set up the event handlers + dialog.addEventListener(evtFuncsNames.cancelupload, function () { + // Cancel the current upload + + // Stop the upload + if (xhr) { + xhr.upload.removeEventListener('progress', xhrProgress) + xhr.removeEventListener('readystatechange', xhrComplete) + xhr.abort() + } + + // Set the dialog to empty + dialog.state('empty') + }) + + dialog.addEventListener(evtFuncsNames.clear, function () { + // Clear the current image + dialog.clear() + image = null + }) + + dialog.addEventListener(evtFuncsNames.fileready, function (ev) { + // Upload a file to the server + var formData + var file = ev.detail().file + // Image type: email temmplate content image or background image + var backgroundImage = ev.detail().backgroundImage + var maxImageWidth = 600 + + // Define functions to handle upload progress and completion + xhrProgress = function (ev) { + // Set the progress for the upload + dialog.progress((ev.loaded / ev.total) * 100) + } + + xhrComplete = function (ev) { + // Check the request is complete + if (ev.target.readyState !== 4) { + return + } + + // Clear the request + xhr = null + xhrProgress = null + xhrComplete = null + + // Handle the result of the upload + if (parseInt(ev.target.status) === 201) { + // Unpack the response (from JSON) + var response = JSON.parse(ev.target.responseText) + // Store the image details + image = { + id: response.id, + name: response.name, + size: response.size, + width: response.edited_width, + url: response.image, + } + if (parseInt(image.size[0]) > maxImageWidth) { + alert(django.gettext('Max allowed image width is 600 px.')) + // Set the dialog to empty + dialog.state('empty') + return + } + // Populate the dialog + dialog.populate(image.url, image.size) + } else { + // The request failed, notify the user + ContentTools.FlashUI('no') + } + } + + // Set the dialog state to uploading and reset the progress bar to 0 + dialog.state('uploading') + dialog.progress(0) + + // Build the form data to post to the server + formData = new FormData() + formData.append('image', file) + // Check if image is background image + typeof backgroundImage !== 'undefined' ? formData.append('background_image', backgroundImage) : null + formData.append('template_url', getTemplatePageUrl()) + // Set the width of the image when it's inserted, this is a default + // the user will be able to resize the image afterwards. + formData.append('width', 600) + // Make the request + xhr = new XMLHttpRequest() + xhr.upload.addEventListener('progress', xhrProgress) + xhr.upload.addEventListener('readystatechange', xhrComplete) + editHtmlTemplate.call('post', window.reverse('html_template_editor:images_add'), formData, true, xhrComplete) + }) + + dialog.addEventListener(evtFuncsNames.save, function () { + var crop, cropRegion, formData, failRequestFlash + + // Define a function to handle the request completion + xhrComplete = function (ev) { + // Check the request is complete + if (ev.target.readyState !== 4) { + return + } + + // Clear the request + xhr = null + xhrComplete = null + + // Free the dialog from its busy state + dialog.busy(false) + + // Handle the result of the rotation + if (parseInt(ev.target.status) === 200) { + // Unpack the response (from JSON) + var response = JSON.parse(ev.target.responseText) + + // Trigger the save event against the dialog with details of the + // image to be inserted. + dialog.save( + response.image, + response.size, + { + alt: response.name, + 'data-ce-max-width': image.size[0], + }) + } else { + // The request failed, notify the user + failRequestFlash = new ContentTools.FlashUI('no') + } + } + + // Set the dialog to busy while the rotate is performed + dialog.busy(true) + + // Build the form data to post to the server + formData = new FormData() + + // Check if a crop region has been defined by the user + if (dialog.cropRegion()) { + formData.append('crop', dialog.cropRegion()) + } + + // Make the request + xhr = new XMLHttpRequest() + xhr.upload.addEventListener('readystatechange', xhrComplete) + editHtmlTemplate.call('put', window.reverse('html_template_editor:images_update', { id: image.id }), formData, true, xhrComplete) + }) + + function rotateImage (direction) { + // Request a rotated version of the image from the server + var formData + + // Define a function to handle the request completion + xhrComplete = function (ev) { + // Check the request is complete + if (ev.target.readyState !== 4) { + return + } + + // Clear the request + xhr = null + xhrComplete = null + + // Free the dialog from its busy state + dialog.busy(false) + + // Handle the result of the rotation + if (parseInt(ev.target.status) === 200) { + // Unpack the response (from JSON) + var response = JSON.parse(ev.target.responseText) + + // Populate the dialog + dialog.populate(response.image, response.size) + } else { + // The request failed, notify the user + new ContentTools.FlashUI('no') + } + } + + // Set the dialog to busy while the rotate is performed + dialog.busy(true) + + // Build the form data to post to the server + formData = new FormData() + formData.append('direction', direction) + + // Make the request + xhr = new XMLHttpRequest() + xhr.upload.addEventListener('readystatechange', xhrComplete) + editHtmlTemplate.call('put', window.reverse('html_template_editor:images_update', { id: image.id }), formData, true, xhrComplete) + } + + dialog.addEventListener(evtFuncsNames.rotateccw, function () { + rotateImage('CCW') + }) + + dialog.addEventListener(evtFuncsNames.rotatecw, function () { + rotateImage('CW') + }) +} +ContentTools.IMAGE_UPLOADER = imageUploader diff --git a/apps/aklub/static/aklub/js/internationalization.js b/apps/aklub/static/aklub/js/internationalization.js new file mode 100644 index 000000000..257323932 --- /dev/null +++ b/apps/aklub/static/aklub/js/internationalization.js @@ -0,0 +1,34 @@ +(function() { + var editor, xhr; + + // Get the editor + editor = new ContentTools.EditorApp.get(); + + // Define our request for the French translation file + xhr = new XMLHttpRequest(); + xhr.open( + 'GET', + window.reverse('aklub:get_contenttools_translation', djangoLang), + true); + + function onStateChange (ev) { + var translations; + if (ev.target.readyState == 4) { + // Convert the JSON data to a native Object + translations = JSON.parse(ev.target.responseText); + + // Add the translations for the French language + ContentEdit.addTranslations(djangoLang, translations); + + // Set French as the editors current language + ContentEdit.LANGUAGE = djangoLang; + } + } + + if (djangoLang != "en") { + xhr.addEventListener('readystatechange', onStateChange); + } + + // Load the language + xhr.send(null); +}()); diff --git a/apps/aklub/static/aklub/js/template_widget.min.js b/apps/aklub/static/aklub/js/template_widget.min.js new file mode 100644 index 000000000..534455731 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget.min.js @@ -0,0 +1,2 @@ +/*! HtmlTemplateEditorDialog v0.0.1 by Tomas Zigo */ +(function(){var a,b,c,d,e,f,g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},i=function(a,b){return function(){return a.apply(b,arguments)}};b=function(){function a(){}return a.prototype.getIdFormat=function(a){return"#"+a},a.prototype.getClassFormat=function(a){return"."+a},a.prototype.getNameFormat=function(a,b){return a+"[name='"+b+"']"},a}(),$(document).ready(function(){var a,b,c,e;return a=500,c=750,e=3e3,b=!0,new d("edit_template_dialog","edit_template",a,"id_template_name","id_template_type","id_hidden_template","field-template","field-template_textarea","id_template_textarea","template_page","id_template",c,b,e,"edit_template_btn"),new d("edit_template_en_dialog","edit_template_en",a,"id_template_en_name","id_template_en_type","id_hidden_template_en","field-template_en","field-template_en_textarea","id_template_en_textarea","template_en_page","id_template_en",c,b,e,"edit_template_en_btn")}),e=function(b){function c(a,b,c,d,e,f,g,h,i){this._title=a,this._editTemplateModalDialogId=c,this._backdrop=b,this._$templateNameField=d,this._destroyDelay=e,this._$mountElement=f,this._popoverModalDialogEditTemplateLinkElementId=g,this._$editTemplateModalDialogPageContainer=h,this._$hiddenTemplateField=i,this._$templateDivField=f}return h(c,b),c.editTemplateLinkText=gettext("Edit template"),c.prototype.show=function(b){return b.click(function(b){return function(c){var d,e,f,g;return c.preventDefault(),f=b._$templateNameField.find("option:selected"),e=f.text(),g=f.val().split(":"),d=new a(b._editTemplateModalDialogId,b._popoverModalDialogEditTemplateLinkElementId,b._$editTemplateModalDialogPageContainer,b._$hiddenTemplateField,b._$templateDivField),d.mount(e,"openViaPopoverDialog",g),b.unmount()}}(this)),this.delay(this._destroyDelay,function(a){return function(){return a.unmount()}}(this))},c.prototype.mount=function(a,b){var c;return c=this._getOpt(),c.offsetTop=a,c.offsetLeft=b,WebuiPopovers.show(this._$mountElement,c)},c.prototype.unmount=function(){return this._$mountElement.webuiPopover("hide"),this._$mountElement.webuiPopover("destroy")},c.prototype.delay=function(a,b){return setTimeout(b,a)},c.getContent=function(a,b){var c,d;return c=$("
"),c.attr({id:this.dialogId,href:""}),d={id:a,"data-type":"modal","data-target":b,"data-fullscreen":"true","data-overlayClick":"true"},c.attr(d),c.html(""+this.editTemplateLinkText+""),c},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b){return a.show(b)}}(this),a=c.getContent(this._popoverModalDialogEditTemplateLinkElementId,this.getIdFormat(this._editTemplateModalDialogId)),{placement:"top-right",trigger:"manual,",title:this._title,content:a,offsetTop:0,offsetLeft:0,animation:"fade",backdrop:this._backdrop,onShow:b}},c}(b),c=function(b){function c(a,b,c,d,e,f,g,h){this._getBtns=i(this._getBtns,this),this._confirmTemplateName=i(this._confirmTemplateName,this),this._dialogId="get_template_name",this._title=gettext("Template name"),this._showDelay=a,this._confirmBtnId="confirm_btn",this._$templateDivField=b,this._templateNameInputId="template_name",this._errorListCSSClass="errorlist",this._templateNameFieldId=c,this._newEmptyTemplateName=null,this._editTemplateModalDialogId=d,this._popoverModalDialogEditTemplateLinkElementId=e,this._templateName=f,this._$editTemplateModalDialogPageContainer=g,this._$hiddenTemplateField=h}return h(c,b),c.prototype.unmount=function(){return $(this.getIdFormat(this._dialogId)).dialog("destroy")},c.prototype.mount=function(){return this._getContainer().dialog(this._getOpt())},c.prototype._getContainer=function(){var a,b,c,d,e,f;return e=$("
"),e.attr({id:this._dialogId}),f=$("
"),f.attr({id:"template_name_form"}),a=$("
"),d=$("
    "),d.attr({class:this._errorListCSSClass}),c=$(""),c.attr({for:this._templateNameInputId}),c.text(gettext("Template name")),b=$(""),b.attr({id:this._templateNameInputId,name:this._templateNameInputId}),a.append(c),a.append(b),a.append(d),e.append(f.append(a)),e},c.prototype.getNeEmptyTemplateName=function(){return this._newEmptyTemplateName},c.prototype._triggerConfirmBtn=function(){var a;return a=function(a){return function(b){if(b.keyCode===$.ui.keyCode.ENTER)return $(a.getIdFormat(a._confirmBtnId)).click()}}(this),$(this.getIdFormat(this._dialogId)).keypress(a)},c.prototype._checkRegexp=function(a,b,c){var d;return!!b.test(a.val())||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
  • "+c+"
  • "),!1)},c.prototype._checkTemplateName=function(a,b,c){var d,e,f;return e=[],f=this.getIdFormat(this._templateNameFieldId)+" > option",$(f).each(function(){return e.push($(this).text())}),!(e.indexOf(b)>-1)||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
  • "+c+"
  • "),!1)},c.prototype._validateTemplateName=function(){var a,b,c,d;return d=!0,a=$(this.getIdFormat(this._templateNameInputId)),$(this.getClassFormat(this._errorListCSSClass)).html(""),c=gettext("Template name may consist of a-z, 0-9, underscores, not spaces and must begin with a letter, and be lowercase."),d=d&&this._checkRegexp(a,/^[a-z]([0-9a-z_])+$/,c),c=gettext("Template name exist."),b=$(this.getIdFormat(this._templateNameInputId)),d=d&&this._checkTemplateName(b,a.val(),c)},c.prototype._confirmTemplateName=function(){var b,c;if(this._validateTemplateName())return this._newEmptyTemplateName=$(this.getIdFormat(this._templateNameInputId)).val(),this.unmount(),this._setNewEmptyTemplateName(),c=[this._newEmptyTemplateName],b=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),b.mount(this._templateName,"openViaSelectBox",c)},c.prototype._setNewEmptyTemplateName=function(){if(null!=this._newEmptyTemplateName)return sessionStorage.setItem("newEmptyTemplateName",this._newEmptyTemplateName)},c.prototype._getBtns=function(){var a,b;return a={},b=function(a){return function(){return a._confirmTemplateName()}}(this),a.ok={text:gettext("Ok"),id:this._confirmBtnId,click:b},a},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b,c){return a._triggerConfirmBtn()}}(this),a=function(a){return function(b,c){return a.unmount(),a._$templateDivField.html("")}}(this),{modal:!0,title:this._title,show:{effect:"fade",delay:this._showDelay},resizable:!1,open:b,beforeClose:a,buttons:this._getBtns()}},c}(b),a=function(a){function b(a,b,c,d,e){this._closeEditTemplateModalDialog=i(this._closeEditTemplateModalDialog,this),this._dialogId=a,this._sessionStorageKey="dbTemplateName",this._popoverEditTemplateLinkElementId=b,this._closeBtnClass="mdl-close",this._$editTemplateModalDialogPageContainer=c,this._$hiddenTemplateField=d,this._$templateDivField=e,this._popoverDialog=null}return h(b,a),b.prototype.mount=function(a,b,c){var d,f;if(sessionStorage.removeItem(this._sessionStorageKey),this._popoverDialog||(this._popoverDialog=e.getContent(this._popoverEditTemplateLinkElementId,this.getIdFormat(this._dialogId)),$("body").append(this._popoverDialog)),this.init(this.getIdFormat(this._popoverEditTemplateLinkElementId)),c.length>1?(f=this._setPageContainerArgs(a,"aklub:get_email_template_from_db"),sessionStorage.setItem(this.sessionStorageKey,f)):f=this._setPageContainerArgs(a,"aklub:get_email_template"),this.show(this.getIdFormat(this._dialogId)),d=$(this.getIdFormat(this._dialogId)+" "+this.getClassFormat(this._closeBtnClass)),d.off("click",this._closeEditTemplateModalDialog),d.on("click",this._closeEditTemplateModalDialog),"openViaSelectBox"===b)return $(this.getIdFormat(this._popoverEditTemplateLinkElementId)).remove()},b.prototype._closeEditTemplateModalDialog=function(a){var b;return b=this._getIframeTemplateContent(),new f(b,$(b).find("article"),this._$templateDivField,this._$hiddenTemplateField)},b.prototype._getIframeTemplateContent=function(){var a;return a=document.querySelector(this.getIdFormat(this._$editTemplateModalDialogPageContainer.attr("id"))),null!=a?a.contentDocument:void 0},b.prototype._setPageContainerArgs=function(a,b){var c,d;return d=window.reverse(b,{template_name:a}),c={data:d,width:$(window).width(),height:$(window).height()},this._$editTemplateModalDialogPageContainer.attr(c),d},b.prototype.init=function(a){return $(a).mdl()},b.prototype.show=function(a){return mdl_open(a)},b}(b),d=function(b){function d(a,b,c,d,e,f,g,h,j,k,l,m,n,o,p){this._confirmTemplateEdit=i(this._confirmTemplateEdit,this),this._loadTemplateContent=i(this._loadTemplateContent,this),this._showEditTemplateModalDialog=i(this._showEditTemplateModalDialog,this),this._editTemplateBtnClickEvt=i(this._editTemplateBtnClickEvt,this),this._mouseMoveOverTemplateEvt=i(this._mouseMoveOverTemplateEvt,this),this._replaceTemplateFieldContent=i(this._replaceTemplateFieldContent,this),this._templateDivFieldId=l,this._hiddenTemplateFieldId=f,this._templateNameFieldId=d,this._getTemplateNameDialogShowDelay=c,this._editTemplateModalDialogId=a,this._popoverModalDialogEditTemplateLinkElementId=b,this._templateDivFormFieldContainerCSSClass=g,this._templateTextareaFieldId=j,this._editTemplateModalDialogPageContainerId=k,this._$templateDivFieldContainer=null,this._$templateTextareaFieldContainer=null,this._templateTextareaFieldValue="",this._showPopoverModalDialogDelay=m,this._popoverModalDialogBackDrop=n,this._popoverModalDialogDestroyDelay=o,this._templateTextAreaFormFieldContainerCSSClass=h,this._templateTypeFieldId=e,this._newEmptyTemplateName=null,this._getTemplateNameDialog=null,this._popoverDialogOpts=null,this._editTemplateBtnName=p,this._editTemplateModalDialog=null,this._cacheDom(),this._bindEvents(),this._templateFormFieldWidgetInit()}return h(d,b),d.prototype._cacheDom=function(){return this._$templateDivField=$(this.getIdFormat(this._templateDivFieldId)),this._$hiddenTemplateField=$(this.getIdFormat(this._hiddenTemplateFieldId)),this._$templateNameField=$(this.getIdFormat(this._templateNameFieldId)),this._$templateTypeField=$(this.getIdFormat(this._templateTypeFieldId)),this._$templateDivFormFieldContainer=$(this.getClassFormat(this._templateDivFormFieldContainerCSSClass)),this._$templateTextAreaFormFieldContainer=$(this.getClassFormat(this._templateTextAreaFormFieldContainerCSSClass)),this._$templateTextareaField=$(this.getIdFormat(this._templateTextareaFieldId)),this._$editTemplateModalDialogPageContainer=$(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),this._$editTemplateBtn=$(this.getNameFormat("input",this._editTemplateBtnName))},d.prototype._bindEvents=function(){return this._$templateNameField.bind("change",this._showEditTemplateModalDialog),this._$templateTypeField.bind("change",this._replaceTemplateFieldContent),this._$editTemplateModalDialogPageContainer.bind("load",this._loadTemplateContent),this._$editTemplateBtn.bind("click",this._editTemplateBtnClickEvt)},d.prototype._templateFormFieldWidgetInit=function(){var a;return this._$templateTextAreaFormFieldContainer.addClass("hidden"),"new"===this._$templateTypeField.val()?(this._exchangeFormFieldContainerContent(),a=this._$templateDivField.html(),this._$templateTextareaField.html(a),this._templateTextareaFieldValue=a,this._$templateDivField.html("")):"existed"===this._$templateTypeField.val()&&this._$hiddenTemplateField.val(this._$templateDivField.html()),this._initEditTemplateModalDialog(),this._enableDisableEditBtn(!0)},d.prototype._enableDisableEditBtn=function(a,b){var c;return null==b&&(b=!0),b?(c=this._$templateNameField.find("option:selected").val(),0===c.length?this._$editTemplateBtn.prop("disabled",a):void 0):this._$editTemplateBtn.prop("disabled",a)},d.prototype._getTemplateFieldContainer=function(){this._$templateDivFieldContainer=this._$templateDivFormFieldContainer,this._$templateTextareaFieldContainer=this._$templateTextAreaFormFieldContainer},d.prototype._checkTemplateFieldWidget=function(){var a;return this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.find("textarea"),a.length?"textarea":"div"},d.prototype._replaceTemplateFieldContent=function(a){var b;b=$(a.target).val(),"new"===b?"textarea"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContent():"existed"===b&&"div"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContentBack()},d.prototype._exchangeFormFieldContainerContentBack=function(){var a;return this._$templateNameField.attr("disabled",!1),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$editTemplateBtn.off("click",this._editTemplateBtnClickEvt),this._$editTemplateBtn.on("click",this._editTemplateBtnClickEvt)},d.prototype.delay=function(a,b){return setTimeout(b,a)},d.prototype._mouseMoveOverTemplateEvt=function(a){var b,c,d,e,f;return b=$(a.currentTarget).offset(),e=a.pageX-b.left,f=a.pageY-b.top,d=f,c=e,this.delay(this._showPopoverModalDialogDelay,function(a){return function(){return a._initPopoverModalDialog(d,c)}}(this))},d.prototype._initPopoverModalDialog=function(a,b){return this._popoverDialogOpts.mount(a,b)},d.prototype._bindPopoverDialogMouseMoveEvt=function(){return this._popoverDialogOpts=new e(gettext("Edit template"),this._popoverModalDialogBackDrop,this._editTemplateModalDialogId,this._$templateNameField,this._popoverModalDialogDestroyDelay,this._$templateDivField,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._$templateDivField.mousemove(this._mouseMoveOverTemplateEvt)},d.prototype._initEditTemplateModalDialog=function(){return this._editTemplateModalDialog=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField)},d.prototype._editTemplateBtnClickEvt=function(a){var b,c,d;return a.preventDefault(),c=this._$templateNameField.find("option:selected"),b=c.text(),d=c.val().split(":"),this._editTemplateModalDialog.mount(b,"openViaBtn",d)},d.prototype._exchangeFormFieldContainerContent=function(){var a;return this._$templateTextareaField.html(this._templateTextareaFieldValue),this._$templateNameField.attr("disabled",!0),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),this._$templateDivFieldContainer.find("label").attr("for",this._templateDivFieldId),this._$templateDivFieldContainer.find("label").text(this._$templateTextareaFieldContainer.find("label").text()),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$templateTextAreaFormFieldContainer.addClass("hidden"),this._$hiddenTemplateField.val("")},d.prototype._exchangeContent=function(){var a,b;this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.children(),b=this._$templateTextareaFieldContainer.children(),this._$templateDivFieldContainer.html(b),this._$templateTextareaFieldContainer.html(a)},d.prototype._showEditTemplateModalDialog=function(b){var d,e,f,g;if(sessionStorage.removeItem("newEmptyTemplateName"),e=""+this.getIdFormat(b.target.id),f=$(e+" option:selected").text(),$(""+e).val().length>0)return"new_empty_template"===f?(this._getTemplateNameDialog=new c(this._getTemplateNameDialogShowDelay,this._$templateDivField,this._templateNameFieldId,this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,f,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._getTemplateNameDialog.mount()):(this._enableDisableEditBtn(!1,!1),g=this._$templateNameField.find("option:selected").val().split(":"),d=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),d.mount(f,"openViaSelectBox",g))},d.prototype._loadTemplateContent=function(a){var b;if(this._newEmptyTemplateName=null!=(b=this._getTemplateNameDialog)?b.getNeEmptyTemplateName():void 0,null!=this._newEmptyTemplateName)return this._addEditTemplateConfirmEvent()},d.prototype._addEditTemplateConfirmEvent=function(){var a,b,c,d;return b="ct-ignition__button--confirm",d=document.querySelector(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),c=d.contentDocument,a=$(c).find(this.getClassFormat(b)),a.unbind("click"),a.bind("click",this._confirmTemplateEdit)},d.prototype._confirmTemplateEdit=function(){if(null!=this._newEmptyTemplateName)return this._addNewSelectOpt(this._newEmptyTemplateName,this._$templateNameField),this._eraseNewTemplateName()},d.prototype._eraseNewTemplateName=function(){return this._newEmptyTemplateName=null},d.prototype._addNewSelectOpt=function(a,b){var c,d,e,f;if(d=$(this.getIdFormat(this._templateNameFieldId)+" option"),f=$.map(d,function(a){return a.value}),e="new_empty_template:"+a,-1===f.indexOf(e))return c=new Option(a,e),b.append(c),b.val(e)},d}(b),f=function(a){function b(a,b,c,d){this._htmlDoc=a,this._$editTemplatePageContainer=b,this._$templateDivField=c,this._$hiddenTemplateField=d,this._videoClass="video",this._verticalSpaceClassName="vertical-space",this._textContainerTags=["p","ul","ol","h1","h2","h3","h4","h5","h6"],this.convertVideoIframeToImgThumbnail()}return h(b,a),b.prototype.convertVideoIframeToImgThumbnail=function(){var a,b,c,d,e,f;return c=[],b=!1,f=new RegExp("youtube.com.*(v=|/embed/)(.{11})"),e=new RegExp("vimeo.com.*./(.*)"),a=function(a,b,c,d,e){return a.css("float",e),b.append(a),b.append(c),$(d).after(b),$(d).remove()},d=function(b){var c;return"vimeo"===this.videoType&&(c={src:b.thumbnail_url,width:b.thumbnail_width,height:b.thumbnail_height},this.$videoThumbnail.attr(c)),this.$videoCaption.text(b.title),a(this.$videoThumbnail,this.$clickableVideoThumbnail,this.$videoCaption,this.element,this.iframeCssFloatProperty)},$(this._htmlDoc).find("iframe").each(function(a){return function(g,h){var i,j,k,l,m,n,o,p,q,r,s,t,u;return p=$(h).attr("src"),n=$(h).css("float"),q=$(h).attr("width"),o=$(h).attr("height"),i=$(''),i.attr("href",p),k=$(''),k.attr({width:q,height:o}),j=$("
    "),m={"text-align":"center","font-size":"12px"},j.css(m),p.match(f)?(b=!0,t=p.match(f).pop(),i.attr("id",t),i.addClass("youtube"),u="//img.youtube.com/vi/"+t+"/0.jpg",k.attr("src",u),r="http://www.youtube.com/watch?v="+t+"&",s="https://noembed.com/embed?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"youtube"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):p.match(e)?(b=!0,t=p.match(e).pop(),i.attr("id",t),i.addClass("vimeo"),r="https://vimeo.com/"+t+"& width="+q+"&height="+o,s="https://vimeo.com/api/oembed.json?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"vimeo"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):void 0}}(this)),$.when.apply(null,c).done(function(a){return function(){return a.convertCssToInlineStyle()}}(this))},b.prototype.getAjaxData=function(a,b,c,d,e){return null==d&&(d="GET"),null==e&&(e="json"),{type:d,url:b,jsonp:"callback",dataType:e,context:a,success:c}},b.prototype.convertCssToInlineStyle=function(){return this.inlineStyler($(this._htmlDoc)),this._fixContentWidth(),this._fixMargin(),this._replaceImgSrc(),this._convertTextCssPositionToTable(),this._eraseVerticalSpaceContent(),this._$templateDivField.html(this._$editTemplatePageContainer),this._$hiddenTemplateField.val(this._$editTemplatePageContainer.html())},b.prototype.inlineStyler=function(a){return a.inlineStyler()},b.prototype.getElementPosition=function(a,b,c){var d,e,f,g;return null==b&&(b=""),null==c&&(c=""),d=$("
    "),d.attr({cellspacing:0,cellpadding:0,border:0,width:"100%"}),e=$(""),g=$(""),f=$(""),f.css({border:"none",padding:0,margin:0}),f.attr({align:a,width:b,heigth:c}),d.append(e.append(g.append(f)))},b.prototype._replaceImgSrc=function(){var a;return a=this._$editTemplatePageContainer.find("img"),a.each(function(a){return function(b,c){var d,e,f,g,h;return d=$(c).closest(a.getClassFormat(a._videoClass)),e=d.length>0?d:$(c),0===d.length&&(h=$(c).attr("src"),g=new RegExp(window.origin),h.match(g)||(f=""+window.origin+$(c).attr("src"),$(c).attr("src",f))),a.convertImageCssPositionToTable(e)}}(this))},b.prototype.convertImageCssPositionToTable=function(a){var b,c,d,e,f,g,h;a=a.find("img").length>0?a.find("img"):a,f=parseInt(a.css("margin-left").split(".")[0].replace("px","")),g=parseInt(a.css("margin-right").split(".")[0].replace("px","")),e=a.css("float"),a.css("display",""),f===g&&"none"===e?c=this.getElementPosition("center","",""):f>g?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):g>f?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):"left"!==e&&"right"!==e||(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)),h=this.getClassFormat(this._videoClass),d=a.closest(h),c.find("td").append(d.length>0?d.clone():a.clone()),"left"===e?c.find("tr").append(b):c.find("tr").prepend(b),d.length>0?d.after(c):a.after(c),d.length>0?d.remove():a.remove()},b.prototype.convertImgFloatCssPositionToTable=function(a,b,c){var d,e,f,g,h,i,j;return i=15,j="left"===c?"0 0 0 "+i+"px":"0 "+i+"px 0 0",e=$(""),e.css({border:"none",padding:j,margin:"0"}),f=a.closest(this.getClassFormat(this._videoClass)),d=f.length>0?f.next().clone():a.next().clone(),h=parseFloat(b.find("#content_table").width()),g=h-parseFloat(a.attr("width"))-i,d.css("width",g),e.attr("width",g),e.append(d),f.length>0?f.next().remove():a.next().remove(),e},b.prototype._convertTextCssPositionToTable=function(){var a;return a=this._textContainerTags.join(", "),this._$editTemplatePageContainer.find(a).each(function(a){return function(b,c){var d,e;switch(e=$(c).attr("class")?$(c).attr("class").split(" "):[]){case e.indexOf("text-left")>-1:d=a.getElementPosition("left","","");break;case e.indexOf("text-right")>-1:d=a.getElementPosition("right","","");break;case e.indexOf("text-center")>-1:d=a.getElementPosition("center","","");break;default:d=a.getElementPosition("center","","")}return a.adjustListCssProperties($(c)),d.find("td").append($(c).clone()),$(c).after(d),$(c).remove()}}(this))},b.prototype.adjustListCssProperties=function(a){if(a.is("ul")||a.is("ol"))return a.css({margin:"10px",padding:"15px"})},b.prototype._fixMargin=function(){return $(this._htmlDoc).find("article").css("margin","")},b.prototype._getNumberValue=function(a,b){return null==b&&(b=-2),parseInt(a.slice(0,a.length-b))},b.prototype._getPaddingValue=function(a){var b,c;return null==a&&(a=!0),c=!0===a?"padding--left":"padding--top",b=$(this._htmlDoc).find(".article__content p").first().css(c),this._getNumberValue(b)},b.prototype._fixContentWidth=function(){var a,b;return b=this._getPaddingValue(),a=this._textContainerTags.join(", "),$(this._htmlDoc).find("article").find(a).each(function(a){return function(c,d){var e;return e=a._getNumberValue($(d).css("width")),$(d).css({width:e-2*b})}}(this))},b.prototype._eraseVerticalSpaceContent=function(){var a;return a=this.getClassFormat(this._verticalSpaceClassName),this._$editTemplatePageContainer.find(a).each(function(a,b){return $(b).find("center").remove()})},b}(b)}).call(this); \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/template_widget/Gruntfile.coffee b/apps/aklub/static/aklub/js/template_widget/Gruntfile.coffee new file mode 100644 index 000000000..e89187d96 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/Gruntfile.coffee @@ -0,0 +1,81 @@ +module.exports = (grunt) -> + + require('es6-promise').polyfill() + + # Project configuration + grunt.initConfig({ + + pkg: grunt.file.readJSON('package.json') + + coffee: + options: + join: true + + build: + #options: + # sourceMap: true + files: + 'src/tmp/template_widget.js': [ + 'src/script/mixins.coffee', + 'src/script/app_init.coffee', + 'src/script/popover_dialog.coffee', + 'src/script/get_template_name_dialog.coffee', + 'src/script/edit_template_dialog.coffee', + 'src/script/html_template_widget.coffee', + 'src/script/process_template.coffee' + + ] + + uglify: + options: + banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> */\n' + mangle: true + # sourceMap: true + # sourceMapIn: './src/tmp/template_widget.js.map' + + build: + src: 'src/tmp/template_widget.js' + dest: '../template_widget.min.js' + + clean: + build: ['src/tmp'] + + jasmine: + build: + src: ['build/content-tools.js'] + options: + specs: 'spec/content-tools-spec.js' + helpers: 'spec/spec-helper.js' + + watch: + build: + files: [ + 'src/scripts/**/*.coffee', + ] + tasks: ['build'] + + spec: + files: ['src/spec/**/*.coffee'] + tasks: ['spec'] + }) + + # Plug-ins + grunt.loadNpmTasks 'grunt-contrib-clean' + grunt.loadNpmTasks 'grunt-contrib-coffee' + grunt.loadNpmTasks 'grunt-contrib-jasmine' + grunt.loadNpmTasks 'grunt-contrib-uglify' + grunt.loadNpmTasks 'grunt-contrib-watch' + + # Tasks + grunt.registerTask 'build', [ + 'coffee:build' + 'uglify:build' + 'clean:build' + ] + + grunt.registerTask 'spec', [ + 'coffee:spec' + ] + + grunt.registerTask 'watch-build', ['watch:build'] + grunt.registerTask 'watch-spec', ['watch:spec'] diff --git a/apps/aklub/static/aklub/js/template_widget/package-lock.json b/apps/aklub/static/aklub/js/template_widget/package-lock.json new file mode 100644 index 000000000..7c62d3977 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/package-lock.json @@ -0,0 +1,4903 @@ +{ + "name": "HtmlTemplateEditorDialog", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "alter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/alter/-/alter-0.2.0.tgz", + "integrity": "sha1-x1iICGF1cgNKrmJICvJrHU0cs80=", + "dev": true, + "requires": { + "stable": "~0.1.3" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-traverse": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ast-traverse/-/ast-traverse-0.1.1.tgz", + "integrity": "sha1-ac8rg4bxnc2hux4F1o/jWdiJfeY=", + "dev": true + }, + "ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", + "dev": true + }, + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "babel": { + "version": "5.8.38", + "resolved": "https://registry.npmjs.org/babel/-/babel-5.8.38.tgz", + "integrity": "sha1-37CHwiiUkXxXb7Z86c8yjUWGKfs=", + "dev": true, + "requires": { + "babel-core": "^5.6.21", + "chokidar": "^1.0.0", + "commander": "^2.6.0", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^0.1.0", + "glob": "^5.0.5", + "lodash": "^3.2.0", + "output-file-sync": "^1.1.0", + "path-exists": "^1.0.0", + "path-is-absolute": "^1.0.0", + "slash": "^1.0.0", + "source-map": "^0.5.0" + } + }, + "babel-core": { + "version": "5.8.38", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-5.8.38.tgz", + "integrity": "sha1-H8ruedfmG3ULALjlT238nQr4ZVg=", + "dev": true, + "requires": { + "babel-plugin-constant-folding": "^1.0.1", + "babel-plugin-dead-code-elimination": "^1.0.2", + "babel-plugin-eval": "^1.0.1", + "babel-plugin-inline-environment-variables": "^1.0.1", + "babel-plugin-jscript": "^1.0.4", + "babel-plugin-member-expression-literals": "^1.0.1", + "babel-plugin-property-literals": "^1.0.1", + "babel-plugin-proto-to-assign": "^1.0.3", + "babel-plugin-react-constant-elements": "^1.0.3", + "babel-plugin-react-display-name": "^1.0.3", + "babel-plugin-remove-console": "^1.0.1", + "babel-plugin-remove-debugger": "^1.0.1", + "babel-plugin-runtime": "^1.0.7", + "babel-plugin-undeclared-variables-check": "^1.0.2", + "babel-plugin-undefined-to-void": "^1.1.6", + "babylon": "^5.8.38", + "bluebird": "^2.9.33", + "chalk": "^1.0.0", + "convert-source-map": "^1.1.0", + "core-js": "^1.0.0", + "debug": "^2.1.1", + "detect-indent": "^3.0.0", + "esutils": "^2.0.0", + "fs-readdir-recursive": "^0.1.0", + "globals": "^6.4.0", + "home-or-tmp": "^1.0.0", + "is-integer": "^1.0.4", + "js-tokens": "1.0.1", + "json5": "^0.4.0", + "lodash": "^3.10.0", + "minimatch": "^2.0.3", + "output-file-sync": "^1.1.0", + "path-exists": "^1.0.0", + "path-is-absolute": "^1.0.0", + "private": "^0.1.6", + "regenerator": "0.8.40", + "regexpu": "^1.3.0", + "repeating": "^1.1.2", + "resolve": "^1.1.6", + "shebang-regex": "^1.0.0", + "slash": "^1.0.0", + "source-map": "^0.5.0", + "source-map-support": "^0.2.10", + "to-fast-properties": "^1.0.0", + "trim-right": "^1.0.0", + "try-resolve": "^1.0.0" + } + }, + "babel-plugin-constant-folding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-constant-folding/-/babel-plugin-constant-folding-1.0.1.tgz", + "integrity": "sha1-g2HTZMmORJw2kr26Ue/whEKQqo4=", + "dev": true + }, + "babel-plugin-dead-code-elimination": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-dead-code-elimination/-/babel-plugin-dead-code-elimination-1.0.2.tgz", + "integrity": "sha1-X3xFEnTc18zNv7s+C4XdKBIfD2U=", + "dev": true + }, + "babel-plugin-eval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-eval/-/babel-plugin-eval-1.0.1.tgz", + "integrity": "sha1-ovrtJc5r5preS/7CY/cBaRlZUNo=", + "dev": true + }, + "babel-plugin-inline-environment-variables": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-inline-environment-variables/-/babel-plugin-inline-environment-variables-1.0.1.tgz", + "integrity": "sha1-H1jOkSB61qgmqL9kX6/mj/X+P/4=", + "dev": true + }, + "babel-plugin-jscript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/babel-plugin-jscript/-/babel-plugin-jscript-1.0.4.tgz", + "integrity": "sha1-jzQsOCduh6R9X6CovT1etsytj8w=", + "dev": true + }, + "babel-plugin-member-expression-literals": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-member-expression-literals/-/babel-plugin-member-expression-literals-1.0.1.tgz", + "integrity": "sha1-zF7bD6qNyScXDnTW0cAkQAIWJNM=", + "dev": true + }, + "babel-plugin-property-literals": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-property-literals/-/babel-plugin-property-literals-1.0.1.tgz", + "integrity": "sha1-AlIwGQAZKYCxwRjv6kjOk6q4MzY=", + "dev": true + }, + "babel-plugin-proto-to-assign": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/babel-plugin-proto-to-assign/-/babel-plugin-proto-to-assign-1.0.4.tgz", + "integrity": "sha1-xJ56/QL1d7xNoF6i3wAiUM980SM=", + "dev": true, + "requires": { + "lodash": "^3.9.3" + } + }, + "babel-plugin-react-constant-elements": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-react-constant-elements/-/babel-plugin-react-constant-elements-1.0.3.tgz", + "integrity": "sha1-lGc26DeEKcvDSdz/YvUcFDs041o=", + "dev": true + }, + "babel-plugin-react-display-name": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-react-display-name/-/babel-plugin-react-display-name-1.0.3.tgz", + "integrity": "sha1-dU/jiSboQkpOexWrbqYTne4FFPw=", + "dev": true + }, + "babel-plugin-remove-console": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-remove-console/-/babel-plugin-remove-console-1.0.1.tgz", + "integrity": "sha1-2PJFVsOgUAXUKqqv0neH9T/wE6c=", + "dev": true + }, + "babel-plugin-remove-debugger": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-remove-debugger/-/babel-plugin-remove-debugger-1.0.1.tgz", + "integrity": "sha1-/S6jzWGkKK0fO5yJiC/0KT6MFMc=", + "dev": true + }, + "babel-plugin-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-runtime/-/babel-plugin-runtime-1.0.7.tgz", + "integrity": "sha1-v3x9lm3Vbs1cF/ocslPJrLflSq8=", + "dev": true + }, + "babel-plugin-undeclared-variables-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-undeclared-variables-check/-/babel-plugin-undeclared-variables-check-1.0.2.tgz", + "integrity": "sha1-XPGqU52BP/ZOmWQSkK9iCWX2Xe4=", + "dev": true, + "requires": { + "leven": "^1.0.2" + } + }, + "babel-plugin-undefined-to-void": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/babel-plugin-undefined-to-void/-/babel-plugin-undefined-to-void-1.1.6.tgz", + "integrity": "sha1-f1eO+LeN+uYAM4XYQXph7aBuL4E=", + "dev": true + }, + "babel-runtime": { + "version": "5.8.38", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz", + "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=", + "dev": true, + "requires": { + "core-js": "^1.0.0" + } + }, + "babylon": { + "version": "5.8.38", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-5.8.38.tgz", + "integrity": "sha1-7JsSCxG/bM1Bc6GL8hfmC3mFn/0=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", + "dev": true, + "requires": { + "readable-stream": "~2.0.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "breakable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/breakable/-/breakable-1.0.0.tgz", + "integrity": "sha1-eEp5eRWjjq0nutRWtVcstLuqeME=", + "dev": true + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true, + "requires": { + "pako": "~0.2.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commoner": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/commoner/-/commoner-0.10.8.tgz", + "integrity": "sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=", + "dev": true, + "requires": { + "commander": "^2.5.0", + "detective": "^4.3.1", + "glob": "^5.0.15", + "graceful-fs": "^4.1.2", + "iconv-lite": "^0.4.5", + "mkdirp": "^0.5.0", + "private": "^0.1.6", + "q": "^1.1.2", + "recast": "^0.11.17" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "dev": true, + "requires": { + "ast-types": "0.9.6", + "esprima": "~3.1.0", + "private": "~0.1.5", + "source-map": "~0.5.0" + } + } + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", + "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.x.x" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "defs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/defs/-/defs-1.1.1.tgz", + "integrity": "sha1-siYJ8sehG6ej2xFoBcE5scr/qdI=", + "dev": true, + "requires": { + "alter": "~0.2.0", + "ast-traverse": "~0.1.1", + "breakable": "~1.0.0", + "esprima-fb": "~15001.1001.0-dev-harmony-fb", + "simple-fmt": "~0.1.0", + "simple-is": "~0.2.0", + "stringmap": "~0.2.2", + "stringset": "~0.2.1", + "tryor": "~0.1.2", + "yargs": "~3.27.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-indent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-3.0.1.tgz", + "integrity": "sha1-ncXl3bzu+DJXZLlFGwK8bVQIT3U=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "minimist": "^1.1.0", + "repeating": "^1.1.0" + } + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "dev": true, + "requires": { + "acorn": "^5.2.1", + "defined": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es5-shim": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.5.13.tgz", + "integrity": "sha512-xi6hh6gsvDE0MaW4Vp1lgNEBpVcCXRWfPXj5egDvtgLz4L9MEvNwYEMdJH+JJinWkwa8c3c3o5HduV7dB/e1Hw==", + "dev": true + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "es6-require": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/es6-require/-/es6-require-0.2.1.tgz", + "integrity": "sha1-j7eR2bsBskjPZuAvxAGVtepSb0o=", + "dev": true, + "requires": { + "babel": "^5.8.5", + "babel-runtime": "^5.8.20" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "esprima-fb": { + "version": "15001.1001.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz", + "integrity": "sha1-Q761fsJujPI3092LM+QlM1d/Jlk=", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extract-zip": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", + "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=", + "dev": true, + "requires": { + "concat-stream": "1.5.0", + "debug": "0.7.4", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "faye-websocket": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.4.tgz", + "integrity": "sha1-wUxbO/FNdBf/v9mQwKdJXNnzN7w=", + "dev": true + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-sync-cmp": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", + "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + } + } + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "dev": true, + "requires": { + "async": "^2.0.1", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.11" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs-extra": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", + "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, + "fs-readdir-recursive": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz", + "integrity": "sha1-MVtPuMHKW4xH3v7zGdBz2tNWgFk=", + "dev": true + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "gaze": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "dev": true, + "requires": { + "globule": "~0.1.0" + } + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "globals": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-6.4.1.tgz", + "integrity": "sha1-hJgDKzttHMge68X3lpDY/in6v08=", + "dev": true + }, + "globule": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "requires": { + "glob": "~3.1.21", + "lodash": "~1.0.1", + "minimatch": "~0.2.11" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "grunt-contrib-clean": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.6.0.tgz", + "integrity": "sha1-9TLbpLghJnTHwBPhRr2mY4uQSPY=", + "dev": true, + "requires": { + "rimraf": "~2.2.1" + } + }, + "grunt-contrib-coffee": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", + "integrity": "sha1-+v48nuikQryNF9WlwZ/I5i2fP0U=", + "dev": true, + "requires": { + "chalk": "~0.5.0", + "coffee-script": "~1.7.0", + "lodash": "~2.4.1" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "coffee-script": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", + "integrity": "sha1-YplqhheAx15tUGnROCJyO3NAS/w=", + "dev": true, + "requires": { + "mkdirp": "~0.3.5" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "grunt-contrib-copy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", + "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "file-sync-cmp": "^0.1.0" + } + }, + "grunt-contrib-jasmine": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/grunt-contrib-jasmine/-/grunt-contrib-jasmine-0.9.2.tgz", + "integrity": "sha1-qMovO4gCRTchN3X8u5iL2dkflqw=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "es5-shim": "^4.0.1", + "grunt-lib-phantomjs": "^0.7.1", + "jasmine-core": "^2.0.4", + "lodash": "~2.4.1", + "rimraf": "^2.1.4" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-contrib-uglify": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.7.0.tgz", + "integrity": "sha1-ed3oGmLOxv+jW0ejKDNwZ0T454c=", + "dev": true, + "requires": { + "chalk": "^0.5.1", + "lodash": "^2.4.1", + "maxmin": "^1.0.0", + "uglify-js": "^2.4.0", + "uri-path": "0.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "grunt-contrib-watch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-0.6.1.tgz", + "integrity": "sha1-ZP3LolpjX1tNobbOb5DaCutuPxU=", + "dev": true, + "requires": { + "async": "~0.2.9", + "gaze": "~0.5.1", + "lodash": "~2.4.1", + "tiny-lr-fork": "0.0.5" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + } + } + }, + "grunt-lib-phantomjs": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/grunt-lib-phantomjs/-/grunt-lib-phantomjs-0.7.1.tgz", + "integrity": "sha1-pJasEEvI6ELiaJN0nVTEkFR16Pw=", + "dev": true, + "requires": { + "eventemitter2": "^0.4.9", + "phantomjs": "^1.9.15", + "semver": "^4.3.0", + "temporary": "^0.0.8" + } + }, + "gzip-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-1.0.0.tgz", + "integrity": "sha1-Zs+LEBBHInuVus5uodoMF37Vwi8=", + "dev": true, + "requires": { + "browserify-zlib": "^0.1.4", + "concat-stream": "^1.4.1" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "dev": true, + "requires": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "home-or-tmp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz", + "integrity": "sha1-S58eQIAMPlDGwn94FnavzOcfOYU=", + "dev": true, + "requires": { + "os-tmpdir": "^1.0.1", + "user-home": "^1.1.1" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + }, + "dependencies": { + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + } + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-integer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-integer/-/is-integer-1.0.7.tgz", + "integrity": "sha1-a96Bqs3feLZZtmKdYpytxRqIbVw=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz", + "integrity": "sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jasmine-core": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", + "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "dev": true + }, + "js-tokens": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.1.tgz", + "integrity": "sha1-zENaXIuUrRWst5gxQPyAGCyJrq4=", + "dev": true + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", + "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=", + "dev": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "leven": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/leven/-/leven-1.0.2.tgz", + "integrity": "sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "dev": true + }, + "maxmin": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-1.1.0.tgz", + "integrity": "sha1-cTZehKmd2Piz99X94vANHn9zvmE=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "figures": "^1.0.1", + "gzip-size": "^1.0.0", + "pretty-bytes": "^1.0.0" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "mime-db": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", + "dev": true, + "requires": { + "mime-db": "1.42.0" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=", + "dev": true + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "noptify": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/noptify/-/noptify-0.0.3.tgz", + "integrity": "sha1-WPZUpz2XU98MUdlobckhBKZ/S7s=", + "dev": true, + "requires": { + "nopt": "~2.0.0" + }, + "dependencies": { + "nopt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.0.0.tgz", + "integrity": "sha1-ynQW8gpeP5w7hhgPlilfo9C1Lg0=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.4", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.0" + } + }, + "package": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package/-/package-1.0.1.tgz", + "integrity": "sha1-0lofmeJQbcsn1nBLg9yooxLk7cw=", + "dev": true + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", + "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "phantomjs": { + "version": "1.9.20", + "resolved": "https://registry.npmjs.org/phantomjs/-/phantomjs-1.9.20.tgz", + "integrity": "sha1-RCSsog4U0lXAsIia9va4lz2hDg0=", + "dev": true, + "requires": { + "extract-zip": "~1.5.0", + "fs-extra": "~0.26.4", + "hasha": "^2.2.0", + "kew": "~0.7.0", + "progress": "~1.1.8", + "request": "~2.67.0", + "request-progress": "~2.0.1", + "which": "~1.2.2" + }, + "dependencies": { + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.1.0" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz", + "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "recast": { + "version": "0.10.33", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.10.33.tgz", + "integrity": "sha1-lCgI96oBbx+nFCxGHX5XBKqo1pc=", + "dev": true, + "requires": { + "ast-types": "0.8.12", + "esprima-fb": "~15001.1001.0-dev-harmony-fb", + "private": "~0.1.5", + "source-map": "~0.5.0" + }, + "dependencies": { + "ast-types": { + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.8.12.tgz", + "integrity": "sha1-oNkOQ1G7iHcWyD/WN+v4GK9K38w=", + "dev": true + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator": { + "version": "0.8.40", + "resolved": "https://registry.npmjs.org/regenerator/-/regenerator-0.8.40.tgz", + "integrity": "sha1-oORXxY69uuV1yfjNdRJ+k3VkNdg=", + "dev": true, + "requires": { + "commoner": "~0.10.3", + "defs": "~1.1.0", + "esprima-fb": "~15001.1001.0-dev-harmony-fb", + "private": "~0.1.5", + "recast": "0.10.33", + "through": "~2.3.8" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexpu/-/regexpu-1.3.0.tgz", + "integrity": "sha1-5TTcmRqeWEYFDJjebX3UpVyeoW0=", + "dev": true, + "requires": { + "esprima": "^2.6.0", + "recast": "^0.10.10", + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + } + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", + "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.67.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz", + "integrity": "sha1-ivdHgOK/EeoK6aqWXBHxGv0nJ0I=", + "dev": true, + "requires": { + "aws-sign2": "~0.6.0", + "bl": "~1.0.0", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~1.0.0-rc3", + "har-validator": "~2.0.2", + "hawk": "~3.1.0", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "node-uuid": "~1.4.7", + "oauth-sign": "~0.8.0", + "qs": "~5.2.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.2.0", + "tunnel-agent": "~0.4.1" + } + }, + "request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "dev": true, + "requires": { + "throttleit": "^1.0.0" + } + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-fmt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", + "integrity": "sha1-GRv1ZqWeZTBILLJatTtKjchcOms=", + "dev": true + }, + "simple-is": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", + "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.2.10.tgz", + "integrity": "sha1-6lo5AKHByyUJagrozFwrSxDe09w=", + "dev": true, + "requires": { + "source-map": "0.1.32" + }, + "dependencies": { + "source-map": { + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz", + "integrity": "sha1-yLbBZ3l7pHQKjqMyUhYv8IWRsmY=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringmap": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", + "integrity": "sha1-VWwTeyWPlCuHdvWy71gqoGnX0bE=", + "dev": true + }, + "stringset": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz", + "integrity": "sha1-7yWcTjSTRDd/zRyRPdLoSMnAQrU=", + "dev": true + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "temporary": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/temporary/-/temporary-0.0.8.tgz", + "integrity": "sha1-oYqYHSi6jKNgJ/s8MFOMPst0CsA=", + "dev": true, + "requires": { + "package": ">= 1.0.0 < 1.2.0" + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tiny-lr-fork": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/tiny-lr-fork/-/tiny-lr-fork-0.0.5.tgz", + "integrity": "sha1-Hpnh4qhGm3NquX2X7vqYxx927Qo=", + "dev": true, + "requires": { + "debug": "~0.7.0", + "faye-websocket": "~0.4.3", + "noptify": "~0.0.3", + "qs": "~0.5.2" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + }, + "qs": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", + "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", + "dev": true + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "tough-cookie": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", + "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "try-resolve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/try-resolve/-/try-resolve-1.0.1.tgz", + "integrity": "sha1-z95vq9ctY+V5fPqrhzq76OcA6RI=", + "dev": true + }, + "tryor": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz", + "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "uri-path": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-0.0.2.tgz", + "integrity": "sha1-gD6wHy/rF5J9zOD2GH5yt19T9VQ=", + "dev": true + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.27.0.tgz", + "integrity": "sha1-ISBUaTFuk5Ex1Z8toMbX+YIh6kA=", + "dev": true, + "requires": { + "camelcase": "^1.2.1", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "os-locale": "^1.4.0", + "window-size": "^0.1.2", + "y18n": "^3.2.0" + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } + } + } +} diff --git a/apps/aklub/static/aklub/js/template_widget/package.json b/apps/aklub/static/aklub/js/template_widget/package.json new file mode 100644 index 000000000..d20eca7e7 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/package.json @@ -0,0 +1,27 @@ +{ + "name": "HtmlTemplateEditorDialog", + "description": "Html template editor dialog", + "version": "0.0.1", + "keywords": [ + "wysiwyg" + ], + "author": { + "name": "Tomas Zigo", + "email": "tomas.zigo@slovanet.sk" + }, + "main": "build/template_widget.js", + "devDependencies": { + "es6-promise": "^3.2.1", + "es6-require": "^0.2.1", + "grunt": "~0.4.5", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-jasmine": "^0.9.2", + "grunt-contrib-uglify": "^0.7.0", + "grunt-contrib-watch": "^0.6.1" + }, + "scripts": { + "test": "grunt jasmine --verbose" + } +} diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee new file mode 100644 index 000000000..bdcf708fc --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee @@ -0,0 +1,42 @@ +$(document).ready -> + + getTemplateNameDialogShowDelay = 500 + showPopoverDialog = 750 + showPopoverDialogDelay = 3000 + popoverDialogBackdrop = true + + htmlTemplateFieldWidget = new HtmlTemplateFormFieldWidget( + 'edit_template_dialog', + 'edit_template', + getTemplateNameDialogShowDelay, + 'id_template_name', + 'id_template_type', + 'id_hidden_template', + 'field-template', + 'field-template_textarea', + 'id_template_textarea', + 'template_page', + 'id_template', + showPopoverDialog, + popoverDialogBackdrop, + showPopoverDialogDelay, + 'edit_template_btn' + ) + + htmlTemplateEnFieldWidget = new HtmlTemplateFormFieldWidget( + 'edit_template_en_dialog', + 'edit_template_en', + getTemplateNameDialogShowDelay, + 'id_template_en_name', + 'id_template_en_type', + 'id_hidden_template_en', + 'field-template_en', + 'field-template_en_textarea', + 'id_template_en_textarea', + 'template_en_page', + 'id_template_en', + showPopoverDialog, + popoverDialogBackdrop, + showPopoverDialogDelay, + 'edit_template_en_btn' + ) diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee new file mode 100644 index 000000000..6847a94fd --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee @@ -0,0 +1,105 @@ +### + EditTemplateModalDialog class +### + +class EditTemplateModalDialog extends FormatSelectorMixin + + constructor: ( + dialogId, + popoverEditTemplateLinkElementId, + $editTemplateModalDialogPageContainer, + $hiddenTemplateField, + $templateDivField, + ) -> + + @_dialogId = dialogId + + @_sessionStorageKey = 'dbTemplateName' + + @_popoverEditTemplateLinkElementId = popoverEditTemplateLinkElementId + + @_closeBtnClass = 'mdl-close' + + @_$editTemplateModalDialogPageContainer = \ + $editTemplateModalDialogPageContainer + + @_$hiddenTemplateField = $hiddenTemplateField + + @_$templateDivField = $templateDivField + + @_popoverDialog = null + + mount: (templateName, openType, templateType) -> + + sessionStorage.removeItem @_sessionStorageKey + + # if openType is 'openViaSelectBox' + + if !@_popoverDialog + # Append popover edit dialog content + # needed for correct dialog initialization + @_popoverDialog = PopoverModalDialog.getContent( + @_popoverEditTemplateLinkElementId, + @getIdFormat @_dialogId + ) + + $('body').append @_popoverDialog + + # Initialize dialog + @init @getIdFormat @_popoverEditTemplateLinkElementId + + # Set html template page container attr + if templateType.length > 1 + url = @_setPageContainerArgs templateName, \ + 'aklub:get_email_template_from_db' + sessionStorage.setItem @sessionStorageKey, url + else + url = @_setPageContainerArgs templateName, 'aklub:get_email_template' + + # Show dialog + @show @getIdFormat @_dialogId + + # Bind close modal template dialog event + $closeBtn = $ "#{ @getIdFormat @_dialogId } + #{ @getClassFormat @_closeBtnClass }" + $closeBtn.off 'click', @_closeEditTemplateModalDialog + $closeBtn.on 'click', @_closeEditTemplateModalDialog + + # Remove popover edit dialog content + if openType is 'openViaSelectBox' + $(@getIdFormat @_popoverEditTemplateLinkElementId).remove() + + _closeEditTemplateModalDialog: (evt) => + + templatePageHtlmDoc = @_getIframeTemplateContent() + + processHtmlTemplate = new PostProcessHtmlTemplate( + templatePageHtlmDoc, + $(templatePageHtlmDoc).find('article'), + @_$templateDivField, + @_$hiddenTemplateField + ) + + _getIframeTemplateContent: () -> + templatePage = document.querySelector @getIdFormat \ + @_$editTemplateModalDialogPageContainer.attr 'id' + templatePage?.contentDocument + + _setPageContainerArgs: (templateName, urlName) -> + # Set html template page container attr + url = window.reverse urlName, {template_name: templateName} + + attr = + data: url, + width: $(window).width(), + height: $(window).height() + + @_$editTemplateModalDialogPageContainer.attr attr + + return url + + init: (elementId) -> + $(elementId).mdl() + + show: (elementId) -> + mdl_open(elementId) diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee new file mode 100644 index 000000000..3f8b59f75 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee @@ -0,0 +1,197 @@ +### + GetTemplateNameDialog class +### + +class GetTemplateNameDialog extends FormatSelectorMixin + + constructor: ( + showDelay, + $templateDivField, + templateNameFieldId, + editTemplateModalDialogId, + popoverModalDialogEditTemplateLinkElementId, + templateName, + $editTemplateModalDialogPageContainer, + $hiddenTemplateField + ) -> + + @_dialogId = 'get_template_name' + + @_title = gettext('Template name') + + @_showDelay = showDelay + + @_confirmBtnId = 'confirm_btn' + + @_$templateDivField = $templateDivField + + @_templateNameInputId = 'template_name' + + @_errorListCSSClass = 'errorlist' + + @_templateNameFieldId = templateNameFieldId + + @_newEmptyTemplateName = null + + @_editTemplateModalDialogId = editTemplateModalDialogId + + @_popoverModalDialogEditTemplateLinkElementId = \ + popoverModalDialogEditTemplateLinkElementId + + @_templateName = templateName + + @_$editTemplateModalDialogPageContainer = \ + $editTemplateModalDialogPageContainer + + @_$hiddenTemplateField = $hiddenTemplateField + + unmount: () -> + $(@getIdFormat @_dialogId).dialog('destroy') + + mount: () -> + @_getContainer().dialog @_getOpt() + + _getContainer: () -> + divContainer = $ '
    ' + divContainer.attr {id: @_dialogId} + + form = $ '
    ' + form.attr {id: 'template_name_form'} + + $formContentContainer = $ '
    ' + + $formErrorListContainer = $ '
      ' + $formErrorListContainer.attr {class: @_errorListCSSClass} + + $formContentLabel = $ '' + $formContentLabel.attr {for: @_templateNameInputId} + $formContentLabel.text gettext 'Template name' + + $formContentInput = $ '' + $formContentInput.attr + id: @_templateNameInputId, + name: @_templateNameInputId + + $formContentContainer.append($formContentLabel) + $formContentContainer.append($formContentInput) + $formContentContainer.append($formErrorListContainer) + + divContainer.append(form.append($formContentContainer)) + + return divContainer + + getNeEmptyTemplateName: () -> + @_newEmptyTemplateName + + _triggerConfirmBtn: () -> + # Trigger click on confirm btn + triggerEvt = (evt) => + if evt.keyCode is $.ui.keyCode.ENTER + $(@getIdFormat @_confirmBtnId).click() + + $(@getIdFormat @_dialogId).keypress triggerEvt + + _checkRegexp: (o, regexp, m) -> + if not regexp.test(o.val()) + errorList = o.closest('form').find @getClassFormat @_errorListCSSClass + errorList.append "
    • #{ m }
    • " + return false + else + return true + + _checkTemplateName: (o, name, m) -> + opts = [] + selector = "#{ @getIdFormat @_templateNameFieldId } > option" + $(selector).each () -> opts.push($(@).text()) + + if opts.indexOf(name) > -1 + errorList = o.closest('form').find @getClassFormat @_errorListCSSClass + errorList.append("
    • #{ m }
    • ") + return false + else + return true + + _validateTemplateName: () -> + # Validate template name + valid = true + $templateName = $ @getIdFormat @_templateNameInputId + $(@getClassFormat @_errorListCSSClass).html '' + + message = gettext('Template name may consist of a-z, 0-9, + underscores, not spaces and must begin with a letter, + and be lowercase.') + valid = valid and @_checkRegexp $templateName, \ + /^[a-z]([0-9a-z_])+$/, message + + message = gettext 'Template name exist.' + $templateNameInput = $ @getIdFormat @_templateNameInputId + + valid = valid and @_checkTemplateName $templateNameInput, \ + $templateName.val(), message + + return valid + + _confirmTemplateName: () => + valid = @_validateTemplateName() + + if valid + @_newEmptyTemplateName = $(@getIdFormat @_templateNameInputId).val() + + # Destroy dialog + @unmount() + + @_setNewEmptyTemplateName() + + templateType = [@_newEmptyTemplateName] + + # Show edit template modal dialog + dialog = new EditTemplateModalDialog( + @_editTemplateModalDialogId, + @_popoverModalDialogEditTemplateLinkElementId, + @_$editTemplateModalDialogPageContainer, + @_$hiddenTemplateField, + @_$templateDivField + ) + + dialog.mount @_templateName, 'openViaSelectBox', templateType + + _setNewEmptyTemplateName: () -> + if @_newEmptyTemplateName? + sessionStorage.setItem('newEmptyTemplateName', @_newEmptyTemplateName) + + _getBtns: () => + # Get dialog buttons + btnNames = {} + + okBtnClick = () => + @_confirmTemplateName() + + btnNames.ok = + text: gettext('Ok'), + id: @_confirmBtnId, + click: okBtnClick + + return btnNames + + _getOpt: () -> + # Get dialog option + + open = (evt, ui) => + @_triggerConfirmBtn() + + close = (evt, ui) => + @unmount() + @_$templateDivField.html('') + + opt = + modal: true, + title: @_title, + show: + effect: 'fade', + delay: @_showDelay, + resizable: false + open: open, + beforeClose: close + buttons: @_getBtns() + + return opt diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee new file mode 100644 index 000000000..4689a5658 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee @@ -0,0 +1,376 @@ +### + HtmlTemplateFormFieldWidget class +### + +class HtmlTemplateFormFieldWidget extends FormatSelectorMixin + + constructor: ( + editTemplateModalDialogId, + popoverModalDialogEditTemplateLinkElementId, + getTemplateNameDialogShowDelay, + templateNameFieldId, + templateTypeFieldId, + hiddenTemplateFieldId, + templateDivFormFieldContainerCSSClass, + templateTextAreaFormFieldContainerCSSClass, + templateTextareaFieldId, + editTemplateModalDialogPageContainerId, + templateDivFieldId, + showPopoverModalDialogDelay, + popoverModalDialogBackDrop, + popoverModalDialogDestroyDelay, + editTemplateBtnName + ) -> + + @_templateDivFieldId = templateDivFieldId + + @_hiddenTemplateFieldId = hiddenTemplateFieldId + + @_templateNameFieldId = templateNameFieldId + + @_getTemplateNameDialogShowDelay = getTemplateNameDialogShowDelay + + @_editTemplateModalDialogId = editTemplateModalDialogId + + @_popoverModalDialogEditTemplateLinkElementId = \ + popoverModalDialogEditTemplateLinkElementId + + @_templateDivFormFieldContainerCSSClass = \ + templateDivFormFieldContainerCSSClass + + @_templateTextareaFieldId = templateTextareaFieldId + + @_editTemplateModalDialogPageContainerId = \ + editTemplateModalDialogPageContainerId + + @_$templateDivFieldContainer = null + + @_$templateTextareaFieldContainer = null + + @_templateTextareaFieldValue = '' + + @_showPopoverModalDialogDelay = showPopoverModalDialogDelay + + @_popoverModalDialogBackDrop = popoverModalDialogBackDrop + + @_popoverModalDialogDestroyDelay = popoverModalDialogDestroyDelay + + @_templateTextAreaFormFieldContainerCSSClass = \ + templateTextAreaFormFieldContainerCSSClass + + @_templateTypeFieldId = templateTypeFieldId + + @_newEmptyTemplateName = null + + @_getTemplateNameDialog = null + + @_popoverDialogOpts = null + + @_editTemplateBtnName = editTemplateBtnName + + @_editTemplateModalDialog = null + + # Methods + @_cacheDom() + + @_bindEvents() + + # @_bindPopoverDialogMouseMoveEvt() + + @_templateFormFieldWidgetInit() + + _cacheDom: () -> + @_$templateDivField = $ @getIdFormat @_templateDivFieldId + @_$hiddenTemplateField = $ @getIdFormat @_hiddenTemplateFieldId + @_$templateNameField = $ @getIdFormat @_templateNameFieldId + @_$templateTypeField = $ @getIdFormat @_templateTypeFieldId + @_$templateDivFormFieldContainer = $ @getClassFormat \ + @_templateDivFormFieldContainerCSSClass + @_$templateTextAreaFormFieldContainer = $ @getClassFormat \ + @_templateTextAreaFormFieldContainerCSSClass + @_$templateTextareaField = $ @getIdFormat @_templateTextareaFieldId + @_$editTemplateModalDialogPageContainer = $ @getIdFormat \ + @_editTemplateModalDialogPageContainerId + @_$editTemplateBtn = $ @getNameFormat 'input', @_editTemplateBtnName + + _bindEvents: () -> + @_$templateNameField.bind 'change', @_showEditTemplateModalDialog + @_$templateTypeField.bind 'change', @_replaceTemplateFieldContent + @_$editTemplateModalDialogPageContainer.bind 'load', @_loadTemplateContent + @_$editTemplateBtn.bind 'click', @_editTemplateBtnClickEvt + + _templateFormFieldWidgetInit: () -> + + @_$templateTextAreaFormFieldContainer.addClass('hidden') + + if @_$templateTypeField.val() is 'new' + + @_exchangeFormFieldContainerContent() + + # Copy value from template form field div widget to textarea and + # save as global value + textareaValue = @_$templateDivField.html() + @_$templateTextareaField.html textareaValue + @_templateTextareaFieldValue = textareaValue + + @_$templateDivField.html '' + + else if @_$templateTypeField.val() is 'existed' + # Copy value from template form field div widget into hidden field + # after form page loaded + @_$hiddenTemplateField.val @_$templateDivField.html() + + # Init edit template modal dialog + @_initEditTemplateModalDialog() + + # Disable edit btn if empty template name + @_enableDisableEditBtn(true) + + _enableDisableEditBtn: (state, checkValue=true) -> + + if checkValue + value = @_$templateNameField.find('option:selected').val() + + if value.length == 0 + @_$editTemplateBtn.prop('disabled', state) + + else + @_$editTemplateBtn.prop('disabled', state) + + _getTemplateFieldContainer: () -> + @_$templateDivFieldContainer = @_$templateDivFormFieldContainer + @_$templateTextareaFieldContainer = @_$templateTextAreaFormFieldContainer + return + + _checkTemplateFieldWidget: () -> + @_getTemplateFieldContainer() + textareaWidget = @_$templateDivFieldContainer.find('textarea') + + if textareaWidget.length + return 'textarea' + else + return 'div' + + _replaceTemplateFieldContent: (evt) => + templateType = $(evt.target).val() + + if templateType is 'new' + if @_checkTemplateFieldWidget() isnt 'textarea' + @_exchangeFormFieldContainerContent() + else if templateType is 'existed' + if @_checkTemplateFieldWidget() isnt 'div' + @_exchangeFormFieldContainerContentBack() + return + + _exchangeFormFieldContainerContentBack: () -> + # Replace 'template' textarea form field widget container + # childrens with childrens from the 'template_textarea' div + # form field widget container + + @_$templateNameField.attr('disabled', false) + + # Exchange form field container childrens + @_exchangeContent() + + # Replace template div form field widget id + attr = + id: @_templateTextareaFieldId + name: @_templateTextareaFieldId.slice(3) + + @_$templateDivField.attr attr + + # Replace template textarea form field widget id + attr = + id: @_templateDivFieldId + name: @_templateDivFieldId.slice(3) + + @_$templateTextareaField.attr attr + + # @_bindPopoverDialogMouseMoveEvt() + + @_$editTemplateBtn.off 'click', @_editTemplateBtnClickEvt + @_$editTemplateBtn.on 'click', @_editTemplateBtnClickEvt + + delay: (ms, func) -> setTimeout func, ms + + _mouseMoveOverTemplateEvt: (evt) => + # Mouse move event over template content + # Initialize popover dialog + offset = $(evt.currentTarget).offset() + relX = evt.pageX - offset.left + relY = evt.pageY - offset.top + offsetTop = relY + offsetLeft = relX + + @delay @_showPopoverModalDialogDelay, => \ + @_initPopoverModalDialog offsetTop, offsetLeft + + _initPopoverModalDialog: (offsetTop, offsetLeft) -> + @_popoverDialogOpts.mount(offsetTop, offsetLeft) + + _bindPopoverDialogMouseMoveEvt: () -> + + @_popoverDialogOpts = new PopoverModalDialog( + gettext('Edit template'), + @_popoverModalDialogBackDrop, + @_editTemplateModalDialogId, + @_$templateNameField, + @_popoverModalDialogDestroyDelay, + @_$templateDivField, + @_popoverModalDialogEditTemplateLinkElementId, + @_$editTemplateModalDialogPageContainer, + @_$hiddenTemplateField, + ) + + @_$templateDivField.mousemove(@_mouseMoveOverTemplateEvt) + + _initEditTemplateModalDialog: () -> + + @_editTemplateModalDialog = new EditTemplateModalDialog( + @_editTemplateModalDialogId, + @_popoverModalDialogEditTemplateLinkElementId, + @_$editTemplateModalDialogPageContainer, + @_$hiddenTemplateField, + @_$templateDivField, + ) + + _editTemplateBtnClickEvt: (evt) => + + evt.preventDefault() + + templateNameSelectedOpt = @_$templateNameField.find('option:selected') + templateName = templateNameSelectedOpt.text() + templateType = templateNameSelectedOpt.val().split(':') + + # Show edit template modal dialog + @_editTemplateModalDialog.mount templateName, 'openViaBtn', templateType + + _exchangeFormFieldContainerContent: () -> + # Replace 'template' div form field widget container + # childrens with childrens from the 'template_textarea' textarea + # form field widget container + + # Set initial template form field textarea widget value + @_$templateTextareaField.html(@_templateTextareaFieldValue) + + @_$templateNameField.attr('disabled', true) + + # Exchange form field container childrens + @_exchangeContent() + + attr = + id: @_templateTextareaFieldId + name: @_templateTextareaFieldId.slice(3) + + @_$templateDivField.attr attr + + # Replace template div form field widget label + @_$templateDivFieldContainer.find('label').attr 'for', @_templateDivFieldId + @_$templateDivFieldContainer.find('label').text \ + @_$templateTextareaFieldContainer.find('label').text() + + # Replace template textarea form field widget id + attr = + id: @_templateDivFieldId + name: @_templateDivFieldId.slice(3) + + @_$templateTextareaField.attr attr + + @_$templateTextAreaFormFieldContainer.addClass('hidden') + + # Erase hidden template form field + @_$hiddenTemplateField.val('') + + _exchangeContent: () -> + @_getTemplateFieldContainer() + + # Replace form field container content + $templateDivFieldContainerChildren = @_$templateDivFieldContainer.children() + $templateTextareaFieldContainerChildren = \ + @_$templateTextareaFieldContainer.children() + + @_$templateDivFieldContainer .html($templateTextareaFieldContainerChildren) + @_$templateTextareaFieldContainer.html($templateDivFieldContainerChildren) + + return + + _showEditTemplateModalDialog: (evt) => + sessionStorage.removeItem('newEmptyTemplateName') + + targetElementId = "#{ @getIdFormat evt.target.id }" + templateName = $("#{ targetElementId } option:selected").text() + + if $("#{ targetElementId }").val().length > 0 + + if templateName is 'new_empty_template' + + # Init get template name dialog + @_getTemplateNameDialog = new GetTemplateNameDialog( + @_getTemplateNameDialogShowDelay, + @_$templateDivField, + @_templateNameFieldId, + @_editTemplateModalDialogId, + @_popoverModalDialogEditTemplateLinkElementId, + templateName, + @_$editTemplateModalDialogPageContainer, + @_$hiddenTemplateField, + ) + + @_getTemplateNameDialog.mount() + + else + + # Enable edit btn if empty template name + @_enableDisableEditBtn(false, false) + + templateType = @_$templateNameField.find( + 'option:selected').val().split ':' + + dialog = new EditTemplateModalDialog( + @_editTemplateModalDialogId, + @_popoverModalDialogEditTemplateLinkElementId, + @_$editTemplateModalDialogPageContainer, + @_$hiddenTemplateField, + @_$templateDivField + ) + + dialog.mount templateName, 'openViaSelectBox', templateType + + _loadTemplateContent: (evt) => + @_newEmptyTemplateName = @_getTemplateNameDialog?.getNeEmptyTemplateName() + if @_newEmptyTemplateName? + # Insert new template name into select template name options + @_addEditTemplateConfirmEvent() + + _addEditTemplateConfirmEvent: () -> + confirmEditBtnClass = 'ct-ignition__button--confirm' + + templatePage = document.querySelector \ + @getIdFormat @_editTemplateModalDialogPageContainerId + htmlDocument = templatePage.contentDocument + + confirmEditBtn = $(htmlDocument).find @getClassFormat confirmEditBtnClass + confirmEditBtn.unbind('click') + confirmEditBtn.bind('click', @_confirmTemplateEdit) + + _confirmTemplateEdit: () => + if @_newEmptyTemplateName? + # Insert new template name into select template name options + @_addNewSelectOpt(@_newEmptyTemplateName, @_$templateNameField) + + # Erase template name + @_eraseNewTemplateName() + + _eraseNewTemplateName: () -> + @_newEmptyTemplateName = null + + _addNewSelectOpt: (text, element) -> + options = $("#{ @getIdFormat @_templateNameFieldId } option") + values = $.map options, (option) -> option.value + + value = "new_empty_template:#{ text }" + + if values.indexOf(value) == -1 + opt = new Option(text, value) + element.append(opt) + element.val(value) diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/mixins.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/mixins.coffee new file mode 100644 index 000000000..5bb6731b4 --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/mixins.coffee @@ -0,0 +1,14 @@ +### + FormatSelectorMixin class +### + +class FormatSelectorMixin + + getIdFormat: (id) -> + "##{ id }" + + getClassFormat: (className) -> + ".#{ className }" + + getNameFormat: (element, name) -> + "#{ element }[name='#{ name }']" \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee new file mode 100644 index 000000000..669d583ab --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee @@ -0,0 +1,126 @@ +### + PopoverModalDialog class +### + +class PopoverModalDialog extends FormatSelectorMixin + + @editTemplateLinkText = gettext('Edit template') + + constructor: ( + title, + backdrop, + editTemplateModalDialogId, + $templateNameField, + destroyDelay, + $mountElement, + popoverModalDialogEditTemplateLinkElementId, + $editTemplateModalDialogPageContainer, + $hiddenTemplateField, + ) -> + + @_title = title + + @_editTemplateModalDialogId = editTemplateModalDialogId + + @_backdrop = backdrop + + @_$templateNameField = $templateNameField + + @_destroyDelay = destroyDelay + + @_$mountElement = $mountElement + + @_popoverModalDialogEditTemplateLinkElementId = \ + popoverModalDialogEditTemplateLinkElementId + + @_$editTemplateModalDialogPageContainer = \ + $editTemplateModalDialogPageContainer + + @_$hiddenTemplateField = $hiddenTemplateField + + @_$templateDivField = $mountElement + + + show: (element) -> + + element.click (evt) => + + evt.preventDefault() + + templateNameSelectedOpt = @_$templateNameField.find('option:selected') + templateName = templateNameSelectedOpt.text() + templateType = templateNameSelectedOpt.val().split(':') + + # Show edit template modal dialog + dialog = new EditTemplateModalDialog( + @_editTemplateModalDialogId, + @_popoverModalDialogEditTemplateLinkElementId, + @_$editTemplateModalDialogPageContainer, + @_$hiddenTemplateField, + @_$templateDivField + ) + + dialog.mount templateName, 'openViaPopoverDialog', templateType + + # Destroy popover dialog + @unmount() + + # Destroy dialog + @delay @_destroyDelay, => @unmount() + + mount: (offsetTop, offsetLeft) -> + + opts = @_getOpt() + opts.offsetTop = offsetTop + opts.offsetLeft = offsetLeft + + WebuiPopovers.show(@_$mountElement, opts) + + unmount: () -> + + @_$mountElement.webuiPopover('hide') + @_$mountElement.webuiPopover('destroy') + + delay: (ms, func) -> setTimeout func, ms + + @getContent: ( + popoverModalDialogEditTemplateLinkElementId, + editTemplateModalDialogId, + ) -> + + $element = $ '' + $element.attr + id: @dialogId, + href: '' + + editTemplateModalDialogAttr = + id: popoverModalDialogEditTemplateLinkElementId, + 'data-type': 'modal', + 'data-target': editTemplateModalDialogId, + 'data-fullscreen': 'true', + 'data-overlayClick': 'true' + + $element.attr editTemplateModalDialogAttr + $element.html "#{ @editTemplateLinkText }" + return $element + + _getOpt: () -> + + show = (element) => + @show element + + content = PopoverModalDialog.getContent( + @_popoverModalDialogEditTemplateLinkElementId, + @getIdFormat @_editTemplateModalDialogId + ) + + opt = + placement: 'top-right', + trigger: 'manual,' + title: @_title, + content: content, + offsetTop: 0, + offsetLeft: 0, + animation: 'fade' + backdrop: @_backdrop, + onShow: show diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee new file mode 100644 index 000000000..f5030588b --- /dev/null +++ b/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee @@ -0,0 +1,432 @@ +### + PostProcessHtmlTemplate class +### + +class PostProcessHtmlTemplate extends FormatSelectorMixin + + constructor: ( + htmlDoc, + $editTemplatePageContainer, + $templateDivField, + $hiddenTemplateField + ) -> + + @_htmlDoc = htmlDoc + + @_$editTemplatePageContainer = $editTemplatePageContainer + + @_$templateDivField = $templateDivField + + @_$hiddenTemplateField = $hiddenTemplateField + + @_videoClass = 'video' + + @_verticalSpaceClassName = 'vertical-space' + + @_textContainerTags = [ + 'p', + 'ul', + 'ol', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6' + ] + + # Methods + @convertVideoIframeToImgThumbnail() + + convertVideoIframeToImgThumbnail: () -> + + promises = [] + hasVideo = false + + youtubeRegex = new RegExp 'youtube.com.*(v=|/embed/)(.{11})' + + vimeoRegex = new RegExp 'vimeo.com.*./(.*)' + + addThumbnail = ( + $videoThumbnail, + $clickableVideoThumbnail, + $videoCaption, + element, + iframeCssFloatProperty + ) -> + + $videoThumbnail.css 'float', iframeCssFloatProperty + + # Add video thumbnail + $clickableVideoThumbnail.append $videoThumbnail + + # Add video thumbnail figcaption + $clickableVideoThumbnail.append $videoCaption + + $(element).after $clickableVideoThumbnail + + # Remove video iframe + $(element).remove() + + success = (data) -> + # videoThumbnailSrc = data[0].thumbnail_large + # attr = + # 'src': videoThumbnailSrc + + if @videoType is 'vimeo' + + attr = + src: data['thumbnail_url'], + width: data['thumbnail_width'], + height: data['thumbnail_height'] + + @$videoThumbnail.attr attr + + @$videoCaption.text data['title'] + + addThumbnail( + @$videoThumbnail, + @$clickableVideoThumbnail, + @$videoCaption, + @element, + @iframeCssFloatProperty + ) + + $(@_htmlDoc).find('iframe').each (i, element) => + + iframeSrc = $(element).attr 'src' + iframeCssFloatProperty = $(element).css 'float' + iframeWidth = $(element).attr 'width' + iframeHeight = $(element).attr 'height' + + $clickableVideoThumbnail = $ '' + $clickableVideoThumbnail.attr 'href', iframeSrc + + $videoThumbnail = $ '' + $videoThumbnail.attr({ + width: iframeWidth, + height: iframeHeight + }) + + $videoCaption = $ '
      ' + css = + 'text-align': 'center', + 'font-size': '12px' + $videoCaption.css css + + if iframeSrc.match(youtubeRegex) + + hasVideo = true + + videoId = iframeSrc.match(youtubeRegex).pop() + $clickableVideoThumbnail.attr 'id', videoId + $clickableVideoThumbnail.addClass 'youtube' + + videoThumbnailSrc = "//img.youtube.com/vi/#{ videoId }/0.jpg" + $videoThumbnail.attr 'src', videoThumbnailSrc + + subUrl = "http://www.youtube.com/watch?v=#{ videoId }&" + url = "https://noembed.com/embed?url=#{ subUrl }" + + context = + $videoThumbnail: $videoThumbnail, + $clickableVideoThumbnail: $clickableVideoThumbnail, + $videoCaption: $videoCaption, + element: element, + iframeCssFloatProperty: iframeCssFloatProperty, + videoType: 'youtube' + + promises.push $.ajax @getAjaxData context, url, success, \ + type='GET', dataType='jsonp' + + else if iframeSrc.match(vimeoRegex) + + hasVideo = true + + videoId = iframeSrc.match(vimeoRegex).pop() + + $clickableVideoThumbnail.attr 'id', videoId + $clickableVideoThumbnail.addClass 'vimeo' + + ### + Get embed player info + Player video thumbnail size != iFrame size + Use embed player video thumbnail size + https://developer.vimeo.com/api/oembed/videos + ### + + subUrl = "https://vimeo.com/#{ videoId }& + width=#{ iframeWidth }&height=#{ iframeHeight }" + url = "https://vimeo.com/api/oembed.json?url=#{ subUrl }" + + context = + $videoThumbnail: $videoThumbnail, + $clickableVideoThumbnail: $clickableVideoThumbnail, + $videoCaption: $videoCaption, + element: element, + iframeCssFloatProperty: iframeCssFloatProperty, + videoType: 'vimeo' + + promises.push $.ajax @getAjaxData context, url, success, \ + type='GET', dataType='jsonp' + + # Wait for an all ajax request done + $.when.apply(null, promises).done () => + @convertCssToInlineStyle() + + getAjaxData: (context, url, success, type='GET', dataType='json') -> + ajaxData = + type: type, + url: url + jsonp: 'callback', + dataType: dataType, + context: context, + success: success + + convertCssToInlineStyle: () -> + + @inlineStyler $(@_htmlDoc) + @_fixContentWidth() + @_fixMargin() + @_replaceImgSrc() + @_convertTextCssPositionToTable() + @_eraseVerticalSpaceContent() + @_$templateDivField.html @_$editTemplatePageContainer + @_$hiddenTemplateField.val @_$editTemplatePageContainer.html() + + inlineStyler: ($element) -> + $element.inlineStyler() + + getElementPosition: (align, width='', heigth='') -> + + $table = $('
      ') + + $table.attr({ + cellspacing: 0, + cellpadding: 0, + border: 0, + width: '100%' + }) + + $tableBody = $('') + $tableRow = $('') + $tableCell = $('') + + $tableCell.css({ + border: 'none', + padding: 0, + margin: 0 + }) + + $tableCell.attr({ + align: align, + width: width, + heigth: heigth + }) + + $table.append $tableBody.append $tableRow.append $tableCell + + _replaceImgSrc: () -> + + $imgs = @_$editTemplatePageContainer.find 'img' + + $imgs.each (i, element) => + + $clickableVideoThumbnail = $(element).closest @getClassFormat @_videoClass + $img = if $clickableVideoThumbnail.length > 0 \ + then $clickableVideoThumbnail else $(element) + + if $clickableVideoThumbnail.length == 0 + src = $(element).attr('src') + + regexPattern = new RegExp(window.origin) + + if not src.match(regexPattern) + imgSrc = "#{ window.origin }#{ $(element).attr('src') }" + $(element).attr('src', imgSrc) + + @convertImageCssPositionToTable $img + + convertImageCssPositionToTable: ($img) -> + + $img = if $img.find('img').length > 0 then $img.find 'img' else $img + + marginLeft = parseInt( + $img.css('margin-left').split('.')[0].replace('px', '') + ) + marginRight = parseInt( + $img.css('margin-right').split('.')[0].replace('px', '') + ) + floatPosition = $img.css 'float' + $img.css 'display', '' + + if (marginLeft is marginRight) and (floatPosition is 'none') + $tableWrapper = @getElementPosition('center', '', '') + + else if marginLeft > marginRight + + $tableWrapper = @getElementPosition( + floatPosition, + $img.attr('width'), + $img.attr('height') + ) + $tableCell = @convertImgFloatCssPositionToTable( + $img, + @_$editTemplatePageContainer, + floatPosition + ) + + else if marginRight > marginLeft + + $tableWrapper = @getElementPosition( + floatPosition, + $img.attr('width'), + $img.attr('height') + ) + $tableCell = @convertImgFloatCssPositionToTable( + $img, + @_$editTemplatePageContainer, + floatPosition + ) + + else if (floatPosition is'left') or (floatPosition is'right') + + $tableWrapper = @getElementPosition( + floatPosition, + $img.attr('width'), + $img.attr('height') + ) + $tableCell = @convertImgFloatCssPositionToTable( + $img, + @_$editTemplatePageContainer, + floatPosition + ) + + videoClassSelector = @getClassFormat @_videoClass + $videoIframe = $img.closest(videoClassSelector) + + $tableWrapper.find('td').append( + if $videoIframe.length > 0 + $videoIframe.clone() + else $img.clone() + ) + + if floatPosition is 'left' + $tableWrapper.find('tr').append $tableCell + else + $tableWrapper.find('tr').prepend $tableCell + + if $videoIframe.length > 0 + $videoIframe.after $tableWrapper + else + $img.after $tableWrapper + + if $videoIframe.length > 0 + $videoIframe.remove() + else + $img.remove() + + return + + convertImgFloatCssPositionToTable: ($img, $editContainer, position) -> + padding = 15 + paddingValues = if position is 'left' then "0 0 0 #{ padding }px" \ + else "0 #{ padding }px 0 0" + + $tableCell = $('') + $tableCell.css({ + border: 'none', + padding: paddingValues, + margin: '0' + }) + + $videoIframe = $img.closest(@getClassFormat @_videoClass) + if $videoIframe.length > 0 + $contextElement = $videoIframe.next().clone() + else + $contextElement = $img.next().clone() + + editContainerWidth = parseFloat( + $editContainer.find('#content_table').width() + ) + contextWidth = editContainerWidth - parseFloat($img.attr('width')) - padding + $contextElement.css 'width', contextWidth + + $tableCell.attr 'width', contextWidth + $tableCell.append $contextElement + + if $videoIframe.length > 0 + $videoIframe.next().remove() + else + $img.next().remove() + + return $tableCell + + _convertTextCssPositionToTable: () -> + + containers = @_textContainerTags.join(', ') + + @_$editTemplatePageContainer.find(containers).each (i, element) => + + elementCssClass = if $(element).attr('class') \ + then $(element).attr('class').split(' ') else [] + + switch elementCssClass + when elementCssClass.indexOf('text-left') > -1 + $tableWrapper = @getElementPosition('left', '', '') + when elementCssClass.indexOf('text-right') > -1 + $tableWrapper = @getElementPosition('right', '', '') + when elementCssClass.indexOf('text-center') > -1 + $tableWrapper = @getElementPosition('center', '', '') + else + $tableWrapper = @getElementPosition('center', '', '') + + @adjustListCssProperties($(element)) + $tableWrapper.find('td').append($(element).clone()) + $(element).after($tableWrapper) + $(element).remove() + + adjustListCssProperties: ($element) -> + + if $element.is('ul') or $element.is('ol') + $element.css({ + margin: '10px' + padding: '15px' + }) + + _fixMargin: () -> + # Fix render margin (chromium render margin wrong) + $(@_htmlDoc).find('article').css('margin', '') + + _getNumberValue: (value, unitPosition=-2) -> + parseInt(value.slice(0, value.length - unitPosition)) + + _getPaddingValue: (horizontalValue=true) -> + + if horizontalValue is true + position = 'padding--left' + else + position = 'padding--top' + + paddingValue = $(@_htmlDoc).find( + '.article__content p').first().css(position) + + @_getNumberValue(paddingValue) + + _fixContentWidth: () -> + + paddingValue = @_getPaddingValue() + containers = @_textContainerTags.join(', ') + + $(@_htmlDoc).find('article').find(containers).each (index, value) => + origWidth = @_getNumberValue($(value).css('width')) + + # Set correct width (orig width - 2 * padding value) + $(value).css + width: origWidth - paddingValue * 2 + + _eraseVerticalSpaceContent: () -> + # Erase vertical space content + selector = @getClassFormat @_verticalSpaceClassName + @_$editTemplatePageContainer.find(selector).each (i, element) -> + $(element).find('center').remove() diff --git a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html index b20271725..bcdcd7a0c 100644 --- a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html +++ b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html @@ -14,75 +14,94 @@ {% endblock %} {% block extrahead %} - - {{ block.super }} - - {% wysiwyg_setup %} - + + {{ block.super }} + + {% endblock %} {% block extrastyle %} - - {{ block.super }} - - - + +{% load automat_fonts %} +{% get_automat_fonts %} + + {{ block.super }} + + + + {% include "email_templates/automat_fonts.html" %} + {% endblock %} {% block content %} - - {{ block.super }} - - {# Replace XXXXXXXX with field name: #} - {% wysiwyg_editor "id_template" %} - {% wysiwyg_editor "id_template_en" %} - - {% comment %} - {# ckeditor custom config example #} - {% wysiwyg_editor "id_XXXXXXXX" "id_XXXXXXXX_editor" "{ toolbar: 'compact_toolbar' }" %} - {% endcomment %} - - {# ... add more here... #} + + {{ block.super }} + + {# Replace XXXXXXXX with field name: #} + {% comment %} + {% wysiwyg_editor "id_template" %} + {% wysiwyg_editor "id_template_en" %} + {% endcomment %} + + {% comment %} + {# ckeditor custom config example #} + {% wysiwyg_editor "id_XXXXXXXX" "id_XXXXXXXX_editor" "{ toolbar: 'compact_toolbar' }" %} + {% endcomment %} + + {# ... add more here... #} + + {# Edit template field modal dialog #} +
      +
      + +
      +
      + {# Edit template_en field modal dialog #} +
      +
      + +
      +
      {% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_fonts.html b/apps/aklub/templates/email_templates/automat_fonts.html new file mode 100644 index 000000000..8304c132f --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_fonts.html @@ -0,0 +1,19 @@ +{% load static %} + + diff --git a/apps/aklub/templates/email_templates/automat_green_newsletter.html b/apps/aklub/templates/email_templates/automat_green_newsletter.html new file mode 100644 index 000000000..a0c856c68 --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_green_newsletter.html @@ -0,0 +1,128 @@ +{% extends 'email_templates/base.html' %} {% load static %} {% block content %} +
      + {% if header %} {% include "email_templates/header.html" %} {% endif %} + + + + + + +
      + {% with default_width="600" default_height="auto" %} + + {% endwith %} + + + + + +
      +
      + {% if article %} {{ article | safe }} {% else %} + + + + + +
      + +

      Title

      +

      + Text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text +

      +

      + Sample image +

      + + + + + + +
      + +

      + Text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text +

      + + + + + +
      + +

      + Sample video +

      + + {% endif %} +
      +
      +
      + {% if footer %} {% include "email_templates/footer.html" %} {% endif %} +
      +{% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_purple_newsletter.html b/apps/aklub/templates/email_templates/automat_purple_newsletter.html new file mode 100644 index 000000000..d7e8bbe79 --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_purple_newsletter.html @@ -0,0 +1,127 @@ +{% extends 'email_templates/base.html' %} {% load static %} {% block content %} +
      + {% if header %} {% include "email_templates/header.html" %} {% endif %} + + + + + + +
      + {% with default_width="600" default_height="auto" %} + + {% endwith %} + + + + + +
      +
      + {% if article %} {{ article | safe }} {% else %} + + + + + +
      + +

      Title

      +

      + Text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text +

      +

      + Sample image +

      + + + + + + +
      + +

      + Text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text +

      + + + + + +
      + +

      + Sample video +

      + + {% endif %} +
      +
      +
      + {% if footer %} {% include "email_templates/footer.html" %} {% endif %} +
      +{% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_red_newsletter.html b/apps/aklub/templates/email_templates/automat_red_newsletter.html new file mode 100644 index 000000000..a0c856c68 --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_red_newsletter.html @@ -0,0 +1,128 @@ +{% extends 'email_templates/base.html' %} {% load static %} {% block content %} +
      + {% if header %} {% include "email_templates/header.html" %} {% endif %} + + + + + + +
      + {% with default_width="600" default_height="auto" %} + + {% endwith %} + + + + + +
      +
      + {% if article %} {{ article | safe }} {% else %} + + + + + +
      + +

      Title

      +

      + Text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text +

      +

      + Sample image +

      + + + + + + +
      + +

      + Text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text text text text + text text text text text text text text +

      + + + + + +
      + +

      + Sample video +

      + + {% endif %} +
      +
      +
      + {% if footer %} {% include "email_templates/footer.html" %} {% endif %} +
      +{% endblock %} diff --git a/apps/aklub/templates/email_templates/base.html b/apps/aklub/templates/email_templates/base.html new file mode 100644 index 000000000..28e91a31e --- /dev/null +++ b/apps/aklub/templates/email_templates/base.html @@ -0,0 +1,38 @@ +{% load static %} +{% load i18n %} +{% load automat_fonts %} +{% get_automat_fonts %} + +{% get_current_language as LANGUAGE_CODE %} + + + + + + {% if page %} + + {% endif %} + ContentTools sandbox + + + + {% include "email_templates/automat_fonts.html" %} + {% for style in styles %} + + {% endfor %} + + + {% block content %} + {% endblock %} + + + + + + + + + + + + diff --git a/apps/aklub/templates/email_templates/footer.html b/apps/aklub/templates/email_templates/footer.html new file mode 100644 index 000000000..77af90874 --- /dev/null +++ b/apps/aklub/templates/email_templates/footer.html @@ -0,0 +1,110 @@ +{% load static %} +{% load filters %} + + diff --git a/apps/aklub/templates/email_templates/header.html b/apps/aklub/templates/email_templates/header.html new file mode 100644 index 000000000..8f34cf7cb --- /dev/null +++ b/apps/aklub/templates/email_templates/header.html @@ -0,0 +1,25 @@ +{% load static %} + +
      + + + + {% if header.logo_position == "left" %} + + + {% else %} + + + {% endif %} + + +
      + + + {{ header.text }}{{ header.logo_position }} + + {{ header.text }}{{ header.logo_position }} + + +
      +
      diff --git a/apps/aklub/templates/email_templates/new_empty_template.html b/apps/aklub/templates/email_templates/new_empty_template.html new file mode 100644 index 000000000..95e16db28 --- /dev/null +++ b/apps/aklub/templates/email_templates/new_empty_template.html @@ -0,0 +1,48 @@ +{% extends 'email_templates/base.html' %} {% load static %} {% block content %} +
      + {% if header %} {% include "email_templates/header.html" %} {% endif %} + + + + + + +
      + {% with default_width="600" default_height="auto" %} + + {% endwith %} + + + + + +
      +
      + {% if article %} {{ article | safe }} {% else %} {% endif %} +
      +
      +
      + {% if footer %} {% include "email_templates/footer.html" %} {% endif %} +
      +{% endblock %} diff --git a/apps/aklub/templates/form_field_html_template_widget.html b/apps/aklub/templates/form_field_html_template_widget.html new file mode 100644 index 000000000..4ec9d74ec --- /dev/null +++ b/apps/aklub/templates/form_field_html_template_widget.html @@ -0,0 +1,5 @@ +{% load i18n %} +
      + {% if widget.value %}{{ widget.value|safe }}{% endif %} +
      + diff --git a/apps/aklub/templates/form_field_html_template_widget_attrs.html b/apps/aklub/templates/form_field_html_template_widget_attrs.html new file mode 100644 index 000000000..c52de9eea --- /dev/null +++ b/apps/aklub/templates/form_field_html_template_widget_attrs.html @@ -0,0 +1 @@ +{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} diff --git a/apps/aklub/templatetags/__init__.py b/apps/aklub/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/aklub/templatetags/automat_fonts.py b/apps/aklub/templatetags/automat_fonts.py new file mode 100644 index 000000000..47aee79ce --- /dev/null +++ b/apps/aklub/templatetags/automat_fonts.py @@ -0,0 +1,25 @@ +import pathlib + +from django import template +from django.contrib.staticfiles import finders + +register = template.Library() + + +@register.inclusion_tag('email_templates/automat_fonts.html') +def get_automat_fonts(): + fonts = [] + _finders = finders.AppDirectoriesFinder() + automat_font_dir = _finders.find_in_app( + app='aklub', + path='aklub/fonts/automat', + ) + automat_font_dir_content = pathlib.Path(automat_font_dir).glob('**/*') + + font_files = [ + (f.stem, f.name) for f in automat_font_dir_content if f.is_file() + ] + for font_name, font_file_name in font_files: + fonts.append((f'AutoMat-{font_name}', font_file_name)) + + return {'automat_fonts': fonts} diff --git a/apps/aklub/urls.py b/apps/aklub/urls.py index 8a429d4dc..a209d257e 100644 --- a/apps/aklub/urls.py +++ b/apps/aklub/urls.py @@ -2,6 +2,7 @@ from django.conf.urls import url from django.contrib.auth import views as auth_views +from django.contrib.auth.decorators import login_required from django.urls import path from . import tasks # noqa @@ -36,4 +37,19 @@ auth_views.PasswordResetCompleteView.as_view(template_name='password/password_reset_complete.html'), name='password_reset_complete', ), + path( + 'get_email_template//', + login_required(views.get_email_template), + name='get_email_template', + ), + path( + 'get_email_template/new_empty_template//', + login_required(views.get_email_template_from_db), + name='get_email_template_from_db', + ), + path( + 'get_contenttools_translation//', + views.get_contenttools_translation, + name='get_contenttools_translation', + ), ] diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index a1c406e48..aadd4d0dc 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -1,14 +1,35 @@ # -*- coding: utf-8 -*- + import datetime +import json +import pathlib +from operator import itemgetter + +from PIL import Image from django.contrib import messages +from django.contrib.staticfiles import finders +from django.contrib.staticfiles.templatetags.staticfiles import static from django.db import models +from django.template import loader from django.urls import reverse from django.utils.html import format_html_join, mark_safe from django.utils.translation import ugettext_lazy as _ +from html_template_editor.models import ( + Images, TemplateContent, TemplateFooter, TemplateHeader, +) + from . import models as aklub_models +EMAIL_TEMPLATES_DENYLIST = [ + 'base.html', + 'new_empty_template.html', + 'footer.html', + 'automat_fonts.html', + 'header.html', +] + def sweet_text(generator): """ @@ -120,3 +141,116 @@ def get_admin_url(self): 'admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=[self.id], ) + + +def get_templates_files(): + """ Get email templates files + + :return list: email templates files list + """ + path = pathlib.Path(__file__).parents[0] / 'templates' \ + / 'email_templates' + + return [ + f.name for f in path.glob('*.html') if f.is_file() + ] + + +def get_email_templates_names(): + """ Get email templates names + + :return list: sorted list of email templates files tuples + (code, value) + """ + + # File templates + file_templates_names = [ + (template, template) for template in [ + template.split('.')[0] for template in get_templates_files() + if template not in EMAIL_TEMPLATES_DENYLIST + ] + ] + # Db templates + db_templates_obj = TemplateContent.objects.filter( + page__contains='new_empty_template', + ) + db_templates = set(db_templates_obj.values_list('page', flat=True)) + + value = 'new_empty_template:{}' + db_templates_names = [ + (value.format(pathlib.Path(t).name), pathlib.Path(t).name) + for t in db_templates + ] + file_templates_names.extend(db_templates_names) + + sorted_templates = sorted(file_templates_names, key=itemgetter(1)) + sorted_templates.insert( + 0, ('new_empty_template', 'new_empty_template'), + ) + sorted_templates.insert(0, ('', '---------')) + + return sorted_templates + + +def get_email_template_context(template_path, template_url): + """ Get email template context """ + + # File templates + file_templates = { + template: f"{template}.jpeg" for template in [ + template.split('.')[0] for template in get_templates_files() + if template not in EMAIL_TEMPLATES_DENYLIST + ] + } + + _finders = finders.AppDirectoriesFinder() + + template = loader.get_template(str(template_path)) + template_obj = TemplateContent.objects.filter(page=template_url) + if (template_obj): + content = template_obj.latest('created') + context = { + 'page': content, + } + regions = json.loads(content.regions) + context.update(regions) + styles = json.loads(content.styles) + context.update({'styles': styles}) + else: + context = {} + + footer = TemplateFooter.objects.filter(show=True).first() + if footer: + context['footer'] = footer + + header = TemplateHeader.objects.filter(show=True).first() + if header: + context['header'] = header + + # Templates in db + background_image = Images.objects.filter( + template_url=template_url, + edited_crop__isnull=False, + ).order_by('-modified').first() + if background_image: + context['bg_img'] = background_image.image.url + context['bg_img_width'] = background_image.image.width + context['bg_img_height'] = background_image.image.height + else: + # Templates in dir + for template_name in file_templates.keys(): + if template_name in template_url: + img_name = file_templates.get(template_name) + static_imgs_dir = _finders.find_in_app( + app='aklub', + path='aklub/images', + ) + img = Image.open( + pathlib.Path(static_imgs_dir) / img_name, + ) + + context['bg_img'] = static(f"aklub/images/{img_name}") + context['bg_img_width'] = img.width + context['bg_img_height'] = img.height + + return template, context diff --git a/apps/aklub/views.py b/apps/aklub/views.py index b14252c4f..da22eeeb7 100644 --- a/apps/aklub/views.py +++ b/apps/aklub/views.py @@ -19,6 +19,7 @@ # Create your views here. import json +import pathlib from collections import OrderedDict from betterforms.multiform import MultiModelForm @@ -34,10 +35,10 @@ from django.core.validators import MinLengthValidator, RegexValidator, ValidationError from django.db.models import Case, CharField, Count, IntegerField, Q, Sum, Value, When from django.db.models.functions import TruncMonth -from django.http import JsonResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render, render_to_response from django.template.loader import render_to_string -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode @@ -59,6 +60,7 @@ Profile, ProfileEmail, Source, Telephone, UserInCampaign, UserProfile, ) +from .utils import get_email_template_context class RegularUserForm_UserProfile(forms.ModelForm): @@ -866,3 +868,63 @@ def get(self, request, *args, **kwargs): template_name="password/password_reset.html", context={"password_reset_form": password_reset_form}, ) + + +def get_email_template(request, template_name): + """ Get email template """ + + _template_name = template_name + + template_name = '.'.join([template_name, 'html']) + template_dir = pathlib.Path('email_templates') + template_path = template_dir / template_name + + template_url = reverse( + 'aklub:get_email_template', + kwargs={'template_name': _template_name}, + ) + + template, context = get_email_template_context( + template_path, template_url, + ) + + return HttpResponse(template.render(context)) + + +def get_email_template_from_db(request, template_name): + """ Get email template from db """ + + new_empty_template = 'new_empty_template' + + template_dir = pathlib.Path('email_templates') + template_path = template_dir / '.'.join([new_empty_template, 'html']) + template_url = reverse( + 'aklub:get_email_template_from_db', + kwargs={'template_name': template_name}, + ) + + template, context = get_email_template_context( + template_path, template_url, + ) + + return HttpResponse(template.render(context)) + + +def get_contenttools_translation(request, language): + """ Get ContentTools translation file """ + + cttools_language_file = ( + pathlib.Path(settings.BASE_DIR) / 'bower_components' / + 'ContentTools' / 'translations' / f"{language}.json" + ) + + if not cttools_language_file.exists(): + message_text = _( + "language '%(lang)s.json' translation file doesn't exist", + ) % {'lang': language} + return JsonResponse({'message': message_text}) + + with open(cttools_language_file) as f: + translation = json.load(f) + + return JsonResponse(translation) diff --git a/apps/html_template_editor/__init__.py b/apps/html_template_editor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/admin.py b/apps/html_template_editor/admin.py new file mode 100644 index 000000000..73a03db3a --- /dev/null +++ b/apps/html_template_editor/admin.py @@ -0,0 +1,137 @@ +from django import forms +from django.contrib import admin + + +from .models import ( + CompanyEmail, CompanyPhone, CompanySocialMedia, + CompanyUrl, TemplateFooter, TemplateHeader, +) + +# Register your models here. + + +@admin.register(CompanyPhone) +class CompanyPhoneAdmin(admin.ModelAdmin): + pass + + +@admin.register(CompanyUrl) +class CompanyUrlAdmin(admin.ModelAdmin): + pass + + +@admin.register(CompanyEmail) +class CompanyEmailAdmin(admin.ModelAdmin): + pass + + +@admin.register(CompanySocialMedia) +class CompanySocialMediaAdmin(admin.ModelAdmin): + pass + + +class CompanyPhoneAdminInline(admin.StackedInline): + model = TemplateFooter.phone.through + extra = 0 + + +class CompanyUrlAdminInline(admin.StackedInline): + model = TemplateFooter.url.through + extra = 0 + + +class CompanyEmailAdminInline(admin.StackedInline): + model = TemplateFooter.email.through + extra = 0 + + +class CompanySocialMediaAdminInline(admin.StackedInline): + model = TemplateFooter.social_media.through + extra = 0 + + +class TemplateFooterAdminForm(forms.ModelForm): + class Meta: + model = TemplateFooter + exclude = ( + 'url', + 'phone', + 'email', + 'social_media', + 'user', + ) + + def save(self, commit=True): + instance = super(TemplateFooterAdminForm, self).save(commit=False) + instance.user = self.user + if commit: + instance.save() + return instance + + +@admin.register(TemplateFooter) +class TemplateFooterAdminInline(admin.ModelAdmin): + form = TemplateFooterAdminForm + list_display = ( + 'company_name', + 'address', + 'get_phone', + 'get_email', + 'get_url', + 'get_social_media', + 'user', + 'show', + ) + list_filter = ( + 'name', + 'company_name', + 'user', + 'show', + ) + inlines = [ + CompanyPhoneAdminInline, + CompanyUrlAdminInline, + CompanyEmailAdminInline, + CompanySocialMediaAdminInline, + ] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + form.user = request.user + return form + + +class TemplateHeaderAdminForm(forms.ModelForm): + class Meta: + model = TemplateHeader + exclude = ('user',) + + def save(self, commit=True): + instance = super(TemplateHeaderAdminForm, self).save(commit=False) + instance.user = self.user + if commit: + instance.save() + return instance + + +@admin.register(TemplateHeader) +class TemplateHeaderAdminInline(admin.ModelAdmin): + form = TemplateHeaderAdminForm + list_display = ( + 'name', + 'text', + 'logo', + 'user', + 'show', + ) + list_filter = ( + 'name', + 'text', + 'user', + 'show', + ) + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + form.user = request.user + return form diff --git a/apps/html_template_editor/forms.py b/apps/html_template_editor/forms.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.mo b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.mo new file mode 100644 index 000000000..9adedc15d Binary files /dev/null and b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.mo differ diff --git a/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..30c61d6f1 --- /dev/null +++ b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,277 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-11 08:27+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n " +"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" + +#: apps/html_template_editor/models.py:21 +msgid "Template content" +msgstr "Obsah šablony" + +#: apps/html_template_editor/models.py:22 +msgid "Template contents" +msgstr "Obsah šablony" + +#: apps/html_template_editor/models.py:25 +#: apps/html_template_editor/models.py:70 +msgid "Created" +msgstr "Vytvořeno" + +#: apps/html_template_editor/models.py:29 +#: apps/html_template_editor/models.py:74 +msgid "Modified" +msgstr "Upraveno" + +#: apps/html_template_editor/models.py:33 +msgid "Universally unique identifier" +msgstr "Univerzálně jedinečný identifikátor" + +#: apps/html_template_editor/models.py:39 +#: apps/html_template_editor/models.py:66 +#: apps/html_template_editor/models.py:67 +msgid "Images" +msgstr "Snímky" + +#: apps/html_template_editor/models.py:45 +msgid "Pages" +msgstr "Stránky" + +#: apps/html_template_editor/models.py:51 +msgid "Regions" +msgstr "Regiony" + +#: apps/html_template_editor/models.py:57 +msgid "Styles" +msgstr "Styly" + +#: apps/html_template_editor/models.py:78 +msgid "Image" +msgstr "Snímek" + +#: apps/html_template_editor/models.py:86 +msgid "Name" +msgstr "Název" + +#: apps/html_template_editor/models.py:92 +msgid "Edited width" +msgstr "Upravená šířka" + +#: apps/html_template_editor/models.py:97 +msgid "Edited crop" +msgstr "Upravené oříznutí" + +#: apps/html_template_editor/models.py:103 +msgid "Edited direction" +msgstr "Upravený směr" + +#: apps/html_template_editor/models.py:109 +msgid "Background image" +msgstr "Obrázek na pozadí" + +#: apps/html_template_editor/models.py:113 +msgid "Template url" +msgstr "Webová adresa (url) šablony" + +#: apps/html_template_editor/models.py:140 +msgid "Company url" +msgstr "Webová adresa (url) společnosti" + +#: apps/html_template_editor/models.py:141 +msgid "Company urls" +msgstr "Webové adresy (url) společnosti" + +#: apps/html_template_editor/models.py:144 +#: apps/html_template_editor/models.py:199 +#: apps/html_template_editor/models.py:249 +#: apps/html_template_editor/models.py:276 +msgid "Url" +msgstr "Webová adressa (url)" + +#: apps/html_template_editor/models.py:156 +msgid "Company phone number" +msgstr "Telefonní číslo společnosti" + +#: apps/html_template_editor/models.py:157 +msgid "Company phone numbers" +msgstr "Telefonní čísla společnosti" + +#: apps/html_template_editor/models.py:160 +#: apps/html_template_editor/models.py:284 +msgid "Phone number" +msgstr "Telefonní číslo" + +#: apps/html_template_editor/models.py:172 +#: apps/html_template_editor/models.py:176 +msgid "Company email" +msgstr "E-mail společnosti" + +#: apps/html_template_editor/models.py:173 +msgid "Company emails" +msgstr "E-maily společnosti" + +#: apps/html_template_editor/models.py:188 +#: apps/html_template_editor/models.py:189 +msgid "Company social media" +msgstr "Sociální média společnosti" + +#: apps/html_template_editor/models.py:192 +msgid "Icon name" +msgstr "Název ikony" + +#: apps/html_template_editor/models.py:217 +msgid "Template footer" +msgstr "Zápatí šablony" + +#: apps/html_template_editor/models.py:218 +msgid "Template footers" +msgstr "Zápatí šablony" + +#: apps/html_template_editor/models.py:221 +msgid "Footer name" +msgstr "Název zápatí" + +#: apps/html_template_editor/models.py:227 +msgid "Company name" +msgstr "Jméno společnosti" + +#: apps/html_template_editor/models.py:233 +msgid "Address" +msgstr "Adresa" + +#: apps/html_template_editor/models.py:239 +msgid "Phone" +msgstr "Telefon" + +#: apps/html_template_editor/models.py:244 +#: apps/html_template_editor/models.py:280 +msgid "Email" +msgstr "E-mail" + +#: apps/html_template_editor/models.py:254 +msgid "Social media" +msgstr "Sociální média" + +#: apps/html_template_editor/models.py:259 +#: apps/html_template_editor/models.py:362 +msgid "User" +msgstr "Uživatel" + +#: apps/html_template_editor/models.py:266 +#: apps/html_template_editor/models.py:369 +msgid "Show" +msgstr "Zobrazit" + +#: apps/html_template_editor/models.py:268 +msgid "Show footer" +msgstr "Zobrazit zápatí" + +#: apps/html_template_editor/models.py:289 +msgid "Social media link" +msgstr "Odkaz na sociální média" + +#: apps/html_template_editor/models.py:295 +msgid "Template header" +msgstr "Záhlaví šablony" + +#: apps/html_template_editor/models.py:296 +msgid "Template headers" +msgstr "Záhlaví šablony" + +#: apps/html_template_editor/models.py:299 +msgid "Header name" +msgstr "Název záhlaví" + +#: apps/html_template_editor/models.py:305 +msgid "Header text" +msgstr "Text záhlaví" + +#: apps/html_template_editor/models.py:311 +msgid "Header horizontal text position" +msgstr "Horizontální poloha záhlaví" + +#: apps/html_template_editor/models.py:312 +#: apps/html_template_editor/models.py:342 +#: apps/html_template_editor/models.py:353 +msgid "Left" +msgstr "Vlevo" + +#: apps/html_template_editor/models.py:313 +#: apps/html_template_editor/models.py:343 +msgid "Center" +msgstr "Vystředit" + +#: apps/html_template_editor/models.py:314 +#: apps/html_template_editor/models.py:344 +#: apps/html_template_editor/models.py:354 +msgid "Right" +msgstr "Vpravo" + +#: apps/html_template_editor/models.py:321 +msgid "Header vertical text position" +msgstr "Vertikální poloha záhlaví textu" + +#: apps/html_template_editor/models.py:322 +msgid "Top" +msgstr "Horní" + +#: apps/html_template_editor/models.py:323 +msgid "Middle" +msgstr "Střední" + +#: apps/html_template_editor/models.py:324 +msgid "Bottom" +msgstr "Spodní" + +#: apps/html_template_editor/models.py:331 +msgid "Logo image" +msgstr "Obrázek loga" + +#: apps/html_template_editor/models.py:337 +msgid "Max. image width 300 px, and max. image file size 5 MB" +msgstr "Max. šířka obrázku 300 px a max. velikost obrazového souboru je 5 MB" + +#: apps/html_template_editor/models.py:340 +msgid "Logo horizontal position" +msgstr "Horizontální poloha loga" + +#: apps/html_template_editor/models.py:351 +msgid "Logo position" +msgstr "Pozice loga" + +#: apps/html_template_editor/models.py:359 +msgid "Left or right column logo position" +msgstr "Umístění loga v levém nebo pravém sloupci" + +#: apps/html_template_editor/models.py:371 +msgid "Show header" +msgstr "Zobrazit záhlaví" + +#: apps/html_template_editor/validators.py:19 +#, python-format +msgid "Allowed image format is %(image_formats)s " +msgstr "Povolený formát obrázku je %(image_formats)s" + +#: apps/html_template_editor/validators.py:25 +#, python-format +msgid "Width is larger than what is allowed %(max_width)s px" +msgstr "Šířka je větší, než je povoleno %(max_width)s px" + +#: apps/html_template_editor/validators.py:31 +#, python-format +msgid "Image file size is larger than what is allowed %(max_file_size)s MB" +msgstr "Velikost obrazového souboru je větší, než je povoleno %(max_file_size)s MB" diff --git a/apps/html_template_editor/migrations/0001_initial.py b/apps/html_template_editor/migrations/0001_initial.py new file mode 100644 index 000000000..4a540caa6 --- /dev/null +++ b/apps/html_template_editor/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 2.2.6 on 2019-10-07 04:47 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Images', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('image', models.ImageField(null=True, upload_to='images')), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('edited_width', models.IntegerField(blank=True, null=True)), + ('edited_crop', models.CharField(blank=True, max_length=15, null=True)), + ('edited_direction', models.CharField(blank=True, max_length=5, null=True)), + ], + ), + migrations.CreateModel( + name='TemplateContent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('images', models.CharField(blank=True, max_length=100, null=True)), + ('page', models.CharField(blank=True, max_length=100, null=True)), + ('regions', models.TextField(blank=True, default='', null=True)), + ], + ), + ] diff --git a/apps/html_template_editor/migrations/0002_auto_20191014_1140.py b/apps/html_template_editor/migrations/0002_auto_20191014_1140.py new file mode 100644 index 000000000..495cb4717 --- /dev/null +++ b/apps/html_template_editor/migrations/0002_auto_20191014_1140.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2019-10-14 09:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='images', + name='edited_crop', + field=models.CharField(blank=True, max_length=80, null=True), + ), + ] diff --git a/apps/html_template_editor/migrations/0003_auto_20200130_0656.py b/apps/html_template_editor/migrations/0003_auto_20200130_0656.py new file mode 100644 index 000000000..292f4a16b --- /dev/null +++ b/apps/html_template_editor/migrations/0003_auto_20200130_0656.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.9 on 2020-01-30 05:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0002_auto_20191014_1140'), + ] + + operations = [ + migrations.AddField( + model_name='images', + name='background_image', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='images', + name='template_url', + field=models.CharField(blank=True, max_length=350, null=True), + ), + ] diff --git a/apps/html_template_editor/migrations/0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter.py b/apps/html_template_editor/migrations/0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter.py new file mode 100644 index 000000000..521078d9d --- /dev/null +++ b/apps/html_template_editor/migrations/0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter.py @@ -0,0 +1,76 @@ +# Generated by Django 2.2.10 on 2020-02-17 08:50 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0003_auto_20200130_0656'), + ] + + operations = [ + migrations.CreateModel( + name='CompanyEmail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Company email')), + ], + options={ + 'verbose_name': 'Company email', + 'verbose_name_plural': 'Company email', + }, + ), + migrations.CreateModel( + name='CompanyPhone', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, verbose_name='Phone number')), + ], + options={ + 'verbose_name': 'Company phone number', + 'verbose_name_plural': 'Company phone numbers', + }, + ), + migrations.CreateModel( + name='CompanySocialMedia', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('icon_name', models.CharField(blank=True, choices=[('facebook', 'facebook'), ('google', 'google'), ('instagram', 'instagram'), ('twitter', 'twitter'), ('vimeo', 'vimeo'), ('youtube', 'youtube')], max_length=20, null=True, verbose_name='Icon name')), + ('url', models.URLField(blank=True, null=True, verbose_name='Url')), + ], + options={ + 'verbose_name': 'Social media', + 'verbose_name_plural': 'Social media', + }, + ), + migrations.CreateModel( + name='CompanyUrl', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField(blank=True, null=True, verbose_name='Url')), + ], + options={ + 'verbose_name': 'Company url', + 'verbose_name_plural': 'Company urls', + }, + ), + migrations.CreateModel( + name='TemplateFooter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('company_name', models.CharField(blank=True, max_length=80, null=True, verbose_name='Company name')), + ('address', models.CharField(blank=True, max_length=80, null=True, verbose_name='Address')), + ('show', models.BooleanField(default=False, help_text='Show footer', verbose_name='Show')), + ('email', models.ManyToManyField(blank=True, to='html_template_editor.CompanyEmail', verbose_name='Email')), + ('phone', models.ManyToManyField(blank=True, to='html_template_editor.CompanyPhone', verbose_name='Phone')), + ('social_media', models.ManyToManyField(blank=True, to='html_template_editor.CompanySocialMedia', verbose_name='Social media')), + ('url', models.ManyToManyField(blank=True, to='html_template_editor.CompanyUrl', verbose_name='Url')), + ], + options={ + 'verbose_name': 'Template footer', + 'verbose_name_plural': 'Template footers', + }, + ), + ] diff --git a/apps/html_template_editor/migrations/0005_templatecontent_styles.py b/apps/html_template_editor/migrations/0005_templatecontent_styles.py new file mode 100644 index 000000000..bc591d964 --- /dev/null +++ b/apps/html_template_editor/migrations/0005_templatecontent_styles.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2020-03-02 12:24 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter'), + ] + + operations = [ + migrations.AddField( + model_name='templatecontent', + name='styles', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + ] diff --git a/apps/html_template_editor/migrations/0006_auto_20200414_0736.py b/apps/html_template_editor/migrations/0006_auto_20200414_0736.py new file mode 100644 index 000000000..032efdc9f --- /dev/null +++ b/apps/html_template_editor/migrations/0006_auto_20200414_0736.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-14 05:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0005_templatecontent_styles'), + ] + + operations = [ + migrations.AlterField( + model_name='companysocialmedia', + name='icon_name', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Icon name'), + ), + ] diff --git a/apps/html_template_editor/migrations/0007_templateheader.py b/apps/html_template_editor/migrations/0007_templateheader.py new file mode 100644 index 000000000..5ef99577a --- /dev/null +++ b/apps/html_template_editor/migrations/0007_templateheader.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.10 on 2020-04-14 15:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import html_template_editor.validators + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('html_template_editor', '0006_auto_20200414_0736'), + ] + + operations = [ + migrations.CreateModel( + name='TemplateHeader', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Header name')), + ('text', models.CharField(blank=True, max_length=255, null=True, verbose_name='Header text')), + ('text_horizontal_position', models.CharField(blank=True, choices=[('left', 'Left'), ('center', 'Center'), ('right', 'Right')], default='left', max_length=20, null=True, verbose_name='Header horizontal text position')), + ('text_vertical_position', models.CharField(blank=True, choices=[('top', 'Top'), ('middle', 'Middle'), ('bottom', 'Bottom')], default='middle', max_length=20, null=True, verbose_name='Header vertical text position')), + ('logo', models.ImageField(help_text='Max. image width 300 px, and max. image file size 5 MB', upload_to='images', validators=[html_template_editor.validators.validate_logo_image], verbose_name='Logo image')), + ('logo_horizontal_position', models.CharField(blank=True, choices=[('left', 'Left'), ('center', 'Center'), ('right', 'Right')], default='left', max_length=20, null=True, verbose_name='Logo horizontal position')), + ('logo_position', models.CharField(blank=True, choices=[('left', 'Left'), ('right', 'Right')], default='left', help_text='Left or right column logo position', max_length=20, null=True, verbose_name='Logo position')), + ('show', models.BooleanField(default=False, help_text='Show header', verbose_name='Show')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Template header', + 'verbose_name_plural': 'Template headers', + }, + ), + ] diff --git a/apps/html_template_editor/migrations/0008_auto_20200425_0532.py b/apps/html_template_editor/migrations/0008_auto_20200425_0532.py new file mode 100644 index 000000000..0d3100bfd --- /dev/null +++ b/apps/html_template_editor/migrations/0008_auto_20200425_0532.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.12 on 2020-04-25 03:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('html_template_editor', '0007_templateheader'), + ] + + operations = [ + migrations.AddField( + model_name='templatefooter', + name='name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Footer name'), + ), + migrations.AddField( + model_name='templatefooter', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/html_template_editor/migrations/0009_auto_20200511_0823.py b/apps/html_template_editor/migrations/0009_auto_20200511_0823.py new file mode 100644 index 000000000..a0f9c0ccf --- /dev/null +++ b/apps/html_template_editor/migrations/0009_auto_20200511_0823.py @@ -0,0 +1,123 @@ +# Generated by Django 2.2.12 on 2020-05-11 06:23 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0008_auto_20200425_0532'), + ] + + operations = [ + migrations.AlterModelOptions( + name='companyemail', + options={'verbose_name': 'Company email', 'verbose_name_plural': 'Company emails'}, + ), + migrations.AlterModelOptions( + name='companysocialmedia', + options={'verbose_name': 'Company social media', 'verbose_name_plural': 'Company social media'}, + ), + migrations.AlterModelOptions( + name='images', + options={'verbose_name': 'Images', 'verbose_name_plural': 'Images'}, + ), + migrations.AlterModelOptions( + name='templatecontent', + options={'verbose_name': 'Template content', 'verbose_name_plural': 'Template contents'}, + ), + migrations.AlterField( + model_name='images', + name='background_image', + field=models.BooleanField(default=False, verbose_name='Background image'), + ), + migrations.AlterField( + model_name='images', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created'), + ), + migrations.AlterField( + model_name='images', + name='edited_crop', + field=models.CharField(blank=True, max_length=80, null=True, verbose_name='Edited crop'), + ), + migrations.AlterField( + model_name='images', + name='edited_direction', + field=models.CharField(blank=True, max_length=5, null=True, verbose_name='Edited direction'), + ), + migrations.AlterField( + model_name='images', + name='edited_width', + field=models.IntegerField(blank=True, null=True, verbose_name='Edited width'), + ), + migrations.AlterField( + model_name='images', + name='image', + field=models.ImageField(null=True, upload_to='images', verbose_name='Image'), + ), + migrations.AlterField( + model_name='images', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='Modified'), + ), + migrations.AlterField( + model_name='images', + name='name', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='images', + name='template_url', + field=models.CharField(blank=True, max_length=350, null=True, verbose_name='Template url'), + ), + migrations.AlterField( + model_name='templatecontent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created'), + ), + migrations.AlterField( + model_name='templatecontent', + name='images', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Images'), + ), + migrations.AlterField( + model_name='templatecontent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='Modified'), + ), + migrations.AlterField( + model_name='templatecontent', + name='page', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Pages'), + ), + migrations.AlterField( + model_name='templatecontent', + name='regions', + field=models.TextField(blank=True, default='', null=True, verbose_name='Regions'), + ), + migrations.AlterField( + model_name='templatecontent', + name='styles', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Styles'), + ), + migrations.AlterField( + model_name='templatecontent', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='Universally unique identifier'), + ), + migrations.AlterField( + model_name='templatefooter', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AlterField( + model_name='templateheader', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/apps/html_template_editor/migrations/0010_auto_20200717_0854.py b/apps/html_template_editor/migrations/0010_auto_20200717_0854.py new file mode 100644 index 000000000..5b0442c98 --- /dev/null +++ b/apps/html_template_editor/migrations/0010_auto_20200717_0854.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.14 on 2020-07-17 06:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0009_auto_20200511_0823'), + ] + + operations = [ + migrations.AlterField( + model_name='companysocialmedia', + name='icon_name', + field=models.CharField(blank=True, choices=[('facebook', 'facebook'), ('google', 'google'), ('instagram', 'instagram'), ('twitter', 'twitter'), ('vimeo', 'vimeo'), ('youtube', 'youtube')], max_length=20, null=True, verbose_name='Icon name'), + ), + ] diff --git a/apps/html_template_editor/migrations/__init__.py b/apps/html_template_editor/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py new file mode 100644 index 000000000..99acf9638 --- /dev/null +++ b/apps/html_template_editor/models.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- + +import uuid +from enum import Enum + +from django.conf import settings +from django.contrib.postgres.fields import JSONField +from django.db import models +from django.utils.html import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from phonenumber_field.modelfields import PhoneNumberField + +from .validators import validate_logo_image + + +class SocialMedia(Enum): + facebook = 'facebook' + google = 'google' + instagram = 'instagram' + twitter = 'twitter' + vimeo = 'vimeo' + youtube = 'youtube' + + +class TemplateContent(models.Model): + + class Meta: + verbose_name = _("Template content") + verbose_name_plural = _("Template contents") + + created = models.DateTimeField( + verbose_name=_("Created"), + auto_now_add=True, + ) + modified = models.DateTimeField( + verbose_name=_("Modified"), + auto_now=True, + ) + uuid = models.UUIDField( + verbose_name=_("Universally unique identifier"), + default=uuid.uuid4, + editable=False, + unique=True, + ) + images = models.CharField( + verbose_name=_("Images"), + max_length=100, + blank=True, + null=True, + ) + page = models.CharField( + verbose_name=_("Pages"), + max_length=100, + blank=True, + null=True, + ) + regions = models.TextField( + verbose_name=_("Regions"), + blank=True, + default='', + null=True, + ) + styles = JSONField( + verbose_name=_("Styles"), + blank=True, + null=True, + ) + + +class Images(models.Model): + + class Meta: + verbose_name = _("Images") + verbose_name_plural = _("Images") + + created = models.DateTimeField( + verbose_name=_("Created"), + auto_now_add=True, + ) + modified = models.DateTimeField( + verbose_name=_("Modified"), + auto_now=True, + ) + image = models.ImageField( + verbose_name=_("Image"), + upload_to='images', + height_field=None, + width_field=None, + max_length=100, + null=True, + ) + name = models.CharField( + verbose_name=_("Name"), + max_length=100, + blank=True, + null=True, + ) + edited_width = models.IntegerField( + verbose_name=_("Edited width"), + blank=True, + null=True, + ) + edited_crop = models.CharField( + verbose_name=_("Edited crop"), + max_length=80, + blank=True, + null=True, + ) + edited_direction = models.CharField( + verbose_name=_("Edited direction"), + max_length=5, + blank=True, + null=True, + ) + background_image = models.BooleanField( + verbose_name=_("Background image"), + default=False, + ) + template_url = models.CharField( + verbose_name=_("Template url"), + max_length=350, + blank=True, + null=True, + ) + + def size(self): + return [self.image.width, self.image.height] + + +# class FileUpload(models.Model): +# created = models.DateTimeField( +# auto_now_add=True, +# ) +# owner = models.ForeignKey( +# User, +# on_delete=models.SET_NULL, +# null=True, +# ) +# datafile = models.FileField( +# upload_to='files', +# ) + + +class CompanyUrl(models.Model): + + class Meta: + verbose_name = _("Company url") + verbose_name_plural = _("Company urls") + + url = models.URLField( + verbose_name=_("Url"), + null=True, + blank=True, + ) + + def __str__(self): + return self.url or '' + + +class CompanyPhone(models.Model): + + class Meta: + verbose_name = _("Company phone number") + verbose_name_plural = _("Company phone numbers") + + phone = PhoneNumberField( + verbose_name=_("Phone number"), + null=True, + blank=True, + ) + + def __str__(self): + return str(self.phone) + + +class CompanyEmail(models.Model): + + class Meta: + verbose_name = _("Company email") + verbose_name_plural = _("Company emails") + + email = models.EmailField( + verbose_name=_("Company email"), + null=True, + blank=True, + ) + + def __str__(self): + return self.email or '' + + +class CompanySocialMedia(models.Model): + + class Meta: + verbose_name = _("Company social media") + verbose_name_plural = _("Company social media") + + icon_name = models.CharField( + verbose_name=_("Icon name"), + choices=((tag.name, tag.value) for tag in SocialMedia), + max_length=20, + null=True, + blank=True, + ) + url = models.URLField( + verbose_name=_("Url"), + null=True, + blank=True, + ) + + def __str__(self): + return f"{self.icon_name} : {self.url}" + + +class TemplateFooter(models.Model): + + class Meta: + verbose_name = _("Template footer") + verbose_name_plural = _("Template footers") + + name = models.CharField( + verbose_name=_("Footer name"), + max_length=255, + null=True, + blank=True, + ) + company_name = models.CharField( + verbose_name=_("Company name"), + max_length=80, + null=True, + blank=True, + ) + address = models.CharField( + verbose_name=_("Address"), + max_length=80, + null=True, + blank=True, + ) + phone = models.ManyToManyField( + verbose_name=_("Phone"), + to=CompanyPhone, + blank=True, + ) + email = models.ManyToManyField( + verbose_name=_("Email"), + to=CompanyEmail, + blank=True, + ) + url = models.ManyToManyField( + verbose_name=_("Url"), + to=CompanyUrl, + blank=True, + ) + social_media = models.ManyToManyField( + verbose_name=_("Social media"), + to=CompanySocialMedia, + blank=True, + ) + user = models.ForeignKey( + verbose_name=_("User"), + to=settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + show = models.BooleanField( + verbose_name=_("Show"), + default=False, + help_text=_("Show footer"), + ) + + def __str__(self): + return self.company_name + + def get_url(self): + return mark_safe("
      ".join([o.url for o in self.url.all()])) + get_url.short_description = _('Url') + + def get_email(self): + return mark_safe("
      ".join([o.email for o in self.email.all()])) + get_email.short_description = _('Email') + + def get_phone(self): + return mark_safe("
      ".join([str(o.phone) for o in self.phone.all()])) + get_phone.short_description = _('Phone number') + + def get_social_media(self): + objs = self.social_media.all() + return mark_safe("
      ".join([f"{o.icon_name}: {o.url}" for o in objs])) + get_social_media.short_description = _('Social media link') + + +class TemplateHeader(models.Model): + + class Meta: + verbose_name = _("Template header") + verbose_name_plural = _("Template headers") + + name = models.CharField( + verbose_name=_("Header name"), + max_length=255, + null=True, + blank=True, + ) + text = models.CharField( + verbose_name=_("Header text"), + max_length=255, + null=True, + blank=True, + ) + text_horizontal_position = models.CharField( + verbose_name=_("Header horizontal text position"), + choices=(("left", _("Left")), + ("center", _("Center")), + ("right", _("Right"))), + default="left", + max_length=20, + null=True, + blank=True, + ) + text_vertical_position = models.CharField( + verbose_name=_("Header vertical text position"), + choices=(("top", _("Top")), + ("middle", _("Middle")), + ("bottom", _("Bottom"))), + default="middle", + max_length=20, + null=True, + blank=True, + ) + logo = models.ImageField( + verbose_name=_("Logo image"), + upload_to='images', + height_field=None, + width_field=None, + max_length=100, + validators=[validate_logo_image], + help_text=_("Max. image width 300 px, and max. image file size 5 MB"), + ) + logo_horizontal_position = models.CharField( + verbose_name=_("Logo horizontal position"), + choices=( + ("left", _("Left")), + ("center", _("Center")), + ("right", _("Right"))), + default="left", + max_length=20, + null=True, + blank=True, + ) + logo_position = models.CharField( + verbose_name=_("Logo position"), + choices=( + ("left", _("Left")), + ("right", _("Right"))), + default="left", + max_length=20, + null=True, + blank=True, + help_text=_("Left or right column logo position"), + ) + user = models.ForeignKey( + verbose_name=_("User"), + to=settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + show = models.BooleanField( + verbose_name=_("Show"), + default=False, + help_text=_("Show header"), + ) + + def __str__(self): + return self.name or '' diff --git a/apps/html_template_editor/serializers.py b/apps/html_template_editor/serializers.py new file mode 100644 index 000000000..b673a939d --- /dev/null +++ b/apps/html_template_editor/serializers.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import json +import time + +from rest_framework import serializers + +from .models import Images, TemplateContent + + +class TemplateContentSerializer(serializers.ModelSerializer): + + class Meta: + model = TemplateContent + fields = ('id', + 'created', + 'uuid', + 'page', + 'regions', + 'images', + 'styles') + + def to_representation(self, instance): + ret = super(TemplateContentSerializer, self).to_representation(instance) + ret['regions'] = json.loads(ret['regions']) + return ret + + def validate(self, data): + return data + + +class ImagesSerializer(serializers.ModelSerializer): + + size = serializers.ListField( + read_only=True, + child=serializers.IntegerField(), + ) + + class Meta: + model = Images + fields = ('id', + 'created', + 'image', + 'name', + 'size', + 'edited_width', + 'edited_crop', + 'edited_direction', + 'background_image', + 'template_url', + ) + + def to_representation(self, instance): + ret = super(ImagesSerializer, self).to_representation(instance) + if ret['image']: + # Modify the image URL by adding an _ignore param + # This will force the browser to reload the image + ret['image'] = "{}?_ignore={}".format(ret['image'], time.time()) + return ret diff --git a/apps/html_template_editor/templatetags/__init__.py b/apps/html_template_editor/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/templatetags/filters.py b/apps/html_template_editor/templatetags/filters.py new file mode 100644 index 000000000..511b867e4 --- /dev/null +++ b/apps/html_template_editor/templatetags/filters.py @@ -0,0 +1,18 @@ +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter(name='get_link_element') +def get_link_element(value, arg): + if arg == 'list': + links = [] + for i in value: + links.append( + mark_safe(f"{i}"), + ) + return links + elif arg == 'str': + return mark_safe( + f"{value}") diff --git a/apps/html_template_editor/tests.py b/apps/html_template_editor/tests.py new file mode 100644 index 000000000..507bc9806 --- /dev/null +++ b/apps/html_template_editor/tests.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +import io + +from PIL import Image + +from aklub.tests.test_admin import CreateSuperUserMixin + +from django.core.files.base import ContentFile +from django.urls import reverse + +from rest_framework import status +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient +from rest_framework.test import APITestCase + + +# Create your tests here. +class TemplateContentTests(CreateSuperUserMixin, APITestCase): + + def setUp(self): + super().setUp() + self.token = Token.objects.create(user=self.superuser) + self.client = APIClient() + + def test_add(self): + """ Test if we can add, retrieve and list """ + self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(self.token.key)) + + # Create the add URL + url = reverse('html_template_editor:add') + + data = { + 'images': ['{}'], + 'regions': [ + ''.join([ + """{"article": "

      """, + """About the author

      """, + """Anthony Blackshaw""", + """

      """, + """Anthony Blackshaw is a co-founder of Getme, an employee owned""", + """company with a focus on web tech. He enjoys writing and talking""", + """about tech, especially code and the occasional""", + """Montecristo No.2s./p>"}""", + ]), + ], + 'page': ['/'], + } + response = self.client.post(url, data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + object_id = response.data['uuid'] + + # Retrieve + url = reverse('html_template_editor:retrieve', args=[object_id]) + response = self.client.get(url, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class ImageUploadTests(CreateSuperUserMixin, APITestCase): + + def setUp(self): + super().setUp() + self.token = Token.objects.create(user=self.superuser) + self.client = APIClient() + + def test_upload_image(self): + """ Test if we can add, update image """ + self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(self.token.key)) + + url = reverse('html_template_editor:images_add') + + file_obj = io.BytesIO() + image = Image.new("RGBA", size=(50, 50), color=(256, 0, 0)) + image.save(file_obj, 'png') + + file_obj.seek(0) + + django_friendly_file = ContentFile(file_obj.read(), 'test.png') + + data = { + 'width': ['600'], + 'image': django_friendly_file, + } + + # Upload a file with an authenticated user + response = self.client.post(url, data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('created', response.data) + + image_id = response.data['id'] + + # + # Crop Image + # + + url = reverse('html_template_editor:images_update', args=[image_id]) + + data = { + 'crop': ['0,0,10,10'], + } + + response = self.client.post(url, data, format='multipart') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # assert unauthenticated user can not upload file + self.client.logout() + response = self.client.post(url, data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/apps/html_template_editor/upload_views.py b/apps/html_template_editor/upload_views.py new file mode 100644 index 000000000..9007d78eb --- /dev/null +++ b/apps/html_template_editor/upload_views.py @@ -0,0 +1,95 @@ + +import decimal + +from PIL import Image + +from rest_framework import generics, permissions + +from .models import Images +from .serializers import ImagesSerializer + +# +# Mixin for all company views. +# Defines serializers, queryset and permissions +# + + +class ImagesMixin(object): + + def get_queryset(self): + return Images.objects.filter() + + def get_serializer_class(self): + return ImagesSerializer + + def get_permissions(self): + return [permissions.IsAuthenticated()] + + +class ImagesAdd(ImagesMixin, generics.CreateAPIView): + + def perform_create(self, serializer): + serializer.save( + image=self.request.data['image'], + name=self.request.data['image'].name, + edited_width=self.request.data['width'], + ) + + +class ImagesList(ImagesMixin, generics.ListAPIView): + ordering_fields = ('created') + ordering = ('-created',) + + +class ImagesUpdate(ImagesMixin, generics.UpdateAPIView): + + lookup_field = 'id' + + def post(self, request, *args, **kwargs): + response = self.update(request, *args, **kwargs) + return response + + def perform_update(self, serializer): + # Get image from image model instance + image = self.get_object().image + + if self.request.data.get('crop'): + + crop_values = self.request.data['crop'].split(',') + + # Crop values are percentages, so we need to convert them into + # pixel values + + top = round(decimal.Decimal(crop_values[0]) * image.height) + left = round(decimal.Decimal(crop_values[1]) * image.width) + bottom = round(decimal.Decimal(crop_values[2]) * image.height) + right = round(decimal.Decimal(crop_values[3]) * image.width) + + crop_box = (left, top, right, bottom) + + # Open the image with PIL + im = Image.open(image.path) + + # Action the image rotation + im = im.crop(crop_box) + + # Save the image + im.save(image.path) + + serializer.save( + edited_crop=self.request.data['crop'], + ) + + if self.request.data.get('direction'): + angle = 270 if self.request.data['direction'] == "CW" else 90 + + # Open the image with PIL + im = Image.open(image.path) + + # Action the image rotation + im = im.rotate(angle) + + # Save the image + im.save(image.path) + + serializer.save(edited_direction=self.request.data['direction']) diff --git a/apps/html_template_editor/urls.py b/apps/html_template_editor/urls.py new file mode 100644 index 000000000..7f4892c40 --- /dev/null +++ b/apps/html_template_editor/urls.py @@ -0,0 +1,47 @@ + +from django.urls import path + +from . import upload_views, views + +urlpatterns = [ + path( + 'add/', + views.TemplateContentAdd.as_view(), + name='add', + ), + path( + 'retrieve//', + views.TemplateContentRetrieve.as_view(), + name="retrieve", + ), + path( + 'update//', + views.TemplateContentUpdate.as_view(), + name='update', + ), + path( + 'list/', + views.TemplateContentList.as_view(), + name="list", + ), + path( + 'delete//', + views.TemplateContentDelete.as_view(), + name="delete", + ), + path( + 'images/add/', + upload_views.ImagesAdd.as_view(), + name='images_add', + ), + path( + 'images/update//', + upload_views.ImagesUpdate.as_view(), + name='images_update', + ), + path( + 'images/list/', + upload_views.ImagesList.as_view(), + name='images_list', + ), +] diff --git a/apps/html_template_editor/utils.py b/apps/html_template_editor/utils.py new file mode 100644 index 000000000..f8bbc59b2 --- /dev/null +++ b/apps/html_template_editor/utils.py @@ -0,0 +1,18 @@ +import pathlib + +from django.contrib.staticfiles import finders + + +def get_social_media_icons_names(): + social_media_choices = [] + _finders = finders.AppDirectoriesFinder() + + img_src_dir = _finders.find_in_app( + app='aklub', + path='aklub/images/social_media', + ) + imgs = pathlib.Path(img_src_dir).glob('**/*') + for img in imgs: + social_media_choices.append((img.stem, (img.stem))) + + return sorted(social_media_choices, key=lambda tup: tup[0]) diff --git a/apps/html_template_editor/validators.py b/apps/html_template_editor/validators.py new file mode 100644 index 000000000..69d347801 --- /dev/null +++ b/apps/html_template_editor/validators.py @@ -0,0 +1,34 @@ +from PIL import Image + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + + +def validate_logo_image(image): + max_width = 300 + megabyte_limit = 5.0 * 1024 * 1024 + allowed_image_formats = ['JPEG', 'PNG'] + + img = Image.open(image) + width, height = img.size + image_file_size = image.tell() + image_file_format = img.format + + if image_file_format not in allowed_image_formats: + raise ValidationError( + _("Allowed image format is %(image_formats)s "), + params={'image_formats': ', '.join(allowed_image_formats)}, + ) + + if width > max_width: + raise ValidationError( + _("Width is larger than what is allowed %(max_width)s px"), + params={'max_width': max_width}, + ) + + if image_file_size > megabyte_limit: + raise ValidationError( + _("Image file size is larger than what is allowed " + "%(max_file_size)s MB"), + params={'max_file_size': megabyte_limit}, + ) diff --git a/apps/html_template_editor/views.py b/apps/html_template_editor/views.py new file mode 100644 index 000000000..2c4a317a9 --- /dev/null +++ b/apps/html_template_editor/views.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from rest_framework import filters, generics +from rest_framework.parsers import FormParser, MultiPartParser + +from .models import TemplateContent +from .serializers import TemplateContentSerializer + +# +# Mixin for all company views. +# Defines serializers, queryset and permissions +# + + +class TemplateContentMixin(object): + def get_queryset(self): + return TemplateContent.objects.filter() + + def get_serializer_class(self): + return TemplateContentSerializer + + +class TemplateContentAdd(TemplateContentMixin, generics.CreateAPIView): + parser_classes = (MultiPartParser, FormParser,) + + def perform_create(self, serializer): + regions = self.request.data.get('regions') + images = self.request.data.get('images') + page = self.request.data.get('page') + styles = self.request.data.get('styles') + + serializer.save( + regions=regions, + images=images, + page=page, + styles=styles, + ) + + +class TemplateContentRetrieve(TemplateContentMixin, generics.RetrieveAPIView): + lookup_field = 'uuid' + + +class TemplateContentUpdate(TemplateContentMixin, generics.UpdateAPIView): + def post(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def perform_update(self, serializer): + serializer.save() + + +class TemplateContentList(TemplateContentMixin, generics.ListAPIView): + filter_backends = (filters.OrderingFilter,) + ordering_fields = ('created') + ordering = ('-created',) + + +class TemplateContentDelete(TemplateContentMixin, generics.DestroyAPIView): + lookup_field = 'uuid' diff --git a/bin/post_compile b/bin/post_compile index 7c55378c1..fa30ca1d2 100755 --- a/bin/post_compile +++ b/bin/post_compile @@ -3,7 +3,8 @@ pip3 uninstall django-jsonfield -y pip3 uninstall jsonfield2 -y pip3 install jsonfield2 npm install bower && python manage.py bower install +npm install -g bower sass && python manage.py bower install python manage.py migrate +python manage.py build_contenttools python manage.py collectstatic --noinput -python3 manage.py compilemessages - +cd apps/aklub/ && pipenv run env DJANGO_SETTINGS_MODULE="" django-admin compilemessages diff --git a/local_migrations/migrations_admin/0011_auto_20191014_0818.py b/local_migrations/migrations_admin/0011_auto_20191014_0818.py new file mode 100644 index 000000000..71a896850 --- /dev/null +++ b/local_migrations/migrations_admin/0011_auto_20191014_0818.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.6 on 2019-10-14 06:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0010_auto_20190912_1254'), + ] + + operations = [ + migrations.AlterField( + model_name='logentry', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + ] diff --git a/local_migrations/migrations_admin/0012_auto_20191014_1320.py b/local_migrations/migrations_admin/0012_auto_20191014_1320.py new file mode 100644 index 000000000..d62e3abaa --- /dev/null +++ b/local_migrations/migrations_admin/0012_auto_20191014_1320.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.6 on 2019-10-14 11:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0011_auto_20191014_0818'), + ] + + operations = [ + migrations.AlterField( + model_name='logentry', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + ] diff --git a/local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py b/local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py new file mode 100644 index 000000000..97b828b99 --- /dev/null +++ b/local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.6 on 2019-10-14 06:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('advanced_filters', '0009_auto_20190912_1254'), + ] + + operations = [ + migrations.AlterField( + model_name='advancedfilter', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_advanced_filters', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='advancedfilter', + name='users', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'), + ), + ] diff --git a/local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py b/local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py new file mode 100644 index 000000000..80f590bfb --- /dev/null +++ b/local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.6 on 2019-10-14 11:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('advanced_filters', '0010_auto_20191014_0818'), + ] + + operations = [ + migrations.AlterField( + model_name='advancedfilter', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_advanced_filters', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='advancedfilter', + name='users', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'), + ), + ] diff --git a/project/settings/base.py b/project/settings/base.py index 744b4dad1..8ca301d70 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -173,6 +173,7 @@ def normpath(*args): LOCALE_PATHS = [ normpath(PROJECT_ROOT, 'apps/aklub/locale'), + normpath(PROJECT_ROOT, 'apps/html_template_editor/locale'), ] USE_L10N = True @@ -249,6 +250,10 @@ def normpath(*args): "pinax.invitations", "pinax.teams", "reversion", + 'rest_framework_swagger', + 'rest_framework.authtoken', + 'js_urls', + 'html_template_editor', ) REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ # noqa @@ -289,6 +294,11 @@ def normpath(*args): 'jquery-ui#~1.12.1', 'd3#3.3.13', 'nvd3#1.7.1', + 'ContentTools#1.6.10', + 'https://github.com/Karl33to/jquery.inlineStyler.git#1.0.1', + 'webui-popover#2.1.15', + 'jquery-mdl', + 'https://github.com/PitPik/colorPicker.git' ) AUTH_PASSWORD_VALIDATORS = [ @@ -440,7 +450,7 @@ def get_helpdesk_query_class(): SECURE_HSTS_SECONDS = 60 SECURE_HSTS_PRELOAD = True SESSION_COOKIE_SECURE = True -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = 'SAMEORIGIN' BROKER_URL = os.environ.get('REDIS_URL', 'redis://redis') SMMAPDFS_CELERY = True @@ -528,3 +538,38 @@ def get_payment_channel_resource(): 'companyprofile': ['is_staff', 'is_superuser', 'user_permissions', 'groups'], }, } + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAdminUser', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser', + ), + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + } + +SWAGGER_SETTINGS = { + 'is_authenticated': False, + 'is_superuser': False, +} + +JS_URLS = ( + 'html_template_editor:add', + 'html_template_editor:images_add', + 'html_template_editor:images_update', + 'aklub:get_email_template', + 'aklub:get_email_template_from_db', + 'aklub:get_email_template_footer', + 'aklub:get_contenttools_translation', +) + +STATICFILES_STORAGE = 'project.settings.custom_storage.CustomStaticFilesStorage' diff --git a/project/settings/custom_storage.py b/project/settings/custom_storage.py new file mode 100644 index 000000000..66495de97 --- /dev/null +++ b/project/settings/custom_storage.py @@ -0,0 +1,91 @@ +import os +import posixpath +import re +from collections import namedtuple +from urllib.parse import unquote, urldefrag + +from django.conf import settings +from django.contrib.staticfiles import storage + + +class CustomStaticFilesStorage(storage.StaticFilesStorage): + + def __handle_comments(self, matchobj, matched): + Position = namedtuple('Position', 'start end') + + # Ignore css statement, rule which is inside css comment + comment_regex = re.compile('/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/') + comments = re.finditer(comment_regex, matchobj.string) + + for comment in comments: + if matched in comment.group(0): + comment_position = Position(*comment.span()) + matched_inside_comment = re.search(re.escape(matched), comment.group(0)) + + matched_position_inside_comment = Position(*matched_inside_comment.span()) + + comment_position = comment_position._replace( + start=comment_position.start + matched_position_inside_comment.start, + ) + + if matchobj.span()[0] == comment_position.start: + return matched + + def url_converter(self, name, hashed_files, template=None): + """ + Return the custom URL converter for the given file name. + """ + if template is None: + template = self.default_template + + def converter(matchobj): + """ + Convert the matched URL to a normalized and hashed URL. + + This requires figuring out which files the matched URL resolves + to and calling the url() method of the storage. + """ + matched, url = matchobj.groups() + + # Ignore absolute/protocol-relative and data-uri URLs. + if re.match(r'^[a-z]+:', url): + return matched + + # Ignore absolute URLs that don't point to a static file (dynamic + # CSS / JS?). Note that STATIC_URL cannot be empty. + if url.startswith('/') and not url.startswith(settings.STATIC_URL): + return matched + + # Jump over comment + comment = self.__handle_comments(matchobj, matched) + if comment: + return comment + + # Strip off the fragment so a path-like fragment won't interfere. + url_path, fragment = urldefrag(url) + + if url_path.startswith('/'): + # Otherwise the condition above would have returned prematurely. + assert url_path.startswith(settings.STATIC_URL) + target_name = url_path[len(settings.STATIC_URL):] + else: + # We're using the posixpath module to mix paths and URLs conveniently. + source_name = name if os.sep == '/' else name.replace(os.sep, '/') + target_name = posixpath.join(posixpath.dirname(source_name), url_path) + + # Determine the hashed name of the target file with the storage backend. + hashed_url = self._url( + self._stored_name, unquote(target_name), + force=True, hashed_files=hashed_files, + ) + + transformed_url = '/'.join(url_path.split('/')[:-1] + hashed_url.split('/')[-1:]) + + # Restore the fragment that was stripped off earlier. + if fragment: + transformed_url += ('?#' if '?#' in url else '#') + fragment + + # Return the hashed version to the file + return template % unquote(transformed_url) + + return converter diff --git a/project/settings/heroku.py b/project/settings/heroku.py index 7420a2240..bc540bace 100644 --- a/project/settings/heroku.py +++ b/project/settings/heroku.py @@ -48,3 +48,5 @@ ) django_heroku.settings(locals()) + +STATICFILES_STORAGE = 'project.settings.custom_storage.CustomStaticFilesStorage' diff --git a/project/urls.py b/project/urls.py index b0c39314d..d453d9feb 100644 --- a/project/urls.py +++ b/project/urls.py @@ -6,11 +6,18 @@ from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth import views as auth_views -from django.urls import path +from django.urls import path, re_path from django.views.i18n import JavaScriptCatalog +from js_urls.views import JsUrlsView + +from rest_framework.authtoken.views import obtain_auth_token + +from rest_framework_swagger.views import get_swagger_view admin.autodiscover() +schema_view = get_swagger_view(title='API') + urlpatterns = [ url(r'^desk/', include("helpdesk.urls")), @@ -26,17 +33,29 @@ url(r'^admin/aklub/stat-payments/', stat_payments, name="stat-payments"), url(r'^', admin.site.urls), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^jsi18n', JavaScriptCatalog.as_view()), + url(r'^admin/', include("massadmin.urls")), + url(r'^jsi18n', JavaScriptCatalog.as_view(), name='jsi18n'), url(r'^tinymce/', include('tinymce.urls')), url(r'^admin_tools/', include('admin_tools.urls')), url(r'^adminactions/', include('adminactions.urls')), url(r'^advanced_filters/', include('advanced_filters.urls')), url(r'^nested_admin/', include('nested_admin.urls')), path('admin_tools_stats/', include('admin_tools_stats.urls')), - url(r'', include("aklub.urls")), path('notifications/', include('django_nyt.urls')), path('help/', include('wiki.urls')), path('api/', include('api.urls')), + url(r'', include(('aklub.urls', 'aklub'), namespace='aklub')), + re_path( + r'^html_template_editor/', + include( + ('html_template_editor.urls', 'html_template_editor'), + namespace='html_template_editor', + ), + ), + re_path(r'^docs/', schema_view), + re_path(r'^api-auth/', include('rest_framework.urls')), + re_path(r'^token-auth/', obtain_auth_token), + re_path(r'^js-urls/$', JsUrlsView.as_view(), name='js_urls'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += i18n_patterns( diff --git a/runtests.sh b/runtests.sh index d7b9e2db5..17dca1f49 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,9 +1,10 @@ #!/bin/bash -e if [ -z "$@" ]; then - coverage run manage.py test aklub - coverage run manage.py test api - coverage run manage.py test pdf_storage + coverage run manage.py test aklub + coverage run manage.py test api + coverage run manage.py test pdf_storage + coverage run manage.py test html_template_editor else - coverage run manage.py test $@ + coverage run manage.py test $@ fi coverage html