deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
36a9f79543
commit
f2a3b3eeab
@ -28,7 +28,10 @@ Q-OPEN-10 判定依据(表无独立 audience 列,故按可持久化证据判
|
|||||||
demo/trial/true/1 族;
|
demo/trial/true/1 族;
|
||||||
2. **预览会话**:调用方传 ``preview_session_ids`` 显式声明哪些 session 属预览环境;
|
2. **预览会话**:调用方传 ``preview_session_ids`` 显式声明哪些 session 属预览环境;
|
||||||
3. **学生私有可见性**:payload 的 ``visibility`` / ``audience`` / ``data_scope`` /
|
3. **学生私有可见性**:payload 的 ``visibility`` / ``audience`` / ``data_scope`` /
|
||||||
``visible_to`` 为 STUDENT 族(学生侧私有,教师统计与正式分析默认不计入)。
|
``visible_to`` 为 STUDENT 族(学生侧私有,教师统计与正式分析默认不计入);
|
||||||
|
4. **预览保留会话号段**:``DEFAULT_PREVIEW_SESSION_IDS``(默认 ``('999',)``)内的 session
|
||||||
|
视为预览 / Playtest 环境会话,缺省一并剔除(``include_default_preview_sessions=False``
|
||||||
|
可关闭,``preview_session_ids`` 可追加)。
|
||||||
|
|
||||||
默认(``include_preview`` 缺省 False)三类全部剔除;显式传 ``include_preview=True`` /
|
默认(``include_preview`` 缺省 False)三类全部剔除;显式传 ``include_preview=True`` /
|
||||||
``include_student_private=True`` 才纳入。出参统一带 ``preview_filter`` 段回显本次扫描/
|
``include_student_private=True`` 才纳入。出参统一带 ``preview_filter`` 段回显本次扫描/
|
||||||
@ -54,6 +57,10 @@ PREVIEW_VALUES = ('preview', 'playtest', 'demo', 'student_preview', 'trial',
|
|||||||
'true', '1', 'yes', 'y')
|
'true', '1', 'yes', 'y')
|
||||||
VISIBILITY_KEYS = ('visibility', 'audience', 'data_scope', 'visible_to')
|
VISIBILITY_KEYS = ('visibility', 'audience', 'data_scope', 'visible_to')
|
||||||
STUDENT_PRIVATE_VALUES = ('student', 'student_only', 'student_private', 'private')
|
STUDENT_PRIVATE_VALUES = ('student', 'student_only', 'student_private', 'private')
|
||||||
|
# Q-OPEN-10 口径④:预览 / Playtest 环境复用「保留会话号段」(与正式会话号段隔离)。
|
||||||
|
# 表内无 audience 列,这类 session 产生的证据无法靠 payload 标记识别,故以号段约定判定;
|
||||||
|
# 调用方可用 preview_session_ids 追加号段,或用 include_default_preview_sessions=False 关闭。
|
||||||
|
DEFAULT_PREVIEW_SESSION_IDS = ('999',)
|
||||||
|
|
||||||
GROUP_BY_KEYS = {
|
GROUP_BY_KEYS = {
|
||||||
'learner': 'learner_id', 'learner_id': 'learner_id', 'student': 'learner_id',
|
'learner': 'learner_id', 'learner_id': 'learner_id', 'student': 'learner_id',
|
||||||
@ -69,6 +76,26 @@ SCAN_COLUMNS = ('id', 'evidence_type', 'source_event_id', 'learner_id', 'bluepri
|
|||||||
'payload_json')
|
'payload_json')
|
||||||
|
|
||||||
|
|
||||||
|
# ── M5b 契约口径:四类证据(设计 docs/01-design/modules/pbl_evidence.md §3.2)────
|
||||||
|
# 与 M5a 的 8 类 appcodes 枚举(evidence_map.EVIDENCE_TYPES)是两套共存口径:
|
||||||
|
# * M5a 8 类 =「证据来源分类」(互评/反思/资源使用…),供 CRUD 下拉与 M6 评估;
|
||||||
|
# * M5b 4 类 =「运行时行为契约口径」,collect 入参校验、四类覆盖度 coverage 以此为准。
|
||||||
|
# by_type 的键集合取「四类打头 ∪ 数据中实际出现的类型」动态并集:既不丢 M5a 已落库
|
||||||
|
# 证据的计数,又保证四类恒有键(缺数据为 0),前端可稳定渲染四类。
|
||||||
|
FOUR_EVIDENCE_TYPES = ('decision', 'action', 'observation', 'artifact_version')
|
||||||
|
ACCEPTED_EVIDENCE_TYPES = FOUR_EVIDENCE_TYPES + tuple(EVIDENCE_TYPES)
|
||||||
|
|
||||||
|
|
||||||
|
def type_universe(rows=None):
|
||||||
|
"""by_type / evidence 分组的键集合:四类打头(顺序稳定),再并入出现的其它类型。"""
|
||||||
|
keys = list(FOUR_EVIDENCE_TYPES)
|
||||||
|
for row in rows or []:
|
||||||
|
t = str(row.get('evidence_type') or '')
|
||||||
|
if t and t not in keys:
|
||||||
|
keys.append(t)
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
def _require_tenant():
|
def _require_tenant():
|
||||||
tid = tenant_id()
|
tid = tenant_id()
|
||||||
if not tid:
|
if not tid:
|
||||||
@ -175,7 +202,15 @@ def apply_default_filters(rows, kw):
|
|||||||
preview_sessions = kw.get('preview_session_ids') or []
|
preview_sessions = kw.get('preview_session_ids') or []
|
||||||
if isinstance(preview_sessions, str):
|
if isinstance(preview_sessions, str):
|
||||||
preview_sessions = [s.strip() for s in preview_sessions.split(',') if s.strip()]
|
preview_sessions = [s.strip() for s in preview_sessions.split(',') if s.strip()]
|
||||||
preview_sessions = list(preview_sessions)
|
preview_sessions = [str(s) for s in preview_sessions]
|
||||||
|
# 预览保留会话号段默认并入(缺省即生效),仅显式传 False 时关闭该口径
|
||||||
|
use_default = kw.get('include_default_preview_sessions')
|
||||||
|
if use_default is None:
|
||||||
|
use_default = True
|
||||||
|
if bool(use_default):
|
||||||
|
for _sid in DEFAULT_PREVIEW_SESSION_IDS:
|
||||||
|
if _sid not in preview_sessions:
|
||||||
|
preview_sessions.append(_sid)
|
||||||
|
|
||||||
subject_type = str(kw.get('subject_type') or '').strip().lower()
|
subject_type = str(kw.get('subject_type') or '').strip().lower()
|
||||||
subject_id = kw.get('subject_id')
|
subject_id = kw.get('subject_id')
|
||||||
@ -190,6 +225,7 @@ def apply_default_filters(rows, kw):
|
|||||||
'include_preview': include_preview,
|
'include_preview': include_preview,
|
||||||
'include_student_private': include_student,
|
'include_student_private': include_student,
|
||||||
'preview_session_count': len(preview_sessions),
|
'preview_session_count': len(preview_sessions),
|
||||||
|
'default_preview_session_ids': list(DEFAULT_PREVIEW_SESSION_IDS),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _note(rule):
|
def _note(rule):
|
||||||
@ -227,7 +263,8 @@ def apply_default_filters(rows, kw):
|
|||||||
def aggregate_rows(rows, group_by='learner'):
|
def aggregate_rows(rows, group_by='learner'):
|
||||||
"""纯聚合(无 IO,可单测):四类计数 + 覆盖度 + 分组 + 时间水位。"""
|
"""纯聚合(无 IO,可单测):四类计数 + 覆盖度 + 分组 + 时间水位。"""
|
||||||
gkey = GROUP_BY_KEYS.get(str(group_by or 'learner').strip().lower(), 'learner_id')
|
gkey = GROUP_BY_KEYS.get(str(group_by or 'learner').strip().lower(), 'learner_id')
|
||||||
by_type = {t: 0 for t in EVIDENCE_TYPES}
|
universe = type_universe(rows) # 四类打头 ∪ 数据中出现的其它类型
|
||||||
|
by_type = {t: 0 for t in universe}
|
||||||
groups = {}
|
groups = {}
|
||||||
max_occurred, min_occurred, max_id = '', '', 0
|
max_occurred, min_occurred, max_id = '', '', 0
|
||||||
|
|
||||||
@ -239,7 +276,7 @@ def aggregate_rows(rows, group_by='learner'):
|
|||||||
bucket = groups.get(gval)
|
bucket = groups.get(gval)
|
||||||
if bucket is None:
|
if bucket is None:
|
||||||
bucket = {'group_key': gval, 'total': 0,
|
bucket = {'group_key': gval, 'total': 0,
|
||||||
'by_type': {t: 0 for t in EVIDENCE_TYPES},
|
'by_type': {t: 0 for t in universe},
|
||||||
'first_occurred_at': None, 'last_occurred_at': None,
|
'first_occurred_at': None, 'last_occurred_at': None,
|
||||||
'blueprint_ids': set(), 'learner_ids': set()}
|
'blueprint_ids': set(), 'learner_ids': set()}
|
||||||
groups[gval] = bucket
|
groups[gval] = bucket
|
||||||
@ -272,18 +309,21 @@ def aggregate_rows(rows, group_by='learner'):
|
|||||||
'learner_count': len(bucket['learner_ids']),
|
'learner_count': len(bucket['learner_ids']),
|
||||||
'first_occurred_at': bucket['first_occurred_at'],
|
'first_occurred_at': bucket['first_occurred_at'],
|
||||||
'last_occurred_at': bucket['last_occurred_at'],
|
'last_occurred_at': bucket['last_occurred_at'],
|
||||||
'type_coverage': sum(1 for t in EVIDENCE_TYPES if bucket['by_type'][t] > 0),
|
# 分组覆盖度只按 M5b 四类计(.get 防动态并集键集下四类之外不参与)
|
||||||
|
'type_coverage': sum(1 for t in FOUR_EVIDENCE_TYPES
|
||||||
|
if bucket['by_type'].get(t)),
|
||||||
})
|
})
|
||||||
group_list.sort(key=lambda x: (-x['total'], x['group_key']))
|
group_list.sort(key=lambda x: (-x['total'], x['group_key']))
|
||||||
|
|
||||||
covered = sum(1 for t in EVIDENCE_TYPES if by_type[t] > 0)
|
# 覆盖度只对 M5b 四类口径计算(设计 §3.2「四类证据」),M5a 八类不参与
|
||||||
|
covered = sum(1 for t in FOUR_EVIDENCE_TYPES if by_type.get(t))
|
||||||
return {
|
return {
|
||||||
'total': len(rows or []),
|
'total': len(rows or []),
|
||||||
'by_type': by_type,
|
'by_type': by_type,
|
||||||
'coverage': {
|
'coverage': {
|
||||||
'covered_types': covered,
|
'covered_types': covered,
|
||||||
'total_types': len(EVIDENCE_TYPES),
|
'total_types': len(FOUR_EVIDENCE_TYPES),
|
||||||
'missing_types': [t for t in EVIDENCE_TYPES if by_type[t] == 0],
|
'missing_types': [t for t in FOUR_EVIDENCE_TYPES if not by_type.get(t)],
|
||||||
},
|
},
|
||||||
'groups': group_list,
|
'groups': group_list,
|
||||||
'group_by': gkey,
|
'group_by': gkey,
|
||||||
@ -328,11 +368,11 @@ def parse_evidence_types(kw):
|
|||||||
return []
|
return []
|
||||||
if not isinstance(types, (list, tuple)):
|
if not isinstance(types, (list, tuple)):
|
||||||
raise PblError('PBL_E_PARAM', 'evidence_types 须为数组或逗号分隔串')
|
raise PblError('PBL_E_PARAM', 'evidence_types 须为数组或逗号分隔串')
|
||||||
bad = [t for t in types if t not in EVIDENCE_TYPES]
|
bad = [t for t in types if t not in ACCEPTED_EVIDENCE_TYPES]
|
||||||
if bad:
|
if bad:
|
||||||
raise PblError('PBL_E_VALIDATION',
|
raise PblError('PBL_E_VALIDATION',
|
||||||
'evidence_types 须属于四类 %s,非法值:%s'
|
'evidence_types 须属于四类 %s(或 M5a 分类 %s),非法值:%s'
|
||||||
% ('/'.join(EVIDENCE_TYPES), bad))
|
% ('/'.join(FOUR_EVIDENCE_TYPES), '/'.join(EVIDENCE_TYPES), bad))
|
||||||
return list(types)
|
return list(types)
|
||||||
|
|
||||||
|
|
||||||
@ -389,7 +429,7 @@ async def pbl_evidence_collect_idempotent(**kw):
|
|||||||
|
|
||||||
tid = _require_tenant()
|
tid = _require_tenant()
|
||||||
etype = str(kw.get('evidence_type') or '').strip()
|
etype = str(kw.get('evidence_type') or '').strip()
|
||||||
if etype and etype not in EVIDENCE_TYPES:
|
if etype and etype not in FOUR_EVIDENCE_TYPES:
|
||||||
raise PblError('PBL_E_VALIDATION',
|
raise PblError('PBL_E_VALIDATION',
|
||||||
'evidence_type 须为四类 %s,收到:%s'
|
'evidence_type 须为四类 %s,收到:%s'
|
||||||
% ('/'.join(EVIDENCE_TYPES), etype))
|
% ('/'.join(EVIDENCE_TYPES), etype))
|
||||||
@ -526,11 +566,13 @@ async def pbl_evidence_subject_summary(**kw):
|
|||||||
rows, truncated = await scan_evidence_rows(where, kw, cols=cols)
|
rows, truncated = await scan_evidence_rows(where, kw, cols=cols)
|
||||||
kept, stats = apply_default_filters(rows, kw)
|
kept, stats = apply_default_filters(rows, kw)
|
||||||
|
|
||||||
grouped = {t: [] for t in EVIDENCE_TYPES}
|
# 键集 = M5b 四类打头 ∪ kept(过滤后保留行)实际出现的类型:四类恒有键(缺数据为空
|
||||||
|
# 数组),未知类型自成一组,绝不塞进 observation 冒充(那会污染四类口径与覆盖度)
|
||||||
|
grouped = {t: [] for t in type_universe(kept)}
|
||||||
for row in kept:
|
for row in kept:
|
||||||
etype = str(row.get('evidence_type') or '')
|
etype = str(row.get('evidence_type') or '') or 'unknown'
|
||||||
if etype not in grouped:
|
if etype not in grouped:
|
||||||
etype = 'observation'
|
grouped[etype] = []
|
||||||
grouped[etype].append({
|
grouped[etype].append({
|
||||||
'id': row.get('id'),
|
'id': row.get('id'),
|
||||||
'source_event_id': row.get('source_event_id'),
|
'source_event_id': row.get('source_event_id'),
|
||||||
@ -562,7 +604,7 @@ async def pbl_evidence_subject_summary(**kw):
|
|||||||
return {
|
return {
|
||||||
'ok': True, 'status': 'OK', 'subject_type': stype, 'subject_id': str(sid),
|
'ok': True, 'status': 'OK', 'subject_type': stype, 'subject_id': str(sid),
|
||||||
'tenant_id': tid, 'evidence_total': len(kept),
|
'tenant_id': tid, 'evidence_total': len(kept),
|
||||||
'by_type': {t: len(grouped.get(t) or []) for t in EVIDENCE_TYPES},
|
'by_type': {t: len(grouped.get(t) or []) for t in grouped},
|
||||||
'evidence': grouped, 'watermark': agg['watermark'],
|
'evidence': grouped, 'watermark': agg['watermark'],
|
||||||
'artifact_count': artifact_count, 'preview_filter': stats,
|
'artifact_count': artifact_count, 'preview_filter': stats,
|
||||||
'truncated': bool(truncated), 'generated_at': now_str(),
|
'truncated': bool(truncated), 'generated_at': now_str(),
|
||||||
|
|||||||
@ -241,6 +241,13 @@ def test_aggregate_four_types_and_coverage():
|
|||||||
assert res['truncated'] is False, res
|
assert res['truncated'] is False, res
|
||||||
|
|
||||||
|
|
||||||
|
# 默认口径下应保留的行(正式环境证据,四类齐)= id 1..5;其余 4 行必须被默认剔除:
|
||||||
|
# id6 payload 预览标记 / id7 env_mode=playtest / id8 预览保留会话 999 / id9 STUDENT 私有
|
||||||
|
KEPT_DEFAULT = {1, 2, 3, 4, 5}
|
||||||
|
PREVIEW_DEFAULT = {6, 7, 8}
|
||||||
|
STUDENT_DEFAULT = {9}
|
||||||
|
|
||||||
|
|
||||||
def test_aggregate_group_by_type_and_time_range():
|
def test_aggregate_group_by_type_and_time_range():
|
||||||
install_fake(seed_rows())
|
install_fake(seed_rows())
|
||||||
res = run(A.pbl_evidence_aggregate(group_by='type',
|
res = run(A.pbl_evidence_aggregate(group_by='type',
|
||||||
@ -252,14 +259,22 @@ def test_aggregate_group_by_type_and_time_range():
|
|||||||
time_range={'from': '2026-09-20 00:00:00',
|
time_range={'from': '2026-09-20 00:00:00',
|
||||||
'to': '2026-09-20 23:59:59'}))
|
'to': '2026-09-20 23:59:59'}))
|
||||||
assert res2['group_by'] == 'session_id', res2
|
assert res2['group_by'] == 'session_id', res2
|
||||||
assert res2['total'] == 3, res2
|
# 09-20 时间窗内的保留行 = id 1,2,3,4(id5 发生在 09-21 被时间窗排除;id6/7 payload
|
||||||
|
# 预览标记、id8 预览保留会话 999、id9 STUDENT 私有被 Q-OPEN-10 默认口径排除)
|
||||||
|
assert res2['total'] == 4, res2['total']
|
||||||
|
assert [g['group_key'] for g in res2['groups']] == ['101'], res2['groups']
|
||||||
|
assert res2['by_type']['decision'] == 2, res2['by_type']
|
||||||
|
|
||||||
|
|
||||||
def test_aggregate_evidence_types_filter():
|
def test_aggregate_evidence_types_filter():
|
||||||
install_fake(seed_rows())
|
install_fake(seed_rows())
|
||||||
res = run(A.pbl_evidence_aggregate(evidence_types=['decision', 'action']))
|
res = run(A.pbl_evidence_aggregate(evidence_types=['decision', 'action']))
|
||||||
assert set(res['by_type']) == {'decision', 'action', 'observation', 'artifact_version'}
|
assert set(res['by_type']) == {'decision', 'action', 'observation', 'artifact_version'}
|
||||||
assert res['total'] == 2, res['total']
|
# 四类 ∩ {decision, action} 且通过默认过滤的保留行 = id 1、5(decision)+ id 2(action);
|
||||||
|
# id6(payload 预览标记)/id8(预览会话 999)虽属该两类,也被 Q-OPEN-10 默认口径剔除
|
||||||
|
assert res['total'] == 3, res['total']
|
||||||
|
assert res['by_type'] == {'decision': 2, 'action': 1, 'observation': 0,
|
||||||
|
'artifact_version': 0}, res['by_type']
|
||||||
assert res['coverage']['missing_types'] == ['observation', 'artifact_version'], res['coverage']
|
assert res['coverage']['missing_types'] == ['observation', 'artifact_version'], res['coverage']
|
||||||
try:
|
try:
|
||||||
run(A.pbl_evidence_aggregate(evidence_types='decision,bogus'))
|
run(A.pbl_evidence_aggregate(evidence_types='decision,bogus'))
|
||||||
@ -282,6 +297,7 @@ def test_query_pagination_and_payload():
|
|||||||
assert len(ids) == 4, ids # 翻页不重叠
|
assert len(ids) == 4, ids # 翻页不重叠
|
||||||
tail = run(A.pbl_evidence_query(page=3, rows=2))
|
tail = run(A.pbl_evidence_query(page=3, rows=2))
|
||||||
assert len(tail['rows']) == 1, tail['rows']
|
assert len(tail['rows']) == 1, tail['rows']
|
||||||
|
assert {r['id'] for r in tail['rows']} == {1}, tail['rows'] # occurred_at 倒序末位
|
||||||
assert run(A.pbl_evidence_query(page=9, rows=20))['rows'] == []
|
assert run(A.pbl_evidence_query(page=9, rows=20))['rows'] == []
|
||||||
|
|
||||||
|
|
||||||
@ -289,6 +305,7 @@ def test_query_filters_by_learner_and_type():
|
|||||||
install_fake(seed_rows())
|
install_fake(seed_rows())
|
||||||
res = run(A.pbl_evidence_query(learner_id='S02'))
|
res = run(A.pbl_evidence_query(learner_id='S02'))
|
||||||
assert res['total'] == 2, res['total']
|
assert res['total'] == 2, res['total']
|
||||||
|
assert {r['id'] for r in res['rows']} == {3, 4}, res['rows']
|
||||||
assert {r['learner_id'] for r in res['rows']} == {'S02'}, res['rows']
|
assert {r['learner_id'] for r in res['rows']} == {'S02'}, res['rows']
|
||||||
res2 = run(A.pbl_evidence_query(evidence_type='decision'))
|
res2 = run(A.pbl_evidence_query(evidence_type='decision'))
|
||||||
assert res2['total'] == 2, res2['total']
|
assert res2['total'] == 2, res2['total']
|
||||||
@ -302,13 +319,24 @@ def test_subject_summary_student_and_team():
|
|||||||
stu = run(A.pbl_evidence_subject_summary(subject_type='student', subject_id='S02'))
|
stu = run(A.pbl_evidence_subject_summary(subject_type='student', subject_id='S02'))
|
||||||
assert stu['ok'] is True, stu
|
assert stu['ok'] is True, stu
|
||||||
assert stu['evidence_total'] == 2, stu['evidence_total']
|
assert stu['evidence_total'] == 2, stu['evidence_total']
|
||||||
assert stu['by_type']['action'] == 1 and stu['by_type']['artifact_version'] == 1, stu['by_type']
|
# S02 的保留行 = id3 observation + id4 artifact_version;decision/action 恒 0 但键必须在
|
||||||
|
assert stu['by_type'] == {'decision': 0, 'action': 0, 'observation': 1,
|
||||||
|
'artifact_version': 1}, stu['by_type']
|
||||||
assert set(stu['evidence']) == {'decision', 'action', 'observation', 'artifact_version'}
|
assert set(stu['evidence']) == {'decision', 'action', 'observation', 'artifact_version'}
|
||||||
|
assert [e['id'] for e in stu['evidence']['observation']] == [3], stu['evidence']
|
||||||
|
assert stu['evidence']['decision'] == [], '四类恒有键(缺数据为空数组,前端稳定渲染)'
|
||||||
assert stu['artifact_count'] == 2, stu['artifact_count']
|
assert stu['artifact_count'] == 2, stu['artifact_count']
|
||||||
tea = run(A.pbl_evidence_subject_summary(subject_type='team', team_id='101',
|
tea = run(A.pbl_evidence_subject_summary(subject_type='team', team_id='101',
|
||||||
blueprint_id='9'))
|
blueprint_id='9'))
|
||||||
assert tea['subject_id'] == '101', tea
|
assert tea['subject_id'] == '101', tea
|
||||||
assert tea['evidence_total'] == 2, tea['evidence_total'] # 999 预览会话已剔除
|
# 会话 101 的保留行 = id 1,2,3,4,5;id6/id7(payload 预览标记)与 id9(STUDENT 私有)
|
||||||
|
# 被剔除(id8 属预览保留会话 999,本就不在 session=101 的扫描范围内)
|
||||||
|
assert tea['evidence_total'] == 5, tea['evidence_total']
|
||||||
|
assert tea['by_type'] == {'decision': 2, 'action': 1, 'observation': 1,
|
||||||
|
'artifact_version': 1}, tea['by_type']
|
||||||
|
assert sorted(i['id'] for i in tea['evidence']['decision']) == [1, 5], tea['evidence']
|
||||||
|
assert tea['preview_filter']['excluded_preview'] == 2, tea['preview_filter']
|
||||||
|
assert tea['preview_filter']['excluded_student'] == 1, tea['preview_filter']
|
||||||
try:
|
try:
|
||||||
run(A.pbl_evidence_subject_summary(subject_type='school', subject_id='X'))
|
run(A.pbl_evidence_subject_summary(subject_type='school', subject_id='X'))
|
||||||
raise AssertionError('非法 subject_type 必须被拒')
|
raise AssertionError('非法 subject_type 必须被拒')
|
||||||
@ -331,11 +359,29 @@ def test_qopen10_preview_excluded_by_default():
|
|||||||
include_student_private=True,
|
include_student_private=True,
|
||||||
preview_session_ids=['999']))
|
preview_session_ids=['999']))
|
||||||
assert default['total'] == 5, default['total']
|
assert default['total'] == 5, default['total']
|
||||||
|
assert {r['id'] for r in default['rows']} == KEPT_DEFAULT, default['rows']
|
||||||
|
assert default['preview_filter']['excluded_preview'] == len(PREVIEW_DEFAULT), default['preview_filter']
|
||||||
|
assert default['preview_filter']['excluded_student'] == len(STUDENT_DEFAULT), default['preview_filter']
|
||||||
assert withprev['total'] == 9, withprev['total']
|
assert withprev['total'] == 9, withprev['total']
|
||||||
assert withprev['preview_filter']['applied'] is False, withprev['preview_filter']
|
assert withprev['preview_filter']['applied'] is False, withprev['preview_filter']
|
||||||
assert withprev['by_type']['observation'] == 2, withprev['by_type']
|
assert withprev['by_type']['observation'] == 2, withprev['by_type']
|
||||||
|
|
||||||
|
|
||||||
|
def test_qopen10_default_preview_session_range():
|
||||||
|
"""Q-OPEN-10 号段口径:预览保留会话默认剔除;显式 False 可关闭;追加号段生效。"""
|
||||||
|
install_fake(seed_rows())
|
||||||
|
res = run(A.pbl_evidence_query())
|
||||||
|
assert 8 not in {r['id'] for r in res['rows']}, res['rows']
|
||||||
|
assert res['preview_filter']['excluded_preview'] == 3, res['preview_filter']
|
||||||
|
assert res['preview_filter']['default_preview_session_ids'] == ['999'], res['preview_filter']
|
||||||
|
off = run(A.pbl_evidence_query(include_default_preview_sessions=False))
|
||||||
|
assert 8 in {r['id'] for r in off['rows']}, off['rows']
|
||||||
|
assert off['total'] == 6, off['total']
|
||||||
|
extra = run(A.pbl_evidence_query(preview_session_ids=['101']))
|
||||||
|
assert extra['total'] == 0, extra['total'] # 正式会话被声明为预览后全部剔除
|
||||||
|
assert extra['preview_filter']['excluded_preview'] == 9, extra['preview_filter']
|
||||||
|
|
||||||
|
|
||||||
def test_qopen10_rules_pure():
|
def test_qopen10_rules_pure():
|
||||||
prev, rule = A.is_preview_row({'payload_json': json.dumps({'pbl_preview': True}),
|
prev, rule = A.is_preview_row({'payload_json': json.dumps({'pbl_preview': True}),
|
||||||
'session_id': '1'})
|
'session_id': '1'})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user