Skip to content

Commit 36e5ab0

Browse files
committed
⚠ reports with dedicated endpoint
1 parent 6fb38df commit 36e5ab0

File tree

24 files changed

+242
-67
lines changed

24 files changed

+242
-67
lines changed

an_website/quotes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
QuoteOfTheDayRSS,
2929
)
3030
from .quotes import QuoteAPIHandler, QuoteById, QuoteMainPage, QuoteRedirectAPI
31+
from .report import QuoteReportApi
3132
from .share import ShareQuote
3233
from .utils import update_cache_periodically
3334

@@ -117,6 +118,8 @@ def get_module_info() -> ModuleInfo:
117118
# author/quote info
118119
(r"/zitate/info/a/([0-9]{1,10})", AuthorsInfoPage),
119120
(r"/zitate/info/z/([0-9]{1,10})", QuotesInfoPage),
121+
# report
122+
(r"/api/zitate/melden", QuoteReportApi),
120123
),
121124
name="Falsch zugeordnete Zitate",
122125
short_name="Falsche Zitate",

an_website/quotes/quotes.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0-or-later
2-
import { get, PopStateHandlers, post, setLastLocation } from "@utils/utils.js";
2+
import { get, PopStateHandlers, post, setLastLocation, d, e as getElementById } from "@utils/utils.js";
33

44
function startQuotes() {
5-
const nextButton = document.getElementById("next") as HTMLAnchorElement;
6-
const upvoteButton = document.getElementById("upvote") as HTMLButtonElement;
7-
const downvoteButton = document.getElementById(
5+
const nextButton = getElementById("next") as HTMLAnchorElement;
6+
const upvoteButton = getElementById("upvote") as HTMLButtonElement;
7+
const downvoteButton = getElementById(
88
"downvote",
99
) as HTMLButtonElement;
10-
const reportButton = document.getElementById("report") as
10+
const reportButton = getElementById("report") as
1111
| HTMLAnchorElement
1212
| null;
1313

1414
const thisQuoteId = [
15-
(document.getElementById("top")!).getAttribute("quote-id")!,
15+
(getElementById("top")!).getAttribute("quote-id")!,
1616
];
1717
const nextQuoteId = [nextButton.getAttribute("quote-id")!];
1818
const params = location.search;
@@ -31,11 +31,11 @@ function startQuotes() {
3131
}
3232
})(); // currently only letter keys are supported
3333

34-
(document.getElementById("wasd")!).innerText =
34+
(getElementById("wasd")!).innerText =
3535
`${keys[0]} (Witzig), ${keys[2]} (Nicht Witzig), ` +
3636
`${keys[1]} (Vorheriges) und ${keys[3]} (Nächstes)`;
3737

