ارسال پوش نوتیفیکیشن از اپلیکیشن های جنگو (Django) – از صفر تا صد


وب به طور مداوم در حال تکامل است و اینک به کارکردهایی دست یافته است که قبلاً تنها روی دستگاههای تلفن همراه وجود داشتند. با معرفی «سرویس ورکرها» (Service Workers) وب تواناییهای جدیدی در زمینه همگامسازی در پسزمینه، کَش کردن آفلاین و ارسال پوش نوتیفیکیشن یافته است.
پوش نوتیفیکیشن به کاربران امکان میدهد که قابلیت دریافت بهروزرسانیها برای اپلیکیشنهای وب و موبایل را کسب کنند. همچنین باعث میشود که کاربران بتوانند با استفاده از محتوای سفارشیسازی شده و مرتبط، تعاملهای مداومی با اپلیکیشنها داشته باشند.
در این راهنما یک اپلیکیشن جَنگو (Django) روی اوبونتو 18.04 میسازیم که هر زمان نیاز باشد کاربر از اپلیکیشن بازدید کند، یک پوش نوتیفیکیشن به وی ارسال میکند. برای این چنین اعلانهایی از بسته Django-Webpush استفاده میکنیم و یک سرویس ورکر برای نمایش اعلانها به کاربر راهاندازی و ثبت میکنیم. اپلیکیشن در حال کار با این اعلانها، ظاهری مانند تصویر زیر خواهد داشت:
پیشنیازها
پیش از آغاز این راهنما باید موارد زیر را آماده کرده باشید:
- یک سرور اوبونتو 18.04 و یک کاربر غیر root با فایروال فعال. برای آمادهسازی این موارد میتوانید از راهنمای «راهاندازی اولیه سرورهای اوبونتو 1۸.۰4 — از صفر تا صد» استفاده کنید.
- pip و venv را روی سرور نصب کنید.
- یک پروژه جنگو به نام djangopush روی دایرکتوری home خود بسازید. اطمینان حاصل کنید که آدرس IP سرور خود را به دایرکتیو ALLOWED_HOSTS در فایل settings.py اضافه کردهاید.
مرحله یکم – نصب Django-Webpush و دریافت کلیدهای Vapid
Django-Webpush بستهای است که امکان ادغام و ارسال پوش نوتیفیکیشنها را به توسعهدهندگان اپلیکیشنهای جنگو میدهد. ما از این بسته برای تحریک و ارسال پوش نوتیفیکیشنها از اپلیکیشن خود استفاده میکنیم. در این مرحله ابتدا Django-Webpush را نصب میکنیم و کلیدهای VAPID که اختصاری برای عبارت «شناسایی داوطلبانه سرور اپلیکیشن» (Voluntary Application Server Identification) است را دریافت میکنیم. این کلیدها برای شناسایی سرور و تضمین یکتا بودن هر درخواست ضروری هستند.
ابتدا مطمئن شوید که در مسیر دایرکتوری پروژه djangopush/~ که در بخش پیشنیازها ایجاد کردید، قرار دارید:
cd ~/djangopush
محیط مجازی خود را فعال کنید:
source my_env/bin/activate
نسخه pip خود را ارتقا دهید تا مطمئن شوید که بهروز است:
pip install --upgrade pip
بسته Django-Webpush را نصب کنید:
pip install django-webpush
پس از نصب بسته آن را به فهرست اپلیکیشنها در فایل settings.py اضافه کنید. ابتدا فایل settings.py را باز کنید:
nano ~/djangopush/djangopush/settings.py
عبارت webpush را به فهرست اپلیکیشنهای نصب شده اضافه کنید:
... INSTALLED_APPS = [ ..., 'webpush', ] ...
فایل را ذخیره کرده و از ویرایشگر خارج شوید.
در این مرحله باید migrations را روی اپلیکیشن اجرا کنید تا تغییراتی که صورت دادیم روی شِمای پایگاه داده اعمال شوند:
python manage.py migrate
خروجی دستور فوق به صورت زیر خواهد بود که نشان میدهد migration موفقیتآمیز بوده است:
خروجی
Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, webpush Running migrations: Applying webpush.0001_initial... OK
مرحله بعدی در مسیر راهاندازی پوش نوتیفیکیشن وب، دریافت کلیدهای VAPID است. این کلیدها به منظور شناسایی سرور اپلیکیشن است و میتوانند برای کاهش پنهانکاری URL-های پوش نوتیفیکیشن مورد استفاده قرار گیرند، زیرا تعداد ثبتنامها در یک سرور خاص را محدود میسازند.
برای دریافت کلیدهای VAPID به مسیر وب اپلیکیشن wep-push-codelab بروید. در این مسیر کلیدهای تولید شده خودکار به شما داده میشوند. کلیدهای عمومی و خصوصی را کپی کنید.
سپس یک مدخل جدید در settings.py برای درج اطلاعات VAPID ایجاد کنید. ابتدا فایل را باز کنید:
nano ~/djangopush/djangopush/settings.py
سپس یک دایرکتیو جدید به نام WEBPUSH_SETTINGS زیر بخش AUTH_PASSWORD_VALIDATORS ایجاد کنید که شامل کلیدهای عمومی و خصوصی VAPID و ایمیل شما خواهد بود:
...
AUTH_PASSWORD_VALIDATORS = [
...
]
WEBPUSH_SETTINGS = {
"VAPID_PUBLIC_KEY": "your_vapid_public_key",
"VAPID_PRIVATE_KEY": "your_vapid_private_key",
"VAPID_ADMIN_EMAIL": "admin@example.com"
}
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
...
فراموش نکنید که مقادیر هایلایت شده your_vapid_public_key, your_vapid_private_key و admin@example.com را در کد فوق با اطلاعات خاص خودتان جایگزین کنید. در صورتی که سرور پوش با هرگونه مشکلی مواجه شود از طریق آدرس ایمیل به شما اطلاعرسانی میکند.
در مرحله بعد view-هایی ایجاد میکنیم که صفحه اصلی اپلیکیشن را به کاربر نمایش داده و پوش نوتیفیکیشنها را به کاربرهای ثبت نام کننده ارسال میکنند.
مرحله دوم – راهاندازی view-ها
در این مرحله یک view ابتدایی به نام home به کمک شیء پاسخ HttpResponse برای صفحه اصلی اپلیکیشن و همچنین یک view به نام send_push میسازیم. این view-ها کارکردهایی هستند که اشیای پاسخ را در برابر درخواستهای وب باز میگردانند. ویوی send_push از کتابخانه Django-Webpush برای ارسال پوش نوتیفیکیشنی که شامل دادهها وارد شده از سوی یک کاربر در صفحه اصلی اپلیکیشن است استفاده میکند.
به پوشه djangopush/djangopush/~ بروید:
cd ~/djangopush/djangopush
با اجرای دستور ls فایلهای اصلی پروژه را در این پوشه مشاهده میکنید:
خروجی
/__init__.py /settings.py /urls.py /wsgi.py
فایلها در این پوشه به طور خودکار از سوی ابزار django-admin تولید شدهاند که برای ایجاد پروژه در مرحله پیشنیاز استفاده کردید. فایل settings.py شامل پیکربندیهای در سطح پروژه مانند اپلیکیشنهای نصب شده و پوشه root استاتیک است. فایل urls.py شامل پیکربندی URL برای پروژه است. این همان جایی است که مسیرهای مطابق با ویوهای ایجاد شده را تنظیم میکنیم.
یک فایل جدید در دایرکتوری djangopush/djangopus/~ به نام views.py بسازید که شامل ویوهای پروژه باشد:
nano ~/djangopush/djangopush/views.py
نخستین ویو که میسازیم home نام دارد و به نمایش صفحه اصلی اپلیکیشن میپردازد که در آن کاربران میتوانند پوش نوتیفیکیشن ارسال کنند. کد زیر را به فایل ایجاد شده اضافه کنید:
from django.http.response import HttpResponse from django.views.decorators.http import require_GET @require_GET def home(request): return HttpResponse('<h1>Home Page<h1>')
ویوی home به وسیله دکوراتور require_GET ایجاد شده که ویو را تنها محدود به دریافت (GET) درخواستها میکند. یک ویو به طور معمول برای هر درخواستی که دریافت میکند، یک پاسخ باز میگرداند. این ویو یک تگ HTML ساده به عنوان پاسخ باز میگرداند.
ویوی بعدی که ایجاد میکنیم send_push نام دارد که به مدیریت پوش نوتیفیکیشنهای ارسالی با استفاده از بسته django-webpush میپردازد. این ویو نیز به درخواستهای POST محدود شده است و بدین ترتیب از حفاظت در برابر Cross Site Request Forgery یعنی (CSRF) معاف شده است. بدین ترتیب امکان تست ویو با استفاده از Postman یا هر سرویس RESTful دیگر را مییابیم. با این وجود، در محیط production باید این دکوراتور را حذف کنید تا از آسیبپذیری اپلیکیشن در برابر CSRF جلوگیری کنید.
برای ایجاد ویوی ابتدا ایمپورتهای زیر را اضافه کنید تا امکان پاسخهای JSON و دسترسی به تابع send_user_notification در کتابخانه webpush فراهم شود:
from django.http.response import JsonResponse, HttpResponse from django.views.decorators.http import require_GET, require_POST from django.shortcuts import get_object_or_404 from django.contrib.auth.models import User from django.views.decorators.csrf import csrf_exempt from webpush import send_user_notification import json
سپس دکوراتور require_POST را اضافه کنید که از متن درخواست ارسالی از سوی کاربر برای ایجاد و ارسال پوش نوتیفیکیشن استفاده میکند:
@require_GET def home(request): ... @require_POST @csrf_exempt def send_push(request): try: body = request.body data = json.loads(body) if 'head' not in data or 'body' not in data or 'id' not in data: return JsonResponse(status=400, data={"message": "Invalid data format"}) user_id = data['id'] user = get_object_or_404(User, pk=user_id) payload = {'head': data['head'], 'body': data['body']} send_user_notification(user=user, payload=payload, ttl=1000) return JsonResponse(status=200, data={"message": "Web push successful"}) except TypeError: return JsonResponse(status=500, data={"message": "An error occurred"})
ما از دو دکوراتور برای ویوی send_push استفاده میکنیم که یکی دکوراتور require_POST برای محدودسازی ویو به درخواستهای صرفاً POST است و دیگری دکوراتور csrf_exempt است که برای معاف کردن ویو از حفاظت CSRF استفاده میشود.
این ویو دادههای POST را پذیرفته و کارهای زیر را انجام میدهد: body درخواست را دریافت کرده و با استفاده از بسته json سند JSON را با استفاده از json.loads به صورت یک شیء پایتون سریالزدایی (تبدیل رشته بایتی به رشتههای بیتی) میکند. json.loads یک سند JSON ساختیافته را گرفته و آن را به یک شیء پایتون تبدیل میکند.
این ویو انتظار دارد که شیء body درخواست حاوی سه مشخصه باشد:
- head: عنوان پوش نوتیفیکیشن
- body: متن نوتیفیکیشن
- id: شناسه کاربر درخواستی
اگر هر یک از مشخصههای مورد تقاضا موجود نباشند، ویو یک JSONResponse با وضعیت 404 یعنی «یافت نشد» (Not Found) باز میگرداند. اگر کاربر با کلید اولیه مفروضی وجود داشته باشد، این ویو یک user با کتابخانه django.shortcuts باز میگرداند. اگر کاربر وجود نداشته باشد، این تابع خطای 404 باز میگرداند.
این ویو همچنین از تابع send_user_notification از کتابخانه webpush نیز بهره میگیرد. این تابع سه پارامتر زیر را میگیرد:
- User: گیرنده پوش نوتیفیکیشن است.
- payload: اطلاعات نوتیفیکیشن است که شامل head و body آن میشود.
- ttl: بیشینه زمانی است که نوتیفیکیشن باید در صورت آفلاین بودن کاربر ذخیره شود (بر حسب ثانیه).
اگر هیچ خطایی رخ ندهد، ویو یک JSONResponse با وضعیت 200 «موفقیت» و یک شیء داده باز میگرداند. اگر یک KeyError رخ بدهد، ویو یک وضعیت 500 «خطای داخلی سرور» (Internal Server Error) باز میگرداند. KeyError زمانی رخ میدهد که کلید مورد تقاضای یک شیء وجود نداشته باشد.
در مرحله بعدی اقدام به ایجاد مسیرهای URL متناظر برای مطابقت با ویوهای ایجاد شده میکنیم.
مرحله سوم – نگاشت URL-ها به View-ها
جنگو امکان ایجاد URL-هایی را فراهم ساخته است که با استفاده از یک ماژول پایتون به نام URLconf به ویوها اتصال یابند. این ماژول اسامی مسیرها را به کارکردهای پایتون (که همان ویوها هستند) نگاشت میکند. به طور معمول یک فایل پیکربندی URL هنگام ساخت پروژه به طور خودکار ایجاد میشود. در این مرحله تلاش میکنیم فایل را بهروزرسانی بهروزرسانی کنیم تا شامل مسیرهای جدید برای ویوهایی ایجاد شده در مرحله قبلی به همراه URL-هایی برای اپلیکیشن django-webpush باشد که شامل نقاط انتهایی برای ثبت نام کاربران جهت دریافت پوش نوتیفیکیشن است.
فایل urls.py را باز کنید:
nano ~/djangopush/djangopush/urls.py
این فایل به صورت زیر است:
"""untitled URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/2.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), ]
مرحله بعدی نگاشت ویوها به URL-های ایجاد شده است. ابتدا ایمپورت include را اضافه کنید تا مطمئن شوید که همه مسیرها برای کتابخانه Django-Webpush به پروژه شما اضافه شده است:
"""webpushdjango URL Configuration
...
"""
from django.contrib import admin
from django.urls import path, include
سپس ویوهایی را که در مرحله قبلی ایجاد کردهاید وارد کرده و فهرست urlpatterns را بهروزرسانی کنید تا به ویوهای شما نگاشت شود:
"""webpushdjango URL Configuration ... """ from django.contrib import admin from django.urls import path, include from .views import home, send_push urlpatterns = [ path('admin/', admin.site.urls), path('', home), path('send_push', send_push), path('webpush/', include('webpush.urls')), ]
در اینجا فهرست urlpatterns اقدام به ثبت URL-ها برای بسته django-webpush کرده و ویوهای شما را به URL-های send_push/ و home/ نگاشت میکند.
در ادامه ویوی home/ را تست میکنیم تا مطمئن شویم آن چنان که انتظار داشتیم کار میکند. اطمینان حاصل کنید که در مسیر دایرکتوری root قرار دارید:
cd ~/djangopush
سرور خود را با استفاده از دستور زیر آغاز کنید:
python manage.py runserver your_server_ip:8000
به مسیر http://your_server_ip:8000 بروید. در این زمان صفحه اصلی زیر را میبینید:
در این مرحله میتوانید سرور را با دستور CTRL+C متوقف کنید و در ادامه به ایجاد قالبها و رندر کردن آنها با استفاده از تابع render برای ویوها بپردازید.
مرحله چهارم – ایجاد قالبها
موتور قالب در جنگو امکان تعریف لایههای در معرض دید کاربر اپلیکیشن را با ایجاد قالبهایی خاص که مشابه فایلهای HTML هستند، فراهم میکند. در این مرحله، قالبی را برای ویوی home خود ایجاد کرده و رندر میکنیم.
یک پوشه به نام templates در دایرکتوری ریشه پروژه ایجاد کنید:
mkdir ~/djangopush/templates
اگر در این زمان دستور ls را در پوشه ریشه پروژه خود اجرا کنید، خروجی آن چیزی شبیه زیر خواهد بود:
خروجی
/djangopush /templates db.sqlite3 manage.py /my_env
فایلی به نام home.html در پوشه templates بسازید:
nano ~/djangopush/templates/home.html
کد زیر را به فایل اضافه کنید تا یک فرم ایجاد شود که کاربران میتوانند اطلاعات خود را برای ایجاد پوش نوتیفیکیشن در آن وارد کنند:
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta name="vapid-key" content="{{ vapid_key }}"> {% if user.id %} <meta name="user_id" content="{{ user.id }}"> {% endif %} <title>Web Push</title> <link href="https://fonts.googleapis.com/css?family=PT+Sans:400,700" rel="stylesheet"> </head> <body> <div> <form id="send-push__form"> <h3 class="header">Send a push notification</h3> <p class="error"></p> <input type="text" name="head" placeholder="Header: Your favorite airline ?"> <textarea name="body" id="" cols="30" rows="10" placeholder="Body: Your flight has been cancelled ???"></textarea> <button>Send Me</button> </form> </div> </body> </html>
بخش body فایل شامل فرمی با دو فیلد است. یک فیلد به عنوان input که حاوی عنوان نوتیفیکیشن است و دیگری به صورت textarea که متن نوتیفیکیشن را شامل میشود.
در بخش head فایل دو تگ meta وجود دارد که کلید عمومی VAPID و id کاربر را نگهداری میکند. این دو متغیر برای ثبت کاربر و ارسال پوش نوتیفیکیشن به آنها ضروری هستند. id کاربر به این دلیل مورد نیاز است که درخواستهای AJAX به سرور ارسال خواهند شد و از id برای شناسایی کاربر استفاده میشود. اگر کاربر کنونی یک کاربر ثبت نام کرده باشد، در این صورت قالب، یک تگ با id وی به عنوان محتوا ایجاد میکند.
مرحله بعدی این است که به جنگو اعلام کنیم کجا میتواند قالبها را پیدا کند. بدین منظور فایل settings.py را ویرایش کرده و فهرست TEMPLATES را بهروزرسانی میکنیم.
فایل settings.py را باز کنید:
nano ~/djangopush/djangopush/settings.py
کد زیر را به فهرست DIRS اضافه کنید تا مسیر دایرکتوری قالبها را بشناسد:
...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
...
],
},
},
]
...
سپس در فایل views.py ویوی home را بهروزرسانی کنید تا قالب home.html را رندر کند. فایل را باز کنید:
nano ~/djangpush/djangopush/views.py
ابتدا برخی ایمپورتهای دیگر اضافه میکنیم که شامل پیکربندی settings است که همه تنظیمات پروژه را از فایل settings.py میخواند و تابع render نیز از django.shortcuts وارد میشود:
... from django.shortcuts import render, get_object_or_404 ... import json from django.conf import settings ...
سپس کد اولیهای که به ویوی home اضافه شده بود را حذف کرده و کد زیر را که شیوه رندر قالب ایجاد شده را معین میکند، اضافه میکنیم:
... @require_GET def home(request): webpush_settings = getattr(settings, 'WEBPUSH_SETTINGS', {}) vapid_key = webpush_settings.get('VAPID_PUBLIC_KEY') user = request.user return render(request, 'home.html', {user: user, 'vapid_key': vapid_key})
کد فوق متغیرهای زیر را انتساب میکند:
- webpush_settings: این همان مقدار انتسابی خصوصیت WEBPUSH_SETTINGS از پیکربندی settings است.
- vapid_key: این متغیر مقدار VAPID_PUBLIC_KEY را از شیء webpush_settings برای ارسال به کلاینت دریافت میکند. کلید عمومی در برابر کلید خصوصی بررسی میشود تا اطمینان حاصل شود که کلاینت دارای کلید عمومی مجوز ارسال پیامهای پوش از سرور را دارد.
- user: این متغیر از درخواست ورودی به دست میآید. هر زمان که کاربر یک درخواست به سرور ایجاد میکند، جزییات برای آن کاربر در فیلد user ذخیره میشود.
تابع render یک فایل HTML و یک شیء context باز میگرداند که شامل کاربر جاری و کلید عمومی VAPID سرور است. این تابع سه پارامتر میگیرد: request، template که باید رندر شود و شیئی که شامل متغیرهای مورد استفاده از سوی قالب است.
زمانی که قالبمان ایجاد، و ویوی home بهروزرسانی شد، میتوانیم به مرحله بعدی که شامل پیکربندی جنگو برای عرضه فایلهای استاتیک است برویم.
مرحله پنجم - عرضه فایلهای استاتیک
وب اپلیکیشنها شامل CSS، JavaScript و دیگر فایلهای تصویر هستند که جنگو آنها را «فایلهای استاتیک» (CSS, JavaScript) مینامد. جنگو امکان گردآوری همه فایلهای استاتیک از اپلیکیشنهای مختلف در یک پروژه و یک مکان منفرد را فراهم ساخته است. سپس فایلها از این مکان منفرد به کاربر عرضه میشوند. این راهحل به صورت django.contrib.staticfiles نامیده میشود. در این مرحله تنظیمات خود را بهروزرسانی میکنیم تا به جنگو اعلام کنیم که فایلهای استاتیک را باید از کجا بخواند.
فایل settings.py را باز کنید:
nano ~/djangopush/djangopush/settings.py
در فایل settings.py ابتدا مطمئن شوید که STATIC_URL تعریف شده است:
... STATIC_URL = '/static/'
سپس یک فهرست از دایرکتوریها به نام STATICFILES_DIRS اضافه کنید که محل فایلهای استاتیک را به جنگو اعلام میکنند:
... STATIC_URL = '/static/' <span style="color: rgb(255, 0, 0);" data-mce-style="color: #ff0000;">STATICFILES_DIRS = [</span> <span style="color: rgb(255, 0, 0);" data-mce-style="color: #ff0000;">os.path.join(BASE_DIR,</span> "static"<span style="color: rgb(255, 0, 0);" data-mce-style="color: #ff0000;">), ]</span>
اینک میتوانید STATIC_URL را به فهرست مسیرهای تعریف شده در فایل urls.py اضافه کنید.
فایل را باز کنید:
nano ~/djangopush/djangopush/urls.py
کد زیر را اضافه کنید که پیکربندی url به نام static را ایمپورت کرده و فهرست urlpatterns را بهروزرسانی میکند. تابع کمکی (helper) در این جا از مشخصات STATIC_URL و STATIC_ROOT ارائه شده در فایل settings.py برای عرضه فایلهای استاتیک پروژه استفاده میکند:
... from django.conf import settings from django.conf.urls.static import static urlpatterns = [ ... ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
زمانی که تنظیمات فایلهای استاتیک ما پیکربندی شدند، میتوانیم شروع به تعیین استایل ظاهری صفحه اصلی پروژه خود بکنیم.
مرحله ششم – سبکبندی صفحه اصلی
پس از راهاندازی اپلیکیشن برای عرضه فایلهای استاتیک میتوانیم شروع به ایجاد استایلشیتهای بیرونی و پیوند دادن آنها به فایل home.html بکنیم. همه فایلهای استاتیک در دایرکتوری static در پوشه ریشه پروژه ذخیره میشوند.
یک پوشه static ایجاد کرده و یک پوشه css درون پوشه static اضافه کنید:
mkdir -p ~/djangopush/static/css
یک فایل css به نام styles.css درون پوشه css ایجاد کنید:
nano ~/djangopush/static/css/styles.css
استایلهای زیر را برای صفحه اصلی اضافه کنید:
body { height: 100%; background: rgba(0, 0, 0, 0.87); font-family: 'PT Sans', sans-serif; } div { height: 100%; display: flex; align-items: center; justify-content: center; } form { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 35%; margin: 10% auto; } form > h3 { font-size: 17px; font-weight: bold; margin: 15px 0; color: orangered; text-transform: uppercase; } form > .error { margin: 0; font-size: 15px; font-weight: normal; color: orange; opacity: 0.7; } form > input, form > textarea { border: 3px solid orangered; box-shadow: unset; padding: 13px 12px; margin: 12px auto; width: 80%; font-size: 13px; font-weight: 500; } form > input:focus, form > textarea:focus { border: 3px solid orangered; box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.2); outline: unset; } form > button { justify-self: center; padding: 12px 25px; border-radius: 0; text-transform: uppercase; font-weight: 600; background: orangered; color: white; border: none; font-size: 14px; letter-spacing: -0.1px; cursor: pointer; } form > button:disabled { background: dimgrey; cursor: not-allowed; }
زمانی که فایل استایلشیت ایجاد شد، میتوانید آن را با استفاده از تگهای قالب استاتیک (+) به فایل home.html اضافه کنید. فایل home.html را باز کنید:
nano ~/djangopush/templates/home.html
بخش head را بهروزرسانی کنید تا شامل پیوندی به استایلشیت بیرونی باشد:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
...
<link href="{% static '/css/styles.css' %}" rel="stylesheet">
</head>
<body>
...
</body>
</html>
مطمئن شوید که در دایرکتوری پروژه اصلی قرار دارید و سرور را مجدداً آغاز کنید تا نتیجه کار را ببینید:
cd ~/djangopush
python manage.py runserver your_server_ip:8000
زمانی که به آدرس http://your_server_ip:8000 مراجعه کنید، باید مانند تصویر زیر باشد:
در این مورد نیز میتوان سرور را با دستور CTRL+C متوقف کرد.
اینک که با موفقیت صفحه home.html را ایجاد و آن را استایل دهی کردهایم، میتوانیم از کاربران ثبت نام کنیم تا هر زمان که از صفحه اصلی بازدید میکنند، یک پوش نوتیفیکیشن برایشان ارسال شود.
مرحله هفتم – ثبت یک سرویس ورکر و ثبت نام کاربران برای ارسال پوش نوتیفیکیشن
پوش نوتیفیکیشنهای وب به کاربران اطلاع میدهند که بهروزرسانیهایی در اپلیکیشنهایی که ثبت نام کردهاند رخ داده است یا این که از آنها دعوت میکنند تا از اپلیکیشنی که قبلاً استفاده کردهاند، مجدداً بهره بگیرند. این پوش نوتیفیکیشنها به طور عمده بر دو فناوری تکیه دارند: API پوش و API نوتیفیکیشن. هر دو فناوری به وجود یک سرویس ورکر وابسته هستند.
یک پوش (push) زمانی فراخوانی میشود که سرور اطلاعاتی را به سرویس ورکر ارسال کند و سرویس ورکر از API نوتیفیکیشن برای نمایش آن اطلاعات استفاده نماید.
ما از کاربران خود برای پوش ثبت نام میکنیم و سپس اطلاعاتی را از این ثبت نام به سرور میفرستیم تا آنها را ثبت نام کنیم.
در دایرکتوری static یک پوشه به نام js بسازید:
mkdir ~/djangopush/static/js
یک فایل به نام registerSw.js ایجاد کنید:
nano ~/djangopush/static/js/registerSw.js
کد زیر را اضافه کنید. این کد پیش از اقدام به ثبت یک سرویس ورکر بررسی میکند که آیا مرورگر کاربر از سرویس ورکرها پشتیبانی میکند یا نه:
const registerSw = async () => { if ('serviceWorker' in navigator) { const reg = await navigator.serviceWorker.register('sw.js'); initialiseState(reg) } else { showNotAllowed("You can't send push notifications ☹️?") } };
ابتدا تابع registers بررسی میکند که آیا مرورگر کاربر از سرویس ورکرها پشتیبانی میکند یا نه. این تابع پس از ثبت، تابع دیگری به نام initializeState را به همراه دادههای ثبت شده فراخوانی میکند. اگر سرویس ورکرها در مرورگر پشتیبانی نمیشوند، در این صورت تابع showNotAllowed را فراخوانی میکند.
سپس کد زیر را در زیر تابع registers اضافه میکند تا قبل از اقدام به ثبت نام از وی بررسی کند که آیا کاربر اجازه دریافت پوش نوتیفیکیشن را دارد:
... const initialiseState = (reg) => { if (!reg.showNotification) { showNotAllowed('Showing notifications isn\'t supported ☹️?'); return } if (Notification.permission === 'denied') { showNotAllowed('You prevented us from showing notifications ☹️?'); return } if (!'PushManager' in window) { showNotAllowed("Push isn't allowed in your browser ?"); return } subscribe(reg); } const showNotAllowed = (message) => { const button = document.querySelector('form>button'); button.innerHTML = `${message}`; button.setAttribute('disabled', 'true'); };
تابع initializeState موارد زیر را بررسی میکند:
- با استفاده از reg.showNotification بررسی میکند که آیا کاربر نوتیفیکیشنها را فعال کرده است یا نه.
- آیا کاربر به اپلیکیشن مجوز نمایش نوتیفیکیشنها را داده است یا نه.
- آیا مرورگر کاربر از API PushManager پشتیبانی میکند یا نه. اگر هر یک از موارد فوق برقرار نباشد، در این صورت تابع showNotAllowed فراخوانی شده و فرایند ثبت نام لغو میشود.
تابع showNotAllowed پیامی را روی دکمه نمایش میدهد و در صورتی که کاربر امکان دریافت نوتیفیکیشن را نداشته باشد آن را غیر فعال میکند. این تابع همچنین در صورتی که کاربر اپلیکیشن را از نمایش نوتیفیکیشنها منع کرده باشد یا مرورگر از پوش نوتیفیکیشنها پشتیبانی نکند، پیامهای مناسبی را نمایش میدهد.
زمانی که مطمئن شویم کاربر امکان دریافت پوش نوتیفیکیشن را دارد، مرحله بعدی آن است که با استفاده از pushManager از وی ثبت نام کنیم. کد زیر را در زیر تابع showNotAllowed اضافه کنید:
... function urlB64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); const outputData = outputArray.map((output, index) => rawData.charCodeAt(index)); return outputData; } const subscribe = async (reg) => { const subscription = await reg.pushManager.getSubscription(); if (subscription) { sendSubData(subscription); return; } const vapidMeta = document.querySelector('meta[name="vapid-key"]'); const key = vapidMeta.content; const options = { userVisibleOnly: true, // if key exists, create applicationServerKey property ...(key && {applicationServerKey: urlB64ToUint8Array(key)}) }; const sub = await reg.pushManager.subscribe(options); sendSubData(sub) };
فراخوانی تابع pushManager.getSubscription دادههایی برای یک ثبت نام فعال باز میگرداند. زمانی که ثبت نام فعالی وجود داشته باشد، تابع sendSubData با اطلاعات ثبت نام که به صورت یک پارامتر ارسال شدهاند فراخوانی میشوند.
زمانی که ثبت نام فعالی موجود نباشد کلید خصوصی VAPID که به صورت URL-safe و Base64 رمزگذاری شده است با استفاده از تابع urlB64ToUint8Array به یک آرایه از نوع Uint8Array تبدیل میشود. سپس pushManager.subscribe با کلید عمومی VAPID و مقدار userVisible به عنوان یک گزینه اختیاری فراخوانی میشود. در مورد گزینههای تابع میتوانید در این لینک (+) بیشتر بخوانید.
پس از ثبت نام موفق یک کاربر، مرحله بعدی آن است که دادههای ثبت نام را به سرور ارسال کنیم. دادهها به نقطه انتهایی webpush/save_information ارائه شده از سوی بسته django-webpush ارسال میشود. کد زیر را در بخش زیر تابع subscribe اضافه کنید:
... const sendSubData = async (subscription) => { const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase(); const data = { status_type: 'subscribe', subscription: subscription.toJSON(), browser: browser, }; const res = await fetch('/webpush/save_information', { method: 'POST', body: JSON.stringify(data), headers: { 'content-type': 'application/json' }, credentials: "include" }); handleResponse(res); }; const handleResponse = (res) => { console.log(res.status); }; registerSw();
نقطه انتهایی save_information نیازمند اطلاعاتی در مورد وضعیت ثبت نام (subscribe و unsubscribe)، دادههای ثبت نام و مرورگر است. در نهایت تابع ()registerSw برای آغاز فرایند ثبت نام از کاربر فراخوانی میشود. فایل کامل مانند زیر خواهد بود:
const registerSw = async () => { if ('serviceWorker' in navigator) { const reg = await navigator.serviceWorker.register('sw.js'); initialiseState(reg) } else { showNotAllowed("You can't send push notifications ☹️?") } }; const initialiseState = (reg) => { if (!reg.showNotification) { showNotAllowed('Showing notifications isn\'t supported ☹️?'); return } if (Notification.permission === 'denied') { showNotAllowed('You prevented us from showing notifications ☹️?'); return } if (!'PushManager' in window) { showNotAllowed("Push isn't allowed in your browser ?"); return } subscribe(reg); } const showNotAllowed = (message) => { const button = document.querySelector('form>button'); button.innerHTML = `${message}`; button.setAttribute('disabled', 'true'); }; function urlB64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); const outputData = outputArray.map((output, index) => rawData.charCodeAt(index)); return outputData; } const subscribe = async (reg) => { const subscription = await reg.pushManager.getSubscription(); if (subscription) { sendSubData(subscription); return; } const vapidMeta = document.querySelector('meta[name="vapid-key"]'); const key = vapidMeta.content; const options = { userVisibleOnly: true, // if key exists, create applicationServerKey property ...(key && {applicationServerKey: urlB64ToUint8Array(key)}) }; const sub = await reg.pushManager.subscribe(options); sendSubData(sub) }; const sendSubData = async (subscription) => { const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase(); const data = { status_type: 'subscribe', subscription: subscription.toJSON(), browser: browser, }; const res = await fetch('/webpush/save_information', { method: 'POST', body: JSON.stringify(data), headers: { 'content-type': 'application/json' }, credentials: "include" }); handleResponse(res); }; const handleResponse = (res) => { console.log(res.status); }; registerSw();
سپس یک تگ script برای فایل registerSw.js در home.htnl اضافه میکنیم. فایل را باز کنید:
nano ~/djangopush/templates/home.html
تگ script را پیش از تگ پایانی عنصر body اضافه کنید:
{% load static %} <!DOCTYPE html> <html lang="en"> <head> ... </head> <body> ... <script src="{% static '/js/registerSw.js' %}"></script> </body> </html>
از آنجا که یک سرویس ورکر هنوز وجود ندارد، اگر اجازه بدهید اپلیکیشن به اجرای خود ادامه بدهد یا تلاش کنید آن را مجدداً آغاز کنید با پیام خطایی مواجه میشود. این وضعیت را با ایجاد یک سرویس ورکر اصلاح میکنیم.
مرحله هشتم – ایجاد یک سرویس ورکر
برای نمایش پوش نوتیفیکیشن باید یک سرویس ورکر فعال روی صفحه اصلی اپلیکیشن نصب شده باشد. ما یک سرویس ورکر ایجاد میکنیم که به رویدادهای push گوش داده و پیامهایی را که آماده شدهاند نمایش میدهد.
از آنجا که میخواهیم حیطه سرویس ورکر محدود به دامنه کلی باشد، باید آن را در ریشه اپلیکیشن نصب کنیم. رویکرد ما به صورت ایجاد یک فایل به نام sw.js در پوشه templates است و سپس اقدام به ثبت یک ویو میکنیم. ابتدا فایل را ایجاد کنید:
nano ~/djangopush/templates/sw.js
کد زیر را که به سرویس ورکر اعلام میکند باید به رویدادهای پوش گوش دهد، اضافه کنید:
// Register event listener for the 'push' event. self.addEventListener('push', function (event) { // Retrieve the textual payload from event.data (a PushMessageData object). // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData. const eventInfo = event.data.text(); const data = JSON.parse(eventInfo); const head = data.head || 'New Notification ??'; const body = data.body || 'This is default content. Your notification didn\'t have one ??'; // Keep the service worker alive until the notification is created. event.waitUntil( self.registration.showNotification(head, { body: body, icon: 'https://i.imgur.com/MZM3K5w.png' }) ); });
بدین ترتیب سرویس ورکر منتظر یک رویداد push میشود. در تابع callback دادههای event به صورت متنی در میآیند. اگر این دادهها شامل عنوان و متن پوش نباشند، از رشتههای پیشفرض title و body استفاده میکنیم. تابع showNotification عنوان نوتیفیکیشن را میگیرد. این همان مقدار هدر نوتیفیکیشن است که باید نمایش یابد و یک شیء options نیز به عنوان پارامتر دریافت میکند. شیء onjects شامل چندین مشخصه برای پیکربندی گزینههای بصری یک اعلان است.
برای این که سرویس ورکر روی کلیت دامنه کار کند، باید آن را در ریشه اپلیکیشن نصب کنید. ما از TemplateView برای ایجاد امکان دسترسی سرویس ورکر به کل دامنه استفاده میکنیم. به این منظور فایل urls.py را باز کنید:
nano ~/djangopush/djangopush/urls.py
یک گزاره ایمپورت و مسیر جدید را در فهرست urlpatterns اضافه کنید تا یک ویوی مبتنی بر کلاس ایجاد شود:
... from django.views.generic import TemplateView urlpatterns = [ ..., path('sw.js', TemplateView.as_view(template_name='sw.js', content_type='application/x-javascript')) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
ویوهای مبتنی بر کلاس مانند TemplateView امکان ایجاد ویوهای منعطف و با قابلیت استفاده مجدد را فراهم میسازند. در این مورد متد TemplateView.as_view با ارسال سرویس ورکر اخیراً ایجاد شده به عنوان یک قالب و application/x-javascript به عنوان content_type قالب، یک مسیر برای سرویس ورکر ایجاد میکند.
اکنون شما یک سرویس ورکر ایجاد کرده و آن را به عنوان یک مسیر ثبت نمودهاید. در ادامه اقدام به راهاندازی آن از صفحه اصلی برای ارسال پوش نوتیفیکیشن میکنیم.
مرحله نهم – ارسال پوش نوتیفیکیشن
کاربران با بهرهگیری از فرم موجود در صفحه اصلی در حالتی که سرور شما در حال اجرا باشد، میتوانند اقدام به ارسال پوش نوتیفیکیشن بکنند. همچنین میتوانید پوش نوتیفیکیشنها را با استفاده از هر سرویس RESTful مانند Postman ارسال کنید. زمانی که کاربر پوش نوتیفیکیشن را از فرم صفحه اصلی ارسال میکند، دادههای آن شامل head و body و همچنین id کاربر دریافت کننده خواهد بود. دادهها باید به روش زیر سازماندهی شده باشند:
{ head: "Title of the notification", body: "Notification body", id: "User's id" }
برای گوش دادن به رویداد submit فرم و ارسال دادههای وارد شده از کاربر به سرور یک فایل به نام site.js در مسیر دایرکتوری djangopush/static/js/~ ایجاد میکنیم. فایل را باز کنید:
nano ~/djangopush/static/js/site.js
ابتدا یک شنونده رویداد submit به فرم اضافه کنید که امکان دریافت مقادیر ورودی فرم را فراهم میسازد و id کاربر نیز درنگ meta قالب ذخیره میشود:
const pushForm = document.getElementById('send-push__form'); const errorMsg = document.querySelector('.error'); pushForm.addEventListener('submit', async function (e) { e.preventDefault(); const input = this[0]; const textarea = this[1]; const button = this[2]; errorMsg.innerText = ''; const head = input.value; const body = textarea.value; const meta = document.querySelector('meta[name="user_id"]'); const id = meta ? meta.content : null; ... // TODO: make an AJAX request to send notification });
تابع pushForm مقادیر input, textarea و button درون فرم را دریافت میکند. همچنین اطلاعاتی را از تگ meta میگیرد که شامل خصوصیت نام user_id و id کاربر است که در خصوصیت content تگ ذخیره شده است. با داشتن این اطلاعات میتوانیم یک درخواست POST به نقطه انتهایی send_push/ روی سرور ارسال کنیم.
برای ارسال درخواست به سرور باید از API بومی Fetch استفاده کنیم. ما از Fetch به این دلیل استفاده میکنیم که از سوی اغلب مرورگرها پشتیبانی میشود و نیازمند کتابخانههای خارجی برای کارکرد خود نیست. در ادامه کدی که باید اضافه کنید و تابع pushForm را بهروزرسانی کنید تا کد ارسال درخواستهای AJAX را شامل شود مشاهده میکنید:
const pushForm = document.getElementById('send-push__form'); const errorMsg = document.querySelector('.error'); pushForm.addEventListener('submit', async function (e) { ... const id = meta ? meta.content : null; if (head && body && id) { button.innerText = 'Sending...'; button.disabled = true; const res = await fetch('/send_push', { method: 'POST', body: JSON.stringify({head, body, id}), headers: { 'content-type': 'application/json' } }); if (res.status === 200) { button.innerText = 'Send another ?!'; button.disabled = false; input.value = ''; textarea.value = ''; } else { errorMsg.innerText = res.message; button.innerText = 'Something broke ?.. Try again?'; button.disabled = false; } } else { let error; if (!head || !body){ error = 'Please ensure you complete the form ??' } else if (!id){ error = "Are you sure you're logged in? ?. Make sure! ??" } errorMsg.innerText = error; } });
اگر پارامترهای ضروری head، body و id موجود باشند، میتوانیم درخواست را ارسال کنیم و دکمه submit را موقتاً غیر فعال کنیم. فایل کامل به صورت زیر خواهد بود:
const pushForm = document.getElementById('send-push__form'); const errorMsg = document.querySelector('.error'); pushForm.addEventListener('submit', async function (e) { e.preventDefault(); const input = this[0]; const textarea = this[1]; const button = this[2]; errorMsg.innerText = ''; const head = input.value; const body = textarea.value; const meta = document.querySelector('meta[name="user_id"]'); const id = meta ? meta.content : null; if (head && body && id) { button.innerText = 'Sending...'; button.disabled = true; const res = await fetch('/send_push', { method: 'POST', body: JSON.stringify({head, body, id}), headers: { 'content-type': 'application/json' } }); if (res.status === 200) { button.innerText = 'Send another ?!'; button.disabled = false; input.value = ''; textarea.value = ''; } else { errorMsg.innerText = res.message; button.innerText = 'Something broke ?.. Try again?'; button.disabled = false; } } else { let error; if (!head || !body){ error = 'Please ensure you complete the form ??' } else if (!id){ error = "Are you sure you're logged in? ?. Make sure! ??" } errorMsg.innerText = error; } });
در نهایت فایل site.js را به home.html اضافه میکنیم:
nano ~/djangopush/templates/home.html
تگ script را نیز اضافه میکنیم:
{% load static %} <!DOCTYPE html> <html lang="en"> <head> ... </head> <body> ... <script src="{% static '/js/site.js' %}"></script> </body> </html>
در این زمان اگر اپلیکیشن در حال اجرا بماند یا تلاش کنید آن را مجدداً آغاز کنید، با خطایی مواجه میشوید، زیرا سرویس ورکرها تنها زمانی میتوانند فعالیت کنند که روی دامنههای امن یا localhost باشند. در مرحله بعدی از ngrok برای ایجاد تونل امن به وبسرور خود استفاده میکنیم:
مرحله دهم – ایجاد یک تونل امن برای تست اپلیکیشن
سرویس ورکرها برای کارکرد روی هر سایتی به جز localhost نیازمند اتصال امن هستند، زیرا در غیر این صورت ممکن است موجب شوند که اتصال کاربران به سرقت برود و پاسخها فیلتر شده و جعل شوند. به همین دلیل یک تونل امن برای سرور خود با استفاده از ngrok میسازیم.
پنجره ترمینال دومی را باز کنید و مطمئن شوید که در دایرکتوری home قرار دارید:
cd ~
اگر کار خود را روی نسخه تازه نصب شدهای از سرور 18.04 آغاز کردهاید، در این صورت باید unzip را نصب کنید:
sudo apt update && sudo apt install unzip
ngrok را دانلود کنید:
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip unzip ngrok-stable-linux-amd64.zip
ngrok را به usr/local/bin/ انتقال دهید، به طوری که به دستور ngrok از ترمینال دسترسی داشته باشید:
sudo mv ngrok /usr/local/bin
در پنجره اول ترمینال خود، مطمئن شوید که در دایرکتوری پروژه قرار دارید و سرور خود را آغاز کنید:
cd ~/djangopush
python manage.py runserver your_server_ip:8000
شما باید این کار را پیش از ایجاد یک تونل امن برای اپلیکیشن خود انجام دهید. در پنجره ترمینال به پوشه پروژه بروید و محیط مجازی را فعال کنید:
cd ~/djangopush source my_env/bin/activate
تونل امن به اپلیکیشن خود را ایجاد کنید:
ngrok http your_server_ip:8000
در این زمان خروجی زیر را میبینید که شامل اطلاعاتی در مورد URL امن ngrok است:
خروجی
ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Session Expires 7 hours, 59 minutes Version 2.2.8 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://ngrok_secure_url -> 203.0.113.0:8000 Forwarding https://ngrok_secure_url -> 203.0.113.0:8000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
مقدار ngrok_secure_url را از خروجی کنسول کپی کنید. آن را باید به فهرست ALLOWED_HOSTS در فایل settings.py خود اضافه کنید.
پنجره ترمینال دیگری باز کنید و به پوشه پروژه بروید و محیط مجازی را فعال کنید:
cd ~/djangopush source my_env/bin/activate
فایل settings.py را باز کنید:
nano ~/djangopush/djangopush/settings.py
فهرست ALLOWED_HOSTS را با تونل امن ngrok بهروزرسانی کنید:
... ALLOWED_HOSTS = ['your_server_ip', 'ngrok_secure_url'] ...
به صفحه امن ادمین در مسیر https://ngrok_secure_url/admin بروید. با صفحهای مانند تصویر زیر مواجه خواهید شد:
اطلاعات کاربر ادمین جنگوی خود را در این صفحه وارد کنید. این همان اطلاعاتی است که هنگام ورود به رابط کاربری ادمین در مراحل پیشنیاز این راهنما وارد کردهاید. اینک آماده ارسال پوش نوتیفیکیشن هستیم.
در مرورگر خود به آدرس https://ngrok_secure_url بروید. با یک اعلان مواجه میشوید که از شما میخواهد تا اجازه نمایش اعلانها را بدهید. با کلیک روی دکمه allow میتوانید به مرورگر خود اجازه بدهید تا پوش نوتیفیکیشنها را نمایش دهد:
یا ارائه یک فرم پر شده مانند زیر میتوانید یک پوش نوتیفیکیشن را نمایش دهید:
دقت کنید که سرور شما باید پیش از اقدام به ارسال نوتیفیکیشن در حال اجرا باشد. اگر نوتیفیکیشن را دریافت کردهاید در این صورت اپلیکیشن شما مطابق انتظار کار میکند.
بدین ترتیب ما موفق شدهایم اپلیکیشنی ایجاد کنیم که امکان ارسال پوش نوتیفیکیشن روی سرور را به کمک سرویس ورکرها برای دریافت و نمایش اعلانها دارد. همچنین با مراحل به دست آوردن کلیدهای VAPID که برای ارسال پوش نوتیفیکیشنها از سرور اپلیکیشن مورد نیاز هستند آشنا شدیم.
سخن پایانی
در این راهنما با روش ثبت نام از کاربران برای ارسال پوش نوتیفیکیشن، نصب سرویس ورکرها و نمایش پوش نوتیفیکیشنها با استفاده از API نوتیفیکیشن آشنا شدید. مراحل دیگر این است که تلاش کنید نوتیفیکیشنها را طوری پیکربندی کنید که هنگام کلیک شدن، کاربر را به بخشهای مختلف اپلیکیشن هدایت کنند. کد منبع این راهنما را میتوانید در این لینک (+) مشاهده کنید.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای پروژهمحور برنامه نویسی
- آموزش جنگو (Django) – فریمورک تحت وب با پایتون (Python)
- مجموعه آموزشهای برنامهنویسی پایتون
- آموزش پایتون (Python) — مجموعه مقالات جامع وبلاگ فرادرس
- چگونه با پایتون برای لینوکس ابزار اعلانات ایجاد کنیم؟ — راهنمای گامبهگام
- زبان برنامه نویسی پایتون (Python) — از صفر تا صد
==
سلام اموزش عالی بود.
الان این برنامه برای هر چند کاربری که قبول کنند نوتیفیکیشن ارسال میکند یا فقط برای همونی که نوتیفیکیشن ارسال میکند؟