Django ile Gerçek Zamanlı(Realtime) Hava Durumu Projesi¶
Part 1¶
Bu yazı serisinde Python Django ve external(harici, üçüncü parti) API'ler ile gerçek zamanlı hava durumu uygulaması geliştireceğiz.
Bu serinin sonunda, Python Django ile bir web uygulamasının nasıl geliştirileceğini, web soketlerini Python Django'ya nasıl uygulayacağınızı ve verileri harici API'lerden nasıl alacağınızı öğreneceksiniz.
Proje Başlatma ve Kurulumlar
hava_durumu adında bir klasör açın ve istediğiniz bir kod editörü ile bu klasörü açın. Tavsiye olarak vscode kullanabilirsiniz.
Terminali açarak hava_durumu klasörü içerisinde bir adet python sanal ortam oluşturalım. Yeni bir python projesine başlarken sanal bir ortam oluşturmak iyi bir pratiktir.
$ cd hava_durumu/
# Sanal ortamımı env311 olarak adlandırdım
$ python3.11 -m venv env311
# Sanal ortamı etkinleştirin (macos/linux kullanıcıları)
$ source env311/bin/activate
# Windows kullanıcısıysanız bu şekilde etkinleştirebilirsiniz.
# $ env311\Scripts\activate
$ tree -I env311 .
.
├── manage.py
└── src
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
1 directory, 6 files
Projeyi çalıştıralım
$ python manage.py runserver
# asagidaki gibi bir console mesaji gorursunuz
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
June 27, 2023 - 13:39:14
Django version 4.2.2, using settings 'src.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Migration
Aşağıdaki komutu çalıştırarak django tarafından üretilmiş varsayılan migration dosyalarını veritabanına(sqlite3) gönderelim
- Migration Çıktısı$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
Aşağıdaki komut ile yönetici/admin kullanıcısı oluşturabilirsiniz.
- İstediğiniz kullanıcı adı(username), email ve parolayı(password) girin ve Enter'a basınız.Username (leave blank to use 'dev'): admin
Email address: [email protected]
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
Yeni bir Uygulama(App) Oluşturalım.
Aşağıdaki komut ile projede yeni bir uygulama oluşturabilirsiniz. Django projeleri birden fazla uygulamadan oluşabilir. Böylece modüler bir çalışma düzenine sahip olur.
- Daha sonra myapp adını verdiğimiz uygulamayı settings.py dosyasında INSTALLED_APPS listesine ekleyelim# hava_durumu/src/settings.py
INSTALLED_APPS = [
# external apps
# TODO
# project apps
'myapp',
# django default apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
BASE_DIR / "templates", # yeni eklendi
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
hava_durumu projemiz içerisine templates adında bir klasör oluşturalım ve içerisine core.html adında bir html dosyası oluşturalım. myapp içerisine templates klasörü onun da içerisine myapp adında bir klasör oluşturalım ve index.html adında bir html dosyasını oluşturalım. URLS dosyaları
myapp içerisine urls.py adında bir python dosyası oluşturalım. İçerisine aşağıdaki path'i ekleyelim
from django.urls import path
# internals
from . import views
urlpatterns = [
path('', views.index, name="app-index"),
]
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request, "myapp/index.html")
# src/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include # yeni
urlpatterns = [
path('admin/', admin.site.urls),
path('', include("myapp.urls")), # yeni
]
.
├── db.sqlite3 # otomatik oluştu
├── manage.py
├── myapp
│ ├── __init__.py
│ ├── __pycache__
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── __pycache__
│ ├── models.py
│ ├── templates # yeni
│ │ └── myapp
│ │ └── index.html
│ ├── tests.py
│ ├── urls.py # yeni
│ └── views.py
├── src
│ ├── __init__.py
│ ├── __pycache__
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── templates # yeni
└── core.html
9 directories, 17 files
templates/core.html
içerisine aşağıdaki kodları yapıştıralım. {% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pytr.info | Hava Durumu Django Projesi</title>
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<!-- header -->
<div class="centered">
{% block core_header%}
<h3>
<a href="/">
Gerçek Zamanlı Hava Durumu Uygulaması
</a>
</h3>
{% endblock core_header%}
</div>
<!-- body -->
<div class="centered">
{% block core_body%}
{% endblock core_body%}
</div>
<!-- footer -->
<div class="centered bg-light">
{% block core_footer%}
<h4>https://pytr.info</h4>
{% endblock core_footer%}
</div>
</body>
</html>
{% extends 'core.html' %}
{% block core_body %}
<div class="container">
<div class="row">
<form>
<input type="text" id="cityInput" class="textbox" placeholder="Şehir ismi giriniz. Ör; Malatya">
<button class="mat-button mat-primary-button" type="button" id="checkWeatherButton">
Kontrol Et
</button>
</form>
</div>
<div class="row">
<div class="card-container" id="weather-container">
<!-- api yanıtı ve html elementleri burada olusturulacak -->
</div>
</div>
</div>
{% endblock core_body %}
Projede kullanılan css/style.css dosyasına https://github.com/adnankaya/pytrinfo-projects/blob/master/hava_durumu/static/css/style.css adresinden ulaşabilirsiniz. Yazıyı kalabalıklaştırmamak için eklemiyorum. hava_durumu içerisinde static adında bir klasör oluşturun ve içerisine css adında bir klasör ve bunun içine de style.css adında bir dosya oluşturun hava_durumu/static/css/style.css olacak şekilde bir dosya dizini oluşturmalısınız.
- settings.py içerisine static dosyalarımızın bulunabilmesi için bir konfigürasyon eklememiz gerekiyor - Bu aşamaya kadar proje oluşturma ve kurulumları gerçekleştirdik. Basit bir frontend arayüzü geliştirdik.Part 2¶
Bu yazımızda https://www.weatherapi.com sitesinin sunduğu hava durumu verilerine API üzerinden erişerek veri çekmeye çalışacağız. Üye olduktan sonra My Account linkine tıklayarak size verilen API Key'e erişebilirsiniz. Bu API Keyi kopyalayın ve proje ana dizininde yani hava_durumu klasöründe .env adında bir dosya açarak içerisine aşağıdaki gibi ekleyin. hava_durumu/.env
- dot env yani .env dosyalarını python, django projelerinden okuyabilmek için python-dotenv paketini kuralım. - Şimdi settings.py dosyasını açalım ve aşağıdaki eklemeleri yapalım.# settings.py
import os # yeni
from dotenv import load_dotenv # yeni
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv( BASE_DIR/ ".env" ) #yeni
WA_APIKEY = os.getenv("WA_APIKEY") # yeni
Ekleme yaptıktan sonra test etmek için shell ortamına geçebilirsiniz.
- API Keyinizi kullanarak weatherapi servisine istek atmak için Docs'ta belirtilen URL'i kullanacağız. curl ile örnek bir istek atmak isterseniz API-KEYINIZ yerine kendi api keyinizi yapıştırınız.curl --location --request GET 'http://api.weatherapi.com/v1/current.json?key=API_KEYINIZ&q=istanbul'
{
"location": {
"name": "Istanbul",
"region": "Istanbul",
"country": "Turkey",
"lat": 41.02,
"lon": 28.96,
"tz_id": "Europe/Istanbul",
"localtime_epoch": 1687881139,
"localtime": "2023-06-27 18:52"
},
"current": {
"last_updated_epoch": 1687880700,
"last_updated": "2023-06-27 18:45",
"temp_c": 28.0,
"temp_f": 82.4,
"is_day": 1,
"condition": {
"text": "Sunny",
"icon": "//cdn.weatherapi.com/weather/64x64/day/113.png",
"code": 1000
},
"wind_mph": 17.4,
"wind_kph": 28.1,
"wind_degree": 30,
"wind_dir": "NNE",
"pressure_mb": 1011.0,
"pressure_in": 29.85,
"precip_mm": 0.0,
"precip_in": 0.0,
"humidity": 40,
"cloud": 0,
"feelslike_c": 28.7,
"feelslike_f": 83.6,
"vis_km": 10.0,
"vis_miles": 6.0,
"uv": 7.0,
"gust_mph": 14.1,
"gust_kph": 22.7
}
}
>>> import os
>>> import requests
>>>
>>> city = "istanbul"
>>> WA_APIKEY = os.getenv("WA_APIKEY")
>>> URL = f"http://api.weatherapi.com/v1/current.json?key={WA_APIKEY}&q={city}"
>>>
>>> response = requests.get(URL)
>>> response
<Response [200]>
>>>
>>> response.json()
{'location': {'name': 'Istanbul', 'region': 'Istanbul', 'country': 'Turkey', 'lat': 41.02, 'lon': 28.96, 'tz_id': 'Europe/Istanbul', 'localtime_epoch': 1687881591, 'localtime': '2023-06-27 18:59'}, 'current': {'last_updated_epoch': 1687880700, 'last_updated': '2023-06-27 18:45', 'temp_c': 28.0, 'temp_f': 82.4, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 17.4, 'wind_kph': 28.1, 'wind_degree': 30, 'wind_dir': 'NNE', 'pressure_mb': 1011.0, 'pressure_in': 29.85, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 40, 'cloud': 0, 'feelslike_c': 28.7, 'feelslike_f': 83.6, 'vis_km': 10.0, 'vis_miles': 6.0, 'uv': 7.0, 'gust_mph': 14.1, 'gust_kph': 22.7}}
>>> data = response.json()
>>> data["location"]["name"]
'Istanbul'
>>> data["current"]["condition"]
{'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}
>>> data["current"]["temp_c"]
28.0
Part 3¶
Bu yazıda weatherapi'dan dönen veriyi kendi uygulamamızda muhafaza edebilmek için model oluşturacağız. Ayrıca gerekli olan view fonksiyonlarını ekleyeceğiz. Ve frontend tarafında hava durumu kayıtlarını göstereceğiz.
myapp/models.py içerisine Weather modelini aşağıdaki gibi ekleyelim.
from django.db import models
# Create your models here.
class Weather(models.Model):
city = models.CharField(max_length=32)
temperature = models.CharField(max_length=24)
description = models.TextField()
icon = models.CharField(max_length=16)
updated_date = models.DateTimeField()
api_response = models.TextField()
def __str__(self) -> str:
return self.city
import requests
from django.conf import settings
def request_to_weatherapi(city: str) -> dict:
URL = f"http://api.weatherapi.com/v1/current.json?key={settings.WA_APIKEY}&q={city}"
res = requests.get(URL)
res.raise_for_status() # HTTP 200 olmayan durum kodları için exception raise eder
res_json = res.json()
return {
"city": res_json["location"]["name"],
"description": res_json["current"]["condition"]["text"],
"icon": f'http:{res_json["current"]["condition"]["icon"]}',
"temperature": res_json["current"]["temp_c"],
"updated_date": res_json["current"]["last_updated"],
"api_response": res.text
}
from django.shortcuts import render
from django.http.response import JsonResponse # yeni
# internals
from .utils import request_to_weatherapi # yeni
def index(request):
city = request.GET.get("city")
if city:
data_dict = request_to_weatherapi(city)
return JsonResponse(data_dict)
return render(request, "myapp/index.html")
{"city": "Ankara", "description": "Clear", "icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png", "temperature": 17.0, "updated_date": "2023-06-28 03:30", "api_response": "{\"location\":{\"name\":\"Ankara\",\"region\":\"Ankara\",\"country\":\"Turkey\",\"lat\":39.93,\"lon\":32.86,\"tz_id\":\"Europe/Istanbul\",\"localtime_epoch\":1687912859,\"localtime\":\"2023-06-28 3:40\"},\"current\":{\"last_updated_epoch\":1687912200,\"last_updated\":\"2023-06-28 03:30\",\"temp_c\":17.0,\"temp_f\":62.6,\"is_day\":0,\"condition\":{\"text\":\"Clear\",\"icon\":\"//cdn.weatherapi.com/weather/64x64/night/113.png\",\"code\":1000},\"wind_mph\":2.2,\"wind_kph\":3.6,\"wind_degree\":290,\"wind_dir\":\"WNW\",\"pressure_mb\":1013.0,\"pressure_in\":29.91,\"precip_mm\":0.0,\"precip_in\":0.0,\"humidity\":77,\"cloud\":0,\"feelslike_c\":17.0,\"feelslike_f\":62.6,\"vis_km\":10.0,\"vis_miles\":6.0,\"uv\":1.0,\"gust_mph\":9.4,\"gust_kph\":15.1}}"}
from django.shortcuts import render
from django.http.response import JsonResponse
# internals
from .utils import request_to_weatherapi
from .models import Weather # yeni
def index(request):
city = request.GET.get("city")
if city:
data_dict = request_to_weatherapi(city)
# veritabanından bu city e ait kaydı getir
qset = Weather.objects.filter(city__iexact=city)
# eğer kayıt veritabanında varsa
if qset.exists():
# veritabanındaki kaydı, API'den dönen veri ile güncelle
qset.update(**data_dict)
else:
# kayıt yoksa yeni kayıt oluştur.
new_weather = Weather.objects.create(**data_dict)
return JsonResponse([data_dict], safe=False)
return render(request, "myapp/index.html")
[{"city": "Istanbul", "description": "Clear", "icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png", "temperature": 21.0, "updated_date": "2023-06-28 04:15", "api_response": "{\"location\":{\"name\":\"Istanbul\",\"region\":\"Istanbul\",\"country\":\"Turkey\",\"lat\":41.02,\"lon\":28.96,\"tz_id\":\"Europe/Istanbul\",\"localtime_epoch\":1687915448,\"localtime\":\"2023-06-28 4:24\"},\"current\":{\"last_updated_epoch\":1687914900,\"last_updated\":\"2023-06-28 04:15\",\"temp_c\":21.0,\"temp_f\":69.8,\"is_day\":0,\"condition\":{\"text\":\"Clear\",\"icon\":\"//cdn.weatherapi.com/weather/64x64/night/113.png\",\"code\":1000},\"wind_mph\":5.6,\"wind_kph\":9.0,\"wind_degree\":70,\"wind_dir\":\"ENE\",\"pressure_mb\":1011.0,\"pressure_in\":29.85,\"precip_mm\":0.0,\"precip_in\":0.0,\"humidity\":83,\"cloud\":0,\"feelslike_c\":21.0,\"feelslike_f\":69.8,\"vis_km\":10.0,\"vis_miles\":6.0,\"uv\":1.0,\"gust_mph\":10.1,\"gust_kph\":16.2}}"}]
# ...
def index(request):
city = request.GET.get("city")
if city:
data_dict = request_to_weatherapi(city)
qset = Weather.objects.filter(city__iexact=city)
if qset.exists():
qset.update(**data_dict)
else:
new_weather = Weather.objects.create(**data_dict)
return JsonResponse([data_dict], safe=False)
# eger all parametresi client tarafindan gonderilmisse butun kayitlari cevap olarak döndür
all = request.GET.get("all")
if all:
weathers = Weather.objects.all()
serialized_data = [
{
"id": obj.id,
"city": obj.city,
"icon": obj.icon,
"temperature": obj.temperature,
"description": obj.description,
"updated_date": obj.updated_date.strftime('%Y-%m-%d %H:%M')
}
for obj in weathers
]
return JsonResponse(serialized_data, safe=False)
return render(request, "myapp/index.html")
[
{
"id": 1,
"city": "Ankara",
"icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png",
"temperature": "17.0",
"description": "Clear",
"updated_date": "2023-06-28 03:45"
},
{
"id": 2,
"city": "Malatya",
"icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png",
"temperature": "21.0",
"description": "Clear",
"updated_date": "2023-06-28 03:45"
},
{
"id": 3,
"city": "Istanbul",
"icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png",
"temperature": "21.0",
"description": "Clear",
"updated_date": "2023-06-28 03:45"
}
]
myapp/templates/myapp/index.html
{% block core_body %}
<div class="container">
<!-- ... -->
</div>
<script>
// HTML elements
const weatherContainer = document.getElementById('weather-container');
const cityInput = document.getElementById('cityInput');
const checkWeatherButton = document.getElementById('checkWeatherButton');
function createElements(data, weatherContainer) {
// Clear the weather container
weatherContainer.innerHTML = '';
// Loop over the weather data and create HTML elements
data.forEach(weather => {
const cardContainer = document.createElement('div');
cardContainer.classList.add('card-container');
const materialCard = document.createElement('div');
materialCard.classList.add('material-card');
const weatherIcon = document.createElement('img');
weatherIcon.src = `${weather.icon}`;
weatherIcon.alt = 'Image';
const temperature = document.createElement('span');
temperature.classList.add('temperature');
temperature.textContent = `${weather.temperature}° C`;
const city = document.createElement('span');
city.classList.add('city');
city.textContent = weather.city;
const description = document.createElement('span');
description.classList.add('description');
description.textContent = weather.description;
const updatedDate = document.createElement('small');
updatedDate.classList.add('updated_date');
updatedDate.textContent = weather.updated_date;
// Append the created elements
materialCard.appendChild(weatherIcon);
materialCard.appendChild(temperature);
materialCard.appendChild(city);
materialCard.appendChild(description);
materialCard.appendChild(updatedDate);
cardContainer.appendChild(materialCard);
weatherContainer.appendChild(cardContainer);
});
}
</script>
{% endblock core_body %}
Bu script içerisine 1 event listener ve 1 fonksiyon ekleyelim.
// HTML elements
const weatherContainer = document.getElementById('weather-container');
const cityInput = document.getElementById('cityInput');
const checkWeatherButton = document.getElementById('checkWeatherButton');
// Event listener for the button click
checkWeatherButton.addEventListener('click', () => {
const city = cityInput.value;
const queryParams = { city };
// Call the getApiDataByParameter function with the query parameters
getApiDataByParameter(queryParams);
});
function getApiDataByParameter(queryParams = {}) {
// Convert query parameters to URL search parameters
const urlSearchParams = new URLSearchParams(queryParams);
const queryString = urlSearchParams.toString();
// Fetch data from the API with query parameters
fetch(`/?${queryString}`)
.then(response => response.json())
.then(data => {
if (data) {
console.log("getApiDataByParameter -> API response:", data);
createElements(data, weatherContainer);
}
})
.catch(error => {
console.error('Error triggering:', error);
});
}
script'in en alt kısmında aşağıdaki şekilde bir çağrı yaparak django backend'den bütün kayıtları alıp frontend'de gösterebiliriz
Yeni Eklediğimiz Script'in Tamamı<script>
// HTML elements
const weatherContainer = document.getElementById('weather-container');
const cityInput = document.getElementById('cityInput');
const checkWeatherButton = document.getElementById('checkWeatherButton');
// Event listener for the button click
checkWeatherButton.addEventListener('click', () => {
const city = cityInput.value;
const queryParams = { city };
// Call the getApiDataByParameter function with the query parameters
getApiDataByParameter(queryParams);
});
function getApiDataByParameter(queryParams = {}) {
// Convert query parameters to URL search parameters
const urlSearchParams = new URLSearchParams(queryParams);
const queryString = urlSearchParams.toString();
// Fetch data from the API with query parameters
fetch(`/?${queryString}`)
.then(response => response.json())
.then(data => {
if (data) {
console.log("getApiDataByParameter -> API response:", data);
createElements(data, weatherContainer);
}
})
.catch(error => {
console.error('Error triggering:', error);
});
}
function createElements(data, weatherContainer) {
// Clear the weather container
weatherContainer.innerHTML = '';
// Loop over the weather data and create HTML elements
data.forEach(weather => {
const cardContainer = document.createElement('div');
cardContainer.classList.add('card-container');
const materialCard = document.createElement('div');
materialCard.classList.add('material-card');
const weatherIcon = document.createElement('img');
weatherIcon.src = `${weather.icon}`;
weatherIcon.alt = 'Image';
const temperature = document.createElement('span');
temperature.classList.add('temperature');
temperature.textContent = `${weather.temperature}° C`;
const city = document.createElement('span');
city.classList.add('city');
city.textContent = weather.city;
const description = document.createElement('span');
description.classList.add('description');
description.textContent = weather.description;
const updatedDate = document.createElement('small');
updatedDate.classList.add('updated_date');
updatedDate.textContent = weather.updated_date;
// Append the created elements
materialCard.appendChild(weatherIcon);
materialCard.appendChild(temperature);
materialCard.appendChild(city);
materialCard.appendChild(description);
materialCard.appendChild(updatedDate);
cardContainer.appendChild(materialCard);
weatherContainer.appendChild(cardContainer);
});
}
// initial call
getApiDataByParameter({"all":true});
</script>
Part 4¶
Serinin bu yazısında websoket üzerinden veri alışverişini projemize entegre ederek gerçek zamanlı veri alışverişini sağlamaya çalışacağız.
Django varsayılan olarak HTTP ile veri alışverişi gerçekleştirecek bir uygulama geliştirme imkanı sağlamaktadır. Uzun süreli bağlantılar(websockets, MQTT, chatbots, amateur radio vs.) gerektiren uygulamalar geliştirebilmek için ekstra kütüphanelere ihtiyaç duyarız. channels kütüphanesi de bunalrdan biridir.
channels, Django'yu alıp WebSockets, chat protokolleri, IoT protokolleri ve daha fazlasını işlemek için djangonun yapabileceklerini HTTP'nin ötesine taşıyan bir projedir. ASGI adlı bir Python spesifikasyonu üzerine inşa edilmiştir.
Bu projede ASGI server olarak da Django Daphne kullanılacaktır.
Bu paketleri kuralım
- settings.py dosyasını açarak INSTALLED_APPS listesinin ilk sırasına daphne uygulamasını ekleyelim ve ASGI_APPLICATION settings değerini aşağıdaki gibi ekleyelim.INSTALLED_APPS = [
# external apps
'daphne', # yeni
# project apps
'myapp',
# django default apps
# ...
]
# ...
WSGI_APPLICATION = 'src.wsgi.application'
# Daphne icin bunu ekliyoruz
ASGI_APPLICATION = "src.asgi.application"
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings')
application = get_asgi_application()
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter # yeni
from channels.auth import AuthMiddlewareStack # yeni
from channels.security.websocket import AllowedHostsOriginValidator # yeni
# yeni
from myapp.routing import websocket_urlpatterns as myapp_websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
# websocket konfigurasyonlari yazilacak...
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(myapp_websocket_urlpatterns))
),
})
Bu konfigurasyon ile beraber daphne kendini Django'ya entegre edecek ve runserver komutunun kontrolünü ele geçirecektir.
Şimdi myapp içerisinde routing.py adında bir python dosyası oluşturalım ve aşağıdaki gibi düzenleyelim
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"weather-websocket-data/$", consumers.WeatherConsumer.as_asgi()),
]
import json
from channels.generic.websocket import WebsocketConsumer
active_consumers = []
class WeatherConsumer(WebsocketConsumer):
def connect(self):
active_consumers.append(self)
self.accept()
def disconnect(self, close_code):
active_consumers.remove(self)
def receive(self, qset):
self.send(text_data=json.dumps(qset))
HTTP istekleri için django urls içerisinde tanımladığımız path'lere gelen istekler view fonksyionları/sınıfları tarafından karşılanır. Websocket istekleri için routing içerisinde tanımladığımız path'lere gelen istekler consumer fonksiyon/sınıfları tarafından karşılanır. Şimdi myapp/index.html içerisine javascript kısmına websocket haberleşme için kullanacağımız fonksiyonu yazalım.
<script>
// HTML elements
const weatherContainer = document.getElementById('weather-container');
const cityInput = document.getElementById('cityInput');
const checkWeatherButton = document.getElementById('checkWeatherButton');
// ...
function getApiDataByParameter(queryParams = {}) {
// ...
}
function createElements(data, weatherContainer) {
// ...
}
// web soket baglantisi olusturmak icin tanimlanan fonksiyon
function connectWebSocket() {
const WEBSOCKET_PATH = "/weather-websocket-data/"
const WEBSOCKET_PROTOCOL_NAME = "ws://"
const SERVER_DOMAIN = window.location.host;
_socket = new WebSocket(
// websocket haberlesme icin URL olusturma. Ör; ws://localhost:8000/my-ws-data/
WEBSOCKET_PROTOCOL_NAME + SERVER_DOMAIN + WEBSOCKET_PATH
);
// backendden veri aliyoruz
_socket.onmessage = function (event) {
const data = JSON.parse(event.data);
// Alinan veriyi isle
console.log("alınan data: ", data);
};
_socket.onclose = function (event) {
console.error('web socket beklenmedik şekilde kapandı');
};
_socket.onerror = function (error) {
console.error('WebSocket error:', error);
// hata alma durumunda socket baglantisini kapat ve yeniden baglan
_socket.close();
setTimeout(connectWebSocket, 2000);
};
}
/*******************************************************/
// Connect to the WebSocket
/*******************************************************/
connectWebSocket();
// initial call
getApiDataByParameter({ "all": true });
</script>
WebSocket için gerekli olan değişkenler tanımlanır. WEBSOCKET_PATH değişkeni, WebSocket yolunu belirtir. WEBSOCKET_PROTOCOL_NAME değişkeni, WebSocket protokolünü (ws://) temsil eder. SERVER_DOMAIN değişkeni, sunucu alan adını (window.location.host) temsil eder. _socket adlı bir WebSocket nesnesi oluşturulur ve belirtilen URL'ye bağlantı yapılır. URL, WEBSOCKET_PROTOCOL_NAME + SERVER_DOMAIN + WEBSOCKET_PATH şeklinde oluşturulur. _socket nesnesinin onmessage olayı dinlenir. Yeni bir mesaj alındığında, alınan veri JSON formatına dönüştürülür (JSON.parse(event.data)) ve data değişkenine atanır. Ardından, data consola yazdırılır. _socket nesnesinin onclose olayı dinlenir. Bağlantı beklenmedik bir şekilde kapatıldığında, bir hata mesajı görüntülenir. _socket nesnesinin onerror olayı dinlenir. Bir WebSocket hatası oluştuğunda, hata mesajı görüntülenir ve bağlantı kapatılır. Ardından, 2 saniye sonra tekrar connectWebSocket fonksiyonu çağrılarak yeniden bağlantı sağlanır. Son olarak, connectWebSocket fonksiyonu çağrılarak WebSocket'e bağlanılır. Bu kod, bir WebSocket bağlantısı kurarak backend verilerini alır ve gelen verilere göre işlemler yapar. Aynı zamanda, bağlantı kesintisi durumunda otomatik olarak yeniden bağlanmayı sağlar.
Buraya kadar ekleme ve düzenlemeler sonucunda, projeyi python manage.py runserver diyerek çalıştırdığınızda, tarayıcı konsolunu da açarak http://localhost:8000 adresine gidebilirsiniz.
Tarayıcı -> Network bölümünden websocket bağlantısını görebilirsiniz.
Eğer django server'i durdurup tarayıcı -> Console'dan kontrol gerçekleştirirseniz "web socket beklenmedik şekilde kapandı" hata mesajını görebilirsiniz.
Websocket Üzerinden Frontend'e Veri Göndermek¶
myapp/views.py dosyasını açalım ve index view fonksiyonuna trigger parametresi herhangi bir değer ile gönderildiğinde websockete bağlanmış istemcilere {"data": "pytr.info sitesinden zengin içerikler..."} verisini cevap olarak dönen birkaç ekleme yapalım.
# myapp/views.py
# ...
from .consumers import active_consumers # yeni
def index(request):
city = request.GET.get("city")
if city:
# ...
all = request.GET.get("all")
if all:
# ...
# websocket ile haberleşme yapmak için view üzerinden tetikleme yapacagiz
triggered = request.GET.get("trigger")
if triggered:
for consumer in active_consumers:
consumer.receive(qset={"data": "pytr.info sitesinden zengin içerikler..."})
return JsonResponse({"message":"Tetiklendi!"}, safe=False)
return render(request, "myapp/index.html")
Terminalden curl ile aşağıdaki gibi test edebiliriz. Bir yandan tarayıcınızdan http://localhost:8000 adresine gidin ve tarayıcı console'unu açın.
dev@developers-MacBook-Pro ~ % curl -X GET "http://localhost:8000/?trigger=true"
{"message": "Tetiklendi!"}
dev@developers-MacBook-Pro ~ % curl -X GET "http://localhost:8000/?trigger=true"
{"message": "Tetiklendi!"}
dev@developers-MacBook-Pro ~ % curl -X GET "http://localhost:8000/?trigger=true"
{"message": "Tetiklendi!"}
dev@developers-MacBook-Pro ~ %
alınan data: {data: 'pytr.info sitesinden zengin içerikler...'}
alınan data: {data: 'pytr.info sitesinden zengin içerikler...'}
alınan data: {data: 'pytr.info sitesinden zengin içerikler...'}
Part 5¶
Bu yazıda web socket üzerinden gönderilen şehir ismini veritabanında arayıp silen ve daha sonra tekrar web socket üzerinden frontend tarafına kalan şehirlerin güncel listesini gönderen, tamamen sayfayı yenilemeden sadece şehirlere ait hava durumu kartlarının bulunduğu weather container elementini güncelleyen ekleme ve düzenlemeyi yapacağız.
Önce myapp/index.html içerisinde web socket fonksiyonuna 1 satırlık ekleme yapalım.
// web soket baglantisi olusturmak icin tanimlanan fonksiyon
function connectWebSocket() {
// ...
// backendden veri aliyoruz
_socket.onmessage = function (event) {
const data = JSON.parse(event.data);
// Alinan veriyi isle
console.log("alınan data: ", data);
// socket uzerinden gelen data ile sehirlerin hava durumu cardlarini yeniden olustur.
createElements(data, weatherContainer); // [Yeni]
};
// ...
}
def index(request):
city = request.GET.get("city")
if city:
# ...
all = request.GET.get("all")
if all:
# ...
# websocket ile haberleşme yapmak için view üzerinden tetikleme yapacagiz
triggered = request.GET.get("trigger")
deleted_city = request.GET.get("deleted_city")
if triggered and deleted_city:
# silinecek objeyi bul
obj = Weather.objects.filter(city__iexact=deleted_city)
if obj.exists():
obj.delete()
# butun kayitlari getir
qset = Weather.objects.all()
# serialize ederek consumer'a gondermemiz gerekiyor.
qs_weathers = [
{
"id": obj.id,
"city": obj.city,
"icon": obj.icon,
"temperature": obj.temperature,
"description": obj.description,
"updated_date": obj.updated_date.strftime('%Y-%m-%d %H:%M')
}
for obj in qset
]
for consumer in active_consumers:
consumer.receive(qset=qs_weathers)
return JsonResponse({"message":"Tetiklendi!"}, safe=False)
return render(request, "myapp/index.html")
[
{
"id": 2,
"city": "Malatya",
"icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png",
"temperature": "19.0",
"description": "Clear",
"updated_date": "2023-06-28 04:15"
},
{
"id": 3,
"city": "Istanbul",
"icon": "http://cdn.weatherapi.com/weather/64x64/night/113.png",
"temperature": "21.0",
"description": "Clear",
"updated_date": "2023-06-28 04:15"
}
]
Ekstra
Periyodik çalışan tasklar yazarak(örneğin celery ile) belirli zaman dilimlerinde kayıtlı olan şehirlere ait hava durumu bilgilerini güncelleyen eklemeler yapabilirsiniz.