# 氣候變遷競賽 網頁後端Django架構技術文件
### 以Django做為架構架設網頁後端, 做出符合需求的網頁介面
Django是使用Python語言的網頁架構,採用MTV pattern,可免費快速開發、建立個別app分工、支援SQL資料庫,適合專題快速且可變的網站架設。
MTV pattern (Models-Template-View):
Models : 定義並紀錄資料種類、儲存進SQL資料庫
Template : 簡易HTML模板,這個部分由前端專責負責
View : 資料流的控制與使用者request、response的處理
View中加入使用者登錄與驗證、偽裝使用者攻擊防患、csrf(跨站請求偽造)攻擊防患、使用者id辨識、資料分級管理等等安全性的管理,並且讓使用者只能看到自己擁有的專案。
在整個空污防護網的架構下, 後端負責接收處理完的資料並存至SQL資料庫中, 在不同使用者 (可以是用戶或是專案開發人員) 開啟網路瀏覽器至前端進行request時, 依照不同的用戶權限給予不同的資料, return給前端進行渲染
在未來可能出現多個專案與多個使用者的情況, 所以後端基本上需要配置資料的儲存與提取, 並管理不同使用者的權限, 例如讓用戶收到精簡的解析過的aerobox資料讓人一目了然, 而讓專案人員除了以上資訊還可以收到儀器狀態與使用的情況
詳細Django開發文件及詳細功能請參考以下:
## storyweb架構如下:
> [黑色字為資料夾,藍色字為py檔案]
1. storyweb(含**urls.py**, **setting.py**, **init.py**)
2. **manage.py**(主要控制的py檔案)
3. dataAPI(資料控管與輸出 app)
4. bot(telegram bot app)
5. db.sqlite3(SQL資料庫)
我們待會的結構討論主要分成:
1. storyweb結構討論
2. dataAPI app結構討論
3. bot app結構討論
## 1.storyweb結構討論
### 1.1 storyweb中**urls.py**:
```python
from django.conf.urls import include, url
from django.urls import path
from django.contrib import admin
from dataAPI.views import index, post_detail, dashboard
```
引用**admin**管理後台, **url**功能以及**dataAPI.views**中的數個方法
```python
urlpatterns = [
path('admin/', admin.site.urls),
path('', index),
path('projects/get/<last>/<bd>/<bt>/<ed>/<et>',post_detail),#, name='post_detail'),
path('dashboard/<pk>/<last>/<bd>/<bt>/<ed>/<et>',dashboard),#,name='dashboard'),
path('telegram_bot/',include('bot.urls','bot'))
]
```
建立5個urls:
* "/admin/" : 對應內建後臺管理方法
* "/" : 對應**index**方法,為登錄頁面
* "/projects/get/<last>/<bd>/<bt>/<ed>/<et>" : 對應**post_detail**方法,用以確認使用者身分與其他資料
* "/dashboard/<pk>/<last>/<bd>/<bt>/<ed>/<et>" : 對應**dashboard**方法,呈現json資料予前端
* "/telegram_bot/..." : 後方仍有urls,交給bot.urls接續
### 1.2 storyweb中**setting.py**重要的部分:
```python
# 允許的host(填*則表示皆可)
ALLOWED_HOSTS = ['*']
# 有用到的app(新增app1,aerobox_api)
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'dataAPI',
'bot',
)
# 中繼模組
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 哪個檔案控制urls
ROOT_URLCONF = 'storyweb.urls'
# 資料庫種類(SQL)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
```
## 2. dataAPI app結構討論
### 2.1 dataAPI的models.py
```python
from django.db import models
from django.contrib.auth.models import User
import hashlib
import os
```
從Django內建程式庫引入**models**以及**User**功能
引入**hashlib**, **os** 用以檢測"personal_key",確認登入者為正常使用者而非惡意資安攻擊
```python
def create_key():
return hashlib.md5(os.urandom(32)).hexdigest()
```
create_key方法,每次登入都會產生隨機的32位hash以md5方式隨機加密使用者每次登入的id,以防冒名使用者的資安漏洞
```python
class AeroboxData(models.Model):
pm = models.FloatField(blank=True)
temp = models.FloatField(blank=True)
rh = models.IntegerField(blank=True)
co2 = models.IntegerField(blank=True)
lon = models.FloatField(blank=True)
lat = models.FloatField(blank=True)
time = models.DateTimeField(auto_now_add=True)
```
**AeroboxData**類別, 代表一台Aerobox擁有的資料,具有屬性:
* pm (FloatField)
* temp (FloatField)
* rh (IntegerField)
* co2 (IntegerField)
* lon (FloatField)
* lat (FloatField)
* time (DateTimeField)
```python
class Aerobox(models.Model):
aerobox_id = models.CharField(max_length=100,default=1)
sitename = models.CharField(max_length=100,default=1)
aeroboxdata = models.ManyToManyField(AeroboxData)
```
**Aerobox**類別, 代表一台Aerobox, 具有屬性:
* aerobox_id (CharField)
* sitename (CharField)
* aeroboxdata (以ManyToManyField的方式連結到**AeroboxData**類別)
```python
class ProjectData(models.Model):
pj_name = models.CharField(max_length=100,default=1)
start_time = models.DateTimeField(auto_now_add=True)
end_time = models.DateTimeField(auto_now=True)
aerobox_data = models.ManyToManyField(AeroboxData)
```
**ProjectData**類別, 代表一個project(一個project可能有多台aerobox), 具有屬性:
* pj_name (CharField)
* start_time (DateTimeField)
* end_time (DateTimeField)
* aerobox (以ManyToManyField的方式連結到**Aerobox**類別)
```python
class UserExtension(models.Model):
user = models.ForeignKey(User,on_delete=models.CASCADE)##ForeignKey==onetoone
u_name = models.CharField(max_length=100,default=1)
personal_key = models.CharField(max_length=33,blank=True,default=create_key,unique=True)
projectdata = models.ManyToManyField(ProjectData)
```
**UserExtension**類別, 代表一個使用者,具有屬性:
* user (ForeignKey)一對一關係,連接到Django內建使用者方法
(更改on_delete方法為models.CASCADE以確保User刪除時UserExtension會一併消失)
* u_name (CharField)
* personal_key (CharField)(登入者將獲得一串加密的personal_key,用以識別此登錄者)
* projectdata (以ManyToManyField的方式連結到**ProjectData**類別)
-----------------------------------------------------------
綜上所述的資料層級:
最低層 AeroboxData => Aerobox => ProjectData => UserExtension 最高層
因此我們可以整理資料組織如下:
>UserExtension
>>user
>>u_name
>>personal_key
>>ProjectData
>>>pj_name
>>>start_time
>>>end_time
>>>Aerobox
>>>>aerobox_id
>>>>sitename
>>>>AeroboxData
>>>>>pm
>>>>>temp
>>>>>rh
>>>>>co2
>>>>>lon
>>>>>lat
>>>>>time
其中**UserExtension**<->**ProjectData**的關係以及**ProjectData**<->**Aerobox**的關係均為**ManyToManyField**
### 2.2 dataAPI的views.py
```python
from django.shortcuts import render, redirect
from .models import UserExtension
from django.contrib import auth
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseRedirect
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from datetime import datetime
import pytz
import os
import hashlib
```
* 從django內建的shortcuts中引用**render**, **redirect**用以傳送資料或重新導向
* 從aerobox_api的models中引用**ProjectData**資料
* 從models中引用**UserExtension**資料
* 從django內建的contrib中引用**auth, authenticate, User, login_required**等登錄功能程式集
* 從django內建的http中引用**HttpResponse, HttpResponseRedirect, JsonResponse**用以return Http或是Json形式的資料給前端或是重新將使用者的urls進行導向
* 從django內建的views引用**csrf_exempt**以防止csrf網路攻擊
* 引入datetime計時使用
* 引入**hashlib, os** 檢測personal_key(使用者登入id)
```python
def index(request):
print(request.POST) #print(request.GET.get('a'))
if request.method=="POST":
print("index login success!! ")
username=request.POST.get('username')
password=request.POST.get('password')
user=auth.authenticate(username=username,password=password)
if user and user.is_active:
auth.login(request, user)
user_e, created = UserExtension.objects.get_or_create(user=user)
user_e.personal_key = hashlib.md5(os.urandom(32)).hexdigest()
user_e.save(update_fields=['personal_key']) #change p_k only
return HttpResponseRedirect('/projects/get/'+request.POST.get('last')+'/'\
+request.POST.get('bd')+'/'+request.POST.get('bt')+'/'+request.POST.get('ed')+'/'+request.POST.get('et'))
else:
return HttpResponse("index login did't success!!")
return render(request, 'storyboard.html')
```
定義index方法:
1.若request的method是POST(即輸入帳號密碼及其他資料按登入後),則伺服器端印出"index login success!!",並且將request.POST中的username以及password記錄下來(即登錄者資料)
再用username以及password進行查找符合的使用者,結果傳給user變數
若user為真(此使用者帳號存在)而且並未被凍結則登入
接著以user查找資料庫UserExtension中是否有物件的user屬性=這裡的user
若有=>就傳給user_e
若沒有=>就創建一個UserExtension資料給user_e
接著將user_e的personal_key屬性以hashlib的方式賦值並儲存
最後重新導向"/monitor"
而若user為假(此使用者帳號不存在)或是被凍結則秀出"index login did't success!!!!"
2.若request的方法為"GET"時(即打開login頁面時)給使用者一個簡單的login.html頁面(位於dataAPI裡的template)登錄帳號密碼即可
>此為登入介面
---
```python
@login_required
def post_detail(request,last,bd,bt,ed,et):
username = request.user
user = User.objects.get(username=username)
u=UserExtension.objects.get(user=user)
np={
'name':u.user.username,
'p_k':u.personal_key,
}
s='/dashboard/'+np['p_k']+ '/'+last+ '/'+bd+ '/'+bt+ '/'+ed+ '/'+et
return HttpResponseRedirect(s)
```
定義post_detail方法:
以修飾子"@"執行login_required,使此方法承襲使用者已經登入成功的資料
使用者會輸入帳號密碼(在request中)以及其他篩選資訊
將request中的user屬性給予變數username
再找username=資料庫User資料中username屬性的物件,給予變數user
再找user=資料庫UserExtension資料中user屬性的物件,給予變數u
創建json資料np並給予初始資料:
* 'name'為u(UserExtension物件)中user屬性中的username屬性
* 'p_k'為u中的personal_key屬性
最後重新導向至/dashboard/<p_k>/<last>/<bd>/<bt>/<ed>/<et>
> 此方法只會在背景執行,用p_k用以確認是否為假冒登錄者token的冒名資料盜取
---
```python
@login_required
def dashboard(request,pk,last,bd,bt,ed,et):
print("=======>dashboard okay!")
compare_username = request.user
user = User.objects.get(username=compare_username)
u=UserExtension.objects.get(user=user)
u_list, p_list, a_list, ad_list={},{},{},{}
pn,an,adn=0,0,0
if(UserExtension.objects.filter(personal_key=pk,user__username=compare_username).first()):#if exists()
for p in u.projectdata.all(): #can use:filter get(just one)
for a in p.aerobox.all():
#for ad in a.aeroboxdata.all():
ad=a.aeroboxdata.last()
adp=a.aeroboxdata.filter(rh=90)#QuerySet
#print(":::::",bd,bt,'/',str(ad.time).split()[0],'/',str(ad.time).split()[1][0:5])
#print(";;;;;",bd>str(ad.time).split()[0])
bd=datetime.strptime(bd+bt, '%Y-%m-%d%H:%M')
print(bd,'//',ad.time)
bd.astimezone(pytz.utc)
ad.time.astimezone(pytz.utc)
'''try:
print(ad.time,"====",bd,"====",ad.time>bd)
#print(adp[0].time,"====",bd,"====",adp[0].time>bd)
except IndexError:
print("no adp")'''
ad_list={
'loading time':ad.time,
'lon':ad.lon,
'lat':ad.lat,
'pm':ad.pm,
'temp':ad.temp,
'rh':ad.rh,
'co2':ad.co2,
}
a_list['last:']=ad_list
adn+=1
p_list['aerobox '+a.aerobox_id]=a_list
an+=1
a_list={}
adn=0
u_list['project"'+p.pj_name+'"']=p_list
pn+=1
p_list={}
an=0
return JsonResponse(u_list)
```
定義dashboard方法:
以修飾子"@"執行login_required,意指使此方法承襲使用者已經登入成功的資料
使用跟post_detail方法同樣的方式找到對應的UserExtension物件
若使用request.user中的username以及前端傳來的p_k找的到相應的UserExtension,即該使用者是對的,就執行下方創建並回傳資料的程式:
創建json資料u_list,在經過特定篩選條件(最後筆數、開始與結束時間)後添加資料進入,以上述之資料結構回傳Json資料u_list
> 此頁面以Json資料的形式回傳篩選後該使用者所擁有的資料,可再經過前端的渲染
---
## 3. bot app結構討論
### 3.0 bot.telegram的內部結構
* params.py
>儲存TELEGRAM_URL="https://api.telegram.org/bot" 以及敏感資料:bot的{TOKEN}
* api_models.py
* TelegramObject類別:含有json_deserializer方法
* Update類別:繼承TelegramObject,有update_id、message、callback_query、channel_post屬性
* Message類別:繼承TelegramObject,有message_id=、from_user、date、chat、forward_from、forward_from_chat、reply_to_message、text、forward_from_message_id、forward_date、photo屬性
* ...Chat、User、CallbackQuery類別
>實作telegram官方的資料型態
* api_method.py
* post(data)方法,requests.post到{TELEGRAM_URL}{TOKEN}/sendMessage
* post_photo(data)方法,requests.post到{TELEGRAM_URL}{TOKEN}/sendPhoto
* send_message(chat_id,text)方法,建立json檔案data,
其中"chat_id": chat_id, "text": text, "parse_mode": "Markdown"
並post(data)
* send_photo(chat_id,photo_id)方法,建立json檔案data,
其中"chat_id": chat_id, "photo": photo_id
並post_photo(data)
* ...send_inline_keyboard、send_keyboard、send_html_message方法
### 3.1 bot的urls.py
```python
from django.urls import path
from .views import webhook
from .telegram.params import *
app_name = 'telegram_bot'
urlpatterns = [
path(f'{TOKEN}/',webhook),
# path(f'{TOKEN}/',webhook,name='web_hook'),
]
```
從django.urls引用path
從views引用webhook方法
從.telegram.params引用所有物件
最後指定主urls檔案的/telegram_bot後接續{TOKEN}
此為telegram訊息傳入的urls
### 3.2 bot的models.py
```python
from django.db import models
from django.contrib.auth import get_user_model
import hashlib
import os
User = get_user_model()
class TelegramChat(models.Model):
chat_id = models.CharField(max_length=50,null=False,blank=False,unique=True)
```
引入models模式
從django.contrib.auth引入get_user_model
並用此方法傳遞使用者信息給變數User
最後創建類別TelegramChat,內含chat_id (CharField)
### 3.3 bot的views.py
```python
from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from .telegram.params import *
from .telegram.api_models import Update
from .telegram.api_method import send_message, send_photo#and orters
from .models import TelegramChat
import requests
import json
def webhook(request):
if request.method == "POST":
print('hey bro!')
j_data = json.loads(request.body.decode())
u_data = Update.json_deserializer(j_data)
print('update:',u_data)
if u_data != None:
if u_data.message.text == '/wantjson':
print('/wantjson')
return_json={
'test':'this is test',
'num':123,
'face':':)',
}
send_message(u_data.message.chat_belong_to.id,return_json)
if u_data.message.text == '/get_storyweb':
print('/get_storyweb')
print('u_data.message.photo ',u_data.message.photo)
send_message(u_data.message.chat_belong_to.id,'https://106601015.pythonanywhere.com')
if u_data.message.photo != None:
print('/get_pictest')
print('u_data.message.photo ',u_data.message.photo)
send_photo(u_data.message.chat_belong_to.id, u_data.message.photo[0]['file_id'])
return HttpResponse('ok')
else:
return HttpResponse("This page is for telegram post, but you are getting!")
```
從django.shortcuts引入render
從django.http引入HttpResponse,JsonResponse
從.telegram.params引入所有物件
從.telegram.api_models引入Update類別
從.telegram.api_method引入send_message,send_photo方法
從.models引入TelegramChat類別
引入基本的requests,json
定義webhook方法:
若request.method是POST(即使用者有訊息傳給bot)
則以json.loads方法解析request.body.decode,傳給j_data變數
再以Update類別的json_deserializer方法解析j_data,傳給u_data變數
若u_data(Update型態)不是空的,則將u_data.message.text進行辨識
* 若為"/wantjson":
定義json檔案return_json={'test':'this is test','num':123,'face':':)',}
使用send_message方法傳送(傳入參數u_data.message.chat_belong_to.id及return_json)
==>使用者即可收到return_json
* 若為"/get_storyweb":
使用send_message方法傳送(傳入參數u_data.message.chat_belong_to.id,'https://106601015.pythonanywhere.com')
==>使用者即可收到"https://106601015.pythonanywhere.com"
* 若u_data.message.photo非空(即使用者傳照片過來):
使用send_photo方法傳送(u_data.message.chat_belong_to.id, u_data.message.photo[0]['file_id'])
==>使用者收到本身原本傳過來的照片
---------
以上為後端大致上的整體架構
* Django基礎簡易網站架設
* Django架構分部功能控制並了解機制
* 使用者登入功能
* 資料存取權分流
* 資安-csrf網路攻擊防患
* 資安-冒名盜取資料防患(pk加密)
* personal_key加密方法機制
* git branch團隊協作
* 資料庫migrations在更改models後的更新
(python manage.py migrate --run-syncdb)
* telegram的測試性實作
https://api.telegram.org/bot1220888644:AAH3nfw22d0cFYXI0KPqCugyUCoruwpQcwU/setWebhook?url=https://3a255e04783d.ngrok.io/telegram_bot/1220888644:AAH3nfw22d0cFYXI0KPqCugyUCoruwpQcwU/