Rewrote the Changelog code.

This commit is contained in:
flash 2023-07-15 02:05:49 +00:00
parent 6d0d49171e
commit 76c9cc50f4
28 changed files with 963 additions and 707 deletions

View file

@ -107,9 +107,7 @@ if(!empty($repoInfo['master']))
if($data->ref !== $repoMaster)
die('only the master branch is tracked');
// the actual changelog api sucks ass
$changeCreate = DB::prepare('INSERT INTO `msz_changelog_changes` (`change_log`, `change_text`, `change_action`, `user_id`, `change_created`) VALUES (:log, :text, :action, :user, FROM_UNIXTIME(:created))');
$changeTag = DB::prepare('REPLACE INTO `msz_changelog_change_tags` VALUES (:change_id, :tag_id)');
$changelog = $msz->getChangelog();
$tags = $repoInfo['tags'] ?? [];
$addresses = $config['addresses'] ?? [];
@ -126,21 +124,14 @@ foreach($data->commits as $commit) {
$line = $index === false ? $message : mb_substr($message, 0, $index);
$body = trim($index === false ? '' : mb_substr($message, $index + 1));
$changeCreate->bind('user', $addresses[$commit->author->email] ?? null);
$changeCreate->bind('action', ghcb_changelog_action($line));
$changeCreate->bind('log', $line);
$changeCreate->bind('text', empty($body) ? null : $body);
$changeCreate->bind('created', max(1, strtotime($commit->timestamp)));
$changeId = $changeCreate->executeGetId();
$changeInfo = $changelog->createChange(
ghcb_changelog_action($line),
$line, $body,
$addresses[$commit->author->email] ?? null,
max(1, strtotime($commit->timestamp))
);
if(!empty($tags) && !empty($changeId)) {
$changeTag->bind('change_id', $changeId);
foreach($tags as $tag) {
$changeTag->bind('tag_id', $tag);
$changeTag->execute();
}
}
unset($changeId, $tag);
if(!empty($tags))
foreach($tags as $tag)
$changelog->addTagToChange($changeInfo, $tag);
}

View file

@ -1,11 +1,11 @@
<?php
namespace Misuzu;
use DateTimeInterface;
use RuntimeException;
use Index\DateTime;
use Misuzu\AuditLog;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\Changelog\ChangelogChangeNotFoundException;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Changelog\ChangelogTagNotFoundException;
use Misuzu\Changelog\Changelog;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
@ -16,78 +16,105 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurren
return;
}
define('MANAGE_ACTIONS', [
['action_id' => ChangelogChange::ACTION_ADD, 'action_name' => 'Added'],
['action_id' => ChangelogChange::ACTION_REMOVE, 'action_name' => 'Removed'],
['action_id' => ChangelogChange::ACTION_UPDATE, 'action_name' => 'Updated'],
['action_id' => ChangelogChange::ACTION_FIX, 'action_name' => 'Fixed'],
['action_id' => ChangelogChange::ACTION_IMPORT, 'action_name' => 'Imported'],
['action_id' => ChangelogChange::ACTION_REVERT, 'action_name' => 'Reverted'],
]);
$changeActions = [];
foreach(Changelog::ACTIONS as $action)
$changeActions[$action] = Changelog::actionText($action);
$changeId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$tags = ChangelogTag::all();
$changelog = $msz->getChangelog();
$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$loadChangeInfo = fn() => $changelog->getChangeById($changeId, withTags: true);
$changeTags = $changelog->getAllTags();
if($changeId > 0)
try {
$change = ChangelogChange::byId($changeId);
} catch(ChangelogChangeNotFoundException $ex) {
url_redirect('manage-changelog-changes');
return;
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!empty($_POST['change']) && is_array($_POST['change'])) {
if(!isset($change)) {
$change = new ChangelogChange;
if(empty($changeId))
$isNew = true;
}
$changeUserId = filter_var($_POST['change']['user'], FILTER_SANITIZE_NUMBER_INT);
if($changeUserId === 0)
$changeUser = null;
else
try {
$changeUser = User::byId($changeUserId);
} catch(UserNotFoundException $ex) {
$changeUser = User::getCurrent();
}
$change->setHeader($_POST['change']['log'])
->setBody($_POST['change']['text'])
->setAction($_POST['change']['action'])
->setUser($changeUser)
->save();
AuditLog::create(
empty($isNew)
? AuditLog::CHANGELOG_ENTRY_EDIT
: AuditLog::CHANGELOG_ENTRY_CREATE,
[$change->getId()]
);
}
if(isset($change) && !empty($_POST['tags']) && is_array($_POST['tags'])) {
$applyTags = [];
foreach($_POST['tags'] as $tagId) {
if(!ctype_digit($tagId))
die('Invalid item encountered in roles list.');
try {
$applyTags[] = ChangelogTag::byId((int)filter_var($tagId, FILTER_SANITIZE_NUMBER_INT));
} catch(ChangelogTagNotFoundException $ex) {}
}
$change->setTags($applyTags);
}
if(!empty($isNew)) {
url_redirect('manage-changelog-change', ['change' => $change->getId()]);
$isNew = false;
$changeInfo = $loadChangeInfo();
} catch(RuntimeException $ex) {
echo render_error(404);
return;
}
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) {
$changelog->deleteChange($changeInfo);
AuditLog::create(AuditLog::CHANGELOG_ENTRY_DELETE, [$changeInfo->getId()]);
url_redirect('manage-changelog-changes');
} else render_error(403);
return;
}
// make errors not echos lol
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$action = trim((string)filter_input(INPUT_POST, 'cl_action'));
$summary = trim((string)filter_input(INPUT_POST, 'cl_summary'));
$body = trim((string)filter_input(INPUT_POST, 'cl_body'));
$userId = (int)filter_input(INPUT_POST, 'cl_user', FILTER_SANITIZE_NUMBER_INT);
$createdAt = trim((string)filter_input(INPUT_POST, 'cl_created'));
$tags = filter_input(INPUT_POST, 'cl_tags', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
if($userId < 1) $userId = null;
else $userId = (string)$userId;
if(empty($createdAt))
$createdAt = null;
else {
$createdAt = DateTime::createFromFormat(DateTimeInterface::ATOM, $createdAt . ':00Z');
if($createdAt->getUnixTimeSeconds() < 0)
$createdAt = null;
}
if($isNew) {
$changeInfo = $changelog->createChange($action, $summary, $body, $userId, $createdAt);
} else {
if($action === $changeInfo->getAction())
$action = null;
if($summary === $changeInfo->getSummary())
$summary = null;
if($body === $changeInfo->getBody())
$body = null;
if($createdAt !== null && $createdAt->equals($changeInfo->getCreatedAt()))
$createdAt = null;
$updateUserInfo = $userId !== $changeInfo->getUserId();
if($action !== null || $summary !== null || $body !== null || $createdAt !== null || $updateUserInfo)
$changelog->updateChange($changeInfo, $action, $summary, $body, $updateUserInfo, $userId, $createdAt);
}
$tCurrent = $changeInfo->getTagIds();
$tApply = $tags;
$tRemove = [];
foreach($tCurrent as $tag)
if(!in_array($tag, $tApply)) {
$tRemove[] = $tag;
$changelog->removeTagFromChange($changeInfo, $tag);
}
$tCurrent = array_diff($tCurrent, $tRemove);
foreach($tApply as $tag)
if(!in_array($tag, $tCurrent)) {
$changelog->addTagToChange($changeInfo, $tag);
$tCurrent[] = $tag;
}
AuditLog::create(
$isNew ? AuditLog::CHANGELOG_ENTRY_CREATE : AuditLog::CHANGELOG_ENTRY_EDIT,
[$changeInfo->getId()]
);
if($isNew) {
url_redirect('manage-changelog-change', ['change' => $changeInfo->getId()]);
return;
} else $changeInfo = $loadChangeInfo();
break;
}
Template::render('manage.changelog.change', [
'change' => $change ?? null,
'change_tags' => $tags,
'change_actions' => MANAGE_ACTIONS,
'change_new' => $isNew,
'change_info' => $changeInfo ?? null,
'change_tags' => $changeTags,
'change_actions' => $changeActions,
]);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -11,14 +10,38 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurren
return;
}
$changelogPagination = new Pagination(ChangelogChange::countAll(), 30);
$changelog = $msz->getChangelog();
$changelogPagination = new Pagination($changelog->countAllChanges(), 30);
if(!$changelogPagination->hasValidOffset()) {
echo render_error(404);
return;
}
$changes = ChangelogChange::all($changelogPagination);
$changeInfos = $changelog->getAllChanges(withTags: true, pagination: $changelogPagination);
$changes = [];
$userInfos = [];
foreach($changeInfos as $changeInfo) {
$userId = $changeInfo->getUserId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
$changes[] = [
'change' => $changeInfo,
'user' => $userInfo,
];
}
Template::render('manage.changelog.changes', [
'changelog_changes' => $changes,

View file

@ -1,9 +1,8 @@
<?php
namespace Misuzu;
use RuntimeException;
use Misuzu\AuditLog;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Changelog\ChangelogTagNotFoundException;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -13,40 +12,62 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurren
return;
}
$tagId = (int)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
$changelog = $msz->getChangelog();
$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
$loadTagInfo = fn() => $changelog->getTagById($tagId);
if($tagId > 0)
if(empty($tagId))
$isNew = true;
else
try {
$tagInfo = ChangelogTag::byId($tagId);
} catch(ChangelogTagNotFoundException $ex) {
url_redirect('manage-changelog-tags');
$isNew = false;
$tagInfo = $loadTagInfo();
} catch(RuntimeException $ex) {
echo render_error(404);
return;
}
if(!empty($_POST['tag']) && is_array($_POST['tag']) && CSRF::validateRequest()) {
if(!isset($tagInfo)) {
$tagInfo = new ChangelogTag;
$isNew = true;
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
if(CSRF::validateRequest()) {
$changelog->deleteTag($tagInfo);
AuditLog::create(AuditLog::CHANGELOG_TAG_DELETE, [$tagInfo->getId()]);
url_redirect('manage-changelog-tags');
} else render_error(403);
return;
}
$tagInfo->setName($_POST['tag']['name'])
->setDescription($_POST['tag']['description'])
->setArchived(!empty($_POST['tag']['archived']))
->save();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$name = trim((string)filter_input(INPUT_POST, 'ct_name'));
$description = trim((string)filter_input(INPUT_POST, 'ct_desc'));
$archive = !empty($_POST['ct_archive']);
if($isNew) {
$tagInfo = $changelog->createTag($name, $description, $archive);
} else {
if($name === $tagInfo->getName())
$name = null;
if($description === $tagInfo->getDescription())
$description = null;
if($archive === $tagInfo->isArchived())
$archive = null;
if($name !== null || $description !== null || $archive !== null)
$changelog->updateTag($tagInfo, $name, $description, $archive);
}
AuditLog::create(
empty($isNew)
? AuditLog::CHANGELOG_TAG_EDIT
: AuditLog::CHANGELOG_TAG_CREATE,
$isNew ? AuditLog::CHANGELOG_TAG_CREATE : AuditLog::CHANGELOG_TAG_EDIT,
[$tagInfo->getId()]
);
if(!empty($isNew)) {
if($isNew) {
url_redirect('manage-changelog-tag', ['tag' => $tagInfo->getId()]);
return;
}
} else $tagInfo = $loadTagInfo();
break;
}
Template::render('manage.changelog.tag', [
'edit_tag' => $tagInfo ?? null,
'tag_new' => $isNew,
'tag_info' => $tagInfo ?? null,
]);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
@ -12,5 +11,5 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurren
}
Template::render('manage.changelog.tags', [
'changelog_tags' => ChangelogTag::all(),
'changelog_tags' => $msz->getChangelog()->getAllTags(),
]);

