From aaa91a9136e5ce7ecd1ec01d395373c44178549b Mon Sep 17 00:00:00 2001 From: Jim Mason Date: Tue, 7 Jan 2025 12:04:51 +0000 Subject: [PATCH] added the review shelf --- css/zoostyle.css | 15 ++++ engine/IReview.php | 14 ++- engine/impl/ReviewImpl.php | 62 +++++++++++-- ui/Reviews.php | 119 ++++++++++++++++++++++++- ui/templates/default/review.shelf.html | 99 ++++++++++++++++++++ 5 files changed, 302 insertions(+), 7 deletions(-) create mode 100644 ui/templates/default/review.shelf.html diff --git a/css/zoostyle.css b/css/zoostyle.css index 3ac6db17..08a9bce3 100644 --- a/css/zoostyle.css +++ b/css/zoostyle.css @@ -282,6 +282,15 @@ table.recent-reviews th { text-align: left; } +/* review shelf */ +table.review-shelf tbody td { + border-color: rgba(0,0,0,0.08); +} + +table.review-shelf tbody tr { + line-height: 30px; +} + /* home page */ .top-albums { margin-top: 4px; @@ -1001,11 +1010,17 @@ li.ui-menu-item .ui-state-active, .form-entry button.edit-mode.default:active:not(:disabled) { background-color: #5897ee; } +.form-entry button.edit-delete, .form-entry button#edit-delete { color: #e33437; border-color: #e33437; } +.form-entry button.edit-alert { + color: #8a2be2; + border-color: #8a2be2; +} + .form-entry label { display:inline-block; font-weight: bold; diff --git a/engine/IReview.php b/engine/IReview.php index 5ba86c0c..8a6dfd9b 100644 --- a/engine/IReview.php +++ b/engine/IReview.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2024 Jim Mason + * @copyright Copyright (C) 1997-2025 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -52,4 +52,16 @@ function insertReview($tag, $private, $airname, $review, $user); function updateReview($tag, $private, $airname, $review, $user); function deleteReview($tag, $user); function setExportId($tag, $user, $exportId); + function getReviewShelf(); + /** + * update review shelf status + * + * @param int $tag target album + * @param ?string $user check album out to user or null (see below) + * @return ?array old status or null if none + * + * If $user is null and album is pending approval, it is moved + * to the library; otherwise, it is returned to the review shelf. + */ + function updateReviewShelf(int $tag, ?string $user = null): ?array; } diff --git a/engine/impl/ReviewImpl.php b/engine/impl/ReviewImpl.php index 7bad1682..a86aaebe 100644 --- a/engine/impl/ReviewImpl.php +++ b/engine/impl/ReviewImpl.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2024 Jim Mason + * @copyright Copyright (C) 1997-2025 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -29,6 +29,18 @@ * Music review operations */ class ReviewImpl extends DBO implements IReview { + // these codes are defined in ILibrary::LOCATIONS + private const ALBUM_AWAITING_REVIEW = 'E'; // Review Shelf + private const ALBUM_IN_REVIEW = 'F'; // Out for Review + private const ALBUM_REVIEWED = 'H'; // Pending Approval + private const ALBUM_LIBRARY = 'L'; // Library + + private const REVIEW_SHELF = [ + self::ALBUM_AWAITING_REVIEW, + self::ALBUM_IN_REVIEW, + self::ALBUM_REVIEWED + ]; + private function getRecentSubquery($user = "", $weeks = 0, $loggedIn = 0) { // IMPORTANT: If columns change, revisit getRecentReviews below $query = "SELECT a.airname, r.user, DATE_FORMAT(r.created, GET_FORMAT(DATE, 'ISO')) reviewed, r.id as rid, u.realname, r.tag, v.category, v.album, v.artist, v.iscoll FROM reviews r "; @@ -191,8 +203,9 @@ protected function syncHashtags(int $tag, string $user, ?string $review = null) } public function insertReview($tag, $private, $airname, $review, $user) { - // we must do this first, as caller depends on lastInsertId from INSERT + // we must do these first as caller depends on lastInsertId from INSERT $this->syncHashtags($tag, $user, $private ? null : $review); + $prev = $this->updateReviewShelf($tag, null, self::ALBUM_REVIEWED); $query = "INSERT INTO reviews " . "(tag, user, created, private, review, airname) VALUES (" . @@ -209,10 +222,14 @@ public function insertReview($tag, $private, $airname, $review, $user) { $stmt->bindValue(5, $airname); $count = $stmt->execute() ? $stmt->rowCount() : 0; - // back out hashtags on failure - if(!$count) + // back out hashtags and review shelf on failure + if(!$count) { $this->syncHashtags($tag, $user); + if($prev) + $this->updateReviewShelf($tag, $prev['user'], $prev['status']); + } + return $count; } @@ -247,8 +264,10 @@ public function deleteReview($tag, $user) { $count = $stmt->execute() ? $stmt->rowCount() : 0; // delete any associated hashtags - if($count) + if($count) { $this->syncHashtags($tag, $user); + $this->updateReviewShelf($tag, null, self::ALBUM_AWAITING_REVIEW); + } return $count; } @@ -262,4 +281,37 @@ public function setExportId($tag, $user, $exportId) { $stmt->bindValue(3, $user); return $stmt->execute()?$stmt->rowCount():0; } + + public function getReviewShelf() { + $n = count(self::REVIEW_SHELF); + $query = "SELECT a.*, u.realname FROM albumvol a LEFT JOIN users u ON a.bin = u.name WHERE location IN ( ?" . str_repeat(', ?', $n - 1) . " ) ORDER BY artist, album"; + $stmt = $this->prepare($query); + foreach(self::REVIEW_SHELF as $status) + $stmt->bindValue($n--, $status); + + return $stmt->executeAndFetchAll(); + } + + function updateReviewShelf(int $tag, ?string $user = null, ?string $status = null): ?array { + $query = "SELECT location status, bin user FROM albumvol WHERE tag = ?"; + $stmt = $this->prepare($query); + $stmt->bindValue(1, $tag); + $result = $stmt->executeAndFetch() ?: null; + + // if album is not in a review status, nothing to do + $ostatus = $result['status'] ?? ''; + if(!in_array($ostatus, self::REVIEW_SHELF)) + return null; + + $location = $ostatus == self::ALBUM_REVIEWED ? + self::ALBUM_LIBRARY : self::ALBUM_AWAITING_REVIEW; + + $query = "UPDATE albumvol SET location = ?, bin = ?, updated = NOW() WHERE tag = ?"; + $stmt = $this->prepare($query); + $stmt->bindValue(1, $status ?? + ($user ? self::ALBUM_IN_REVIEW : $location)); + $stmt->bindValue(2, $user); + $stmt->bindValue(3, $tag); + return $stmt->execute() ? $result : null; + } } diff --git a/ui/Reviews.php b/ui/Reviews.php index 9ba0b6ad..c6c8bec5 100644 --- a/ui/Reviews.php +++ b/ui/Reviews.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2024 Jim Mason + * @copyright Copyright (C) 1997-2025 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -55,6 +55,8 @@ class Reviews extends MenuItem { [ "a", "viewDJ", "By DJ", "reviewsByDJ" ], [ "a", "viewHashtag", "Trending", "viewTrending" ], [ "a", "trendingData", 0, "getTrendingData" ], + [ "u", "viewReviewShelf", "Review Shelf", "viewReviewShelf" ], + [ "u", "updateReviewShelf", 0, "updateReviewShelf" ], ]; private $subaction; @@ -148,6 +150,49 @@ public function getTrendingData() { echo json_encode($data); } + public function viewReviewShelf() { + $albums = Engine::api(IReview::class)->getReviewShelf(); + $this->addVar('GENRES', ILibrary::GENRES); + $this->addVar('albums', $albums); + $this->setTemplate("review.shelf.html"); + } + + public function updateReviewShelf() { + $op = $_REQUEST['op'] ?? false; + $tag = $_REQUEST['tag'] ?? false; + if(!$op || !$tag) { + http_response_code(400); // bad request + return; + } + + switch($op) { + case 'claim': + Engine::api(IReview::class)->updateReviewShelf($tag, $this->session->getUser()); + break; + case 'release': + // fall through... + case 'xdtm': + Engine::api(IReview::class)->updateReviewShelf($tag, null); + break; + } + + $album = Engine::api(ILibrary::class)->search(ILibrary::ALBUM_KEY, 0, 1, $tag)[0]; + if($album['bin']) { + $user = Engine::api(ILibrary::class)->search(ILibrary::PASSWD_NAME, 0, 1, $album['bin']); + if(count($user)) + $album['realname'] = $user[0]['realname']; + } + + $this->claimReview($tag, $op); + + $this->addVar('GENRES', ILibrary::GENRES); + $this->addVar('album', $album); + $this->setTemplate("review.shelf.html"); + $html = $this->render('album'); + + echo json_encode(['html' => $html]); + } + public function viewRecentReviews() { $isAuthorized = $this->session->isAuth('u'); $author = $isAuthorized && ($_GET['dj'] ?? '') == 'Me' ? $this->session->getUser() : ''; @@ -164,6 +209,78 @@ public function viewReview() { $this->newEntity(Search::class)->searchByAlbumKey($_REQUEST["tag"]); } + private function claimReview($tag, $op) { + // nothing to do if Slack is not configured + $config = Engine::param('slack'); + if($op == 'dtm' || + !$config || !($token = $config['token']) || + !($channel = $config['review_channel'])) { + return; + } + + // find the album + $libAPI = Engine::api(ILibrary::class); + $albums = $libAPI->search(ILibrary::ALBUM_KEY, 0, 1, $tag); + if(!count($albums)) + return; + + $artist = $albums[0]["iscoll"] ? "Various Artists" : $albums[0]["artist"]; + $album = $albums[0]["album"]; + + $user = $libAPI->search(ILibrary::PASSWD_NAME, 0, 1, $this->session->getUser()); + if(!count($user)) + return; + + $reviewer = $user[0]["realname"]; + $unclaim = $op == 'release'; + $action = $unclaim ? "has returned" : "is reviewing"; + $verb = $unclaim ? "returned" : "claimed"; + + $base = Engine::getBaseUrl(); + $title = Engine::param('station_title'); + + // compose the message + $body = [ + 'type' => 'section', + 'text' => [ + 'type' => 'mrkdwn', + 'text' => ":lower_left_fountain_pen: $reviewer $action <$base?s=byAlbumKey&n=$tag&action=search|$artist / $album>" + ], + ]; + + $client = new Client([ + 'base_uri' => self::SLACK_BASE, + RequestOptions::HEADERS => [ + 'User-Agent' => Engine::UA, + 'Authorization' => 'Bearer ' . $token + ] + ]); + + try { + $options = [ + 'channel' => $channel, + 'text' => "$reviewer $verb $artist / $album", + 'blocks' => [ + $body, + ], + ]; + + $method = 'chat.postMessage'; + + $response = $client->post($method, [ + RequestOptions::JSON => $options + ]); + + // Slack returns success/failure in 'ok' property + $body = $response->getBody()->getContents(); + $json = json_decode($body); + if(!$json->ok) + error_log("postReview: $body"); + } catch(\Exception $e) { + error_log("postReview: " . $e->getMessage()); + } + } + private function postReview($tag) { // nothing to do if Slack is not configured $config = Engine::param('slack'); diff --git a/ui/templates/default/review.shelf.html b/ui/templates/default/review.shelf.html new file mode 100644 index 00000000..f73e57ac --- /dev/null +++ b/ui/templates/default/review.shelf.html @@ -0,0 +1,99 @@ +
+ + + + + + + + + + + + + + + + + + + + + +{% for album in albums %} +{% block album %} + {%- set genre = GENRES[album.category] %} + +{% if album.location == 'E' %} + {#~ review shelf #} + +{% elseif album.location == 'F' and app.session.getUser() == album.bin %} + {#~ out for review by this user #} + +{% elseif album.location == 'H' and app.session.isAuth('n') %} + {#~ pending approval for add manager #} + +{% else %} + +{% endif %} + + + +{% if album.location == 'F' %} {# out for review #} + + + +{% else %} + {%~ if album.location == 'E' %} + + {%~ else %} + + {%~ endif %} + + +{% endif %} + +{% endblock %} +{% endfor %} + + +
+