Coverage for an_website/quotes/create.py: 71.200%
125 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-22 15:59 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-22 15:59 +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 tornado.web import HTTPError, MissingArgumentError
23from ..utils.data_parsing import parse_args
24from ..utils.utils import bounded_edit_distance
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 bounded_edit_distance(
121 author.name.lower(), author_name_lower, max_distance + 1
122 )
123 <= max_distance
124 ),
125 fix_author_name(author_name),
126 ]
127 # authors should be in most cases title case
128 fixed_author = author_name.title()
129 if fixed_author not in authors:
130 authors.append(fixed_author)
131 # no other fixes for authors that are less long
132 if len(author_name) < 2:
133 return authors
134 # maybe only the first letter upper case
135 fixed_author_2 = author_name[0].upper() + author_name[1:]
136 if fixed_author_2 not in authors:
137 authors.append(fixed_author_2)
138 return authors
141def get_author_by_name(name: str) -> None | Author:
142 """Get an author by its name."""
143 lower_name = fix_author_name(name).lower()
144 for author in AUTHORS_CACHE.values():
145 if author.name.lower() == lower_name:
146 return author
147 return None
150def get_quote_by_str(quote_str: str) -> None | Quote:
151 """Get an author by its name."""
152 lower_quote = fix_quote_str(quote_str).lower()
153 for quote in QUOTES_CACHE.values():
154 if quote.quote.lower() == lower_quote:
155 return quote
156 return None
159async def get_quotes(quote_str: str) -> list[Quote | str]:
160 """Get the possible meant quotes based on the str."""
161 quote: None | Quote = get_quote_by_str(quote_str)
162 if isinstance(quote, Quote):
163 return [quote]
165 lower_quote_str = quote_str.lower()
166 max_distance = min(16, len(quote_str) // 2 + 1)
167 quotes: list[Quote | str] = [
168 *(
169 quote
170 for quote in QUOTES_CACHE.values()
171 if bounded_edit_distance(
172 quote.quote.lower(), lower_quote_str, max_distance + 1
173 )
174 <= max_distance
175 ),
176 fix_quote_str(quote_str),
177 ]
178 if not (
179 quote_str.endswith("?")
180 or quote_str.endswith(".")
181 or quote_str.endswith("!")
182 ):
183 quotes.append(quote_str + ".")
184 return quotes
187@dataclass(slots=True, frozen=True)
188class QuoteInfoArgs:
189 """Class representing a quote id and an author id."""
191 quote: int | None = None
192 author: int | None = None
195class CreatePage1(QuoteReadyCheckHandler):
196 """The request handler for the create page."""
198 RATELIMIT_POST_LIMIT = 5
199 RATELIMIT_POST_COUNT_PER_PERIOD = 10
201 @parse_args(type_=QuoteInfoArgs)
202 async def get(self, *, args: QuoteInfoArgs, head: bool = False) -> None:
203 """Handle GET requests to the create page."""
204 if args.quote is not None and args.author is not None:
205 return self.redirect(f"/zitate/{args.quote}-{args.author}")
207 if head:
208 return
210 await self.render(
211 "pages/quotes/create1.html",
212 quotes=tuple(QUOTES_CACHE.values()),
213 authors=tuple(AUTHORS_CACHE.values()),
214 selected_quote=(
215 None if args.quote is None else QUOTES_CACHE.get(args.quote)
216 ),
217 selected_author=(
218 None if args.author is None else AUTHORS_CACHE.get(args.author)
219 ),
220 )
222 async def post(self) -> None:
223 """Handle POST requests to the create page."""
224 quote_str = self.get_argument("quote-1", None)
225 fake_author_str = self.get_argument("fake-author-1", None)
226 if not (quote_str and fake_author_str):
227 raise HTTPError(
228 400,
229 reason=(
230 "Missing arguments. quote-1 and fake-author-1 are needed."
231 ),
232 )
233 quote: None | Quote = get_quote_by_str(quote_str)
234 fake_author: None | Author = get_author_by_name(fake_author_str)
236 if quote and fake_author:
237 # if selected existing quote and existing
238 # fake author just redirect to the page of this quote
239 return self.redirect(
240 self.fix_url(f"/zitate/{quote.id}-{fake_author.id}"),
241 )
243 if not quote:
244 # TODO: search for real author, to reduce work for users
245 real_author_str = self.get_argument("real-author-1", None)
246 if not real_author_str:
247 raise HTTPError(
248 400, reason="Missing arguments. real-author-1 is needed."
249 )
251 quotes: list[Quote | str] = (
252 [quote] if quote else await get_quotes(quote_str)
253 )
254 real_authors: list[Author | str] = (
255 [quote.author]
256 if quote
257 else await get_authors(cast(str, real_author_str)) # type: ignore[possibly-undefined] # noqa: B950
258 )
259 fake_authors: list[Author | str] = (
260 [fake_author] if fake_author else await get_authors(fake_author_str)
261 )
263 if 1 == len(quotes) == len(real_authors) == len(fake_authors):
264 wq_id = await create_wrong_quote(
265 real_authors[0],
266 fake_authors[0],
267 quotes[0],
268 )
269 return self.redirect(self.fix_url(f"/zitate/{wq_id}"))
271 await self.render(
272 "pages/quotes/create2.html",
273 quotes=quotes,
274 real_authors=real_authors,
275 fake_authors=fake_authors,
276 )
279class CreatePage2(QuoteReadyCheckHandler):
280 """The request handler for the second part of the create page."""
282 RATELIMIT_POST_LIMIT = 5
283 RATELIMIT_POST_COUNT_PER_PERIOD = 10
285 async def post(self) -> None:
286 """Handle POST requests to the create page."""
287 quote_str = self.get_argument("quote-2", None)
288 if not quote_str:
289 raise MissingArgumentError("quote-2")
290 fake_author_str = self.get_argument("fake-author-2", None)
291 if not fake_author_str:
292 raise MissingArgumentError("fake-author-2")
294 if (quote := get_quote_by_str(quote_str)) and (
295 fake_author := get_author_by_name(fake_author_str)
296 ):
297 # if selected existing quote and existing
298 # fake author just redirect to the page of this quote
299 return self.redirect(
300 self.fix_url(f"/zitate/{quote.id}-{fake_author.id}"),
301 )
303 real_author = self.get_argument("real-author-2", None)
304 if not real_author:
305 raise MissingArgumentError("real-author-2")
307 wq_id = await create_wrong_quote(
308 real_author,
309 fake_author_str,
310 quote_str,
311 )
312 return self.redirect(
313 self.fix_url(
314 f"/zitate/{wq_id}",
315 )
316 )