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

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 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) 

32 

33LOGGER: Final = logging.getLogger(__name__) 

34 

35 

36class QuoteOfTheDayBaseHandler(QuoteReadyCheckHandler): 

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

38 

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) 

45 

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 ) 

55 

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 

75 

76 def get_scheme_and_netloc(self) -> str: 

77 """Get the beginning of the URL.""" 

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

79 

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) 

84 

85 

86class QuoteOfTheDayRSS(QuoteOfTheDayBaseHandler): 

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

88 

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

90 "application/rss+xml", 

91 "application/xml", 

92 ) 

93 

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 ) 

110 

111 

112class QuoteOfTheDayAPI(APIRequestHandler, QuoteOfTheDayBaseHandler): 

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

114 

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 ) 

127 

128 if not quote_data: 

129 raise HTTPError(404 if date_str else 503) 

130 

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

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

133 

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 ) 

143 

144 

145class QuoteOfTheDayRedirect(QuoteOfTheDayBaseHandler): 

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

147 

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 ) 

160 

161 if not wrong_quote_data: 

162 raise HTTPError(404 if date_str else 503) 

163 

164 self.redirect( 

165 self.fix_url( 

166 wrong_quote_data.get_quote_url(), 

167 ), 

168 False, 

169 )