-
Notifications
You must be signed in to change notification settings - Fork 11
Feat/yesno filter #78
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
base: main
Are you sure you want to change the base?
Changes from all commits
2f05307
ed6d8a9
54547ca
d9bb4c3
d9975c4
d2c6d91
b27c58b
91c0cb0
caa2a80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -8,14 +8,16 @@ use pyo3::types::PyType; | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
use crate::filters::{ | ||||||||||||||||||||||||||||||
AddFilter, AddSlashesFilter, CapfirstFilter, DefaultFilter, EscapeFilter, ExternalFilter, | ||||||||||||||||||||||||||||||
FilterType, LowerFilter, SafeFilter, SlugifyFilter, | ||||||||||||||||||||||||||||||
FilterType, LowerFilter, SafeFilter, SlugifyFilter, YesnoFilter, | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
use crate::parse::Filter; | ||||||||||||||||||||||||||||||
use crate::render::types::{Content, Context}; | ||||||||||||||||||||||||||||||
use crate::render::{Resolve, ResolveResult}; | ||||||||||||||||||||||||||||||
use crate::types::TemplateString; | ||||||||||||||||||||||||||||||
use regex::Regex; | ||||||||||||||||||||||||||||||
use unicode_normalization::UnicodeNormalization; | ||||||||||||||||||||||||||||||
use crate::error::RenderError; | ||||||||||||||||||||||||||||||
use crate::render::PyRenderError; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Used for replacing all non-word and non-spaces with an empty string | ||||||||||||||||||||||||||||||
static NON_WORD_RE: LazyLock<Regex> = | ||||||||||||||||||||||||||||||
|
@@ -71,6 +73,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::Yesno(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
result | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
@@ -319,6 +322,75 @@ impl ResolveFilter for SlugifyFilter { | |||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
impl ResolveFilter for YesnoFilter { | ||||||||||||||||||||||||||||||
fn resolve<'t, 'py>( | ||||||||||||||||||||||||||||||
&self, | ||||||||||||||||||||||||||||||
variable: Option<Content<'t, 'py>>, | ||||||||||||||||||||||||||||||
py: Python<'py>, | ||||||||||||||||||||||||||||||
template: TemplateString<'t>, | ||||||||||||||||||||||||||||||
context: &mut Context, | ||||||||||||||||||||||||||||||
) -> ResolveResult<'t, 'py> { | ||||||||||||||||||||||||||||||
let left = match variable { | ||||||||||||||||||||||||||||||
Some(var) => var, | ||||||||||||||||||||||||||||||
None => return Err(PyRenderError::RenderError(RenderError::VariableDoesNotExist { | ||||||||||||||||||||||||||||||
key: "yesno".to_string(), | ||||||||||||||||||||||||||||||
object: "filter".to_string(), | ||||||||||||||||||||||||||||||
key_at: (0, 0).into(), | ||||||||||||||||||||||||||||||
object_at: None, | ||||||||||||||||||||||||||||||
})), | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Handle the case with a custom mapping argument | ||||||||||||||||||||||||||||||
if let Some(arg) = &self.argument { | ||||||||||||||||||||||||||||||
// Get mapping string from the argument | ||||||||||||||||||||||||||||||
let arg_content = arg.resolve(py, template, context)? | ||||||||||||||||||||||||||||||
.expect("missing argument in context should already have raised"); | ||||||||||||||||||||||||||||||
let mapping = match arg_content.to_py(py)?.extract::<String>() { | ||||||||||||||||||||||||||||||
Ok(s) => s, | ||||||||||||||||||||||||||||||
Err(_) => { | ||||||||||||||||||||||||||||||
return Err(PyRenderError::RenderError(RenderError::VariableDoesNotExist { | ||||||||||||||||||||||||||||||
key: "yesno argument".to_string(), | ||||||||||||||||||||||||||||||
object: "string".to_string(), | ||||||||||||||||||||||||||||||
key_at: (0, 0).into(), | ||||||||||||||||||||||||||||||
object_at: None, | ||||||||||||||||||||||||||||||
})); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
Comment on lines
+348
to
+358
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we shouldn't need to call Something like:
Suggested change
Otherwise, we should probably |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
let parts: Vec<&str> = mapping.split(',').collect(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Handle None values | ||||||||||||||||||||||||||||||
if left.to_py(py)?.is_none() { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should add a new method to |
||||||||||||||||||||||||||||||
// Return "maybe" if provided, otherwise fallback to "no" | ||||||||||||||||||||||||||||||
let result = if parts.len() >= 3 { | ||||||||||||||||||||||||||||||
parts[2].to_string() | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
parts.get(1).unwrap_or(&"no").to_string() | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
return Ok(Some(Content::String(Cow::Owned(result)))); | ||||||||||||||||||||||||||||||
Comment on lines
+365
to
+370
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be cleaner to match on the length of
Suggested change
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Handle True/False values | ||||||||||||||||||||||||||||||
match left.to_py(py)?.is_truthy() { | ||||||||||||||||||||||||||||||
Ok(true) => Ok(Some(Content::String(Cow::Owned(parts.get(0).unwrap_or(&"yes").to_string())))), | ||||||||||||||||||||||||||||||
Ok(false) => Ok(Some(Content::String(Cow::Owned(parts.get(1).unwrap_or(&"no").to_string())))), | ||||||||||||||||||||||||||||||
Err(e) => Err(PyRenderError::PyErr(e)), | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
// Default mapping without an argument | ||||||||||||||||||||||||||||||
if left.to_py(py)?.is_none() { | ||||||||||||||||||||||||||||||
return Ok(Some(Content::String(Cow::Borrowed("maybe")))); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
match left.to_py(py)?.is_truthy() { | ||||||||||||||||||||||||||||||
Ok(true) => Ok(Some(Content::String(Cow::Borrowed("yes")))), | ||||||||||||||||||||||||||||||
Ok(false) => Ok(Some(Content::String(Cow::Borrowed("no")))), | ||||||||||||||||||||||||||||||
Err(e) => Err(PyRenderError::PyErr(e)), | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
#[cfg(test)] | ||||||||||||||||||||||||||||||
mod tests { | ||||||||||||||||||||||||||||||
use super::*; | ||||||||||||||||||||||||||||||
|
@@ -797,4 +869,78 @@ mod tests { | |||||||||||||||||||||||||||||
assert_eq!(rendered, "bryony"); | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||||||
fn test_render_filter_yesno_default() { | ||||||||||||||||||||||||||||||
pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Python::with_gil(|py| { | ||||||||||||||||||||||||||||||
let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with true value | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", true).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "yes"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with false value | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", false).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "no"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with None value | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", py.None()).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "maybe"); | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||||||
fn test_render_filter_yesno_custom() { | ||||||||||||||||||||||||||||||
pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Python::with_gil(|py| { | ||||||||||||||||||||||||||||||
let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with true value | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno:'yeah,no,maybe' }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", true).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "yeah"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with false value | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno:'yeah,no,maybe' }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", false).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "no"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with None value | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno:'yeah,no,maybe' }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", py.None()).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "maybe"); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Test with None value and no "maybe" option | ||||||||||||||||||||||||||||||
let template_string = "{{ value|yesno:'yeah,no' }}".to_string(); | ||||||||||||||||||||||||||||||
let context = PyDict::new(py); | ||||||||||||||||||||||||||||||
context.set_item("value", py.None()).unwrap(); | ||||||||||||||||||||||||||||||
let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||
let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!(result, "no"); | ||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
from django.template import Template, Context | ||
from django.test import SimpleTestCase | ||
|
||
|
||
class YesNoTests(SimpleTestCase): | ||
def test_yesno_default(self): | ||
# Test default mapping (yes, no, maybe) | ||
template = Template("{{ var|yesno }}") | ||
|
||
# Test with True value | ||
rendered = template.render(Context({"var": True})) | ||
self.assertEqual(rendered, "yes") | ||
|
||
# Test with False value | ||
rendered = template.render(Context({"var": False})) | ||
self.assertEqual(rendered, "no") | ||
|
||
# Test with None value | ||
rendered = template.render(Context({"var": None})) | ||
self.assertEqual(rendered, "maybe") | ||
|
||
def test_yesno_custom(self): | ||
# Test custom mapping | ||
template = Template("{{ var|yesno:'yeah,nope,perhaps' }}") | ||
|
||
# Test with True value | ||
rendered = template.render(Context({"var": True})) | ||
self.assertEqual(rendered, "yeah") | ||
|
||
# Test with False value | ||
rendered = template.render(Context({"var": False})) | ||
self.assertEqual(rendered, "nope") | ||
|
||
# Test with None value | ||
rendered = template.render(Context({"var": None})) | ||
self.assertEqual(rendered, "perhaps") | ||
|
||
def test_yesno_two_options(self): | ||
# Test with only two options - None uses the second option | ||
template = Template("{{ var|yesno:'yep,nah' }}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have tests for three and for two mappings. We should also test that we match Django's behaviour for zero, one or more than three mappings. |
||
|
||
# Test with True value | ||
rendered = template.render(Context({"var": True})) | ||
self.assertEqual(rendered, "yep") | ||
|
||
# Test with False value | ||
rendered = template.render(Context({"var": False})) | ||
self.assertEqual(rendered, "nah") | ||
|
||
# Test with None value - should use second option when no third option | ||
rendered = template.render(Context({"var": None})) | ||
self.assertEqual(rendered, "nah") | ||
|
||
def test_yesno_empty_string(self): | ||
# Test with empty string (which is falsy) | ||
template = Template("{{ var|yesno }}") | ||
rendered = template.render(Context({"var": ""})) | ||
self.assertEqual(rendered, "no") | ||
|
||
def test_yesno_empty_list(self): | ||
# Test with empty list (which is falsy) | ||
template = Template("{{ var|yesno }}") | ||
rendered = template.render(Context({"var": []})) | ||
self.assertEqual(rendered, "no") | ||
|
||
def test_yesno_non_empty_list(self): | ||
# Test with non-empty list (which is truthy) | ||
template = Template("{{ var|yesno }}") | ||
rendered = template.render(Context({"var": ["item"]})) | ||
self.assertEqual(rendered, "yes") | ||
|
||
def test_yesno_non_empty_string(self): | ||
# Test with non-empty string (which is truthy) | ||
template = Template("{{ var|yesno }}") | ||
rendered = template.render(Context({"var": "value"})) | ||
self.assertEqual(rendered, "yes") | ||
|
||
def test_yesno_zero(self): | ||
# Test with zero (which is falsy) | ||
template = Template("{{ var|yesno }}") | ||
rendered = template.render(Context({"var": 0})) | ||
self.assertEqual(rendered, "no") | ||
|
||
def test_yesno_nonzero(self): | ||
# Test with non-zero number (which is truthy) | ||
template = Template("{{ var|yesno }}") | ||
rendered = template.render(Context({"var": 1})) | ||
self.assertEqual(rendered, "yes") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we test this code path?
You can find an example of the sort of way I'd test errors here:
https://github.com/LilyFoote/django-rusty-templates/blob/a7e3e9fc524ea65a5fc9c91eeb52ce7f6c815f07/tests/filters/test_add.py#L25-L45