View file

@ -49,7 +49,6 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if($order == 0)
$order = null;
$reload = false;
if($isNew) {
$emoteInfo = $emotes->createEmote($url, $minRank, $order);
} else {
@ -60,11 +59,9 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if($url === $emoteInfo->getUrl())
$url = null;
if($order !== null || $minRank !== null || $url !== null) {
$reload = true;
if($order !== null || $minRank !== null || $url !== null)
$emotes->updateEmote($emoteInfo, $order, $minRank, $url);
}
}
$sCurrent = $emoteInfo->getStringsRaw();
$sApply = $strings;
@ -78,9 +75,6 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$sCurrent = array_diff($sCurrent, $sRemove);
if(!$reload)
$reload = !empty($sRemove) || !empty(array_diff($sApply, $sCurrent));
foreach($sApply as $string)
if(!in_array($string, $sCurrent)) {
$checkString = $emotes->checkEmoteString($string);
@ -101,12 +95,15 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$sCurrent[] = $string;
}
if($reload) {
if($isNew)
AuditLog::create(
$isNew ? AuditLog::EMOTICON_CREATE : AuditLog::EMOTICON_EDIT,
[$emoteInfo->getId()]
);
if($isNew) {
url_redirect('manage-general-emoticon', ['emote' => $emoteInfo->getId()]);
else
$emoteInfo = $loadEmoteInfo();
}
return;
} else $emoteInfo = $loadEmoteInfo();
break;
}

View file

@ -25,17 +25,21 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
if(!empty($_GET['delete'])) {
$emotes->deleteEmote($emoteInfo);
AuditLog::create(AuditLog::EMOTICON_DELETE, [$emoteInfo->getId()]);
} else {
if(isset($_GET['order'])) {
$order = filter_input(INPUT_GET, 'order');
$offset = $order === 'i' ? 1 : ($order === 'd' ? -1 : 0);
$emotes->updateEmoteOrderOffset($emoteInfo, $offset);
AuditLog::create(AuditLog::EMOTICON_ORDER, [$emoteInfo->getId()]);
}
if(isset($_GET['alias'])) {
$alias = (string)filter_input(INPUT_GET, 'alias');
if($emotes->checkEmoteString($alias) === '')
if($emotes->checkEmoteString($alias) === '') {
$emotes->addEmoteString($emoteInfo, $alias);
AuditLog::create(AuditLog::EMOTICON_ALIAS, [$emoteInfo->getId(), $alias]);
}
}
}

View file

