feat: add the lastModifyTime attribute for post and single page (#3101)

#### What type of PR is this?
/kind improvement
/milestone 2.2.x

#### What this PR does / why we need it:
文章和自定义页面支持展示最后修改时间
- 文章发布后会同步 post.spec.releasedSnapshot 记录的最后修改时间到 post.status.lastModifyTime,自定义页面亦如是。
- 主题端可以通过 `${item.status.lastModifyTime}` 获取(item表示文章或自定义页面 Vo)。
#### Which issue(s) this PR fixes:

Fixes #3090 

#### Special notes for your reviewer:
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
文章和自定义页面支持展示最后修改时间
```
pull/3102/head^2
guqing 2023-01-05 10:40:36 +08:00 committed by GitHub
parent 4533a83c0a
commit 9740de8d4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 8 deletions

View File

@ -150,6 +150,8 @@ public class Post extends AbstractExtension {
private List<String> contributors;
private Instant lastModifyTime;
@JsonIgnore
public ConditionList getConditionsOrDefault() {
if (this.conditions == null) {

View File

@ -5,6 +5,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@ -13,7 +14,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import run.halo.app.content.ContentService;
import run.halo.app.content.PostService;
import run.halo.app.content.permalinks.PostPermalinkPolicy;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Post;
@ -53,7 +53,6 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
private static final String FINALIZER_NAME = "post-protection";
private final ExtensionClient client;
private final ContentService contentService;
private final PostService postService;
private final PostPermalinkPolicy postPermalinkPolicy;
private final CounterService counterService;
@ -126,9 +125,9 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
Post.PostStatus status = post.getStatusOrDefault();
// validate release snapshot
boolean present = client.fetch(Snapshot.class, releaseSnapshot)
.isPresent();
if (!present) {
Optional<Snapshot> releasedSnapshotOpt =
client.fetch(Snapshot.class, releaseSnapshot);
if (releasedSnapshotOpt.isEmpty()) {
Condition condition = Condition.builder()
.type(Post.PostPhase.FAILED.name())
.reason("SnapshotNotFound")
@ -159,6 +158,9 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
post.getSpec().setPublishTime(Instant.now());
}
// populate lastModifyTime
status.setLastModifyTime(releasedSnapshotOpt.get().getSpec().getLastModifyTime());
client.update(post);
applicationContext.publishEvent(new PostPublishedEvent(this, name));
});
@ -301,6 +303,12 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
!StringUtils.equals(headSnapshot, post.getSpec().getReleaseSnapshot()));
});
if (post.isPublished() && status.getLastModifyTime() == null) {
client.fetch(Snapshot.class, post.getSpec().getReleaseSnapshot())
.ifPresent(releasedSnapshot ->
status.setLastModifyTime(releasedSnapshot.getSpec().getLastModifyTime()));
}
if (!oldPost.equals(post)) {
client.update(post);
}

View File

@ -8,6 +8,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -127,9 +128,9 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
SinglePage.SinglePageStatus status = page.getStatusOrDefault();
// validate release snapshot
boolean present = client.fetch(Snapshot.class, releaseSnapshot)
.isPresent();
if (!present) {
Optional<Snapshot> releasedSnapshotOpt =
client.fetch(Snapshot.class, releaseSnapshot);
if (releasedSnapshotOpt.isEmpty()) {
Condition condition = Condition.builder()
.type(Post.PostPhase.FAILED.name())
.reason("SnapshotNotFound")
@ -161,6 +162,9 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
page.getSpec().setPublishTime(Instant.now());
}
// populate lastModifyTime
status.setLastModifyTime(releasedSnapshotOpt.get().getSpec().getLastModifyTime());
client.update(page);
});
}
@ -365,6 +369,12 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
status.setInProgress(!StringUtils.equals(releaseSnapshot, headSnapshot));
});
if (singlePage.isPublished() && status.getLastModifyTime() == null) {
client.fetch(Snapshot.class, singlePage.getSpec().getReleaseSnapshot())
.ifPresent(releasedSnapshot ->
status.setLastModifyTime(releasedSnapshot.getSpec().getLastModifyTime()));
}
if (!oldPage.equals(singlePage)) {
client.update(singlePage);
}

View File

@ -7,16 +7,19 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.ContentService;
@ -26,6 +29,7 @@ import run.halo.app.content.TestPost;
import run.halo.app.content.permalinks.PostPermalinkPolicy;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Snapshot;
import run.halo.app.event.post.PostPublishedEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.controller.Reconciler;
@ -48,6 +52,9 @@ class PostReconcilerTest {
@Mock
private PostService postService;
@Mock
private ApplicationContext applicationContext;
@InjectMocks
private PostReconciler postReconciler;
@ -119,4 +126,68 @@ class PostReconcilerTest {
Post value = captor.getValue();
assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world");
}
@Nested
class LastModifyTimeTest {
@Test
void reconcileLastModifyTimeWhenPostIsPublished() {
String name = "post-A";
Post post = TestPost.postV1();
post.getSpec().setPublish(true);
post.getSpec().setHeadSnapshot("post-A-head-snapshot");
post.getSpec().setReleaseSnapshot("post-fake-released-snapshot");
when(client.fetch(eq(Post.class), eq(name)))
.thenReturn(Optional.of(post));
when(contentService.getContent(eq(post.getSpec().getReleaseSnapshot())))
.thenReturn(Mono.just(ContentWrapper.builder()
.snapshotName(post.getSpec().getHeadSnapshot())
.raw("hello world")
.content("<p>hello world</p>")
.rawType("markdown")
.build()));
Instant lastModifyTime = Instant.now();
Snapshot snapshotV2 = TestPost.snapshotV2();
snapshotV2.getSpec().setLastModifyTime(lastModifyTime);
when(client.fetch(eq(Snapshot.class), eq(post.getSpec().getReleaseSnapshot())))
.thenReturn(Optional.of(snapshotV2));
when(contentService.listSnapshots(any()))
.thenReturn(Flux.empty());
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
postReconciler.reconcile(new Reconciler.Request(name));
verify(client, times(4)).update(captor.capture());
Post value = captor.getValue();
assertThat(value.getStatus().getLastModifyTime()).isEqualTo(lastModifyTime);
verify(applicationContext).publishEvent(any(PostPublishedEvent.class));
}
@Test
void reconcileLastModifyTimeWhenPostIsNotPublished() {
String name = "post-A";
Post post = TestPost.postV1();
post.getSpec().setPublish(false);
post.getSpec().setHeadSnapshot("post-A-head-snapshot");
when(client.fetch(eq(Post.class), eq(name)))
.thenReturn(Optional.of(post));
when(contentService.getContent(eq(post.getSpec().getReleaseSnapshot())))
.thenReturn(Mono.just(ContentWrapper.builder()
.snapshotName(post.getSpec().getHeadSnapshot())
.raw("hello world")
.content("<p>hello world</p>")
.rawType("markdown")
.build()));
when(contentService.listSnapshots(any()))
.thenReturn(Flux.empty());
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
postReconciler.reconcile(new Reconciler.Request(name));
verify(client, times(3)).update(captor.capture());
Post value = captor.getValue();
assertThat(value.getStatus().getLastModifyTime()).isNull();
}
}
}

View File

@ -10,9 +10,11 @@ import static org.mockito.Mockito.when;
import static run.halo.app.content.TestPost.snapshotV1;
import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@ -123,6 +125,74 @@ class SinglePageReconcilerTest {
assertThat(permalink).isEqualTo("http://example.com/%E4%B8%AD%E6%96%87%20slug");
}
@Nested
class LastModifyTimeTest {
@Test
void reconcileLastModifyTimeWhenPageIsPublished() {
String name = "page-A";
when(externalUrlSupplier.get()).thenReturn(URI.create(""));
SinglePage page = pageV1();
page.getSpec().setPublish(true);
page.getSpec().setHeadSnapshot("page-A-head-snapshot");
page.getSpec().setReleaseSnapshot("page-fake-released-snapshot");
when(client.fetch(eq(SinglePage.class), eq(name)))
.thenReturn(Optional.of(page));
when(contentService.getContent(eq(page.getSpec().getHeadSnapshot())))
.thenReturn(Mono.just(ContentWrapper.builder()
.snapshotName(page.getSpec().getHeadSnapshot())
.raw("hello world")
.content("<p>hello world</p>")
.rawType("markdown")
.build())
);
Instant lastModifyTime = Instant.now();
Snapshot snapshotV2 = TestPost.snapshotV2();
snapshotV2.getSpec().setLastModifyTime(lastModifyTime);
when(client.fetch(eq(Snapshot.class), eq(page.getSpec().getReleaseSnapshot())))
.thenReturn(Optional.of(snapshotV2));
when(contentService.listSnapshots(any()))
.thenReturn(Flux.empty());
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
singlePageReconciler.reconcile(new Reconciler.Request(name));
verify(client, times(4)).update(captor.capture());
SinglePage value = captor.getValue();
assertThat(value.getStatus().getLastModifyTime()).isEqualTo(lastModifyTime);
}
@Test
void reconcileLastModifyTimeWhenPageIsNotPublished() {
String name = "page-A";
when(externalUrlSupplier.get()).thenReturn(URI.create(""));
SinglePage page = pageV1();
page.getSpec().setPublish(false);
when(client.fetch(eq(SinglePage.class), eq(name)))
.thenReturn(Optional.of(page));
when(contentService.getContent(eq(page.getSpec().getHeadSnapshot())))
.thenReturn(Mono.just(ContentWrapper.builder()
.snapshotName(page.getSpec().getHeadSnapshot())
.raw("hello world")
.content("<p>hello world</p>")
.rawType("markdown")
.build())
);
when(contentService.listSnapshots(any()))
.thenReturn(Flux.empty());
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
singlePageReconciler.reconcile(new Reconciler.Request(name));
verify(client, times(3)).update(captor.capture());
SinglePage value = captor.getValue();
assertThat(value.getStatus().getLastModifyTime()).isNull();
}
}
public static SinglePage pageV1() {
SinglePage page = new SinglePage();
page.setKind(Post.KIND);