├── accounts ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── forms.py └── views.py ├── djangofcm ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── notifications ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── urls.py ├── apps.py ├── forms.py └── views.py ├── .gitignore ├── requirements.txt ├── images ├── project_settings.png └── test_notification.png ├── .env ├── CONTRIBUTING.md ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── templates ├── accounts │ ├── login.html │ └── register.html ├── notifications │ └── index.html ├── firebase-messaging-sw.js └── base.html ├── manage.py ├── LICENSE ├── static └── js │ └── index.js ├── CODE_OF_CONDUCT.md └── README.md /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djangofcm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /notifications/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /notifications/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | venv/ 4 | 5 | *.sqlite3 -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /notifications/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /notifications/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /notifications/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | djangorestframework 3 | fcm-django 4 | firebase-admin 5 | django-crispy-forms 6 | -------------------------------------------------------------------------------- /images/project_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpt9m0/django-fcm-sample-project/HEAD/images/project_settings.png -------------------------------------------------------------------------------- /images/test_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpt9m0/django-fcm-sample-project/HEAD/images/test_notification.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Never publish this file on your repository 2 | GOOGLE_APPLICATION_CREDENTIALS=C:\\Users\\aliay\\Desktop\\djangofcm_credentials.json 3 | -------------------------------------------------------------------------------- /notifications/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index_page, name='index'), 7 | ] 8 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'accounts' 7 | -------------------------------------------------------------------------------- /notifications/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NotificationsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'notifications' 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Forget all complex contributing guide lines! 2 | Just consider following ways: 3 | - Submitting issues with enough details. 4 | - Fork, develope your changes and make pull requests. 5 | - Star and share the repository. 6 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('register/', views.register_page, name="register"), 7 | path("login/", views.login_page, name="login"), 8 | path("logout/", views.logout_request, name= "logout"), 9 | ] 10 | -------------------------------------------------------------------------------- /notifications/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class TestNotification(forms.Form): 5 | user_id = forms.IntegerField() 6 | title = forms.CharField() 7 | body = forms.CharField() 8 | icon_url = forms.CharField(help_text="Image to display.") 9 | url = forms.CharField(help_text="Url to open by clicking.") 10 | -------------------------------------------------------------------------------- /djangofcm/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for djangofcm project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangofcm.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /djangofcm/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djangofcm 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/3.2/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', 'djangofcm.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.contrib.auth.models import User 4 | 5 | class NewUserForm(UserCreationForm): 6 | email = forms.EmailField(required=True) 7 | 8 | class Meta: 9 | model = User 10 | fields = ("username", "email", "password1", "password2") 11 | 12 | def save(self, commit=True): 13 | user = super(NewUserForm, self).save(commit=False) 14 | user.email = self.cleaned_data['email'] 15 | if commit: 16 | user.save() 17 | return user 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /templates/accounts/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block body %} 5 | 6 |
7 |
8 |
9 | 10 |
11 |

Login

12 |
13 | {% csrf_token %} 14 | {{ login_form|crispy }} 15 | 16 |
17 |

Don't have an account? Create an account.

18 |
19 |
20 |
21 |
22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /templates/accounts/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block body %} 5 | 6 | 7 |
8 |
9 |
10 | 11 |
12 |

Register

13 |
14 | {% csrf_token %} 15 | {{ register_form | crispy }} 16 | 17 |
18 |

If you already have an account, login instead.

19 |
20 |
21 |
22 |
23 | 24 | {% endblock %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangofcm.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /templates/notifications/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block body %} 6 |
7 |
8 |
9 | 10 | 11 | 12 |
13 |

Test Notification

