Coverage for an_website/quotes/create.py: 71.200%
125 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-16 19:56 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-16 19:56 +0000
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/>.
14"""A page to create new wrong quotes."""
16from __future__ import annotations
18from dataclasses import dataclass
19from typing import cast
21from rapidfuzz.distance.Levenshtein import distance
22from tornado.web import HTTPError, MissingArgumentError
24from ..utils.data_parsing import parse_args
25from .utils import (
26 AUTHORS_CACHE,
27 QUOTES_CACHE,
28 Author,
29 Quote,
30 QuoteReadyCheckHandler,
31 fix_author_name,
32 fix_quote_str,
33 make_api_request,
34 parse_author,
35 parse_quote,
36)
39async def create_quote(quote_str: str, author: Author) -> Quote:
40 """Create a quote."""
41 quote_str = fix_quote_str(quote_str)
43 quote = get_quote_by_str(quote_str)
44 if quote is not None:
45 return quote
47 return parse_quote(
48 await make_api_request(
49 "quotes",
50 method="POST",
51 body={
52 "author": str(author.id),
53 "quote": quote_str,
54 },
55 entity_should_exist=False,
56 )
57 )
60async def create_author(author_str: str) -> Author:
61 """Create an author."""
62 author_str = fix_author_name(author_str)
64 author = get_author_by_name(author_str)
65 if author is not None:
66 return author
68 return parse_author(
69 await make_api_request(
70 "authors",
71 method="POST",
72 body={"author": author_str},
73 entity_should_exist=False,
74 )
75 )
78async def create_wrong_quote(
79 real_author_param: Author | str,
80 fake_author_param: Author | str,
81 quote_param: Quote | str,
82) -> str:
83 """Create a wrong quote and return the id in the q_id-a_id format."""
84 if isinstance(fake_author_param, str):
85 if not fake_author_param:
86 raise HTTPError(400, "Fake author is needed, but empty.")
87 fake_author = await create_author(fake_author_param)
88 else:
89 fake_author = fake_author_param
91 if isinstance(quote_param, str):
92 if not quote_param:
93 raise HTTPError(400, "Quote is needed, but empty.")
95 if isinstance(real_author_param, str):
96 if not real_author_param:
97 raise HTTPError(400, "Real author is needed, but empty.")
98 real_author = await create_author(real_author_param)
99 else:
100 real_author = real_author_param
101 quote = await create_quote(quote_param, real_author)
102 else:
103 quote = quote_param
105 return f"{quote.id}-{fake_author.id}"
108async def get_authors(author_name: str) -> list[Author | str]:
109 """Get the possible meant authors based on the str."""
110 author = get_author_by_name(author_name)
111 if author is not None:
112 return [author]
114 author_name_lower = author_name.lower()
115 max_distance = min(5, len(author_name) // 2 + 1)
116 authors: list[Author | str] = [
117 *(
118 author
119 for author in AUTHORS_CACHE.values()
120 if distance(author.name.lower(), author_name_lower) <= max_distance
121 ),
122 fix_author_name(author_name),
123 ]
124 # authors should be in most cases title case
125 fixed_author = author_name.title()
126 if fixed_author not in authors:
127 authors.append(fixed_author)
128 # no other fixes for authors that are less long
129 if len(author_name) < 2:
130 return authors
131 # maybe only the first letter upper case
132 fixed_author_2 = author_name[0].upper() + author_name[1:]
133 if fixed_author_2 not in authors:
134 authors.append(fixed_author_2)
135 return authors
138def get_author_by_name(name: str) -> None | Author:
139 """Get an author by its name."""
140 lower_name = fix_author_name(name).lower()
141 for author in AUTHORS_CACHE.values():
142 if author.name.lower() == lower_name:
143 return author
144 return None
147def get_quote_by_str(quote_str: str) -> None | Quote:
148 """Get an author by its name."""
149 lower_quote = fix_quote_str(quote_str).lower()
150 for quote in QUOTES_CACHE.values():
151 if quote.quote.lower() == lower_quote:
152 return quote
153 return None
156async def get_quotes(quote_str: str) -> list[Quote | str]:
157 """Get the possible meant quotes based on the str."""
158 quote: None | Quote = get_quote_by_str(quote_str)
159 if isinstance(quote, Quote):
160 return [quote]
162 lower_quote_str = quote_str.lower()
163 max_distance = min(16, len(quote_str) // 2 + 1)
164 quotes: list[Quote | str] = [
165 *(
166 quote
167 for quote in QUOTES_CACHE.values()
168 if distance(quote.quote.lower(), lower_quote_str) <= max_distance
169 ),
170 fix_quote_str(quote_str),
171 ]
172 if not (
173 quote_str.endswith("?")
174 or quote_str.endswith(".")
175 or quote_str.endswith("!")
176 ):
177 quotes.append(quote_str + ".")
178 return quotes
181@dataclass(slots=True, frozen=True)
182class QuoteInfoArgs:
183 """Class representing a quote id and an author id."""
185 quote: int | None = None
186 author: int | None = None
189class CreatePage1(QuoteReadyCheckHandler):
190 """The request handler for the create page."""
192 RATELIMIT_POST_LIMIT = 5
193 RATELIMIT_POST_COUNT_PER_PERIOD = 10
195 @parse_args(type_=QuoteInfoArgs)
196 async def get(self, *, args: QuoteInfoArgs, head: bool = False) -> None:
197 """Handle GET requests to the create page."""
198 if args.quote is not None and args.author is not None:
199 return self.redirect(f"/zitate/{args.quote}-{args.author}")
201 if head:
202 return
204 await self.render(
205 "pages/quotes/create1.html",
206 quotes=tuple(QUOTES_CACHE.values()),
207 authors=tuple(AUTHORS_CACHE.values()),
208 selected_quote=(
209 None if args.quote is None else QUOTES_CACHE.get(args.quote)
210 ),
211 selected_author=(
212 None if args.author is None else AUTHORS_CACHE.get(args.author)
213 ),
214 )
216 async def post(self) -> None:
217 """Handle POST requests to the create page."""
218 quote_str = self.get_argument("quote-1", None)
219 fake_author_str = self.get_argument("fake-author-1", None)
220 if not (quote_str and fake_author_str):
221 raise HTTPError(
222 400,
223 reason=(
224 "Missing arguments. quote-1 and fake-author-1 are needed."
225 ),
226 )
227 quote: None | Quote = get_quote_by_str(quote_str)
228 fake_author: None | Author = get_author_by_name(fake_author_str)
230 if quote and fake_author:
231 # if selected existing quote and existing
232 # fake author just redirect to the page of this quote
233 return self.redirect(
234 self.fix_url(f"/zitate/{quote.id}-{fake_author.id}"),
235 )
237 if not quote:
238 # TODO: search for real author, to reduce work for users
239 real_author_str = self.get_argument("real-author-1", None)
240 if not real_author_str:
241 raise HTTPError(
242 400, reason="Missing arguments. real-author-1 is needed."
243 )
245 quotes: list[Quote | str] = (
246 [quote] if quote else await get_quotes(quote_str)
247 )
248 real_authors: list[Author | str] = (
249 [quote.author]
250 if quote
251 else await get_authors(cast(str, real_author_str)) # type: ignore[possibly-undefined] # noqa: B950
252 )
253 fake_authors: list[Author | str] = (
254 [fake_author] if fake_author else await get_authors(fake_author_str)
255 )
257 if 1 == len(quotes) == len(real_authors) == len(fake_authors):
258 wq_id = await create_wrong_quote(
259 real_authors[0],
260 fake_authors[0],
261 quotes[0],
262 )
263 return self.redirect(self.fix_url(f"/zitate/{wq_id}"))
265 await self.render(
266 "pages/quotes/create2.html",
267 quotes=quotes,
268 real_authors=real_authors,
269 fake_authors=fake_authors,
270 )
273class CreatePage2(QuoteReadyCheckHandler):
274 """The request handler for the second part of the create page."""
276 RATELIMIT_POST_LIMIT = 5
277 RATELIMIT_POST_COUNT_PER_PERIOD = 10
279 async def post(self) -> None:
280 """Handle POST requests to the create page."""
281 quote_str = self.get_argument("quote-2", None)
282 if not quote_str:
283 raise MissingArgumentError("quote-2")
284 fake_author_str = self.get_argument("fake-author-2", None)
285 if not fake_author_str:
286 raise MissingArgumentError("fake-author-2")
288 if (quote := get_quote_by_str(quote_str)) and (
289 fake_author := get_author_by_name(fake_author_str)
290 ):
291 # if selected existing quote and existing
292 # fake author just redirect to the page of this quote
293 return self.redirect(
294 self.fix_url(f"/zitate/{quote.id}-{fake_author.id}"),
295 )
297 real_author = self.get_argument("real-author-2", None)
298 if not real_author:
299 raise MissingArgumentError("real-author-2")
301 wq_id = await create_wrong_quote(
302 real_author,
303 fake_author_str,
304 quote_str,
305 )
306 return self.redirect(
307 self.fix_url(
308 f"/zitate/{wq_id}",
309 )
310 )