38-
document.onkeydown = (event) => {
38+
d.onkeydown = (event) => {
3939
switch (event.code) {
4040
case `Key${keys[0]}`:
4141
upvoteButton.click();
@@ -51,19 +51,19 @@ function startQuotes() {
5151
}
5252
};
5353

54-
const shareButton = document.getElementById("share") as HTMLAnchorElement;
55-
const downloadButton = document.getElementById(
54+
const shareButton = getElementById("share") as HTMLAnchorElement;
55+
const downloadButton = getElementById(
5656
"download",
5757
) as HTMLAnchorElement;
5858

59-
const author = document.getElementById("author") as HTMLAnchorElement;
60-
const quote = document.getElementById("quote") as HTMLAnchorElement;
61-
const realAuthor = document.getElementById(
59+
const author = getElementById("author") as HTMLAnchorElement;
60+
const quote = getElementById("quote") as HTMLAnchorElement;
61+
const realAuthor = getElementById(
6262
"real-author-name",
6363
) as HTMLAnchorElement;
6464

65-
const ratingText = document.getElementById("rating-text") as HTMLDivElement;
66-
const ratingImageContainer = document.getElementById(
65+
const ratingText = getElementById("rating-text") as HTMLDivElement;
66+
const ratingImageContainer = getElementById(
6767
"rating-img-container",
6868
) as HTMLDivElement;
6969

@@ -86,7 +86,7 @@ function startQuotes() {
8686
return;
8787
}
8888
const ratingNum = Number.parseInt(rating);
89-
const ratingImg = document.createElement("div");
89+
const ratingImg = d.createElement("div");
9090
ratingImg.className = "rating-img" + (
9191
ratingNum > 0 ? " witzig" : " nicht-witzig"
9292
);
@@ -152,7 +152,7 @@ function startQuotes() {
152152
author.innerText = `- ${data.author}`;
153153
realAuthor.innerText = data.real_author;
154154
realAuthor.href = `/zitate/info/a/${data.real_author_id}${params}`;
155-
if (reportButton) {
155+
if (reportButton?.href) {
156156
const reportHrefParams = new URLSearchParams(params);
157157
reportHrefParams.set(
158158
"subject",
@@ -224,7 +224,7 @@ function startQuotes() {
224224
}
225225

226226
for (
227-
const autoSubmitEl of (document.getElementsByClassName(
227+
const autoSubmitEl of (d.getElementsByClassName(
228228
"auto-submit-element",
229229
) as HTMLCollectionOf<HTMLInputElement>)
230230
) {

an_website/quotes/report.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# This program is free software: you can redistribute it and/or modify
2+
# it under the terms of the GNU Affero General Public License as
3+
# published by the Free Software Foundation, either version 3 of the
4+
# License, or (at your option) any later version.
5+
#
6+
# This program is distributed in the hope that it will be useful,
7+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+
# GNU Affero General Public License for more details.
10+
#
11+
# You should have received a copy of the GNU Affero General Public License
12+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
13+
14+
"""Report wrong quotes for being correct."""
15+
import dataclasses
16+
17+
from tornado.web import MissingArgumentError
18+
19+
from ..main import LOGGER
20+
from ..utils.data_parsing import parse_args
21+
from ..utils.request_handler import APIRequestHandler
22+
from .utils import (
23+
AUTHORS_CACHE,
24+
QUOTES_CACHE,
25+
Author,
26+
Quote,
27+
WrongQuote,
28+
get_wrong_quote,
29+
)
30+
31+
32+
@dataclasses.dataclass(slots=True)
33+
class QuoteReportArgs:
34+
"""Arguments to the quote-report API."""
35+
36+
author_id: int | None = None
37+
quote_id: int | None = None
38+
reason: str | None = None
39+
40+
def validate(self) -> None:
41+
"""Validate the arguments."""
42+
if self.author_id is None and self.quote_id is None:
43+
raise MissingArgumentError("quote_id")
44+
45+
def get_quote_url_path(self) -> str:
46+
"""Get the URL that got reported."""
47+
if self.author_id is None:
48+
return f"/zitate/info/q/{self.quote_id}"
49+
if self.quote_id is None:
50+
return f"/zitate/info/a/{self.author_id}"
51+
return f"/zitate/{self.quote_id}-{self.quote_id}"
52+
53+
async def get_quote_object(self) -> Quote | WrongQuote | Author | None:
54+
"""Get the quote object."""
55+
if self.author_id is not None:
56+
if self.quote_id is not None:
57+
return await get_wrong_quote(
58+
quote_id=self.quote_id, author_id=self.author_id
59+
)
60+
return AUTHORS_CACHE.get(self.author_id)
61+
if self.quote_id is not None:
62+
return QUOTES_CACHE.get(self.quote_id)
63+
return None
64+
65+
66+
class QuoteReportApi(APIRequestHandler):
67+
"""Report wrong quotes."""
68+
69+
@parse_args(type_=QuoteReportArgs, validation_method="validate")
70+
async def post(self, *, args: QuoteReportArgs) -> None:
71+
"""Handle POST requests to the author info page."""
72+
quote_obj = await args.get_quote_object()
73+
LOGGER.critical(
74+
"Reported: %s\n" "Reason: %r\n" "\n" "%r",
75+
self.fix_url(args.get_quote_url_path()),
76+
args.reason,
77+
quote_obj.to_json() if quote_obj else None,
78+
)
79+
if self.content_type == "application/json":
80+
await self.finish("true\n")
81+
else:
82+
await self.finish()

an_website/quotes/report.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { post, e as getElementById } from "@utils/utils.js";
2+
3+
interface Reasoned {
4+
reason?: string;
5+
}
6+
7+
type Data = {
8+
author_id: number,
9+
quote_id: number,
10+
} | {
11+
author_id: number,
12+
} | {
13+
quote_id: number,
14+
};
15+
16+
function getDataFromUrl(): Data {
17+
const path: string[] = location.pathname.split('/');
18+
console.debug(path);
19+
if (path[path.length - 1] == "") {
20+
path.pop();
21+
}
22+
const infoIndex = path.indexOf("info");
23+
const last = path.pop()!;
24+
if (infoIndex == -1) {
25+
const [quote_id, author_id] = last.split("-", 2).map(Number);
26+
return { quote_id: quote_id!, author_id: author_id! };
27+
}
28+
const type = path[infoIndex + 1];
29+
if (type == "z") {
30+
return { quote_id: Number(last) };
31+
}
32+
if (type == "a") {
33+
return { quote_id: Number(last) };
34+
}
35+
console.error({ msg: "invalid path", path });
36+
throw new Error("Invalid state");
37+
}
38+
39+
function createReportButton(anchor: HTMLAnchorElement) {
40+
anchor.removeAttribute('href')
41+
anchor.addEventListener("click", (event: Event) => {
42+
event.preventDefault();
43+
const data: Data & Reasoned = getDataFromUrl();
44+
const reason = prompt("Grund fürs Melden?", "");
45+
if (reason === null) {
46+
console.debug("Aborted");
47+
return;
48+
}
49+
if (reason) {
50+
console.debug("Provided reason: " + reason);
51+
data.reason = reason;
52+
}
53+
void reportQuote(data);
54+
});
55+
}
56+
57+
function reportQuote(reportData: Data & Reasoned): Promise<void> {
58+
console.debug({ msg: "reporting", reportData });
59+
return post(
60+
"/api/zitate/melden",
61+
reportData,
62+
(data: boolean) => {
63+
if (data) {
64+
alert("Erfolgreich gemeldet!")
65+
}
66+
}
67+
);
68+
}
69+
70+
createReportButton(getElementById("report")! as HTMLAnchorElement);

an_website/search/search.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0-or-later
2-
import { get, PopStateHandlers, setURLParam } from "@utils/utils.js";
2+
import { get, PopStateHandlers, setURLParam, e as getElementById, d } from "@utils/utils.js";
33

4-
const resultsList = document.getElementById("search-results")!;
5-
const searchForm = document.getElementById("search-form") as HTMLFormElement;
6-
const searchInput = document.getElementById("search-input") as HTMLInputElement;
4+
const resultsList = getElementById("search-results")!;
5+
const searchForm = getElementById("search-form") as HTMLFormElement;
6+
const searchInput = getElementById("search-input") as HTMLInputElement;
77

88
interface Result {
99
score: number;
@@ -15,7 +15,7 @@ interface Result {
1515
function displayResults(results: Result[]) {
1616
resultsList.innerHTML = "";
1717
for (const result of results) {
18-
const resultElement = document.createElement("li");
18+
const resultElement = d.createElement("li");
1919
resultElement.setAttribute("score", String(result.score));
2020
resultElement.innerHTML = `<a href='${result.url}'>` +
2121
`${result.title}</a> ${result.description}`;

an_website/static/js/currency_converter/converter.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)