feat: local Bootstrap Vue ECharts dashboards and universal genealogy write scope

This commit is contained in:
Codex
2026-10-08 22:41:19 +08:00
parent d8826152c6
commit fb87ae4de9
33 changed files with 980 additions and 110 deletions
+84 -4
View File
@@ -3,6 +3,7 @@ import geography
import datetime as dt
import json
import secrets
import family_permissions as permissions
from urllib.parse import urlsplit
@@ -34,14 +35,59 @@ class Family:
with self.a.LOCK:
can_edit = self.a.HOUSEHOLDS.manager(user) or user.get('familyAccess') == 'edit'
people = self.a.HOUSEHOLDS.objects(user, 'people')
links = self.a.HOUSEHOLDS.objects(user, 'family_links')
scope = self.edit_scope(user, people, links)
return {'revision': self.a.setting(self.a.HOUSEHOLDS.revision_key(user), 0), 'people': people,
'links': self.a.HOUSEHOLDS.objects(user, 'family_links'),
'links': links,
'events': [e for e in self.a.HOUSEHOLDS.objects(user, 'family_events')
if can_edit or (e.get('status') == 'published' and not e.get('archived'))],
'canEdit': can_edit,
'selfId': user.get('personId', '') if any(p['id'] == user.get('personId') for p in people) else '',
'canEdit': can_edit and bool(scope['levels']) and not scope['conflict'],
'editScope': scope,
'selfId': scope['selfId'],
'familyId': self.a.HOUSEHOLDS.scope(user)}
def edit_scope(self, user, people=None, links=None):
a = self.a
people = a.HOUSEHOLDS.objects(user, 'people') if people is None else people
links = a.HOUSEHOLDS.objects(user, 'family_links') if links is None else links
# Always use the account's binding, never a selected graph reference or submitted ID.
account = a.get_object('users', user.get('id')) if user.get('id') else None
self_id = (account or user).get('personId', '')
levels, conflict = permissions.generations(people, links, self_id)
granted = a.HOUSEHOLDS.manager(user) or user.get('familyAccess') == 'edit'
editable = [key for key in levels if permissions.within(levels, key)] if granted and not conflict else []
note = ('族谱维护限本人上下各五代,所有角色(含超级管理员)相同;远代仍可按权限查看。'
if levels else '尚未关联本家庭族谱中的本人,暂不能维护。请在成员与权限中关联本人。')
if conflict:
note = '亲属关系存在代际冲突,无法可靠确定五代范围,已暂时锁定维护。'
can_start = bool(granted and account and not account.get('personId')
and a.HOUSEHOLDS.family_of(account) == a.HOUSEHOLDS.scope(user))
return dict(limit=permissions.LIMIT, selfId=self_id if levels else '', levels=levels,
conflict=conflict, editablePersonIds=editable, canCreateSelf=can_start, note=note)
def check_write(self, user, kind, item, old=None, proposed_people=None, proposed_links=None):
scope = self.edit_scope(user)
if not scope['levels'] or scope['conflict']:
raise self.a.Problem(scope['note'], 403)
levels = scope['levels']
if old and any(not permissions.within(levels, key) for key in permissions.targets(kind, old)):
raise self.a.Problem('该资料超出本人上下五代维护范围,所有角色均只能查看', 403)
if (kind == 'person' and old is not None) or kind == 'event':
if any(not permissions.within(levels, key) for key in permissions.targets(kind, item)):
raise self.a.Problem('只能维护本人上下五代内且已连接的亲属资料', 403)
if proposed_links is not None:
next_levels, conflict = permissions.generations(proposed_people, proposed_links, scope['selfId'])
if conflict:
raise self.a.Problem('这条关系会产生代际冲突,不能据此确定维护范围', 409)
# Prevent moving/disconnecting an existing branch and reattaching it closer
# to oneself to circumvent the five-generation rule.
if any(next_levels.get(key) != value for key, value in levels.items()):
raise self.a.Problem('不能改变已确认人物的代际位置或断开其世系;请先补充同代的正确关系', 403)
if any(not permissions.within(next_levels, key) for key in permissions.targets(kind, item)):
raise self.a.Problem('新增关系或人物超出本人上下五代维护范围', 403)
if kind == 'link' and not any(permissions.within(levels, key) for key in permissions.targets(kind, item)):
raise self.a.Problem('请从本人上下五代内的已知亲属连接关系', 403)
def save(self, kind, data, user, restored_from=None):
self.authorize(user, True)
a = self.a
@@ -63,6 +109,32 @@ class Family:
raise a.Problem('家谱记录不存在', 404)
item = {'person': self.person, 'link': self.link, 'event': self.event}[kind](data, old)
item.update(familyId=family_id, updatedAt=dt.datetime.now(dt.timezone.utc).isoformat(), updatedBy=user['username'])
added_link = None
bind_self = data.get('asSelf', False)
if not isinstance(bind_self, bool):
raise a.Problem('本人关联设置不正确')
if bind_self and (kind != 'person' or old or not self.edit_scope(user)['canCreateSelf']):
raise a.Problem('已有本人关联,不能用另一个人物切换维护范围', 403)
people = a.HOUSEHOLDS.objects(user, 'people')
links = a.HOUSEHOLDS.objects(user, 'family_links')
if kind == 'person' and not old and not bind_self:
relative = data.get('relativeId')
a.HOUSEHOLDS.owns(user, a.get_object('people', relative))
relation = data.get('relativeKind')
if relation not in ('parent', 'child', 'spouse'):
raise a.Problem('新增人物时请选择与已有亲属的关系')
if relative not in self.edit_scope(user)['editablePersonIds']:
raise a.Problem('请选择本人上下五代内可维护的亲属', 403)
added_link = dict(id=secrets.token_hex(8), fromId=item['id'] if relation == 'parent' else relative,
toId=relative if relation == 'parent' else item['id'],
kind='spouse' if relation == 'spouse' else 'parent', active=True,
lineage='unspecified', note='', familyId=family_id,
updatedAt=item['updatedAt'], updatedBy=user['username'])
self.check_write(user, kind, item, proposed_people=people+[item], proposed_links=links+[added_link])
elif kind == 'link':
self.check_write(user, kind, item, old, people, [e for e in links if e['id'] != item['id']]+[item])
elif not bind_self:
self.check_write(user, kind, item, old)
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'])
@@ -79,8 +151,16 @@ class Family:
a.DB.execute('INSERT INTO family_changes VALUES (?,?,?,?,?,?,?,?)',
(change_revision, kind, item['id'], json.dumps(old) if old else None,
json.dumps(item), user['username'], item['updatedAt'], restored_from))
if added_link:
a.DB.execute('INSERT INTO family_links VALUES (?,?)', (added_link['id'], json.dumps(added_link)))
a.DB.execute('INSERT INTO family_changes VALUES (?,?,?,?,?,?,?,?)',
(change_revision+1, 'link', added_link['id'], None, json.dumps(added_link),
user['username'], item['updatedAt'], None))
if bind_self:
account = dict(a.get_object('users', user['id']), personId=item['id'])
a.DB.execute('UPDATE users SET body=? WHERE id=?', (json.dumps(account), user['id']))
a.audit({'person': '更新家谱人物', 'link': '更新家谱关系', 'event': '更新家族记事'}[kind], user['username'])
return {'item': item, 'revision': revision + 1}
return {'item': item, 'revision': revision + 1, 'changeRevision': change_revision}
def history(self, user, kind, target_id, before=None):
# Previous drafts and withdrawn text are visible only to family editors.