+# 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.contrib import admin
+from typing import override
-# Register your models here.
+from georeport.models import Category, Report
+
+
+# TODO: CategoryAdmin
+
+
+class CategoryInline(admin.TabularInline):
+ model = Category
+ extra = 0
+ can_delete = False
+ show_change_link = True
+
+ @override
+ def has_change_permission(self, request, obj=None):
+ return False
+
+
+@admin.register(Category)
+class CategoryAdmin(admin.ModelAdmin):
+ """
+ Class extending model-Admin to register the model Category on
+ the admin site, such that the model can be edited on there.
+ """
+
+ exclude = ["user", "groups"]
+ inlines = [CategoryInline]
+ search_fields = ["name"]
+ # TODO: Prevent circles while creating groups
+
+
+# TODO: ReportAdmin
+@admin.register(Report)
+class ReportAdmin(admin.ModelAdmin):
+ exclude = [
+ "_oldstate",
+ ]
--- /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)
+*/
+
+/*
+ * A simple script, which extracts the latitude and longitude value from http
+ * and adds a marker at the correspoing location on the map.
+*/
+
+var marker = L.marker()
+
+var lat = document.getElementById("p-lat").dataset.lat;
+var lng = documetn.getElementById("p-lng").dataset.lng;
+
+marker.setLatLng([lat, lng]).addTo(map);
* GNU General Public License v3.0 (see LICSENE or https://www.gnu.org/license/gpl-3.0.md)
*/
// Center the map-view on Paderborn and use openstreetmap as a mapservice
+
+// Define the map and set the tilelayer
var map = L.map("map").setView([51.7173, 8.753557], 15);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
--- /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)
+*/
+
+/*
+ * Function which adds a new category-selection field, if the current category has at least one subcategory.
+ * If this is not the case, and the former selection had a subcategory, the selection fields for
+ * subcategories are removed.
+*/
+
+// Global variable to check the depth of the selections
+var maxlevel = 0;
+
+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);
+
+ // 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
+
+ g
+ level++;
+ let subcats = data["subcategories"];
+ //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);
+
+
+ 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 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);
+ }
+
+ element.name = "root";
+ form.replaceChild(select, oldselect);
+ maxlevel = level;
+ }
+ //Reappend submit
+ form.appendChild(submit);
+ })
+}
--- /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)
+*/
+/*
+ * A small script, which extracts the coordinates given by leaflet
+ * and inserts them into the correct fields. It also works the other way.
+ */
+
+// Specify all needed elements
+var lat_element = document.getElementById("latitude");
+var lng_element = document.getElementById("longitude");
+let marker = L.marker();
+
+
+// Add change listener to the input-elements
+lat_element.addEventListener("change", () => {
+ marker.setLatLng([lat_element.value, lng_element.value])
+ .addTo(map);
+});
+lng_element.addEventListener("change", () => {
+ marker.setLatLng([lat_element.value, lng_element.value])
+ .addTo(map);
+});
+
+
+/*
+ * Read event-data if clicked on the map to get the geocoordinates.
+ * The values are then capped to 6 decimals to get a precision of ~10cm.
+ * Which is enoug for this usecase.
+ * The precirsion is accorcding to https://en.wikipedia.org/wiki/Decimal_degrees
+ */
+function onMapClick(e, decimal_precision = 6) {
+ marker.setLatLng(e.latlng).addTo(map);
+
+ lat_element.value = e.latlng.lat.toFixed(decimal_precision);
+ lng_element.value = e.latlng.lng.toFixed(decimal_precision);
+
+}
-#map {
- height: 500px;
+body {
+ background-color: whitesmoke;
+}
+
+h1 {
+ text-align: center;
+}
+
+#map{
+ height: 400px;
+ width: 75%;
+ border: 3px solid darkgray;
+ margin: auto;
+ border-radius: 25px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.5);
+ padding: 15px;
+
+}
+
+
+.container{
+ width: 75%;
+ margin: auto;
+}
+
+.content{
+ display:flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ justify-content: border-box;
+ align-items: center;
+}
+
+.list{
+ flex: 1;
+ border-radius: 25px;
+ margin: 10px;
+ padding: 15px;
+ background-color: white;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.5);
+}
+
+.detail{
+ flex:1;
+ max-width: 75%;
+ border-radius: 25px;
+ margin: auto;
+ margin-top: 15px;
+ padding: 15px;
+ background-color: white;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.5);
+}
+
+#newReport{
+ text-align:center
+ font-weight: bold;
+ background-color: snow;
}
-
<!--
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)
{% load static %}
<html>
<head>
- <title>Pinpoint-Report</title>
+ <title>{% block title %}Georeport{% endblock %}</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin="">
</script>
- <link rel="stylesheet" href="{% static 'georeport/style.css' %}"/>
+
+ <link rel="stylesheet" href="{% static 'georeport/style.css' %}">
+
</head>
+
<body>
- <h1>Pinpoint-Report</h1>
- <div id="map"></div>
- <script src="{% static 'georeport/mapsetup.js' %}"> </script>
+ <div class="container">
+ <h1>Pinpoint</h1>
+ <div id="map"></div>
+ <script src="{% static 'georeport/mapsetup.js' %}"></script>
+ {% block body %}
+ {% endblock %}
+ </div>
</body>
</html>
-
--- /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)
+-->
+{% extends "georeport/base.html" %}
+{% load static %}
+{% block title %}Detail {{ category.id }} {% endblock %}
+{% block body %}
+<div class="detail">
+ <h2>Category {{ category.id }}</h2>
+ <p>Name: {{ category.name }}</p>
+ {% if category.parent %}
+ <p>Supercategory:<a href={{category.parent.id }}>{{category.parent}}</a></p>
+ {%endif%}
+ {% if category.children.exists %}
+ <h3>Subcategories:</h3>
+ <ul>
+ {% for child in category.children.all %}
+ <li><a href={{ child.id }}>{{child.name}} </a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ <a href="{% url 'georeport:index' %}">Back</a>
+</div>
+{% endblock %}
--- /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)
+-->
+{% extends "georeport/base.html" %}
+{% load static %}
+{% block title %}New report {% endblock %}
+{% block body %}
+
+<!--
+<form method="post" enctype="multipart/form-data">
+ {% csrf_token %}
+ {{ reportForm }}
+ <input type="submit"/>
+</form>
+-->
+<script src="{% static 'georeport/recurse_selection.js' %}"></script>
+<div class="detail">
+<form method="post" id="form" enctype="multipart/form-data">
+ {% csrf_token %}
+ <label for="title">Title:</label>
+ <input type="text" id="title" name="title" required> </br>
+ <label for="description">Description:</label>
+ <input type="text" id="description" name="description"> </br>
+ <label for="latitude">Latitude:</label>
+ <input type="number" id="latitude" name="latitude" step=0.000001 required > </br>
+ <label for="longitude">Longitude:</label>
+ <input type="number" id="longitude" name="longitude" step=0.000001 required > </br>
+ <label for="email">Email:</label>
+ <input type="email" id="email" name="email"i required > </br>
+ <label for="images">Bilder</label>
+ <input type="file" accept="images/*" name="image" id ="image" multiple> </br>
+ <select id="category" name="category" onchange="getsubcats(this)" required>
+ <option value="" disabled selected>Choose a category.</option>
+ {% for cat in categories %}
+ {% if cat.parent is none %}
+ <option value="{{cat.id}}">{{cat.name}}</option>
+ {% endif %}
+ {% endfor %}
+ </select></br>
+ <input type="submit" id="submit">
+</form>
+<script src="{% static 'georeport/retreiveCoordinates.js' %}"></script>
+</div>
+<div class="detail">
+<!-- TODO better URLS -->
+<a href="/georeport">Cancel</a>
+</div>
+{% endblock %}
+
--- /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)
+-->
+{% extends "georeport/base.html" %}
+{% load static %}
+{% load georeport_extras %}
+{% block title %}Detail {{ report.id }} {% endblock %}
+{% block body %}
+ <div class="detail">
+ <h2>Report {{ report.id }}</h2>
+ <p>Title: {{ report.title }}</p>
+ <p>Description: {{report.description }}</p>
+ <p>Erstellt am : {{ report.creation_time }}</p>
+ <p>Geändert: {{ report.last_changed }}</p>
+ <p id="p-lat" data-lat="{{ report.latitude }}">Latitude: {{ report.latitude }}</p>
+ <p id="p-lng" data-lng="{{ report.longitude }}">Longitude: {{ report.longitude }}</p>
+ <p>Status: {{ report.get_state_display }} </p>
+ <p>Kategorie: {{ report.category }} </p>
+ <a href="{% url 'index' %}">Back</a>
+ <script src="{% static 'georeport/addMarker.js' %}"></script>
+<!-- <img src="{{report.image.url}}" alt="Kein Bild vorhanden" scale=0.25>-->
+ {% for img in report.images.all %}
+ <!--<img src="{% static 'georeport/images/' %}{{img.file}}" alt={{img.alt}} scale=0.25 width=500px>-->
+ <img src={{urls|key:img.alt}} alt={{img.alt}} width=500px>
+ {% endfor %}
+</div>
+
+{% endblock %}
--- /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)
+-->
+{% extends "georeport/base.html" %}
+{% load static %}
+{% block title %}Index{% endblock %}
+{% block body %}
+<div class="detail" id="newReport"><h3><a href="create">New Report</a></h3></div>
+<div class="content">
+ <div class="list">
+ <h2>Reports </h2>
+ <ul>
+ <!-- List with published reports -->
+ {% for report in report_list %}
+ {% if report.published %}
+ <li><a href="{{ report.id }}">{{ report.title }}</a></li>
+ <script>
+ let marker{{report.id}} = L.marker([{{report.latitude}},{{report.longitude}}]);
+ marker{{report.id}}.addTo(map);
+ </script>
+ {% endif %}
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="list">
+ <h2>Categories</h2>
+ <ul>
+ {% for category in category_list %}
+ <li><a href="category/{{ category.id }}">{{ category.name }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+</div>
+
+{% endblock %}
from . import views
from django.urls import path
# TODO: Adjust to open311
-# /services: -> List with Categories <- GET
-# /sercvice/{id} -> single Category <- GET
+# /services: -> List with Categories <- GET ✅
+# /sercvice/{id} -> single Category <- GET ✅
# /requests -> Create a new Request <- POST
# /requests -> Get all Requests <- GET
# /requests/{id} -> Get a specific Request <- GET
+app_name = "georeport"
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_details, name="category"),
- # # TODO
- # path("category/<int:id>/children", views.get_subcategories, name="subcategories"),
+ 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("<str:b64nonce>/<str:b64ct>", views.finish_link, name="finish"),
]
A view takes a request and creates a respond for the request.
"""
-from django.shortcuts import render
-from django.views.decorators.http import require_safe
+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 .models import Category, Report
@require_safe
-def index(request):
+def index(request) -> HttpResponse:
"""
Function which handles request going to "/georeport".
+
+ Returns:
+ HttpResponse
+ """
+ reports = Report.objects.all() # type: ignore Attribute object unknown
+ categories = Category.objects.all() # type: ignore Attribute object unknown
+
+ return render(
+ request,
+ "georeport/index.html",
+ context={"report_list": reports, "category_list": categories},
+ )
+
+
+@require_GET
+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.
+
+ Arguments:
+ request: HttpRequest
+
+ id: int
+ Integer-identifier of the category, from which the subcategories shall be send.
+ If it is not provided, all categories are included in the response
+
+ Returns:
+ JsonResponse: Contains categories as data
"""
- return render(request, "georeport/index.html")
+ if id is None:
+ cats = Category.objects.all() # type:ignore Object attribute unknown
+ 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}
+ return JsonResponse(data)
-# TODO: Category-List
# TODO: Category-Detail
-# TODO: Subcategories
+
+
+def category_detail_view(request, id) -> HttpResponse:
+ """
+ Function to handle requests to see information about a single category identified by id
+
+ Arguments:
+ request: HttpRequest
+ id: int
+ Integer-identifier of the category to be seen.
+ """
+ cat = get_object_or_404(Category, pk=id)
+
+ # Check if the user is allowed to view the category
+ user = request.user
+ allowed_user = cat.user.all()
+ allowed_groups = cat.groups.all()
+ allowed = False
+ if user.is_superuser:
+ allowed = True
+ if user in allowed_user:
+ allowed = True
+
+ for group in allowed_groups:
+ if user in group.user_set.all():
+ allowed = True
+
+ if allowed:
+ return render(request, "georeport/category.html", context={"categroy": cat})
+
+ else:
+ raise PermissionDenied
+
# TODO: Report-List
# TODO: Create-Report
-# TODO: Detailview Report
+# TODO: Detail-View Report
# TODO: Finish Link
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
- "DIRS": [],
+ "DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+INTERNAL_IPS = [
+ "127.0.0.1",
+]
TESTING = "test" in sys.argv
if not TESTING:
"""
from django.contrib import admin
-from django.urls import path
+from django.urls import path, include
from django.conf import settings
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
path("admin/", admin.site.urls),
+ path("georeport/", include("georeport.urls")),
]
-if settings.TESTING:
+if not settings.TESTING:
urlpatterns = [*urlpatterns] + debug_toolbar_urls()