@ -21,6 +21,7 @@ class AuditLog {
public const CHANGELOG_TAG_REMOVE = 'CHANGELOG_TAG_REMOVE';
public const CHANGELOG_TAG_CREATE = 'CHANGELOG_TAG_CREATE';
public const CHANGELOG_TAG_EDIT = 'CHANGELOG_TAG_EDIT';
public const CHANGELOG_TAG_DELETE = 'CHANGELOG_TAG_DELETE';
public const CHANGELOG_ACTION_CREATE = 'CHANGELOG_ACTION_CREATE';
public const CHANGELOG_ACTION_EDIT = 'CHANGELOG_ACTION_EDIT';
@ -51,6 +52,12 @@ class AuditLog {
public const CONFIG_UPDATE = 'CONFIG_UPDATE';
public const CONFIG_DELETE = 'CONFIG_DELETE';
public const EMOTICON_CREATE = 'EMOTICON_CREATE';
public const EMOTICON_EDIT = 'EMOTICON_EDIT';
public const EMOTICON_DELETE = 'EMOTICON_DELETE';
public const EMOTICON_ORDER = 'EMOTICON_ORDER';
public const EMOTICON_ALIAS = 'EMOTICON_ALIAS';
public const FORMATS = [
self::PERSONAL_EMAIL_CHANGE => 'Changed e-mail address to %s.',
self::PERSONAL_PASSWORD_CHANGE => 'Changed account password.',
@ -66,6 +73,7 @@ class AuditLog {
self::CHANGELOG_TAG_REMOVE => 'Removed tag #%2$d from changelog entry #%1$d.',
self::CHANGELOG_TAG_CREATE => 'Created new changelog tag #%d.',
self::CHANGELOG_TAG_EDIT => 'Edited changelog tag #%d.',
self::CHANGELOG_TAG_DELETE => 'Deleted changelog tag #%d.',
self::CHANGELOG_ACTION_CREATE => 'Created new changelog action #%d.',
self::CHANGELOG_ACTION_EDIT => 'Edited changelog action #%d.',
@ -95,6 +103,12 @@ class AuditLog {
self::CONFIG_CREATE => 'Created config value with name "%s".',
self::CONFIG_UPDATE => 'Updated config value with name "%s".',
self::CONFIG_DELETE => 'Deleted config value with name "%s".',
self::EMOTICON_CREATE => 'Created emoticon #%s.',
self::EMOTICON_EDIT => 'Edited emoticon #%s.',
self::EMOTICON_DELETE => 'Deleted emoticon #%s.',
self::EMOTICON_ORDER => 'Changed order of emoticon #%s.',
self::EMOTICON_ALIAS => 'Added alias "%2$s" to emoticon #%1$s.',
];
// Database fields

View file

@ -0,0 +1,88 @@
<?php
namespace Misuzu\Changelog;
use Index\DateTime;
use Index\Data\IDbResult;
class ChangeInfo {
private string $id;
private ?string $userId;
private int $action;
private int $created;
private string $summary;
private string $body;
private array $tags;
public function __construct(IDbResult $result, array $tags = []) {
$this->id = (string)$result->getInteger(0);
$this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1);
$this->action = $result->getInteger(2);
$this->created = $result->getInteger(3);
$this->summary = $result->getString(4);
$this->body = $result->getString(5);
$this->tags = $tags;
}
public function getId(): string {
return $this->id;
}
public function getUserId(): ?string {
return $this->userId;
}
public function getActionId(): int {
return $this->action;
}
public function getAction(): string {
return Changelog::convertFromActionId($this->action);
}
public function getActionText(): string {
return Changelog::actionText($this->action);
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function getDate(): string {
return gmdate('Y-m-d', $this->created);
}
public function getSummary(): string {
return $this->summary;
}
public function hasBody(): bool {
return !empty($this->body);
}
public function getBody(): string {
return $this->body;
}
public function getCommentsCategoryName(): string {
return sprintf('changelog-date-%s', $this->getDate());
}
public function getTags(): array {
return $this->tags;
}
public function getTagIds(): array {
$ids = [];
foreach($this->tags as $tagInfo)
$ids[] = $tagInfo->getId();
return $ids;
}
public function hasTag(ChangeTagInfo|string $infoOrId): bool {
return in_array($infoOrId, $this->tags);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Misuzu\Changelog;
use Stringable;
use Index\DateTime;
use Index\Data\IDbResult;
class ChangeTagInfo implements Stringable {
private string $id;
private string $name;
private string $description;
private int $created;
private int $archived;
private int $changes;
public function __construct(IDbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->name = $result->getString(1);
$this->description = $result->getString(2);
$this->created = $result->getInteger(3);
$this->archived = $result->getInteger(4);
$this->changes = $result->getInteger(5);
}
public function getId(): string {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getDescription(): string {
return $this->description;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function getArchivedTime(): int {
return $this->archived;
}
public function getArchivedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function isArchived(): bool {
return $this->archived > 0;
}
public function getChangesCount(): int {
return $this->changes;
}
public function __toString(): string {
return $this->id;
}
}

426
src/Changelog/Changelog.php Normal file
View file

@ -0,0 +1,426 @@
<?php
namespace Misuzu\Changelog;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\DbStatementCache;
use Misuzu\Pagination;
use Misuzu\Users\User;
class Changelog {
// not a strict list but useful to have
public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert'];
private IDbConnection $dbConn;
private DbStatementCache $cache;
private array $tags = [];
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
public static function convertFromActionId(int $actionId): string {
return match($actionId) {
1 => 'add',
2 => 'remove',
3 => 'update',
4 => 'fix',
5 => 'import',
6 => 'revert',
default => 'unknown',
};
}
public static function convertToActionId(string $action): int {
return match($action) {
'add' => 1,
'remove' => 2,
'update' => 3,
'fix' => 4,
'import' => 5,
'revert' => 6,
default => 0,
};
}
public static function actionText(string|int $action): string {
if(is_int($action))
$action = self::convertFromActionId($action);
return match($action) {
'add' => 'Added',
'remove' => 'Removed',
'update' => 'Updated',
'fix' => 'Fixed',
'import' => 'Imported',
'revert' => 'Reverted',
default => 'Changed',
};
}
private function readChanges(IDbResult $result, bool $withTags): array {
$changes = [];
if($withTags) {
while($result->next())
$changes[] = new ChangeInfo(
$result,
$this->getTagsByChange((string)$result->getInteger(0))
);
} else {
while($result->next())
$changes[] = new ChangeInfo($result);
}
return $changes;
}
private function readTags(IDbResult $result): array {
$tags = [];
while($result->next()) {
$tagId = (string)$result->getInteger(0);
if(array_key_exists($tagId, $this->tags))
$tags[] = $this->tags[$tagId];
else
$tags[] = $this->tags[$tagId] = new ChangeTagInfo($result);
}
return $tags;
}
public function countAllChanges(
User|string|null $userInfo = null,
DateTime|int|null $dateTime = null,
?array $tags = null
): int {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds();
$args = 0;
$hasUserInfo = $userInfo !== null;
$hasDateTime = $dateTime !== null;
$hasTags = !empty($tags);
$query = 'SELECT COUNT(*) FROM msz_changelog_changes';
if($hasUserInfo) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' user_id = ?';
}
if($hasDateTime) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))';
}
if($hasTags) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= sprintf(
' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))',
implode(', ', array_fill(0, count($tags), '?'))
);
}
$stmt = $this->cache->get($query);
$args = 0;
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasDateTime)
$stmt->addParameter(++$args, $dateTime);
if($hasTags)
foreach($tags as $tag)
$stmt->addParameter(++$args, (string)$tag);
$stmt->execute();
$result = $stmt->getResult();
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function getAllChanges(
bool $withTags = false,
User|string|null $userInfo = null,
DateTime|int|null $dateTime = null,
?array $tags = null,
?Pagination $pagination = null
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds();
$args = 0;
$hasUserInfo = $userInfo !== null;
$hasDateTime = $dateTime !== null;
$hasTags = !empty($tags);
$hasPagination = $pagination !== null;
$query = 'SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes';
if($hasUserInfo) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' user_id = ?';
}
if($hasDateTime) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))';
}
if($hasTags) {
$query .= (++$args > 1 ? ' AND' : ' WHERE');
$query .= sprintf(
' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))',
implode(', ', array_fill(0, count($tags), '?'))
);
}
$query .= ' GROUP BY change_created, change_id ORDER BY change_created DESC, change_id DESC';
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$stmt = $this->cache->get($query);
$args = 0;
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasDateTime)
$stmt->addParameter(++$args, $dateTime);
if($hasTags)
foreach($tags as $tag)
$stmt->addParameter(++$args, (string)$tag);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return self::readChanges($stmt->getResult(), $withTags);
}
public function getChangeById(string $changeId, bool $withTags = false): ChangeInfo {
$stmt = $this->cache->get('SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes WHERE change_id = ?');
$stmt->addParameter(1, $changeId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No tag with that ID exists.');
$tags = [];
if($withTags)
$tags = $this->getTagsByChange((string)$result->getInteger(0));
return new ChangeInfo($result, $tags);
}
public function createChange(
string|int $action,
string $summary,
string $body = '',
User|string|null $userInfo = null,
DateTime|int|null $createdAt = null
): ChangeInfo {
if(is_string($action))
$action = self::convertToActionId($action);
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds();
$summary = trim($summary);
if(empty($summary))
throw new InvalidArgumentException('$summary may not be empty');
$body = trim($body);
if(empty($body))
$body = null;
$stmt = $this->cache->get('INSERT INTO msz_changelog_changes (user_id, change_action, change_created, change_log, change_text) VALUES (?, ?, FROM_UNIXTIME(?), ?, ?)');
$stmt->addParameter(1, $userInfo);
$stmt->addParameter(2, $action);
$stmt->addParameter(3, $createdAt);
$stmt->addParameter(4, $summary);
$stmt->addParameter(5, $body);
$stmt->execute();
return $this->getChangeById((string)$this->dbConn->getLastInsertId());
}
public function deleteChange(ChangeInfo|string $infoOrId): void {
if($infoOrId instanceof ChangeInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('DELETE FROM msz_changelog_changes WHERE change_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function updateChange(
ChangeInfo|string $infoOrId,
string|int|null $action = null,
?string $summary = null,
?string $body = null,
bool $updateUserInfo = false,
User|string|null $userInfo = null,
DateTime|int|null $createdAt = null
): void {
if($infoOrId instanceof ChangeInfo)
$infoOrId = $infoOrId->getId();
if(is_string($action))
$action = self::convertToActionId($action);
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds();
if($summary !== null) {
$summary = trim($summary);
if(empty($summary))
throw new InvalidArgumentException('$summary may not be empty');
}
$hasBody = $body !== null;
if($hasBody) {
$body = trim($body);
if(empty($body))
$body = null;
}
$stmt = $this->cache->get('UPDATE msz_changelog_changes SET change_action = COALESCE(?, change_action), change_log = COALESCE(?, change_log), change_text = IF(?, ?, change_text), user_id = IF(?, ?, user_id), change_created = COALESCE(FROM_UNIXTIME(?), change_created) WHERE change_id = ?');
$stmt->addParameter(1, $action);
$stmt->addParameter(2, $summary);
$stmt->addParameter(3, $hasBody ? 1 : 0);
$stmt->addParameter(4, $body);
$stmt->addParameter(5, $updateUserInfo ? 1 : 0);
$stmt->addParameter(6, $userInfo);
$stmt->addParameter(7, $createdAt);
$stmt->addParameter(8, $infoOrId);
$stmt->execute();
}
public function getAllTags(): array {
// only putting the changes count in here for now, it is only used in manage
return $this->readTags(
$this->dbConn->query('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), (SELECT COUNT(*) FROM msz_changelog_change_tags AS ct WHERE ct.tag_id = t.tag_id) AS `tag_changes` FROM msz_changelog_tags AS t')
);
}
public function getTagsByChange(ChangeInfo|string $infoOrId): array {
if($infoOrId instanceof ChangeInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), 0 AS `tag_changes` FROM msz_changelog_tags WHERE tag_id IN (SELECT tag_id FROM msz_changelog_change_tags WHERE change_id = ?)');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
return $this->readTags($stmt->getResult());
}
public function getTagById(string $tagId): ChangeTagInfo {
$stmt = $this->cache->get('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), 0 AS `tag_changes` FROM msz_changelog_tags WHERE tag_id = ?');
$stmt->addParameter(1, $tagId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No tag with that ID exists.');
return new ChangeTagInfo($result);
}
public function createTag(
string $name,
string $description,
bool $archived
): ChangeTagInfo {
$name = trim($name);
if(empty($name))
throw new InvalidArgumentException('$name may not be empty');
$description = trim($description);
if(empty($description))
$description = null;
$stmt = $this->cache->get('INSERT INTO msz_changelog_tags (tag_name, tag_description, tag_archived) VALUES (?, ?, IF(?, NOW(), NULL))');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $description);
$stmt->addParameter(3, $archived ? 1 : 0);
$stmt->execute();
return $this->getTagById((string)$this->dbConn->getLastInsertId());
}
public function deleteTag(ChangeTagInfo|string $infoOrId): void {
if($infoOrId instanceof ChangeTagInfo)
$infoOrId = $infoOrId->getId();
$stmt = $this->cache->get('DELETE FROM msz_changelog_tags WHERE tag_id = ?');
$stmt->addParameter(1, $infoOrId);
$stmt->execute();
}
public function updateTag(
ChangeTagInfo|string $infoOrId,
?string $name = null,
?string $description = null,
?bool $archived = null
): void {
if($infoOrId instanceof ChangeTagInfo)
$infoOrId = $infoOrId->getId();
if($name !== null) {
$name = trim($name);
if(empty($name))
throw new InvalidArgumentException('$name may not be empty');
}
$hasDescription = $description !== null;
if($hasDescription) {
$description = trim($description);
if(empty($description))
$description = null;
}
$hasArchived = $archived !== null;
$stmt = $this->cache->get('UPDATE msz_changelog_tags SET tag_name = COALESCE(?, tag_name), tag_description = IF(?, ?, tag_description), tag_archived = IF(?, IF(?, NOW(), NULL), tag_archived) WHERE tag_id = ?');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $hasDescription ? 1 : 0);
$stmt->addParameter(3, $description);
$stmt->addParameter(4, $hasArchived ? 1 : 0);
$stmt->addParameter(5, $archived ? 1 : 0);
$stmt->addParameter(6, $infoOrId);
$stmt->execute();
}
public function addTagToChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void {
if($change instanceof ChangeInfo)
$change = $change->getId();
if($tag instanceof ChangeTagInfo)
$tag = $tag->getId();
$stmt = $this->cache->get('INSERT INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)');
$stmt->addParameter(1, $change);
$stmt->addParameter(2, $tag);
$stmt->execute();
}
public function removeTagFromChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void {
if($change instanceof ChangeInfo)
$change = $change->getId();
if($tag instanceof ChangeTagInfo)
$tag = $tag->getId();
$stmt = $this->cache->get('DELETE FROM msz_changelog_change_tags WHERE change_id = ? AND tag_id = ?');
$stmt->addParameter(1, $change);
$stmt->addParameter(2, $tag);
$stmt->execute();
}
}

View file

@ -1,19 +1,6 @@
<?php
namespace Misuzu\Changelog;
use UnexpectedValueException;
use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class ChangelogChangeException extends ChangelogException {}
class ChangelogChangeNotFoundException extends ChangelogChangeException {}
class ChangelogChange {
public const ACTION_UNKNOWN = 0;
public const ACTION_ADD = 1;
@ -22,247 +9,4 @@ class ChangelogChange {
public const ACTION_FIX = 4;
public const ACTION_IMPORT = 5;
public const ACTION_REVERT = 6;
private const ACTION_STRINGS = [
self::ACTION_UNKNOWN => ['unknown', 'Changed'],
self::ACTION_ADD => ['add', 'Added'],
self::ACTION_REMOVE => ['remove', 'Removed'],
self::ACTION_UPDATE => ['update', 'Updated'],
self::ACTION_FIX => ['fix', 'Fixed'],
self::ACTION_IMPORT => ['import', 'Imported'],
self::ACTION_REVERT => ['revert', 'Reverted'],
];
public const DEFAULT_DATE = '0000-00-00';
// Database fields
private $change_id = -1;
private $user_id = null;
private $change_action = null; // defaults null apparently, probably a previous oversight
private $change_created = null;
private $change_log = '';
private $change_text = '';
private $user = null;
private $userLookedUp = false;
private $comments = null;
private $tags = null;
private $tagRelations = null;
public const TABLE = 'changelog_changes';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`change_id`, %1$s.`user_id`, %1$s.`change_action`, %1$s.`change_log`, %1$s.`change_text`'
. ', UNIX_TIMESTAMP(%1$s.`change_created`) AS `change_created`';
public function getId(): int {
return $this->change_id < 1 ? -1 : $this->change_id;
}
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function setUserId(?int $userId): self {
$this->user_id = $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
public function getUser(): ?User {
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user;
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}
public function getAction(): int {
return $this->change_action ?? self::ACTION_UNKNOWN;
}
public function setAction(int $actionId): self {
$this->change_action = $actionId;
return $this;
}
private function getActionInfo(): array {
return self::ACTION_STRINGS[$this->getAction()] ?? self::ACTION_STRINGS[self::ACTION_UNKNOWN];
}
public function getActionClass(): string {
return $this->getActionInfo()[0];
}
public function getActionString(): string {
return $this->getActionInfo()[1];
}
public function getCreatedTime(): int {
return $this->change_created ?? -1;
}
public function getDate(): string {
return ($time = $this->getCreatedTime()) < 0 ? self::DEFAULT_DATE : gmdate('Y-m-d', $time);
}
public function getHeader(): string {
return $this->change_log;
}
public function setHeader(string $header): self {
$this->change_log = $header;
return $this;
}
public function getBody(): string {
return $this->change_text ?? '';
}
public function setBody(string $body): self {
$this->change_text = $body;
return $this;
}
public function hasBody(): bool {
return !empty($this->change_text);
}
public function getParsedBody(): string {
return Parser::instance(Parser::MARKDOWN)->parseText($this->getBody());
}
public function getCommentsCategoryName(): ?string {
return ($date = $this->getDate()) === self::DEFAULT_DATE ? null : sprintf('changelog-date-%s', $this->getDate());
}
public function hasCommentsCategory(): bool {
return $this->getCreatedTime() >= 0;
}
public function getCommentsCategory(): CommentsCategory {
if($this->comments === null) {
$categoryName = $this->getCommentsCategoryName();
if(empty($categoryName))
throw new UnexpectedValueException('Change comments category name is empty.');
try {
$this->comments = CommentsCategory::byName($categoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$this->comments = new CommentsCategory($categoryName);
$this->comments->save();
}
}
return $this->comments;
}
public function getTags(): array {
if($this->tags === null)
$this->tags = ChangelogTag::byChange($this);
return $this->tags;
}
public function getTagRelations(): array {
if($this->tagRelations === null)
$this->tagRelations = ChangelogChangeTag::byChange($this);
return $this->tagRelations;
}
public function setTags(array $tags): self {
ChangelogChangeTag::purgeChange($this);
foreach($tags as $tag)
if($tag instanceof ChangelogTag)
ChangelogChangeTag::create($this, $tag);
$this->tags = $tags;
$this->tagRelations = null;
return $this;
}
public function hasTag(ChangelogTag $other): bool {
foreach($this->getTags() as $tag)
if($tag->compare($other))
return true;
return false;
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`user_id`, `change_action`, `change_log`, `change_text`)'
. ' VALUES (:user, :action, :header, :body)';
} else {
$query = 'UPDATE `%1$s%2$s` SET `user_id` = :user, `change_action` = :action, `change_log` = :header, `change_text` = :body'
. ' WHERE `change_id` = :change';
}
$saveChange = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('user', $this->user_id)
->bind('action', $this->change_action)
->bind('header', $this->change_log)
->bind('body', $this->change_text);
if($isInsert) {
$this->change_id = $saveChange->executeGetId();
$this->change_created = time();
} else {
$saveChange->bind('change', $this->getId())
->execute();
}
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`change_id`)', self::TABLE));
}
public static function countAll(?int $date = null, ?User $user = null): int {
$countChanges = DB::prepare(
self::countQueryBase()
. ' WHERE 1' // this is still disgusting
. ($date === null ? '' : ' AND DATE(`change_created`) = :date')
. ($user === null ? '' : ' AND `user_id` = :user')
);
if($date !== null)
$countChanges->bind('date', gmdate('Y-m-d', $date));
if($user !== null)
$countChanges->bind('user', $user->getId());
return (int)$countChanges->fetchColumn();
}
private static function memoizer(): Memoizer {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $changeId): self {
return self::memoizer()->find($changeId, function() use ($changeId) {
$change = DB::prepare(self::byQueryBase() . ' WHERE `change_id` = :change')
->bind('change', $changeId)
->fetchObject(self::class);
if(!$change)
throw new ChangelogChangeNotFoundException;
return $change;
});
}
public static function all(?Pagination $pagination = null, ?int $date = null, ?User $user = null): array {
$changeQuery = self::byQueryBase()
. ' WHERE 1' // this is still disgusting
. ($date === null ? '' : ' AND DATE(`change_created`) = :date')
. ($user === null ? '' : ' AND `user_id` = :user')
. ' GROUP BY `change_created`, `change_id`'
. ' ORDER BY `change_created` DESC, `change_id` DESC';
if($pagination !== null)
$changeQuery .= ' LIMIT :range OFFSET :offset';
$getChanges = DB::prepare($changeQuery);
if($date !== null)
$getChanges->bind('date', gmdate('Y-m-d', $date));
if($user !== null)
$getChanges->bind('user', $user->getId());
if($pagination !== null)
$getChanges->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getChanges->fetchObjects(self::class);
}
}

View file

@ -1,88 +0,0 @@
<?php
namespace Misuzu\Changelog;
use Misuzu\DB;
class ChangelogChangeTagException extends ChangelogException {}
class ChangelogChangeTagNotFoundException extends ChangelogChangeTagException {}
class ChangelogChangeCreationFailedException extends ChangelogChangeTagException {}
class ChangelogChangeTag {
// Database fields
private $change_id = -1;
private $tag_id = -1;
private $change = null;
private $tag = null;
public const TABLE = 'changelog_change_tags';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`change_id`, %1$s.`tag_id`';
public function getChangeId(): int {
return $this->change_id < 1 ? -1 : $this->change_id;
}
public function getChange(): ChangelogChange {
if($this->change === null)
$this->change = ChangelogChange::byId($this->getChangeId());
return $this->change;
}
public function getTagId(): int {
return $this->tag_id < 1 ? -1 : $this->tag_id;
}
public function getTag(): ChangelogTag {
if($this->tag === null)
$this->tag = ChangelogTag::byId($this->getTagId());
return $this->tag;
}
public static function create(ChangelogChange $change, ChangelogTag $tag, bool $return = false): ?self {
$createRelation = DB::prepare(
'REPLACE INTO `' . DB::PREFIX . self::TABLE . '` (`change_id`, `tag_id`)'
. ' VALUES (:change, :tag)'
)->bind('change', $change->getId())->bind('tag', $tag->getId());
if(!$createRelation->execute())
throw new ChangelogChangeCreationFailedException;
if(!$return)
return null;
return self::byExact($change, $tag);
}
public static function purgeChange(ChangelogChange $change): void {
DB::prepare(
'DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `change_id` = :change'
)->bind('change', $change->getId())->execute();
}
private static function countQueryBase(string $column): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`%s`)', self::TABLE, $column));
}
public static function countByTag(ChangelogTag $tag): int {
return (int)DB::prepare(
self::countQueryBase('change_id')
. ' WHERE `tag_id` = :tag'
)->bind('tag', $tag->getId())->fetchColumn();
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byExact(ChangelogChange $change, ChangelogTag $tag): self {
$tag = DB::prepare(self::byQueryBase() . ' WHERE `tag_id` = :tag')
->bind('change', $change->getId())
->bind('tag', $tag->getId())
->fetchObject(self::class);
if(!$tag)
throw new ChangelogChangeTagNotFoundException;
return $tag;
}
public static function byChange(ChangelogChange $change): array {
return DB::prepare(
self::byQueryBase()
. ' WHERE `change_id` = :change'
)->bind('change', $change->getId())->fetchObjects(self::class);
}
}

