├── .DS_Store ├── .dockerignore ├── .gitignore ├── .python-version ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── ReleaseNotes.txt ├── dev_static └── site │ ├── css │ ├── base-admin-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── font-awesome-ie7.css │ ├── font-awesome-ie7.min.css │ ├── font-awesome.css │ ├── font-awesome.min.css │ ├── pages │ │ ├── channels.css │ │ ├── dashboard.css │ │ ├── faq.css │ │ ├── plans.css │ │ ├── reports.css │ │ ├── search.css │ │ └── signin.css │ └── style.css │ ├── font │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.svgz │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfontd41d.eot │ ├── img │ ├── apps.png │ ├── appstore.jpeg │ ├── arduino_logo_40.png │ ├── beta_testing.jpeg │ ├── body-bg.png │ ├── channel_lock.png │ ├── chrome-icon-64.png │ ├── chrome-web-store.png │ ├── chrome_extension.png │ ├── device_android.png │ ├── device_chrome.png │ ├── device_ios.png │ ├── device_safari.png │ ├── device_windows.png │ ├── favicon.ico │ ├── glyphicons-halflings-white.html │ ├── glyphicons-halflings.html │ ├── icons-sa7c41345d9.png │ ├── java_logo_40.png │ ├── loader.gif │ ├── loading-wheel.gif │ ├── logo_100.png │ ├── logo_100_obscured.png │ ├── logo_50.png │ ├── message_avatar1.png │ ├── message_avatar2.png │ ├── net_logo_40.png │ ├── new_label.png │ ├── php_logo_40.png │ ├── playstore.png │ ├── powershell_logo_40.png │ ├── python_logo_40.png │ ├── safari-icon-64.png │ ├── signin │ │ ├── check.png │ │ ├── fb_btn.png │ │ ├── password.png │ │ ├── twitter_btn.png │ │ └── user.png │ ├── webicon-facebook.png │ ├── webicon-googleplus.png │ ├── webicon-pinterest.png │ ├── webicon-rss.png │ ├── webicon-twitter.png │ └── webicon-youtube.png │ └── js │ ├── base.js │ ├── bootstrap.file-input.js │ ├── bootstrap.js │ ├── chart.min.js │ ├── charts │ ├── area.js │ ├── bar.js │ ├── donut.js │ ├── line.js │ └── pie.js │ ├── cookie_banner.js │ ├── excanvas.min.js │ ├── faq.js │ ├── full-calendar │ ├── fullcalendar.css │ └── fullcalendar.min.js │ ├── guidely │ ├── guidely-number.png │ ├── guidely.css │ └── guidely.min.js │ ├── jquery-1.7.2.min.js │ ├── jquery.fileupload.js │ ├── jquery.iframe-transport.js │ ├── jquery.ui.widget.js │ ├── pushetta.js │ └── signin.js ├── docker-compose.yml ├── docker-entrypoint.sh ├── docker_push ├── pushetta ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── channels_sl.py │ ├── crashes_sl.py │ ├── feedback_sl.py │ ├── messages_sl.py │ ├── models.py │ ├── permissions.py │ ├── publisher_sl.py │ ├── pushes_sl.py │ ├── serializers.py │ ├── subscriber_sl.py │ ├── sys_sl.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── feedback_manager.py │ ├── fileds_validators.py │ ├── models.py │ ├── push_manager.py │ ├── push_providers │ │ ├── AndroidPushProvider.py │ │ ├── MQTTPushProvider.py │ │ ├── SafariPushProvider.py │ │ ├── TestPushProvider.py │ │ ├── WP8PushProvider.py │ │ ├── __init__.py │ │ ├── common.py │ │ ├── iOSPushProvider.py │ │ └── test.py │ ├── search_indexes.py │ ├── security_helpers.py │ ├── services.py │ ├── signal.py │ ├── subscriber_manager.py │ ├── tasks.py │ ├── test.py │ ├── utility.py │ └── utils.py ├── manage.py ├── pushetta │ ├── __init__.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ ├── uwsgi.ini │ └── wsgi.py └── www │ ├── __init__.py │ ├── apps.py │ ├── browser_views.py │ ├── dispatcher_view.py │ ├── forms.py │ ├── my_views.py │ ├── safari_views.py │ ├── templatetags │ ├── __init__.py │ ├── activemenu.py │ └── storelink.py │ ├── test.py │ ├── upload_views.py │ └── urls.py ├── requirements.txt └── templates ├── account ├── account_inactive.html ├── base.html ├── email.html ├── email │ ├── email_confirmation_message.txt │ ├── email_confirmation_signup_message.txt │ ├── email_confirmation_signup_subject.txt │ ├── email_confirmation_subject.txt │ ├── password_reset_key_message.txt │ └── password_reset_key_subject.txt ├── email_confirm.html ├── email_confirmed.html ├── login.html ├── logout.html ├── messages │ ├── cannot_delete_primary_email.txt │ ├── email_confirmation_sent.txt │ ├── email_confirmed.txt │ ├── email_deleted.txt │ ├── logged_in.txt │ ├── logged_out.txt │ ├── password_changed.txt │ ├── password_set.txt │ ├── primary_email_set.txt │ └── unverified_primary_email.txt ├── password_change.html ├── password_reset.html ├── password_reset_done.html ├── password_reset_from_key.html ├── password_reset_from_key_done.html ├── password_set.html ├── signup.html ├── signup_closed.html ├── snippets │ └── already_logged_in.html ├── verification_sent.html └── verified_email_required.html ├── email └── email_subscribe_request.txt ├── rest_framework_swagger └── index.html ├── search └── indexes │ └── core │ └── channel_text.txt └── www ├── 404.html ├── 500.html ├── __base.html ├── __bootstrap.html ├── android-subscribe.html ├── api-docs.html ├── api.html ├── apps.html ├── channel-dispatcher.html ├── chrome-subscribe.html ├── cookie_policy.html ├── dashboard.html ├── docs.html ├── downloads.html ├── index.html ├── ios-subscribe.html ├── my_channel-add.html ├── my_channel-view.html ├── my_channels.html ├── partials └── _messages_list.html ├── robots.txt ├── safari-subscribe.html └── search.html /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Sensitive or high-churn files 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | .idea/**/dbnavigator.xml 20 | 21 | # Gradle 22 | .idea/**/gradle.xml 23 | .idea/**/libraries 24 | 25 | # CMake 26 | cmake-build-debug/ 27 | cmake-build-release/ 28 | 29 | # Mongo Explorer plugin 30 | .idea/**/mongoSettings.xml 31 | 32 | # File-based project format 33 | *.iws 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | # Editor-based Rest Client 54 | .idea/httpRequests 55 | ### Python template 56 | # Byte-compiled / optimized / DLL files 57 | __pycache__/ 58 | *.py[cod] 59 | *$py.class 60 | 61 | # C extensions 62 | *.so 63 | 64 | # Distribution / packaging 65 | .Python 66 | build/ 67 | develop-eggs/ 68 | dist/ 69 | downloads/ 70 | eggs/ 71 | .eggs/ 72 | lib/ 73 | lib64/ 74 | parts/ 75 | sdist/ 76 | var/ 77 | wheels/ 78 | *.egg-info/ 79 | .installed.cfg 80 | *.egg 81 | MANIFEST 82 | 83 | # PyInstaller 84 | # Usually these files are written by a python script from a template 85 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 86 | *.manifest 87 | *.spec 88 | 89 | # Installer logs 90 | pip-log.txt 91 | pip-delete-this-directory.txt 92 | 93 | # Unit test / coverage reports 94 | htmlcov/ 95 | .tox/ 96 | .coverage 97 | .coverage.* 98 | .cache 99 | nosetests.xml 100 | coverage.xml 101 | *.cover 102 | .hypothesis/ 103 | .pytest_cache/ 104 | 105 | # Translations 106 | *.mo 107 | *.pot 108 | 109 | # Django stuff: 110 | *.log 111 | local_settings.py 112 | db.sqlite3 113 | 114 | # Flask stuff: 115 | instance/ 116 | .webassets-cache 117 | 118 | # Scrapy stuff: 119 | .scrapy 120 | 121 | # Sphinx documentation 122 | docs/_build/ 123 | 124 | # PyBuilder 125 | target/ 126 | 127 | # Jupyter Notebook 128 | .ipynb_checkpoints 129 | 130 | # pyenv 131 | .python-version 132 | 133 | # celery beat schedule file 134 | celerybeat-schedule 135 | 136 | # SageMath parsed files 137 | *.sage.py 138 | 139 | # Environments 140 | .env 141 | .venv 142 | env/ 143 | venv/ 144 | ENV/ 145 | env.bak/ 146 | venv.bak/ 147 | 148 | # Spyder project settings 149 | .spyderproject 150 | .spyproject 151 | 152 | # Rope project settings 153 | .ropeproject 154 | 155 | # mkdocs documentation 156 | /site 157 | 158 | # mypy 159 | .mypy_cache/ 160 | 161 | *.env 162 | 163 | docker_push 164 | docker-compose.yml 165 | Dockerfile 166 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | 8 | # Python 9 | __pycache__/ 10 | *.py[cod] 11 | *.so 12 | whoosh_index/ 13 | bin/ 14 | uploads/ 15 | log/ 16 | include/ 17 | woosh/ 18 | solr/ 19 | local/ 20 | lib/ 21 | static/ 22 | .idea/ 23 | migrations/ 24 | venv/.Python 25 | venv/pip-selfcheck.json 26 | /log/ 27 | *.env 28 | /venv/ 29 | *.log 30 | 31 | # persistent volumes 32 | volumes/ 33 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | pushetta 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | install: [] 7 | 8 | script: 9 | - export TAG_VER=`cat pushetta/pushetta/__init__.py|grep __version__| awk '{split($0, a, "="); print a[2] }'|sed "s/'//g"| sed "s/ //g"` 10 | - docker build -t "guglielmino/pushetta-api:$TAG_VER" . 11 | 12 | deploy: 13 | provider: script 14 | script: bash docker_push $TAG_VER 15 | on: 16 | branch: master 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Django", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/pushetta/manage.py", 12 | "args": [ 13 | "runserver", 14 | "--noreload" 15 | ], 16 | "django": true 17 | 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.enabled": false, 3 | "python.formatting.provider": "autopep8", 4 | "python.pythonPath": "/Users/guglielmino/.pyenv/versions/pushetta/bin/python" 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.15-alpine3.6 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN set -e; \ 6 | apk add --no-cache --virtual .build-deps \ 7 | gcc \ 8 | libc-dev \ 9 | linux-headers \ 10 | mariadb-dev \ 11 | libffi-dev \ 12 | libxml2-dev 13 | 14 | RUN adduser -D -g '' pushetta 15 | 16 | WORKDIR /usr/src/app 17 | 18 | COPY requirements.txt ./ 19 | # pip doesn't follow requirements file order then the following hack 20 | RUN cat requirements.txt | xargs -n 1 pip install --no-cache-dir 21 | COPY . . 22 | COPY dev_static/ static/ 23 | 24 | RUN chown -R pushetta /usr/src/app 25 | 26 | #USER pushetta 27 | 28 | EXPOSE 8001 29 | 30 | ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] 31 | CMD ["uwsgi", "--ini", "/usr/src/app/pushetta/pushetta/uwsgi.ini"] 32 | 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARG := $(word 2, $(MAKECMDGOALS) ) 2 | 3 | activate: 4 | pyenv local 5 | 6 | install: 7 | pip install -r requirements.txt 8 | 9 | compose-services: 10 | docker-compose up -d db elastic redis mosquitto 11 | 12 | compose-full: 13 | docker-compose up -d 14 | 15 | start: 16 | python pushetta/manage.py runserver 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pushetta is a system made to push realtime notifications to many different devices 4 | (mobile phones, browsers, smart TV, ...). 5 | It addresses a simple problem: get realtime information without using outdated methods. 6 | -------------------------------------------------------------------------------- /ReleaseNotes.txt: -------------------------------------------------------------------------------- 1 | Release 1.1.5 2 | ------------- 3 | - Fix FCM payload format to handle tray area message 4 | 5 | Release 1.1.4 6 | ------------- 7 | - Refactor Android push provider to manage FCM data payload 8 | 9 | Release 1.1.2 10 | ------------- 11 | - Renamed PUSHETTA_ALLOWED_HOST in PUSHETTA_ALLOWED_HOSTS supporting multiple hostnames (comma separated) 12 | 13 | Release 1.1.1 14 | ------------- 15 | - Add PUSHETTA_ALLOWED_HOST to defining binding hostname from env var 16 | 17 | Release 1.1.0 18 | ------------- 19 | - Migration to Django 1.8.19 20 | - Add Dockerfile for application 21 | - Add docker-compose for full environment 22 | 23 | Release 1.0.2 24 | ------------- 25 | - Target nel push (per discriminare a quale target inviare) INCOMPLETE 26 | - Grab della url nel messaggio INCOMPLETE 27 | - Push con MQTT INCOMPLETE/TESTING 28 | 29 | Release 1.0.1 30 | ------------- 31 | - Codice per ethernet shield 32 | - Push su browser Safari 33 | - Push su browser chrome (con extension) 34 | - Packaging della lib per Python -------------------------------------------------------------------------------- /dev_static/site/css/base-admin-responsive.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dev_static/site/css/pages/channels.css: -------------------------------------------------------------------------------- 1 | 2 | /*------------------------------------------------------------------ 3 | Channels List 4 | */ 5 | 6 | .channels-items li { 7 | display: table; 8 | padding: 0 2em 0 1.5em; 9 | padding-bottom: 1em; 10 | margin-bottom: 1em; 11 | margin-left: 2em; 12 | margin-top: 2em; 13 | border-bottom: 1px dotted #CCC; 14 | width: 80%; 15 | 16 | } 17 | 18 | .channels-items li:last-child { padding-bottom: 0; border: none; } 19 | 20 | .channels-item-date { 21 | display: table-cell; 22 | } 23 | 24 | .channels-item-detail { 25 | display: table-cell; 26 | width: 45%; 27 | } 28 | 29 | .channels-item-title { 30 | font-size: 13px; 31 | font-weight: 600; 32 | } 33 | 34 | .channels-item-date { 35 | width: 75px; 36 | vertical-align: middle; 37 | text-align: center; 38 | 39 | } 40 | 41 | .channels-item-day { 42 | display: block; 43 | margin-bottom: .25em; 44 | 45 | font-size: 24px; 46 | color: #888; 47 | } 48 | 49 | .channels-item-preview { 50 | margin-bottom: 0; 51 | 52 | color: #777; 53 | } 54 | 55 | .channels-item-month { 56 | display: block; 57 | padding-right: 1px; 58 | 59 | font-size: 12px; 60 | font-weight: 600; 61 | color: #888; 62 | } 63 | 64 | 65 | .channels-properties li 66 | { 67 | display: inline; 68 | list-style-type: none; 69 | padding-right: 20px; 70 | font-size: 30px; 71 | } 72 | 73 | -------------------------------------------------------------------------------- /dev_static/site/css/pages/faq.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | 8 | .faq-search { 9 | margin-bottom: 2em; 10 | 11 | text-align: right; 12 | } 13 | 14 | .faq-search input { 15 | width: 96%; 16 | display: block; 17 | padding: 2%; 18 | } 19 | 20 | 21 | 22 | .faq-empty { 23 | display: none; 24 | } 25 | 26 | 27 | 28 | .faq-toc { 29 | padding: 1.5em 0; 30 | margin: 2em 0 0; 31 | 32 | border: 1px dotted #CCC; 33 | border-right: none; 34 | border-left: none; 35 | } 36 | 37 | .faq-toc ol { 38 | padding: 0; 39 | margin: 0; 40 | } 41 | 42 | .faq-toc li { 43 | margin-bottom: .75em; 44 | 45 | list-style: none; 46 | } 47 | 48 | .faq-toc a { 49 | margin-left: .5em; 50 | } 51 | 52 | 53 | 54 | .faq-list { 55 | padding: 0; 56 | margin: 3em 0 0; 57 | 58 | list-style: none; 59 | } 60 | 61 | .faq-list li { 62 | display: table; 63 | margin-bottom: 2em; 64 | } 65 | 66 | .faq-icon { 67 | display: table-cell; 68 | padding-right: 1.25em; 69 | vertical-align: top; 70 | } 71 | 72 | .faq-text { 73 | display: table-cell; 74 | vertical-align: top; 75 | 76 | } 77 | 78 | 79 | .faq-number { 80 | width: 32px; 81 | height: 32px; 82 | 83 | font-size: 14px; 84 | font-weight: 600; 85 | text-align: center; 86 | line-height: 32px; 87 | color: #FFF; 88 | 89 | background: #00ba8b; 90 | 91 | border: 3px solid #FFF; 92 | 93 | 94 | 95 | border-radius: 100px; 96 | 97 | 98 | } 99 | 100 | 101 | 102 | 103 | .btn-support-ask { 104 | display: block; font-size: 22px; padding: 14px 0; font-weight: 600; margin-bottom: .75em; 105 | } 106 | 107 | .btn-support-contact { 108 | display: block; padding: 12px 0; font-size: 18px; font-weight: 600; 109 | } -------------------------------------------------------------------------------- /dev_static/site/css/pages/reports.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | .info-box { 6 | background:#ffffff; 7 | border:1px solid #c9c9c9; 8 | -webkit-border-radius: 3px; 9 | -moz-border-radius: 3px; 10 | border-radius: 3px; 11 | 12 | margin-bottom: 30px; 13 | } 14 | 15 | .stats-box { 16 | margin:40px 0px; 17 | color:#5f5f5f; 18 | } 19 | .stats-box-title { 20 | text-align:center; 21 | font-weight:bold; 22 | } 23 | .stats-box-all-info { 24 | text-align:center; 25 | font-weight:bold; 26 | font-size:48px; 27 | margin-top:20px; 28 | margin-bottom: 40px; 29 | } 30 | .stats-box-all-info i{ 31 | width:60px; 32 | height:60px; 33 | } -------------------------------------------------------------------------------- /dev_static/site/css/pages/search.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | /*------------------------------------------------------------------ 8 | [1. Search] 9 | */ 10 | 11 | #channel-search-form { 12 | margin: 0; 13 | margin-top: 5px; 14 | padding: 0; 15 | text-align: center; 16 | padding-bottom: 50px; 17 | } 18 | #channel-search-form .search-query { 19 | padding-right: 3px; 20 | padding-right: 4px \9; 21 | padding-left: 3px; 22 | padding-left: 4px \9; 23 | /* IE7-8 doesn't have border-radius, so don't indent the padding */ 24 | margin-bottom: 0; 25 | -webkit-border-radius: 3px; 26 | -moz-border-radius: 3px; 27 | border-radius: 3px; 28 | width: 80%; 29 | height: 24px; 30 | font-size: 22px; 31 | } 32 | #channel-search-form button { 33 | border: 0; 34 | background: none; 35 | /** belows styles are working good */ 36 | padding: 2px 5px; 37 | margin-top: 2px; 38 | position: relative; 39 | left: -28px; 40 | /* IE7-8 doesn't have border-radius, so don't indent the padding */ 41 | margin-bottom: 0; 42 | -webkit-border-radius: 3px; 43 | -moz-border-radius: 3px; 44 | border-radius: 3px; 45 | } 46 | .search-query:focus + button { z-index: 3 } 47 | /* do not group these rules */ 48 | *::-webkit-input-placeholder { 49 | font-size: 20px; 50 | line-height: normal; 51 | } 52 | *:-moz-placeholder { 53 | /* FF 4-18 */ 54 | font-size: 20px; 55 | line-height: normal; 56 | } 57 | *::-moz-placeholder { 58 | /* FF 19+ */ 59 | font-size: 20px; 60 | line-height: normal; 61 | } 62 | *:-ms-input-placeholder { 63 | /* IE 10+ */ 64 | font-size: 20px; 65 | line-height: normal; 66 | } -------------------------------------------------------------------------------- /dev_static/site/css/pages/signin.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------ 2 | Bootstrap Admin Template by EGrappler.com 3 | ------------------------------------------------------------------*/ 4 | 5 | 6 | 7 | 8 | /** Base Body Styles **/ 9 | body{ background: url(../../img/body-bg.png); color:#838383; font: 13px/1.7em 'Open Sans';} 10 | 11 | 12 | .account-container { 13 | width: 380px; 14 | display: block; 15 | margin: 60px auto 0 auto; 16 | 17 | background: #f9f9f9; 18 | 19 | border: 1px solid #d5d5d5; 20 | 21 | -webkit-border-radius: 5px; 22 | -moz-border-radius: 5px; 23 | border-radius: 5px; 24 | 25 | box-shadow: 0px 0px 2px #dadada, inset 0px -3px 0px #e6e6e6; 26 | } 27 | 28 | .content { 29 | padding: 16px 28px 23px; 30 | } 31 | 32 | .login-fields { 33 | 34 | } 35 | 36 | .login-fields .field { 37 | margin-bottom: 1.25em; 38 | } 39 | 40 | .login-fields label { 41 | display: none; 42 | } 43 | 44 | .login-fields input { 45 | font-family: 'Open Sans'; 46 | font-size: 13px; 47 | color: #8e8d8d; 48 | padding: 11px 15px 10px 50px; 49 | background-color: #fdfdfd; 50 | width: 255px; 51 | display: block; 52 | margin: 0; 53 | box-shadow: inset 2px 2px 4px #f1f1f1; 54 | } 55 | 56 | .username-field { background: url(../../img/signin/user.png) no-repeat; } 57 | 58 | .password-field { background: url(../../img/signin/password.png) no-repeat; } 59 | 60 | 61 | 62 | 63 | .login-actions { 64 | float: left; 65 | 66 | width: 100%; 67 | 68 | margin-top: -1em; 69 | margin-bottom: 1.25em; 70 | } 71 | 72 | .login-social { 73 | float: left; 74 | 75 | padding: 10px 0 15px; 76 | 77 | border: 1px dotted #CCC; 78 | border-right: none; 79 | border-left: none; 80 | } 81 | 82 | span.login-checkbox { 83 | float: left; 84 | margin-top: 31px; 85 | } 86 | 87 | span.login-checkbox > input[type='checkbox'] { 88 | opacity: 0; 89 | float: left; 90 | width: 15px; 91 | } 92 | 93 | span.login-checkbox > input[type='checkbox'] + label { 94 | clear: none; 95 | 96 | height: 15px; 97 | display: block; 98 | padding: 0 0 0 22px; 99 | margin: 0; 100 | 101 | font-size: 12px; 102 | line-height: 1.2em; 103 | 104 | background: url(../../img/signin/check.png) no-repeat 0 0; 105 | 106 | cursor: pointer; 107 | } 108 | 109 | span.login-checkbox > input[type='checkbox']:checked + label { 110 | background-position: 0 -15px; 111 | } 112 | 113 | /** Text Under Box**/ 114 | .login-extra { 115 | display: block; 116 | width: 300px; 117 | margin: 1.5em auto; 118 | 119 | text-align: left; 120 | line-height: 19px; 121 | 122 | text-shadow: 1px 1px 0px #fff; 123 | } 124 | 125 | 126 | .account-container h1 { 127 | margin-bottom: .4em; 128 | 129 | color: #525252; 130 | } 131 | 132 | /** Buttons **/ 133 | .twitter, .fb { 134 | position: relative; 135 | 136 | height: 32px; 137 | width: 157px; 138 | display: block; 139 | 140 | background: url(../../img/signin/twitter_btn.png) no-repeat; 141 | 142 | } 143 | 144 | .fb { 145 | width: 162px; 146 | 147 | background: url(../../img/signin/fb_btn.png) no-repeat; 148 | } 149 | 150 | .twitter:active, .fb:active { 151 | top: 1px; 152 | } 153 | 154 | .twitter:hover, .fb:hover { 155 | background-position: 0 -32px; 156 | } 157 | 158 | .twitter a, .fb a { 159 | padding: 5px 0 0 35px; 160 | text-shadow: -1px -1px 0px rgba(0,0,0,.3); 161 | color:#fff; 162 | font-weight: bold; 163 | font-size: 11px; 164 | height: 32px; 165 | display: block; 166 | } 167 | 168 | .fb a { 169 | padding: 5px 0 0 31px; 170 | 171 | } 172 | 173 | .twitter, .fb { 174 | display: inline-block; 175 | } 176 | 177 | .twitter a:hover, .fb a:hover { 178 | color: #FFF; 179 | text-decoration: none; 180 | } 181 | 182 | .button {-webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; display: inline-block; float: right; margin-top: 18px;} 183 | 184 | 185 | 186 | 187 | 188 | 189 | .register .login-social { 190 | margin-bottom: 1em; 191 | } 192 | 193 | .register .login-actions { 194 | margin-bottom: 0; 195 | } 196 | 197 | .register .login-fields input { 198 | width: 299px; 199 | padding-left: 6px; 200 | } 201 | 202 | .register h1 { 203 | color: #444; 204 | } 205 | 206 | .register span.login-checkbox { 207 | position: relative; 208 | top: -6px; 209 | 210 | width: 200px; 211 | } 212 | 213 | .register span.login-checkbox > input[type="checkbox"] + label { 214 | 215 | position: relative; 216 | 217 | line-height: 1.3em; 218 | } 219 | 220 | 221 | 222 | @media (max-width: 480px) { 223 | 224 | .account-container { 225 | width: 280px; 226 | margin-top: 35px; 227 | } 228 | 229 | .login-fields input { 230 | width: 160px; 231 | } 232 | 233 | .login-social { 234 | width: 100%; 235 | } 236 | 237 | .twitter { 238 | display: block; 239 | margin-bottom: 1em; 240 | } 241 | 242 | .register .login-fields input { 243 | width: 204px; 244 | padding-left: 6px; 245 | } 246 | 247 | } -------------------------------------------------------------------------------- /dev_static/site/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/font/FontAwesome.otf -------------------------------------------------------------------------------- /dev_static/site/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /dev_static/site/font/fontawesome-webfont.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/font/fontawesome-webfont.svgz -------------------------------------------------------------------------------- /dev_static/site/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /dev_static/site/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /dev_static/site/font/fontawesome-webfontd41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/font/fontawesome-webfontd41d.eot -------------------------------------------------------------------------------- /dev_static/site/img/apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/apps.png -------------------------------------------------------------------------------- /dev_static/site/img/appstore.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/appstore.jpeg -------------------------------------------------------------------------------- /dev_static/site/img/arduino_logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/arduino_logo_40.png -------------------------------------------------------------------------------- /dev_static/site/img/beta_testing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/beta_testing.jpeg -------------------------------------------------------------------------------- /dev_static/site/img/body-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/body-bg.png -------------------------------------------------------------------------------- /dev_static/site/img/channel_lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/channel_lock.png -------------------------------------------------------------------------------- /dev_static/site/img/chrome-icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/chrome-icon-64.png -------------------------------------------------------------------------------- /dev_static/site/img/chrome-web-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/chrome-web-store.png -------------------------------------------------------------------------------- /dev_static/site/img/chrome_extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/chrome_extension.png -------------------------------------------------------------------------------- /dev_static/site/img/device_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/device_android.png -------------------------------------------------------------------------------- /dev_static/site/img/device_chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/device_chrome.png -------------------------------------------------------------------------------- /dev_static/site/img/device_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/device_ios.png -------------------------------------------------------------------------------- /dev_static/site/img/device_safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/device_safari.png -------------------------------------------------------------------------------- /dev_static/site/img/device_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/device_windows.png -------------------------------------------------------------------------------- /dev_static/site/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/favicon.ico -------------------------------------------------------------------------------- /dev_static/site/img/glyphicons-halflings-white.html: -------------------------------------------------------------------------------- 1 | 2 | 404 Not Found 3 | 4 |

