fix(education): control content node visibility
This commit is contained in:
@@ -51,7 +51,7 @@ public class ContentNodeAuthoringController {
|
||||
|
||||
private ContentNodeAuthoringCommand toCommand(ContentNodeDraftReqVO request, Integer expectedVersion) {
|
||||
return new ContentNodeAuthoringCommand(request.getEntryId(), request.getParentId(), request.getName(),
|
||||
request.getTitle(), request.getNodeType(), request.getMarkerType(), request.getLeaf(),
|
||||
request.getSelectable(), request.getHidden(), request.getSortOrder(), request.getMetadata(), expectedVersion);
|
||||
request.getTitle(), request.getNodeType(), request.getMarkerType(), request.getSelectable(),
|
||||
request.getSortOrder(), request.getMetadata(), expectedVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ public class ContentNodeDraftReqVO {
|
||||
@Size(max = 200) private String title;
|
||||
@NotBlank @Size(max = 50) private String nodeType;
|
||||
@Size(max = 50) private String markerType;
|
||||
@NotNull private Boolean leaf;
|
||||
@NotNull private Boolean selectable;
|
||||
@NotNull private Boolean hidden;
|
||||
private Integer sortOrder;
|
||||
private String metadata;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ public interface ContentNodeMapper extends BaseMapperX<ContentNodeDO> {
|
||||
.eq(ContentNodeDO::getAuthoringVersion, expectedVersion)
|
||||
.set(ContentNodeDO::getPublicationStatus, targetStatus)
|
||||
.set(ContentNodeDO::getIsActive, active)
|
||||
.set(ContentNodeDO::getIsHidden, !active)
|
||||
.set(ContentNodeDO::getAuthoringVersion, expectedVersion + 1));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.module.education.service.contentnode.authoring;
|
||||
|
||||
public record ContentNodeAuthoringCommand(Long entryId, Long parentId, String name, String title,
|
||||
String nodeType, String markerType, Boolean leaf,
|
||||
Boolean selectable, Boolean hidden, Integer sortOrder,
|
||||
String metadata, Integer expectedAuthoringVersion) {
|
||||
String nodeType, String markerType, Boolean selectable,
|
||||
Integer sortOrder, String metadata,
|
||||
Integer expectedAuthoringVersion) {
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ public class ContentNodeAuthoringServiceImpl implements ContentNodeAuthoringServ
|
||||
ContentNodeDO node = new ContentNodeDO();
|
||||
apply(node, command);
|
||||
node.setTenantId(tenantId); node.setScope(TENANT_OWNED);
|
||||
node.setPublicationStatus("DRAFT"); node.setAuthoringVersion(0); node.setIsActive(false);
|
||||
node.setPublicationStatus("DRAFT"); node.setAuthoringVersion(0);
|
||||
node.setIsActive(false); node.setIsHidden(true); node.setIsLeaf(true);
|
||||
nodeMapper.insert(node);
|
||||
return node.getId();
|
||||
}
|
||||
@@ -105,7 +106,7 @@ public class ContentNodeAuthoringServiceImpl implements ContentNodeAuthoringServ
|
||||
private void apply(ContentNodeDO node, ContentNodeAuthoringCommand command) {
|
||||
node.setEntryId(command.entryId()); node.setParentId(command.parentId()); node.setName(command.name());
|
||||
node.setTitle(command.title()); node.setNodeType(command.nodeType()); node.setMarkerType(command.markerType());
|
||||
node.setIsLeaf(command.leaf()); node.setIsSelectable(command.selectable()); node.setIsHidden(command.hidden());
|
||||
node.setIsSelectable(command.selectable());
|
||||
node.setSortOrder(command.sortOrder() != null ? command.sortOrder() : 0); node.setMetadata(command.metadata());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,18 @@ ALTER TABLE education_content_node
|
||||
ADD COLUMN authoring_version INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE education_content_node
|
||||
SET publication_status = CASE WHEN is_active THEN 'ACTIVE' ELSE 'ARCHIVED' END;
|
||||
SET publication_status = CASE WHEN is_active AND NOT is_hidden THEN 'ACTIVE' ELSE 'ARCHIVED' END,
|
||||
is_active = is_active AND NOT is_hidden,
|
||||
is_hidden = NOT (is_active AND NOT is_hidden);
|
||||
|
||||
ALTER TABLE education_content_node
|
||||
ADD CONSTRAINT ck_education_content_node_publication_status
|
||||
CHECK (publication_status IN ('DRAFT', 'ACTIVE', 'ARCHIVED')),
|
||||
ADD CONSTRAINT ck_education_content_node_authoring_version CHECK (authoring_version >= 0),
|
||||
ADD CONSTRAINT ck_education_content_node_lifecycle_consistent
|
||||
CHECK (is_active = (publication_status = 'ACTIVE'));
|
||||
ADD CONSTRAINT ck_education_content_node_lifecycle_consistent CHECK (
|
||||
(publication_status = 'ACTIVE' AND is_active AND NOT is_hidden) OR
|
||||
(publication_status IN ('DRAFT', 'ARCHIVED') AND NOT is_active AND is_hidden)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN education_content_node.publication_status IS '内容节点发布状态:DRAFT、ACTIVE、ARCHIVED';
|
||||
COMMENT ON COLUMN education_content_node.authoring_version IS '内容节点创作和生命周期共用的乐观锁版本';
|
||||
@@ -47,11 +51,13 @@ RETURNS TRIGGER LANGUAGE plpgsql SET search_path = pg_catalog, pg_temp AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF NEW.scope = 'PUBLIC' THEN
|
||||
IF NEW.publication_status <> 'ACTIVE' OR NOT NEW.is_active OR NEW.authoring_version <> 0 THEN
|
||||
IF NEW.publication_status <> 'ACTIVE' OR NOT NEW.is_active OR NEW.is_hidden
|
||||
OR NEW.authoring_version <> 0 THEN
|
||||
RAISE EXCEPTION 'PUBLIC content node writes are not managed by tenant authoring' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
ELSIF NEW.scope = 'TENANT_OWNED' AND
|
||||
(NEW.publication_status <> 'DRAFT' OR NEW.is_active OR NEW.authoring_version <> 0) THEN
|
||||
(NEW.publication_status <> 'DRAFT' OR NEW.is_active OR NOT NEW.is_hidden
|
||||
OR NEW.authoring_version <> 0) THEN
|
||||
RAISE EXCEPTION 'tenant content nodes must start in DRAFT' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
|
||||
@@ -57,7 +57,7 @@ class ContentNodeAuthoringControllerContractTest {
|
||||
security.permissions.add("education:content-node:author");
|
||||
ContentNodeReviseReqVO request = new ContentNodeReviseReqVO();
|
||||
request.setExpectedAuthoringVersion(2); request.setName("Revised"); request.setNodeType("category");
|
||||
request.setEntryId(100L); request.setSelectable(true); request.setHidden(false); request.setLeaf(true);
|
||||
request.setEntryId(100L); request.setSelectable(true);
|
||||
assertEquals(3, controller.revise(101L, request).getData());
|
||||
assertEquals(2, service.command.expectedAuthoringVersion());
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class ContentNodeAuthoringControllerContractTest {
|
||||
private ContentNodeDraftReqVO request() {
|
||||
ContentNodeDraftReqVO request = new ContentNodeDraftReqVO();
|
||||
request.setEntryId(100L); request.setName("Algebra"); request.setNodeType("category");
|
||||
request.setSelectable(true); request.setHidden(false); request.setLeaf(true);
|
||||
request.setSelectable(true);
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,15 +38,25 @@ class ContentNodeAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbIntegrat
|
||||
|
||||
@Test void draftActivateArchiveControlsStudentDiscoveryAndPlacement() {
|
||||
Long id = service.createDraft(command(null, null, "Algebra", null));
|
||||
ContentNodeDO draft = nodeMapper.selectTenantOwnedById(10L, id);
|
||||
assertFalse(draft.getIsActive());
|
||||
assertTrue(draft.getIsHidden());
|
||||
assertTrue(draft.getIsLeaf());
|
||||
assertEquals(0, nodeMapper.selectAllByEntryId(10L, 100L, false, null).size());
|
||||
assertNull(nodeMapper.selectAvailablePlacementTarget(10L, id));
|
||||
|
||||
assertEquals(1, service.reviseDraft(id, command(null, null, "Algebra revised", 0)));
|
||||
assertEquals(2, service.activate(id, 1, 7L));
|
||||
ContentNodeDO active = nodeMapper.selectTenantOwnedById(10L, id);
|
||||
assertTrue(active.getIsActive());
|
||||
assertFalse(active.getIsHidden());
|
||||
assertEquals(1, nodeMapper.selectAllByEntryId(10L, 100L, false, null).size());
|
||||
assertNotNull(nodeMapper.selectAvailablePlacementTarget(10L, id));
|
||||
|
||||
assertEquals(3, service.archive(id, 2, 7L));
|
||||
ContentNodeDO archived = nodeMapper.selectTenantOwnedById(10L, id);
|
||||
assertFalse(archived.getIsActive());
|
||||
assertTrue(archived.getIsHidden());
|
||||
assertEquals(0, nodeMapper.selectAllByEntryId(10L, 100L, false, null).size());
|
||||
assertNull(nodeMapper.selectAvailablePlacementTarget(10L, id));
|
||||
assertEquals(2L, auditMapper.selectCount());
|
||||
@@ -74,6 +84,6 @@ class ContentNodeAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbIntegrat
|
||||
|
||||
private ContentNodeAuthoringCommand command(Long entryId, Long parentId, String name, Integer version) {
|
||||
return new ContentNodeAuthoringCommand(entryId != null ? entryId : 100L, parentId, name, null,
|
||||
"category", null, true, true, false, 0, null, version);
|
||||
"category", null, true, 0, null, version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,12 +484,12 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
(id, tenant_id, scope, entry_key, name, entry_type)
|
||||
VALUES (100, 10, 'TENANT_OWNED', 'questions', 'Questions', 'question');
|
||||
INSERT INTO education_content_node
|
||||
(id, tenant_id, scope, entry_id, name, node_type, publication_status, is_active)
|
||||
VALUES (200, 10, 'TENANT_OWNED', 100, 'Draft', 'category', 'DRAFT', false);
|
||||
(id, tenant_id, scope, entry_id, name, node_type, publication_status, is_active, is_hidden)
|
||||
VALUES (200, 10, 'TENANT_OWNED', 100, 'Draft', 'category', 'DRAFT', false, true);
|
||||
""");
|
||||
assertThatThrownBy(() -> execute(schema, """
|
||||
UPDATE education_content_node
|
||||
SET publication_status = 'ACTIVE', is_active = true, authoring_version = 1
|
||||
SET publication_status = 'ACTIVE', is_active = true, is_hidden = false, authoring_version = 1
|
||||
WHERE id = 200;
|
||||
""")).hasMessageContaining("content node lifecycle audit must accompany its transition");
|
||||
assertThatThrownBy(() -> execute(schema, """
|
||||
|
||||
Reference in New Issue
Block a user