Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow empty string to clear URL-type DB fields. #4780

Merged
merged 6 commits into from
Jun 6, 2024

Conversation

dessalines
Copy link
Member

- To address difficulties with clearing URL-type fields like
  avatars, banners, site icons, this PR turns the URL type form
  fields into strings.
- This allows an empty string to be used as a "clear data", as
  in the case with the regular text form fields.
- Also includes various cleanups.
- Fixes #4777
- Context: #2287
max_length_check(body, BODY_MAX_LENGTH, LemmyErrorType::InvalidBodyField)?;
};
}
pub fn is_valid_body_field(body: &str, post: bool) -> LemmyResult<()> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a lot of if let Some(... in the API code, so I've removed a few of these internal if let wrappers, as they're pretty pointless. Makes sense to just work with the data directly in a lot of cases.

@@ -350,12 +340,11 @@ pub fn build_url_str_without_scheme(url_str: &str) -> LemmyResult<String> {
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing really important below, just removing the unwraps in favor of using LemmyResult in the tests.

&process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?,
);

let icon = diesel_url_update(&data.icon)?;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heart of it is here. We transform these url-type ones into proper diesel forms for an update to an optional url column (IE Option<Option<DbUrl>> ), before doing later actions to it.

@@ -198,4 +199,14 @@ test("Set a new avatar, old avatar is deleted", async () => {
// make sure only the new avatar is kept
const listMediaRes3 = await alphaImage.listMedia();
expect(listMediaRes3.images.length).toBe(1);

// Now try to save a user settings, with the icon missing,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test here to make sure that it isn't clearing the avatar, when that field is missing.

old_image: &Option<DbUrl>,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
if let (Some(new_image), Some(old_image)) = (new_image, old_image) {
if let (Some(Some(new_image)), Some(old_image)) = (new_image, old_image) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following should work:

Suggested change
if let (Some(Some(new_image)), Some(old_image)) = (new_image, old_image) {
if let Some((new_image, old_image)) = new_image.flatten().zip(old_image) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might work, but seems less clear to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. Personally, I find this a bit easier than needing to parse nested Somes.

@@ -1004,26 +1004,25 @@ pub(crate) async fn proxy_image_link(link: Url, context: &LemmyContext) -> Lemmy
}

pub async fn proxy_image_link_opt_api(
link: &Option<String>,
link: Option<Option<DbUrl>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything preventing us from using a single layer of Option here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ones I left as helper functions, because of the internal async call, and not being able to use async map. The one right below this uses the single option version.

@@ -340,15 +340,15 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Lemm

/// When adding a new avatar, banner or similar image, delete the old one.
pub async fn replace_image(
new_image: &Option<String>,
new_image: &Option<Option<DbUrl>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as here.

context: &LemmyContext,
) -> LemmyResult<Option<Option<DbUrl>>> {
proxy_image_link_api(link, context).await.map(Some)
if let Some(Some(link)) = link {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can call flatten on link if you're stuck using 2 levels of Option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but at least for this one, it would've forced me to use a clone and deref with it, in this case its cleaner to just do Some(Some .

check_slurs(name, &slur_regex)?;
}

if let Some(Some(body)) = &body {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can call flatten on body.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but in all these cases it forces me to do a .clone(), in order to not move the value of body, and .flatten() doesn't work on refs of options for some reason.

Comment on lines +72 to +83
if let Some(Some(alt_text)) = &alt_text {
is_valid_alt_text_field(alt_text)?;
}

if let Some(Some(url)) = &url {
is_url_blocked(url, &url_blocklist)?;
check_url_scheme(url)?;
}

if let Some(Some(custom_thumbnail)) = &custom_thumbnail {
check_url_scheme(custom_thumbnail)?;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flatten

pub fn diesel_option_overwrite(opt: Option<String>) -> Option<Option<String>> {
match opt {
/// Takes an API text input, and converts it to an optional diesel DB update.
pub fn diesel_string_update(opt: &Option<String>) -> Option<Option<String>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be taking an Option<&String>, maybe even Option<&str>. See here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that at first, but it forces you to add a lot of deref calls. This one uses the least amount of cloning and mutating.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does calling as_ref on the param at the site of the function call work for you? It should let you turn, e.g., an &Option<String> to Option<&String>. Docs for reference.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably won't make too big a difference, but I've now updated it. Does add a lot of deref calls unfortunately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should at least prevent us from needing to clone and pass owned objects around as much, which I think is a win.

@SleeplessOne1917
Copy link
Member

I know I've asked the question before, but what is with all of the Option<Option<T>>s? I'm not sure what that type expresses that just Option<T> doesn't already express. I'm inclined to even call it a code smell.

@dullbananas
Copy link
Collaborator

If I understand correctly, None keeps the existing value, and Some(None) removes the existing value

match opt.as_ref().map(String::as_str) {
/// Takes an optional API URL-type input, and converts it to an optional diesel DB update.
/// Also cleans the url params.
pub fn diesel_url_update(opt: &Option<String>) -> LemmyResult<Option<Option<DbUrl>>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here.

@SleeplessOne1917
Copy link
Member

If I understand correctly, None keeps the existing value, and Some(None) removes the existing value

I suppose that makes sense. Is that how diesel usually handles this sort of thing, or is that idiosyncratic to our codebase?

@dessalines
Copy link
Member Author

If I understand correctly, None keeps the existing value, and Some(None) removes the existing value

That's correct. I made a table for this somewhere but I can't find it, for update type (create or update), column type (nullable vs not null), and default type (has default or no default value). But the main outlier case, is that if its a nullable DB column update, then you need to use Option<Option< on update changesets, so that Some(None can be a set null.

@dessalines dessalines merged commit 16a8286 into main Jun 6, 2024
2 checks passed
@SleeplessOne1917 SleeplessOne1917 deleted the dont_db_delete_empty_url_fields branch June 6, 2024 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: Site logo disappearing
3 participants