404 Not Found

5 |
nginx
6 | 7 | 8 | -------------------------------------------------------------------------------- /dev_static/site/img/glyphicons-halflings.html: -------------------------------------------------------------------------------- 1 | 2 | 404 Not Found 3 | 4 |

404 Not Found

5 |
nginx
6 | 7 | 8 | -------------------------------------------------------------------------------- /dev_static/site/img/icons-sa7c41345d9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/icons-sa7c41345d9.png -------------------------------------------------------------------------------- /dev_static/site/img/java_logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/java_logo_40.png -------------------------------------------------------------------------------- /dev_static/site/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/loader.gif -------------------------------------------------------------------------------- /dev_static/site/img/loading-wheel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/loading-wheel.gif -------------------------------------------------------------------------------- /dev_static/site/img/logo_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/logo_100.png -------------------------------------------------------------------------------- /dev_static/site/img/logo_100_obscured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/logo_100_obscured.png -------------------------------------------------------------------------------- /dev_static/site/img/logo_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/logo_50.png -------------------------------------------------------------------------------- /dev_static/site/img/message_avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/message_avatar1.png -------------------------------------------------------------------------------- /dev_static/site/img/message_avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/message_avatar2.png -------------------------------------------------------------------------------- /dev_static/site/img/net_logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/net_logo_40.png -------------------------------------------------------------------------------- /dev_static/site/img/new_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/new_label.png -------------------------------------------------------------------------------- /dev_static/site/img/php_logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/php_logo_40.png -------------------------------------------------------------------------------- /dev_static/site/img/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/playstore.png -------------------------------------------------------------------------------- /dev_static/site/img/powershell_logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/powershell_logo_40.png -------------------------------------------------------------------------------- /dev_static/site/img/python_logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/python_logo_40.png -------------------------------------------------------------------------------- /dev_static/site/img/safari-icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/safari-icon-64.png -------------------------------------------------------------------------------- /dev_static/site/img/signin/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/signin/check.png -------------------------------------------------------------------------------- /dev_static/site/img/signin/fb_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/signin/fb_btn.png -------------------------------------------------------------------------------- /dev_static/site/img/signin/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/signin/password.png -------------------------------------------------------------------------------- /dev_static/site/img/signin/twitter_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/signin/twitter_btn.png -------------------------------------------------------------------------------- /dev_static/site/img/signin/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/signin/user.png -------------------------------------------------------------------------------- /dev_static/site/img/webicon-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/webicon-facebook.png -------------------------------------------------------------------------------- /dev_static/site/img/webicon-googleplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/webicon-googleplus.png -------------------------------------------------------------------------------- /dev_static/site/img/webicon-pinterest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/webicon-pinterest.png -------------------------------------------------------------------------------- /dev_static/site/img/webicon-rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/webicon-rss.png -------------------------------------------------------------------------------- /dev_static/site/img/webicon-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/webicon-twitter.png -------------------------------------------------------------------------------- /dev_static/site/img/webicon-youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/img/webicon-youtube.png -------------------------------------------------------------------------------- /dev_static/site/js/base.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | 4 | $('.subnavbar').find ('li').each (function (i) { 5 | 6 | var mod = i % 3; 7 | 8 | if (mod === 2) { 9 | $(this).addClass ('subnavbar-open-right'); 10 | } 11 | 12 | }); 13 | 14 | 15 | 16 | }); -------------------------------------------------------------------------------- /dev_static/site/js/charts/area.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | // we use an inline data source in the example, usually data would 3 | // be fetched from a server 4 | var data = [], totalPoints = 200; 5 | function getRandomData() { 6 | if (data.length > 0) 7 | data = data.slice(1); 8 | 9 | while (data.length < totalPoints) { 10 | var prev = data.length > 0 ? data[data.length - 1] : 50; 11 | var y = prev + Math.random() * 10 - 5; 12 | if (y < 0) 13 | y = 0; 14 | if (y > 100) 15 | y = 100; 16 | data.push(y); 17 | } 18 | 19 | var res = []; 20 | for (var i = 0; i < data.length; ++i) 21 | res.push([i, data[i]]) 22 | return res; 23 | } 24 | 25 | // setup plot 26 | var options = { 27 | yaxis: { min: 0, max: 100 }, 28 | xaxis: { min: 0, max: 100 }, 29 | colors: ["#F90", "#222", "#666", "#BBB"], 30 | series: { 31 | lines: { 32 | lineWidth: 2, 33 | fill: true, 34 | fillColor: { colors: [ { opacity: 0.6 }, { opacity: 0.2 } ] }, 35 | steps: false 36 | 37 | } 38 | } 39 | }; 40 | 41 | var plot = $.plot($("#area-chart"), [ getRandomData() ], options); 42 | }); -------------------------------------------------------------------------------- /dev_static/site/js/charts/bar.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var data = new Array (); 3 | var ds = new Array(); 4 | 5 | data.push ([[1,25],[2,34],[3,37],[4,45],[5,56]]); 6 | data.push ([[1,13],[2,29],[3,25],[4,23],[5,31]]); 7 | data.push ([[1,8],[2,13],[3,19],[4,15],[5,14]]); 8 | data.push ([[1,20],[2,43],[3,29],[4,23],[5,25]]); 9 | 10 | for (var i=0, j=data.length; i'+label+'
'+Math.round(series.percent)+'%'; 20 | }, 21 | threshold: 0.1 22 | } 23 | } 24 | }, 25 | legend: { 26 | show: true, 27 | noColumns: 1, // number of colums in legend table 28 | labelFormatter: null, // fn: string -> string 29 | labelBoxBorderColor: "#888", // border color for the little label boxes 30 | container: null, // container (as jQuery object) to put legend in, null means default on top of graph 31 | position: "ne", // position of default legend container within plot 32 | margin: [5, 10], // distance from grid edge to default legend container within plot 33 | backgroundOpacity: 0 // set to 0 to avoid background 34 | }, 35 | grid: { 36 | hoverable: false, 37 | clickable: false 38 | }, 39 | }); 40 | 41 | }); -------------------------------------------------------------------------------- /dev_static/site/js/cookie_banner.js: -------------------------------------------------------------------------------- 1 | // Creare's 'Implied Consent' EU Cookie Law Banner v:2.4 2 | // Conceived by Robert Kent, James Bavington & Tom Foyster 3 | 4 | var dropCookie = true; // false disables the Cookie, allowing you to style the banner 5 | var cookieDuration = 14; // Number of days before the cookie expires, and the banner reappears 6 | var cookieName = 'complianceCookie'; // Name of our cookie 7 | var cookieValue = 'on'; // Value of cookie 8 | 9 | function createDiv(){ 10 | var bodytag = document.getElementsByTagName('body')[0]; 11 | var div = document.createElement('div'); 12 | div.setAttribute('id','cookie-law'); 13 | div.setAttribute('class', 'alert alert-success') 14 | div.innerHTML = 'Our website uses cookies. By continuing we assume your permission to deploy cookies, as detailed in our privacy and cookies policy. X'; 15 | bodytag.insertBefore(div,bodytag.firstChild); // Adds the Cookie Law Banner just after the opening tag 16 | } 17 | 18 | 19 | function createCookie(name,value,days) { 20 | if (days) { 21 | var date = new Date(); 22 | date.setTime(date.getTime()+(days*24*60*60*1000)); 23 | var expires = "; expires="+date.toGMTString(); 24 | } 25 | else var expires = ""; 26 | if(window.dropCookie) { 27 | document.cookie = name+"="+value+expires+"; path=/"; 28 | } 29 | } 30 | 31 | function checkCookie(name) { 32 | var nameEQ = name + "="; 33 | var ca = document.cookie.split(';'); 34 | for(var i=0;i < ca.length;i++) { 35 | var c = ca[i]; 36 | while (c.charAt(0)==' ') c = c.substring(1,c.length); 37 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 38 | } 39 | return null; 40 | } 41 | 42 | function eraseCookie(name) { 43 | createCookie(name,"",-1); 44 | } 45 | 46 | window.onload = function(){ 47 | if(checkCookie(window.cookieName) != window.cookieValue){ 48 | createDiv(); 49 | } 50 | } 51 | 52 | function removeMe(){ 53 | var element = document.getElementById('cookie-law'); 54 | element.parentNode.removeChild(element); 55 | createCookie(window.cookieName, 'on', window.cookieDuration); 56 | } -------------------------------------------------------------------------------- /dev_static/site/js/faq.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name FAQ Plugin 3 | * @author Rod Howard 4 | * @url http://goideate.com 5 | * @date April 28, 2012 6 | * @license GNU/GPL Version 3 7 | * 8 | * 9 | * Example: 10 | * 11 | * $(function() { 12 | * $("element, #id, .class").technify({ 13 | * // #{EDIT-HERE}# Your run-time options go here ... 14 | * }); 15 | * }); 16 | */ 17 | /** 18 | * Create an anonymous function to avoid library conflicts 19 | */ 20 | (function($) { 21 | /** 22 | * Add our plugin to the jQuery.fn object 23 | */ 24 | $.fn.goFaq = function(options) { 25 | /** 26 | * Define some default settings 27 | */ 28 | var defaults = { 29 | enableSearch: true, 30 | enableToc: true, 31 | enableStyling: true, 32 | //numberHtml: '{{#}}.', 33 | numberHtml: '
{{#}}
' 34 | }; 35 | /** 36 | * Merge the runtime options with the default settings 37 | */ 38 | var options = $.extend({}, defaults, options); 39 | /** 40 | * Iterate through the collection of elements and 41 | * return the object to preserve method chaining 42 | */ 43 | return this.each(function(i) { 44 | /** 45 | * Wrap the current element in an instance of jQuery 46 | */ 47 | var $this = $(this); 48 | 49 | var $container = $this.wrap ('
'); 50 | 51 | $this.addClass ('faq-list'); 52 | 53 | if (options.enableSearch) { 54 | var $form = generateSearchForm (); 55 | $form.insertBefore ($this); 56 | } 57 | 58 | if (options.enableToc) { 59 | var $toc = generateToc ($this); 60 | $toc.insertBefore ($this); 61 | } 62 | 63 | 64 | var $empty = generateEmptySearch (); 65 | $empty.appendTo ($container); 66 | 67 | generateAnswerNumbers ($this); 68 | 69 | }); 70 | 71 | function search (e) { 72 | var el, container, filter, count, pattern, container, answers, toc, tocs, empty; 73 | 74 | el = $(this); 75 | container = el.parents ('.faq-container'); 76 | filter = el.val (); 77 | toc = container.find ('.faq-toc'); 78 | answers = container.find ('.faq-list').find ('li'); 79 | tocs = container.find ('.faq-toc').find('li'); 80 | empty = container.find ('.faq-empty'); 81 | pattern = new RegExp (filter, 'i'); 82 | 83 | answers.hide (); 84 | tocs.hide (); 85 | 86 | $.grep (answers.find ('.faq-text'), function (input) { 87 | if (pattern.test ($(input).text ())) { 88 | $(input).parents ('li').show (); 89 | 90 | var index = $(input).parents ('li').index (); 91 | tocs.eq (index).show (); 92 | } 93 | }); 94 | 95 | if (!answers.is (':visible')) { 96 | empty.show (); 97 | toc.hide (); 98 | } else { 99 | empty.hide (); 100 | toc.show (); 101 | } 102 | } 103 | 104 | 105 | function generateEmptySearch () { 106 | var $empty = $('
', { 'class': 'faq-empty' }); 107 | 108 | return $empty.html ('Nothing Found'); 109 | } 110 | 111 | function generateSearchForm () { 112 | 113 | var $form = $('
', { 'class': 'faq-search' }); 114 | var $input = $('', { 'type': 'text', 'name': 'search', 'placeholder': 'Search by Keyword' }); 115 | 116 | $input.appendTo ($form); 117 | 118 | $input.bind ('keyup', search) 119 | 120 | return $form; 121 | } 122 | 123 | function generateAnswerNumbers ($list) { 124 | $list.find ('li').each (function (i) { 125 | var id = parseInt (i+1); 126 | 127 | 128 | $(this).wrapInner ('
'); 129 | 130 | 131 | if (options.enableStyling) { 132 | var icon = '
' + options.numberHtml + '
'; 133 | 134 | icon = icon.replace ('{{#}}', id); 135 | $(this).prepend (icon); 136 | } 137 | }); 138 | } 139 | 140 | function generateToc ($list) { 141 | var html = '
    '; 142 | 143 | $list.find ('li').each (function (i) { 144 | var id = parseInt (i+1); 145 | html += '
  1. ' + id + '. ' + $(this).find ('h4').text () + '
  2. '; 146 | $(this).attr ('id', 'faq-' + id); 147 | }); 148 | 149 | html += '
'; 150 | 151 | var $toc = $('
', { 'class': 'faq-toc' }); 152 | 153 | $toc.html (html); 154 | 155 | return $toc; 156 | 157 | } 158 | }; 159 | })(jQuery); -------------------------------------------------------------------------------- /dev_static/site/js/guidely/guidely-number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/js/guidely/guidely-number.png -------------------------------------------------------------------------------- /dev_static/site/js/guidely/guidely.css: -------------------------------------------------------------------------------- 1 | .guidely-number { 2 | background: url(guidely-number.png) no-repeat 0 0; 3 | width: 45px; 4 | height: 45px; 5 | display: none; 6 | position: absolute; 7 | cursor: pointer; 8 | z-index: 10002; 9 | } 10 | 11 | .guidely-number span { 12 | width: 43px; 13 | height: 43px; 14 | font-family: arial, sans-serif; 15 | font-size: 20px; 16 | font-weight: bold; 17 | text-align: center; 18 | color: #FFF; 19 | text-align: center; 20 | display: block; 21 | line-height: 44px; 22 | } 23 | 24 | .guidely-guide { 25 | background: #FFF; 26 | width: 300px; 27 | display: none; 28 | border: 3px solid #999; 29 | 30 | -webkit-border-radius:5px; 31 | -moz-border-radius:5px; 32 | border-radius:5px; 33 | 34 | -webkit-box-shadow:0 0 12px rgba(0,0,0,0.4); 35 | -moz-box-shadow:0 0 12px rgba(0,0,0,0.4); 36 | box-shadow:0 0 12px rgba(0,0,0,0.4); 37 | 38 | z-index: 10001; 39 | } 40 | 41 | .guidely-guide h4 { 42 | font-family: Helvetica, arial, sans-serif; 43 | font-size: 15px; 44 | font-weight: bold; 45 | color: #333; 46 | padding-bottom: 15px !important; 47 | padding: 0; 48 | margin: 0 0 1em; 49 | border-bottom: 1px dotted #CCC; 50 | } 51 | 52 | .guidely-guide-pad { 53 | font-size: 12px; 54 | line-height: 1.7em; 55 | padding: 15px 15px 5px 30px; 56 | 57 | } 58 | 59 | .guidely-anchor-right .guidely-guide-pad { padding: 15px 30px 5px 15px; } 60 | 61 | .guidely-anchor-right .guidely-close-trigger { right: 30px; } 62 | 63 | .guidely-popup 64 | { 65 | color: #444; 66 | display:block; 67 | padding: 0; 68 | background: #fff; 69 | 70 | -webkit-border-top-left-radius: 4px; 71 | -webkit-border-top-right-radius: 4px; 72 | -moz-border-radius-topleft: 4px; 73 | -moz-border-radius-topright: 4px; 74 | border-top-left-radius: 4px; 75 | border-top-right-radius: 4px; 76 | } 77 | 78 | 79 | 80 | 81 | 82 | .guidely-controls { 83 | background: #EEE; 84 | text-align: right; 85 | padding: 7px 10px; 86 | margin-top: 1em; 87 | } 88 | 89 | .guidely-controls button { 90 | font-size: 11px; 91 | padding: 3px 8px; 92 | *padding: 1px 4px; 93 | cursor: pointer; 94 | } 95 | 96 | .guidely-overlay 97 | { 98 | position: fixed; 99 | top: 0px; 100 | left: 0px; 101 | height:100%; 102 | width:100%; 103 | background-color: #000; 104 | z-index: 10000; 105 | 106 | filter: alpha(opacity=30); 107 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=30); 108 | -moz-opacity: 0.30; 109 | opacity:0.30; 110 | } 111 | 112 | .guidely-start-trigger { 113 | background: #444; 114 | background: rgba(0,0,0,.6); 115 | text-decoration: none; 116 | color: #FFF; 117 | cursor: pointer; 118 | padding: 4px 10px 4px 12px; 119 | position: fixed; 120 | top: 0; 121 | right: 0; 122 | 123 | z-index: 9999; 124 | 125 | -webkit-border-bottom-left-radius: 5px; 126 | -moz-border-radius-bottomleft: 5px; 127 | border-bottom-left-radius: 5px; 128 | 129 | } 130 | 131 | .guidely-close-trigger { 132 | font-family: Helvetica, arial, sans-serif; 133 | font-size: 13px; 134 | font-weight: bold; 135 | text-decoration: none; 136 | color: #AAA; 137 | position: absolute; 138 | right:16px; 139 | top: 12px; 140 | } 141 | 142 | #guide-welcome { width: 350px; } 143 | 144 | #guide-welcome .guidely-guide-pad { padding: 15px 15px 5px 15px; } -------------------------------------------------------------------------------- /dev_static/site/js/pushetta.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Hook delle richieste ajax per gestire il csfr token 3 | */ 4 | $(document).ajaxSend(function(event, xhr, settings) { 5 | function getCookie(name) { 6 | var cookieValue = null; 7 | if (document.cookie && document.cookie != '') { 8 | var cookies = document.cookie.split(';'); 9 | for (var i = 0; i < cookies.length; i++) { 10 | var cookie = jQuery.trim(cookies[i]); 11 | // Does this cookie string begin with the name we want? 12 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 13 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 14 | break; 15 | } 16 | } 17 | } 18 | return cookieValue; 19 | } 20 | function sameOrigin(url) { 21 | // url could be relative or scheme relative or absolute 22 | var host = document.location.host; // host + port 23 | var protocol = document.location.protocol; 24 | var sr_origin = '//' + host; 25 | var origin = protocol + sr_origin; 26 | // Allow absolute or scheme relative URLs to same origin 27 | return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || 28 | (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || 29 | // or any other URL that isn't scheme relative or absolute i.e relative. 30 | !(/^(\/\/|http:|https:).*/.test(url)); 31 | } 32 | function safeMethod(method) { 33 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 34 | } 35 | 36 | if (!safeMethod(settings.type) && sameOrigin(settings.url)) { 37 | xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); 38 | } 39 | }); 40 | 41 | function generateUUID() { 42 | var d = new Date().getTime(); 43 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 44 | var r = (d + Math.random() * 16) % 16 | 0; 45 | d = Math.floor(d / 16); 46 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); 47 | }); 48 | return uuid; 49 | } 50 | 51 | function getQueryParameterByName(name) { 52 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 53 | var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), 54 | results = regex.exec(location.search); 55 | return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); 56 | } -------------------------------------------------------------------------------- /dev_static/site/js/signin.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | jQuery.support.placeholder = false; 4 | test = document.createElement('input'); 5 | if('placeholder' in test) jQuery.support.placeholder = true; 6 | 7 | if (!$.support.placeholder) { 8 | 9 | $('.field').find ('label').show (); 10 | 11 | } 12 | 13 | }); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pushetta: 5 | container_name: pushetta-be 6 | command: python pushetta/manage.py runserver 0.0.0.0:8001 7 | build: 8 | context: . 9 | expose: 10 | - 8001 11 | ports: 12 | - 8001:8001 13 | depends_on: 14 | - db 15 | - redis 16 | - elastic 17 | env_file: 18 | - ./pushetta.env 19 | volumes: 20 | - ./volumes/pushetta:/usr/share/pushetta/ 21 | 22 | db: 23 | image: mysql:5.5 24 | container_name: db 25 | ports: 26 | - 3306:3306 27 | expose: 28 | - 3306 29 | env_file: 30 | - ./database.env 31 | volumes: 32 | - ./volumes/database/:/var/lib/mysql/ 33 | 34 | elastic: 35 | image: elasticsearch:2 36 | container_name: elastic 37 | ports: 38 | - '9200:9200' 39 | expose: 40 | - 9200 41 | environment: 42 | - http.host=0.0.0.0 43 | - transport.host=127.0.0.1 44 | - bootstrap.memory_lock=true 45 | - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' 46 | cap_add: 47 | - IPC_LOCK 48 | volumes: 49 | - ./volumes/elasticsearch:/usr/share/elasticsearch/data 50 | 51 | redis: 52 | image: redis:4.0-alpine 53 | container_name: redis 54 | ports: 55 | - 6379:6379 56 | expose: 57 | - 6379 58 | volumes: 59 | - ./volumes/redis:/data 60 | 61 | mosquitto: 62 | image: guglielmino/mosquitto-pushetta-auth-plugin:v1.0.9 63 | container_name: mosquitto 64 | ports: 65 | - 1883:1883 66 | expose: 67 | - 1883 68 | depends_on: 69 | - db 70 | command: 71 | ['/wait_for', 'db:3306', '--', '/usr/sbin/mosquitto', '-c', '/etc/mosquitto/mosquitto.conf'] 72 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | mkdir -p /usr/src/app/log/ 6 | 7 | if [ "x$DJANGO_MANAGEPY_MIGRATE" = 'xon' ]; then 8 | python pushetta/manage.py migrate --noinput 9 | fi 10 | 11 | if [ "x$DJANGO_MANAGEPY_COLLECTSTATIC" = 'xon' ]; then 12 | python pushetta/manage.py collectstatic --noinput 13 | fi 14 | 15 | exec "$@" -------------------------------------------------------------------------------- /docker_push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 3 | docker push guglielmino/pushetta-api:$1 4 | -------------------------------------------------------------------------------- /pushetta/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/api/__init__.py -------------------------------------------------------------------------------- /pushetta/api/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | # Register your models here. 7 | -------------------------------------------------------------------------------- /pushetta/api/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class ApiConfig(AppConfig): 8 | name = 'api' 9 | -------------------------------------------------------------------------------- /pushetta/api/crashes_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le funzionalità per gestire i crashlog delle App (Android inizialmente) 5 | 6 | from django.conf import settings 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | from rest_framework import generics 12 | from rest_framework.response import Response 13 | from rest_framework import status 14 | 15 | 16 | class CrashLogService(generics.GenericAPIView): 17 | def post(self, request, format=None): 18 | logger.error(str(request.DATA)) 19 | return Response(status=status.HTTP_200_OK) 20 | -------------------------------------------------------------------------------- /pushetta/api/feedback_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le funzionalità per feedback letture 5 | 6 | import logging 7 | logger = logging.getLogger(__name__) 8 | 9 | from rest_framework import generics 10 | from rest_framework.response import Response 11 | from rest_framework import status 12 | 13 | from api.serializers import FeedbackSerializer 14 | from core.feedback_manager import FeedbackManager 15 | from core.models import Channel 16 | from api.permissions import IsDeviceCallAuthorized 17 | from core.services import set_read_feedback_multiple 18 | 19 | 20 | class FeedbackService(generics.GenericAPIView): 21 | serializer_class = FeedbackSerializer 22 | 23 | permission_classes = [ 24 | IsDeviceCallAuthorized 25 | ] 26 | 27 | def post(self, request, format=None, messages_id=None): 28 | serializer = FeedbackSerializer(data=request.DATA) 29 | if not serializer.is_valid(): 30 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 31 | 32 | 33 | feedback_dict = serializer.object 34 | set_read_feedback_multiple(feedback_dict["device_id"], feedback_dict["messages_id"]) 35 | return Response(status=status.HTTP_201_CREATED) 36 | 37 | -------------------------------------------------------------------------------- /pushetta/api/messages_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le funzionalità dei messaggi storati 5 | 6 | from datetime import datetime 7 | 8 | from rest_framework import generics 9 | from rest_framework.response import Response 10 | from rest_framework import status 11 | from django.shortcuts import get_object_or_404 12 | from django.db.models import Q 13 | 14 | from api.serializers import ChannelMsgSerializer 15 | from core.subscriber_manager import SubscriberManager 16 | from core.models import ChannelMsg 17 | from api.permissions import IsDeviceCallAuthorized 18 | 19 | 20 | class MessageList(generics.GenericAPIView): 21 | """ 22 | Get a single message or a messages list belonging to 23 | calling device 24 | message_id -- Message identifier 25 | """ 26 | 27 | serializer_class = ChannelMsgSerializer 28 | 29 | permission_classes = [ 30 | IsDeviceCallAuthorized 31 | ] 32 | 33 | # NOTA: Va definito un meccanismo di protezione per la get dei messaggi di un device per evitare 34 | # che conoscendo il device_id di un utente si possano acquisire i messaggi (problema reale solo 35 | # per i canali privati, quelli pubblici hanno messaggi visibili a tutti per definizione) 36 | def get(self, request, format=None, message_id=None, device_id=None): 37 | 38 | if message_id != None: 39 | obj = get_object_or_404(ChannelMsg, pk=message_id) 40 | serializer = ChannelMsgSerializer(obj) 41 | 42 | return Response(serializer.data, status=status.HTTP_200_OK) 43 | 44 | if device_id != None: 45 | channel_names = SubscriberManager().get_device_subscriptions(device_id) 46 | # Nota: verificare come gestire la casistica di risultati molto ampi, introdotto inanto il throttling per limitare 47 | messages = ChannelMsg.objects.filter(Q(expire__isnull=True) | Q(expire__gte=datetime.utcnow())).filter( 48 | channel__name__in=channel_names) 49 | serializer = ChannelMsgSerializer(messages, many=True) 50 | 51 | return Response(serializer.data, status=status.HTTP_200_OK) 52 | 53 | return Response(status=status.HTTP_404_NOT_FOUND) 54 | 55 | -------------------------------------------------------------------------------- /pushetta/api/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | # Create your models here. 7 | -------------------------------------------------------------------------------- /pushetta/api/permissions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Permission custom 5 | 6 | from rest_framework import permissions 7 | 8 | 9 | class IsDeviceCallAuthorized(permissions.BasePermission): 10 | """ 11 | Custom permission to check if a device can invoke a API method. 12 | It check a custom HTTP Header for operations made from devices (it check a signature based on seed delivered with push notification) 13 | Custom HTTP header is X-Auth-Token 14 | """ 15 | 16 | def has_permission(self, request, view): 17 | if 'HTTP_X_AUTH_TOKEN' in request.META: 18 | req_custom_header = request.META['HTTP_X_AUTH_TOKEN'] 19 | print req_custom_header 20 | 21 | return True 22 | 23 | def has_object_permission(self, request, view, obj): 24 | return True 25 | 26 | 27 | class IsChannelOwner(permissions.BasePermission): 28 | """ 29 | Object-level permission to only allow owners of an object to edit it. 30 | Assumes the model instance has an `owner` attribute. 31 | """ 32 | 33 | def has_permission(self, request, view): 34 | print "IsChannelOwner has_permission" 35 | return True 36 | 37 | def has_object_permission(self, request, view, obj): 38 | print "IsChannelOwner has_object_permission" 39 | # Read permissions are allowed to any request, 40 | # so we'll always allow GET, HEAD or OPTIONS requests. 41 | if request.method in permissions.SAFE_METHODS: 42 | return True 43 | 44 | # Instance must have an attribute named `owner`. 45 | return obj.owner == request.user -------------------------------------------------------------------------------- /pushetta/api/publisher_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le funzionalità per la gestione Publishers 5 | 6 | from rest_framework import generics 7 | 8 | from api.serializers import PublisherSerializer 9 | 10 | 11 | 12 | 13 | class PublisherList(generics.GenericAPIView): 14 | """ 15 | Channels publishers subscriptions 16 | """ 17 | serializer_class = PublisherSerializer 18 | 19 | def post(self, request, format=None, name=None): 20 | pass -------------------------------------------------------------------------------- /pushetta/api/pushes_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le funzionalità di push e lettura dei messaggi 5 | 6 | from datetime import datetime 7 | from dateutil.relativedelta import relativedelta 8 | 9 | from rest_framework import generics, permissions 10 | from rest_framework.response import Response 11 | from rest_framework import status 12 | from rest_framework.throttling import UserRateThrottle 13 | 14 | from django.conf import settings 15 | 16 | from core.models import Channel, ChannelMsg 17 | from api.serializers import PushMessageSerializer, PushResponseSerializer, TargetSerializer 18 | 19 | from core.services import send_push_message, get_push_targets, SendPushResponse 20 | 21 | class PushList(generics.GenericAPIView): 22 | serializer_class = PushMessageSerializer 23 | throttle_classes = (UserRateThrottle,) 24 | 25 | #permission_classes = (IsChannelOwner, permissions.IsAuthenticated,) 26 | 27 | def post(self, request, format=None, name=None): 28 | channel = None 29 | try: 30 | channel = Channel.objects.get(name=name) 31 | except Channel.DoesNotExist: 32 | return Response(status=status.HTTP_404_NOT_FOUND) 33 | 34 | if channel.owner != request.user: 35 | return Response(status=status.HTTP_403_FORBIDDEN) 36 | 37 | serializer = PushMessageSerializer(data=request.DATA) 38 | if not serializer.is_valid(): 39 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 40 | 41 | 42 | push_msg = serializer.object 43 | 44 | expire = datetime.today() + relativedelta(months=1) # Default 1 mese 45 | if 'expire' in push_msg: 46 | expire = push_msg["expire"] 47 | 48 | target = None 49 | if "target" in push_msg: 50 | target = push_msg["target"] 51 | 52 | response = send_push_message(channel, push_msg["message_type"], push_msg["body"], expire, target) 53 | 54 | serializer = PushResponseSerializer(response) 55 | return Response(serializer.data, status=status.HTTP_200_OK) 56 | 57 | class TargetList(generics.GenericAPIView): 58 | serializer_class = TargetSerializer 59 | 60 | def get(self, request): 61 | resp_list = map(lambda x: { "target" : x}, get_push_targets()) 62 | serializer = TargetSerializer(resp_list) 63 | return Response(serializer.source, status=status.HTTP_200_OK) -------------------------------------------------------------------------------- /pushetta/api/serializers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Serializer dei DTO 5 | 6 | from drf_compound_fields.fields import ListField 7 | 8 | from rest_framework import serializers 9 | from rest_framework.pagination import PaginationSerializer 10 | from rest_framework.serializers import ValidationError 11 | from django.contrib.auth.models import User 12 | 13 | from core.models import Channel, ChannelMsg, Subscriber, ChannelSubscribeRequest 14 | 15 | 16 | class UserSerializer(serializers.ModelSerializer): 17 | class Meta: 18 | model = User 19 | fields = ('id', 'username', 'first_name', 'last_name', 'email', ) 20 | 21 | 22 | class ChannelSerializer(serializers.ModelSerializer): 23 | class Meta: 24 | model = Channel 25 | fields = ('id', 'name', 'image', 'description', 'kind', ) 26 | 27 | 28 | class MinimalChannelSerializer(serializers.ModelSerializer): 29 | class Meta: 30 | model = Channel 31 | fields = ('id', 'name', 'image', ) 32 | 33 | 34 | class PaginatedChannelSerializer(PaginationSerializer): 35 | start_index = serializers.SerializerMethodField('get_start_index') 36 | end_index = serializers.SerializerMethodField('get_end_index') 37 | num_pages = serializers.Field(source='paginator.num_pages') 38 | 39 | class Meta: 40 | object_serializer_class = ChannelSerializer 41 | 42 | def get_start_index(self, page): 43 | return page.start_index() 44 | 45 | def get_end_index(self, page): 46 | return page.end_index() 47 | 48 | def get_curr_page(self, page): 49 | return page.number 50 | 51 | 52 | class ChannelMsgSerializer(serializers.ModelSerializer): 53 | channel = MinimalChannelSerializer() 54 | 55 | class Meta: 56 | model = ChannelMsg 57 | fields = ('id', 'body', 'date_created', 'expire', 'channel', 'preview_url', ) 58 | 59 | 60 | class SubscriberModelSerializer(serializers.ModelSerializer): 61 | class Meta: 62 | model = Subscriber 63 | fields = ('device_id', 'token', 'sub_type', 'name', ) 64 | 65 | 66 | class SubscriberSerializer(serializers.Serializer): 67 | sub_type = serializers.CharField(max_length=100) # iOS, Android, ... 68 | device_id = serializers.CharField(max_length=200) 69 | token = serializers.CharField(max_length=500) 70 | name = serializers.CharField(max_length=250) 71 | 72 | 73 | class ChannelSubscriptionSerializer(serializers.Serializer): 74 | sub_type = serializers.CharField(max_length=100) # iOS, Android, ... 75 | device_id = serializers.CharField(max_length=200) 76 | token = serializers.CharField(max_length=500) 77 | 78 | 79 | class PushMessageSerializer(serializers.Serializer): 80 | message_type = serializers.CharField(max_length=100) 81 | body = serializers.CharField(max_length=500) 82 | expire = serializers.DateField(required=False) 83 | target = serializers.CharField(max_length=20, required=False) 84 | 85 | 86 | class PublisherSerializer(serializers.Serializer): 87 | dummy = serializers.CharField(max_length=100) 88 | 89 | 90 | class FeedbackSerializer(serializers.Serializer): 91 | device_id = serializers.CharField(max_length=200) 92 | messages_id = ListField(serializers.IntegerField()) 93 | 94 | 95 | class PushResponseSerializer(serializers.Serializer): 96 | success = serializers.BooleanField() 97 | error_code = serializers.CharField(max_length=200) 98 | 99 | 100 | class CheckVersionSerializer(serializers.Serializer): 101 | need_update = serializers.BooleanField() 102 | message = serializers.CharField(max_length=200) 103 | 104 | 105 | class ChannelSubscribeRequestSerializer(serializers.ModelSerializer): 106 | channel = ChannelSerializer() 107 | 108 | class Meta: 109 | model = ChannelSubscribeRequest 110 | fields = ('channel', 'status') 111 | 112 | 113 | class TargetSerializer(serializers.WritableField): 114 | 115 | def from_native(self, data): 116 | if isinstance(data, list): 117 | return data 118 | else: 119 | msg = self.error_messages['invalid'] 120 | raise ValidationError(msg) 121 | 122 | def to_native(self, obj): 123 | return obj -------------------------------------------------------------------------------- /pushetta/api/subscriber_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le funzionalità per la gestione Subscribers 5 | 6 | from rest_framework import generics, permissions 7 | from rest_framework.response import Response 8 | from rest_framework import status 9 | from django.conf import settings 10 | 11 | from core.models import Subscriber, Channel, ChannelSubscribeRequest 12 | from core.models import ACCEPTED, PENDING 13 | 14 | from api.serializers import SubscriberSerializer, ChannelSerializer, ChannelSubscribeRequestSerializer 15 | from core.subscriber_manager import SubscriberManager 16 | 17 | 18 | class SubscriberList(generics.GenericAPIView): 19 | """ 20 | Handle device subscription to Pushetta 21 | """ 22 | 23 | serializer_class = SubscriberSerializer 24 | 25 | def post(self, request, format=None): 26 | 27 | serializer = SubscriberSerializer(data=request.DATA) 28 | if serializer.is_valid(): 29 | 30 | is_sandbox = (True if settings.ENVIRONMENT == "dev" else False) 31 | subscriber_data = serializer.object 32 | 33 | subscriber, created = Subscriber.objects.get_or_create(device_id=subscriber_data["device_id"], 34 | defaults={'sub_type': subscriber_data["sub_type"], 35 | 'sandbox': is_sandbox, 'enabled': True, 36 | 'name': subscriber_data["name"], 37 | 'token': subscriber_data["token"]}) 38 | 39 | if not created: 40 | subscriber.token = subscriber_data["token"] 41 | subscriber.name = subscriber_data["name"] 42 | 43 | subscriber.save() 44 | 45 | # Update del token nelle subscription del device 46 | subMamager = SubscriberManager() 47 | channel_subscriptions = subMamager.get_device_subscriptions(subscriber_data["device_id"]) 48 | for channel_sub in channel_subscriptions: 49 | subMamager.subscribe(channel_sub, subscriber_data["sub_type"], subscriber_data["device_id"], 50 | subscriber_data["token"]) 51 | 52 | return Response(serializer.data, status=status.HTTP_201_CREATED) 53 | else: 54 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 55 | 56 | 57 | class SubcriptionsList(generics.GenericAPIView): 58 | """ 59 | Handle subscriptions to channels of a specific device 60 | """ 61 | 62 | permission_classes = [ 63 | permissions.AllowAny 64 | ] 65 | 66 | serializer_class = ChannelSerializer 67 | 68 | def get(self, request, format=None, deviceId=None): 69 | channel_names = SubscriberManager().get_device_subscriptions(deviceId) 70 | 71 | channels = Channel.objects.filter(name__in=channel_names) 72 | serializer = ChannelSerializer(channels, many=True) 73 | 74 | return Response(serializer.data) 75 | 76 | 77 | class DeviceSubscriptionsRequests(generics.GenericAPIView): 78 | """ 79 | Handle list of device requests (subscribed and pending subscriptions) 80 | """ 81 | permission_classes = [ 82 | permissions.AllowAny 83 | ] 84 | 85 | serializer_class = ChannelSubscribeRequestSerializer 86 | 87 | def get(self, request, format=None, deviceId=None): 88 | channel_names = SubscriberManager().get_device_subscriptions(deviceId) 89 | 90 | # Uso ChannelSubscribeRequestSerializer e quelli già sottoscritti li aggiungo fake come ACCEPTED 91 | channels = Channel.objects.filter(name__in=channel_names) 92 | subscribed = [ChannelSubscribeRequest(channel=ch, device_id=deviceId, status=ACCEPTED) for ch in channels] 93 | # Le richieste visualizzate client side sono solo quelle 94 | requests = ChannelSubscribeRequest.objects.filter(device_id=deviceId).filter(status=PENDING) 95 | 96 | serializer = ChannelSubscribeRequestSerializer(subscribed + list(requests), many=True) 97 | 98 | return Response(serializer.data) 99 | -------------------------------------------------------------------------------- /pushetta/api/sys_sl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Service layer con le API generiche di sistema (tipo check versione client) 5 | 6 | from datetime import datetime 7 | from dateutil.relativedelta import relativedelta 8 | 9 | from rest_framework import generics, permissions 10 | from rest_framework.response import Response 11 | from rest_framework import status 12 | from django.conf import settings 13 | 14 | from api.permissions import IsChannelOwner 15 | 16 | from api.serializers import CheckVersionSerializer 17 | 18 | class CheckVersionData(object): 19 | def __init__(self, need_update, message): 20 | self.need_update = need_update 21 | self.message = message 22 | 23 | 24 | 25 | class CheckVersion(generics.GenericAPIView): 26 | serializer_class = CheckVersionSerializer 27 | 28 | def get(self, request, *args, **kwargs): 29 | checkData = CheckVersionData(False, "") 30 | serializer = CheckVersionSerializer(checkData) 31 | return Response(serializer.data, status=status.HTTP_200_OK) 32 | -------------------------------------------------------------------------------- /pushetta/api/urls.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Definizione delle Url per la API 5 | 6 | from django.conf.urls import url, include 7 | from django.contrib.auth.decorators import login_required 8 | 9 | from api.channels_sl import ChannelsList, ChannelSearch, ChannelSubscription, ChannelSuggestion, ChannelUnSubscription 10 | from api.publisher_sl import PublisherList 11 | from api.pushes_sl import PushList, TargetList 12 | from api.subscriber_sl import SubscriberList, SubcriptionsList, DeviceSubscriptionsRequests 13 | from api.messages_sl import MessageList 14 | from api.feedback_sl import FeedbackService 15 | from api.crashes_sl import CrashLogService 16 | from api.sys_sl import CheckVersion 17 | 18 | 19 | channel_urls = [ 20 | url(r'^/subscription/(?P[\w|\W]+)/(?P[\w|\W]+)/(?P[\w|\W]+)/$', ChannelUnSubscription.as_view(), name='unsubscribe-channel-by-name'), 21 | url(r'^/subscription/(?P[\w|\W]+)/$', ChannelSubscription.as_view(),name='subscribe-channel-by-name'), 22 | url(r'^/$', ChannelsList.as_view(), name='channel-list'), 23 | url(r'^/search/$', ChannelSearch.as_view(), name='channel-search'), 24 | url(r'^/suggestions/(?P[\w|\W]+)/$', ChannelSuggestion.as_view(), name='channel-suggestions') 25 | ] 26 | 27 | publisher_urls = [ 28 | url(r'^/channels/(?P[\w|\W]+)/$', PublisherList.as_view(), name='publishers-by-name'), 29 | ] 30 | 31 | messages_urls = [ 32 | url(r'^/my/(?P[\w|\W]+)/$', MessageList.as_view(), name='messages-by-owner'), 33 | url(r'^/(?P[\w|\W]+)/$', MessageList.as_view(), name='messages-by-id'), 34 | ] 35 | 36 | pushes_urls = [ 37 | url(r'^/(?P[\w|\W]+)/$', PushList.as_view(), name='pushes-by-name'), 38 | ] 39 | 40 | targets_urls = [ 41 | url(r'^/$', TargetList.as_view(), name='target-list'), 42 | ] 43 | 44 | subscribers_urls = [ 45 | url(r'^/$', SubscriberList.as_view(), name='subscribers-list'), 46 | ] 47 | 48 | subscriptions_urls = [ 49 | url(r'^/requests/(?P[\w|\W]+)/$', DeviceSubscriptionsRequests.as_view(), name='requests-list'), 50 | url(r'^/(?P[\w|\W]+)/$', SubcriptionsList.as_view(), name='subsctiptions-list'), 51 | ] 52 | 53 | feedback_urls = [ 54 | 55 | url(r'^/(?P[\w|\W]+)/$', FeedbackService.as_view(), name='feedback-one-message'), 56 | url(r'^/$', FeedbackService.as_view(), name='feedback-many-messages'), 57 | ] 58 | 59 | android_urls = [ 60 | url(r'^/crashlog/', CrashLogService.as_view(), name='crashlog'), 61 | ] 62 | 63 | sys_urls = [ 64 | url(r'^/version/$', CheckVersion.as_view(), name='sys-app-version'), 65 | ] 66 | 67 | 68 | 69 | urlpatterns = [ 70 | # Autenticazione con AuthToken del django rest framework 71 | url(r'^auth/', 'rest_framework.authtoken.views.obtain_auth_token', name="auth-token"), 72 | 73 | url(r'^sys', include(sys_urls, namespace="sys-api")), 74 | 75 | url(r'^channels', include(channel_urls, namespace="channels-api")), 76 | url(r'^publisher', include(publisher_urls, namespace="publisher-api")), 77 | url(r'^pushes', include(pushes_urls, namespace="push-api")), 78 | url(r'^targets', include(targets_urls, namespace="target-api")), 79 | 80 | # NOTA :Verificare il porting del get di messages su questo 81 | url(r'^messages', include(messages_urls, namespace="messages-api")), 82 | url(r'^subscribers', include(subscribers_urls, namespace="subscribers-api")), 83 | url(r'^subscriptions', include(subscriptions_urls, namespace="subscriptions-api")), 84 | url(r'^feedback', include(feedback_urls, namespace="feedback-api")), 85 | 86 | url(r'^android/crashlog/', include(android_urls, namespace="android-api")), 87 | ] -------------------------------------------------------------------------------- /pushetta/api/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import render 5 | 6 | # Create your views here. 7 | -------------------------------------------------------------------------------- /pushetta/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/core/__init__.py -------------------------------------------------------------------------------- /pushetta/core/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Progetto: Pushetta API 3 | # Estensioni della Admin 4 | 5 | from __future__ import unicode_literals 6 | 7 | from django.contrib import admin 8 | 9 | from models import Channel, ChannelMsg, Subscriber, StoredImage 10 | 11 | class ChannelAdmin(admin.ModelAdmin): 12 | list_display = ('name', 'description', 'subscriptions', 'date_created',) 13 | 14 | class ChannelMsgAdmin(admin.ModelAdmin): 15 | list_display = ('date_created', ) 16 | 17 | class SubscriberAdmin(admin.ModelAdmin): 18 | list_display = ('sub_type', 'device_id', 'token', 'enabled', 'name', ) 19 | 20 | admin.site.register(Channel, ChannelAdmin) 21 | admin.site.register(ChannelMsg, ChannelMsgAdmin) 22 | admin.site.register(Subscriber, SubscriberAdmin) 23 | 24 | -------------------------------------------------------------------------------- /pushetta/core/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class CoreConfig(AppConfig): 8 | name = 'core' 9 | -------------------------------------------------------------------------------- /pushetta/core/feedback_manager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Manager for handling read feedback of channels messages 5 | 6 | import redis 7 | from django.conf import settings 8 | 9 | 10 | class FeedbackManager(): 11 | """ 12 | This class mantain an Set for every message containing all devices 13 | who reads the message 14 | """ 15 | 16 | 17 | def __init__(self): 18 | pool = redis.ConnectionPool(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB) 19 | self.redis_client = redis.Redis(connection_pool=pool) 20 | 21 | def setFeedback(self, channel_name, message_id, device_id): 22 | return self.redis_client.sadd(self.__getSetKey(channel_name, message_id), device_id) 23 | 24 | def getFeedbackCount(self, channel_name, message_id): 25 | return self.redis_client.scard(self.__getSetKey(channel_name, message_id)) 26 | 27 | 28 | def __getSetKey(self, channel_name, message_id): 29 | return "{0}:{1}:{2}:{3}".format(settings.REDIS_KEY_PREFIX, "fback", channel_name, message_id) 30 | -------------------------------------------------------------------------------- /pushetta/core/fileds_validators.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Definizione dei validators custom per i model fileds 5 | 6 | from django.core.validators import RegexValidator 7 | 8 | # '^[\w]*$' 9 | isalphavalidator = RegexValidator(r'[a-zA-Z0-9_\-\s].*', 10 | message='invalid characters in field', 11 | code='Invalid value') -------------------------------------------------------------------------------- /pushetta/core/push_manager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Classes for interact with push server (Google Cloud Messaging, Apple Push Notification Server, ...) 5 | 6 | from push_providers.iOSPushProvider import iOSPushProvider 7 | from push_providers.AndroidPushProvider import AndroidPushProvider 8 | from push_providers.WP8PushProvider import WP8PushProvider 9 | from push_providers.TestPushProvider import TestPushProvider 10 | from push_providers.SafariPushProvider import SafariPushProvider 11 | from push_providers.MQTTPushProvider import MQTTPushProvider 12 | 13 | """ 14 | Known push providers 15 | """ 16 | providers_map = { 17 | 'ios': iOSPushProvider(), 18 | 'android': AndroidPushProvider(), 19 | 'wp8': WP8PushProvider(), 20 | 'test': TestPushProvider(), 21 | 'safari': SafariPushProvider(), 22 | 'chrome': AndroidPushProvider(), 23 | 'iot_device': MQTTPushProvider(), 24 | } 25 | 26 | 27 | class PushProviderFactory: 28 | """ 29 | Factory class for different push providers 30 | """ 31 | 32 | @staticmethod 33 | def create(providerType, logger): 34 | if not providerType in providers_map: 35 | assert 1, "Bad provider creation: " + providerType 36 | return None 37 | else: 38 | pusher = providers_map[providerType] 39 | pusher.logger = logger 40 | return pusher 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /pushetta/core/push_providers/AndroidPushProvider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Provider for push to Google Cloud Messaging 5 | 6 | import logging 7 | 8 | import sys 9 | import traceback 10 | import random 11 | from pyfcm import FCMNotification 12 | from django.conf import settings 13 | 14 | from common import BaseProvider 15 | 16 | # Samples for FCM pip module here https://github.com/olucurious/pyfcm 17 | 18 | class AndroidPushProvider(BaseProvider): 19 | def __init__(self): 20 | self.gcm = FCMNotification(api_key=settings.FCM_SERVER_KEY) 21 | 22 | 23 | def pushMessage(self, message, destToken, channel_name): 24 | send_count = len(destToken) 25 | 26 | if send_count > 0: 27 | # TODO: Gestire paginazione dei tokens a gruppi di 6/700 (il limite dovrebbe essere 1000 per call) 28 | # TODO: gestione del ttl 29 | # Nota: converto in un dict perché altrimenti il serializzatore non riesce a lavorare sul PushMessage 30 | dic_obj = {'alert_msg': message.alert_msg, 'data_dic': message.data_dic, 'push_type': message.push_type} 31 | response = self.gcm.notify_multiple_devices(registration_ids=destToken, message_body=message.alert_msg, data_message=dic_obj) 32 | 33 | self.log_debug(str(response)) 34 | 35 | # Handling errors 36 | if 'errors' in response: 37 | for error, reg_ids in response['errors'].items(): 38 | self.log_error("push error {0}".format(error)) 39 | # Check for errors and act accordingly 40 | if error is 'NotRegistered': 41 | pass 42 | # Remove reg_ids from database 43 | # for reg_id in reg_ids: 44 | # entity.filter(registration_id=reg_id).delete() 45 | 46 | if 'canonical' in response: 47 | for reg_id, canonical_id in response['canonical'].items(): 48 | self.log_error("canonical {0}".format(reg_id)) 49 | # # Repace reg_id with canonical_id in your database 50 | # entry = entity.filter(registration_id=reg_id) 51 | # entry.registration_id = canonical_id 52 | # entry.save() 53 | 54 | # Nota: al momento non viene gestito correttamente il conteggio dei push inviati, si da per scontato che tutti 55 | # lo siano 56 | else: 57 | self.log_info("Nothing to send for AndroidPushProvider") 58 | 59 | 60 | return send_count -------------------------------------------------------------------------------- /pushetta/core/push_providers/MQTTPushProvider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Push provider for MQTT protocol 5 | 6 | import sys 7 | import traceback 8 | import random 9 | from common import PushMessage, BaseProvider, PushProviderException 10 | import paho.mqtt.publish as publish 11 | from django.conf import settings 12 | 13 | class MQTTPushProvider(BaseProvider): 14 | push_pattern = "/pushetta.com/channels/{0}" 15 | 16 | def pushMessage(self, message, tokens, channel_name): 17 | if not isinstance(tokens, list): 18 | raise PushProviderException("tokens must be a list") 19 | return False 20 | 21 | if not isinstance(message, PushMessage): 22 | raise PushProviderException("message must be a PushMessage") 23 | return False 24 | 25 | # TODO: Condizionare il push verso MQTT al message.push_type ?? 26 | 27 | try: 28 | topic = self.push_pattern.format(channel_name) 29 | auth_object = {'username': settings.MOSQ_USERNAME, 'password': settings.MOSQ_PASSWORD} 30 | publish.single(topic, payload=message.alert_msg, qos=1, retain=False, hostname=settings.MOSQ_HOST, 31 | port=settings.MOSQ_PORT, client_id="pushetta-api", keepalive=60, auth=auth_object) 32 | except: 33 | exc_type, exc_value, exc_traceback = sys.exc_info() 34 | lines = traceback.format_exception(exc_type, exc_value, exc_traceback) 35 | self.log_error("MQTTPushProvider -- exception {0}".format(''.join('!! ' + line for line in lines))) 36 | 37 | return True -------------------------------------------------------------------------------- /pushetta/core/push_providers/SafariPushProvider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # push provider for Safari browser push notification 5 | 6 | import logging 7 | import sys 8 | import traceback 9 | import random 10 | 11 | from django.conf import settings 12 | import time 13 | 14 | from common import BaseProvider 15 | from apns import APNs, Frame, Payload 16 | from common import PushMessage, BaseProvider, PushProviderException 17 | 18 | #  Procedura di invio massivo ispirata a https://gist.github.com/jimhorng/594401f68ce48282ced5 19 | SEND_INTERNAL = 0.01 20 | 21 | # Custom payload class fr Safari 22 | 23 | 24 | class PayloadSafari(Payload): 25 | 26 | def __init__(self, alert=None, url_args=[]): 27 | super(Payload, self).__init__() 28 | self.alert = alert 29 | self.url_args = url_args 30 | self._check_size() 31 | 32 | def dict(self): 33 | d = {"alert": self.alert, "url-args": self.url_args} 34 | d = {'aps': d} 35 | #print d 36 | return d 37 | 38 | 39 | class SafariPushProvider(BaseProvider): 40 | 41 | def response_listener(self, error_response): 42 | self.log_info( 43 | "SafariProvider:client get error-response: " + str(error_response)) 44 | 45 | def wait_till_error_response_unchanged(self): 46 | 47 | if hasattr(self.apns.gateway_server, '_is_resending'): 48 | delay = 1 49 | count = 0 50 | while True: 51 | if self.apns.gateway_server._is_resending == False: 52 | time.sleep(delay) 53 | if self.apns.gateway_server._is_resending == False: 54 | count = count + delay 55 | else: 56 | count = 0 57 | else: 58 | count = 0 59 | 60 | if count >= 10: 61 | break 62 | return delay * count 63 | return 0 64 | 65 | def pushMessage(self, message, destToken, channel_name): 66 | send_count = len(destToken) 67 | if send_count > 0: 68 | try: 69 | # USO l'apns enanched 70 | self.apns = APNs(use_sandbox=settings.APNS_IS_SANDBOX, 71 | cert_file=settings.APNS_SAFARI_CERT_FILE, enhanced=True) 72 | 73 | alert_dict = { 74 | "title": message.alert_msg[:40], "body": message.alert_msg, "action": "View"} 75 | payload = PayloadSafari(alert=alert_dict, url_args=[ 76 | message.data_dic['channel_name']]) 77 | 78 | # TODO: Gestire l'expire 79 | # Expire in un mese di default (TODO: Ricavare dai dati passati) 80 | expiry = time.time() + 3600 * 24 * 30 81 | 82 | priority = 10 83 | for token in destToken: 84 | self.log_info( 85 | "SafariPushProvider -- send token {0}".format(token)) 86 | identifier = random.getrandbits(32) 87 | self.apns.gateway_server.register_response_listener( 88 | self.response_listener) 89 | self.apns.gateway_server.send_notification( 90 | token, payload, identifier=identifier) 91 | 92 | # Get feedback messages. 93 | for (token_hex, fail_time) in self.apns.feedback_server.items(): 94 | self.log_info( 95 | "SafariPushProvider -- feedback token {0} fail time {1}".format(token_hex, fail_time)) 96 | delay = self.wait_till_error_response_unchanged() 97 | self.apns.gateway_server.force_close() 98 | except: 99 | exc_type, exc_value, exc_traceback = sys.exc_info() 100 | lines = traceback.format_exception( 101 | exc_type, exc_value, exc_traceback) 102 | self.log_error( 103 | "SafariPushProvider -- exception {0}".format(''.join('!! ' + line for line in lines))) 104 | else: 105 | self.log_info("Nothing to send for SafariPushProvider") 106 | 107 | return send_count 108 | -------------------------------------------------------------------------------- /pushetta/core/push_providers/TestPushProvider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Fake push provider for test only 5 | 6 | from common import PushMessage, BaseProvider, PushProviderException 7 | 8 | 9 | class TestPushProvider(BaseProvider): 10 | def pushMessage(self, message, tokens, channel_name): 11 | if not isinstance(tokens, list): 12 | raise PushProviderException("tokens must be a list") 13 | return False 14 | 15 | if not isinstance(message, PushMessage): 16 | raise PushProviderException("message must be a PushMessage") 17 | return False 18 | 19 | return True -------------------------------------------------------------------------------- /pushetta/core/push_providers/WP8PushProvider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Provider for push to Windows Phone 8 Platform 5 | 6 | import logging 7 | 8 | from mpns import MPNSToast 9 | from django.utils.http import urlquote 10 | 11 | from common import BaseProvider 12 | 13 | 14 | # Testare questa lib https://pypi.python.org/pypi/python-mpns/0.1.3 15 | # Eventualmente provare approccio low level http://stackoverflow.com/questions/19366308/python-windows-phone-8-authenticated-push 16 | 17 | 18 | class WP8PushProvider(BaseProvider): 19 | 20 | def pushMessage(self, message, destToken, channel_name): 21 | send_count = len(destToken) 22 | sent = 0 23 | if send_count > 0: 24 | 25 | toast = MPNSToast() 26 | 27 | # Nota il body per sicurezza va mandato solo in versione "short" per il tile 28 | # ma i dati del messaggio li prende l'app con una get sull'id 29 | paramString = "?body=" + urlquote(message.alert_msg) + "&push_type=" + message.push_type + "&" 30 | 31 | for k in message.data_dic: 32 | paramString += k + "=" + str(urlquote(message.data_dic[k])) + "&" 33 | 34 | for tok in destToken: 35 | toast.send(tok, {'text1': message.alert_msg, 'text2': 'Pushetta', 'param': paramString[:-1]}) 36 | sent = sent + 1 37 | else: 38 | self.log_info("Nothing to send for WP8PushProvider") 39 | 40 | 41 | return sent 42 | -------------------------------------------------------------------------------- /pushetta/core/push_providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/core/push_providers/__init__.py -------------------------------------------------------------------------------- /pushetta/core/push_providers/common.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Common for push providers 5 | 6 | 7 | class PushMessage: 8 | """ 9 | A single message to send, aler_msg is show by device when it receive the notification. 10 | data_dic is the payolad of message 11 | """ 12 | 13 | def __init__(self, alert_msg, push_type, data_dic): 14 | self.alert_msg = alert_msg 15 | self.push_type = push_type 16 | self.data_dic = data_dic 17 | 18 | 19 | class PushProviderException(Exception): 20 | pass 21 | 22 | 23 | # Base class for all providers 24 | # TODO: logging management is a crap!!! Please refactor 25 | class BaseProvider(object): 26 | logger = None 27 | 28 | def log_debug(self, message): 29 | if self.logger != None: 30 | self.logger.debug(message) 31 | 32 | def log_info(self, message): 33 | if self.logger != None: 34 | self.logger.info(message) 35 | 36 | def log_error(self, message): 37 | if self.logger != None: 38 | self.logger.error(message) 39 | 40 | def pushMessage(self, message, tokens, channel_name): 41 | raise NotImplementedError("Method must be implemented from subclass") 42 | -------------------------------------------------------------------------------- /pushetta/core/push_providers/iOSPushProvider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Provider for push to Apple Push Message System 5 | 6 | import logging 7 | import sys 8 | import traceback 9 | import random 10 | 11 | from django.conf import settings 12 | import time 13 | 14 | from common import BaseProvider 15 | from apns import APNs, Frame, Payload 16 | 17 | 18 | # Doc per implementazione qui https://github.com/djacobs/PyAPNs 19 | # Nota installare con "pip install git+git://github.com/djacobs/PyAPNs.git" 20 | 21 | 22 | #  Procedura di invio massivo ispirata a https://gist.github.com/jimhorng/594401f68ce48282ced5 23 | SEND_INTERNAL = 0.01 24 | 25 | 26 | class iOSPushProvider(BaseProvider): 27 | 28 | def response_listener(self, error_response): 29 | self.log_info( 30 | "iOSprovider:client get error-response: " + str(error_response)) 31 | 32 | def wait_till_error_response_unchanged(self): 33 | 34 | if hasattr(self.apns.gateway_server, '_is_resending'): 35 | delay = 1 36 | count = 0 37 | while True: 38 | if self.apns.gateway_server._is_resending == False: 39 | time.sleep(delay) 40 | if self.apns.gateway_server._is_resending == False: 41 | count = count + delay 42 | else: 43 | count = 0 44 | else: 45 | count = 0 46 | 47 | if count >= 10: 48 | break 49 | return delay * count 50 | return 0 51 | 52 | def pushMessage(self, message, destToken, channel_name): 53 | send_count = len(destToken) 54 | if send_count > 0: 55 | try: 56 | 57 | # USO l'apns enanched 58 | self.apns = APNs(use_sandbox=settings.APNS_IS_SANDBOX, 59 | cert_file=settings.APNS_CERT_FILE, enhanced=True) 60 | 61 | payload = Payload( 62 | alert=message.alert_msg, sound="notification.aiff", badge=1, custom=message.data_dic) 63 | 64 | # TODO: Gestire l'expire 65 | # Expire in un mese di default (TODO: Ricavare dai dati passati) 66 | expiry = time.time() + 3600 * 24 * 30 67 | 68 | priority = 10 69 | for token in destToken: 70 | self.log_info( 71 | "iOSPushProvider -- send token {0}".format(token)) 72 | identifier = random.getrandbits(32) 73 | self.apns.gateway_server.register_response_listener( 74 | self.response_listener) 75 | self.apns.gateway_server.send_notification( 76 | token, payload, identifier=identifier) 77 | 78 | # Get feedback messages. 79 | for (token_hex, fail_time) in self.apns.feedback_server.items(): 80 | self.log_info( 81 | "iOSPushProvider -- feedback token {0} fail time {1}".format(token_hex, fail_time)) 82 | delay = self.wait_till_error_response_unchanged() 83 | self.apns.gateway_server.force_close() 84 | except: 85 | exc_type, exc_value, exc_traceback = sys.exc_info() 86 | lines = traceback.format_exception( 87 | exc_type, exc_value, exc_traceback) 88 | self.log_error( 89 | "iOSPushProvider -- exception {0}".format(''.join('!! ' + line for line in lines))) 90 | else: 91 | self.log_info("Nothing to send for iOSPushProvider") 92 | 93 | return send_count 94 | -------------------------------------------------------------------------------- /pushetta/core/push_providers/test.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Test for push providers 5 | 6 | import logging 7 | logger = logging.getLogger(__name__) 8 | 9 | from datetime import datetime 10 | from datetime import timedelta 11 | 12 | from django.test import TestCase 13 | from django.conf import settings 14 | 15 | from core.push_manager import PushProviderFactory 16 | from core.push_providers.common import PushMessage, PushProviderException 17 | 18 | class PushProvidersTestCase(TestCase): 19 | 20 | """ 21 | Test cases for Push Providers 22 | """ 23 | 24 | def setUp(self): 25 | settings.ENVIRONMENT = "dev" 26 | 27 | def test_ios_provider(self): 28 | # Creazione del messaggio da "pushare" 29 | extra_data = {"message_id": 141, "channel_name": "Test", "channel_image_url": "http://www.pushetta.com/uploads/channel_media/05e704fc35e544dfa50bacf89dda0eee.jpeg", 30 | "ott": "111"} 31 | 32 | pmsg = PushMessage(alert_msg="Long text with ", 33 | push_type="plain_push", 34 | data_dic=extra_data) 35 | 36 | provider = PushProviderFactory.create('ios', logger) 37 | provider.pushMessage(pmsg, ['c0f5eb4db72a04d24d474e7cae5f1e7828f6f9bcaa8f083476e4840cc552a4c3'], "a channel") 38 | 39 | def test_mqtt_provider(self): 40 | extra_data = { 41 | "message_id": 141, 42 | "channel_name": "flower", 43 | "channel_image_url": "http://www.pushetta.com/uploads/channel_media/05e704fc35e544dfa50bacf89dda0eee.jpeg", 44 | "ott": "111" 45 | } 46 | 47 | 48 | pmsg = PushMessage(alert_msg="Message over MQTT test", 49 | push_type="plain_push", 50 | data_dic=extra_data) 51 | 52 | provider = PushProviderFactory.create('iot_device', logger) 53 | # Nota...il push su MQTT non richiede token perchè la subscription al canale è uno stato 54 | # persistente gestita dal protcollo (chi è connesso e ha fatto subscribe al topic riceve il messaggio) 55 | provider.pushMessage(pmsg, ['#'], "flower") 56 | 57 | 58 | -------------------------------------------------------------------------------- /pushetta/core/search_indexes.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Indici per il motore di ricerca 5 | 6 | from haystack import indexes 7 | 8 | from core.models import Channel 9 | 10 | ''' 11 | class ChannelIndex(indexes.ModelSearchIndex, indexes.Indexable): 12 | class Meta: 13 | model = Channel 14 | 15 | def index_queryset(self, using=None): 16 | """Used when the entire index for model is updated.""" 17 | # filter(kind!=PRIVATE). 18 | return self.get_model().objects.filter(hidden=False) 19 | ''' 20 | 21 | 22 | class ChannelIndex(indexes.SearchIndex, indexes.Indexable): 23 | text = indexes.EdgeNgramField(document=True, use_template=True) 24 | 25 | name = indexes.CharField(model_attr='name') 26 | description = indexes.CharField(model_attr='description') 27 | image = indexes.CharField(model_attr='image') 28 | hidden = indexes.BooleanField(model_attr='hidden') 29 | kind = indexes.IntegerField(model_attr='kind') 30 | subscriptions = indexes.IntegerField(model_attr='subscriptions') 31 | 32 | def get_model(self): 33 | return Channel 34 | 35 | 36 | def index_queryset(self, using=None): 37 | """Used when the entire index for model is updated.""" 38 | # filter(kind!=PRIVATE). 39 | return self.get_model().objects.filter(hidden='false') 40 | -------------------------------------------------------------------------------- /pushetta/core/security_helpers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import hmac 4 | import hashlib 5 | import base64 6 | import string 7 | import random 8 | 9 | import redis 10 | from django.conf import settings 11 | 12 | 13 | class OTTManager(): 14 | """ 15 | Handler of One Time Token 16 | """ 17 | 18 | key_pattern = "{0}:ott:{1}" 19 | 20 | def __init__(self, ott_expire_seconds): 21 | pool = redis.ConnectionPool(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB) 22 | self.redis_client = redis.Redis(connection_pool=pool) 23 | self.ott_expire_seconds = ott_expire_seconds 24 | 25 | def getOneTimeToken(self): 26 | ott = self.__random_string_generator() 27 | # Il contenuto della chiave è insignificante, il token è rapresentato dall'esistenza della chiave stessa 28 | key = self.__get_key(ott) 29 | self.redis_client.set(key, 1) 30 | self.redis_client.expire(key, self.ott_expire_seconds) 31 | return ott 32 | 33 | 34 | def consumeOneTimeToken(self, ott): 35 | ret = False 36 | key = self.__get_key(ott) 37 | if self.redis_client.exists(key): 38 | self.redis_client.delete(key) 39 | ret = True 40 | 41 | return ret 42 | 43 | def existsOneTimeToken(self, ott): 44 | key = self.__get_key(ott) 45 | return self.redis_client.exists(key) 46 | 47 | def __random_string_generator(self, size=6, chars=string.ascii_uppercase + string.digits): 48 | return ''.join(random.choice(string.ascii_uppercase) for i in range(size)) 49 | # return ''.join(random.choice(chars) for _ in range(size)) 50 | 51 | def __get_key(self, ott): 52 | return self.key_pattern.format(settings.REDIS_KEY_PREFIX, ott) 53 | 54 | 55 | # Nota: seed è meglio che sia 20 caratteri 56 | def computeSignature(self, seed, contentType, dateString, methodPath): 57 | """ 58 | Compute the signature to validate devices requeste 59 | seed -- seed shared with devices with push notifications 60 | contentType -- value of http header Content-Type 61 | dateString -- UTC string with request date dd-mm-yyyy format 62 | methodPath -- method of API request (absolute path) 63 | """ 64 | # Nota: il seed è il seme condiviso tra client e server, se compromesso va cambiato 65 | # viene condiviso mediante notifiche push 66 | key = "4kGSVVETG8Ox/sF6zy6dFCpO97A3wUHr9jc41441" 67 | data = "POST\n\n{0}\n{1}\n{2}".format(contentType, dateString, methodPath) 68 | 69 | cKey = key.encode('ascii', 'ignore') 70 | cData = data.encode('ascii', 'ignore') 71 | 72 | dig = hmac.new(cKey, msg=cData, digestmod=hashlib.sha256).digest() 73 | b64HmacSha256 = base64.b64encode(dig).decode() # py3k-mode 74 | ret = "{1}:{0}".format(seed, b64HmacSha256) 75 | return ret 76 | -------------------------------------------------------------------------------- /pushetta/core/signal.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta Core 4 | # Handling dei signals sui modelli 5 | 6 | import logging 7 | from datetime import datetime 8 | from dateutil.relativedelta import relativedelta 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | from django.dispatch import receiver 13 | from django.db.models.signals import pre_delete, pre_save 14 | 15 | from core.services import unsubscribe_channel 16 | from core.models import Channel, Subscriber, ChannelMsg 17 | 18 | from core.subscriber_manager import SubscriberManager 19 | 20 | ''' 21 | Signal post delete del Channel che cancella i subscriber 22 | ''' 23 | 24 | 25 | @receiver(signal=pre_delete, sender=Channel) 26 | def pre_remove_channel(sender, **kwargs): 27 | channel = kwargs.get('instance') 28 | sub_manager = SubscriberManager() 29 | sub_tokens = sub_manager.get_all_subscribers(channel.name) 30 | devices = Subscriber.objects.filter(token__in=sub_tokens) 31 | 32 | logger.info("Delete channel ({0}) signal ".format(channel.name)) 33 | 34 | for dev in devices.all(): 35 | logger.info("\t removing subscriber {0}".format(dev.device_id)) 36 | unsubscribe_channel(channel, dev.device_id) 37 | 38 | 39 | ''' 40 | Signal post delete del Subscriber che cancella le sue sottoscrizioni 41 | ''' 42 | 43 | 44 | @receiver(signal=pre_delete, sender=Subscriber) 45 | def pre_remove_subscription(sender, **kwargs): 46 | subscriber = kwargs.get('instance') 47 | sub_manager = SubscriberManager() 48 | subscriptions = sub_manager.get_device_subscriptions(subscriber.device_id) 49 | 50 | logger.info("Delete subscriber ({0} - {1}) signal ".format(subscriber.name, subscriber.sub_type)) 51 | 52 | for channel_name in subscriptions: 53 | unsubscribe_channel(channel, subscriber.device_id) 54 | 55 | 56 | ''' 57 | Signal pre save del messaggio per bonificare l'expire se non passato 58 | ''' 59 | 60 | 61 | @receiver(signal=pre_save, sender=ChannelMsg) 62 | def pre_default_expire(sender, **kwargs): 63 | channelMsg = kwargs.get('instance') 64 | if not channelMsg.expire: 65 | channelMsg.expire = (datetime.utcnow() + relativedelta(months=1)) -------------------------------------------------------------------------------- /pushetta/core/subscriber_manager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Wrapper for interaction with Subscriber storage (Redis) 5 | 6 | # All strings in Unicode 7 | from __future__ import unicode_literals 8 | 9 | import redis 10 | from django.conf import settings 11 | 12 | 13 | class SubscriberManager(): 14 | """ 15 | This class mantain an Hash for every Channel containing all subscribers 16 | and a Set for every device containing all Channels subscribed 17 | """ 18 | 19 | # Key part for hash key (subscriber to channel) 20 | subsc_key_part = "subsc" 21 | #  Key part for set with devices subscribing a channel 22 | subdev_key_part = "subdev" 23 | 24 | 25 | def __init__(self): 26 | pool = redis.ConnectionPool(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB) 27 | self.redis_client = redis.Redis(connection_pool=pool) 28 | 29 | 30 | def flushdb(self): 31 | """ 32 | WARNING: Remove all keys from current database 33 | """ 34 | self.redis_client.flushdb() 35 | 36 | def subscribe(self, channel_name, sub_type, device_id, token): 37 | """ 38 | Subscribe a device to a Channel 39 | """ 40 | 41 | channel_name = channel_name.lower() 42 | 43 | key = self.__getHashKey(channel_name, sub_type) 44 | 45 | # Token in hash with Channel's subscribers 46 | self.redis_client.hset(key, self.__getFieldKey(device_id), token) 47 | skey = self.__getSetKey(device_id) 48 | # Channel name in set of channel subscribed by device 49 | self.redis_client.sadd(skey, channel_name) 50 | 51 | def unsubscribe(self, channel_name, device_id, sub_type): 52 | """ 53 | Unsubscribe a device from a channel 54 | """ 55 | 56 | channel_name = channel_name.lower() 57 | 58 | key = self.__getHashKey(channel_name, sub_type) 59 | # Token removed from hash with Channel's subscribers 60 | self.redis_client.hdel(key, self.__getFieldKey(device_id)) 61 | # Channel name removed from set of channel subscribed by device 62 | skey = self.__getSetKey(device_id) 63 | self.redis_client.srem(skey, channel_name) 64 | 65 | 66 | def get_subscription(self, channel_name, sub_type, device_id): 67 | """ 68 | Get channel subscription token for a device 69 | """ 70 | 71 | channel_name = channel_name.lower() 72 | 73 | key = self.__getHashKey(channel_name, sub_type) 74 | field_value = self.redis_client.hget(key, self.__getFieldKey(device_id)) 75 | return field_value 76 | 77 | 78 | def get_device_subscriptions(self, device_id): 79 | """ 80 | Returns all Channel subscribed by a device 81 | """ 82 | key = self.__getSetKey(device_id) 83 | return self.redis_client.smembers(key) 84 | 85 | def get_subscribers(self, channel_name, sub_type): 86 | """ 87 | Return all tokens subscribing a channel 88 | """ 89 | 90 | channel_name = channel_name.lower() 91 | 92 | key = self.__getHashKey(channel_name, sub_type) 93 | return self.redis_client.hvals(key) 94 | 95 | def get_all_subscribers(self, channel_name): 96 | """ 97 | Return all tokens subscribing a channel 98 | """ 99 | 100 | channel_name = channel_name.lower() 101 | 102 | keys = self.redis_client.keys(self.__getHashKeyPattern(channel_name)) 103 | result = [] 104 | for key in keys: 105 | for token in self.redis_client.hvals(key): 106 | result.append(token) 107 | return result 108 | 109 | 110 | def __getSetKey(self, device_id): 111 | return "{0}:{1}:{2}".format(settings.REDIS_KEY_PREFIX, self.subdev_key_part, device_id) 112 | 113 | def __getHashKeyPattern(self, channel_name): 114 | """ 115 | Hash key composed by prefisx:subsc:channel_name:sub_type 116 | """ 117 | return "{0}:{1}:{2}:*".format(settings.REDIS_KEY_PREFIX, self.subsc_key_part, channel_name) 118 | 119 | 120 | def __getHashKey(self, channel_name, sub_type): 121 | """ 122 | Hash key composed by prefisx:subsc:channel_name:sub_type 123 | """ 124 | return "{0}:{1}:{2}:{3}".format(settings.REDIS_KEY_PREFIX, self.subsc_key_part, channel_name, sub_type) 125 | 126 | def __getFieldKey(self, device_id): 127 | return device_id -------------------------------------------------------------------------------- /pushetta/core/tasks.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Task asincroni gestiti da celery 5 | 6 | 7 | from celery.task.schedules import crontab 8 | from celery.task import task, periodic_task 9 | from celery.utils.log import get_task_logger 10 | 11 | from core.models import ChannelMsg 12 | 13 | from core.push_manager import PushProviderFactory 14 | from core.subscriber_manager import SubscriberManager 15 | from core.feedback_manager import FeedbackManager 16 | from core.utility import grab_url_screenshot 17 | 18 | # Per l'uso email standard Django 19 | from django.core.mail import EmailMessage 20 | 21 | from django.core.cache import cache 22 | 23 | logger = get_task_logger(__name__) 24 | 25 | 26 | @task(name='core.tasks.push_messages', ignore_result=False, max_retries=1) 27 | def push_messages(pmsg, channel_name, platform): 28 | """ 29 | 30 | :param pmsg: Message to push 31 | :param channel_name: Channel to push 32 | :param platform: Target platform 33 | """ 34 | 35 | sub_tokens = SubscriberManager().get_subscribers(channel_name, platform) 36 | 37 | logger.debug("Pushing {0} tokens to channel {1} for platform {2}".format(len(sub_tokens), channel_name, platform)) 38 | 39 | #  PUSH EFFETTIVO 40 | pusher = PushProviderFactory.create(platform, logger) 41 | pusher.pushMessage(pmsg, sub_tokens, channel_name) 42 | 43 | 44 | @task(name='core.tasks.read_feedback_multi', ignore_result=True, max_retries=1) 45 | def read_feedback_multi(deviceId, messageIds): 46 | """ 47 | 48 | :param deviceId: Device id to mark read for 49 | :param messageIds: Array of id message to mark read 50 | """ 51 | marked = 0 52 | try: 53 | for msgId in messageIds: 54 | 55 | msg = ChannelMsg.objects.get(id=msgId) 56 | c = msg.channel.name 57 | 58 | if FeedbackManager().setFeedback(c, msgId, deviceId) > 0: 59 | marked = marked + 1 60 | 61 | except ChannelMsg.DoesNotExist: 62 | logger.error("Trying to set read feedback on non existent message") 63 | 64 | return marked 65 | 66 | 67 | @periodic_task(name='core.tasks.search_reindex', run_every=crontab(hour=4, minute=30)) 68 | def search_reindex(): 69 | from haystack.management.commands import update_index 70 | 71 | update_index.Command().handle(using='default', remove=True) 72 | logger.info("Reindexing of search database") 73 | 74 | 75 | @task(name='sendMailMessage', ignore_result=True, max_retries=1) 76 | def sendMailMessage(subject, body, rcpts): 77 | msg = EmailMessage(subject, body, 'support@gumino.com', rcpts, headers={'Reply-To': 'no-reply@pushetta.com'}) 78 | msg.send() 79 | 80 | LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes 81 | 82 | 83 | @task(name='get_screenshot', max_retries=3) 84 | def get_screenshot(url, message_id=None): 85 | # Questo task deve essere eseguito in modo seriale, solo uno in un dato 86 | # momento è attivo. Per questo si usa un lock 87 | ret = False 88 | lock_id = "screenshot_url-lock-id" 89 | acquire_lock = lambda: cache.add(lock_id, 'true', LOCK_EXPIRE) 90 | release_lock = lambda: cache.delete(lock_id) 91 | 92 | if acquire_lock(): 93 | try: 94 | grabbed_url = grab_url_screenshot(url) 95 | if grabbed_url and message_id: 96 | message = ChannelMsg.objects.get(id=message_id) 97 | message.preview_url = grabbed_url 98 | message.save() 99 | 100 | ret = True 101 | except: 102 | logger.error("Try to run screenshot_url while already running") 103 | finally: 104 | release_lock() 105 | 106 | return ret 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /pushetta/core/utility.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta Core 4 | # Varie funzioni di utilità usate nel progetto 5 | 6 | 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | import os 12 | import re 13 | import hashlib 14 | 15 | from urlparse import urlparse, urljoin 16 | 17 | from PIL import Image 18 | from ghost import Ghost 19 | 20 | from django.conf import settings 21 | 22 | 23 | def check_for_url(text): 24 | """ 25 | Check if text contains one o more urls 26 | :param text: text where to look for the urls 27 | :return: array of found urls 28 | """ 29 | 30 | #p = re.compile(ur'(?:https?:\/\/)?([\da-z\.-]+\.[a-z\.]{2,6}[\/\w]?)') 31 | p = re.compile(ur'(?:https?:\/\/)?([\da-z\.-]+\.[a-z\.]{2,6}[\/\w]?[]\w\/\.\-]*)') 32 | return re.findall(p, text) 33 | 34 | def grab_url_screenshot(url): 35 | """ 36 | Grab an url making a screenshot of it 37 | Filename is SHA256 of url 38 | :param url: 39 | :return: 40 | """ 41 | 42 | ret = None 43 | 44 | try: 45 | # Bonifica url (se manca lo schema assumo http://) 46 | url_res = urlparse(url) 47 | if not url_res.scheme: 48 | url = "http://" + url 49 | 50 | g = Ghost() 51 | with g.start() as ghost: 52 | page, res = ghost.open(url) 53 | if not page is None and page.http_status == 200: 54 | url_sha256 = hashlib.sha256(url).hexdigest() 55 | image_path = os.path.join('url_previews', url_sha256 + ".png") 56 | full_path = os.path.join(settings.MEDIA_ROOT, image_path) 57 | 58 | ghost.capture_to(full_path) 59 | ghost.page = None 60 | 61 | image_path = image_path.replace(".png", ".thumb.png") 62 | thumb_full_path = os.path.join(settings.MEDIA_ROOT,image_path) 63 | resize_and_crop(full_path, thumb_full_path, (550, 500)) 64 | ret = urljoin(settings.BASE_URL, "uploads/" + image_path) 65 | else: 66 | logger.error("Failed to capture screenshot for {0}".format(url)) 67 | except Exception, e: 68 | logger.exception(e) 69 | finally: 70 | del ghost 71 | return ret 72 | 73 | 74 | def resize_and_crop(img_path, modified_path, size, crop_type='top'): 75 | """ 76 | Resize and crop an image to fit the specified size. 77 | 78 | args: 79 | img_path: path for the image to resize. 80 | modified_path: path to store the modified image. 81 | size: `(width, height)` tuple. 82 | crop_type: can be 'top', 'middle' or 'bottom', depending on this 83 | value, the image will cropped getting the 'top/left', 'middle' or 84 | 'bottom/right' of the image to fit the size. 85 | raises: 86 | Exception: if can not open the file in img_path of there is problems 87 | to save the image. 88 | ValueError: if an invalid `crop_type` is provided. 89 | """ 90 | # If height is higher we resize vertically, if not we resize horizontally 91 | img = Image.open(img_path) 92 | # Get current and desired ratio for the images 93 | img_ratio = img.size[0] / float(img.size[1]) 94 | ratio = size[0] / float(size[1]) 95 | #The image is scaled/cropped vertically or horizontally depending on the ratio 96 | if ratio > img_ratio: 97 | img = img.resize((size[0], int(round(size[0] * img.size[1] / img.size[0]))), 98 | Image.ANTIALIAS) 99 | # Crop in the top, middle or bottom 100 | if crop_type == 'top': 101 | box = (0, 0, img.size[0], size[1]) 102 | elif crop_type == 'middle': 103 | box = (0, int(round((img.size[1] - size[1]) / 2)), img.size[0], 104 | int(round((img.size[1] + size[1]) / 2))) 105 | elif crop_type == 'bottom': 106 | box = (0, img.size[1] - size[1], img.size[0], img.size[1]) 107 | else : 108 | raise ValueError('ERROR: invalid value for crop_type') 109 | img = img.crop(box) 110 | elif ratio < img_ratio: 111 | img = img.resize((int(round(size[1] * img.size[0] / img.size[1])), size[1]), 112 | Image.ANTIALIAS) 113 | # Crop in the top, middle or bottom 114 | if crop_type == 'top': 115 | box = (0, 0, size[0], img.size[1]) 116 | elif crop_type == 'middle': 117 | box = (int(round((img.size[0] - size[0]) / 2)), 0, 118 | int(round((img.size[0] + size[0]) / 2)), img.size[1]) 119 | elif crop_type == 'bottom': 120 | box = (img.size[0] - size[0], 0, img.size[0], img.size[1]) 121 | else : 122 | raise ValueError('ERROR: invalid value for crop_type') 123 | img = img.crop(box) 124 | else : 125 | img = img.resize((size[0], size[1]), 126 | Image.ANTIALIAS) 127 | # If the scale is the same, we do not need to crop 128 | img.save(modified_path) 129 | -------------------------------------------------------------------------------- /pushetta/core/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta Core 4 | # Varie funzioni di utilità usate nel progetto 5 | 6 | from django.utils import six 7 | from django.conf import settings 8 | 9 | 10 | def custom_get_identifier(obj_or_string): 11 | """ 12 | Get an unique identifier for the object or a string representing the 13 | object. 14 | 15 | If not overridden, uses ... 16 | """ 17 | if isinstance(obj_or_string, six.string_types): 18 | if not IDENTIFIER_REGEX.match(obj_or_string): 19 | raise AttributeError(u"Provided string '%s' is not a valid identifier." % obj_or_string) 20 | 21 | return obj_or_string 22 | 23 | return u"%s" % (obj_or_string._get_pk_val()) 24 | -------------------------------------------------------------------------------- /pushetta/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pushetta.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /pushetta/pushetta/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | # This will make sure the app is always imported when 5 | # Django starts so that shared_task will use this app. 6 | from .celery import app as celery_app 7 | from core import signal 8 | 9 | __version__ = '1.1.5' 10 | __build__ = '' 11 | -------------------------------------------------------------------------------- /pushetta/pushetta/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from celery import Celery 6 | from django.conf import settings 7 | 8 | # set the default Django settings module for the 'celery' program. 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pushetta.settings') 10 | 11 | app = Celery('pushetta') 12 | 13 | # Using a string here means the worker will not have to 14 | # pickle the object when using Windows. 15 | app.config_from_object('django.conf:settings') 16 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 17 | 18 | 19 | @app.task(bind=True) 20 | def debug_task(self): 21 | print('Request: {0!r}'.format(self.request)) 22 | -------------------------------------------------------------------------------- /pushetta/pushetta/urls.py: -------------------------------------------------------------------------------- 1 | """pushetta URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | from django.conf import settings 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', admin.site.urls), 23 | url(r'^', include('www.urls')), 24 | url(r'^admin/', include(admin.site.urls)), 25 | url(r'^api/', include('api.urls')), 26 | ] 27 | 28 | if settings.SERVE_STATIC == True: 29 | urlpatterns.append(url(r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT})) 30 | urlpatterns.append(url(r'^uploads/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT})) 31 | -------------------------------------------------------------------------------- /pushetta/pushetta/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = 0.0.0.0:8001 3 | chmod-socket = 666 4 | processes = 1 5 | master = true 6 | logto = %v/uwsgi.log 7 | uid = pushetta 8 | chdir = %v/pushetta/ 9 | env = DJANGO_SETTINGS_MODULE=pushetta.settings 10 | module = pushetta.wsgi:application 11 | -------------------------------------------------------------------------------- /pushetta/pushetta/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for pushetta project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pushetta.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /pushetta/www/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/www/__init__.py -------------------------------------------------------------------------------- /pushetta/www/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class WwwConfig(AppConfig): 8 | name = 'www' 9 | -------------------------------------------------------------------------------- /pushetta/www/browser_views.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta Site 4 | # Class view per la gestione dei metodi legati alla gestione delle push si Chrome 5 | 6 | import json 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | from django.http import HttpResponse 12 | from django.views.generic import View 13 | 14 | from django.shortcuts import get_object_or_404 15 | 16 | from core.services import ask_subscribe_channel 17 | 18 | from core.models import Subscriber, Channel, User 19 | from core.subscriber_manager import SubscriberManager 20 | 21 | 22 | class WebPushRegistration(View): 23 | """ 24 | Custom API to handle post of registration data (user, token,...) 25 | Invoked by Ajax call in callback of permissionRequest client side 26 | """ 27 | 28 | 29 | 30 | # Check if device_id is subscriber of channel_name 31 | def get(self, request, device_id=None, channel_name=None): 32 | channel = get_object_or_404(Channel, name=channel_name) 33 | 34 | channels = SubscriberManager().get_device_subscriptions(device_id) 35 | resp = 200 36 | if next((x for x in channels if x == channel.name.lower()), None) == None: 37 | resp = 404 38 | 39 | 40 | return HttpResponse(status=resp) 41 | 42 | # Subscribe to a channel 43 | def post(self, request): 44 | post_data = json.loads(request.body) 45 | channel_name = None 46 | if 'channel' in post_data: 47 | channel_name = post_data['channel'] 48 | 49 | deviceToken = post_data['token'] 50 | browser = post_data['browser'] 51 | deviceId = post_data['device_id'] 52 | 53 | name = "-" 54 | if request.user.is_authenticated(): 55 | name = request.user.username 56 | 57 | # Create il subscriber if it doesn't exist 58 | subscriber, created = Subscriber.objects.update_or_create(device_id=deviceId, 59 | defaults={'sub_type': browser, 60 | 'sandbox': False, 'enabled': True, 61 | 'name': name, 62 | 'token': deviceToken}) 63 | 64 | # Channel subscription 65 | if channel_name is not None: 66 | channel = get_object_or_404(Channel, name=channel_name) 67 | ask_subscribe_channel(channel, deviceId) 68 | 69 | return HttpResponse(status=201 if created else 200) 70 | 71 | # Delete a channel subscription 72 | def delete(self, request, device_id=None, channel_name=None): 73 | channel = get_object_or_404(Channel, name=channel_name) 74 | channels = SubscriberManager().get_device_subscriptions(device_id) 75 | 76 | resp = 404 77 | if next((x for x in channels if x == channel.name.lower()), None) != None: 78 | current_dev = Subscriber.objects.get(device_id=device_id) 79 | SubscriberManager().unsubscribe(channel_name, device_id, current_dev.sub_type) 80 | resp = 200 81 | 82 | return HttpResponse(status=resp) 83 | 84 | 85 | -------------------------------------------------------------------------------- /pushetta/www/dispatcher_view.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta Site 4 | # Calss view per la gestione del dispatcher su device mobile 5 | 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | from django.http import HttpResponse, HttpResponseRedirect 11 | from django.views.generic import View 12 | from django.core.urlresolvers import reverse 13 | 14 | from django.shortcuts import render_to_response, get_object_or_404 15 | 16 | 17 | from django.template import RequestContext 18 | from core.models import Channel 19 | 20 | from user_agents import parse 21 | 22 | 23 | class ChannelSubscriberDispatcher(View): 24 | def get(self, request, channel_name=None): 25 | ua_string = request.META.get('HTTP_USER_AGENT', '') 26 | user_agent = parse(ua_string) 27 | 28 | logger.info(user_agent.browser.family) 29 | 30 | if user_agent.os.family == 'Android': 31 | return HttpResponseRedirect(reverse('android-subscribe', args=[channel_name])) 32 | elif user_agent.os.family == "iOS": 33 | return HttpResponseRedirect(reverse('ios-subscribe', args=[channel_name])) 34 | 35 | channel = get_object_or_404(Channel, name=channel_name) 36 | return render_to_response("channel-dispatcher.html", { 37 | 'channel': channel, 38 | 'browser' : user_agent.browser.family, 39 | }, 40 | context_instance=RequestContext(request)) 41 | 42 | 43 | 44 | 45 | # Identificazione del mobile device per reagire nel modo corretto con il dispatcher 46 | # Può tornare : iphone, ipad, android, blackberry, wp7, wp8 o wp (windows mobile generico) 47 | def __mobile(self, request): 48 | 49 | device = '' 50 | 51 | ua = request.META.get('HTTP_USER_AGENT', '').lower() 52 | 53 | if ua.find("iphone") > 0: 54 | device = "iphone" # + re.search("iphone os (\d)", ua).groups(0)[0] 55 | 56 | if ua.find("ipad") > 0: 57 | device = "ipad" 58 | 59 | if ua.find("android") > 0: 60 | device = "android" # + re.search("android (\d\.\d)", ua).groups(0)[0].translate(None, '.') 61 | 62 | if ua.find("blackberry") > 0: 63 | device = "blackberry" 64 | 65 | if ua.find("windows phone os 7") > 0: 66 | device = "wp7" 67 | elif ua.find("windows phone 8") > 0: 68 | device = "wp8" 69 | elif ua.find("iemobile") > 0: 70 | device = "wp" 71 | 72 | if not device: # either desktop, or something we don't care about. 73 | device = "baseline" 74 | 75 | return device 76 | 77 | class AndroidSubscribe(View): 78 | 79 | def get(self, request, channel_name=None): 80 | channel = None 81 | try: 82 | channel = Channel.objects.get(name=channel_name) 83 | except Channel.DoesNotExist: 84 | pass 85 | 86 | return render_to_response("android-subscribe.html", {'channel': channel}, 87 | context_instance=RequestContext(request)) 88 | 89 | class iOSSubscribe(View): 90 | 91 | def get(self, request, channel_name=None): 92 | channel = None 93 | try: 94 | channel = Channel.objects.get(name=channel_name) 95 | except Channel.DoesNotExist: 96 | pass 97 | 98 | return render_to_response("ios-subscribe.html", {'channel': channel}, 99 | context_instance=RequestContext(request)) 100 | 101 | class SafariSubscribe(View): 102 | 103 | def get(self, request, channel_name=None): 104 | channel = None 105 | try: 106 | channel = Channel.objects.get(name=channel_name) 107 | except Channel.DoesNotExist: 108 | pass 109 | 110 | return render_to_response("safari-subscribe.html", {'channel': channel}, 111 | context_instance=RequestContext(request)) 112 | 113 | class ChromeSubscribe(View): 114 | 115 | def get(self, request, channel_name=None): 116 | channel = None 117 | try: 118 | channel = Channel.objects.get(name=channel_name) 119 | except Channel.DoesNotExist: 120 | pass 121 | 122 | return render_to_response("chrome-subscribe.html", {'channel': channel}, 123 | context_instance=RequestContext(request)) 124 | -------------------------------------------------------------------------------- /pushetta/www/forms.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Definizione delle form usate nel progetto 5 | 6 | from django.forms import ModelForm 7 | from django import forms 8 | 9 | from core.models import Channel, ChannelMsg 10 | 11 | 12 | class ChannelForm(ModelForm): 13 | class Meta: 14 | model = Channel 15 | fields = ['name', 'image', 'description', 'kind', 'hidden'] 16 | 17 | widgets = { 18 | 'name': forms.TextInput(attrs={ 19 | 'class': 'span6', 20 | 'placeholder': 'Choose a name for Your channel' 21 | }), 22 | 'description': forms.Textarea(attrs={ 23 | 'class': 'span6', 24 | 'rows': 4, 25 | 'placeholder': 'Give a brief description of the meaning of Your channel' 26 | }), 27 | 'image': forms.HiddenInput() 28 | } 29 | 30 | -------------------------------------------------------------------------------- /pushetta/www/safari_views.py: -------------------------------------------------------------------------------- 1 | # Progetto: Pushetta Site 2 | # Class view per la gestione dei metodi legati alla gestione del token per le push su Safari 3 | 4 | # -*- coding: utf-8 -*- 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import json 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | from datetime import datetime 14 | from dateutil.relativedelta import relativedelta 15 | 16 | from django.conf import settings 17 | 18 | from django.http import Http404 19 | from django.http import HttpResponse, HttpResponseBadRequest 20 | from django.views.generic import View 21 | from django.views.generic.edit import DeletionMixin 22 | from django.views.generic.edit import CreateView 23 | from django.template import RequestContext 24 | 25 | from django.shortcuts import render_to_response, get_object_or_404 26 | from django.core.files import File 27 | 28 | from wsgiref.util import FileWrapper 29 | 30 | from core.subscriber_manager import SubscriberManager 31 | from core.services import ask_subscribe_channel, unsubscribe_channel, get_device_subscriptions 32 | 33 | from core.models import Subscriber, ChannelSubscribeRequest, Channel 34 | from core.models import ACCEPTED, REJECTED, PENDING, SUBSCRIBE_REQ_STATUS 35 | 36 | 37 | 38 | class SafariPushService(View): 39 | """ 40 | Handle token registration for Safari push 41 | """ 42 | 43 | def post(self, request, deviceToken=None, websitePushID=None): 44 | logger.info("POST deviceToken={0} websitePushID={1}".format(deviceToken, websitePushID)) 45 | return HttpResponse(status=200) 46 | 47 | def delete(self, request, deviceToken=None, websitePushID=None): 48 | logger.info("DELETE deviceToken={0} auth={1}".format(deviceToken, request.META['HTTP_AUTHORIZATION'])) 49 | # Rimozione della subscription (a tendere valutare logical delete) 50 | # Subscriber.objects.filter(token=deviceToken).delete() 51 | subscribed_channels = get_device_subscriptions(deviceToken) 52 | 53 | for channel in [c.channel for c in subscribed_channels]: 54 | unsubscribe_channel(channel, deviceToken) 55 | 56 | return HttpResponse(status=200) 57 | 58 | 59 | class SafariLogService(View): 60 | """ 61 | Handle log of errors for Safari push 62 | """ 63 | 64 | def post(self, request): 65 | logger.info("LOG BODY {0}".format(request.body)) 66 | return HttpResponse(status=200) 67 | 68 | 69 | class SafariPackageDownloadService(View): 70 | """ 71 | Handle download of Website Package for Safari push notification 72 | """ 73 | 74 | def post(self, request, deviceToken=None): 75 | PACKAGE_FILE = os.path.join(settings.BASE_DIR, '..', "var/Pushetta.pushpackage.zip") 76 | packageFile = open(PACKAGE_FILE, 'rb') 77 | 78 | response = HttpResponse(FileWrapper(packageFile), content_type='application/zip') 79 | response['Content-Disposition'] = 'attachment; filename="%s"' % 'Pushetta.pushpackage.zip' 80 | return response 81 | -------------------------------------------------------------------------------- /pushetta/www/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/www/templatetags/__init__.py -------------------------------------------------------------------------------- /pushetta/www/templatetags/activemenu.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Custom tags 5 | 6 | from django import template 7 | from django.core import urlresolvers 8 | 9 | 10 | register = template.Library() 11 | 12 | 13 | @register.simple_tag(takes_context=True) 14 | def active(context, url_name, return_value=' active', **kwargs): 15 | matches = current_url_equals(context, url_name, **kwargs) 16 | return return_value if matches else '' 17 | 18 | 19 | def current_url_equals(context, url_name, **kwargs): 20 | resolved = False 21 | try: 22 | resolved = urlresolvers.resolve(context.get('request').path) 23 | except: 24 | pass 25 | matches = resolved and resolved.url_name == url_name 26 | if matches and kwargs: 27 | for key in kwargs: 28 | kwarg = kwargs.get(key) 29 | resolved_kwarg = resolved.kwargs.get(key) 30 | if not resolved_kwarg or kwarg != resolved_kwarg: 31 | return False 32 | return matches -------------------------------------------------------------------------------- /pushetta/www/templatetags/storelink.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Progetto: Pushetta API 4 | # Custom tag con la definizione dei link per il download delle App dai vari store 5 | 6 | from django import template 7 | 8 | register = template.Library() 9 | 10 | store_links = {'android': "/apps", 11 | 'ios': "/apps", 12 | 'wp8': "/apps"} 13 | 14 | 15 | @register.simple_tag(takes_context=True) 16 | def storelink(context, platform, **kwargs): 17 | return_value = "" 18 | if platform in store_links: 19 | return_value = store_links[platform] 20 | return return_value 21 | -------------------------------------------------------------------------------- /pushetta/www/upload_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | from PIL import Image 7 | 8 | from django.shortcuts import render_to_response, get_object_or_404 9 | from django.conf import settings 10 | from django.http import HttpResponse, HttpResponseBadRequest 11 | from core.models import StoredImage 12 | from django.core.files.uploadedfile import UploadedFile 13 | 14 | 15 | import json 16 | 17 | # Nota: a tendere va rimosso con la gestione client del CSRF 18 | from django.views.decorators.csrf import csrf_exempt 19 | 20 | import logging 21 | 22 | log = logging 23 | 24 | 25 | @csrf_exempt 26 | def multiuploader_delete(request, pk): 27 | """ 28 | View for deleting photos with multiuploader AJAX plugin. 29 | made from api on: 30 | https://github.com/blueimp/jQuery-File-Upload 31 | """ 32 | if request.method == 'POST': 33 | log.info('Called delete image. Photo id=' + str(pk)) 34 | image = get_object_or_404(Image, pk=pk) 35 | image.delete() 36 | log.info('DONE. Deleted photo id=' + str(pk)) 37 | return HttpResponse(str(pk)) 38 | else: 39 | log.info('Recieved not POST request todelete image view') 40 | return HttpResponseBadRequest('Only POST accepted') 41 | 42 | 43 | @csrf_exempt 44 | def multiuploader(request): 45 | if request.method == 'POST': 46 | res = dict(success=False, error="", name="", url="") 47 | 48 | if request.FILES == None: 49 | return HttpResponseBadRequest('Must have files attached!') 50 | 51 | #getting file data for farther manipulations 52 | file = request.FILES[u'files[]'] 53 | wrapped_file = UploadedFile(file) 54 | filename = wrapped_file.name 55 | file_size = wrapped_file.file.size 56 | log.info('Got file: "' + str(filename) + '"') 57 | 58 | try: 59 | image = Image.open(file) 60 | #To get the image size, in pixels. 61 | (width,height) = image.size 62 | if width != 256 and height != 256: 63 | res["success"] = False 64 | res["error"] = "Image size must be 256x256 px" 65 | else: 66 | log.info('image w:{0} h:{1}'.format(width,height)) 67 | 68 | storedimg = StoredImage() 69 | storedimg.title = "" 70 | storedimg.image = file 71 | storedimg.save() 72 | 73 | #getting file url here 74 | file_url = settings.MEDIA_URL 75 | 76 | res["success"] = True 77 | res["name"] = str(storedimg) 78 | res["url"] = file_url 79 | res["error"] = "" 80 | except IOError: 81 | res["success"] = False 82 | res["error"] = "Format not recognized" 83 | 84 | response_data = json.dumps(res) 85 | return HttpResponse(response_data, content_type='application/json') 86 | else: #GET 87 | return render_to_response('multiuploader_main.html', 88 | dict(static_url=settings.MEDIA_URL, open_tv=u'{{', close_tv=u'}}'), 89 | ) 90 | 91 | 92 | def image_view(request): 93 | items = StoredImage.objects.all() 94 | return render_to_response('images.html', {'items': items}) -------------------------------------------------------------------------------- /pushetta/www/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | # Progetto: Pushetta API 5 | # Definizione delle Url per la API 6 | 7 | from django.conf.urls import url, include 8 | from django.views.generic import TemplateView 9 | from django.contrib.auth.decorators import login_required 10 | 11 | from dispatcher_view import ChannelSubscriberDispatcher, AndroidSubscribe, iOSSubscribe, SafariSubscribe, \ 12 | ChromeSubscribe 13 | from my_views import MyDashboardView, MyChannelsView, ChannelCreate, ChannelSearchView, ApproveRequestsView, \ 14 | UpdateChannelView 15 | 16 | from safari_views import SafariPushService, SafariPackageDownloadService, SafariLogService 17 | from browser_views import WebPushRegistration 18 | 19 | from upload_views import multiuploader, multiuploader_delete 20 | 21 | from django.views.decorators.csrf import csrf_exempt 22 | 23 | #  Funzionalità disponibili solo all'utente loggato 24 | privatearea_urls = [ 25 | url(r'^dashboard/$', login_required(MyDashboardView.as_view()), name="pushetta-dashboard"), 26 | url(r'^channels/$', login_required(MyChannelsView.as_view()), name="my-channels"), 27 | url(r'^channels/create/$', login_required(ChannelCreate.as_view()), name="my-channelcreate"), 28 | url(r'^channels/update-props/$', login_required(UpdateChannelView.as_view()), name="my-channelupdate-props"), 29 | url(r'^channels/(?P[\w|\W]+)/push/$', login_required(MyChannelsView.as_view()), 30 | name="my-channel-pushmessage"), 31 | url(r'^channels/(?P[\w|\W]+)/$', login_required(MyChannelsView.as_view()), name="my-channelview"), 32 | url(r'^requests/approve/$', login_required(ApproveRequestsView.as_view()), name="my-approve-requests"), 33 | ] 34 | 35 | # Url per la gestione della sottoscrizione ai Canali con gestione 36 | # del device di provenienza (Android, iOs, Wp, ...) 37 | channel_subscription_urls = [ 38 | url(r'^dispatch/(?P[\w|\W]+)/$', ChannelSubscriberDispatcher.as_view(), name="channel-subscription-dispatcher"), 39 | url(r'^android/(?P[\w|\W]+)/$', AndroidSubscribe.as_view(), name="android-subscribe"), 40 | url(r'^ios/(?P[\w|\W]+)/$', iOSSubscribe.as_view(), name="ios-subscribe"), 41 | url(r'^safari/(?P[\w|\W]+)/$',login_required(SafariSubscribe.as_view()), name="safari-subscribe"), 42 | url(r'^chrome/(?P[\w|\W]+)/$', login_required(ChromeSubscribe.as_view()),name="chrome-subscribe"), 43 | ] 44 | 45 | file_uploads_urls = [ 46 | url(r'^delete/(\d+)/$', multiuploader_delete, name="upload-delete"), 47 | url(r'^multi/$', multiuploader, name="upload-file"), 48 | ] 49 | 50 | # Url per la gestione della meccanica per il push to browser Safari 51 | safari_urls = [ 52 | url(r'^/devices/(?P[\w|\W]+)/registrations/web.com.pushetta$', csrf_exempt(SafariPushService.as_view()), name='safari-token-register'), 53 | url(r'^/pushPackages/web.com.pushetta$', csrf_exempt(SafariPackageDownloadService.as_view()), name='safari-package-download'), 54 | url(r'^/log', csrf_exempt(SafariLogService.as_view()), name='safari-log'), 55 | ] 56 | 57 | browser_urls = [ 58 | # Endpoint per la registrazione anonima del solo device 59 | url(r'^register/device$', csrf_exempt(WebPushRegistration.as_view()), name='browser-registration-device'), 60 | url(r'^register/(?P[\w|\W]+)/(?P[\w|\W]+)/$', login_required(csrf_exempt(WebPushRegistration.as_view())), name='browser-get-registration'), 61 | url(r'^register$', login_required(csrf_exempt(WebPushRegistration.as_view())), name='browser-registration'), 62 | ] 63 | 64 | urlpatterns = [ 65 | url(r'^$', TemplateView.as_view(template_name="index.html"), name="pushetta-index"), 66 | url(r'^cookie_policy/$', TemplateView.as_view(template_name="cookie_policy.html"), name="cookie_policy"), 67 | url(r'^apps/$', TemplateView.as_view(template_name="apps.html"), name="pushetta-apps"), 68 | url(r'^search/$', ChannelSearchView.as_view(), name="site-channel-search"), 69 | url(r'^my/', include(privatearea_urls)), 70 | url(r'^pushetta-search/$', ChannelSearchView.as_view(), name="pushetta-search"), 71 | url(r'^pushetta-api/', TemplateView.as_view(template_name="api-docs.html"), name="pushetta-api-doc"), 72 | url(r'^pushetta-docs/$', TemplateView.as_view(template_name="docs.html"), name="pushetta-docs"), 73 | url(r'^pushetta-downloads/$', TemplateView.as_view(template_name="downloads.html"), name="pushetta-downloads"), 74 | 75 | # API per l'integrazione push Safari 76 | url(r'^safari/v1', include(safari_urls)), 77 | 78 | # API per la gestione dei token acquisiti dal browser per le notifiche Push 79 | url(r'^browser/', include(browser_urls)), 80 | 81 | # Login, registrazione, ... con allauth 82 | url(r'^accounts/', include('allauth.urls')), 83 | 84 | url(r'subs/', include(channel_subscription_urls)), 85 | url(r'resource/', include(file_uploads_urls)), 86 | 87 | url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')), 88 | ] 89 | 90 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==1.4.5 2 | anyjson==0.3.3 3 | apns==2.0.1 4 | asn1crypto==0.24.0 5 | billiard==3.3.0.16 6 | bitarray==0.8.1 7 | boto==2.39.0 8 | celery==3.1.10 9 | certifi==2018.8.24 10 | cffi==1.11.5 11 | chardet==3.0.4 12 | click==6.7 13 | cryptography==2.2.2 14 | cssselect==0.9.1 15 | Django==1.8.19 16 | django-allauth==0.29.0 17 | django-bootstrap3==9.1.0 18 | django-debug-toolbar==1.2.1 19 | django-haystack==2.7.0 20 | django-rest-swagger==0.1.14 21 | django-ses==0.6.0 22 | django-taggit===0.21.1 23 | django-websocket-redis==0.4.1 24 | djangorestframework==2.4.8 25 | djangorestframework-jwt==0.1.4 26 | docutils==0.11 27 | drf-compound-fields==0.2.2 28 | drip==1.0.8 29 | elasticsearch==1.7.0 30 | enum34==1.1.6 31 | factory-boy==2.3.1 32 | gevent==1.3.3 33 | ghost==0.6.1 34 | Ghost.py==0.2.3 35 | greenlet==0.4.13 36 | idna==2.7 37 | ipaddress==1.0.22 38 | kombu==3.0.30 39 | lxml==3.3.6 40 | MySQL-python==1.2.5 41 | oauthlib==0.6.2 42 | paho-mqtt==1.3.1 43 | parse==1.8.4 44 | Pillow==2.4.0 45 | pyasn1==0.1.7 46 | pycparser==2.10 47 | pycrypto==2.6.1 48 | pyelasticsearch==1.4.1 49 | PyJWT==0.2.1 50 | pyOpenSSL==16.2.0 51 | pysolr==3.2.0 52 | python-dateutil==2.2 53 | pyfcm==1.4.7 54 | python-mpns==0.1.6 55 | python-openid==2.2.5 56 | pytz==2014.2 57 | redis==2.9.1 58 | requests==2.19.1 59 | requests-oauthlib==0.4.0 60 | rsa==3.1.4 61 | shortuuid==0.4.2 62 | simplejson==3.16.0 63 | six==1.11.0 64 | sqlparse==0.1.11 65 | South==0.8.4 66 | tinydb==3.9.0.post1 67 | ua-parser==0.8.0 68 | urllib3==1.23 69 | user-agents==1.1.0 70 | uWSGI==2.0.17 71 | Whoosh==2.6.0 72 | -------------------------------------------------------------------------------- /templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 | 15 |

{% trans "Account Inactive" %}

16 |
17 |
18 |

{% trans "This account is inactive." %}

19 |
20 |
21 | 22 |
23 |
24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/account/email.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load i18n %} 4 | {% load url from future %} 5 | 6 | {% block head_title %}{% trans "Account" %}{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |
13 | 14 | 15 |

{% trans "E-mail Addresses" %}

16 |
17 |
18 | 19 | {% if user.emailaddress_set.all %} 20 |

{% trans 'The following e-mail addresses are associated with your account:' %}

21 | 22 | 23 | {% csrf_token %} 24 |
25 | 26 | {% for emailaddress in user.emailaddress_set.all %} 27 |
28 | 44 |
45 | {% endfor %} 46 | 47 |
48 | 50 | 52 | 54 |
55 | 56 |
57 | 58 | 59 | {% else %} 60 |

61 | {% trans 'Warning:' %} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %} 62 |

63 | 64 | {% endif %} 65 | 66 |

{% trans "Add E-mail Address" %}

67 | 68 |
69 | {% csrf_token %} 70 | {{ form.as_p }} 71 | 72 |
73 | 74 |
75 |
76 |
77 |
78 | 79 | 80 | {% endblock %} 81 | 82 | 83 | {% block extra_body %} 84 | 97 | 98 | 99 | 100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_message.txt: -------------------------------------------------------------------------------- 1 | {% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with current_site.name as site_name %}User {{ user_display }} at {{ site_name }} has given this as an email address. 2 | 3 | To confirm this is correct, go to {{ activate_url }} 4 | {% endblocktrans %}{% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_signup_message.txt: -------------------------------------------------------------------------------- 1 | {% include "account/email/email_confirmation_message.txt" %} 2 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_signup_subject.txt: -------------------------------------------------------------------------------- 1 | {% include "account/email/email_confirmation_subject.txt" %} 2 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Confirm E-mail Address{% endblocktrans %} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_key_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% blocktrans with site.domain as site_domain and user.username as username %}You're receiving this e-mail because you or someone else has requested a password for your user account at {{site_domain}}. 2 | It can be safely ignored if you did not request a password reset. Click the link below to reset your password. 3 | 4 | {{password_reset_url}} 5 | 6 | In case you forgot, your username is {{username}}. 7 | 8 | Thanks for using our site! 9 | {% endblocktrans %} 10 | -------------------------------------------------------------------------------- /templates/account/email/password_reset_key_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %}Password Reset E-mail{% endblocktrans %} 4 | {% endautoescape %} -------------------------------------------------------------------------------- /templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | 4 | {% load url from future %} 5 | {% load i18n %} 6 | {% load account %} 7 | 8 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 9 | 10 | 11 | {% block content %} 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 | 20 | 21 |

{% trans "Confirm E-mail Address" %}

22 |
23 |
24 | {% if confirmation %} 25 | {% user_display confirmation.email_address.user as user_display %} 26 |

{% blocktrans with confirmation.email_address.email as email %}Please confirm that 27 | {{ email }} is an e-mail address for user 28 | {{ user_display }}.{% endblocktrans %}

29 |
30 | {% csrf_token %} 31 | 32 |
33 | {% else %} 34 | {% url 'account_email' as email_url %} 35 |

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please 36 | issue a new e-mail confirmation request.{% endblocktrans %} 37 |

38 | {% endif %} 39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /templates/account/email_confirmed.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 |
13 |
14 | 15 |

{% trans "Confirm E-mail Address" %}

16 |
17 |
18 | {% user_display email_address.user as user_display %} 19 | 20 |

{% blocktrans with email_address.email as email %}You have confirmed that 21 | {{ email }} is an e-mail address for user {{ user_display }} 22 | .{% endblocktrans %}

23 | 24 |
25 |
26 |
27 |
28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | 4 | {% load i18n %} 5 | {% load account %} 6 | {% load url from future %} 7 | 8 | {% block head_title %}{% trans "Sign In" %}{% endblock %} 9 | 10 | {% block bootstrap3_extra_head %} 11 | {{ block.super }} 12 | 13 | {% endblock %} 14 | 15 | 16 | {% block content %} 17 | 18 |

{% blocktrans %}If you have not created an account yet, then please 19 | sign up first.{% endblocktrans %}

20 | 21 | 22 | 23 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | 6 | {% block head_title %}{% trans "Sign Out" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Sign Out" %}

10 | 11 |

{% trans 'Are you sure you want to sign out?' %}

12 | 13 |
14 | {% csrf_token %} 15 | {% if redirect_field_value %} 16 | 17 | {% endif %} 18 | 19 |
20 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/account/messages/cannot_delete_primary_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You cannot remove your primary e-mail address ({{email}}).{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/email_confirmation_sent.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Confirmation e-mail sent to {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/email_confirmed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have confirmed {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/email_deleted.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Removed e-mail address {{email}}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/logged_in.txt: -------------------------------------------------------------------------------- 1 | {% load account %} 2 | {% load i18n %} 3 | {% user_display user as name %} 4 | {% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %} 5 | -------------------------------------------------------------------------------- /templates/account/messages/logged_out.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}You have signed out.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/password_changed.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Password successfully changed.{% endblocktrans %} 3 | 4 | -------------------------------------------------------------------------------- /templates/account/messages/password_set.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Password successfully set.{% endblocktrans %} 3 | 4 | -------------------------------------------------------------------------------- /templates/account/messages/primary_email_set.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Primary e-mail address set.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/messages/unverified_primary_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans %}Your primary e-mail address must be verified.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | 6 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |

{% trans "Change Password" %}

17 |
18 |
19 |
20 | {% csrf_token %} 21 | {{ form.as_p }} 22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 | 31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | 4 | {% load i18n %} 5 | {% load account %} 6 | {% load url from future %} 7 | 8 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 |
15 |
16 | 17 |

{% trans "Password Reset" %}

18 |
19 | 20 |
21 | 22 | {% if user.is_authenticated %} 23 | {% include "account/snippets/already_logged_in.html" %} 24 | {% endif %} 25 | 26 |

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

27 | 28 |
29 | {% csrf_token %} 30 | {{ form.as_p }} 31 | 32 |
33 | 34 |

{% blocktrans %}Please contact us if you have any trouble resetting your 35 | password.{% endblocktrans %}

36 | 37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 | 45 | {% endblock %} 46 | 47 | {% block extra_body %} 48 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 |
13 |
14 | 15 |

{% trans "Password Reset" %}

16 |
17 |
18 | 19 | {% if user.is_authenticated %} 20 | {% include "account/snippets/already_logged_in.html" %} 21 | {% endif %} 22 | 23 |

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a 24 | few minutes.{% endblocktrans %}

25 |
26 |
27 |
28 |
29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 | 13 | 14 |

{% if token_fail %}{% trans "Bad Token" %}{% else %} 15 | {% trans "Change Password" %}{% endif %}

16 |
17 |
18 | 19 | 20 | {% if token_fail %} 21 | {% url 'account_reset_password' as passwd_reset_url %} 22 |

{% blocktrans %}The password reset link was invalid, possibly because it has already been 23 | used. Please request a new password reset 24 | .{% endblocktrans %}

25 | {% else %} 26 | {% if form %} 27 |
28 | {% csrf_token %} 29 | {{ form.as_p }} 30 | 31 |
32 | {% else %} 33 |

{% trans 'Your password is now changed.' %}

34 | {% endif %} 35 | {% endif %} 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 |

{% trans "Change Password" %}

15 |
16 |
17 |

{% trans 'Your password is now changed.' %}

18 |
19 | 20 |
21 |
22 |
23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | 4 | {% load url from future %} 5 | {% load i18n %} 6 | 7 | {% block head_title %}{% trans "Set Password" %}{% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 |
13 |
14 |
15 | 16 |

{% trans "Set Password" %}

17 |
18 |
19 |
20 | {% csrf_token %} 21 | {{ form.as_p }} 22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | 6 | {% block head_title %}{% trans "Signup" %}{% endblock %} 7 | 8 | {% block bootstrap3_extra_head %} 9 | {{ block.super }} 10 | 11 | {% endblock %} 12 | 13 | 14 | {% block content %} 15 | 16 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {% endblock %} 64 | 65 | 66 | -------------------------------------------------------------------------------- /templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | 6 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 |

{% trans "Sign Up Closed" %}

19 |
20 | 21 |
22 | 23 |

{% trans "We are sorry, but the sign up is currently closed." %}

24 |
25 |
26 |
27 |
28 | 29 | 30 | {% endblock %} 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /templates/account/snippets/already_logged_in.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load account %} 3 | 4 | {% user_display user as user_display %} 5 | 6 |

{% trans "Note" %}: {% blocktrans %}you are already logged in as {{ user_display }}.{% endblocktrans %}

7 | -------------------------------------------------------------------------------- /templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 | 15 |

{% trans "Verify Your E-mail Address" %}

16 |
17 | 18 |
19 | 20 |

{% blocktrans %}We have sent an e-mail to you for verification.
21 | Follow the link provided to finalize the signup process.
22 | Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

23 |
24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% load url from future %} 4 | {% load i18n %} 5 | 6 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |

{% trans "Verify Your E-mail Address" %}

17 | {% url 'account_email' as email_url %} 18 |
19 | 20 |
21 | 22 |

{% blocktrans %}This part of the site requires us to verify that 23 | you are who you claim to be. For this purpose, we require that you 24 | verify ownership of your e-mail address. {% endblocktrans %}

25 | 26 |

{% blocktrans %}We have sent an e-mail to you for 27 | verification. Please click on the link inside this e-mail. Please 28 | contact us if you do not receive it within a few minutes.{% endblocktrans %}

29 | 30 |

{% blocktrans %}Note: you can still 31 | change your e-mail address.{% endblocktrans %}

32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /templates/email/email_subscribe_request.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% autoescape off %} 3 | {% blocktrans %} 4 | 5 | A device named {{ dev_name }} is asking to subscribe {{ channel_name }}, to authorize/reject request go to channel edit page at this 6 | url http://www.pushetta.com{{ channel_admin_url }} . 7 | 8 | 9 | Thanks for using Pushetta 10 | 11 | ------ 12 | Follow Us on Facebook https://www.facebook.com/www.pushetta 13 | 14 | 15 | {% endblocktrans %} 16 | {% endautoescape %} 17 | -------------------------------------------------------------------------------- /templates/rest_framework_swagger/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'www/__base.html' %} 2 | 3 | {% block title %}Pushetta API{% endblock %} 4 | {% load staticfiles %} 5 | 6 | {% block bootstrap3_extra_head %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | 12 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 | 17 |
18 |   19 |
20 |
21 | 22 |
23 | 24 | {% endblock %} 25 | 26 | {% block bootstrap3_extra_script %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 76 | 77 | {% endblock %} -------------------------------------------------------------------------------- /templates/search/indexes/core/channel_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.name }} 2 | {{ object.description }} 3 | {{ object.image }} 4 | {{ object.hidden }} 5 | {{ object.kind }} 6 | {{ object.subscriptions }} 7 | -------------------------------------------------------------------------------- /templates/www/404.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | {% block title %}Pushetta{% endblock %} 3 | {% block bootstrap3_extra_head %} 4 | {{ block.super }} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |

404

18 | 19 |

Who! bad trip man. No more pixels for you.

20 | 21 |
22 | Sorry, an error has occured! Why not try going back to the home page 23 |
24 | 25 | {% if user.is_authenticated %} 26 | 33 | {% endif %} 34 | 35 |
36 | 37 |
38 | 39 |
40 | {% endblock %} 41 | 42 | 43 | {% block bootstrap3_extra_script %} 44 | 45 | {% endblock %} 46 | 47 | -------------------------------------------------------------------------------- /templates/www/500.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | {% block title %}Pushetta{% endblock %} 3 | {% block bootstrap3_extra_head %} 4 | {{ block.super }} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |

404

18 | 19 |

ARRGGG!! Something goes wrong.

20 | 21 |
22 | Every time someone get this page a developer cry, try to go to the home page to stop this :-) 23 | 24 |
25 | 26 | {% if user.is_authenticated %} 27 | 34 | {% endif %} 35 | 36 |
37 | 38 |
39 | 40 |
41 | {% endblock %} 42 | 43 | 44 | {% block bootstrap3_extra_script %} 45 | 46 | {% endblock %} 47 | 48 | -------------------------------------------------------------------------------- /templates/www/__bootstrap.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap3/bootstrap3.html' %} 2 | 3 | {% block bootstrap3_title %}{% block title %}{% endblock %}{% endblock %} -------------------------------------------------------------------------------- /templates/www/android-subscribe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pushetta 7 | 10 | 11 | 12 | 13 | 19 | 35 | 36 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | {{ channel.name }} 92 | {{ channel.name }}
93 |

94 | {{ channel.description }} 95 |

96 | 97 |
98 | 99 |
 
100 |
101 | 102 |
Or
103 | 104 | 105 |
106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /templates/www/api.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% block title %}Pushetta{% endblock %} 4 | 5 | {% block bootstrap3_extra_head %} 6 | {{ block.super }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 |
15 |
16 | 17 |

Stats

18 |
19 | 20 |
21 |
22 |
23 |
Your Channels stats
24 | 25 |
26 |
851
27 |
423
28 |
922
29 |
25%
30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | {% endblock %} 43 | 44 | {% block bootstrap3_extra_script %} 45 | 46 | 47 | 48 | 49 | {% endblock %} -------------------------------------------------------------------------------- /templates/www/apps.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% block title %}Pushetta - Apps{% endblock %} 4 | 5 | {% block bootstrap3_extra_head %} 6 | {{ block.super }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 |

15 | Pushetta is in BETA testing stage. If You want to test the Apps write to guglielmino@gumino.com. 16 | At today there are three apps in develop: Android (most completed), Windows Phone 8 and iOS.
17 | In email specify which one You are interested for.

18 | 19 | Many thanks 20 |

21 | 22 |
23 | 24 |
25 | {% endblock %} 26 | 27 | {% block bootstrap3_extra_script %} 28 | {% endblock %} -------------------------------------------------------------------------------- /templates/www/chrome-subscribe.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% block title %}Pushetta - {{ channel.name }}{% endblock %} 4 | {% load storelink from storelink %} 5 | {% block bootstrap3_extra_head %} 6 | {{ block.super }} 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 | 12 |
13 | 14 |
15 | 16 |