14 |
15 | {% csrf_token %} 16 | {{ form | crispy }} 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | {% endblock %} 25 | 26 | {% block js %} 27 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Seyyed Ali Ayati 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /notifications/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib import messages 3 | from django.contrib.auth.decorators import login_required 4 | 5 | from .forms import TestNotification 6 | 7 | from firebase_admin.messaging import Message 8 | from fcm_django.models import FCMDevice 9 | 10 | 11 | @login_required 12 | def index_page(request): 13 | if request.method == "POST": 14 | form = TestNotification(request.POST) 15 | if form.is_valid(): 16 | message_obj = Message( 17 | data={ 18 | 'title': form.cleaned_data.get('title'), 19 | 'body': form.cleaned_data.get('body'), 20 | 'icon_url': form.cleaned_data.get('icon_url'), 21 | 'url': form.cleaned_data.get('url'), 22 | }, 23 | ) 24 | devices = FCMDevice.objects.filter(user_id=form.cleaned_data.get('user_id')) 25 | if devices.exists(): 26 | print(devices) 27 | devices.send_message(message_obj) 28 | return redirect('index') 29 | messages.error( 30 | request, "Unsuccessful. Invalid information.") 31 | form = TestNotification() 32 | return render(request, 'notifications/index.html', context={'form': form}) 33 | -------------------------------------------------------------------------------- /djangofcm/urls.py: -------------------------------------------------------------------------------- 1 | """djangofcm URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/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: path('', 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: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | from django.views.generic import TemplateView 21 | 22 | from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet 23 | from rest_framework.routers import DefaultRouter 24 | 25 | router = DefaultRouter() 26 | router.register('devices', FCMDeviceAuthorizedViewSet) 27 | 28 | urlpatterns = [ 29 | path("firebase-messaging-sw.js", 30 | TemplateView.as_view( 31 | template_name="firebase-messaging-sw.js", 32 | content_type="application/javascript", 33 | ), 34 | name="firebase-messaging-sw.js" 35 | ), 36 | path('admin/', admin.site.urls), 37 | path('api/', include(router.urls)), 38 | path('', include('notifications.urls')), 39 | path('accounts/', include('accounts.urls')), 40 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 41 | -------------------------------------------------------------------------------- /templates/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | // [START initialize_firebase_in_sw] 2 | // Give the service worker access to Firebase Messaging. 3 | // Note that you can only use Firebase Messaging here, other Firebase libraries 4 | // are not available in the service worker. 5 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 6 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 7 | 8 | // Initialize the Firebase app in the service worker by passing in the 9 | // messagingSenderId. 10 | firebase.initializeApp({ 11 | // Replace messagingSenderId with yours 12 | 'messagingSenderId': '729476541475' 13 | }); 14 | 15 | // Retrieve an instance of Firebase Messaging so that it can handle background 16 | // messages. 17 | const messaging = firebase.messaging(); 18 | // [END initialize_firebase_in_sw] 19 | 20 | // If you would like to customize notifications that are received in the 21 | // background (Web app is closed or not in browser focus) then you should 22 | // implement this optional method. 23 | // [START background_handler] 24 | messaging.setBackgroundMessageHandler(function(payload) { 25 | console.log('[firebase-messaging-sw.js] Received background message ', payload); 26 | // Customize notification here 27 | payload = payload.data; 28 | const notificationTitle = payload.title; 29 | const notificationOptions = { 30 | body: payload.body, 31 | icon: payload.icon_url, 32 | }; 33 | 34 | self.addEventListener('notificationclick', function (event) { 35 | event.notification.close(); 36 | clients.openWindow(payload.url); 37 | }); 38 | 39 | return self.registration.showNotification(notificationTitle, 40 | notificationOptions); 41 | }); 42 | // [END background_handler] -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib import messages 3 | from django.contrib.auth import login, authenticate, logout 4 | from django.contrib.auth.forms import AuthenticationForm 5 | 6 | from .forms import NewUserForm 7 | 8 | 9 | def register_page(request): 10 | if request.method == "POST": 11 | form = NewUserForm(request.POST) 12 | if form.is_valid(): 13 | user = form.save() 14 | login(request, user) 15 | messages.success(request, "Registration successful.") 16 | return redirect("login") 17 | messages.error( 18 | request, "Unsuccessful registration. Invalid information.") 19 | form = NewUserForm() 20 | return render(request=request, template_name="accounts/register.html", context={"register_form": form}) 21 | 22 | 23 | def login_page(request): 24 | if request.method == "POST": 25 | form = AuthenticationForm(request, data=request.POST) 26 | if form.is_valid(): 27 | username = form.cleaned_data.get('username') 28 | password = form.cleaned_data.get('password') 29 | user = authenticate(username=username, password=password) 30 | if user is not None: 31 | login(request, user) 32 | messages.info(request, f"You are now logged in as {username}.") 33 | return redirect("index") 34 | else: 35 | messages.error(request, "Invalid username or password.") 36 | else: 37 | messages.error(request, "Invalid username or password.") 38 | form = AuthenticationForm() 39 | return render(request=request, template_name="accounts/login.html", context={"login_form": form}) 40 | 41 | def logout_request(request): 42 | logout(request) 43 | messages.info(request, "You have successfully logged out.") 44 | return redirect("index") 45 | -------------------------------------------------------------------------------- /djangofcm/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangofcm project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.9. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from firebase_admin import initialize_app 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-4g-ep^qaqrr+^*=qztt68e&qy-)kcs6y@)l1xzdmzrvtcn1w7c' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'accounts.apps.AccountsConfig', 42 | 'notifications.apps.NotificationsConfig', 43 | 'crispy_forms', 44 | 'rest_framework', 45 | 'fcm_django', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'djangofcm.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [BASE_DIR / 'templates', ], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | CRISPY_TEMPLATE_PACK = 'bootstrap4' 77 | MESSAGE_TAGS = { 78 | 10: 'alert-secondary', 79 | 20: 'alert-info', 80 | 25: 'alert-success', 81 | 30: 'alert-warning', 82 | 40: 'alert-danger', 83 | } 84 | 85 | WSGI_APPLICATION = 'djangofcm.wsgi.application' 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 90 | 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.sqlite3', 94 | 'NAME': BASE_DIR / 'db.sqlite3', 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 120 | 121 | LANGUAGE_CODE = 'en-us' 122 | 123 | TIME_ZONE = 'UTC' 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 134 | 135 | STATIC_URL = '/static/' 136 | STATICFILES_DIRS = [ 137 | BASE_DIR / 'static', 138 | ] 139 | 140 | # Default primary key field type 141 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 142 | 143 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 144 | 145 | # Optional ONLY IF you have initialized a firebase app already: 146 | # Visit https://firebase.google.com/docs/admin/setup/#python 147 | # for more options for the following: 148 | # Store an environment variable called GOOGLE_APPLICATION_CREDENTIALS 149 | # which is a path that point to a json file with your credentials. 150 | # Additional arguments are available: credentials, options, name 151 | FIREBASE_APP = initialize_app() 152 | # To learn more, visit the docs here: 153 | # https://cloud.google.com/docs/authentication/getting-started> 154 | 155 | FCM_DJANGO_SETTINGS = { 156 | # default: _('FCM Django') 157 | # "APP_VERBOSE_NAME": "[string for AppConfig's verbose_name]", 158 | # true if you want to have only one active device per registered user at a time 159 | # default: False 160 | "ONE_DEVICE_PER_USER": False, 161 | # devices to which notifications cannot be sent, 162 | # are deleted upon receiving error response from FCM 163 | # default: False 164 | "DELETE_INACTIVE_DEVICES": False, 165 | } 166 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 39 | 40 | {% for message in messages %} 41 |
42 | 48 |
49 | {% endfor %} 50 | 51 |
52 | {% block body %}{% endblock %} 53 |
54 | 55 | 56 | 104 | 105 | 106 | 107 | 108 | 109 | {% block js %}{% endblock %} 110 | 111 | 112 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | const firebaseConfig = { 2 | // Replace this with your config; otherwise it will not work! 3 | apiKey: "AIzaSyDGU5p3PoV1e7CyATtK6llT6gSYCzcvcs8", 4 | authDomain: "djangofcm-aa5e1.firebaseapp.com", 5 | projectId: "djangofcm-aa5e1", 6 | storageBucket: "djangofcm-aa5e1.appspot.com", 7 | messagingSenderId: "729476541475", 8 | appId: "1:729476541475:web:c8c134008274621fa45596" 9 | }; 10 | 11 | firebase.initializeApp(firebaseConfig); 12 | console.log("Firebase initialized..."); 13 | // Firebase Messaging Service 14 | const messaging = firebase.messaging(); 15 | 16 | function sendTokenToServer(currentToken) { 17 | if (!isTokenSentToServer()) { 18 | // The API Endpoint will be explained at step 8 19 | $.ajax({ 20 | url: "/api/devices/", 21 | method: "POST", 22 | async: false, 23 | data: { 24 | 'registration_id': currentToken, 25 | 'type': 'web' 26 | }, 27 | success: function (data) { 28 | console.log(data); 29 | setTokenSentToServer(true); 30 | }, 31 | error: function (err) { 32 | console.log(err); 33 | setTokenSentToServer(false); 34 | } 35 | }); 36 | 37 | } else { 38 | console.log('Token already sent to server so won\'t send it again ' + 39 | 'unless it changes'); 40 | } 41 | } 42 | 43 | function isTokenSentToServer() { 44 | return window.localStorage.getItem("sentToServer") === "1"; 45 | } 46 | 47 | function setTokenSentToServer(sent) { 48 | if (sent) { 49 | window.localStorage.setItem("sentToServer", "1"); 50 | } else { 51 | window.localStorage.setItem("sentToServer", "0"); 52 | } 53 | } 54 | 55 | 56 | function requestPermission() { 57 | messaging.requestPermission().then(function () { 58 | console.log("Has permission!"); 59 | resetUI(); 60 | }).catch(function (err) { 61 | console.log('Unable to get permission to notify.', err); 62 | }); 63 | } 64 | 65 | function resetUI() { 66 | console.log("In reset ui"); 67 | messaging.getToken().then(function (currentToken) { 68 | console.log(currentToken); 69 | if (currentToken) { 70 | sendTokenToServer(currentToken); 71 | } else { 72 | setTokenSentToServer(false); 73 | } 74 | }).catch(function (err) { 75 | console.log(err); 76 | setTokenSentToServer(false); 77 | }); 78 | } 79 | 80 | 81 | $('document').ready(function () { 82 | // Document is ready. 83 | console.log("loaded index.js"); 84 | // Setup AJAX 85 | $.ajaxSetup({ 86 | beforeSend: function (xhr, settings) { 87 | function getCookie(name) { 88 | var cookieValue = null; 89 | if (document.cookie && document.cookie != '') { 90 | var cookies = document.cookie.split(';'); 91 | for (var i = 0; i < cookies.length; i++) { 92 | var cookie = jQuery.trim(cookies[i]); 93 | // Does this cookie string begin with the name we want? 94 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 95 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 96 | break; 97 | } 98 | } 99 | } 100 | return cookieValue; 101 | } 102 | 103 | if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) { 104 | // Only send the token to relative URLs i.e. locally. 105 | xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); 106 | } 107 | } 108 | }); 109 | 110 | messaging.onTokenRefresh(function () { 111 | messaging.getToken().then(function (refreshedToken) { 112 | console.log("Token refreshed."); 113 | // Indicate that the new Instance ID token has not yet been sent to the 114 | // app server. 115 | setTokenSentToServer(false); 116 | // Send Instance ID token to app server. 117 | sendTokenToServer(refreshedToken); 118 | resetUI(); 119 | }).catch(function (err) { 120 | console.log("Unable to retrieve refreshed token ", err); 121 | }); 122 | }); 123 | 124 | messaging.onMessage(function (payload) { 125 | payload = payload.data; 126 | // Create notification manually when user is focused on the tab 127 | const notificationTitle = payload.title; 128 | const notificationOptions = { 129 | body: payload.body, 130 | icon: payload.icon_url, 131 | }; 132 | 133 | if (!("Notification" in window)) { 134 | console.log("This browser does not support system notifications"); 135 | } 136 | // Let's check whether notification permissions have already been granted 137 | else if (Notification.permission === "granted") { 138 | // If it's okay let's create a notification 139 | var notification = new Notification(notificationTitle, notificationOptions); 140 | notification.onclick = function (event) { 141 | event.preventDefault(); // prevent the browser from focusing the Notification's tab 142 | window.open(payload.url, '_blank'); 143 | notification.close(); 144 | } 145 | } 146 | }); 147 | 148 | requestPermission(); 149 | }); 150 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | seyyedaliayati@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adding Firebase Cloud Messaging Service into a Django Project 2 | The aim of this repository is to provide a step-by-step guide and a basic project sample to implement FCM (Firebase Cloud Messaging) feature into a django-based project. 3 | 4 | # Step 1: Create a Firebase Project 5 | Goto [firebase console](https://console.firebase.google.com/) and click on `Add project` to create a new firebase project. Since the goal of using firebase is its cloud messaging feature, you can disable Google Analytics. 6 | 7 | # Step 2: Download a JSON File Including Project's Credentials 8 | Next to the `Project Overview` on the left menu there is a gear icon which redirects you to the project settings page. 9 | 10 | ![Click on the gear icon and select `Project settings`](images/project_settings.png) 11 | 12 | Then, click on `Service accounts` and then click on `Generate new private key`. Download and store the JSON file on your device. 13 | 14 | **Security Note: Do NOT keep this file inside the project root and never publish it on Github, Gitlab,...** 15 | 16 | # Step 3: Install the `fcm-django` App 17 | You can use any other package, or you can develope a custom app yourself, but, I prefer `fcm-django` because it is simple and single responsible! 18 | ```bash 19 | pip install fcm-django 20 | ``` 21 | For communicating through API it is necessary to install Django Rest Framework: 22 | ```bash 23 | pip install djangorestframework 24 | ``` 25 | Here are the package's repository on Github and its documentation: 26 | - [Github Repo](https://github.com/xtrinch/fcm-django) 27 | - [Documentation](https://fcm-django.readthedocs.io/en/latest/) 28 | 29 | # Step 4: Install the `firebase-admin` Package 30 | The `firebase-admin` is the official library from firebase that communicates with its services. 31 | ```bash 32 | pip install firebase-admin 33 | ``` 34 | 35 | # Step 5: Modifying Django Settings 36 | 37 | Import `initialize_app` from `firebase_admin` in your setting file: 38 | ```python 39 | from firebase_admin import initialize_app 40 | ``` 41 | 42 | Add `fcm_django` and `rest_framework` to the `INSTALLED_APPS`: 43 | ```python 44 | INSTALLED_APPS = [ 45 | 'django.contrib.admin', 46 | 'django.contrib.auth', 47 | 'django.contrib.contenttypes', 48 | 'django.contrib.sessions', 49 | 'django.contrib.messages', 50 | 'django.contrib.staticfiles', 51 | # You Apps Here... 52 | 'rest_framework', # For Rest API 53 | 'fcm_django', # New 54 | ] 55 | ``` 56 | Add required configs for `fcm-django` app: 57 | ```python 58 | # Optional ONLY IF you have initialized a firebase app already: 59 | # Visit https://firebase.google.com/docs/admin/setup/#python 60 | # for more options for the following: 61 | # Store an environment variable called GOOGLE_APPLICATION_CREDENTIALS 62 | # which is a path that point to a json file with your credentials. 63 | # Additional arguments are available: credentials, options, name 64 | FIREBASE_APP = initialize_app() 65 | # To learn more, visit the docs here: 66 | # https://cloud.google.com/docs/authentication/getting-started> 67 | 68 | FCM_DJANGO_SETTINGS = { 69 | # default: _('FCM Django') 70 | "APP_VERBOSE_NAME": "[string for AppConfig's verbose_name]", 71 | # true if you want to have only one active device per registered user at a time 72 | # default: False 73 | "ONE_DEVICE_PER_USER": True/False, 74 | # devices to which notifications cannot be sent, 75 | # are deleted upon receiving error response from FCM 76 | # default: False 77 | "DELETE_INACTIVE_DEVICES": True/False, 78 | } 79 | ``` 80 | 81 | Run the migrate command before running the server: 82 | ```bash 83 | python manage.py migrate 84 | ``` 85 | 86 | **Note:** Before running the project make sure to set an environment variable named `GOOGLE_APPLICATION_CREDENTIALS` to the path of your downloaded JSON file from firebase in Step 2. 87 | 88 | # Step 6: The Default Service Worker 89 | For handling background notifications, when user is not focused on tab, it is required to have a service worker. Just create a file named `firebase-messaging-sw.js` and put it in root, so it should be accessible by `/firebase-messaging-sw.js/` URL. In django you can put this file in `templates` and add following line of code in your root `urls.py`: 90 | ```python 91 | from django.urls import include, path 92 | from django.views.generic import TemplateView 93 | 94 | urlpatterns = [ 95 | ..., 96 | path("firebase-messaging-sw.js", 97 | TemplateView.as_view( 98 | template_name="firebase-messaging-sw.js", 99 | content_type="application/javascript", 100 | ), 101 | name="firebase-messaging-sw.js" 102 | ), 103 | ... 104 | ] 105 | ``` 106 | 107 | So the content of `templates/firebase-messaging-sw.js` is: 108 | ```javascript 109 | // [START initialize_firebase_in_sw] 110 | // Give the service worker access to Firebase Messaging. 111 | // Note that you can only use Firebase Messaging here, other Firebase libraries 112 | // are not available in the service worker. 113 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js'); 114 | importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js'); 115 | 116 | // Initialize the Firebase app in the service worker by passing in the 117 | // messagingSenderId. 118 | firebase.initializeApp({ 119 | // Replace messagingSenderId with yours 120 | 'messagingSenderId': '504975596104' 121 | }); 122 | 123 | // Retrieve an instance of Firebase Messaging so that it can handle background 124 | // messages. 125 | const messaging = firebase.messaging(); 126 | // [END initialize_firebase_in_sw] 127 | 128 | // If you would like to customize notifications that are received in the 129 | // background (Web app is closed or not in browser focus) then you should 130 | // implement this optional method. 131 | // [START background_handler] 132 | messaging.setBackgroundMessageHandler(function(payload) { 133 | console.log('[firebase-messaging-sw.js] Received background message ', payload); 134 | // Customize notification here 135 | payload = payload.data; 136 | const notificationTitle = payload.title; 137 | const notificationOptions = { 138 | body: payload.body, 139 | icon: payload.icon_url, 140 | }; 141 | 142 | self.addEventListener('notificationclick', function (event) { 143 | event.notification.close(); 144 | clients.openWindow(payload.url); 145 | }); 146 | 147 | return self.registration.showNotification(notificationTitle, 148 | notificationOptions); 149 | }); 150 | // [END background_handler] 151 | ``` 152 | # Step 7: Register User Devices 153 | Registering the user device can be done by some JavaScript code which communicates with Firebase API. You can write a better, cleaner and more secure version of this code yourself. This is just for educational purposes. 154 | 155 | ```html 156 | 157 | 158 | 276 | ``` 277 | 278 | # Step 8: Prepare Create Device Endpoint 279 | Simply you can add following code snippet to your root `urls.py`: 280 | ```python 281 | from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet 282 | from rest_framework.routers import DefaultRouter 283 | 284 | router = DefaultRouter() 285 | router.register('devices', FCMDeviceAuthorizedViewSet) 286 | 287 | urlpatterns = [ 288 | # URLs will show up at /devices 289 | # DRF browsable API which lists all available endpoints 290 | path('api/', include(router.urls)), 291 | # ... 292 | ] 293 | ``` 294 | 295 | # Step 9: Sending Messages 296 | Sending message to users is quite straightforward. First, create a `Message` object with your customized data, then send it to target devices: 297 | ```python 298 | from firebase_admin.messaging import Message 299 | from fcm_django.models import FCMDevice 300 | 301 | message_obj = Message( 302 | data={ 303 | "Nick" : "Mario", 304 | "body" : "great match!", 305 | "Room" : "PortugalVSDenmark" 306 | }, 307 | ) 308 | 309 | # You can still use .filter() or any methods that return QuerySet (from the chain) 310 | device = FCMDevice.objects.all().first() 311 | # send_message parameters include: message, dry_run, app 312 | device.send_message(message_obj) 313 | # Boom! 314 | ``` 315 | 316 | # Step 10: Important Notes 317 | - **In the sample project login and register are not the responsibility of the notifications app.** I just put it there for the sake of simplicity! So, please be careful in your real-life projects! 318 | 319 | - Users should login at least one time in order to receive notifications. 320 | 321 | - These code snippets and the sample project is just a tutorial to implement FCM in django project. You should consider your use-cases and read the documentations carefully. 322 | 323 | 324 | - Finally, if this tutorial was helpful to you, give me a star ⭐ and share it with your friends. --------------------------------------------------------------------------------