Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.woltlab.wcf/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
Required order of the following steps for the update to 6.3:
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_likeObject.php</instruction>
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_6.3_step1.php</instruction>
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_articleLastUpdateTime.php</instruction>
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_conditions.php</instruction>
The `objectType` PIP must run after `update_com.woltlab.wcf_6.3_conditions.php`,
because deleting the legacy condition object types cascades to `wcf1_condition`.
Expand Down
11 changes: 11 additions & 0 deletions com.woltlab.wcf/templates/articleContentHeaderTitle.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@
{icon name='clock'}
<a href="{$article->getLink()}">{time time=$article->time}</a>
<meta itemprop="datePublished" content="{$article->time|date:'c'}">
<meta itemprop="dateModified" content="{$article->lastUpdateTime|date:'c'}">
</li>

{if $article->flagAsUpdated}
<li>
{icon name='arrows-rotate'}
<span>{time time=$article->lastUpdateTime}</span>
</li>
{/if}

{if $articleContent->getReadingTime()}
<li>
{icon name='hourglass'}
Expand Down Expand Up @@ -61,6 +69,9 @@
{if $article->isFeatured}
<span class="badge green">{lang}wcf.article.featured{/lang}</span>
{/if}
{if $article->flagAsUpdated}
<span class="badge">{lang}wcf.article.updated{/lang}</span>
{/if}
{if $article->isDeleted}
<span class="badge red">{lang}wcf.message.status.deleted{/lang}</span>
{/if}
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/articleListItems.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<div class="entryCardList__item__badges">
{content}
{if $article->isFeatured}<span class="badge green">{lang}wcf.article.featured{/lang}</span>{/if}
{if $article->flagAsUpdated}<span class="badge">{lang}wcf.article.updated{/lang}</span>{/if}
{if $article->isDeleted}<span class="badge red">{lang}wcf.message.status.deleted{/lang}</span>{/if}
{if !$article->isPublished()}<span class="badge green">{lang}wcf.message.status.disabled{/lang}</span>{/if}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
->defaultValue(0)
->drop(),
DefaultFalseBooleanDatabaseTableColumn::create('isFeatured'),
DefaultFalseBooleanDatabaseTableColumn::create('flagAsUpdated'),
NotNullInt10DatabaseTableColumn::create('lastUpdateTime')
->defaultValue(0),
])
->indices([
DatabaseTableIndex::create('lastUpdateTime')
->columns(['lastUpdateTime']),
]),
PartialDatabaseTable::create('wcf1_article_content')
->columns([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

use wcf\system\WCF;

// Articles that are not flagged as updated use their publication time as the
// time of the last update.
$sql = "UPDATE wcf1_article
SET lastUpdateTime = time
WHERE flagAsUpdated = ?";
$statement = WCF::getDB()->prepare($sql);
$statement->execute([0]);
51 changes: 51 additions & 0 deletions wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use wcf\system\form\builder\field\AbstractFormField;
use wcf\system\form\builder\field\BooleanFormField;
use wcf\system\form\builder\field\DateFormField;
use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency;
use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
use wcf\system\form\builder\field\HiddenFormField;
use wcf\system\form\builder\field\IFormField;
Expand Down Expand Up @@ -245,6 +246,56 @@ protected function createForm(): void
->loadValueCallback(static function (Article $object, IFormField $field) {
$field->value($object->time);
}),
BooleanFormField::create('flagAsUpdated')
->label('wcf.acp.article.flagAsUpdated')
->description('wcf.acp.article.flagAsUpdated.description')
// The flag drives the time of the last update.
->saveValueCallback(static function (ArticleBuilder $builder, IFormField $field) {
$dateField = $field->getDocument()->getFormField('lastUpdateTime');
$builder->setLastUpdate(
(bool)$field->getSaveValue()
&& $dateField !== null
&& $dateField->getSaveValue() !== null
? (int)$dateField->getSaveValue()
: null
);
})
->loadValueCallback(static function (Article $object, IFormField $field) {
$field->value((bool)$object->flagAsUpdated);
}),
DateFormField::create('lastUpdateTime')
->supportTime()
->required()
->label('wcf.acp.article.lastUpdateTime')
->value(\TIME_NOW)
->addDependency(
NonEmptyFormFieldDependency::create('flagAsUpdated')
->fieldId('flagAsUpdated')
)
->addValidator(new FormFieldValidator('timeCheck', static function (IFormField $field) {
$timeField = $field->getDocument()->getFormField('time');
$time = $timeField !== null ? (int)$timeField->getSaveValue() : 0;
$lastUpdateTime = (int)$field->getSaveValue();

// Delayed articles may be flagged as updated in the future,
// published articles must not.
$statusField = $field->getDocument()->getFormField('publicationStatus');
$status = $statusField !== null
? (int)$statusField->getSaveValue()
: Article::UNPUBLISHED;

if (
$lastUpdateTime < $time
|| ($status === Article::PUBLISHED && $lastUpdateTime > \TIME_NOW)
) {
$field->addValidationError(new FormFieldValidationError('invalid'));
}
}))
->loadValueCallback(static function (Article $object, IFormField $field) {
if ($object->flagAsUpdated !== 0) {
$field->value($object->lastUpdateTime);
}
}),
RadioButtonFormField::create('publicationStatus')
->label('wcf.acp.article.publicationStatus')
->options([
Expand Down
8 changes: 5 additions & 3 deletions wcfsetup/install/files/lib/data/article/Article.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
* @property-read 0|1 $isDeleted is 1 if the article is in trash bin, otherwise 0
* @property-read 0|1 $hasLabels is `1` if labels are assigned to the article
* @property-read 0|1 $isFeatured is `1` if the article is featured which pins it at the top, otherwise `0`
* @property-read 0|1 $flagAsUpdated is `1` if the article is flagged as updated, otherwise `0`
* @property-read int $lastUpdateTime timestamp of the last update or the publication time if the article is not flagged as updated
*
* @extends CollectionDatabaseObject<ArticleCollection>
*/
Expand Down Expand Up @@ -495,7 +497,7 @@ public function getVisitTime(): int
*/
public function isNew(): bool
{
return $this->time > $this->getEffectiveVisitTime();
return $this->lastUpdateTime > $this->getEffectiveVisitTime();
}

/**
Expand Down Expand Up @@ -562,12 +564,12 @@ public static function getUnreadArticles(): int
$conditionBuilder = new PreparedStatementConditionBuilder();
$conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]);
$conditionBuilder->add(
'article.time > ?',
'article.lastUpdateTime > ?',
[VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')]
);
$conditionBuilder->add('article.isDeleted = ?', [0]);
$conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]);
$conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');
$conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');

$sql = "SELECT COUNT(*)
FROM wcf1_article article
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ class ArticleAction extends AbstractDatabaseObjectAction
#[\Override]
public function create()
{
if (!isset($this->parameters['data']['lastUpdateTime'])) {
$this->parameters['data']['lastUpdateTime'] = $this->parameters['data']['time'] ?? \TIME_NOW;
}

$article = parent::create();

// save article content
Expand Down
36 changes: 36 additions & 0 deletions wcfsetup/install/files/lib/data/article/ArticleBuilder.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,45 @@ public function setTime(int $time): static
{
$this->properties['time'] = $time;

// The time of the last update follows the publication time unless the
// article is flagged as updated, it must never precede it though.
if ($this->isFlaggedAsUpdated()) {
$this->properties['lastUpdateTime'] = \max($this->getLastUpdateTime(), $time);
} else {
$this->properties['lastUpdateTime'] = $time;
}

return $this;
}

/**
* Flags the article as updated at the given time, or removes the flag when
* passing `null`, causing the time of the last update to fall back to the
* publication time.
*/
public function setLastUpdate(?int $lastUpdateTime): static
{
if ($lastUpdateTime === null) {
$this->properties['flagAsUpdated'] = 0;
$this->properties['lastUpdateTime'] = $this->properties['time'] ?? $this->existingObject->time ?? 0;
} else {
$this->properties['flagAsUpdated'] = 1;
$this->properties['lastUpdateTime'] = $lastUpdateTime;
}

return $this;
}

private function isFlaggedAsUpdated(): bool
{
return (bool)($this->properties['flagAsUpdated'] ?? $this->existingObject->flagAsUpdated ?? 0);
}

private function getLastUpdateTime(): int
{
return (int)($this->properties['lastUpdateTime'] ?? $this->existingObject->lastUpdateTime ?? 0);
}

public function setCategory(ArticleCategory $category): static
{
$this->properties['categoryID'] = $category->categoryID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function __construct(Tag|array $tags)
{
parent::__construct();

$this->sqlOrderBy = 'article.time ' . \ARTICLE_SORT_ORDER;
$this->sqlOrderBy = 'article.lastUpdateTime ' . \ARTICLE_SORT_ORDER;

$subselect = TagEngine::getInstance()->getSubselectForObjectsByTags(
'com.woltlab.wcf.article',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,12 @@ private static function fetchUnreadArticlesForCategory(int $articleCategoryID)
$conditionBuilder = new PreparedStatementConditionBuilder();
$conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]);
$conditionBuilder->add(
'article.time > ?',
'article.lastUpdateTime > ?',
[VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')]
);
$conditionBuilder->add('article.isDeleted = ?', [0]);
$conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]);
$conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');
$conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');

$sql = "SELECT COUNT(*)
FROM wcf1_article article
Expand Down Expand Up @@ -204,12 +204,12 @@ public static function getWatchedUnreadArticles()
$conditionBuilder = new PreparedStatementConditionBuilder();
$conditionBuilder->add('article.categoryID IN (?)', [$categoryIDs]);
$conditionBuilder->add(
'article.time > ?',
'article.lastUpdateTime > ?',
[VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')]
);
$conditionBuilder->add('article.isDeleted = ?', [0]);
$conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]);
$conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');
$conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');

$sql = "SELECT COUNT(*)
FROM wcf1_article article
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ protected function initUnreadArticles(): void

$conditionBuilder = new PreparedStatementConditionBuilder();
$conditionBuilder->add(
'article.time > ?',
'article.lastUpdateTime > ?',
[VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.article')]
);
$conditionBuilder->add('article.isDeleted = ?', [0]);
$conditionBuilder->add('article.publicationStatus = ?', [Article::PUBLISHED]);
$conditionBuilder->add('(article.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');
$conditionBuilder->add('(article.lastUpdateTime > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)');

$sql = "SELECT COUNT(*) AS count, article.categoryID
FROM wcf1_article article
Expand Down
14 changes: 7 additions & 7 deletions wcfsetup/install/files/lib/page/ArticlePage.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ protected function loadNextArticle(): void
{
$articleList = new CategoryArticleList($this->article->categoryID);
$articleList->getConditionBuilder()->add(
'article.time ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '>' : '<') . ' ?',
[$this->article->time]
'article.lastUpdateTime ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '>' : '<') . ' ?',
[$this->article->lastUpdateTime]
);
$articleList->sqlOrderBy = 'article.time ' . (\ARTICLE_SORT_ORDER === 'DESC' ? 'ASC' : 'DESC');
$articleList->sqlOrderBy = 'article.lastUpdateTime ' . (\ARTICLE_SORT_ORDER === 'DESC' ? 'ASC' : 'DESC');
$articleList->sqlLimit = 1;
$articleList->readObjects();
foreach ($articleList as $article) {
Expand All @@ -186,10 +186,10 @@ protected function loadPreviousArticle(): void
{
$articleList = new CategoryArticleList($this->article->categoryID);
$articleList->getConditionBuilder()->add(
'article.time ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '<' : '>') . ' ?',
[$this->article->time]
'article.lastUpdateTime ' . (\ARTICLE_SORT_ORDER === 'DESC' ? '<' : '>') . ' ?',
[$this->article->lastUpdateTime]
);
$articleList->sqlOrderBy = 'article.time ' . \ARTICLE_SORT_ORDER;
$articleList->sqlOrderBy = 'article.lastUpdateTime ' . \ARTICLE_SORT_ORDER;
$articleList->sqlLimit = 1;
$articleList->readObjects();
foreach ($articleList as $article) {
Expand Down Expand Up @@ -227,7 +227,7 @@ protected function setMetaTags(): void
MetaTagHandler::getInstance()->addTag(
'article:modified_time',
'article:modified_time',
\gmdate('c', $this->article->time),
\gmdate('c', $this->article->lastUpdateTime),
true
);
MetaTagHandler::getInstance()->addTag(
Expand Down
4 changes: 2 additions & 2 deletions wcfsetup/install/files/lib/page/ArticleRssFeedPage.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function readData()
} else {
$this->articles = new AccessibleArticleList();
}
$this->articles->sqlOrderBy = 'article.time ' . \ARTICLE_SORT_ORDER;
$this->articles->sqlOrderBy = 'article.lastUpdateTime ' . \ARTICLE_SORT_ORDER;
$this->articles->sqlLimit = 20;
$this->articles->readObjects();
}
Expand All @@ -85,7 +85,7 @@ protected function getRssFeed(): RssFeed
}

if ($this->articles->valid()) {
$channel->lastBuildDateFromTimestamp($this->articles->current()->getTime());
$channel->lastBuildDateFromTimestamp($this->articles->current()->lastUpdateTime);
}
$feed->channel($channel);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ public function render(mixed $value, DatabaseObject $row): string
WCF::getLanguage()->get('wcf.article.featured')
);
}
if ($row->flagAsUpdated !== 0) {
if ($badges !== '') {
$badges .= ' ';
}
$badges .= \sprintf(
'<span class="badge">%s</span>',
WCF::getLanguage()->get('wcf.article.updated')
);
}

// @phpstan-ignore property.notFound
$articleTitle = StringUtil::encodeHTML($row->title);
Expand Down Expand Up @@ -185,6 +194,25 @@ public function render(mixed $value, DatabaseObject $row): string
->sortable(defaultSortOrder: 'DESC')
->renderer(new TimeColumnRenderer())
->filter(TimeFilter::class),
GridViewColumn::for('lastUpdateTime')
->label('wcf.acp.article.lastUpdateTime')
->sortable(defaultSortOrder: 'DESC')
->renderer(new class extends TimeColumnRenderer {
#[\Override]
public function render(mixed $value, DatabaseObject $row): string
{
\assert($row instanceof Article);

// Articles that are not flagged as updated use the
// publication time, which the `time` column already shows.
if ($row->flagAsUpdated === 0) {
return '';
}

return parent::render($value, $row);
}
})
->filter(TimeFilter::class),
]);

$this->addAvailableFilters([
Expand Down Expand Up @@ -213,6 +241,7 @@ public function applyFilter(DatabaseObjectList $list, string $value): void
),
new BooleanFilter('isDeleted', 'wcf.acp.article.isDeleted'),
new BooleanFilter('isFeatured', 'wcf.article.featured'),
new BooleanFilter('flagAsUpdated', 'wcf.article.updated'),
]);
$provider = new ArticleInteractions();
$provider->addInteractions([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public function import(mixed $oldID, array $data, array $additionalData = [])
$data['enableComments'] = \ARTICLE_ENABLE_COMMENTS_DEFAULT_VALUE !== 0 ? 1 : 0;
}

if (!isset($data['lastUpdateTime'])) {
$data['lastUpdateTime'] = $data['time'] ?? \TIME_NOW;
}

// save article
$article = ArticleEditor::create($data);

Expand Down
Loading
Loading