---
# System prepended metadata

title: 氣候變遷競賽 網頁後端Django架構技術文件

---

# 氣候變遷競賽 網頁後端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/