Coverage for an_website/quotes/quote_of_the_day/__init__.py: 78.571%
70 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"""Get a random quote for a given day."""
16from __future__ import annotations
18import logging
19from datetime import date, datetime, timedelta, timezone
20from typing import ClassVar, Final
22from tornado.web import HTTPError
24from ...utils.request_handler import APIRequestHandler
25from ..utils import (
26 QuoteReadyCheckHandler,
27 WrongQuote,
28 get_wrong_quote,
29 get_wrong_quotes,
30)
31from .data import QuoteOfTheDayData
32from .store import (
33 QUOTE_COUNT_TO_SHOW_IN_FEED,
34 QuoteOfTheDayStore,
35 RedisQuoteOfTheDayStore,
36)
38LOGGER: Final = logging.getLogger(__name__)
41class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler):
42 """The base request handler for the quote of the day."""
44 async def get_quote_by_date(
45 self, wq_date: date | str
46 ) -> None | QuoteOfTheDayData:
47 """Get the quote of the date if one was saved."""
48 if isinstance(wq_date, str):
49 wq_date = date.fromisoformat(wq_date)
51 wq_id = await self.qod_store.get_quote_id_by_date(wq_date)
52 if not wq_id:
53 return None
54 wrong_quote = await get_wrong_quote(*wq_id)
55 if not wrong_quote: # type: ignore[truthy-bool]
56 return None
57 return QuoteOfTheDayData(
58 wq_date, wrong_quote, self.get_scheme_and_netloc()
59 )
61 async def get_quote_of_today(self) -> None | QuoteOfTheDayData:
62 """Get the quote for today."""
63 today = datetime.now(timezone.utc).date()
64 quote_data = await self.get_quote_by_date(today)
65 if quote_data: # if was saved already
66 return quote_data
67 quotes = get_wrong_quotes(lambda wq: wq.rating > 1, shuffle=True)
68 if not quotes:
69 LOGGER.error("No quotes available")
70 return None
71 for quote in quotes:
72 if await self.qod_store.has_quote_been_used(quote.get_id()):
73 continue
74 wq_id = quote.get_id()
75 await self.qod_store.set_quote_to_used(wq_id)
76 await self.qod_store.set_quote_id_by_date(today, wq_id)
77 return QuoteOfTheDayData(today, quote, self.get_scheme_and_netloc())
78 LOGGER.critical("Failed to generate a new quote of the day")
79 return None
81 def get_scheme_and_netloc(self) -> str:
82 """Get the beginning of the URL."""
83 return f"{self.request.protocol}://{self.request.host}"
85 @property
86 def qod_store(self) -> QuoteOfTheDayStore:
87 """Get the store used for storing the quote of the day."""
88 return RedisQuoteOfTheDayStore(self.redis, self.redis_prefix)
91class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler):
92 """The request handler for the quote of the day RSS feed."""
94 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = (
95 "application/rss+xml",
96 "application/xml",
97 )
99 async def get(self, *, head: bool = False) -> None:
100 """Handle GET requests."""
101 if head:
102 return
103 today = datetime.now(timezone.utc).date()
104 quotes = (
105 await self.get_quote_of_today(),
106 *[
107 await self.get_quote_by_date(today - timedelta(days=i))
108 for i in range(1, QUOTE_COUNT_TO_SHOW_IN_FEED)
109 ],
110 )
111 await self.render(
112 "rss/quote_of_the_day.xml",
113 quotes=tuple(q for q in quotes if q),
114 )
117class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler):
118 """Handler for the JSON API that returns the quote of the day."""
120 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = (
121 *APIRequestHandler.POSSIBLE_CONTENT_TYPES,
122 "text/plain",
123 )
125 async def get(
126 self,
127 date_str: None | str = None,
128 *,
129 head: bool = False, # pylint: disable=unused-argument
130 ) -> None:
131 """Handle GET requests."""
132 quote_data = await (
133 self.get_quote_by_date(date_str)
134 if date_str
135 else self.get_quote_of_today()
136 )
138 if not quote_data:
139 raise HTTPError(404 if date_str else 503)
141 wrong_quote: WrongQuote = quote_data.wrong_quote
143 if self.content_type == "text/plain":
144 return await self.finish(str(wrong_quote))
146 if self.request.path.endswith("/full"):
147 return await self.finish(quote_data.to_json())
149 await self.finish_dict(
150 date=quote_data.date.isoformat(),
151 url=quote_data.get_quote_url(),
152 id=wrong_quote.get_id_as_str(),
153 quote=str(wrong_quote.quote),
154 author=str(wrong_quote.author),
155 rating=wrong_quote.rating,
156 )
159class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler):
160 """Redirect to the quote of the day."""
162 async def get(
163 self,
164 date_str: None | str = None,
165 *,
166 head: bool = False, # pylint: disable=unused-argument
167 ) -> None:
168 """Handle GET requests."""
169 wrong_quote_data = await (
170 self.get_quote_by_date(date_str)
171 if date_str
172 else self.get_quote_of_today()
173 )
175 if not wrong_quote_data:
176 raise HTTPError(404 if date_str else 503)
178 self.redirect(
179 self.fix_url(
180 wrong_quote_data.get_quote_url(),
181 ),
182 False,
183 )