Coverage for an_website/quotes/quote_of_the_day/__init__.py: 69.565%
69 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18:56 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18: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"""Get a random quote for a given day."""
16import logging
17from datetime import date, datetime, timedelta, timezone
18from typing import ClassVar, Final
20from tornado.web import HTTPError
22from ...utils.request_handler import APIRequestHandler
23from ..utils import (
24 QuoteReadyCheckHandler,
25 WrongQuote,
26 get_wrong_quote,
27 get_wrong_quotes,
28)
29from .data import QuoteOfTheDayData
30from .store import (
31 QUOTE_COUNT_TO_SHOW_IN_FEED,
32 QuoteOfTheDayStore,
33 RedisQuoteOfTheDayStore,
34)
36LOGGER: Final = logging.getLogger(__name__)
39class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler):
40 """The base request handler for the quote of the day."""
42 async def get_quote_by_date(
43 self, wq_date: date | str
44 ) -> None | QuoteOfTheDayData:
45 """Get the quote of the date if one was saved."""
46 if isinstance(wq_date, str):
47 wq_date = date.fromisoformat(wq_date)
49 wq_id = await self.qod_store.get_quote_id_by_date(wq_date)
50 if not wq_id:
51 return None
52 wrong_quote = await get_wrong_quote(*wq_id)
53 if not wrong_quote:
54 return None
55 return QuoteOfTheDayData(
56 wq_date, wrong_quote, self.get_scheme_and_netloc()
57 )
59 async def get_quote_of_today(self) -> None | QuoteOfTheDayData:
60 """Get the quote for today."""
61 today = datetime.now(timezone.utc).date()
62 quote_data = await self.get_quote_by_date(today)
63 if quote_data: # if was saved already
64 return quote_data
65 quotes = get_wrong_quotes(lambda wq: wq.rating > 1, shuffle=True)
66 if not quotes:
67 LOGGER.error("No quotes available")
68 return None
69 for quote in quotes:
70 if await self.qod_store.has_quote_been_used(quote.get_id()):
71 continue
72 wq_id = quote.get_id()
73 await self.qod_store.set_quote_to_used(wq_id)
74 await self.qod_store.set_quote_id_by_date(today, wq_id)
75 return QuoteOfTheDayData(today, quote, self.get_scheme_and_netloc())
76 LOGGER.critical("Failed to generate a new quote of the day")
77 return None
79 def get_scheme_and_netloc(self) -> str:
80 """Get the beginning of the URL."""
81 return f"{self.request.protocol}://{self.request.host}"
83 @property
84 def qod_store(self) -> QuoteOfTheDayStore:
85 """Get the store used for storing the quote of the day."""
86 return RedisQuoteOfTheDayStore(self.redis, self.redis_prefix)
89class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler):
90 """The request handler for the quote of the day RSS feed."""
92 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = (
93 "application/rss+xml",
94 "application/xml",
95 )
97 async def get(self, *, head: bool = False) -> None:
98 """Handle GET requests."""
99 if head:
100 return
101 today = datetime.now(timezone.utc).date()
102 quotes = (
103 await self.get_quote_of_today(),
104 *[
105 await self.get_quote_by_date(today - timedelta(days=i))
106 for i in range(1, QUOTE_COUNT_TO_SHOW_IN_FEED)
107 ],
108 )
109 await self.render(
110 "rss/quote_of_the_day.xml",
111 quotes=tuple(q for q in quotes if q),
112 )
115class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler):
116 """Handler for the JSON API that returns the quote of the day."""
118 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = (
119 *APIRequestHandler.POSSIBLE_CONTENT_TYPES,
120 "text/plain",
121 )
123 async def get(
124 self,
125 date_str: None | str = None,
126 *,
127 head: bool = False, # pylint: disable=unused-argument
128 ) -> None:
129 """Handle GET requests."""
130 quote_data = await (
131 self.get_quote_by_date(date_str)
132 if date_str
133 else self.get_quote_of_today()
134 )
136 if not quote_data:
137 raise HTTPError(404 if date_str else 503)
139 wrong_quote: WrongQuote = quote_data.wrong_quote
141 if self.content_type == "text/plain":
142 return await self.finish(str(wrong_quote))
144 if self.request.path.endswith("/full"):
145 return await self.finish(quote_data.to_json())
147 await self.finish_dict(
148 date=quote_data.date.isoformat(),
149 url=quote_data.get_quote_url(),
150 id=wrong_quote.get_id_as_str(),
151 quote=str(wrong_quote.quote),
152 author=str(wrong_quote.author),
153 rating=wrong_quote.rating,
154 )
157class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler):
158 """Redirect to the quote of the day."""
160 async def get(
161 self,
162 date_str: None | str = None,
163 *,
164 head: bool = False, # pylint: disable=unused-argument
165 ) -> None:
166 """Handle GET requests."""
167 wrong_quote_data = await (
168 self.get_quote_by_date(date_str)
169 if date_str
170 else self.get_quote_of_today()
171 )
173 if not wrong_quote_data:
174 raise HTTPError(404 if date_str else 503)
176 self.redirect(
177 self.fix_url(
178 wrong_quote_data.get_quote_url(),
179 ),
180 False,
181 )