Subscribing to {{ channel.name }} ...

17 | 18 |
19 | 23 | 24 | 27 | 28 |
29 | 36 |
37 |
38 | {% endblock %} 39 | 40 | {% block bootstrap3_extra_script %} 41 | 42 | 43 | 108 | 109 | {% endblock %} -------------------------------------------------------------------------------- /templates/www/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% block title %}Pushetta{% endblock %} 4 | 5 | {% block bootstrap3_extra_head %} 6 | {{ block.super }} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 |
15 |
16 | Your API Key (read documentation about this) 17 | 18 |

{{ apikey }}

19 |
20 |
21 |
22 |
23 | 24 | {% if user.channels.count == 0 %} 25 | 26 |
27 |
28 |
29 |

30 | You don't have created any channel right now. 31 |

32 | 33 | 36 |
37 |
38 |
39 | 40 | {% else %} 41 |
42 |
43 |
44 |
45 |

Channels

46 | 47 |
48 | {{ owner_channels_count }} 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |

Subscribers

58 | 59 |
60 | {{ subscribers_count }} 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Messages

70 | 71 |
72 | {{ messages_count }} 73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |

Latest channels

83 |
84 | {% for latest in latest_channels %} 85 |

{{ latest.name }} 86 | {{ latest.date_created }} 87 |

