edx笔记系统是很有意思的一个话题

架构

  • 前端
  • 后端
  • 通信方式:RESTful接口

由此可知笔记可以作为一项服务

前端

笔记库:annotator

edx中的依赖

edx中业务逻辑

后端

已废弃

edx-notes-api

lms

其他

RESTful接口

通信相关

在LMS中记条笔记的来龙去脉

在lms里做笔记,观察ajax请求,可以看到笔记以POST方式发往:http://NOTESERVER/api/v1/annotations/

header中带有x-annotator-auth-tokenX-CSRFToken

x-annotator-auth-token实际上是一个JWT编码的串,放到jwt.io解码完,可看到结果形如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "sub": "35774bce800db0e8a76980d9c332df73",
  "administrator": true,
  "name": "ww",
  "exp": 1464079917,
  "iss": "http://LMS/oauth2",
  "iat": 1464079887,
  "preferred_username": "ww",
  "email": "ww@qq.com",
  "aud": "edx-notes-id"
}

代码见get_token

上边链接所在的模块就是lms与NOTESERVER通信的模块,通信的url入口为get_internal_endpoint

新技能

这里边有个值得注意的地方是:get_id_token,这个工具是通用的,如果想用jwt来发送受信信息的话

通过JWT,我们可以保证数据是被签名过可信任的。

值得注意的还有当前这部分并不完善,许多地方还是硬编码的,诸如:CLIENT_NAME


观察网络面板,可以看到,发送的数据形如

1
{"ranges":[{"start":"/div[1]/p[2]","startOffset":8,"end":"/div[1]/p[2]","endOffset":18}],"quote":"你可以观看介绍视频","text":"课程介绍","tags":["fun"],"user":"35774bce800db0e8a76980d9c332df73","usage_id":"block-v1:edX+test+2014_T2+type@html+block@af9f16e4a4704f4ab52bc90e5280ba18","course_id":"course-v1:edX+test+2014_T2"}

相关的前端模型定义在edxnotes/models/note.js

前端note工厂为edxnotes/views/notes_factory.js

user是不是采用了什么编码/加密(和edx的匿名用户id一样吗?)

对此有兴趣的同学可以参考common/templates/edxnotes_wrapper.html,其中关键部分为:

1
2
from student.models import anonymous_id_for_user
params.update({'user': anonymous_id_for_user(user, None)})

猜测和edx成绩单中匿名用户的机制一样

出于好奇,追踪到底:anonymous_id_for_user,至此我们揭开了匿名ip的全部谜团

  • Return a unique id for a (user, course) pair
  • If user is an AnonymousUser, returns None

核心源码为

1
2
3
4
5
6
hasher = hashlib.md5()
hasher.update(settings.SECRET_KEY)
hasher.update(unicode(user.id))
if course_id:
    hasher.update(course_id.to_deprecated_string().encode('utf-8'))
digest = hasher.hexdigest()

由此可知,登录用户的anonymous_id是唯一的,使其具有唯一性的影响因素包括:

  • settings.SECRET_KEY(站点级别)
  • course_id
  • user.id

心满意足收场

搜索笔记

我们在lms的笔记汇总页面http://LMS/courses/course-v1:edX+DemoX+Demo_Course/edxnotes/

搜索笔记时,发出的http(ajax)请求为http://LMS/courses/course-v1:edX+DemoX+Demo_Course/edxnotes/search/?text=hello

实际上,这个请求实际上是代理了edx-notes-api。

edx-notes-api

我们使用httpie来向edx-notes-api请求笔记数据

http://NOTESERVER/api/v1/search/?user=106ecd878f4148a5cabb6bbb0979b730&usage_id=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40html%2Bblock%4082d599b014b246c7a9b5dfc750dc08a9&course_id=course-v1%3AedX%2BDemoX%2BDemo_Course

url解码完为http://NOTESERVER/api/v1/search/?user=106ecd878f4148a5cabb6bbb0979b730&usage_id=block-v1:edX+DemoX+Demo_Course+type@html+block@82d599b014b246c7a9b5dfc750dc08a9&course_id=course-v1:edX+DemoX+Demo_Course

我们使用httpie测试

http “http://NOTESERVER/api/v1/search/?user=106ecd878f4148a5cabb6bbb0979b730&usage_id=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40html%2Bblock%4082d599b014b246c7a9b5dfc750dc08a9&course_id=course-v1%3AedX%2BDemoX%2BDemo_Course” (注意要url编码!)

得到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
    "rows": [
        {
            "course_id": "course-v1:edX+DemoX+Demo_Course",
            "created": "2016-05-12T03:38:24.565900+00:00",
            "id": "2",
            "quote": "nks above. At edX",
            "ranges": [
                {
                    "end": "/div[1]/p[2]",
                    "endOffset": 206,
                    "start": "/div[1]/p[2]",
                    "startOffset": 188
                }
            ],
            "tags": [
                "tag"
            ],
            "text": "df",
            "updated": "2016-05-12T03:38:24.566048+00:00",
            "usage_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@82d599b014b246c7a9b5dfc750dc08a9",
            "user": "106ecd878f4148a5cabb6bbb0979b730"
        },
        {
            "course_id": "course-v1:edX+DemoX+Demo_Course",
            "created": "2016-05-12T03:09:01.496272+00:00",
            "id": "1",
            "quote": "to",
            "ranges": [
                {
                    "end": "/div[1]/p[1]",
                    "endOffset": 10,
                    "start": "/div[1]/p[1]",
                    "startOffset": 8
                }
            ],
            "tags": [],
            "text": "a",
            "updated": "2016-05-12T03:09:01.496376+00:00",
            "usage_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@82d599b014b246c7a9b5dfc750dc08a9",
            "user": "106ecd878f4148a5cabb6bbb0979b730"
        }
    ],
    "total": 2
}

注意其中的ranges属性,这是定位的关键,end和start是xpath,tags属性用于存放标签,usage_id是课程内容的定位符,user是用户匿名化后的结果

架构设计

edx的架构总体而言是采用RESTful api来解耦,笔记模块也不例外

edx的笔记前端模块采用的是openannotation开放出来的annotator,openannotation同时也开放了后端,edX没有采用他们的后端,而是使用django-rest-framework重写了笔记部分的后端,我想应该是出于一致性的考虑,这样一来,整个团队的技术栈是统一的,个人而言我觉得这是个明智的决定,尽管这可能浪费一些时间(这确实浪费了不少时间,要知道笔记系统在birch版本中就准备投入使用的!)。

我之所以会赞同这种做法,可能是因为对论坛模块的恐惧吧,论坛采用ruby写的,想对此做定制优化,就显得艰难(事实证明论坛也的确不够稳定)

保持技术栈的一致,有利于让团队成员更热衷维护它们

如果你有兴趣进一步看笔记的模型,请阅读notesapi/v1/models.py

总结

edx的笔记系统可以单独部署,只要做好oauth2的配置即可

由于笔记系统是基于oauth2的,所以可以为手机端所用,同时如果我们将笔记系统视为一个服务,我们可以将它用在edx之外的网站或app里

后话

我对阅读和笔记偏好有加,想基于以下东西做个工具

  • annotator
  • firebase
  • chrome插件
    • 参考划词翻译或者pocket

一个随手摘工具,同时能高亮做过笔记的网页