diff --git a/src/parse.rs b/src/parse.rs
index 71f6cd8d..a9953bef 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -470,6 +470,7 @@ pub enum Tag {
Load,
SimpleTag(SimpleTag),
Url(Url),
+ CsrfToken,
}
#[derive(PartialEq, Eq)]
@@ -1046,6 +1047,7 @@ impl<'t, 'l, 'py> Parser<'t, 'l, 'py> {
};
Ok(match self.template.content(tag.at) {
"url" => Either::Left(self.parse_url(at, parts)?),
+ "csrf_token" => Either::Left(TokenTree::Tag(Tag::CsrfToken)),
"load" => Either::Left(self.parse_load(at, parts)?),
"autoescape" => Either::Left(self.parse_autoescape(at, parts)?),
"endautoescape" => Either::Right(EndTag {
diff --git a/src/render/tags.rs b/src/render/tags.rs
index ed3cda52..ffc60402 100644
--- a/src/render/tags.rs
+++ b/src/render/tags.rs
@@ -654,6 +654,39 @@ impl Render for Tag {
Self::Load => Cow::Borrowed(""),
Self::SimpleTag(simple_tag) => simple_tag.render(py, template, context)?,
Self::Url(url) => url.render(py, template, context)?,
+ Self::CsrfToken => match context.get("csrf_token") {
+ Some(token) => {
+ let bound_token = token.bind(py);
+ if bound_token.is_truthy().unwrap_or(false) {
+ if bound_token.eq("NOTPROVIDED")? {
+ Cow::Borrowed("")
+ } else {
+ Cow::Owned(format!(
+ r#""#,
+ html_escape::encode_quoted_attribute(bound_token.str()?.to_str()?)
+ ))
+ }
+ } else {
+ Cow::Borrowed("")
+ }
+ }
+ None => {
+ let debug = py
+ .import("django.conf")?
+ .getattr("settings")?
+ .getattr("DEBUG")?
+ .is_truthy()?;
+
+ if debug {
+ py.import("warnings")?.call_method1(
+ "warn",
+ ("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.",)
+ )?;
+ }
+
+ Cow::Borrowed("")
+ }
+ },
})
}
}
diff --git a/tests/tags/test_csrf_token.py b/tests/tags/test_csrf_token.py
new file mode 100644
index 00000000..e9af7fec
--- /dev/null
+++ b/tests/tags/test_csrf_token.py
@@ -0,0 +1,133 @@
+import warnings
+from django.template import engines
+from django.test import override_settings
+
+
+def test_csrf_token_basic():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": "test123"}
+ expected = ''
+
+ assert django_template.render(context) == expected
+ assert rust_template.render(context) == expected
+
+
+def test_csrf_token_not_provided():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": "NOTPROVIDED"}
+
+ assert django_template.render(context) == ""
+ assert rust_template.render(context) == ""
+
+
+def test_csrf_token_missing():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ assert django_template.render({}) == ""
+ assert rust_template.render({}) == ""
+
+
+def test_csrf_token_escaping():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": 'test"with&'}
+
+ django_result = django_template.render(context)
+ rust_result = rust_template.render(context)
+
+ assert django_result == rust_result
+ assert 'value="test"with<quotes>&"' in rust_result
+
+
+def test_csrf_token_empty_string():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": ""}
+
+ assert django_template.render(context) == ""
+ assert rust_template.render(context) == ""
+
+
+def test_csrf_token_none_value():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": None}
+
+ assert django_template.render(context) == ""
+ assert rust_template.render(context) == ""
+
+
+def test_csrf_token_numeric_value():
+ template = "{% csrf_token %}"
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": 12345}
+ expected = ''
+
+ assert django_template.render(context) == expected
+ assert rust_template.render(context) == expected
+
+
+def test_csrf_token_zero_value():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": 0}
+
+ assert django_template.render(context) == ""
+ assert rust_template.render(context) == ""
+
+
+def test_csrf_token_false_value():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ context = {"csrf_token": False}
+
+ assert django_template.render(context) == ""
+ assert rust_template.render(context) == ""
+
+
+@override_settings(DEBUG=True)
+def test_csrf_token_missing_debug_warning():
+ template = "{% csrf_token %}"
+
+ django_template = engines["django"].from_string(template)
+ rust_template = engines["rusty"].from_string(template)
+
+ expected = "A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext."
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ assert django_template.render({}) == ""
+ assert str(w[0].message) == expected
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ assert rust_template.render({}) == ""
+ assert str(w[0].message) == expected