88 | 89 | {% endfor %} 90 | 91 |
92 |
93 |

Latest subscribers

94 |
95 | {% for subscriber in subscribers %} 96 |

97 | {{ subscriber.name }} 98 | 99 | {{ subscriber.date_created }} 100 | 101 |

102 | {% endfor %} 103 |
104 |
105 | 106 | {% endif %} 107 | 108 | {% endblock %} 109 | 110 | 111 | {% block bootstrap3_extra_script %} 112 | 113 | 114 | 115 | 116 | {% endblock %} 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /templates/www/downloads.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | {% block title %}Pushetta{% endblock %} 3 | {% block bootstrap3_extra_head %} 4 | {{ block.super }} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 |

Android App

18 |
Download Android App
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 |

iOS App

28 |
Download iOS App
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 |

Windows Phone App

38 |
Download Windows Phone 8 App
39 | 40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 | 49 |

Chrome Extension

50 |
Download Chrome Extension
51 | 52 |
53 |
54 |
55 | 56 |
57 | 58 | 59 | 73 | 74 | {% endblock %} 75 | 76 | 77 | {% block bootstrap3_extra_script %} 78 | 79 | {% endblock %} 80 | 81 | -------------------------------------------------------------------------------- /templates/www/index.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | {% block title %}Pushetta{% endblock %} 3 | {% block bootstrap3_extra_head %} 4 | {{ block.super }} 5 | 6 | 7 | {% endblock %} 8 | {% block content %} 9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 |
19 |

