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

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 

16 

17import logging 

18from datetime import date, datetime, timedelta, timezone 

19from typing import ClassVar, Final 

20 

21from tornado.web import HTTPError 

22 

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) 

36 

37LOGGER: Final = logging.getLogger(__name__) 

38 

39 

40class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler): 

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

42 

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) 

49 

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 ) 

59 

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 

79 

80 def get_scheme_and_netloc(self) -> str: 

81 """Get the beginning of the URL.""" 

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

83 

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) 

88 

89 

90class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler): 

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

92 

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

94 "application/rss+xml", 

95 "application/xml", 

96 ) 

97 

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 ) 

114 

115 

116class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler): 

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

118 

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

120 *APIRequestHandler.POSSIBLE_CONTENT_TYPES, 

121 "text/plain", 

122 ) 

123 

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 ) 

136 

137 if not quote_data: 

138 raise HTTPError(404 if date_str else 503) 

139 

140 wrong_quote: WrongQuote = quote_data.wrong_quote 

141 

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

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

144 

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

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

147 

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 ) 

156 

157 

158class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler): 

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

160 

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 ) 

173 

174 if not wrong_quote_data: 

175 raise HTTPError(404 if date_str else 503) 

176 

177 self.redirect( 

178 self.fix_url( 

179 wrong_quote_data.get_quote_url(), 

180 ), 

181 False, 

182 )