View file

@ -1,6 +0,0 @@
<?php
namespace Misuzu\Changelog;
use RuntimeException;
class ChangelogException extends RuntimeException {}

View file

@ -1,129 +0,0 @@
<?php
namespace Misuzu\Changelog;
use Misuzu\DB;
use Misuzu\Memoizer;
class ChangelogTagException extends ChangelogException {}
class ChangelogTagNotFoundException extends ChangelogTagException {}
class ChangelogTag {
// Database fields
private $tag_id = -1;
private $tag_name = '';
private $tag_description = '';
private $tag_created = null;
private $tag_archived = null;
private $changeCount = -1;
public const TABLE = 'changelog_tags';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`tag_id`, %1$s.`tag_name`, %1$s.`tag_description`'
. ', UNIX_TIMESTAMP(%1$s.`tag_created`) AS `tag_created`'
. ', UNIX_TIMESTAMP(%1$s.`tag_archived`) AS `tag_archived`';
public function getId(): int {
return $this->tag_id < 1 ? -1 : $this->tag_id;
}
public function getName(): string {
return $this->tag_name;
}
public function setName(string $name): self {
$this->tag_name = $name;
return $this;
}
public function getDescription(): string {
return $this->tag_description;
}
public function hasDescription(): bool {
return !empty($this->tag_description);
}
public function setDescription(string $description): self {
$this->tag_description = $description;
return $this;
}
public function getCreatedTime(): int {
return $this->tag_created ?? -1;
}
public function getArchivedTime(): int {
return $this->tag_archived ?? -1;
}
public function isArchived(): bool {
return $this->getArchivedTime() >= 0;
}
public function setArchived(bool $archived): self {
if($this->isArchived() !== $archived)
$this->tag_archived = $archived ? time() : null;
return $this;
}
public function getChangeCount(): int {
if($this->changeCount < 0)
$this->changeCount = ChangelogChangeTag::countByTag($this);
return $this->changeCount;
}
public function compare(ChangelogTag $other): bool {
return $other === $this || $other->getId() === $this->getId();
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`tag_name`, `tag_description`, `tag_archived`)'
. ' VALUES (:name, :description, FROM_UNIXTIME(:archived))';
} else {
$query = 'UPDATE `%1$s%2$s` SET `tag_name` = :name, `tag_description` = :description, `tag_archived` = FROM_UNIXTIME(:archived)'
. ' WHERE `tag_id` = :tag';
}
$saveTag = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('name', $this->tag_name)
->bind('description', $this->tag_description)
->bind('archived', $this->tag_archived);
if($isInsert) {
$this->tag_id = $saveTag->executeGetId();
$this->tag_created = time();
} else {
$saveTag->bind('tag', $this->getId())
->execute();
}
}
private static function memoizer(): Memoizer {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $tagId): self {
return self::memoizer()->find($tagId, function() use ($tagId) {
$tag = DB::prepare(self::byQueryBase() . ' WHERE `tag_id` = :tag')
->bind('tag', $tagId)
->fetchObject(self::class);
if(!$tag)
throw new ChangelogTagNotFoundException;
return $tag;
});
}
public static function byChange(ChangelogChange $change): array {
return DB::prepare(
self::byQueryBase()
. ' WHERE `tag_id` IN (SELECT `tag_id` FROM `' . DB::PREFIX . ChangelogChangeTag::TABLE . '` WHERE `change_id` = :change)'
)->bind('change', $change->getId())->fetchObjects(self::class);
}
public static function all(): array {
return DB::prepare(self::byQueryBase())
->fetchObjects(self::class);
}
}

