Add complete family graph and private family chronicle
This commit is contained in:
+53
-4
@@ -2,6 +2,7 @@
|
||||
import datetime as dt
|
||||
import json
|
||||
import secrets
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
class Family:
|
||||
@@ -13,6 +14,7 @@ class Family:
|
||||
self.a.DB.executescript('''
|
||||
CREATE TABLE IF NOT EXISTS people (id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS family_links (id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS family_events (id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
||||
''')
|
||||
self.a.DB.commit()
|
||||
|
||||
@@ -23,9 +25,12 @@ class Family:
|
||||
def snapshot(self, user):
|
||||
self.authorize(user)
|
||||
with self.a.LOCK:
|
||||
can_edit = user['role'] == 'admin' or user.get('familyAccess') == 'edit'
|
||||
return {'revision': self.a.setting('familyRevision', 0), 'people': self.a.objects('people'),
|
||||
'links': self.a.objects('family_links'),
|
||||
'canEdit': user['role'] == 'admin' or user.get('familyAccess') == 'edit',
|
||||
'events': [e for e in self.a.objects('family_events')
|
||||
if can_edit or (e.get('status') == 'published' and not e.get('archived'))],
|
||||
'canEdit': can_edit,
|
||||
'selfId': user.get('personId', '')}
|
||||
|
||||
def save(self, kind, data, user):
|
||||
@@ -35,19 +40,63 @@ class Family:
|
||||
revision = a.setting('familyRevision', 0)
|
||||
if type(data.get('revision')) is not int or data['revision'] != revision:
|
||||
raise a.Problem('家谱已被更新,请重新载入后再编辑', 409)
|
||||
table = 'people' if kind == 'person' else 'family_links'
|
||||
if kind not in ('person', 'link', 'event'):
|
||||
raise a.Problem('家谱记录类型不存在', 404)
|
||||
table = {'person': 'people', 'link': 'family_links', 'event': 'family_events'}[kind]
|
||||
old = a.get_object(table, data.get('id'))
|
||||
if data.get('id') and not old:
|
||||
raise a.Problem('家谱记录不存在', 404)
|
||||
item = self.person(data, old) if kind == 'person' else self.link(data, old)
|
||||
item = {'person': self.person, 'link': self.link, 'event': self.event}[kind](data, old)
|
||||
item.update(updatedAt=dt.datetime.now(dt.timezone.utc).isoformat(), updatedBy=user['username'])
|
||||
if kind == 'event':
|
||||
item.update(createdAt=old.get('createdAt', item['updatedAt']) if old else item['updatedAt'],
|
||||
createdBy=old.get('createdBy', user['username']) if old else user['username'])
|
||||
if item['status'] == 'published':
|
||||
item['publishedAt'] = old.get('publishedAt', item['updatedAt']) if old else item['updatedAt']
|
||||
# Revision and record are committed together, so concurrent forms cannot overwrite silently.
|
||||
a.DB.execute('INSERT OR REPLACE INTO ' + table + ' VALUES (?,?)', (item['id'], json.dumps(item)))
|
||||
a.DB.execute('INSERT OR REPLACE INTO settings VALUES (?,?)', ('familyRevision', json.dumps(revision + 1)))
|
||||
a.DB.commit()
|
||||
a.audit('更新家谱人物' if kind == 'person' else '更新家谱关系', user['username'])
|
||||
a.audit({'person': '更新家谱人物', 'link': '更新家谱关系', 'event': '更新家族记事'}[kind], user['username'])
|
||||
return {'item': item, 'revision': revision + 1}
|
||||
|
||||
def event(self, data, old):
|
||||
a = self.a
|
||||
item = {'id': old['id'] if old else secrets.token_hex(8)}
|
||||
for key, maximum, required in [('title', 160, True), ('eventDate', 10, True),
|
||||
('summary', 400, False), ('sourceName', 160, False),
|
||||
('sourceUrl', 2000, False)]:
|
||||
item[key] = a.clean_text(data.get(key, ''), maximum, required)
|
||||
try:
|
||||
if dt.date.fromisoformat(item['eventDate']).isoformat() != item['eventDate']:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
raise a.Problem('记事日期请填写有效日期,格式为 YYYY-MM-DD')
|
||||
body = data.get('body', '')
|
||||
if not isinstance(body, str) or len(body) > 12000 or any(ord(c) < 32 and c not in '\r\n\t' for c in body):
|
||||
raise a.Problem('记事正文格式不正确,最多 12000 字')
|
||||
item['body'] = body.strip()
|
||||
item['personIds'] = data.get('personIds', [])
|
||||
if (not isinstance(item['personIds'], list) or len(item['personIds']) > 200
|
||||
or any(not isinstance(p, str) or not a.get_object('people', p) for p in item['personIds'])):
|
||||
raise a.Problem('请选择有效的关联人物,最多 200 位')
|
||||
item['personIds'] = list(dict.fromkeys(item['personIds']))
|
||||
item['status'] = data.get('status', 'draft')
|
||||
item['archived'] = data.get('archived', old.get('archived', False) if old else False)
|
||||
if item['status'] not in ('draft', 'published') or not isinstance(item['archived'], bool):
|
||||
raise a.Problem('记事状态不正确')
|
||||
if item['sourceUrl']:
|
||||
try:
|
||||
parsed = urlsplit(item['sourceUrl'])
|
||||
if parsed.scheme not in ('https', 'http') or not parsed.hostname or parsed.username or parsed.password:
|
||||
raise ValueError()
|
||||
parsed.port
|
||||
except ValueError:
|
||||
raise a.Problem('报道链接请填写完整的 http 或 https 地址,不可包含账号密码')
|
||||
if item['status'] == 'published' and not (item['body'] or item['summary'] or item['sourceUrl']):
|
||||
raise a.Problem('发布前请填写记事正文、摘要或报道链接')
|
||||
return item
|
||||
|
||||
def person(self, data, old):
|
||||
a = self.a
|
||||
item = {'id': old['id'] if old else secrets.token_hex(8)}
|
||||
|
||||
Reference in New Issue
Block a user