Pushetta logo Pushetta

20 |

21 | Pushetta is a system made to push realtime notifications to many 22 | different devices (mobile phones, browsers, smart TV, ...).
23 | It address a simple problem: get realtime information without using outdated methods (someone 24 | said "emails"?). 25 |
26 |
27 | Read more 28 |

29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 | 40 |
41 |

New house for Pushetta


42 |

43 | A BIG thank You to Utopía Soluciones Informáticas for supporting Us. 44 | Pushetta is now hosted in the server farm of Utopía and this is a big deal for our future. 45 | 46 |

47 |
48 | 49 |
50 | 51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
Pushetta
60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 | 68 | 69 | 70 | 71 | {% endblock %} 72 | {% block bootstrap3_extra_script %} 73 | 74 | 75 | 76 | 77 | 82 | 83 | 95 | 96 | 97 | 111 | 112 | 113 | {% endblock %} 114 | -------------------------------------------------------------------------------- /templates/www/ios-subscribe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pushetta 7 | 10 | 11 | 12 | 13 | 19 | 37 | 38 | 87 | 88 | 89 | 90 |
91 |

{{ channel.name }}{{ channel.name }}

92 | 93 |
94 | 95 |

96 | {{ channel.description }} 97 |

98 | 99 |
100 | 101 | waiting 102 |
103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /templates/www/partials/_messages_list.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |

