Coverage for an_website/quotes/quote_of_the_day/__init__.py: 80.597%
67 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"""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 QuoteReadyCheckHandler, get_wrong_quote, get_wrong_quotes
26from .data import QuoteOfTheDayData
27from .store import (
28 QUOTE_COUNT_TO_SHOW_IN_FEED,
29 QuoteOfTheDayStore,
30 RedisQuoteOfTheDayStore,
31)
33LOGGER: Final = logging.getLogger(__name__)
36class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler):
37 """The base request handler for the quote of the day."""
39 async def get_quote_by_date(
40 self, wq_date: date | str
41 ) -> None | QuoteOfTheDayData:
42 """Get the quote of the date if one was saved."""
43 if isinstance(wq_date, str):
44 wq_date = date.fromisoformat(wq_date)
46 wq_id = await self.qod_store.get_quote_id_by_date(wq_date)
47 if not wq_id:
48 return None
49 wrong_quote = await get_wrong_quote(*wq_id)
50 if not wrong_quote: # type: ignore[truthy-bool]
51 return None
52 return QuoteOfTheDayData(
53 wq_date, wrong_quote, self.get_scheme_and_netloc()
54 )
56 async def get_quote_of_today(self) -> None | QuoteOfTheDayData:
57 """Get the quote for today."""
58 today = datetime.now(timezone.utc).date()
59 quote_data = await self.get_quote_by_date(today)
60 if quote_data: # if was saved already
61 return quote_data
62 quotes = get_wrong_quotes(lambda wq: wq.rating > 1, shuffle=True)
63 if not quotes:
64 LOGGER.error("No quotes available")
65 return None
66 for quote in quotes:
67 if await self.qod_store.has_quote_been_used(quote.get_id()):
68 continue
69 wq_id = quote.get_id()
70 await self.qod_store.set_quote_to_used(wq_id)
71 await self.qod_store.set_quote_id_by_date(today, wq_id)
72 return QuoteOfTheDayData(today, quote, self.get_scheme_and_netloc())
73 LOGGER.critical("Failed to generate a new quote of the day")
74 return None
76 def get_scheme_and_netloc(self) -> str:
77 """Get the beginning of the URL."""
78 return f"{self.request.protocol}://{self.request.host}"
80 @property
81 def qod_store(self) -> QuoteOfTheDayStore:
82 """Get the store used for storing the quote of the day."""
83 return RedisQuoteOfTheDayStore(self.redis, self.redis_prefix)
86class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler):
87 """The request handler for the quote of the day RSS feed."""
89 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = (
90 "application/rss+xml",
91 "application/xml",
92 )
94 async def get(self, *, head: bool = False) -> None:
95 """Handle GET requests."""
96 if head:
97 return
98 today = datetime.now(timezone.utc).date()
99 quotes = (
100 await self.get_quote_of_today(),
101 *[
102 await self.get_quote_by_date(today - timedelta(days=i))
103 for i in range(1, QUOTE_COUNT_TO_SHOW_IN_FEED)
104 ],
105 )
106 await self.render(
107 "rss/quote_of_the_day.xml",
108 quotes=tuple(q for q in quotes if q),
109 )
112class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler):
113 """Handler for the JSON API that returns the quote of the day."""
115 async def get(
116 self,
117 date_str: None | str = None,
118 *,
119 head: bool = False, # pylint: disable=unused-argument
120 ) -> None:
121 """Handle GET requests."""
122 quote_data = await (
123 self.get_quote_by_date(date_str)
124 if date_str
125 else self.get_quote_of_today()
126 )
128 if not quote_data:
129 raise HTTPError(404 if date_str else 503)
131 if self.request.path.endswith("/full"):
132 return await self.finish(quote_data.to_json())
134 wrong_quote = quote_data.wrong_quote
135 await self.finish_dict(
136 date=quote_data.date.isoformat(),
137 url=quote_data.get_quote_url(),
138 id=wrong_quote.get_id_as_str(),
139 quote=str(wrong_quote.quote),
140 author=str(wrong_quote.author),
141 rating=wrong_quote.rating,
142 )
145class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler):
146 """Redirect to the quote of the day."""
148 async def get(
149 self,
150 date_str: None | str = None,
151 *,
152 head: bool = False, # pylint: disable=unused-argument
153 ) -> None:
154 """Handle GET requests."""
155 wrong_quote_data = await (
156 self.get_quote_by_date(date_str)
157 if date_str
158 else self.get_quote_of_today()
159 )
161 if not wrong_quote_data:
162 raise HTTPError(404 if date_str else 503)
164 self.redirect(
165 self.fix_url(
166 wrong_quote_data.get_quote_url(),
167 ),
168 False,
169 )