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