* Fix a few form options for diesel. Fixes #2287 * Adding TODO comment.
This commit is contained in:
parent
42eac1560f
commit
8bfeb4b627
13 changed files with 56 additions and 56 deletions
|
@ -42,7 +42,7 @@ impl Perform for HideCommunity {
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
name: read_community.name,
|
name: read_community.name,
|
||||||
title: read_community.title,
|
title: read_community.title,
|
||||||
description: read_community.description.to_owned(),
|
description: Some(read_community.description.to_owned()),
|
||||||
hidden: Some(data.hidden),
|
hidden: Some(data.hidden),
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
..CommunityForm::default()
|
..CommunityForm::default()
|
||||||
|
|
|
@ -26,7 +26,7 @@ use lemmy_db_schema::{
|
||||||
site::Site,
|
site::Site,
|
||||||
},
|
},
|
||||||
traits::{Crud, Followable, Joinable},
|
traits::{Crud, Followable, Joinable},
|
||||||
utils::diesel_option_overwrite_to_url,
|
utils::{diesel_option_overwrite, diesel_option_overwrite_to_url},
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::structs::CommunityView;
|
use lemmy_db_views_actor::structs::CommunityView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
|
@ -60,6 +60,7 @@ impl PerformCrud for CreateCommunity {
|
||||||
// Check to make sure the icon and banners are urls
|
// Check to make sure the icon and banners are urls
|
||||||
let icon = diesel_option_overwrite_to_url(&data.icon)?;
|
let icon = diesel_option_overwrite_to_url(&data.icon)?;
|
||||||
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
||||||
|
let description = diesel_option_overwrite(&data.description);
|
||||||
|
|
||||||
check_slurs(&data.name, &context.settings().slur_regex())?;
|
check_slurs(&data.name, &context.settings().slur_regex())?;
|
||||||
check_slurs(&data.title, &context.settings().slur_regex())?;
|
check_slurs(&data.title, &context.settings().slur_regex())?;
|
||||||
|
@ -87,7 +88,7 @@ impl PerformCrud for CreateCommunity {
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
title: data.title.to_owned(),
|
title: data.title.to_owned(),
|
||||||
description: data.description.to_owned(),
|
description,
|
||||||
icon,
|
icon,
|
||||||
banner,
|
banner,
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use lemmy_db_schema::{
|
||||||
newtypes::PersonId,
|
newtypes::PersonId,
|
||||||
source::community::{Community, CommunityForm},
|
source::community::{Community, CommunityForm},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
utils::{diesel_option_overwrite_to_url, naive_now},
|
utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||||
use lemmy_utils::{error::LemmyError, utils::check_slurs_opt, ConnectionId};
|
use lemmy_utils::{error::LemmyError, utils::check_slurs_opt, ConnectionId};
|
||||||
|
@ -31,6 +31,7 @@ impl PerformCrud for EditCommunity {
|
||||||
|
|
||||||
let icon = diesel_option_overwrite_to_url(&data.icon)?;
|
let icon = diesel_option_overwrite_to_url(&data.icon)?;
|
||||||
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
||||||
|
let description = diesel_option_overwrite(&data.description);
|
||||||
|
|
||||||
check_slurs_opt(&data.title, &context.settings().slur_regex())?;
|
check_slurs_opt(&data.title, &context.settings().slur_regex())?;
|
||||||
check_slurs_opt(&data.description, &context.settings().slur_regex())?;
|
check_slurs_opt(&data.description, &context.settings().slur_regex())?;
|
||||||
|
@ -55,7 +56,7 @@ impl PerformCrud for EditCommunity {
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
name: read_community.name,
|
name: read_community.name,
|
||||||
title: data.title.to_owned().unwrap_or(read_community.title),
|
title: data.title.to_owned().unwrap_or(read_community.title),
|
||||||
description: data.description.to_owned(),
|
description,
|
||||||
icon,
|
icon,
|
||||||
banner,
|
banner,
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
|
|
|
@ -24,17 +24,12 @@ use lemmy_db_schema::{
|
||||||
post::{Post, PostForm, PostLike, PostLikeForm},
|
post::{Post, PostForm, PostLike, PostLikeForm},
|
||||||
},
|
},
|
||||||
traits::{Crud, Likeable},
|
traits::{Crud, Likeable},
|
||||||
|
utils::diesel_option_overwrite,
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::structs::CommunityView;
|
use lemmy_db_views_actor::structs::CommunityView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::LemmyError,
|
error::LemmyError,
|
||||||
utils::{
|
utils::{check_slurs, check_slurs_opt, clean_url_params, is_valid_post_title},
|
||||||
check_slurs,
|
|
||||||
check_slurs_opt,
|
|
||||||
clean_optional_text,
|
|
||||||
clean_url_params,
|
|
||||||
is_valid_post_title,
|
|
||||||
},
|
|
||||||
ConnectionId,
|
ConnectionId,
|
||||||
};
|
};
|
||||||
use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
|
use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
|
||||||
|
@ -61,6 +56,10 @@ impl PerformCrud for CreatePost {
|
||||||
check_slurs_opt(&data.body, slur_regex)?;
|
check_slurs_opt(&data.body, slur_regex)?;
|
||||||
honeypot_check(&data.honeypot)?;
|
honeypot_check(&data.honeypot)?;
|
||||||
|
|
||||||
|
let data_url = data.url.as_ref();
|
||||||
|
let url = Some(data_url.map(clean_url_params).map(Into::into)); // TODO no good way to handle a "clear"
|
||||||
|
let body = diesel_option_overwrite(&data.body);
|
||||||
|
|
||||||
if !is_valid_post_title(&data.name) {
|
if !is_valid_post_title(&data.name) {
|
||||||
return Err(LemmyError::from_message("invalid_post_title"));
|
return Err(LemmyError::from_message("invalid_post_title"));
|
||||||
}
|
}
|
||||||
|
@ -85,24 +84,23 @@ impl PerformCrud for CreatePost {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch post links and pictrs cached image
|
// Fetch post links and pictrs cached image
|
||||||
let data_url = data.url.as_ref();
|
|
||||||
let (metadata_res, thumbnail_url) =
|
let (metadata_res, thumbnail_url) =
|
||||||
fetch_site_data(context.client(), context.settings(), data_url).await;
|
fetch_site_data(context.client(), context.settings(), data_url).await;
|
||||||
let (embed_title, embed_description, embed_video_url) = metadata_res
|
let (embed_title, embed_description, embed_video_url) = metadata_res
|
||||||
.map(|u| (u.title, u.description, u.embed_video_url))
|
.map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
name: data.name.trim().to_owned(),
|
name: data.name.trim().to_owned(),
|
||||||
url: data_url.map(|u| clean_url_params(u.to_owned()).into()),
|
url,
|
||||||
body: clean_optional_text(&data.body),
|
body,
|
||||||
community_id: data.community_id,
|
community_id: data.community_id,
|
||||||
creator_id: local_user_view.person.id,
|
creator_id: local_user_view.person.id,
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
embed_title,
|
embed_title,
|
||||||
embed_description,
|
embed_description,
|
||||||
embed_video_url,
|
embed_video_url,
|
||||||
thumbnail_url,
|
thumbnail_url: Some(thumbnail_url),
|
||||||
..PostForm::default()
|
..PostForm::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,11 @@ use lemmy_apub::protocol::activities::{
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::post::{Post, PostForm},
|
source::post::{Post, PostForm},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
utils::naive_now,
|
utils::{diesel_option_overwrite, naive_now},
|
||||||
};
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::LemmyError,
|
error::LemmyError,
|
||||||
utils::{check_slurs_opt, clean_optional_text, clean_url_params, is_valid_post_title},
|
utils::{check_slurs_opt, clean_url_params, is_valid_post_title},
|
||||||
ConnectionId,
|
ConnectionId,
|
||||||
};
|
};
|
||||||
use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
|
use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
|
||||||
|
@ -41,6 +41,13 @@ impl PerformCrud for EditPost {
|
||||||
let local_user_view =
|
let local_user_view =
|
||||||
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
|
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
|
||||||
|
|
||||||
|
let data_url = data.url.as_ref();
|
||||||
|
|
||||||
|
// TODO No good way to handle a clear.
|
||||||
|
// Issue link: https://github.com/LemmyNet/lemmy/issues/2287
|
||||||
|
let url = Some(data_url.map(clean_url_params).map(Into::into));
|
||||||
|
let body = diesel_option_overwrite(&data.body);
|
||||||
|
|
||||||
let slur_regex = &context.settings().slur_regex();
|
let slur_regex = &context.settings().slur_regex();
|
||||||
check_slurs_opt(&data.name, slur_regex)?;
|
check_slurs_opt(&data.name, slur_regex)?;
|
||||||
check_slurs_opt(&data.body, slur_regex)?;
|
check_slurs_opt(&data.body, slur_regex)?;
|
||||||
|
@ -72,21 +79,21 @@ impl PerformCrud for EditPost {
|
||||||
let (metadata_res, thumbnail_url) =
|
let (metadata_res, thumbnail_url) =
|
||||||
fetch_site_data(context.client(), context.settings(), data_url).await;
|
fetch_site_data(context.client(), context.settings(), data_url).await;
|
||||||
let (embed_title, embed_description, embed_video_url) = metadata_res
|
let (embed_title, embed_description, embed_video_url) = metadata_res
|
||||||
.map(|u| (u.title, u.description, u.embed_video_url))
|
.map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
creator_id: orig_post.creator_id.to_owned(),
|
creator_id: orig_post.creator_id.to_owned(),
|
||||||
community_id: orig_post.community_id,
|
community_id: orig_post.community_id,
|
||||||
name: data.name.to_owned().unwrap_or(orig_post.name),
|
name: data.name.to_owned().unwrap_or(orig_post.name),
|
||||||
url: data_url.map(|u| clean_url_params(u.to_owned()).into()),
|
url,
|
||||||
body: clean_optional_text(&data.body),
|
body,
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
embed_title,
|
embed_title,
|
||||||
embed_description,
|
embed_description,
|
||||||
embed_video_url,
|
embed_video_url,
|
||||||
thumbnail_url,
|
thumbnail_url: Some(thumbnail_url),
|
||||||
..PostForm::default()
|
..PostForm::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ impl PerformCrud for EditSite {
|
||||||
let sidebar = diesel_option_overwrite(&data.sidebar);
|
let sidebar = diesel_option_overwrite(&data.sidebar);
|
||||||
let description = diesel_option_overwrite(&data.description);
|
let description = diesel_option_overwrite(&data.description);
|
||||||
let application_question = diesel_option_overwrite(&data.application_question);
|
let application_question = diesel_option_overwrite(&data.application_question);
|
||||||
|
let legal_information = diesel_option_overwrite(&data.legal_information);
|
||||||
let icon = diesel_option_overwrite_to_url(&data.icon)?;
|
let icon = diesel_option_overwrite_to_url(&data.icon)?;
|
||||||
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@ impl PerformCrud for EditSite {
|
||||||
private_instance: data.private_instance,
|
private_instance: data.private_instance,
|
||||||
default_theme: data.default_theme.clone(),
|
default_theme: data.default_theme.clone(),
|
||||||
default_post_listing_type: data.default_post_listing_type.clone(),
|
default_post_listing_type: data.default_post_listing_type.clone(),
|
||||||
legal_information: data.legal_information.clone(),
|
legal_information,
|
||||||
..SiteForm::default()
|
..SiteForm::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -173,15 +173,15 @@ impl ApubObject for ApubPost {
|
||||||
(None, page.image.map(|i| i.url.into()))
|
(None, page.image.map(|i| i.url.into()))
|
||||||
};
|
};
|
||||||
let (embed_title, embed_description, embed_video_url) = metadata_res
|
let (embed_title, embed_description, embed_video_url) = metadata_res
|
||||||
.map(|u| (u.title, u.description, u.embed_video_url))
|
.map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let body_slurs_removed =
|
let body_slurs_removed =
|
||||||
read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
|
read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
|
||||||
.map(|s| remove_slurs(&s, &context.settings().slur_regex()));
|
.map(|s| Some(remove_slurs(&s, &context.settings().slur_regex())));
|
||||||
|
|
||||||
PostForm {
|
PostForm {
|
||||||
name: page.name.clone(),
|
name: page.name.clone(),
|
||||||
url: url.map(Into::into),
|
url: Some(url.map(Into::into)),
|
||||||
body: body_slurs_removed,
|
body: body_slurs_removed,
|
||||||
creator_id: creator.id,
|
creator_id: creator.id,
|
||||||
community_id: community.id,
|
community_id: community.id,
|
||||||
|
@ -195,7 +195,7 @@ impl ApubObject for ApubPost {
|
||||||
embed_title,
|
embed_title,
|
||||||
embed_description,
|
embed_description,
|
||||||
embed_video_url,
|
embed_video_url,
|
||||||
thumbnail_url,
|
thumbnail_url: Some(thumbnail_url),
|
||||||
ap_id: Some(page.id.clone().into()),
|
ap_id: Some(page.id.clone().into()),
|
||||||
local: Some(false),
|
local: Some(false),
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,11 @@ impl Group {
|
||||||
CommunityForm {
|
CommunityForm {
|
||||||
name: self.preferred_username.clone(),
|
name: self.preferred_username.clone(),
|
||||||
title: self.name.unwrap_or(self.preferred_username),
|
title: self.name.unwrap_or(self.preferred_username),
|
||||||
description: read_from_string_or_source_opt(&self.summary, &None, &self.source),
|
description: Some(read_from_string_or_source_opt(
|
||||||
|
&self.summary,
|
||||||
|
&None,
|
||||||
|
&self.source,
|
||||||
|
)),
|
||||||
removed: None,
|
removed: None,
|
||||||
published: self.published.map(|u| u.naive_local()),
|
published: self.published.map(|u| u.naive_local()),
|
||||||
updated: self.updated.map(|u| u.naive_local()),
|
updated: self.updated.map(|u| u.naive_local()),
|
||||||
|
|
|
@ -59,7 +59,7 @@ pub struct CommunitySafe {
|
||||||
pub struct CommunityForm {
|
pub struct CommunityForm {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<Option<String>>,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub published: Option<chrono::NaiveDateTime>,
|
pub published: Option<chrono::NaiveDateTime>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
|
|
@ -37,18 +37,18 @@ pub struct PostForm {
|
||||||
pub creator_id: PersonId,
|
pub creator_id: PersonId,
|
||||||
pub community_id: CommunityId,
|
pub community_id: CommunityId,
|
||||||
pub nsfw: Option<bool>,
|
pub nsfw: Option<bool>,
|
||||||
pub url: Option<DbUrl>,
|
pub url: Option<Option<DbUrl>>,
|
||||||
pub body: Option<String>,
|
pub body: Option<Option<String>>,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub locked: Option<bool>,
|
pub locked: Option<bool>,
|
||||||
pub published: Option<chrono::NaiveDateTime>,
|
pub published: Option<chrono::NaiveDateTime>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: Option<bool>,
|
pub deleted: Option<bool>,
|
||||||
pub stickied: Option<bool>,
|
pub stickied: Option<bool>,
|
||||||
pub embed_title: Option<String>,
|
pub embed_title: Option<Option<String>>,
|
||||||
pub embed_description: Option<String>,
|
pub embed_description: Option<Option<String>>,
|
||||||
pub embed_video_url: Option<DbUrl>,
|
pub embed_video_url: Option<Option<DbUrl>>,
|
||||||
pub thumbnail_url: Option<DbUrl>,
|
pub thumbnail_url: Option<Option<DbUrl>>,
|
||||||
pub ap_id: Option<DbUrl>,
|
pub ap_id: Option<DbUrl>,
|
||||||
pub local: Option<bool>,
|
pub local: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,5 +60,5 @@ pub struct SiteForm {
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
pub default_theme: Option<String>,
|
pub default_theme: Option<String>,
|
||||||
pub default_post_listing_type: Option<String>,
|
pub default_post_listing_type: Option<String>,
|
||||||
pub legal_information: Option<String>,
|
pub legal_information: Option<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,29 +165,17 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> IpAddr {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clean_url_params(mut url: Url) -> Url {
|
pub fn clean_url_params(url: &Url) -> Url {
|
||||||
|
let mut url_out = url.to_owned();
|
||||||
if url.query().is_some() {
|
if url.query().is_some() {
|
||||||
let new_query = url
|
let new_query = url
|
||||||
.query_pairs()
|
.query_pairs()
|
||||||
.filter(|q| !CLEAN_URL_PARAMS_REGEX.is_match(&q.0))
|
.filter(|q| !CLEAN_URL_PARAMS_REGEX.is_match(&q.0))
|
||||||
.map(|q| format!("{}={}", q.0, q.1))
|
.map(|q| format!("{}={}", q.0, q.1))
|
||||||
.join("&");
|
.join("&");
|
||||||
url.set_query(Some(&new_query));
|
url_out.set_query(Some(&new_query));
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clean_optional_text(text: &Option<String>) -> Option<String> {
|
|
||||||
if let Some(text) = text {
|
|
||||||
let trimmed = text.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(trimmed.to_owned())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
url_out
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -198,12 +186,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clean_url_params() {
|
fn test_clean_url_params() {
|
||||||
let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&username=randomuser&id=123").unwrap();
|
let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&username=randomuser&id=123").unwrap();
|
||||||
let cleaned = clean_url_params(url);
|
let cleaned = clean_url_params(&url);
|
||||||
let expected = Url::parse("https://example.com/path/123?username=randomuser&id=123").unwrap();
|
let expected = Url::parse("https://example.com/path/123?username=randomuser&id=123").unwrap();
|
||||||
assert_eq!(expected.to_string(), cleaned.to_string());
|
assert_eq!(expected.to_string(), cleaned.to_string());
|
||||||
|
|
||||||
let url = Url::parse("https://example.com/path/123").unwrap();
|
let url = Url::parse("https://example.com/path/123").unwrap();
|
||||||
let cleaned = clean_url_params(url.clone());
|
let cleaned = clean_url_params(&url);
|
||||||
assert_eq!(url.to_string(), cleaned.to_string());
|
assert_eq!(url.to_string(), cleaned.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ fn community_updates_2020_04_02(
|
||||||
let form = CommunityForm {
|
let form = CommunityForm {
|
||||||
name: ccommunity.name.to_owned(),
|
name: ccommunity.name.to_owned(),
|
||||||
title: ccommunity.title.to_owned(),
|
title: ccommunity.title.to_owned(),
|
||||||
description: ccommunity.description.to_owned(),
|
description: Some(ccommunity.description.to_owned()),
|
||||||
hidden: Some(false),
|
hidden: Some(false),
|
||||||
actor_id: Some(community_actor_id.to_owned()),
|
actor_id: Some(community_actor_id.to_owned()),
|
||||||
local: Some(ccommunity.local),
|
local: Some(ccommunity.local),
|
||||||
|
|
Loading…
Reference in a new issue