-
+
diff --git a/RELEASE.md b/RELEASE.md index f1c186f..c60d7c9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,9 +12,19 @@ If the answer to any of the above is yes, then the release MUST be a major or mi - [ ] Add a commit which adds a database upgrade script at `db/upgrade/[oldversion]-[newversion].sql` - ALWAYS use a transaction for the entirety of this file - At minimum, DB version must be bumped -- [ ] Add a commit which bumps the version in `app/version.ts` and `db/00-version.sql` + - Test the upgrade script as follows: + ``` + git checkout [previous release tag] + docker-compose down + docker-compose up + psql -h 0 -U postgres < db/upgrade/v[previous]-v[current].sql + ``` + Update the version in `src/version.ts` and verify the app works as expected. +- [ ] Add a commit which bumps the version in `src/version.ts` and `db/00-version.sql`, entitled `Bump version to vMAJOR.MINOR` - [ ] Tag the latest commit with `vMAJOR.MINOR` +- [ ] Write release notes ## Patch Releases -- [ ] Add a commit which bumps the patch level in `app/version.ts` +- [ ] Add a commit which bumps the patch level in `src/version.ts`, entitled `Bump version to vMAJOR.MINOR.PATCH` - [ ] Tag the latest commit with `vMAJOR.MINOR.PATCH` +- [ ] Write release notes \ No newline at end of file diff --git a/db/00-version.sql b/db/00-version.sql index 02983fb..6686897 100644 --- a/db/00-version.sql +++ b/db/00-version.sql @@ -8,4 +8,4 @@ CREATE OR REPLACE FUNCTION get_version() RETURNS VARCHAR AS $$SELECT major || '.' || minor FROM version $$ LANGUAGE SQL; -INSERT INTO version (major,minor) VALUES (0,6); +INSERT INTO version (major,minor) VALUES (0,8); diff --git a/db/05-config.sql b/db/05-config.sql index 1c4b65a..2e830ab 100644 --- a/db/05-config.sql +++ b/db/05-config.sql @@ -1,12 +1,14 @@ CREATE TABLE config ( rowlock bool DEFAULT TRUE UNIQUE NOT NULL CHECK (rowlock = TRUE), - normaluservotepoints int NOT NULL, - followervotepoints int NOT NULL, - subscribervotepoints int NOT NULL, - title varchar NOT NULL, - colors jsonb NOT NULL, + normaluservotepoints int NOT NULL DEFAULT 10, + followervotepoints int NOT NULL DEFAULT 50, + subscribervotepoints int NOT NULL DEFAULT 100, + normaluserratelimit int NOT NULL DEFAULT 1, + followerratelimit int NOT NULL DEFAULT 2, + subscriberratelimit int NOT NULL DEFAULT 3, + title varchar NOT NULL DEFAULT '{username}''s Learn Request Queue', + colors jsonb NOT NULL DEFAULT '{"bg": {"primary": "#444444","table": "#282828","navbar": "#666666","error": "#ff0000"},"fg": {"primary": "#dddddd","ahover": "#ffffff","title": "#eeeeee"}}', PRIMARY KEY (rowLock) ); -INSERT INTO config (normalUserVotePoints,followerVotePoints,subscriberVotePoints,title,colors) - VALUES (10,50,100,'{username}''s Learn Request Queue','{"bg": {"primary": "#444444","table": "#282828","navbar": "#666666","error": "#ff0000"},"fg": {"primary": "#dddddd","ahover": "#ffffff","title": "#eeeeee"}}'); +INSERT INTO config (rowlock) VALUES (true); diff --git a/db/15-streamer.sql b/db/15-streamer.sql index 53068c8..b39604c 100644 --- a/db/15-streamer.sql +++ b/db/15-streamer.sql @@ -3,4 +3,4 @@ CREATE TABLE streamer ( tokenPair json, PRIMARY KEY (userid), FOREIGN KEY (userid) REFERENCES users(userid) -) +); diff --git a/db/20-requests.sql b/db/20-requests.sql index c3f3cf5..b63c359 100644 --- a/db/20-requests.sql +++ b/db/20-requests.sql @@ -1,5 +1,5 @@ CREATE TABLE requests ( - url varchar NOT NULL UNIQUE, + url varchar NOT NULL, requester int NOT NULL, state varchar NOT NULL DEFAULT 'Requested', reqTimestamp timestamptz NOT NULL DEFAULT NOW(), diff --git a/db/30-votes.sql b/db/30-votes.sql index 0b878ba..8da76f0 100644 --- a/db/30-votes.sql +++ b/db/30-votes.sql @@ -1,6 +1,6 @@ CREATE TABLE votes ( - requestUrl varchar, - userId int, + requestUrl varchar NOT NULL, + userId int NOT NULL, PRIMARY KEY (requestUrl,userId), FOREIGN KEY (requestUrl) REFERENCES requests(url) ON DELETE CASCADE, FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE diff --git a/db/50-bans.sql b/db/50-bans.sql index 979ae21..20ecfa1 100644 --- a/db/50-bans.sql +++ b/db/50-bans.sql @@ -1,4 +1,4 @@ CREATE TABLE bans ( userid integer, PRIMARY KEY (userid) -) +); diff --git a/db/50-follows.sql b/db/50-follows.sql index c511c43..63e1200 100644 --- a/db/50-follows.sql +++ b/db/50-follows.sql @@ -1,4 +1,4 @@ CREATE TABLE follows ( userid integer, PRIMARY KEY (userid) -) +); diff --git a/db/50-subscriptions.sql b/db/50-subscriptions.sql index 4271623..7e23cf1 100644 --- a/db/50-subscriptions.sql +++ b/db/50-subscriptions.sql @@ -1,4 +1,4 @@ CREATE TABLE subscriptions ( userid integer, PRIMARY KEY (userid) -) +); diff --git a/db/90-views.sql b/db/90-views.sql index 14896b0..a31ec16 100644 --- a/db/90-views.sql +++ b/db/90-views.sql @@ -60,3 +60,22 @@ CREATE OR REPLACE VIEW streamer_user_vw AS SELECT users.userid as userid, users.displayname as displayname, users.imageurl as imageurl FROM streamer LEFT JOIN users ON streamer.userid = users.userid; + +CREATE OR REPLACE VIEW ratelimit_vw AS + SELECT users.userid,COALESCE(count,0),ratelimit.reqcount AS max,COALESCE(count,0) >= ratelimit.reqcount AS status + FROM users + LEFT JOIN (SELECT requester,COUNT(url) + FROM requests + WHERE reqtimestamp > (now() - '24 hours'::interval) + GROUP BY requests.requester + ) AS requests ON users.userid = requests.requester + LEFT JOIN follows ON requests.requester = follows.userid + LEFT JOIN subscriptions ON requests.requester = subscriptions.userid + CROSS JOIN config + CROSS JOIN LATERAL (VALUES ( + CASE + WHEN follows.userid IS NULL AND subscriptions.userid IS NULL THEN config.normaluserratelimit + WHEN follows.userid IS NOT NULL AND subscriptions.userid IS NULL THEN config.followerratelimit + WHEN subscriptions.userid IS NOT NULL THEN config.subscriberratelimit + END + )) AS ratelimit(reqcount); diff --git a/db/95-procedures.sql b/db/95-procedures.sql index 28ec802..650f119 100644 --- a/db/95-procedures.sql +++ b/db/95-procedures.sql @@ -16,6 +16,13 @@ CREATE OR REPLACE PROCEDURE add_request(url varchar,requester int) CALL update_scores(); $$; +CREATE OR REPLACE PROCEDURE clear_zero_votes() + LANGUAGE SQL + AS $$ + DELETE FROM requests WHERE NOT EXISTS + (SELECT FROM votes WHERE requests.url = votes.requesturl); + $$; + CREATE OR REPLACE PROCEDURE add_vote(url varchar,voteuser int) LANGUAGE SQL AS $$ @@ -28,6 +35,7 @@ CREATE OR REPLACE PROCEDURE delete_vote(url varchar,voteuser int) AS $$ DELETE FROM votes WHERE requesturl = url AND userid = voteuser; CALL update_scores(); + CALL clear_zero_votes(); $$; CREATE OR REPLACE PROCEDURE update_request_score_modifier(updateurl varchar, scoreDiff int) diff --git a/db/upgrade/v0.6-v0.7.sql b/db/upgrade/v0.6-v0.7.sql new file mode 100644 index 0000000..0939227 --- /dev/null +++ b/db/upgrade/v0.6-v0.7.sql @@ -0,0 +1,24 @@ +BEGIN; + +UPDATE version SET minor = 7; + +ALTER TABLE votes + ALTER COLUMN requestUrl SET NOT NULL, + ALTER COLUMN userId SET NOT NULL; + +CREATE OR REPLACE PROCEDURE clear_zero_votes() + LANGUAGE SQL + AS $$ + DELETE FROM requests WHERE NOT EXISTS + (SELECT FROM votes WHERE requests.url = votes.requesturl); + $$; + +CREATE OR REPLACE PROCEDURE delete_vote(url varchar,voteuser int) + LANGUAGE SQL + AS $$ + DELETE FROM votes WHERE requesturl = url AND userid = voteuser; + CALL update_scores(); + CALL clear_zero_votes(); + $$; + +COMMIT; diff --git a/db/upgrade/v0.7-v0.8.sql b/db/upgrade/v0.7-v0.8.sql new file mode 100644 index 0000000..af83dae --- /dev/null +++ b/db/upgrade/v0.7-v0.8.sql @@ -0,0 +1,35 @@ +BEGIN; + +UPDATE version SET minor = 8; + +ALTER TABLE config + ALTER COLUMN normaluservotepoints SET DEFAULT 10, + ALTER COLUMN followervotepoints SET DEFAULT 50, + ALTER COLUMN subscribervotepoints SET DEFAULT 100, + ALTER COLUMN title SET DEFAULT '{username}''s Learn Request Queue', + ALTER COLUMN colors SET DEFAULT '{"bg": {"primary": "#444444","table": "#282828","navbar": "#666666","error": "#ff0000"},"fg": {"primary": "#dddddd","ahover": "#ffffff","title": "#eeeeee"}}', + ADD COLUMN normaluserratelimit int NOT NULL DEFAULT 1, + ADD COLUMN followerratelimit int NOT NULL DEFAULT 2, + ADD COLUMN subscriberratelimit int NOT NULL DEFAULT 3; + +CREATE OR REPLACE VIEW ratelimit_vw AS + SELECT users.userid,COALESCE(count,0),ratelimit.reqcount AS max,COALESCE(count,0) >= ratelimit.reqcount AS status + FROM users + LEFT JOIN (SELECT requester,COUNT(url) + FROM requests + WHERE reqtimestamp > (now() - '24 hours'::interval) + GROUP BY requests.requester + ) AS requests ON users.userid = requests.requester + LEFT JOIN follows ON requests.requester = follows.userid + LEFT JOIN subscriptions ON requests.requester = subscriptions.userid + CROSS JOIN config + CROSS JOIN LATERAL (VALUES ( + CASE + WHEN follows.userid IS NULL AND subscriptions.userid IS NULL THEN config.normaluserratelimit + WHEN follows.userid IS NOT NULL AND subscriptions.userid IS NULL THEN config.followerratelimit + WHEN subscriptions.userid IS NOT NULL THEN config.subscriberratelimit + END + )) AS ratelimit(reqcount); + + +COMMIT; diff --git a/public/main.js b/public/main.js index 3b2da71..e9fee5d 100644 --- a/public/main.js +++ b/public/main.js @@ -2,11 +2,14 @@ var requestsDiv = document.getElementById("requests"); var cronJobs = ['processBans']; var currentPage = 1; var totalPages = 1; +var count = document.getElementById("count").value; +var sortBy = document.getElementById("sortBy").value; +var sortDir = "desc"; -function getRequests(count,offset,allRequests) { +function getRequests(offset,allRequests) { if (allRequests) var reqUrl = "/api/getAllRequests"; else var reqUrl = "/api/getRequests"; - reqUrl += `?count=${count}&offset=${offset}`; + reqUrl += `?count=${count}&offset=${offset}&sort=${sortBy}&sortDirection=${sortDir}`; fetch(reqUrl) .then(response => response.json()) .then(requests => { @@ -38,7 +41,7 @@ function buildTable(requests) { document.getElementById("page").innerHTML += ``; } document.getElementById("page").value = currentPage; - var requestsDivHTML = '
Song | Requester | Score | '; + var requestsDivHTML = '
---|
Song Title | Requester | Score | '; requestsDivHTML += 'State'; if (window.loggedIn) requestsDivHTML += ' | Vote'; if (window.isStreamer) requestsDivHTML += ' | Update | ' @@ -65,14 +68,14 @@ function buildTable(requests) { } function updateTable() { - var count = document.getElementById("count").value; var offset = (currentPage - 1) * count; var allRequests = document.getElementById("allRequests").checked; - getRequests(count,offset,allRequests); + getRequests(offset,allRequests); } function applyUrlTransforms(url) { console.log("Begin applyUrlTransforms:" + url); + url = url.trim(); if (url.match(/^https?:\/\/(www\.)?youtu(\.be|be\.com)\//)) { // Youtube console.log("Youtube"); var videoid = ""; @@ -233,7 +236,7 @@ function validateAndSubmitRequest() { updateTable(); document.getElementById("addRequestUrl").value = ""; response.text().then((message) => { - closeAddRequestModal(); + closeAllModals(); showMessage(message); }); }); @@ -375,5 +378,16 @@ for(state of validStates) { var opt = document.createElement("option"); opt.text = state; opt.value = state; - updateRequestStateSelect.add(opt) + updateRequestStateSelect.add(opt); +} + +function toggleSortDir() { + if (window.sortDir == "desc") { + document.getElementById("sortDir").innerText = "↑"; + window.sortDir = "asc"; + } else { + document.getElementById("sortDir").innerText = "↓"; + window.sortDir = "desc"; + } + updateTable(); } diff --git a/public/style.css b/public/style.css index 6be863d..ccea461 100644 --- a/public/style.css +++ b/public/style.css @@ -111,11 +111,20 @@ div#nav-userpic { text-align: right; } -#tableSettings { +.tableSettings { display: flex; + align-items: center; justify-content: space-between; } +.tableSettings > span { + margin: auto; +} + +#tableSettingsTop { + margin-bottom: 10px; +} + #modalBackground { display: none; position: fixed; @@ -128,6 +137,7 @@ div#nav-userpic { height: 100%; background-color: #444; background-color: #444a; + margin: 5px; } .modal { @@ -201,3 +211,7 @@ div#nav-userpic { display: flex; justify-content: space-between; } + +#sortDir { + padding: 0; +} diff --git a/src/app.ts b/src/app.ts index efaa358..583a56e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -46,16 +46,37 @@ app.get("/api/getRequests", async (request, response) => { await validateApiToken(request.session); var requestCount = ( request.query.count ? parseInt(request.query.count as string, 10) : 5 ); var requestOffset = ( request.query.offset ? parseInt(request.query.offset as string, 10) : 0 ); + var sortDirection = ( request.query.sortDirection == "asc" ? "ASC" : "DESC" ); + var inverseSortDirection = ( sortDirection == "ASC" ? "DESC" : "ASC" ); + switch (request.query.sort) { + case undefined: // Default sort by newest + case "timestamp": + var requestSort = `reqTimestamp ${sortDirection}`; + break; + case "score": + var requestSort = `score ${sortDirection}, reqTimestamp ${inverseSortDirection}`; + break; + case "title": + var requestSort = `title ${sortDirection}` + break; + case "requester": + var requestSort = `requester ${sortDirection}, title ${sortDirection}` + break; + default: + response.status(400); + response.send("Invalid sort"); + return; + }; var requestsTotal = await requests.getRequestsTotal(); if (request.session.user) { - requests.getRequestsVoted(requestCount,requestOffset,request.session.user.id) + requests.getRequestsVoted(requestCount,requestOffset,requestSort,request.session.user.id) .then((val: Array
---|