本文是 Django 实验平台系列教程的第三篇。阅读本文前需要你先阅读前两篇文章。
完成前两篇文章的步骤之后,你的手上应该有一个可以 Hello World 的 onlineExperimentTutorial
。我们在这个基础上研究下面的流程。
0 基本流程和概念
在上一篇文章的末尾,其实我们已经把这个系统需要的核心功能梳理出来了:
- 如何让实验程序发出数据?
- 如何建立并管理 Django 模型?
- Django 视图如何接收实验程序的数据?如何处理这些数据?如何储存这些数据?
到这里,需要引入一些基本的概念,我们来一条一条解决。
0.1 网站如何和你的客户端交换数据?
隆重介绍:HTTP 协议。没错,就是你在浏览器里看到的这个 HTTP。它是你和网站交互时沟通信息的指定方法。它的全称叫做“超文本传输协议(Hyper Text Transfer Protocol)”。它所传输的内容,也就是“超文本”——基本上仍然是文本,但不只是文本。
如果拥有 jsPsych 的基础的话,你已经知道网页实验程序的本质是什么了——一大段一大段的文本、代码。这些代码通过 HTTP 协议发送到你的浏览器里,浏览器解析这些文本,并加载对应的内容,就变成了你所看到的网页。想要更进一步了解 HTTP 的相关内容的话,可以移步HTTP 教程 | 菜鸟教程。
我们稍微快进一点。我们的目标是“让实验程序发出数据”。发出数据应该怎么做呢?
HTTP 协议规定了五种标准的方法(method),分别如下:
- GET:请求从服务器获取指定资源。这是最常用的方法,用于访问页面。
- POST:请求服务器接受并处理请求体中的数据,通常用于表单提交。
- PUT:请求服务器存储一个资源,并用请求体中的内容替换目标资源的所有内容。
- DELETE:请求服务器删除指定的资源。
- HEAD:与 GET 类似,但不获取资源的内容,只获取响应头信息。
通常访问网页时,我们的浏览器发出的是 GET 请求。那么上传一段数据应该用什么请求呢?
答案是 POST 请求。注意,并不是 PUT,PUT 的作用是上传一个文件。虽然大家眼中的实验数据确实是以文件形式储存的,但我们在后续的操作中并不会手动去上传一个文件——我们会让实验程序直接把数据发送到服务端,再让服务端对数据做处理。(而且在现在实际的应用中,PUT 请求真的已经很少用了。)
0.2 Django 如何管理数据?
上一篇文章提过,Django 的架构是这样的:
所以,很自然地,我们需要去模型里面处理数据。我们先试试如何建立一个模型。
0.2.1 如何正确地创建一个模型?
首先,我们要在 Django 项目里新建一个应用(app)。进入这个项目对应的终端(请回顾前面的教程),然后运行以下命令:
django-admin startapp myapp
应用是 Django 里的一种功能单元,你可以把不同的功能写在不同的 app 里,这对于日后维护代码有很大的帮助。创建成功后,你会在目录里看到这样的一个东西:
注意,models 必须在 app 内调用,不能在项目的根目录调用。打开 models.py。
让我们来随便写点什么:
from django.db import models
# Create your models here.
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
我们创建了一个叫做 Person
的模型,并指定了两个变量:first_name
和 last_name
。
0.2.2 注册并执行迁移
接下来,我们要怎么让 Django 知道这个模型的存在呢?为了在 Django 中注册这个模型,我们需要首先注册我们刚刚生成的应用。
回到 \onlineExperimentTutorial\
下面,找到这个叫 settings.py
的文件。这个文件管理的是整个项目的设置。打开它,看到大约 30 行左右的位置,找到 INSTALLED_APPS
这一项:
接下来,我们还要执行一个步骤:迁移。回到终端,运行以下命令:
python manage.py makemigrations
python manage.py migrate
这样的话,Django 就知道了这一个模型的存在,并在系统里注册好了这个模型。之后每次创建新的 app 或新的 model 时,你都需要执行一遍这个流程。
这里究竟发生了什么?如果你想要知道这个问题的答案的话,这是一个更深层次的解释,但你也可以跳过这一段。
Django 本体程序其实并不能单独存在,其一定需要一个数据库后端来支撑各项功能(它自己本身也是有很多数据需要储存的!不只是我们让它储存的那些)。只不过当你新建一个 Django 项目时,它会自动创建一个 SQLite 数据库,也就是你会看见的
db.sqlite3
的那个文件。这个文件由 Django 自己来驱动,不需要额外的应用程序。当你新建一个模型之后,Django 需要告诉数据库:我们新增了一个数据模型(在数据库的术语中,被称作“表”),随后让数据库新建用于储存这个模型的数据表。这也就是迁移的目的——让 Django 把我们对代码的最新改动反映到数据库里。这个过程通常不需要我们自己操心,Django 可以自行完成。
在互联网领域,有各种各样的数据库驱动程序(比如 MySQL 等等)。你完全可以切换各种各样的数据库(只要 Django 支持),它们在性能上可能会有差别,但在功能上都是一样的,以便于管理和储存的方式来保存数据。
0.2.3 如何管理这个模型?
模型是确实创建好了,但它目前对我们是看不见摸不着的。我们要怎么管理这个模型呢?
我们需要用到 Django 自带的 Admin 系统。这是 Django 网站的后台组件,默认预装在了你的项目里。
在刚刚我们新建的 \myapp\
下面,有一个 admin.py
的文件。
我们把刚刚注册好的模型放进去:
from django.contrib import admin
from .models import Person # First import our model
# Register your models here.
admin.site.register(Person) # Then register it in the admin panel
接下来,我们运行这个项目(终端执行 python manage.py runserver
),然后访问 127.0.0.1:8000/admin
,打开后台面板
哦等下,我们还没注册管理员账户。
回到终端(你可能需要先退出 Django 的运行),输入这个指令创建一个超级用户。
python manage.py createsuperuser
指定好必要的信息(用户名、密码等等)之后,你就可以登录了。
你可以在管理面板里看到我们刚刚新建的 myapp
和它下面的 Persons
……哎等下,怎么多了个复数?
嗯,Django 是这样的,在这个面板里命名都会加一个 s,正常现象。你也可以在模型的 Meta 属性中指定它的名字,相关说明请参考官方教程。
总之你已经看到这个数据的储存位置了!你也可以点开看看,往里面新增或者修改一些数据,试试这个管理面板的功能。
1 新增 Django 视图和模型
铺垫到此结束,让我们接下来看看怎么开始整合实验程序和 Django。
在 Django 部分,我们需要一个视图用来接收数据、新建一个模型用来储存数据,并在视图里编写代码把数据放到数据库里。
1.1 编写视图和模型
我们先来新建一个 app,用来承载我们的视图和模型。
django-admin startapp simpleExperimentData
分别编辑 views.py
、models.py
、admin.py
如下:
1.1.1 views.py
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
import json
from .models import JsonData
@csrf_exempt
def handle_json_upload(request):
if request.method == 'POST': # 规定方式为 POST
try:
json_data = json.loads(request.body) # 直接解析JSON数据
JsonData.objects.create(json_data=json_data) # 将JSON数据存储到模型中
return JsonResponse({'status': 'success'})
except json.JSONDecodeError:
return JsonResponse({'status': 'error', 'message': 'Invalid JSON data'}) # 无法解析时报错
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}) # 其他错误
else:
return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) # 报错,不接受其他方式传入
这是一个简单的视图,规定了只接受方法为 POST 的请求,并将请求体中的数据解析后保存到 JsonData
模型中。为了方便调试,我们还加入了一点点简单的报错。我们在这里指定的数据类型为 JSON,因此后续发送数据时应当发送 JSON 格式的数据。
注意到我们在编写这个视图时使用了
@csrf_exempt
这一装饰器。这个装饰器表示下面的视图不进行任何的跨站攻击保护。如果只用于开发环境,或者短时间的数据收集,这样或许没有太大的风险。但如果要长期运转网站的话,你需要学习 Django 中的防跨站攻击保护(CSRF)相关的内容。这一部分我们会在下一篇教程中有所涉及。
1.1.2 models.py
from django.db import models
class JsonData(models.Model):
json_data = models.JSONField()
class Meta:
verbose_name = "JSON Data TS1.1"
verbose_name_plural = "JSON Data TS1.1"
我们创建了一个与之对应的 model,JsonData
,并储存了一个名为 json_data
的字段。同时我们还指定了它在面板中显示的名字。
1.1.3 admin.py
from django.contrib import admin
from .models import JsonData
admin.site.register(JsonData)
再在管理面板中注册相关模型即可。
1.1.4 注册与迁移
和前面一样,前往项目的 settings.py
,添加 app:
INSTALLED_APPS = [
"django.contrib.admin",
# ...
"myapp",
"simpleExperimentData" # <<<<
]
接下来进行迁移:python manage.py makemigrations
,python manage.py migrate
。
1.2 添加路由
为了让我们能访问这个视图,我们需要添加一个指向这个视图的 URL(还记得教程 1 里的 Hello World 吗?)。
在我们刚刚新建的 \simpleExperimentData\
下新建一个 urls.py
:
from django.urls import path
from .views import handle_json_upload
urlpatterns = [
path('data_upload/', handle_json_upload, name='handle_csv_upload'),
]
然后,修改该项目的主路由文件 urls.py
:
from django.contrib import admin
from django.urls import include, path # <<<< import include
from . import views
urlpatterns = [
path("admin/", admin.site.urls),
path("hello/", views.hello, name="hello"),
path("simpleExp/", include('simpleExperimentData.urls')) # <<<< 加上我们刚刚的 url 配置。
]
接下来,保存,运行一下试试?
你可以访问一下127.0.0.1:8000/simpleExp/data_upload/
,看看会发生什么?
对的!报错了,但报的是我们刚刚在视图里定义的错误!这说明我们的视图已经正常运转起来了!
2 编辑实验文件
假设你已经拥有了一个 jsPsych 的实验程序,并且你已经本地测试过它能正常运行了。如果你使用的是 PsychoPy 转写的 PsychoJS 的话……我确实好长时间没接触过了,你可能需要自己稍微研究一下,变通一下相关的代码,但基本的思路是一致的。
2.0 实验的数据在哪?
在浏览器里。准确地说,在浏览器的内存里。我们可以从内存里将数据导出来。
如果你还不太清楚数据储存在什么位置,先打开一个你手边有的 jsPsych 程序,做一会,然后打开控制台,输入下面的命令:
jspsych.data.get()
可以看到,这个数据是以 JavaScript Object 的形式保存在浏览器中。我们可以把它变换成其他易于存取的格式:
jspsych.data.get().json() // 转化为 JSON
jspsych.data.get().csv() // 转化为 CSV
2.1 将实验文件放进 Django 里面
我们的实验文件:.html
、.css
、.js
,以及其他你可能会用到的资源(图片、音频、视频等等),他们都是网站上的静态资源。要让 Django 服务器正确地分发静态资源文件,你就需要正确地配置相关设置。
注意,以下方法适合快速部署,但不适合成规模地部署实验。
正常情况下,我们应当考虑让 Django 的视图和模板来提供网页,然后加载
.js
和.css
文件,这样的话整个流程都在 Django 可以管理的范围内。但,作为一个简易快速教程,我们将把实验程序用到的
.html
资源作为完全静态的资源来托管,这也就意味着 Django 无法管理这个文件,无法在这个文件里加入 Django 特有的功能。如果你需要成规模地部署网页实验,请考虑使用视图和相应的模板来正确地部署
.html
文件。
2.1.1 修改 settings.py
在 settings.py
的大约 110 行左右,你会看到一个设置:
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "static/"
这表示了所有静态文件的 URL 路径,指示 Django 在这里去找对应的文件。
接下来,我们去创建一下实验文件。在刚刚的 \simpleExperimentData\
下,让我们创建一个 static
文件夹,再把实验文件整体放进去。
2.1.2 修改 urls.py
接下来,我们要让 Django 提供这些文件。我们需要修改项目的 urls.py
:
from django.conf import settings
from django.conf.urls.static import static # <<<< 在头部新增两个 include
urlpatterns = [
# ... the rest of your URLconf goes here ...
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # <<<< 通过这种方式提供静态文件的 URL
重启一下服务端,访问 127.0.0.1:8000/static/simple_experiment/index.html
,你应该就成功打开了你的实验文件了。
注意,这种提供文件的方式仅适用于开发环境,Django 标记此种方式为不安全的文件提供方式。如果你要把实验部署在服务器上,你应当正确配置一个反代服务器。我们会在下一篇教程中提到相关的问题。
2.2 使用 POST 请求发送数据
接下来我们开始上手编辑实验文件。前面我们设置了在 127.0.0.1:8000/simpleExp/data_upload/
这个地址接收传入的数据。
从思路上来说,在不考虑特殊状况的情况下,我们有两种路径在实验结束时发送数据:
- 通过
call-function
插件,在实验末尾增加一个特殊的 trial,发送数据; - 在初始化 jsPsych 对象时,使用
on_finish
参数,编辑自定义函数发送数据。
本文通过第二种路径来实现相关功能。
在实验文件(.js
)中,找到 jsPsych 初始化的部分,写入以下函数:
// 自定义数据保存函数
function saveData() {
var xhr = new XMLHttpRequest(); // 通过 XHR 构造请求
xhr.open('POST', '/simpleExp/data_upload/'); // 向指定接口发送 POST 请求,接口在前面编辑视图时定义
xhr.setRequestHeader('Content-Type', 'application/json'); // 指定请求标头,告诉接口发送的数据类型
xhr.onload = function() {
if(xhr.status == 200){
var response = JSON.parse(xhr.responseText); // 如果接口有响应,则发送数据
console.log(response.status); // 接受返回值并输出
}
};
xhr.send(jsPsych.data.get().json()); // 将实验产生的 JSON 格式数据发出
}
//初始化jsPsych,本文以 jsPsych 7.x 为例
let jsPsych = initJsPsych({
on_finish: function () { // 添加 on_finish 参数,在实验结束时执行 saveData()
saveData()
}
});
保存,运行实验并把实验做完(记得在运行实验的同时打开 F12)。如果数据发送成功了,那会有这样一条提示:
我们还可以进后台看看数据有没有正常保存下来:
2.3 常见问题排查
考虑到网站开发是个复杂到要命的东西,这里简要讲述本文里操作时会遇到的几种常见问题。
2.3.1 JS 里写好了保存数据的代码,但是打开网页之后没有发送数据
检查 F12 中的 Source
(源代码/来源),找到你的实验文件,看看和你刚刚保存的一不一样。不一样的话是有缓存,使用 Ctrl + Shift + R 强制刷新。下一篇文章会简单介绍如何要求客户端不保留缓存,强制加载最新版网页。
2.3.2 Django 找不到静态文件
检查 STATIC_URL
配置是否正确,urls.py
是否添加了检测静态文件的部分。
2.3.3 保存数据时出现了 CSRF 相关的东西并报错,导致保存失败
出于开发目的,在本文的视角下,请保证视图装饰器为 @csrf_exempt
。我们会在下文中处理这个问题,使用标准的安全保护方式来加载文件。
3 接下来还要做什么?
到这里,你已经成功搭好了一个可以在本地运行,收集数据并保存到数据库的程序了!我们在前面所设定的目标已经全部完成了,恭喜你!
那么我们下面还要做什么呢?
或者说,你想要试试上服务器运行吗?
下一篇教程会介绍如何将我们的这个项目以一个正确的方式部署到云服务器上,并向网络开放,收集数据。