Skip to content

Implement upper filter #81

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

Merged
merged 7 commits into from
May 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub enum FilterType {
Lower(LowerFilter),
Safe(SafeFilter),
Slugify(SlugifyFilter),
Upper(UpperFilter),
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -81,3 +82,6 @@ pub struct SafeFilter;

#[derive(Clone, Debug, PartialEq)]
pub struct SlugifyFilter;

#[derive(Clone, Debug, PartialEq)]
pub struct UpperFilter;
5 changes: 5 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::filters::FilterType;
use crate::filters::LowerFilter;
use crate::filters::SafeFilter;
use crate::filters::SlugifyFilter;
use crate::filters::UpperFilter;
use crate::lex::START_TAG_LEN;
use crate::lex::autoescape::{AutoescapeEnabled, AutoescapeError, lex_autoescape_argument};
use crate::lex::common::LexerError;
Expand Down Expand Up @@ -123,6 +124,10 @@ impl Filter {
Some(right) => return Err(unexpected_argument("slugify", right)),
None => FilterType::Slugify(SlugifyFilter),
},
"upper" => match right {
Some(right) => return Err(unexpected_argument("upper", right)),
None => FilterType::Upper(UpperFilter),
},
external => {
let external = match parser.external_filters.get(external) {
Some(external) => external.clone().unbind(),
Expand Down
74 changes: 72 additions & 2 deletions src/render/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use pyo3::types::PyType;

use crate::filters::{
AddFilter, AddSlashesFilter, CapfirstFilter, DefaultFilter, EscapeFilter, ExternalFilter,
FilterType, LowerFilter, SafeFilter, SlugifyFilter,
FilterType, LowerFilter, SafeFilter, SlugifyFilter, UpperFilter,
};
use crate::parse::Filter;
use crate::render::types::{Content, Context};
Expand Down Expand Up @@ -72,6 +72,7 @@ impl Resolve for Filter {
FilterType::Lower(filter) => filter.resolve(left, py, template, context),
FilterType::Safe(filter) => filter.resolve(left, py, template, context),
FilterType::Slugify(filter) => filter.resolve(left, py, template, context),
FilterType::Upper(filter) => filter.resolve(left, py, template, context),
};
result
}
Expand Down Expand Up @@ -322,10 +323,30 @@ impl ResolveFilter for SlugifyFilter {
}
}

impl ResolveFilter for UpperFilter {
fn resolve<'t, 'py>(
&self,
variable: Option<Content<'t, 'py>>,
_py: Python<'py>,
_template: TemplateString<'t>,
context: &mut Context,
) -> ResolveResult<'t, 'py> {
let content = match variable {
Some(content) => Some(
content
.resolve_string(context)?
.map_content(|content| Cow::Owned(content.to_uppercase())),
Copy link
Owner

Choose a reason for hiding this comment

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

upper is a bit unusual because it's not considered HTML-safe upstream in Django.

To handle this correctly here, we'll need to replace .map_content with something like:

Suggested change
.map_content(|content| Cow::Owned(content.to_uppercase())),
.map(|content| content.content().to_uppercase())
.map(|content| Cow::Owned(Content::String(content))),

We'll also need a test to demonstrate we follow Django's behaviour here. (I'd add it in the Python tests.) This test should include a html tag which will become html-escaped as well as uppercased in the expected output.

For more information about this quirk, see my blog post or the ticket and Django PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This Django case is really interesting.

I've been trying to use upper filter for HTML text in Django, and I'm not sure about the proposed solution. Django uses uppercase to encode text before encoding HTML characters. In this case, the resolve_string() method will encode HTML characters; I always have to call resolve_string() before calling to_uppercase() in the code. So the encoded HTML characters will be capitalized, which Django doesn't do.

I added test cases, especially the last one which I feel is not solvable without change resolve_string for allowing to return a HtmlUnsafe ?

Copy link
Owner

Choose a reason for hiding this comment

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

That's a very good point - I think this is essentially a bug in the existing Rust implementation that's been exposed by your work here.

I've had a play around with your idea of introducing a HtmlUnsafe variant to the Content and ContentString enums and I've got the tests passing. 🚀

),
None => "".as_content(),
};
Ok(content)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::filters::{AddSlashesFilter, DefaultFilter, LowerFilter};
use crate::filters::{AddSlashesFilter, DefaultFilter, LowerFilter, UpperFilter};
use crate::parse::TagElement;
use crate::render::Render;
use crate::template::django_rusty_templates::{EngineData, Template};
Expand Down Expand Up @@ -800,4 +821,53 @@ mod tests {
assert_eq!(rendered, "bryony");
})
}

#[test]
fn test_render_filter_upper() {
pyo3::prepare_freethreaded_python();

Python::with_gil(|py| {
let name = PyString::new(py, "Foo").into_any();
let context = HashMap::from([("name".to_string(), name.unbind())]);
let mut context = Context {
context,
request: None,
autoescape: false,
};
let template = TemplateString("{{ name|upper }}");
let variable = Variable::new((3, 4));
let filter = Filter {
at: (8, 5),
left: TagElement::Variable(variable),
filter: FilterType::Upper(UpperFilter),
};

let rendered = filter.render(py, template, &mut context).unwrap();
assert_eq!(rendered, "FOO");
})
}

#[test]
fn test_render_filter_upper_missing_left() {
pyo3::prepare_freethreaded_python();

Python::with_gil(|py| {
let context = HashMap::new();
let mut context = Context {
context,
request: None,
autoescape: false,
};
let template = TemplateString("{{ name|upper }}");
let variable = Variable::new((3, 4));
let filter = Filter {
at: (8, 5),
left: TagElement::Variable(variable),
filter: FilterType::Upper(UpperFilter),
};

let rendered = filter.render(py, template, &mut context).unwrap();
assert_eq!(rendered, "");
})
}
}
30 changes: 30 additions & 0 deletions tests/filters/test_upper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.template import engines


def test_upper_string():
template = "{{ var|upper }}"
django_template = engines["django"].from_string(template)
rust_template = engines["rusty"].from_string(template)

var = "foo"
uppered = "FOO"
assert django_template.render({"var": var}) == uppered
assert rust_template.render({"var": var}) == uppered

def test_upper_undefined():
template = "{{ var|upper }}"
django_template = engines["django"].from_string(template)
rust_template = engines["rusty"].from_string(template)

assert django_template.render() == ""
assert rust_template.render() == ""

def test_upper_integer():
template = "{{ var|upper }}"
django_template = engines["django"].from_string(template)
rust_template = engines["rusty"].from_string(template)

var = "3"
uppered = "3"
assert django_template.render({"var": var}) == uppered
assert rust_template.render({"var": var}) == uppered