├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── authentication.rst ├── conf.py ├── extending_device.rst ├── index.rst ├── quickstart.rst ├── registration.rst ├── sending_messages.rst └── signals.rst ├── example ├── apikeyauth_project │ ├── apikeyauth_project │ │ ├── __init__.py │ │ ├── my_app │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ ├── resources.py │ │ │ └── urls.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── manage.py └── basic_project │ ├── basic_project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ └── manage.py ├── gcm ├── __init__.py ├── admin.py ├── api.py ├── conf.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── gcm_messenger.py │ │ └── gcm_urls.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── resources.py ├── signals.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__chg_field_device_reg_id__add_unique_device_reg_id.py │ ├── 0003_auto__chg_field_device_reg_id.py │ └── __init__.py ├── templates │ └── gcm │ │ └── admin │ │ └── send_message.html ├── tests.py ├── urls.py └── utils.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | omit = 4 | gcm/migrations/* 5 | gcm/south_migrations/* 6 | gcm/tests.py 7 | source = gcm 8 | 9 | [report] 10 | exclude_lines = 11 | pragma: no cover 12 | raise NotImplementedError 13 | return NotImplemented 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | database.db 6 | .idea 7 | *~ 8 | dist 9 | *.egg-info 10 | .coverage 11 | .tox 12 | htmlcov 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | install: 4 | - pip install tox codecov 5 | 6 | script: 7 | - tox 8 | 9 | env: 10 | - TOXENV=py27-django17 11 | - TOXENV=py27-django18 12 | - TOXENV=py27-django19 13 | - TOXENV=py27-django_master 14 | - TOXENV=py34-django17 15 | - TOXENV=py34-django18 16 | - TOXENV=py34-django19 17 | - TOXENV=py34-django_master 18 | - TOXENV=py35-django18 19 | - TOXENV=py35-django19 20 | - TOXENV=py35-django_master 21 | 22 | matrix: 23 | allow_failures: 24 | - env: TOXENV=py27-django_master 25 | - env: TOXENV=py34-django_master 26 | - env: TOXENV=py35-django_master 27 | 28 | after_success: codecov 29 | 30 | addons: 31 | apt: 32 | sources: 33 | - deadsnakes 34 | packages: 35 | - python3.5 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Adam Bogdał 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | recursive-include gcm/templates * 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | pip install -e .[dev] --upgrade --process-dependency-links 3 | pip install tox 4 | 5 | test: 6 | coverage erase 7 | tox 8 | coverage html 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-gcm 2 | ========== 3 | 4 | .. image:: https://img.shields.io/travis/bogdal/django-gcm/master.svg 5 | :target: https://travis-ci.org/bogdal/django-gcm 6 | 7 | .. image:: https://img.shields.io/codecov/c/github/bogdal/django-gcm/master.svg 8 | :target: https://codecov.io/github/bogdal/django-gcm?branch=master 9 | 10 | .. image:: https://requires.io/github/bogdal/django-gcm/requirements.svg?branch=master 11 | :target: https://requires.io/github/bogdal/django-gcm/requirements/?branch=master 12 | 13 | .. image:: https://img.shields.io/pypi/v/django-gcm.svg 14 | :target: https://pypi.python.org/pypi/django-gcm/ 15 | 16 | 17 | Google Cloud Messaging Server in Django 18 | 19 | Quickstart 20 | ---------- 21 | 22 | Install the package via ``pip``:: 23 | 24 | pip install django-gcm --process-dependency-links 25 | 26 | Add ``gcm`` to ``INSTALLED_APPS`` in ``settings.py`` 27 | 28 | Add ``GCM_APIKEY`` to ``settings.py`` file: 29 | 30 | .. code-block:: python 31 | 32 | GCM_APIKEY = "" 33 | 34 | 35 | Add ``gcm urls`` to ``urls.py`` file: 36 | 37 | .. code-block:: python 38 | 39 | urlpatterns = [ 40 | ... 41 | url(r'', include('gcm.urls')), 42 | ... 43 | ] 44 | 45 | 46 | Documentation: `https://django-gcm.readthedocs.org `_ 47 | 48 | 49 | Client 50 | ------ 51 | 52 | Simple client application you can find `here `_. 53 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-gcm.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-gcm.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-gcm" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-gcm" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/authentication.rst: -------------------------------------------------------------------------------- 1 | Api key authentication 2 | ====================== 3 | 4 | Allows you to manage access to the GCM api using one of the available ``tastypie`` authentication methods - `ApiKeyAuthentication`. 5 | 6 | .. _django-tastypie Authentication: http://django-tastypie.readthedocs.org/en/latest/authentication.html 7 | 8 | .. note:: I strongly recommend see `django-tastypie Authentication`_ docs. 9 | 10 | 11 | Adding authentication requires `tastypie` added to your `INSTALLED_APPS` in the ``settings.py`` file: 12 | 13 | .. code-block:: python 14 | 15 | INSTALLED_APPS = [ 16 | ... 17 | 'gcm', 18 | 'tastypie', 19 | ] 20 | 21 | 22 | Adding user field 23 | -------------- 24 | 25 | You need to extend `Device` model and add user field. (See :ref:`extending_device`) 26 | 27 | .. code-block:: python 28 | 29 | # your_app/models.py 30 | from django.conf import settings 31 | from django.db import models 32 | from gcm.models import AbstractDevice 33 | 34 | class MyDevice(AbstractDevice): 35 | 36 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 37 | 38 | 39 | Add appropriate path to the ``settings.py`` file: 40 | 41 | .. code-block:: python 42 | 43 | GCM_DEVICE_MODEL = 'your_app.models.MyDevice' 44 | 45 | 46 | Resource class 47 | -------------- 48 | 49 | In your application, you need to create your own Resource class. It has to inherit from `gcm.resources.DeviceResource`. 50 | 51 | 52 | .. code-block:: python 53 | 54 | # your_app/resources.py 55 | from gcm.resources import DeviceResource 56 | from tastypie.authentication import ApiKeyAuthentication 57 | 58 | class AuthResource(DeviceResource): 59 | 60 | class Meta(DeviceResource.Meta): 61 | authentication = ApiKeyAuthentication() 62 | 63 | def get_queryset(self): 64 | qs = super(AuthResource, self).get_queryset() 65 | # to make sure that user can update only his own devices 66 | return qs.filter(user=self.request.user) 67 | 68 | def form_valid(self, form): 69 | form.instance.user = self.request.user 70 | return super(AuthResource, self).form_valid(form) 71 | 72 | 73 | 74 | You need to hook your resource class up in your ``urls.py`` file: 75 | 76 | .. code-block:: python 77 | 78 | # your_app/urls.py 79 | from django.conf.urls import include, url 80 | from tastypie.api import Api 81 | from .resources import AuthResource 82 | 83 | gcm_api = Api(api_name='v1') 84 | gcm_api.register(AuthResource()) 85 | 86 | 87 | urlpatterns = [ 88 | url(r'^gcm/', include(gcm_api.urls)), 89 | ] 90 | 91 | 92 | Include your ``urls.py`` file in the main URL router: 93 | 94 | .. code-block:: python 95 | 96 | # urls.py 97 | from django.conf.urls import include, url 98 | 99 | urlpatterns = [ 100 | url(r'', include('your_app.urls')), 101 | ] 102 | 103 | 104 | .. note:: See an example project ``gcm/example/apikeyauth_project`` 105 | 106 | 107 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-gcm documentation build configuration file, created by 4 | # sphinx-quickstart on Wed May 22 08:26:11 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-gcm' 44 | copyright = u'2015, Adam Bogdał' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '1.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '1.0.9' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | # If true, keep warnings as "system message" paragraphs in the built documents. 90 | #keep_warnings = False 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'django-gcmdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'django-gcm.tex', u'django-gcm Documentation', 190 | u'Adam Bogdal', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'django-gcm', u'django-gcm Documentation', 220 | [u'Adam Bogdal'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'django-gcm', u'django-gcm Documentation', 234 | u'Adam Bogdal', 'django-gcm', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | 247 | # If true, do not generate a @detailmenu in the "Top" node's menu. 248 | #texinfo_no_detailmenu = False 249 | -------------------------------------------------------------------------------- /docs/extending_device.rst: -------------------------------------------------------------------------------- 1 | .. _extending_device: 2 | 3 | Extending device model 4 | ====================== 5 | 6 | Allows you to store additional data in the device model (e.g. foreign key to the user) 7 | 8 | 9 | Device model 10 | ------------ 11 | 12 | In your application, you need to create your own `Device` model. This model has to inherit from `gcm.models.AbstractDevice`. 13 | 14 | .. code-block:: python 15 | 16 | # import the AbstractDevice class to inherit from 17 | from gcm.models import AbstractDevice 18 | 19 | class MyDevice(AbstractDevice): 20 | pass 21 | 22 | 23 | Use your model 24 | -------------- 25 | 26 | In the end, you have to inform `django-gcm` where it can find your model. 27 | 28 | Add appropriate path to the ``settings.py`` file: 29 | 30 | .. code-block:: python 31 | 32 | GCM_DEVICE_MODEL = 'your_app.models.MyDevice' 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-gcm's documentation 2 | ===================================== 3 | 4 | `Django-gcm` is a reusable application. It serves as a server for the GCM service and allows you to register devices and send messages to them. 5 | 6 | 7 | .. image:: https://travis-ci.org/bogdal/django-gcm.png?branch=develop 8 | :target: https://travis-ci.org/bogdal/django-gcm 9 | 10 | .. image:: https://version-image.appspot.com/pypi/?name=django-gcm 11 | :target: https://pypi.python.org/pypi/django-gcm 12 | 13 | 14 | Contents: 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | :numbered: 19 | 20 | quickstart.rst 21 | sending_messages.rst 22 | signals.rst 23 | extending_device.rst 24 | authentication.rst 25 | registration.rst 26 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | #. Install package via `pip`: 5 | 6 | .. code-block:: bash 7 | 8 | $ pip install django-gcm 9 | 10 | #. Add `django-gcm` resources to your URL router:: 11 | 12 | # urls.py 13 | from django.conf.urls import include, url 14 | 15 | urlpatterns = [ 16 | url(r'', include('gcm.urls')), 17 | ] 18 | 19 | 20 | To check gcm urls just use the following command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python manage.py gcm_urls 25 | 26 | GCM urls: 27 | * Register device 28 | /gcm/v1/device/register/ 29 | * Unregister device 30 | /gcm/v1/device/unregister/ 31 | 32 | 33 | #. Configure `django-gcm` in your ``settings.py`` file:: 34 | 35 | INSTALLED_APPS = [ 36 | # ... 37 | 'gcm', 38 | ] 39 | 40 | GCM_APIKEY = "" 41 | 42 | .. note:: To obtain api key go to https://code.google.com/apis/console and grab the key for the server app. -------------------------------------------------------------------------------- /docs/registration.rst: -------------------------------------------------------------------------------- 1 | Device registration endpoints 2 | ============================= 3 | 4 | Default ``django-gcm`` endpoints: 5 | 6 | * /gcm/v1/device/register/ 7 | * /gcm/v1/device/unregister/ 8 | 9 | .. note:: Command ``python manage.py gcm_urls`` returns the current endpoints. 10 | 11 | Register 12 | -------- 13 | 14 | POST parameters: 15 | 16 | ``dev_id`` 17 | Unique device identifier 18 | 19 | ``reg_id`` 20 | Registration token 21 | 22 | ``name`` 23 | Optional device name 24 | 25 | 26 | .. code-block:: bash 27 | 28 | curl -X POST -H "Content-Type: application/json" -d '{"dev_id": "test", "reg_id":"abcd", "name":"test device"}' \ 29 | http://localhost:8000/gcm/v1/device/register/ 30 | 31 | Unregister 32 | ---------- 33 | 34 | POST parameters: 35 | 36 | ``dev_id`` 37 | Unique device identifier 38 | 39 | 40 | .. code-block:: bash 41 | 42 | curl -X POST -H "Content-Type: application/json" -d '{"dev_id": "test"}' http://localhost:8000/gcm/v1/device/unregister/ 43 | -------------------------------------------------------------------------------- /docs/sending_messages.rst: -------------------------------------------------------------------------------- 1 | Sending messages 2 | ================ 3 | 4 | Using ``console``: 5 | 6 | .. code-block:: bash 7 | 8 | # Get the list of devices 9 | $ python manage.py gcm_messenger --devices 10 | > Devices list: 11 | > (#1) My phone 12 | 13 | # python manage.py gcm_messenger [--collapse-key ] 14 | $ python manage.py gcm_messenger 1 'my test message' 15 | 16 | Using ``Django orm``:: 17 | 18 | from gcm.models import get_device_model 19 | Device = get_device_model() 20 | 21 | my_phone = Device.objects.get(name='My phone') 22 | my_phone.send_message({'message':'my test message'}, collapse_key='something') 23 | 24 | ``collapse_key`` parameter is optional (default message). 25 | 26 | If you want to send additional arguments like ``delay_while_idle`` or other, add them as named variables e.g.:: 27 | 28 | my_phone.send_message({'message':'my test message'}, delay_while_idle=True, time_to_live=5) 29 | 30 | .. _Lifetime of a Message: https://developer.android.com/google/gcm/server.html#lifetime 31 | .. _Sending a downstream message: https://developer.android.com/google/gcm/server-ref.html#send-downstream 32 | 33 | .. note:: For more information, see `Lifetime of a Message`_ and `Sending a downstream message`_ docs. 34 | 35 | 36 | Multicast message 37 | ----------------- 38 | 39 | ``django-gcm`` supports sending messages to multiple devices at once. E.g.:: 40 | 41 | from gcm.models import get_device_model 42 | Device = get_device_model() 43 | 44 | Device.objects.all().send_message({'message':'my test message'}) 45 | 46 | 47 | Topic messaging 48 | ----------------------- 49 | 50 | ``django-gcm`` supports sending messages to multiple devices that have opted in to a particular gcm topic:: 51 | 52 | from gcm.api import GCMMessage 53 | 54 | GCMMessage().send({'message':'my test message'}, to='/topics/my-topic') 55 | 56 | .. _Send messages to topics: https://developers.google.com/cloud-messaging/topic-messaging 57 | 58 | .. note:: For more information, see `Send messages to topics`_. 59 | -------------------------------------------------------------------------------- /docs/signals.rst: -------------------------------------------------------------------------------- 1 | Signals 2 | ======= 3 | 4 | .. _signals: 5 | .. module:: gcm.signals 6 | 7 | .. data:: device_registered 8 | 9 | Sent when a device is registered. Provides the following arguments: 10 | 11 | ``sender`` 12 | The resource class used to register the device. 13 | 14 | ``device`` 15 | An instance of ``gcm.models.Device`` (see :ref:`extending_device`) 16 | represents the registered device. 17 | 18 | ``request`` 19 | The ``HttpRequest`` in which the device was registered. 20 | 21 | .. data:: device_unregistered 22 | 23 | Sent when a device is unregistered. Provides the following arguments: 24 | 25 | ``sender`` 26 | The resource class used to unregister the device. 27 | 28 | ``device`` 29 | An instance of ``gcm.models.Device`` (see :ref:`extending_device`) 30 | represents the unregistered device. 31 | 32 | ``request`` 33 | The ``HttpRequest`` in which the device was unregistered. 34 | -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/example/apikeyauth_project/apikeyauth_project/__init__.py -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/my_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/example/apikeyauth_project/apikeyauth_project/my_app/__init__.py -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/my_app/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from gcm.models import AbstractDevice 4 | 5 | 6 | class MyDevice(AbstractDevice): 7 | 8 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 9 | -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/my_app/resources.py: -------------------------------------------------------------------------------- 1 | from gcm.resources import DeviceResource 2 | from tastypie.authentication import ApiKeyAuthentication 3 | 4 | 5 | class AuthResource(DeviceResource): 6 | 7 | class Meta(DeviceResource.Meta): 8 | authentication = ApiKeyAuthentication() 9 | 10 | def get_queryset(self): 11 | qs = super(AuthResource, self).get_queryset() 12 | # to make sure that user can update only his own devices 13 | return qs.filter(user=self.request.user) 14 | 15 | def form_valid(self, form): 16 | form.instance.user = self.request.user 17 | return super(AuthResource, self).form_valid(form) 18 | -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/my_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from tastypie.api import Api 3 | from .resources import AuthResource 4 | 5 | gcm_api = Api(api_name='v1') 6 | gcm_api.register(AuthResource()) 7 | 8 | 9 | urlpatterns = patterns('', 10 | url(r'^gcm/', include(gcm_api.urls)), 11 | ) 12 | -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for apikeyauth_project project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'v2@zq1=_zs@!_ywj4a^@h!)h7=0884pl=whgs*#94lc3z*u70o' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 40 | # internal 41 | 'apikeyauth_project.my_app', 42 | 43 | # external 44 | 'tastypie', 45 | 'gcm', 46 | ) 47 | 48 | MIDDLEWARE_CLASSES = ( 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ) 56 | 57 | ROOT_URLCONF = 'apikeyauth_project.urls' 58 | 59 | WSGI_APPLICATION = 'apikeyauth_project.wsgi.application' 60 | 61 | 62 | # Database 63 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 64 | 65 | DATABASES = { 66 | 'default': { 67 | 'ENGINE': 'django.db.backends.sqlite3', 68 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 69 | } 70 | } 71 | 72 | # Internationalization 73 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 74 | 75 | LANGUAGE_CODE = 'en-us' 76 | 77 | TIME_ZONE = 'UTC' 78 | 79 | USE_I18N = True 80 | 81 | USE_L10N = True 82 | 83 | USE_TZ = True 84 | 85 | 86 | # Static files (CSS, JavaScript, Images) 87 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 88 | 89 | STATIC_URL = '/static/' 90 | 91 | # ApiKey - https://code.google.com/apis/console (Key for server apps) 92 | GCM_APIKEY = "" 93 | 94 | GCM_DEVICE_MODEL = 'apikeyauth_project.my_app.models.MyDevice' 95 | -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = [ 7 | url(r'^admin/', include(admin.site.urls)), 8 | url(r'', include('apikeyauth_project.my_app.urls')), 9 | ] 10 | -------------------------------------------------------------------------------- /example/apikeyauth_project/apikeyauth_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for apikeyauth_project 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.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apikeyauth_project.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /example/apikeyauth_project/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", "apikeyauth_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/basic_project/basic_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/example/basic_project/basic_project/__init__.py -------------------------------------------------------------------------------- /example/basic_project/basic_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for basic_project project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '_zaq#ww5_xmt5*icbna(4o=xinr7izj&u^3-xln1d&tl34q0f9' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 40 | 'gcm', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ) 51 | 52 | ROOT_URLCONF = 'basic_project.urls' 53 | 54 | WSGI_APPLICATION = 'basic_project.wsgi.application' 55 | 56 | 57 | # Database 58 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 59 | 60 | DATABASES = { 61 | 'default': { 62 | 'ENGINE': 'django.db.backends.sqlite3', 63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 64 | } 65 | } 66 | 67 | # Internationalization 68 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 69 | 70 | LANGUAGE_CODE = 'en-us' 71 | 72 | TIME_ZONE = 'UTC' 73 | 74 | USE_I18N = True 75 | 76 | USE_L10N = True 77 | 78 | USE_TZ = True 79 | 80 | 81 | # Static files (CSS, JavaScript, Images) 82 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 83 | 84 | STATIC_URL = '/static/' 85 | 86 | # ApiKey - https://code.google.com/apis/console (Key for server apps) 87 | GCM_APIKEY = "" -------------------------------------------------------------------------------- /example/basic_project/basic_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = [ 7 | url(r'^admin/', include(admin.site.urls)), 8 | url(r'', include('gcm.urls')), 9 | ] 10 | -------------------------------------------------------------------------------- /example/basic_project/basic_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for basic_project 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.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "basic_project.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /example/basic_project/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", "basic_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /gcm/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '1.2.0' 2 | -------------------------------------------------------------------------------- /gcm/admin.py: -------------------------------------------------------------------------------- 1 | from functools import update_wrapper 2 | from django.contrib import admin 3 | from django.shortcuts import redirect, render 4 | from django.template import RequestContext 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from .forms import MessageForm 8 | from .models import get_device_model 9 | 10 | Device = get_device_model() 11 | 12 | 13 | class DeviceAdmin(admin.ModelAdmin): 14 | list_display = ['dev_id', 'name', 'modified_date', 'is_active'] 15 | search_fields = ('dev_id', 'name') 16 | list_filter = ['is_active'] 17 | date_hierarchy = 'modified_date' 18 | readonly_fields = ('dev_id', 'reg_id') 19 | actions = ['send_message_action'] 20 | 21 | def get_urls(self): 22 | from django.conf.urls import url 23 | 24 | def wrap(view): 25 | def wrapper(*args, **kwargs): 26 | return self.admin_site.admin_view(view)(*args, **kwargs) 27 | return update_wrapper(wrapper, view) 28 | 29 | urlpatterns = [ 30 | url(r'^send-message/$', wrap(self.send_message_view), 31 | name=self.build_admin_url('send_message'))] 32 | return urlpatterns + super(DeviceAdmin, self).get_urls() 33 | 34 | def build_admin_url(self, url_name): 35 | return '%s_%s_%s' % (self.model._meta.app_label, 36 | self.model._meta.model_name, 37 | url_name) 38 | 39 | def send_message_view(self, request): 40 | base_view = 'admin:%s' % self.build_admin_url('changelist') 41 | session_key = 'device_ids' 42 | device_ids = request.session.get(session_key) 43 | if not device_ids: 44 | return redirect(base_view) 45 | 46 | form = MessageForm(data=request.POST or None) 47 | if form.is_valid(): 48 | devices = Device.objects.filter(pk__in=device_ids) 49 | for device in devices: 50 | device.send_message(form.cleaned_data['message']) 51 | self.message_user(request, _('Message was sent.')) 52 | del request.session[session_key] 53 | return redirect(base_view) 54 | 55 | context = {'form': form, 'opts': self.model._meta, 'add': False} 56 | return render(request, 'gcm/admin/send_message.html', context) 57 | 58 | def send_message_action(self, request, queryset): 59 | ids = queryset.values_list('id', flat=True) 60 | request.session['device_ids'] = list(ids) 61 | url = 'admin:%s' % self.build_admin_url('send_message') 62 | return redirect(url) 63 | send_message_action.short_description = _("Send message") 64 | 65 | 66 | admin.site.register(Device, DeviceAdmin) 67 | -------------------------------------------------------------------------------- /gcm/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.utils.encoding import force_text 6 | 7 | from . import conf 8 | 9 | 10 | class GCMMessage(object): 11 | 12 | def __init__(self): 13 | self.api_key = conf.GCM_APIKEY 14 | 15 | if not self.api_key: 16 | raise ImproperlyConfigured( 17 | "You haven't set the 'GCM_APIKEY' setting yet.") 18 | 19 | def _chunks(self, items, limit): 20 | """ 21 | Yield successive chunks from list \a items with a minimum size \a limit 22 | """ 23 | for i in range(0, len(items), limit): 24 | yield items[i:i + limit] 25 | 26 | def send(self, data, registration_ids=None, **kwargs): 27 | """ 28 | Send a GCM message for one or more devices, using json data 29 | registration_ids: A list with the devices which will be receiving a message 30 | data: The dict data which will be send 31 | Optional params e.g.: 32 | collapse_key: A string to group messages 33 | For more info see the following documentation: 34 | https://developer.android.com/google/gcm/server-ref.html#send-downstream 35 | """ 36 | 37 | if not isinstance(data, dict): 38 | data = {'msg': data} 39 | 40 | registration_ids = registration_ids or [] 41 | 42 | if len(registration_ids) > conf.GCM_MAX_RECIPIENTS: 43 | ret = [] 44 | for chunk in self._chunks( 45 | registration_ids, conf.GCM_MAX_RECIPIENTS): 46 | ret.append(self.send(data, registration_ids=chunk, **kwargs)) 47 | return ret 48 | 49 | values = { 50 | 'data': data, 51 | 'collapse_key': 'message'} 52 | if registration_ids: 53 | values.update({'registration_ids': registration_ids}) 54 | values.update(kwargs) 55 | 56 | values = json.dumps(values) 57 | 58 | headers = { 59 | 'UserAgent': "GCM-Server", 60 | 'Content-Type': 'application/json', 61 | 'Authorization': 'key=' + self.api_key} 62 | 63 | response = requests.post( 64 | url="https://gcm-http.googleapis.com/gcm/send", 65 | data=values, headers=headers) 66 | 67 | response.raise_for_status() 68 | return registration_ids, json.loads(force_text(response.content)) 69 | -------------------------------------------------------------------------------- /gcm/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | GCM_DEVICE_MODEL = getattr(settings, 'GCM_DEVICE_MODEL', 'gcm.models.Device') 4 | 5 | GCM_APIKEY = getattr(settings, 'GCM_APIKEY', None) 6 | 7 | GCM_MAX_RECIPIENTS = getattr(settings, 'GCM_MAX_RECIPIENTS', 1000) 8 | -------------------------------------------------------------------------------- /gcm/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from .models import get_device_model 5 | 6 | 7 | class RegisterDeviceForm(forms.ModelForm): 8 | 9 | class Meta: 10 | model = get_device_model() 11 | fields = ('dev_id', 'reg_id', 'name',) 12 | 13 | def save(self, commit=True): 14 | self.instance.is_active = True 15 | return super(RegisterDeviceForm, self).save(commit) 16 | 17 | 18 | class UnregisterDeviceForm(forms.ModelForm): 19 | 20 | class Meta: 21 | model = get_device_model() 22 | fields = ('dev_id',) 23 | 24 | def clean(self): 25 | if not self.instance.pk: 26 | raise forms.ValidationError( 27 | "Device '%s' does not exist" % self.cleaned_data['dev_id']) 28 | return self.cleaned_data 29 | 30 | def save(self, commit=True): 31 | self.instance.mark_inactive() 32 | return super(UnregisterDeviceForm, self).save(commit) 33 | 34 | 35 | class MessageForm(forms.Form): 36 | 37 | message = forms.CharField(label=_('Message'), required=True) 38 | -------------------------------------------------------------------------------- /gcm/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/gcm/management/__init__.py -------------------------------------------------------------------------------- /gcm/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/gcm/management/commands/__init__.py -------------------------------------------------------------------------------- /gcm/management/commands/gcm_messenger.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | from django.core.management.base import BaseCommand, CommandError 3 | 4 | from gcm.models import get_device_model 5 | 6 | Device = get_device_model() 7 | 8 | 9 | class Command(BaseCommand): 10 | args = '' 11 | help = 'Send message through gcm api' 12 | 13 | def add_arguments(self, parser): 14 | parser.add_argument('device_id', nargs='?', type=int) 15 | parser.add_argument('message', nargs='?', type=str) 16 | parser.add_argument( 17 | '--devices', 18 | action='store_true', 19 | dest='devices', 20 | default=False, 21 | help='List of available devices' 22 | ) 23 | parser.add_argument( 24 | '--collapse-key', 25 | dest='collapse_key', 26 | default='message', 27 | help='Set value of collapse_key flag, default is "message"' 28 | ) 29 | 30 | def handle(self, *args, **options): 31 | 32 | if options.get('devices', False): 33 | devices = Device.objects.filter(is_active=True) 34 | 35 | self.stdout.write("Devices list:\n") 36 | for device in devices: 37 | self.stdout.write("(#%s) %s\n" % (device.id, device.name)) 38 | self.stdout.write("\n") 39 | else: 40 | collapse_key = options.get('collapse_key', 'message') 41 | id = options.get('device_id') 42 | message = options.get('message') 43 | 44 | if not (id and message): 45 | raise CommandError( 46 | "Invalid params. You have to put all params: " 47 | "python manage.py gcm_messenger ") 48 | 49 | try: 50 | device = Device.objects.get(pk=int(id), is_active=True) 51 | except Device.DoesNotExist: 52 | raise CommandError( 53 | 'Unknown device (id=%s). Check list: ' 54 | 'python manage.py gcm_messenger --devices' % id) 55 | else: 56 | result = device.send_message( 57 | {'message': message}, collapse_key=collapse_key) 58 | 59 | self.stdout.write("[OK] device #%s (%s): %s\n" % 60 | (id, device.name, result)) 61 | -------------------------------------------------------------------------------- /gcm/management/commands/gcm_urls.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.core.urlresolvers import reverse 3 | 4 | 5 | class Command(BaseCommand): 6 | help = "Show GCM urls" 7 | 8 | def show_line(self): 9 | self.stdout.write("%s\n" % ("-" * 30)) 10 | 11 | def handle(self, *args, **options): 12 | url_kwargs = {'resource_name': 'device', 'api_name': 'v1'} 13 | register_url = reverse("register-device", kwargs=url_kwargs) 14 | unregister_url = reverse("unregister-device", kwargs=url_kwargs) 15 | 16 | self.show_line() 17 | self.stdout.write("GCM urls:\n") 18 | self.stdout.write("* Register device\n %s\n" % register_url) 19 | self.stdout.write("* Unregister device\n %s\n" % unregister_url) 20 | self.show_line() 21 | -------------------------------------------------------------------------------- /gcm/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Device', 15 | fields=[ 16 | ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), 17 | ('dev_id', models.CharField(unique=True, verbose_name='Device ID', max_length=50)), 18 | ('reg_id', models.CharField(unique=True, verbose_name='Registration ID', max_length=255)), 19 | ('name', models.CharField(null=True, blank=True, verbose_name='Name', max_length=255)), 20 | ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), 21 | ('modified_date', models.DateTimeField(verbose_name='Modified date', auto_now=True)), 22 | ('is_active', models.BooleanField(verbose_name='Is active?', default=False)), 23 | ], 24 | options={ 25 | 'ordering': ['-modified_date'], 26 | 'verbose_name_plural': 'Devices', 27 | 'abstract': False, 28 | 'verbose_name': 'Device', 29 | }, 30 | bases=(models.Model,), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /gcm/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/gcm/migrations/__init__.py -------------------------------------------------------------------------------- /gcm/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db import models 4 | from django.db.models.query import QuerySet 5 | from django.utils.encoding import python_2_unicode_compatible 6 | 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from . import conf 10 | from . import api 11 | from .utils import load_object 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def get_device_model(): 18 | return load_object(conf.GCM_DEVICE_MODEL) 19 | 20 | 21 | class GCMMessage(api.GCMMessage): 22 | GCM_INVALID_ID_ERRORS = ['InvalidRegistration', 23 | 'NotRegistered', 24 | 'MismatchSenderId'] 25 | 26 | def send(self, data, registration_ids=None, **kwargs): 27 | response = super(GCMMessage, self).send( 28 | data, registration_ids=registration_ids, **kwargs) 29 | chunks = [response] if not isinstance(response, list) else response 30 | for chunk in chunks: 31 | self.post_send(*chunk) 32 | return response 33 | 34 | def post_send(self, registration_ids, response): 35 | if response.get('failure'): 36 | invalid_messages = dict(filter( 37 | lambda x: x[1].get('error') in self.GCM_INVALID_ID_ERRORS, 38 | zip(registration_ids, response.get('results')))) 39 | 40 | regs = list(invalid_messages.keys()) 41 | for device in get_device_model().objects.filter(reg_id__in=regs): 42 | device.mark_inactive( 43 | error_message=invalid_messages[device.reg_id]['error']) 44 | 45 | 46 | class DeviceQuerySet(QuerySet): 47 | 48 | def send_message(self, data, **kwargs): 49 | if self: 50 | registration_ids = list(self.values_list("reg_id", flat=True)) 51 | return GCMMessage().send( 52 | data, registration_ids=registration_ids, **kwargs) 53 | 54 | 55 | class DeviceManager(models.Manager): 56 | 57 | def get_queryset(self): 58 | return DeviceQuerySet(self.model) 59 | get_query_set = get_queryset # Django < 1.6 compatiblity 60 | 61 | 62 | @python_2_unicode_compatible 63 | class AbstractDevice(models.Model): 64 | 65 | dev_id = models.CharField( 66 | verbose_name=_("Device ID"), max_length=50, unique=True,) 67 | reg_id = models.CharField( 68 | verbose_name=_("Registration ID"), max_length=255, unique=True) 69 | name = models.CharField( 70 | verbose_name=_("Name"), max_length=255, blank=True, null=True) 71 | creation_date = models.DateTimeField( 72 | verbose_name=_("Creation date"), auto_now_add=True) 73 | modified_date = models.DateTimeField( 74 | verbose_name=_("Modified date"), auto_now=True) 75 | is_active = models.BooleanField( 76 | verbose_name=_("Is active?"), default=False) 77 | 78 | objects = DeviceManager() 79 | 80 | def __str__(self): 81 | return self.dev_id 82 | 83 | class Meta: 84 | abstract = True 85 | verbose_name = _("Device") 86 | verbose_name_plural = _("Devices") 87 | ordering = ['-modified_date'] 88 | 89 | def send_message(self, data, **kwargs): 90 | return GCMMessage().send( 91 | registration_ids=[self.reg_id], data=data, **kwargs) 92 | 93 | def mark_inactive(self, **kwargs): 94 | self.is_active = False 95 | self.save() 96 | if kwargs.get('error_message'): 97 | logger.debug("Device %s (%s) marked inactive due to error: %s", 98 | self.dev_id, self.name, kwargs['error_message']) 99 | 100 | 101 | class Device(AbstractDevice): 102 | pass 103 | -------------------------------------------------------------------------------- /gcm/resources.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.http import HttpResponse, HttpResponseBadRequest 4 | from tastypie.authentication import Authentication 5 | from tastypie.authorization import Authorization 6 | from tastypie.resources import Resource 7 | from tastypie.serializers import Serializer 8 | 9 | from . import signals 10 | from .forms import RegisterDeviceForm, UnregisterDeviceForm 11 | from .models import get_device_model 12 | 13 | 14 | class DeviceResource(Resource): 15 | 16 | DEVICE_ID_FIELD_NAME = 'dev_id' 17 | 18 | model_class = get_device_model() 19 | register_form_class = RegisterDeviceForm 20 | unregister_form_class = UnregisterDeviceForm 21 | object = None 22 | request = None 23 | 24 | class Meta: 25 | resource_name = 'device' 26 | allowed_methods = ['post'] 27 | authentication = Authentication() 28 | authorization = Authorization() 29 | serializer = Serializer(formats=['json']) 30 | 31 | def prepend_urls(self): 32 | return [ 33 | url(r"^(?P%s)/register/$" % 34 | self._meta.resource_name, self.wrap_view('register'), 35 | name="register-device"), 36 | url(r"^(?P%s)/unregister/$" % 37 | self._meta.resource_name, self.wrap_view('unregister'), 38 | name="unregister-device"), 39 | ] 40 | 41 | def _verify(self, request): 42 | self.is_authenticated(request) 43 | self.throttle_check(request) 44 | self.method_check(request, self._meta.allowed_methods) 45 | 46 | def get_form_kwargs(self): 47 | data = self.deserialize(self.request, self.request.body) 48 | kwargs = {'data': data} 49 | try: 50 | kwargs['instance'] = self.get_instance(data) 51 | except ObjectDoesNotExist: 52 | pass 53 | return kwargs 54 | 55 | def get_form(self, form_class): 56 | return form_class(**self.get_form_kwargs()) 57 | 58 | def get_queryset(self): 59 | return self.model_class.objects.all() 60 | 61 | def get_instance(self, data): 62 | kwargs = {self.DEVICE_ID_FIELD_NAME: 63 | data.get(self.DEVICE_ID_FIELD_NAME)} 64 | return self.get_queryset().get(**kwargs) 65 | 66 | def form_valid(self, form): 67 | self.object = form.save() 68 | return HttpResponse 69 | 70 | def form_invalid(self, form): 71 | return HttpResponseBadRequest 72 | 73 | def _form_processing(self, form_class, request): 74 | self._verify(request) 75 | self.request = request 76 | 77 | form = self.get_form(form_class) 78 | 79 | if form.is_valid(): 80 | response_class = self.form_valid(form) 81 | else: 82 | response_class = self.form_invalid(form) 83 | 84 | return self.create_response(request, data={}, 85 | response_class=response_class) 86 | 87 | def register(self, request, **kwargs): 88 | form_class = self.register_form_class 89 | response = self._form_processing(form_class, request) 90 | signals.device_registered.send(sender=self, device=self.object, 91 | request=request) 92 | return response 93 | 94 | def unregister(self, request, **kwargs): 95 | form_class = self.unregister_form_class 96 | response = self._form_processing(form_class, request) 97 | signals.device_unregistered.send(sender=self, device=self.object, 98 | request=request) 99 | return response 100 | -------------------------------------------------------------------------------- /gcm/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | signal_args = ['device', 'request'] 4 | 5 | device_registered = Signal(providing_args=signal_args) 6 | device_unregistered = Signal(providing_args=signal_args) 7 | -------------------------------------------------------------------------------- /gcm/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from south.utils import datetime_utils as datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding model 'Device' 13 | db.create_table(u'gcm_device', ( 14 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('name', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), 16 | ('dev_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=50)), 17 | ('reg_id', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 18 | ('creation_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 19 | ('modified_date', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 20 | ('is_active', self.gf('django.db.models.fields.BooleanField')(default=False)), 21 | )) 22 | db.send_create_signal(u'gcm', ['Device']) 23 | 24 | 25 | def backwards(self, orm): 26 | # Deleting model 'Device' 27 | db.delete_table(u'gcm_device') 28 | 29 | 30 | models = { 31 | u'gcm.device': { 32 | 'Meta': {'ordering': "['-modified_date']", 'object_name': 'Device'}, 33 | 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 34 | 'dev_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}), 35 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 36 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 37 | 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 38 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 39 | 'reg_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) 40 | } 41 | } 42 | 43 | complete_apps = ['gcm'] -------------------------------------------------------------------------------- /gcm/south_migrations/0002_auto__chg_field_device_reg_id__add_unique_device_reg_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from south.utils import datetime_utils as datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | 13 | # Changing field 'Device.reg_id' 14 | db.alter_column(u'gcm_device', 'reg_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)) 15 | # Adding unique constraint on 'Device', fields ['reg_id'] 16 | db.create_unique(u'gcm_device', ['reg_id']) 17 | 18 | 19 | def backwards(self, orm): 20 | # Removing unique constraint on 'Device', fields ['reg_id'] 21 | db.delete_unique(u'gcm_device', ['reg_id']) 22 | 23 | 24 | # Changing field 'Device.reg_id' 25 | db.alter_column(u'gcm_device', 'reg_id', self.gf('django.db.models.fields.TextField')(null=True)) 26 | 27 | models = { 28 | u'gcm.device': { 29 | 'Meta': {'ordering': "['-modified_date']", 'object_name': 'Device'}, 30 | 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 31 | 'dev_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}), 32 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 34 | 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 36 | 'reg_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) 37 | } 38 | } 39 | 40 | complete_apps = ['gcm'] -------------------------------------------------------------------------------- /gcm/south_migrations/0003_auto__chg_field_device_reg_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from south.utils import datetime_utils as datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | 13 | # Changing field 'Device.reg_id' 14 | db.alter_column(u'gcm_device', 'reg_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)) 15 | 16 | def backwards(self, orm): 17 | pass 18 | 19 | models = { 20 | u'gcm.device': { 21 | 'Meta': {'ordering': "['-modified_date']", 'object_name': 'Device'}, 22 | 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 23 | 'dev_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}), 24 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 26 | 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 28 | 'reg_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) 29 | } 30 | } 31 | 32 | complete_apps = ['gcm'] -------------------------------------------------------------------------------- /gcm/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogdal/django-gcm/d11f8fcb038677e292bf8ffb4057ef51cf3a2938/gcm/south_migrations/__init__.py -------------------------------------------------------------------------------- /gcm/templates/gcm/admin/send_message.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n static %} 3 | 4 | {% block breadcrumbs %} 5 | {% with app_label=opts.app_label original=_('Send message') has_change_permission=1 %} 6 | {{ block.super }} 7 | {% endwith %} 8 | {% endblock %} 9 | 10 | {% block field_sets %} 11 |

{% trans "Send message" %}

12 |
13 | {{ form }} 14 |
15 | {% endblock field_sets %} 16 | 17 | {% block inline_field_sets %}{% endblock inline_field_sets %} 18 | 19 | 20 | {% block submit_buttons_bottom %} 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /gcm/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import json 3 | try: 4 | from StringIO import StringIO 5 | except ImportError: 6 | from io import StringIO 7 | 8 | from django.contrib.auth import get_user_model 9 | from django.core import management 10 | from django.core.exceptions import ImproperlyConfigured 11 | from django.core.management import CommandError 12 | from django.core.urlresolvers import reverse 13 | from django.test import TestCase 14 | from tastypie.test import ResourceTestCase 15 | from mock import patch, PropertyMock, MagicMock 16 | import requests 17 | 18 | from . import conf 19 | from .models import get_device_model 20 | from .api import GCMMessage as ApiGCMMessage 21 | 22 | Device = get_device_model() 23 | User = get_user_model() 24 | 25 | 26 | class CommandTest(TestCase): 27 | 28 | def test_gcm_urls(self): 29 | out = StringIO() 30 | management.call_command('gcm_urls', stdout=out) 31 | self.assertIn('/gcm/v1/device/register', out.getvalue()) 32 | self.assertIn('/gcm/v1/device/unregister', out.getvalue()) 33 | 34 | @patch.object(ApiGCMMessage, 'send') 35 | def test_send_message(self, mock_send): 36 | device_name = 'My test device' 37 | device = Device.objects.create(dev_id='device_1', name=device_name, 38 | reg_id='000123abc001', is_active=True) 39 | 40 | mock_send.return_value = ( 41 | Device.objects.values_list('reg_id', flat=True), 42 | {'failure': 0, 'canonical_ids': 0, 'success': 1, 43 | 'multicast_id': 112233, 'results': [{'message_id': '0:123123'}]}) 44 | 45 | out = StringIO() 46 | management.call_command('gcm_messenger', device_id=device.id, message='test', stdout=out) 47 | self.assertTrue(mock_send.called) 48 | 49 | management.call_command('gcm_messenger', devices=True, stdout=out) 50 | self.assertIn(device_name, out.getvalue()) 51 | 52 | self.assertRaises( 53 | CommandError, management.call_command, 'gcm_messenger') 54 | self.assertRaises( 55 | CommandError, management.call_command, 56 | 'gcm_messenger', '999', 'test') 57 | 58 | 59 | class AdminTest(TestCase): 60 | 61 | def setUp(self): 62 | user_password = 'password' 63 | user = User.objects.create_superuser( 64 | 'admin', 'admin@test.com', user_password) 65 | self.client.login(username=user.username, password=user_password) 66 | 67 | @patch.object(ApiGCMMessage, 'send') 68 | def test_send_message(self, mock_send): 69 | device = Device.objects.create( 70 | dev_id='device_1', reg_id='000123abc001', is_active=True) 71 | 72 | mock_send.return_value = ( 73 | Device.objects.values_list('reg_id', flat=True), 74 | {'failure': 0, 'canonical_ids': 0, 'success': 1, 75 | 'multicast_id': 112233, 'results': [{'message_id': '0:123123'}]}) 76 | 77 | self.client.post('/admin/gcm/device/', 78 | data={'action': 'send_message_action', 79 | '_selected_action': device.id}) 80 | 81 | response = self.client.post('/admin/gcm/device/send-message/', 82 | data={'message': 'admin test message'}) 83 | 84 | self.assertTrue(mock_send.called) 85 | self.assertEqual(response.status_code, 302) 86 | 87 | def test_do_not_send_empty_message(self): 88 | device = Device.objects.create( 89 | dev_id='device_1', reg_id='000123abc001', is_active=True) 90 | 91 | self.client.post('/admin/gcm/device/', 92 | data={'action': 'send_message_action', 93 | '_selected_action': device.id}) 94 | 95 | response = self.client.post('/admin/gcm/device/send-message/') 96 | self.assertEqual(response.status_code, 200) 97 | 98 | def test_send_message_view_requires_devices(self): 99 | response = self.client.get('/admin/gcm/device/send-message/') 100 | self.assertRedirects(response, '/admin/gcm/device/') 101 | 102 | 103 | class DeviceResourceTest(ResourceTestCase): 104 | 105 | def setUp(self): 106 | super(DeviceResourceTest, self).setUp() 107 | url_kwargs = {'resource_name': 'device', 'api_name': 'v1'} 108 | self.api_register_url = reverse( 109 | "register-device", kwargs=url_kwargs) 110 | self.api_unregister_url = reverse( 111 | "unregister-device", kwargs=url_kwargs) 112 | 113 | def _not_allowed_methods(self, url): 114 | self.assertHttpMethodNotAllowed(self.api_client.get(url)) 115 | self.assertHttpMethodNotAllowed(self.api_client.put(url)) 116 | self.assertHttpMethodNotAllowed(self.api_client.delete(url)) 117 | 118 | def test_register_device_not_allowed_methods(self): 119 | self._not_allowed_methods(self.api_register_url) 120 | 121 | def test_unregister_device_not_allowed_methods(self): 122 | self._not_allowed_methods(self.api_unregister_url) 123 | 124 | def test_register_device(self): 125 | device_id = 'TEST001' 126 | data = { 127 | 'dev_id': device_id, 128 | 'reg_id': 'abcd1234', 129 | } 130 | response = self.api_client.post(self.api_register_url, data=data) 131 | self.assertHttpOK(response) 132 | 133 | devices = Device.objects.filter(dev_id=device_id) 134 | self.assertEqual(devices.count(), 1) 135 | self.assertTrue(list(devices).pop().is_active) 136 | 137 | def test_unregister_device(self): 138 | device_id = 'TEST001' 139 | data = {'dev_id': device_id} 140 | Device.objects.create(dev_id=device_id, reg_id='abc1') 141 | response = self.api_client.post(self.api_unregister_url, data=data) 142 | self.assertHttpOK(response) 143 | 144 | devices = Device.objects.filter(dev_id=device_id) 145 | self.assertEqual(devices.count(), 1) 146 | self.assertFalse(list(devices).pop().is_active) 147 | 148 | def test_cannot_unregister_non_existent_device(self): 149 | device_id = 'FAKE_DEVICE_ID' 150 | data = {'dev_id': device_id} 151 | response = self.api_client.post(self.api_unregister_url, data=data) 152 | self.assertHttpBadRequest(response) 153 | self.assertEqual(Device.objects.all().count(), 0) 154 | 155 | def test_register_device_without_id(self): 156 | response = self.api_client.post(self.api_register_url, data={}) 157 | self.assertHttpBadRequest(response) 158 | 159 | def test_update_registration_id(self): 160 | device_id = 'TEST' 161 | expected_registration_id = 'xyz1' 162 | device = Device.objects.create(dev_id=device_id, reg_id='abc1') 163 | 164 | data = { 165 | 'dev_id': device_id, 166 | 'reg_id': expected_registration_id 167 | } 168 | response = self.api_client.post(self.api_register_url, data=data) 169 | self.assertHttpOK(response) 170 | 171 | reg_id = Device.objects.get(pk=device.pk).reg_id 172 | self.assertEqual(reg_id, expected_registration_id) 173 | 174 | 175 | class GCMMessageTest(TestCase): 176 | 177 | @patch.object(ApiGCMMessage, 'send') 178 | def test_mark_inactive(self, mock_send): 179 | Device.objects.create( 180 | dev_id='device_1', reg_id='000123abc001', is_active=True) 181 | Device.objects.create( 182 | dev_id='device_2', reg_id='000123abc002', is_active=True) 183 | Device.objects.create( 184 | dev_id='device_3', reg_id='000123abc003', is_active=True) 185 | 186 | mock_send.return_value = ( 187 | Device.objects.values_list('reg_id', flat=True), 188 | {'failure': 2, 'canonical_ids': 0, 'success': 1, 189 | 'multicast_id': 112233, 'results': [ 190 | {'error': 'InvalidRegistration'}, 191 | {'message_id': '0:123123'}, 192 | {'error': 'NotRegistered'}]}) 193 | 194 | Device.objects.all().send_message('test message') 195 | 196 | devices = Device.objects.filter(is_active=False) 197 | self.assertEqual(devices.count(), 2) 198 | 199 | @patch.object(ApiGCMMessage, 'send') 200 | def test_ignore_unhandled_error(self, mock_send): 201 | Device.objects.create( 202 | dev_id='device_1', reg_id='000123abc001', is_active=True) 203 | 204 | mock_send.return_value = ( 205 | Device.objects.values_list('reg_id', flat=True), 206 | {'failure': 1, 'canonical_ids': 0, 'success': 0, 207 | 'multicast_id': 112233, 'results': [{'error': 'UnhandledError'}]}) 208 | 209 | Device.objects.all().send_message('test message') 210 | 211 | device = Device.objects.get(dev_id='device_1') 212 | self.assertTrue(device.is_active) 213 | 214 | @patch.object(ApiGCMMessage, 'send') 215 | def test_ignore_active_device(self, mock_send): 216 | dev_id = 'device_1' 217 | device = Device.objects.create( 218 | dev_id=dev_id, reg_id='000123abc001', is_active=True) 219 | 220 | mock_send.return_value = ( 221 | Device.objects.values_list('reg_id', flat=True), 222 | {'failure': 0, 'canonical_ids': 0, 'success': 1, 223 | 'multicast_id': 112233, 'results': [{'message_id': '0:123123'}]}) 224 | 225 | device.send_message('test message') 226 | self.assertEqual(str(Device.objects.get(is_active=True)), dev_id) 227 | 228 | @patch('requests.post') 229 | def test_send_message_to_topic(self, mocked_post): 230 | post = MagicMock() 231 | post.content = '{}' 232 | post.status_code = 200 233 | mocked_post.return_value = post 234 | gcm_message = ApiGCMMessage() 235 | message = 'test message' 236 | topic = '/topics/test-topic' 237 | gcm_message.send(message, to=topic) 238 | expected_data = { 239 | 'data': {'msg': message}, 240 | 'to': topic, 241 | 'collapse_key': 'message'} 242 | self.assertDictEqual( 243 | json.loads(mocked_post.call_args[1]['data']), expected_data) 244 | 245 | @patch.object(ApiGCMMessage, 'send') 246 | def test_ignore_empty_queryset(self, mock_send): 247 | Device.objects.all().send_message('test') 248 | self.assertFalse(mock_send.called) 249 | 250 | @patch.object(conf, 'GCM_MAX_RECIPIENTS', 251 | new_callable=PropertyMock(return_value=2)) 252 | def test_split_to_chunks(self, mock_max_recipients): 253 | 254 | Device.objects.create( 255 | dev_id='device_1', reg_id='000123abc001', is_active=True) 256 | Device.objects.create( 257 | dev_id='device_2', reg_id='000123abc002', is_active=True) 258 | 259 | Device.objects.create( 260 | dev_id='device_3', reg_id='000123abc003', is_active=True) 261 | Device.objects.create( 262 | dev_id='device_4', reg_id='000123abc004', is_active=True) 263 | 264 | Device.objects.create( 265 | dev_id='device_5', reg_id='000123abc005', is_active=True) 266 | 267 | chunk_messages = [ 268 | {'failure': 1, 'canonical_ids': 0, 'success': 0, 269 | 'multicast_id': '0003', 'results': [{'error': 'NotRegistered'}]}, 270 | {'failure': 1, 'canonical_ids': 0, 'success': 1, 271 | 'multicast_id': '0002', 'results': [ 272 | {'message_id': '0:123123'}, 273 | {'error': 'InvalidRegistration'}]}, 274 | {'failure': 1, 'canonical_ids': 0, 'success': 1, 275 | 'multicast_id': '0001', 'results': [ 276 | {'error': 'InvalidRegistration'}, 277 | {'message_id': '0:123123'}]}] 278 | 279 | def side_effect(**kwargs): 280 | mock = MagicMock() 281 | mock.content = json.dumps(chunk_messages.pop()) 282 | return mock 283 | 284 | with patch.object(requests, 'post', side_effect=side_effect): 285 | Device.objects.all().send_message('test message') 286 | 287 | devices = Device.objects.filter(is_active=False) 288 | self.assertEqual(devices.count(), 3) 289 | 290 | @patch.object(conf, 'GCM_APIKEY', 291 | new_callable=PropertyMock(return_value=None)) 292 | def test_configuration(self, mock_apikey): 293 | device = Device.objects.create( 294 | dev_id='device_1', reg_id='000123abc001', is_active=True) 295 | self.assertRaises( 296 | ImproperlyConfigured, device.send_message, data='test') 297 | -------------------------------------------------------------------------------- /gcm/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from tastypie.api import Api 3 | 4 | from .resources import DeviceResource 5 | 6 | gcm_api = Api(api_name='v1') 7 | gcm_api.register(DeviceResource()) 8 | 9 | 10 | urlpatterns = [ 11 | url(r'^gcm/', include(gcm_api.urls))] 12 | -------------------------------------------------------------------------------- /gcm/utils.py: -------------------------------------------------------------------------------- 1 | from django.utils.module_loading import import_module 2 | 3 | 4 | def load_object(object_path): 5 | 6 | module_path, object_name = object_path.rsplit('.', 1) 7 | module = import_module(module_path) 8 | 9 | return getattr(module, object_name) 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # ~*~ coding: utf-8 ~*~ 2 | from setuptools import setup, find_packages 3 | from gcm import VERSION 4 | 5 | setup( 6 | name='django-gcm', 7 | version=VERSION, 8 | description='Google Cloud Messaging Server', 9 | long_description=open('README.rst').read(), 10 | author='Adam Bogdał', 11 | author_email='adam@bogdal.pl', 12 | url='https://github.com/bogdal/django-gcm', 13 | packages=find_packages(), 14 | package_data={ 15 | 'gcm': ['locale/*/LC_MESSAGES/*'] 16 | }, 17 | include_package_data=True, 18 | classifiers=[ 19 | 'Environment :: Web Environment', 20 | 'Framework :: Django', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3.4', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 29 | 'Topic :: Software Development :: Libraries :: Python Modules'], 30 | zip_safe=False, 31 | install_requires=[ 32 | 'django>=1.7', 33 | 'django-tastypie>=0.12.3a0', 34 | 'pytz>=2013.8', 35 | 'requests>=1.2.0', 36 | ], 37 | dependency_links=[ 38 | 'https://github.com/django-tastypie/django-tastypie/tarball/f0d07abd12432df7c77f9527f5d3d211e7a68797#egg=django-tastypie-0.12.3a0', 39 | ], 40 | extras_require={ 41 | 'dev': [ 42 | 'mock==1.3.0', 43 | 'coverage' 44 | ] 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34}-django{17,18,19,_master},py35-django{18,19,_master} 3 | skipsdist = true 4 | 5 | [testenv] 6 | commands = 7 | python --version 8 | coverage run example/basic_project/manage.py test gcm 9 | install_command = pip install --process-dependency-links --pre {opts} {packages} 10 | deps = 11 | django17: django>=1.7,<1.8a0 12 | django18: django>=1.8,<1.9a0 13 | django19: django>=1.9a1,<1.10a0 14 | django_master: https://github.com/django/django/archive/master.tar.gz 15 | -e.[dev] 16 | --------------------------------------------------------------------------------