--- /dev/null
+# Copyright: (c) 2025, Jörn Menne <jmenne@posteo.de>
+# GNU General Public License v3.0 (see LICSENE or https://www.gnu.org/license/gpl-3.0.md)
+
+from django.forms import ModelForm
+from .models import Report
+
+
+class ReportForm(ModelForm):
+ """
+ Small class to map a dictionary to the report model.
+ Thic class extends the django-ModelForm
+ """
+
+ class Meta:
+ model = Report
+ fields = ["title", "description", "email", "category"]
--- /dev/null
+# Generated by Django 5.1.5 on 2025-02-08 08:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('georeport', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='report',
+ old_name='descrption',
+ new_name='description',
+ ),
+ migrations.AddField(
+ model_name='report',
+ name='latitude',
+ field=models.DecimalField(decimal_places=6, default=0, max_digits=8),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='report',
+ name='longitude',
+ field=models.DecimalField(decimal_places=6, default=0, max_digits=9),
+ preserve_default=False,
+ ),
+ ]
from django.contrib.auth.models import Group, User
from typing import override
+from django.forms import DecimalField
+
# Create your models here.
Category, on_delete=models.RESTRICT, related_name="reports"
)
title = models.CharField(max_length=100, unique=True)
- descrption = models.TextField(blank=True, null=True)
+ description = models.TextField(blank=True, null=True)
email = models.EmailField()
# TODO: Images
"""
published = models.BooleanField(default=False) # type: ignore Correct type can not be dtermined
+ # Location
+ # NOTE: Latitude is between -90 and 90°, while Longitude is between -180 and 180°
+ # Therefore the latitude field is slightly smaller
+ longitude = models.DecimalField(max_digits=9, decimal_places=6)
+ latitude = models.DecimalField(max_digits=8, decimal_places=6)
+
@override
def __str__(self) -> str:
return str(self.title)
function getsubcats(element) {
- // Set the current level to know which selections have to be removed (if any)
- const id = element.id;
- var level;
- if (id == "category")
- level = 0;
- else
- level = id;
- console.log(level);
+ // Set the current level to know which selections have to be removed (if any)
+ const id = element.id;
+ var level;
+ if (id == "category")
+ level = 0;
+ else
+ level = id;
+ console.log(level);
- // Get surrunding elements
- const form = document.getElementById("form");
- const submit = document.getElementById("submit");
+ // Get surrunding elements
+ const form = document.getElementById("form");
+ const submit = document.getElementById("submit");
- //create a url to fetch the children
- let url = "category/${element.value}/children";
- console.log(url);
- fetch(url)
- // Check if the response is correct
- .then(response => {
- if (!response.ok)
- throw new Error("HTTP Error: Status: ${response.status}");
- return response.json();
- })
- //Handle the json-Data
- .then(data => {
- console.log(data);
- // NOTE: Level has to be increased here, since if it would be later increased, it would be handled as a string
- // while removing the higher levels
+ //create a url to fetch the children
+ let url = "category/${element.value}/children";
+ console.log(url);
+ fetch(url)
+ // Check if the response is correct
+ .then(response => {
+ if (!response.ok)
+ throw new Error("HTTP Error: Status: ${response.status}");
+ return response.json();
+ })
+ //Handle the json-Data
+ .then(data => {
+ console.log(data);
+ // NOTE: Level has to be increased here, since if it would be later increased, it would be handled as a string
+ // while removing the higher levels
- g
- level++;
- let subcats = data["subcategories"];
- //Remove submit temporarly to set the correct position
- form.removeChild(submit);
+ g
+ level++;
+ let subcats = data["categories"];
+ //Remove submit temporarly to set the correct position
+ form.removeChild(submit);
- //Remove all selects with a higher level than element
- if (maxlevel >= level) {
- for (let i = level; i <= maxlevel; i++) {
- oldselect = document.getElementById(i);
- oldselect.remove();
- }
- }
- let oldselect = document.getElementById(level);
+ //Remove all selects with a higher level than element
+ if (maxlevel >= level) {
+ for (let i = level; i <= maxlevel; i++) {
+ oldselect = document.getElementById(i);
+ oldselect.remove();
+ }
+ }
+ let oldselect = document.getElementById(level);
- if (subcats.length == 0) {
- // set element as the lowest level
- element.name = "category";
- maxlevel = level - 1;
- }
- else {
+ if (subcats.length == 0) {
+ // set element as the lowest level
+ element.name = "category";
+ maxlevel = level - 1;
+ }
+ else {
- //Create a new selection element as lowest level
- let select = document.createElement("select");
- select.id = level;
- select.name = select.id;
- select.value = "";
- select.innerHTML = "Choose a subcategory";
- select.onchange = function () {
- getsubcats(this);
- }
- select.name = "category";
+ //Create a new selection element as lowest level
+ let select = document.createElement("select");
+ select.id = level;
+ select.name = select.id;
+ select.value = "";
+ select.innerHTML = "Choose a subcategory";
+ select.onchange = function () {
+ getsubcats(this);
+ }
+ select.name = "category";
- // Create the new options
- var option = document.createElement("option");
- option.value = "";
- option.innerHTML = "Subcategory";
- option.disabled = true;
- option.selected = true;
- select.appendChild(option);
+ // Create the new options
+ var option = document.createElement("option");
+ option.value = "";
+ option.innerHTML = "Subcategory";
+ option.disabled = true;
+ option.selected = true;
+ select.appendChild(option);
- for (var cat of subcats) {
- option = document.createElement("option")
- option.value = cat.id;
- option.innerHTML = cat.name;
- select.appendChild(option);
- }
+ for (var cat of subcats) {
+ option = document.createElement("option")
+ option.value = cat.id;
+ option.innerHTML = cat.name;
+ select.appendChild(option);
+ }
- element.name = "root";
- form.replaceChild(select, oldselect);
- maxlevel = level;
- }
- //Reappend submit
- form.appendChild(submit);
- })
+ element.name = "root";
+ form.replaceChild(select, oldselect);
+ maxlevel = level;
+ }
+ //Reappend submit
+ form.appendChild(submit);
+ })
}
# GNU General Public License v3.0 (see LICSENE or https://www.gnu.org/license/gpl-3.0.md)
from django.test import TestCase
+from django.urls import reverse
from .models import Category, Report
title="Test",
email="test@test.de",
category=Category.objects.first(), # type:ignore Attribute object is unknown
+ latitude=0,
+ longitude=0,
)
def test_unpulished_as_default(self):
report = Report.objects.get(title="Test") # type:ignore Attribute object is unknown
self.assertEqual(report.published, False)
+
+
+# TODO: Test latlng
+
+
+# TODO: Test get_categories
+
+
+class CategoryViewTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ """
+ Creates a test dataset:
+ - Root categories (no parent)
+ - Nested categories (children of other categories)
+ """
+ cls.root1 = Category.objects.create(name="Root 1") # type: ignore Attribute object unknown
+ cls.root2 = Category.objects.create(name="Root 2") # type: ignore Attribute object unknown
+
+ cls.child1 = Category.objects.create(name="Child 1", parent=cls.root1) # type: ignore Attribute object unknown
+ cls.child2 = Category.objects.create(name="Child 2", parent=cls.root1) # type: ignore Attribute object unknown
+ cls.child3 = Category.objects.create(name="Child 3", parent=cls.root2) # type: ignore Attribute object unknown
+
+ cls.subchild1 = Category.objects.create(name="SubChild 1", parent=cls.child1) # type: ignore Attribute object unknown
+
+ def test_get_all_categories(self):
+ """
+ Test if all categories are within the response
+ """
+ response = self.client.get(reverse("georeport:category-list"))
+ self.assertEqual(response.status_code, 200) # type: ignore Attribute status_code unknown
+ data = response.json() # type: ignore Attribute json unknown
+ expected_ids = {
+ self.root1.id,
+ self.root2.id,
+ self.child1.id,
+ self.child2.id,
+ self.child3.id,
+ self.subchild1.id,
+ }
+ response_ids = {cat["id"] for cat in data["categories"]}
+ self.assertEqual(response_ids, expected_ids)
+
+ def test_get_children_of_valid_category(self):
+ """Test that only direct children of a given category are returned when accessing 'subcategories/<id>/'."""
+ response = self.client.get(
+ reverse("georeport:subcategories", args=[self.root1.id])
+ )
+ self.assertEqual(response.status_code, 200) # type: ignore Attribute status_code unknown
+
+ data = response.json() # type: ignore Attribute json unknown
+ expected_ids = {self.child1.id, self.child2.id}
+ response_ids = {category["id"] for category in data["categories"]}
+ self.assertSetEqual(response_ids, expected_ids)
+
+ def test_get_children_of_category_without_children(self):
+ """Test that requesting children of a category with no children returns an empty list."""
+ response = self.client.get(
+ reverse("georeport:subcategories", args=[self.child2.id])
+ )
+ self.assertEqual(response.status_code, 200) # type: ignore Attribute status_code unknown
+
+ data = response.json() # type: ignore Attribute json unknown
+ self.assertEqual(data["categories"], [])
+
+ def test_get_children_of_non_existent_category(self):
+ """Test that requesting children of a non-existent category returns an empty list."""
+ response = self.client.get(reverse("georeport:subcategories", args=[99999]))
+ self.assertEqual(response.status_code, 200) # type: ignore Attribute status_code unknown
+
+ data = response.json() # type: ignore Attribute json unknown
+ self.assertEqual(data["categories"], [])
+
+ def test_response_format(self):
+ """Test that response format is correct."""
+ response = self.client.get(reverse("georeport:category-list"))
+ self.assertEqual(response.status_code, 200) # type: ignore Attribute status_code unknown
+
+ data = response.json() # type: ignore Attribute json unknown
+ self.assertIn("categories", data)
+ if data["categories"]: # If categories exist, check their structure
+ category = data["categories"][0]
+ self.assertIn("id", category)
+ self.assertIn("name", category)
urlpatterns = [
path("", views.index, name="index"),
- # path("<int:id>", views.details, name="detail"),
- # path("create", views.create, name="create"),
path("category/<int:id>", views.category_detail_view, name="category"),
- path("services/<int:id>", views.category_detail_view, name="servcice"),
path("category/<int:id>/children", views.get_categories, name="subcategories"),
- path("services/", views.get_categories),
+ path("services/<int:id>", views.category_detail_view, name="servcice"),
+ path("services/", views.get_categories, name="category-list"),
+ # path("<int:id>", views.details, name="detail"),
+ # path("create", views.create, name="create"),
# path("<str:b64nonce>/<str:b64ct>", views.finish_link, name="finish"),
]
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404, render
-from django.views.decorators.http import require_GET, require_safe
+from django.views.decorators.http import require_GET, require_safe, require_http_methods
from .models import Category, Report
+from .forms import ReportForm
+
@require_safe
def index(request) -> HttpResponse:
def get_categories(request, id=None) -> JsonResponse:
"""
Creates a jsonResponse containing the available categories.
- If an id was given, only the subcategories of the category with the given id are returned.
+ If an id was given, only direct subcategories of the category with the given id are returned.
Arguments:
request: HttpRequest
else:
cats = Category.objects.filter(parent__id=id) # type: ignore Attribute object us unknown
data = [{"id": cat.id, "name": cat.name} for cat in cats]
- data = {"subcategories": data}
+ data = {"categories": data}
return JsonResponse(data)
# TODO: Category-Detail
+@require_safe
def category_detail_view(request, id) -> HttpResponse:
"""
Function to handle requests to see information about a single category identified by id
# TODO: Report-List
# TODO: Create-Report
+
+@require_http_methods(["GET", "POST"])
+def create_report_view(request):
+ if request.method == "POST":
+ post = request.POST
+
+ # create custom dictionary for Report-Form
+ report = {}
+ report["title"] = post["title"]
+ report["description"] = post["description"]
+ report["category"] = post["category"]
+ report["email"] = post["email"]
+
+ reportform = ReportForm(report)
+
+ # TODO: Location
+
+ return render(
+ request,
+ "georeport/create.html",
+ context={"categories": Category.objects.all()}, # type: ignore Attribute objects unknown
+ )
+
+
# TODO: Detail-View Report
# TODO: Finish Link