rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode};
use rjango::http::{HttpResponse, request::HttpRequest};
use rjango::views::csrf::csrf_failure;
use rjango::views::generic::dates::{
    DateDetailView, DayArchiveView, MonthArchiveView, TodayArchiveView, WeekArchiveView,
    YearArchiveView,
};
use rjango::views::mixins::{
    ContextMixin, FormMixin, MultipleObjectMixin, SingleObjectMixin, TemplateResponseMixin,
};
use serde_json::json;

fn build_request(path: &str) -> HttpRequest {
    HttpRequest::from_axum(
        Method::GET,
        path.parse().expect("valid path"),
        HeaderMap::new(),
        Bytes::new(),
    )
}

struct EmptyContext;

impl ContextMixin for EmptyContext {}

struct TemplateExample;

impl TemplateResponseMixin for TemplateExample {
    fn get_template_names(&self) -> Vec<String> {
        vec!["articles/detail.html".to_string()]
    }
}

struct FormExample;

impl FormMixin for FormExample {
    fn get_form_class(&self) -> &str {
        "ArticleForm"
    }

    fn get_success_url(&self) -> &str {
        "/articles/"
    }

    fn form_valid(&self) -> HttpResponse {
        HttpResponse::with_status(StatusCode::CREATED, "created")
    }

    fn form_invalid(&self) -> HttpResponse {
        HttpResponse::with_status(StatusCode::BAD_REQUEST, "invalid")
    }
}

struct EmptySingleObject;

impl SingleObjectMixin for EmptySingleObject {}

struct EmptyMultipleObject;

impl MultipleObjectMixin for EmptyMultipleObject {}

#[test]
fn csrf_failure_returns_forbidden_response_with_reason_and_path() {
    let request = build_request("/forms/contact/");
    let response = csrf_failure(&request, "Missing token");

    assert_eq!(response.status_code, StatusCode::FORBIDDEN);
    let body = std::str::from_utf8(&response.content).expect("utf8 body");
    assert!(body.contains("Missing token"));
    assert!(body.contains("/forms/contact/"));
}

#[test]
fn context_mixin_default_context_is_empty() {
    let mixin = EmptyContext;

    assert!(mixin.get_context_data().is_empty());
}

#[test]
fn context_mixin_supports_trait_objects() {
    let mixin: &dyn ContextMixin = &EmptyContext;

    assert!(mixin.get_context_data().is_empty());
}

#[test]
fn template_response_mixin_returns_template_names() {
    let mixin = TemplateExample;

    assert_eq!(
        mixin.get_template_names(),
        vec!["articles/detail.html".to_string()]
    );
}

#[test]
fn template_response_mixin_defaults_content_type_to_html() {
    let mixin = TemplateExample;

    assert_eq!(mixin.get_content_type(), "text/html");
}

#[test]
fn template_response_mixin_supports_trait_objects() {
    let mixin: &dyn TemplateResponseMixin = &TemplateExample;

    assert_eq!(
        mixin.get_template_names(),
        vec!["articles/detail.html".to_string()]
    );
    assert_eq!(mixin.get_content_type(), "text/html");
}

#[test]
fn form_mixin_exposes_form_configuration() {
    let mixin = FormExample;

    assert_eq!(mixin.get_form_class(), "ArticleForm");
    assert_eq!(mixin.get_success_url(), "/articles/");
}

#[test]
fn form_mixin_supports_trait_objects() {
    let mixin: &dyn FormMixin = &FormExample;

    assert_eq!(mixin.get_form_class(), "ArticleForm");
    assert_eq!(mixin.get_success_url(), "/articles/");
    assert_eq!(mixin.form_valid().status_code, StatusCode::CREATED);
    assert_eq!(mixin.form_invalid().status_code, StatusCode::BAD_REQUEST);
}