Last messages sent

7 |
8 | 9 |
10 |
    11 | {% if channel.messages.count > 0 %} 12 | {% for msg in channel.messages.all|slice:":10" %} 13 | 14 |
  • 15 |
    {{ msg.date_created|date:"d" }} {{ msg.date_created|date:"M" }}
    18 |
    19 | Expire 20 | on {{ msg.expire|date:"M d" }} 21 | 22 |

    {{ msg.body }}

    23 |
    24 |
  • 25 | 26 | {% endfor %} 27 | {% else %} 28 |
  • 29 |
    30 |

    NO MESSAGES

    31 |
    32 |
  • 33 | 34 | {% endif %} 35 |
36 |
37 | 38 |
-------------------------------------------------------------------------------- /templates/www/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /my/ -------------------------------------------------------------------------------- /templates/www/search.html: -------------------------------------------------------------------------------- 1 | {% extends '__base.html' %} 2 | 3 | {% block title %}Pushetta{% endblock %} 4 | 5 | {% block bootstrap3_extra_head %} 6 | {{ block.super }} 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 15 |
16 |
17 | 23 | 24 |
25 | 26 |
27 | 28 |
29 | {% if suggested.count > 0 %} 30 | {% for channel in suggested %} 31 | 52 | {% endfor %} 53 | {% else %} 54 |
55 | No Channel found 56 |
57 | {% endif %} 58 |
59 | 60 | 61 | {% endblock %} 62 | 63 | {% block bootstrap3_extra_script %} 64 | 65 | 66 | 67 | 68 | 107 | 108 | {% endblock %} --------------------------------------------------------------------------------