]> Untitled Git - lemmy.git/blob - crates/db_schema/src/impls/actor_language.rs
implement language tags for site/community in db and api (#2434)
[lemmy.git] / crates / db_schema / src / impls / actor_language.rs
1 use crate::{
2   diesel::JoinOnDsl,
3   newtypes::{CommunityId, LanguageId, LocalUserId, SiteId},
4   source::{actor_language::*, language::Language},
5 };
6 use diesel::{
7   delete,
8   dsl::*,
9   insert_into,
10   result::Error,
11   select,
12   ExpressionMethods,
13   PgConnection,
14   QueryDsl,
15   RunQueryDsl,
16 };
17 use lemmy_utils::error::LemmyError;
18
19 impl LocalUserLanguage {
20   pub fn read(
21     conn: &mut PgConnection,
22     for_local_user_id: LocalUserId,
23   ) -> Result<Vec<LanguageId>, Error> {
24     use crate::schema::local_user_language::dsl::*;
25
26     local_user_language
27       .filter(local_user_id.eq(for_local_user_id))
28       .select(language_id)
29       .get_results(conn)
30   }
31
32   /// Update the user's languages.
33   ///
34   /// If no language_id vector is given, it will show all languages
35   pub fn update(
36     conn: &mut PgConnection,
37     language_ids: Vec<LanguageId>,
38     for_local_user_id: LocalUserId,
39   ) -> Result<(), Error> {
40     conn.build_transaction().read_write().run(|conn| {
41       use crate::schema::local_user_language::dsl::*;
42       // Clear the current user languages
43       delete(local_user_language.filter(local_user_id.eq(for_local_user_id))).execute(conn)?;
44
45       let lang_ids = update_languages(conn, language_ids)?;
46       for l in lang_ids {
47         let form = LocalUserLanguageForm {
48           local_user_id: for_local_user_id,
49           language_id: l,
50         };
51         insert_into(local_user_language)
52           .values(form)
53           .get_result::<Self>(conn)?;
54       }
55       Ok(())
56     })
57   }
58 }
59
60 impl SiteLanguage {
61   pub fn read_local(conn: &mut PgConnection) -> Result<Vec<LanguageId>, Error> {
62     use crate::schema::{site, site_language::dsl::*};
63     // TODO: remove this subquery once site.local column is added
64     let subquery = crate::schema::site::dsl::site
65       .order_by(site::id)
66       .select(site::id)
67       .limit(1)
68       .into_boxed();
69     site_language
70       .filter(site_id.eq_any(subquery))
71       .select(language_id)
72       .load(conn)
73   }
74
75   pub fn read(conn: &mut PgConnection, for_site_id: SiteId) -> Result<Vec<LanguageId>, Error> {
76     use crate::schema::site_language::dsl::*;
77     site_language
78       .filter(site_id.eq(for_site_id))
79       .select(language_id)
80       .load(conn)
81   }
82
83   pub fn update(
84     conn: &mut PgConnection,
85     language_ids: Vec<LanguageId>,
86     for_site_id: SiteId,
87   ) -> Result<(), Error> {
88     conn.build_transaction().read_write().run(|conn| {
89       use crate::schema::site_language::dsl::*;
90       // Clear the current languages
91       delete(site_language.filter(site_id.eq(for_site_id))).execute(conn)?;
92
93       let lang_ids = update_languages(conn, language_ids)?;
94       for l in lang_ids {
95         let form = SiteLanguageForm {
96           site_id: for_site_id,
97           language_id: l,
98         };
99         insert_into(site_language)
100           .values(form)
101           .get_result::<Self>(conn)?;
102       }
103
104       CommunityLanguage::limit_languages(conn)?;
105
106       Ok(())
107     })
108   }
109 }
110
111 impl CommunityLanguage {
112   /// Returns true if the given language is one of configured languages for given community
113   pub fn is_allowed_community_language(
114     conn: &mut PgConnection,
115     for_language_id: Option<LanguageId>,
116     for_community_id: CommunityId,
117   ) -> Result<(), LemmyError> {
118     use crate::schema::community_language::dsl::*;
119     if let Some(for_language_id) = for_language_id {
120       let is_allowed = select(exists(
121         community_language
122           .filter(language_id.eq(for_language_id))
123           .filter(community_id.eq(for_community_id)),
124       ))
125       .get_result(conn)?;
126
127       if is_allowed {
128         Ok(())
129       } else {
130         Err(LemmyError::from_message("language_not_allowed"))
131       }
132     } else {
133       Ok(())
134     }
135   }
136
137   /// When site languages are updated, delete all languages of local communities which are not
138   /// also part of site languages. This is because post/comment language is only checked against
139   /// community language, and it shouldnt be possible to post content in languages which are not
140   /// allowed by local site.
141   fn limit_languages(conn: &mut PgConnection) -> Result<(), Error> {
142     use crate::schema::{
143       community::dsl as c,
144       community_language::dsl as cl,
145       site_language::dsl as sl,
146     };
147     let community_languages: Vec<LanguageId> = cl::community_language
148       .left_outer_join(sl::site_language.on(cl::language_id.eq(sl::language_id)))
149       .inner_join(c::community)
150       .filter(c::local)
151       .filter(sl::language_id.is_null())
152       .select(cl::language_id)
153       .get_results(conn)?;
154
155     for c in community_languages {
156       delete(cl::community_language.filter(cl::language_id.eq(c))).execute(conn)?;
157     }
158     Ok(())
159   }
160
161   pub fn read(
162     conn: &mut PgConnection,
163     for_community_id: CommunityId,
164   ) -> Result<Vec<LanguageId>, Error> {
165     use crate::schema::community_language::dsl::*;
166     community_language
167       .filter(community_id.eq(for_community_id))
168       .select(language_id)
169       .get_results(conn)
170   }
171
172   pub fn update(
173     conn: &mut PgConnection,
174     mut language_ids: Vec<LanguageId>,
175     for_community_id: CommunityId,
176   ) -> Result<(), Error> {
177     conn.build_transaction().read_write().run(|conn| {
178       use crate::schema::community_language::dsl::*;
179       // Clear the current languages
180       delete(community_language.filter(community_id.eq(for_community_id))).execute(conn)?;
181
182       if language_ids.is_empty() {
183         language_ids = SiteLanguage::read_local(conn)?;
184       }
185       for l in language_ids {
186         let form = CommunityLanguageForm {
187           community_id: for_community_id,
188           language_id: l,
189         };
190         insert_into(community_language)
191           .values(form)
192           .get_result::<Self>(conn)?;
193       }
194       Ok(())
195     })
196   }
197 }
198
199 pub fn default_post_language(
200   conn: &mut PgConnection,
201   community_id: CommunityId,
202   local_user_id: LocalUserId,
203 ) -> Result<Option<LanguageId>, Error> {
204   use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul};
205   let intersection = ul::local_user_language
206     .inner_join(cl::community_language.on(ul::language_id.eq(cl::language_id)))
207     .filter(ul::local_user_id.eq(local_user_id))
208     .filter(cl::community_id.eq(community_id))
209     .select(cl::language_id)
210     .get_results::<LanguageId>(conn)?;
211
212   if intersection.len() == 1 {
213     Ok(Some(intersection[0]))
214   } else {
215     Ok(None)
216   }
217 }
218
219 // If no language is given, set all languages
220 fn update_languages(
221   conn: &mut PgConnection,
222   language_ids: Vec<LanguageId>,
223 ) -> Result<Vec<LanguageId>, Error> {
224   if language_ids.is_empty() {
225     Ok(
226       Language::read_all(conn)?
227         .into_iter()
228         .map(|l| l.id)
229         .collect(),
230     )
231   } else {
232     Ok(language_ids)
233   }
234 }
235
236 #[cfg(test)]
237 mod tests {
238   use crate::{
239     impls::actor_language::*,
240     source::{
241       community::{Community, CommunityForm},
242       local_user::{LocalUser, LocalUserForm},
243       person::{Person, PersonForm},
244       site::{Site, SiteForm},
245     },
246     traits::Crud,
247     utils::establish_unpooled_connection,
248   };
249   use serial_test::serial;
250
251   fn test_langs1(conn: &mut PgConnection) -> Vec<LanguageId> {
252     vec![
253       Language::read_id_from_code(conn, "en").unwrap(),
254       Language::read_id_from_code(conn, "fr").unwrap(),
255       Language::read_id_from_code(conn, "ru").unwrap(),
256     ]
257   }
258   fn test_langs2(conn: &mut PgConnection) -> Vec<LanguageId> {
259     vec![
260       Language::read_id_from_code(conn, "fi").unwrap(),
261       Language::read_id_from_code(conn, "se").unwrap(),
262     ]
263   }
264
265   fn create_test_site(conn: &mut PgConnection) -> Site {
266     let site_form = SiteForm {
267       name: "test site".to_string(),
268       ..Default::default()
269     };
270     Site::create(conn, &site_form).unwrap()
271   }
272
273   #[test]
274   #[serial]
275   fn test_update_languages() {
276     let conn = &mut establish_unpooled_connection();
277
278     // call with empty vec, returns all languages
279     let updated1 = update_languages(conn, vec![]).unwrap();
280     assert_eq!(184, updated1.len());
281
282     // call with nonempty vec, returns same vec
283     let test_langs = test_langs1(conn);
284     let updated2 = update_languages(conn, test_langs.clone()).unwrap();
285     assert_eq!(test_langs, updated2);
286   }
287
288   #[test]
289   #[serial]
290   fn test_site_languages() {
291     let conn = &mut establish_unpooled_connection();
292
293     let site = create_test_site(conn);
294     let site_languages1 = SiteLanguage::read_local(conn).unwrap();
295     // site is created with all languages
296     assert_eq!(184, site_languages1.len());
297
298     let test_langs = test_langs1(conn);
299     SiteLanguage::update(conn, test_langs.clone(), site.id).unwrap();
300
301     let site_languages2 = SiteLanguage::read_local(conn).unwrap();
302     // after update, site only has new languages
303     assert_eq!(test_langs, site_languages2);
304
305     Site::delete(conn, site.id).unwrap();
306   }
307
308   #[test]
309   #[serial]
310   fn test_user_languages() {
311     let conn = &mut establish_unpooled_connection();
312
313     let site = create_test_site(conn);
314     let test_langs = test_langs1(conn);
315     SiteLanguage::update(conn, test_langs.clone(), site.id).unwrap();
316
317     let person_form = PersonForm {
318       name: "my test person".to_string(),
319       public_key: Some("pubkey".to_string()),
320       ..Default::default()
321     };
322     let person = Person::create(conn, &person_form).unwrap();
323     let local_user_form = LocalUserForm {
324       person_id: Some(person.id),
325       password_encrypted: Some("my_pw".to_string()),
326       ..Default::default()
327     };
328     let local_user = LocalUser::create(conn, &local_user_form).unwrap();
329     let local_user_langs1 = LocalUserLanguage::read(conn, local_user.id).unwrap();
330
331     // new user should be initialized with site languages
332     assert_eq!(test_langs, local_user_langs1);
333
334     // update user languages
335     let test_langs2 = test_langs2(conn);
336     LocalUserLanguage::update(conn, test_langs2, local_user.id).unwrap();
337     let local_user_langs2 = LocalUserLanguage::read(conn, local_user.id).unwrap();
338     assert_eq!(2, local_user_langs2.len());
339
340     Person::delete(conn, person.id).unwrap();
341     LocalUser::delete(conn, local_user.id).unwrap();
342     Site::delete(conn, site.id).unwrap();
343   }
344
345   #[test]
346   #[serial]
347   fn test_community_languages() {
348     let conn = &mut establish_unpooled_connection();
349     let site = create_test_site(conn);
350     let test_langs = test_langs1(conn);
351     SiteLanguage::update(conn, test_langs.clone(), site.id).unwrap();
352
353     let community_form = CommunityForm {
354       name: "test community".to_string(),
355       title: "test community".to_string(),
356       public_key: Some("pubkey".to_string()),
357       ..Default::default()
358     };
359     let community = Community::create(conn, &community_form).unwrap();
360     let community_langs1 = CommunityLanguage::read(conn, community.id).unwrap();
361     // community is initialized with site languages
362     assert_eq!(test_langs, community_langs1);
363
364     let allowed_lang1 =
365       CommunityLanguage::is_allowed_community_language(conn, Some(test_langs[0]), community.id);
366     assert!(allowed_lang1.is_ok());
367
368     let test_langs2 = test_langs2(conn);
369     let allowed_lang2 =
370       CommunityLanguage::is_allowed_community_language(conn, Some(test_langs2[0]), community.id);
371     assert!(allowed_lang2.is_err());
372
373     // limit site languages to en, fi. after this, community languages should be updated to
374     // intersection of old languages (en, fr, ru) and (en, fi), which is only fi.
375     SiteLanguage::update(conn, vec![test_langs[0], test_langs2[0]], site.id).unwrap();
376     let community_langs2 = CommunityLanguage::read(conn, community.id).unwrap();
377     assert_eq!(vec![test_langs[0]], community_langs2);
378
379     // update community languages to different ones
380     CommunityLanguage::update(conn, test_langs2.clone(), community.id).unwrap();
381     let community_langs3 = CommunityLanguage::read(conn, community.id).unwrap();
382     assert_eq!(test_langs2, community_langs3);
383
384     Site::delete(conn, site.id).unwrap();
385     Community::delete(conn, community.id).unwrap();
386   }
387
388   #[test]
389   #[serial]
390   fn test_default_post_language() {
391     let conn = &mut establish_unpooled_connection();
392     let test_langs = test_langs1(conn);
393     let test_langs2 = test_langs2(conn);
394
395     let community_form = CommunityForm {
396       name: "test community".to_string(),
397       title: "test community".to_string(),
398       public_key: Some("pubkey".to_string()),
399       ..Default::default()
400     };
401     let community = Community::create(conn, &community_form).unwrap();
402     CommunityLanguage::update(conn, test_langs, community.id).unwrap();
403
404     let person_form = PersonForm {
405       name: "my test person".to_string(),
406       public_key: Some("pubkey".to_string()),
407       ..Default::default()
408     };
409     let person = Person::create(conn, &person_form).unwrap();
410     let local_user_form = LocalUserForm {
411       person_id: Some(person.id),
412       password_encrypted: Some("my_pw".to_string()),
413       ..Default::default()
414     };
415     let local_user = LocalUser::create(conn, &local_user_form).unwrap();
416     LocalUserLanguage::update(conn, test_langs2, local_user.id).unwrap();
417
418     // no overlap in user/community languages, so no default language for post
419     let def1 = default_post_language(conn, community.id, local_user.id).unwrap();
420     assert_eq!(None, def1);
421
422     let ru = Language::read_id_from_code(conn, "ru").unwrap();
423     let test_langs3 = vec![
424       ru,
425       Language::read_id_from_code(conn, "fi").unwrap(),
426       Language::read_id_from_code(conn, "se").unwrap(),
427     ];
428     LocalUserLanguage::update(conn, test_langs3, local_user.id).unwrap();
429
430     // this time, both have ru as common lang
431     let def2 = default_post_language(conn, community.id, local_user.id).unwrap();
432     assert_eq!(Some(ru), def2);
433
434     Person::delete(conn, person.id).unwrap();
435     Community::delete(conn, community.id).unwrap();
436     LocalUser::delete(conn, local_user.id).unwrap();
437   }
438 }