1111from django .utils import timezone
1212from django .utils .http import urlencode
1313
14+ from weblate .trans .actions import ActionEvents
1415from weblate .trans .feeds import TranslationChangesFeed
1516from weblate .trans .models import Change , Unit
1617from weblate .trans .tests .test_views import FixtureTestCase , ViewTestCase
18+ from weblate .utils .xml import parse_xml
1719
1820
1921class FeedQueriesTest (FixtureTestCase ):
@@ -61,10 +63,55 @@ def test_translation_feed_queries_do_not_scale_with_change_count(self) -> None:
6163
6264
6365class ChangesTest (ViewTestCase ):
66+ def assert_rss_response (self , response ) -> None :
67+ self .assertEqual (response .status_code , 200 )
68+ self .assertEqual (response ["Content-Type" ], "application/rss+xml; charset=utf-8" )
69+ self .assertContains (response , "<rss" )
70+
71+ def add_filtered_rss_changes (self ) -> None :
72+ Change .objects .all ().delete ()
73+ self .change_unit ("Initial RSS target\n " , user = self .user )
74+ Change .objects .all ().delete ()
75+ self .change_unit ("Current user RSS target\n " , user = self .user )
76+ self .change_unit ("Another user RSS target\n " , user = self .anotheruser )
77+
78+ def add_same_unit_rss_changes (self ) -> None :
79+ Change .objects .all ().delete ()
80+ self .change_unit ("Initial RSS target\n " , user = self .user )
81+ Change .objects .all ().delete ()
82+ self .change_unit ("First RSS target\n " , user = self .user )
83+ self .change_unit ("Second RSS target\n " , user = self .user )
84+
85+ def assert_per_change_rss_guids (self , url : str ) -> None :
86+ self .add_same_unit_rss_changes ()
87+ response = self .client .get (url )
88+ self .assert_rss_response (response )
89+ items = parse_xml (response .content ).findall ("./channel/item" )
90+ self .assertGreaterEqual (len (items ), 2 )
91+ links = [item .findtext ("link" ) for item in items [:2 ]]
92+ guids = [item .find ("guid" ) for item in items [:2 ]]
93+ self .assertEqual (len (set (links )), 1 )
94+ self .assertEqual (len ({guid .text for guid in guids if guid is not None }), 2 )
95+ for guid in guids :
96+ self .assertIsNotNone (guid )
97+ self .assertEqual (guid .attrib ["isPermaLink" ], "false" )
98+ self .assertIn ("/changes/render/" , guid .text )
99+
64100 def test_basic (self ) -> None :
65101 response = self .client .get (reverse ("changes" ))
66102 self .assertContains (response , "Resource update" )
67103
104+ def test_basic_rss (self ) -> None :
105+ response = self .client .get (reverse ("changes-rss" ))
106+ self .assert_rss_response (response )
107+ self .assertContains (response , "Resource update" )
108+
109+ def test_changes_rss_guids_identify_changes (self ) -> None :
110+ self .assert_per_change_rss_guids (reverse ("changes-rss" ))
111+
112+ def test_export_rss_guids_identify_changes (self ) -> None :
113+ self .assert_per_change_rss_guids (reverse ("rss" ))
114+
68115 def test_basic_csv_denied (self ) -> None :
69116 response = self .client .get (reverse ("changes-csv" ))
70117 self .assertEqual (response .status_code , 403 )
@@ -113,6 +160,13 @@ def test_user(self) -> None:
113160 )
114161 self .assertNotContains (response , f'title="{ self .user .full_name } "' )
115162
163+ def test_user_rss (self ) -> None :
164+ self .add_filtered_rss_changes ()
165+ response = self .client .get (reverse ("changes-rss" ), {"user" : self .user .username })
166+ self .assert_rss_response (response )
167+ self .assertContains (response , "testuser" )
168+ self .assertNotContains (response , "Jane Doe" )
169+
116170 def test_exclude_user (self ) -> None :
117171 self .edit_unit ("Hello, world!\n " , "Nazdar svete!\n " )
118172 response = self .client .get (reverse ("changes" ))
@@ -130,13 +184,76 @@ def test_exclude_user(self) -> None:
130184 )
131185 self .assertContains (response , f'title="{ self .user .full_name } "' )
132186
187+ def test_exclude_user_rss (self ) -> None :
188+ self .add_filtered_rss_changes ()
189+ response = self .client .get (
190+ reverse ("changes-rss" ), {"exclude_user" : self .user .username }
191+ )
192+ self .assert_rss_response (response )
193+ self .assertNotContains (response , "Weblate Test" )
194+ self .assertContains (response , "Jane Doe" )
195+
196+ def test_action_rss (self ) -> None :
197+ Change .objects .all ().delete ()
198+ self .change_unit ("Initial RSS target\n " , user = self .user )
199+ Change .objects .all ().delete ()
200+ self .change_unit ("Current user RSS target\n " , user = self .user )
201+ response = self .client .get (
202+ reverse ("changes-rss" ), {"action" : ActionEvents .CHANGE }
203+ )
204+ self .assert_rss_response (response )
205+ self .assertContains (response , "Translation changed" )
206+ response = self .client .get (
207+ reverse ("changes-rss" ), {"action" : ActionEvents .UPDATE }
208+ )
209+ self .assert_rss_response (response )
210+ self .assertNotContains (response , "Translation changed" )
211+
133212 def test_daterange (self ) -> None :
134213 end = timezone .now ()
135214 start = end - timedelta (days = 1 )
136215 period = f"{ start .strftime ('%m/%d/%Y' )} - { end .strftime ('%m/%d/%Y' )} "
137216 response = self .client .get (reverse ("changes" ), {"period" : period })
138217 self .assertContains (response , "Resource update" )
139218
219+ def test_daterange_rss (self ) -> None :
220+ Change .objects .all ().delete ()
221+ self .change_unit ("Initial RSS target\n " , user = self .user )
222+ Change .objects .all ().delete ()
223+ self .change_unit ("Current user RSS target\n " , user = self .user )
224+ end = timezone .now ()
225+ start = end - timedelta (days = 1 )
226+ period = f"{ start .strftime ('%m/%d/%Y' )} - { end .strftime ('%m/%d/%Y' )} "
227+ response = self .client .get (reverse ("changes-rss" ), {"period" : period })
228+ self .assert_rss_response (response )
229+ self .assertContains (response , "Translation changed" )
230+ response = self .client .get (
231+ reverse ("changes-rss" ), {"period" : "01/01/2020 - 01/02/2020" }
232+ )
233+ self .assert_rss_response (response )
234+ self .assertNotContains (response , "Translation changed" )
235+
236+ def test_scoped_rss (self ) -> None :
237+ Change .objects .all ().delete ()
238+ self .change_unit ("Initial RSS target\n " , user = self .user )
239+ Change .objects .all ().delete ()
240+ unit = self .change_unit ("Scoped RSS target\n " , user = self .user )
241+ paths = (
242+ self .project .get_url_path (),
243+ self .component .get_url_path (),
244+ self .translation .get_url_path (),
245+ ["-" , "-" , self .translation .language .code ],
246+ [self .project .slug , "-" , self .translation .language .code ],
247+ unit .get_url_path (),
248+ )
249+ for path in paths :
250+ with self .subTest (path = path ):
251+ response = self .client .get (
252+ reverse ("changes-rss" , kwargs = {"path" : path })
253+ )
254+ self .assert_rss_response (response )
255+ self .assertContains (response , "Translation changed" )
256+
140257 def test_pagination (self ) -> None :
141258 end = timezone .now ()
142259 start = end - timedelta (days = 1 )
@@ -149,6 +266,11 @@ def test_pagination(self) -> None:
149266 )
150267 self .assertContains (response , "String added in the upload" )
151268
269+ def test_rss_link_keeps_query_string (self ) -> None :
270+ response = self .client .get (reverse ("changes" ), {"user" : self .user .username })
271+ query_string = urlencode ({"user" : self .user .username })
272+ self .assertContains (response , f"{ reverse ('changes-rss' )} ?{ query_string } " )
273+
152274 def test_last_changes_display (self ) -> None :
153275 unit_to_delete = self .get_unit ("Orangutan has %d banana" )
154276 unit_to_delete .context = "Orangutan unit context"
0 commit comments