View file

@ -28,7 +28,7 @@ class Emotes {
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('No emoticon with ID exists.');
throw new RuntimeException('No emoticon with that ID exists.');
$strings = $withStrings ? $this->getEmoteStrings($emoteId) : [];

View file

@ -2,6 +2,7 @@
namespace Misuzu\Http\Handlers;
use Misuzu\GitInfo;
use Misuzu\MisuzuContext;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\Assets\StaticUserImageAsset;
@ -20,9 +21,9 @@ final class AssetsHandler extends Handler {
],
];
public function __construct() {
public function __construct(MisuzuContext $context) {
$GLOBALS['misuzuBypassLockdown'] = true;
parent::__construct();
parent::__construct($context);
}
private static function recurse(string $dir): string {

View file

@ -2,14 +2,13 @@
namespace Misuzu\Http\Handlers;
use ErrorException;
use RuntimeException;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\Changelog\ChangelogChangeNotFoundException;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Changelog\ChangelogTagNotFoundException;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
@ -19,11 +18,13 @@ use Misuzu\Users\UserNotFoundException;
class ChangelogHandler extends Handler {
public function index($response, $request) {
$filterDate = $request->getParam('date');
$filterUser = $request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
//$filterTags = $request->getParam('tags');
$filterDate = (string)$request->getParam('date');
$filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
$filterTags = (string)$request->getParam('tags');
if($filterDate !== null)
if(empty($filterDate))
$filterDate = null;
else
try {
$dateParts = explode('-', $filterDate, 3);
$filterDate = gmmktime(12, 0, 0, (int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]);
@ -31,57 +32,102 @@ class ChangelogHandler extends Handler {
return 404;
}
if($filterUser !== null)
if($filterUser > 0)
try {
$filterUser = User::byId($filterUser);
} catch(UserNotFoundException $ex) {
return 404;
}
else
$filterUser = null;
/*if($filterTags !== null) {
$splitTags = explode(',', $filterTags);
$filterTags = [];
for($i = 0; $i < min(10, count($splitTags)); ++$i)
try {
$filterTags[] = ChangelogTag::byId($splitTags[$i]);
} catch(ChangelogTagNotFoundException $ex) {
return 404;
if(empty($filterTags))
$filterTags = null;
else {
$filterTags = explode(',', $filterTags);
foreach($filterTags as &$tag)
$tag = trim($tag);
}
}*/
$count = $filterDate !== null ? -1 : ChangelogChange::countAll($filterDate, $filterUser);
$changelog = $this->context->getChangelog();
$count = $changelog->countAllChanges($filterUser, $filterDate, $filterTags);
$pagination = new Pagination($count, 30);
if(!$pagination->hasValidOffset())
return 404;
$changes = ChangelogChange::all($pagination, $filterDate, $filterUser);
if(empty($changes))
$changeInfos = $changelog->getAllChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
if(empty($changeInfos))
return 404;
$changes = [];
$userInfos = [];
foreach($changeInfos as $changeInfo) {
$userId = $changeInfo->getUserId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
$changes[] = [
'change' => $changeInfo,
'user' => $userInfo,
];
}
$response->setContent(Template::renderRaw('changelog.index', [
'changelog_infos' => $changes,
'changelog_date' => $filterDate,
'changelog_user' => $filterUser,
'changelog_tags' => $filterTags,
'changelog_pagination' => $pagination,
'comments_user' => User::getCurrent(),
'comments_category' => empty($filterDate) ? null : self::getCommentsCategory($changeInfos[0]->getCommentsCategoryName()),
]));
}
public function change($response, $request, int $changeId) {
private static function getCommentsCategory(string $categoryName): CommentsCategory {
try {
$changeInfo = ChangelogChange::byId($changeId);
} catch(ChangelogChangeNotFoundException $ex) {
$category = CommentsCategory::byName($categoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$category = new CommentsCategory($categoryName);
$category->save();
}
return $category;
}
public function change($response, $request, string $changeId) {
try {
$changeInfo = $this->context->getChangelog()->getChangeById($changeId, withTags: true);
} catch(RuntimeException $ex) {
return 404;
}
try {
$userInfo = User::byId($changeInfo->getUserId());
} catch(UserNotFoundException $ex) {
$userInfo = null;
}
$response->setContent(Template::renderRaw('changelog.change', [
'change_info' => $changeInfo,
'change_user_info' => $userInfo,
'comments_user' => User::getCurrent(),
'comments_category' => self::getCommentsCategory($changeInfo->getCommentsCategoryName()),
]));
}
private function createFeed(string $feedMode): Feed {
$changes = ChangelogChange::all(new Pagination(10));
$changes = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
$feed = (new Feed)
->setTitle(Config::get('site.name', IConfig::T_STR, 'Misuzu') . ' » Changelog')
@ -94,7 +140,7 @@ class ChangelogHandler extends Handler {
$commentsUrl = url_prefix(false) . url('changelog-change-comments', ['change' => $change->getId()]);
$feedItem = (new FeedItem)
->setTitle($change->getActionString() . ': ' . $change->getHeader())
->setTitle($change->getActionText() . ': ' . $change->getSummary())
->setCreationDate($change->getCreatedTime())
->setUniqueId($changeUrl)
->setContentUrl($changeUrl)

View file

@ -1,8 +1,13 @@
<?php
namespace Misuzu\Http\Handlers;
use Misuzu\MisuzuContext;
abstract class Handler {
public function __construct() {
protected MisuzuContext $context;
public function __construct(MisuzuContext $context) {
\Misuzu\mszLockdown();
$this->context = $context;
}
}

View file

@ -6,7 +6,6 @@ use Misuzu\Config\IConfig;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\News\NewsPost;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
@ -115,7 +114,7 @@ final class HomeHandler extends Handler {
. ' (SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `post_deleted` IS NULL) AS `count_forum_posts`'
)->fetch();
$changelog = ChangelogChange::all(new Pagination(10));
$changelog = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
$birthdays = User::byBirthdate();
$latestUser = !empty($birthdays) ? null : User::byLatest();

View file

@ -2,6 +2,7 @@
namespace Misuzu;
use Misuzu\Template;
use Misuzu\Changelog\Changelog;
use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes;
use Misuzu\SharpChat\SharpChatRoutes;
@ -23,12 +24,14 @@ class MisuzuContext {
private Users $users;
private HttpFx $router;
private Emotes $emotes;
private Changelog $changelog;
public function __construct(IDbConnection $dbConn, IConfig $config) {
$this->dbConn = $dbConn;
$this->config = $config;
$this->users = new Users($this->dbConn);
$this->emotes = new Emotes($this->dbConn);
$this->changelog = new Changelog($this->dbConn);
}
public function getDbConn(): IDbConnection {
@ -64,6 +67,10 @@ class MisuzuContext {
return $this->emotes;
}
public function getChangelog(): Changelog {
return $this->changelog;
}
public function setUpHttp(bool $legacy = false): void {
$this->router = new HttpFx;
$this->router->use('/', function($response) {
@ -101,36 +108,31 @@ class MisuzuContext {
}
private function registerHttpRoutes(): void {
function msz_compat_handler(string $className, string $method) {
return function(...$args) use ($className, $method) {
$className = "\\Misuzu\\Http\\Handlers\\{$className}Handler";
return (new $className)->{$method}(...$args);
};
}
$mszCompatHandler = fn($className, $method) => fn(...$args) => (new ("\\Misuzu\\Http\\Handlers\\{$className}Handler")($this))->{$method}(...$args);
$this->router->get('/', msz_compat_handler('Home', 'index'));
$this->router->get('/', $mszCompatHandler('Home', 'index'));
$this->router->get('/assets/:filename', msz_compat_handler('Assets', 'serveComponent'));
$this->router->get('/assets/avatar/:filename', msz_compat_handler('Assets', 'serveAvatar'));
$this->router->get('/assets/profile-background/:filename', msz_compat_handler('Assets', 'serveProfileBackground'));
$this->router->get('/assets/:filename', $mszCompatHandler('Assets', 'serveComponent'));
$this->router->get('/assets/avatar/:filename', $mszCompatHandler('Assets', 'serveAvatar'));
$this->router->get('/assets/profile-background/:filename', $mszCompatHandler('Assets', 'serveProfileBackground'));
$this->router->get('/info', msz_compat_handler('Info', 'index'));
$this->router->get('/info/:name', msz_compat_handler('Info', 'page'));
$this->router->get('/info/:project/:name', msz_compat_handler('Info', 'page'));
$this->router->get('/info', $mszCompatHandler('Info', 'index'));
$this->router->get('/info/:name', $mszCompatHandler('Info', 'page'));
$this->router->get('/info/:project/:name', $mszCompatHandler('Info', 'page'));
$this->router->get('/changelog', msz_compat_handler('Changelog', 'index'));
$this->router->get('/changelog.rss', msz_compat_handler('Changelog', 'feedRss'));
$this->router->get('/changelog.atom', msz_compat_handler('Changelog', 'feedAtom'));
$this->router->get('/changelog/change/:id', msz_compat_handler('Changelog', 'change'));
$this->router->get('/changelog', $mszCompatHandler('Changelog', 'index'));
$this->router->get('/changelog.rss', $mszCompatHandler('Changelog', 'feedRss'));
$this->router->get('/changelog.atom', $mszCompatHandler('Changelog', 'feedAtom'));
$this->router->get('/changelog/change/:id', $mszCompatHandler('Changelog', 'change'));
$this->router->get('/news', msz_compat_handler('News', 'index'));
$this->router->get('/news.rss', msz_compat_handler('News', 'feedIndexRss'));
$this->router->get('/news.atom', msz_compat_handler('News', 'feedIndexAtom'));
$this->router->get('/news/:category', msz_compat_handler('News', 'viewCategory'));
$this->router->get('/news/post/:id', msz_compat_handler('News', 'viewPost'));
$this->router->get('/news', $mszCompatHandler('News', 'index'));
$this->router->get('/news.rss', $mszCompatHandler('News', 'feedIndexRss'));
$this->router->get('/news.atom', $mszCompatHandler('News', 'feedIndexAtom'));
$this->router->get('/news/:category', $mszCompatHandler('News', 'viewCategory'));
$this->router->get('/news/post/:id', $mszCompatHandler('News', 'viewPost'));
$this->router->get('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadGET'));
$this->router->post('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadPOST'));
$this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET'));
$this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST'));
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this->emotes);
}

View file

@ -110,8 +110,10 @@ define('MSZ_URLS', [
'manage-changelog-changes' => ['/manage/changelog'],
'manage-changelog-change' => ['/manage/changelog/change.php', ['c' => '<change>']],
'manage-changelog-change-delete' => ['/manage/changelog/change.php', ['c' => '<change>', 'delete' => '1', 'csrf' => '{token}']],
'manage-changelog-tags' => ['/manage/changelog/tags.php'],
'manage-changelog-tag' => ['/manage/changelog/tag.php', ['t' => '<tag>']],
'manage-changelog-tag-delete' => ['/manage/changelog/tag.php', ['t' => '<tag>', 'delete' => '1', 'csrf' => '{token}']],
'manage-news-categories' => ['/manage/news/categories.php'],
'manage-news-category' => ['/manage/news/category.php', ['c' => '<category>']],
@ -173,8 +175,12 @@ function url_redirect(string $name, array $variables = []): void {
}
function url_variable(string $value, array $variables): string {
if(str_starts_with($value, '<') && str_ends_with($value, '>'))
return $variables[trim($value, '<>')] ?? '';
if(str_starts_with($value, '<') && str_ends_with($value, '>')) {
$value = $variables[trim($value, '<>')] ?? '';
if(is_array($value))
$value = implode(',', $value);
return (string)$value;
}
if(str_starts_with($value, '[') && str_ends_with($value, ']'))
return '';

View file

@ -5,32 +5,32 @@
{% set title = 'Changelog » Change #' ~ change_info.id %}
{% set canonical_url = url('changelog-change', {'change': change_info.id}) %}
{% set manage_link = url('manage-changelog-change', {'change': change_info.id}) %}
{% set description = change_info.header %}
{% set description = change_info.summary %}
{% block content %}
<div class="container changelog__log changelog__action--{{ change_info.actionClass }}">
<div class="container changelog__log changelog__action--{{ change_info.action }}">
<div class="changelog__log__action">
{{ change_info.actionString }}
{{ change_info.actionText }}
</div>
<div class="changelog__log__text">
{{ change_info.header }}
{{ change_info.summary }}
</div>
</div>
<div class="container changelog__change"{% if change_info.user is not null %} style="--accent-colour: {{ change_info.user.colour }}"{% endif %}>
<div class="container changelog__change"{% if change_user_info is not null %} style="--accent-colour: {{ change_user_info.colour }}"{% endif %}>
<div class="changelog__change__info">
<div class="changelog__change__info__background"></div>
<div class="changelog__change__info__content">
{% if change_info.user.id|default(null) is not null %}
{% if change_user_info.id|default(null) is not null %}
<div class="changelog__change__user">
<a class="changelog__change__avatar" href="{{ url('user-profile', {'user': change_info.user.id}) }}">
{{ avatar(change_info.user.id, 60, change_info.user.username) }}
<a class="changelog__change__avatar" href="{{ url('user-profile', {'user': change_user_info.id}) }}">
{{ avatar(change_user_info.id, 60, change_user_info.username) }}
</a>
<div class="changelog__change__user__details">
<a class="changelog__change__username" href="{{ url('user-profile', {'user': change_info.user.id}) }}">{{ change_info.user.username }}</a>
<a class="changelog__change__userrole" href="{{ url('user-list', {'role': change_info.user.displayRoleId}) }}">{{ change_info.user.title }}</a>
<a class="changelog__change__username" href="{{ url('user-profile', {'user': change_user_info.id}) }}">{{ change_user_info.username }}</a>
<a class="changelog__change__userrole" href="{{ url('user-list', {'role': change_user_info.displayRoleId}) }}">{{ change_user_info.title }}</a>
</div>
</div>
{% endif %}
@ -67,10 +67,8 @@
</div>
</div>
{% if change_info.hasCommentsCategory %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change_info.date) }}
{{ comments_section(change_info.commentsCategory, comments_user) }}
{{ comments_section(comments_category, comments_user) }}
</div>
{% endif %}
{% endblock %}

View file

@ -11,11 +11,22 @@
{% set canonical_url = url('changelog-index', {
'date': changelog_date_fmt,
'user': changelog_user.id|default(0),
'tags': changelog_tags,
'page': changelog_pagination.page < 2 ? 0 : changelog_pagination.page,
}) %}
{% if is_date or is_user %}
{% set title = title ~ ' »' ~ (is_date ? ' ' ~ changelog_infos[0].date : '') ~ (is_user ? ' by ' ~ changelog_infos[0].user.username : '') %}
{% set title = title ~ ' »' %}
{% set first_change_info = changelog_infos[0] %}
{% if is_date %}
{% set first_change_date = first_change_info.date is defined ? first_change_info.date : first_change_info.change.date %}
{% set title = title ~ ' ' ~ first_change_date %}
{% endif %}
{% if is_user %}
{% set title = title ~ ' by ' ~ first_change_info.user.username %}
{% endif %}
{% else %}
{% set feeds = [
{
@ -47,7 +58,7 @@
{% if is_date %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
{{ comments_section(changelog_infos[0].commentsCategory, comments_user) }}
{{ comments_section(comments_category, comments_user) }}
</div>
{% endif %}
{% endblock %}

View file

@ -4,8 +4,10 @@
<div class="changelog__listing">
{% if changes|length > 0 %}
{% for change in changes %}
{% if not hide_dates and (last_date is not defined or last_date != change.date) %}
{% set last_date = change.date %}
{% set change_date = change.change is defined ? change.change.date : change.date %}
{% if not hide_dates and (last_date is not defined or last_date != change_date) %}
{% set last_date = change_date %}
<a href="{{ is_manage ? '#cd' ~ last_date : url('changelog-index', {'date': last_date}) }}" class="changelog__listing__date" id="cd{{ last_date }}">
{{ last_date }}
@ -23,6 +25,11 @@
{% endmacro %}
{% macro changelog_entry(change, is_small, is_manage) %}
{% set user = change.user %}
{% if change.change is defined %}
{% set change = change.change %}
{% endif %}
{% set change_url = url(is_manage ? 'manage-changelog-change' : 'changelog-change', {'change': change.id}) %}
<div class="changelog__entry" id="cl{{ change.id }}">
@ -37,22 +44,22 @@
</a>
{% endif %}
<a class="changelog__entry__action changelog__action--{{ change.actionClass }}"
<a class="changelog__entry__action changelog__action--{{ change.action }}"
href="{{ change_url }}"
{% if is_small %}title="{{ change.actionString }}"{% endif %}>
{% if is_small %}title="{{ change.actionText }}"{% endif %}>
{% if not is_small %}
<div class="changelog__entry__action__text">
{{ change.actionString }}
{{ change.actionText }}
</div>
{% endif %}
</a>
{% if not is_small %}
<a class="changelog__entry__user"
href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': change.user.id|default(0)}) }}"
style="--user-colour: {{ change.user.colour|default('inherit') }}">
href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': user.id|default(0)}) }}"
style="--user-colour: {{ user.colour|default('inherit') }}">
<div class="changelog__entry__user__text">
{{ change.user.username|default('Anonymous') }}
{{ user.username|default('Anonymous') }}
</div>
</a>
{% endif %}
@ -61,13 +68,13 @@
<div class="changelog__entry__text">
<a class="changelog__entry__log{% if change.hasBody %} changelog__entry__log--link{% endif %}"
{% if change.hasBody %}href="{{ change_url }}"{% endif %}>
{{ change.header }}
{{ change.summary }}
</a>
{% if is_manage %}
<div class="changelog__entry__tags">
{% for tag in change.tags %}
<a href="{{ url(is_manage ? 'manage-changelog-tag' : 'changelog-tag', {'tag': tag.id}) }}" class="changelog__entry__tag">
<a href="{{ is_manage ? url('manage-changelog-tag', {'tag': tag.id}) : url('changelog-index', {'tags': tag.id}) }}" class="changelog__entry__tag">
{{ tag.name }}
</a>
{% endfor %}

View file

@ -2,40 +2,40 @@
{% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_csrf, input_text, input_select, input_checkbox %}
{% if change is not null %}
{% set site_link = url('changelog-change', {'change': change.id}) %}
{% if not change_new %}
{% set site_link = url('changelog-change', {'change': change_info.id}) %}
{% endif %}
{% block manage_content %}
<div class="container">
<form action="{{ url('manage-changelog-change', {'change': change.id|default(0)}) }}" method="post">
<form action="{{ url('manage-changelog-change', {'change': change_info.id|default(0)}) }}" method="post">
{{ input_csrf() }}
{{ container_title(change is not null ? 'Editing #' ~ change.id : 'Adding a new change') }}
{{ container_title(change_new ? 'Adding a new change' : 'Editing #' ~ change_info.id) }}
<div style="display: flex; margin: 2px 5px;">
{{ input_select('change[action]', change_actions, change.action|default(0), 'action_name', 'action_id') }}
{{ input_text('change[log]', '', change.header|default(''), 'text', '', true, {'maxlength':255,'style':'flex-grow:1'}) }}
{{ input_select('cl_action', change_actions, change_info.action|default('add')) }}
{{ input_text('cl_summary', '', change_info.summary|default(''), 'text', '', true, {'maxlength': 255, 'style': 'flex-grow: 1'}) }}
</div>
<label class="form__label">
<div class="form__label__text">Text</div>
<div class="form__label__input">
<textarea class="input__textarea" name="change[text]" maxlength="65535">{{ change.body|default('') }}</textarea>
<textarea class="input__textarea" name="cl_body" maxlength="65535">{{ change_info.body|default('') }}</textarea>
</div>
</label>
<label class="form__label">
<div class="form__label__text">Contributor Id</div>
<div class="form__label__input">
{{ input_text('change[user]', '', change.userId|default(current_user.id), 'number', '', false, {'min':1}) }}
{{ input_text('cl_user', '', change_info.userId|default(current_user.id), 'number', '', false, {'min':1}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Created</div>
<div class="form__label__input">
{{ input_text('change[created]', '', change.createdTime|default(null)|date('Y-m-d H:i:s'), 'text', '', true) }}
{{ input_text('cl_created', '', change_info.createdTime|default(-1)|date('Y-m-d\\TH:i'), 'datetime-local', '', true) }}
</div>
</label>
@ -44,7 +44,7 @@
<label class="manage__tag">
<div class="manage__tag__background"></div>
<div class="manage__tag__content">
{{ input_checkbox('tags[]', '', change.hasTag(tag)|default(false), 'manage__tag__checkbox', tag.id) }}
{{ input_checkbox('cl_tags[]', '', change_info.hasTag(tag)|default(0), 'manage__tag__checkbox', tag.id) }}
<div class="manage__tag__title">
{{ tag.name }}
</div>
@ -55,6 +55,9 @@
<div>
<button class="input__button">Save</button>
{% if not change_new %}
<a href="{{ url('manage-changelog-change-delete', {'change': change_info.id}) }}" class="input__button input__button--destroy" onclick="return confirm('Are you sure?');">Delete</a>
{% endif %}
</div>
</form>
</div>

View file

@ -4,43 +4,46 @@
{% block manage_content %}
<div class="container">
<form action="{{ url('manage-changelog-tag', {'tag': edit_tag.id|default(0)}) }}" method="post">
<form action="{{ url('manage-changelog-tag', {'tag': tag_info.id|default(0)}) }}" method="post">
{{ input_csrf() }}
{{ container_title(edit_tag.id is defined ? 'Editing ' ~ edit_tag.name ~ ' (' ~ edit_tag.id ~ ')' : 'Adding a new tag') }}
{{ container_title(tag_new ? 'Adding a new tag' : 'Editing ' ~ tag_info.name ~ ' (' ~ tag_info.id ~ ')') }}
<label class="form__label" style="width:100%">
<div class="form__label__text">Name</div>
<div class="form__label__input">
{{ input_text('tag[name]', '', edit_tag.id is defined ? edit_tag.name : '', 'text', '', true, {'maxlength':255}) }}
{{ input_text('ct_name', '', tag_info.id is defined ? tag_info.name : '', 'text', '', true, {'maxlength': 255}) }}
</div>
</label>
<label class="form__label" style="width:100%">
<div class="form__label__text">Description</div>
<div class="form__label__input">
<textarea class="input__textarea" name="tag[description]" maxlength="65535">{{ edit_tag.description|default('') }}</textarea>
<textarea class="input__textarea" name="ct_desc" maxlength="65535">{{ tag_info.description|default('') }}</textarea>
</div>
</label>
<label class="form__label">
<div class="form__label__text">Archived</div>
<div class="form__label__input">
{{ input_checkbox('tag[archived]', '', edit_tag.archived|default(false)) }}
{{ input_checkbox('ct_archive', '', tag_info.archived|default(false)) }}
</div>
</label>
{% if edit_tag.id is defined %}
{% if not tag_new %}
<label class="form__label">
<div class="form__label__text">Created</div>
<div class="form__label__input">
{{ input_text('', '', edit_tag.createdTime|date('r')) }}
{{ input_text('', '', tag_info.createdTime|date('r')) }}
</div>
</label>
{% endif %}
<div>
<button class="input__button">Save</button>
{% if not tag_new %}
<a href="{{ url('manage-changelog-tag-delete', {'tag': tag_info.id}) }}" class="input__button input__button--destroy" onclick="return confirm('Are you sure?');">Delete</a>
{% endif %}
</div>
</form>
</div>

View file

@ -13,7 +13,7 @@
<a href="{{ url('manage-changelog-tag', {'tag': tag.id}) }}" class="changelog-actions-tags__entry">
<div class="listing__entry__content changelog-tags__content">
<div class="changelog-tags__text">
{{ tag.name }} ({{ tag.changeCount }})
{{ tag.name }} ({{ tag.changesCount }}){% if tag.archived %} <strong>[ARCHIVED]</strong>{% endif %}
</div>
<div class="changelog-tags__description">