#[test]
fn single_object_mixin_defaults_to_none_and_slug_field() {
    let mixin = EmptySingleObject;

    assert_eq!(mixin.get_object(), None);
    assert_eq!(mixin.get_slug_field(), "slug");
}

#[test]
fn multiple_object_mixin_defaults_to_empty_queryset_and_no_pagination() {
    let mixin = EmptyMultipleObject;

    assert!(mixin.get_queryset().is_empty());
    assert_eq!(mixin.get_paginate_by(), None);
}

#[test]
fn year_archive_view_new_sets_fields() {
    let view = YearArchiveView::new(2024);

    assert_eq!(view.year, 2024);
    assert_eq!(view.template_name, "");
}

#[test]
fn year_archive_view_builder_updates_fields() {
    let view = YearArchiveView::new(2024)
        .with_year(2025)
        .with_template_name("archive/year.html");

    assert_eq!(view.year, 2025);
    assert_eq!(view.template_name, "archive/year.html");
}

#[test]
fn month_archive_view_new_sets_fields() {
    let view = MonthArchiveView::new(2024, 3);

    assert_eq!(view.year, 2024);
    assert_eq!(view.month, 3);
    assert_eq!(view.template_name, "");
}

#[test]
fn week_archive_view_builder_updates_fields() {
    let view = WeekArchiveView::new(2024, 12)
        .with_year(2025)
        .with_week(13)
        .with_template_name("archive/week.html");

    assert_eq!(view.year, 2025);
    assert_eq!(view.week, 13);
    assert_eq!(view.template_name, "archive/week.html");
}

#[test]
fn day_archive_view_builder_updates_fields() {
    let view = DayArchiveView::new(2024, 3, 14)
        .with_year(2025)
        .with_month(4)
        .with_day(15)
        .with_template_name("archive/day.html");

    assert_eq!(view.year, 2025);
    assert_eq!(view.month, 4);
    assert_eq!(view.day, 15);
    assert_eq!(view.template_name, "archive/day.html");
}

#[test]
fn today_archive_view_new_starts_with_empty_template_name() {
    let view = TodayArchiveView::new();

    assert_eq!(view.template_name, "");
}

#[test]
fn today_archive_view_builder_updates_template_name() {
    let view = TodayArchiveView::new().with_template_name("archive/today.html");

    assert_eq!(view.template_name, "archive/today.html");
}

#[test]
fn date_detail_view_new_sets_fields() {
    let view = DateDetailView::new(2024, 3, 14, "42");

    assert_eq!(view.year, 2024);
    assert_eq!(view.month, 3);
    assert_eq!(view.day, 14);
    assert_eq!(view.pk, "42");
    assert_eq!(view.template_name, "");
}

#[test]
fn date_detail_view_builder_updates_fields() {
    let view = DateDetailView::new(2024, 3, 14, "42")
        .with_year(2025)
        .with_month(4)
        .with_day(15)
        .with_pk("84")
        .with_template_name("archive/detail.html");

    assert_eq!(view.year, 2025);
    assert_eq!(view.month, 4);
    assert_eq!(view.day, 15);
    assert_eq!(view.pk, "84");
    assert_eq!(view.template_name, "archive/detail.html");
}

#[test]
fn single_and_multiple_object_mixins_can_be_overridden() {
    struct CustomMixins;

    impl SingleObjectMixin for CustomMixins {
        fn get_object(&self) -> Option<serde_json::Value> {
            Some(json!({"id": 1}))
        }
    }

    impl MultipleObjectMixin for CustomMixins {
        fn get_queryset(&self) -> Vec<serde_json::Value> {
            vec![json!({"id": 1}), json!({"id": 2})]
        }

        fn get_paginate_by(&self) -> Option<usize> {
            Some(20)
        }
    }

    let mixin = CustomMixins;

    assert_eq!(mixin.get_object(), Some(json!({"id": 1})));
    assert_eq!(mixin.get_queryset().len(), 2);
    assert_eq!(mixin.get_paginate_by(), Some(20));
}