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

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 

16import logging 

17from datetime import date, datetime, timedelta, timezone 

18from typing import ClassVar, Final 

19 

20from tornado.web import HTTPError 

21 

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) 

35 

36LOGGER: Final = logging.getLogger(__name__) 

37 

38 

39class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler): 

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

41 

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) 

48 

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 ) 

58 

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 

78 

79 def get_scheme_and_netloc(self) -> str: 

80 """Get the beginning of the URL.""" 

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

82 

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) 

87 

88 

89class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler): 

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

91 

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

93 "application/rss+xml", 

94 "application/xml", 

95 ) 

96 

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 ) 

113 

114 

115class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler): 

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

117 

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

119 *APIRequestHandler.POSSIBLE_CONTENT_TYPES, 

120 "text/plain", 

121 ) 

122 

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 ) 

135 

136 if not quote_data: 

137 raise HTTPError(404 if date_str else 503) 

138 

139 wrong_quote: WrongQuote = quote_data.wrong_quote 

140 

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

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

143 

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

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

146 

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 ) 

155 

156 

157class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler): 

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

159 

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 ) 

172 

173 if not wrong_quote_data: 

174 raise HTTPError(404 if date_str else 503) 

175 

176 self.redirect( 

177 self.fix_url( 

178 wrong_quote_data.get_quote_url(), 

179 ), 

180 False, 

181 )