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

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/>. 

13 

14"""Get a random quote for a given day.""" 

15 

16from __future__ import annotations 

17 

18import logging 

19from datetime import date, datetime, timedelta, timezone 

20from typing import ClassVar, Final 

21 

22from tornado.web import HTTPError 

23 

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) 

37 

38LOGGER: Final = logging.getLogger(__name__) 

39 

40 

41class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler): 

42 """The base request handler for the quote of the day.""" 

43 

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) 

50 

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 ) 

60 

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 

80 

81 def get_scheme_and_netloc(self) -> str: 

82 """Get the beginning of the URL.""" 

83 return f"{self.request.protocol}://{self.request.host}" 

84 

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) 

89 

90 

91class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler): 

92 """The request handler for the quote of the day RSS feed.""" 

93 

94 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = ( 

95 "application/rss+xml", 

96 "application/xml", 

97 ) 

98 

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 ) 

115 

116 

117class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler): 

118 """Handler for the JSON API that returns the quote of the day.""" 

119 

120 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = ( 

121 *APIRequestHandler.POSSIBLE_CONTENT_TYPES, 

122 "text/plain", 

123 ) 

124 

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 ) 

137 

138 if not quote_data: 

139 raise HTTPError(404 if date_str else 503) 

140 

141 wrong_quote: WrongQuote = quote_data.wrong_quote 

142 

143 if self.content_type == "text/plain": 

144 return await self.finish(str(wrong_quote)) 

145 

146 if self.request.path.endswith("/full"): 

147 return await self.finish(quote_data.to_json()) 

148 

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 ) 

157 

158 

159class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler): 

160 """Redirect to the quote of the day.""" 

161 

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 ) 

174 

175 if not wrong_quote_data: 

176 raise HTTPError(404 if date_str else 503) 

177 

178 self.redirect( 

179 self.fix_url( 

180 wrong_quote_data.get_quote_url(), 

181 ), 

182 False, 

183 )