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