Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/utils/bootstrap/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,8 @@ def create_camp_villages(self, camp: Camp, users: dict) -> None:
camp=camp,
name="Baconsvin",
slug="baconsvin",
approved=True,
location=Point(9.9401295, 55.3881695),
description="The camp with the doorbell-pig! Baconsvin is a group of happy people from Denmark "
"doing a lot of open source, and are always happy to talk about infosec, hacking, BSD, and much more. "
"A lot of the organizers of BornHack live in Baconsvin village. "
Expand All @@ -1324,6 +1326,7 @@ def create_camp_villages(self, camp: Camp, users: dict) -> None:
camp=camp,
name="NetworkWarriors",
slug="networkwarriors",
approved=True,
description="We will have a tent which house the NOC people, various lab equipment people "
"can play with, and have fun. If you want to talk about networking, come by, and if you have "
"trouble with the Bornhack network contact us.",
Expand Down
24 changes: 24 additions & 0 deletions src/villages/migrations/0016_alter_village_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.21 on 2025-06-14 11:13
from __future__ import annotations

import django.contrib.gis.db.models.fields
from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("villages", "0015_alter_village_options"),
]

operations = [
migrations.AlterField(
model_name="village",
name="location",
field=django.contrib.gis.db.models.fields.PointField(
blank=True,
help_text="The location of this village.",
null=True,
srid=4326,
),
),
]
4 changes: 2 additions & 2 deletions src/villages/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

from django.contrib.gis.db.models import PointField
from django.contrib.gis.geos import Point
from django.db import models
from django.urls import reverse_lazy
from django_prometheus.models import ExportModelOperationsMixin
Expand Down Expand Up @@ -40,7 +39,8 @@ class Meta:

# default to near general camping
location = PointField(
default=Point(9.9401295, 55.3881695),
null=True,
blank=True,
help_text="The location of this village.",
)

Expand Down
1 change: 1 addition & 0 deletions src/villages/templates/village_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ <h3>{{ village.name }}</h3>
<a href="{% url 'villages:village_update' camp_slug=village.camp.slug slug=village.slug %}" class="btn btn-primary">Edit</a>
<a href="{% url 'villages:village_delete' camp_slug=village.camp.slug slug=village.slug %}" class="btn btn-danger">Delete</a>
{% endif %}
<a href="{% url 'villages:village_list' camp_slug=village.camp.slug %}" class="btn btn-secondary"><i class="fas fa-undo"></i> Back</a>
<script type="text/javascript" src="{% static 'js/villages/village_detail.js' %}"></script>

{% endblock %}
4 changes: 4 additions & 0 deletions src/villages/templates/village_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

{% block content %}
<h3>Create {{ camp.title }} Village</h3>
<div class="alert alert-warning" role="alert">
Note: Putting a village on the map is to indicate where you village is, not to reserve space!
</div>
<form method="post">
{% csrf_token %}

Expand All @@ -27,6 +30,7 @@ <h3>Create {{ camp.title }} Village</h3>
<hr />

<button class="btn btn-primary pull-right" type="submit">Save</button>
<a href="{% url 'villages:village_list' camp_slug=camp.slug %}" class="btn btn-secondary"><i class="fas fa-undo"></i> Back</a>
</form>
<script src="{% static 'js/maps/generic/init_loader.js' %}"></script>

Expand Down
2 changes: 1 addition & 1 deletion src/villages/templates/village_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h2>Villages</h2>

<hr>
{% if villages %}
<table class="table table-hover table-condensed table-striped">
<table class="table table-hover table-condensed table-striped" id="main_table">
<thead>
<tr>
<th>Name</th>
Expand Down
Empty file added src/villages/tests/__init__.py
Empty file.
219 changes: 219 additions & 0 deletions src/villages/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""Test cases for the base and member views of the teams application."""

from __future__ import annotations

from bs4 import BeautifulSoup
from django.urls import reverse

from utils.tests import BornhackTestBase


class VillageViewTest(BornhackTestBase):
"""Test Village Views."""

@classmethod
def setUpTestData(cls) -> None:
"""Setup test data."""
# first add users and other basics
super().setUpTestData()

cls.bootstrap.create_camp_villages(camp=cls.camp, users=cls.users)

def test_village_list_view(self) -> None:
"""Test the village list view."""
url = reverse(
"villages:village_list",
kwargs={
"camp_slug": self.camp.slug,
},
)
response = self.client.get(path=url)
assert response.status_code == 200

content = response.content.decode()
soup = BeautifulSoup(content, "html.parser")
rows = soup.select("table#main_table > tbody > tr")
self.assertEqual(len(rows), 2, "village list does not return 2 entries")

def test_village_create_view(self) -> None:
"""Test the village create view."""
self.client.force_login(self.users[0])
url = reverse(
"villages:village_create",
kwargs={
"camp_slug": self.camp.slug,
},
)
response = self.client.get(path=url)
assert response.status_code == 200

response = self.client.post(
path=url,
data={
"name": "Test Village",
"description": "Some test village description.",
"private": False,
"location": """{"type":"Point","coordinates":[9.9401295,55.3881695]}""",
},
follow=True,
)
assert response.status_code == 200
content = response.content.decode()
soup = BeautifulSoup(content, "html.parser")
rows = soup.select("div.alert.alert-success")
matches = [s for s in rows if "Your request to create a village has been registered" in str(s)]
self.assertEqual(len(matches), 1, "failed to create a village.")

# Create village without location.
response = self.client.post(
path=url,
data={
"name": "Test Village",
"description": "Some test village description.",
"private": False,
"location": "",
},
follow=True,
)
assert response.status_code == 200
content = response.content.decode()
soup = BeautifulSoup(content, "html.parser")
rows = soup.select("div.alert.alert-success")
matches = [s for s in rows if "Your request to create a village has been registered" in str(s)]
self.assertEqual(len(matches), 1, "failed to create a village without location.")

def test_village_geojson_view(self) -> None:
"""Test the village geojson view."""
url = reverse(
"villages:villages_geojson",
kwargs={
"camp_slug": self.camp.slug,
},
)
response = self.client.get(path=url)
assert response.status_code == 200
content = response.json()
self.assertEqual(len(content["features"]), 1, "GeoJSON view did not return 1 feature")

def test_village_update_view(self) -> None:
"""Test the village update view."""
self.client.force_login(self.users[2])
url = reverse(
"villages:village_update",
kwargs={
"camp_slug": self.camp.slug,
"slug": "networkwarriors",
},
)
response = self.client.get(path=url)
assert response.status_code == 200

response = self.client.post(
path=url,
data={
"name": "NetworkWarriors",
"description": "Some test village description for NetworkWarriors.",
"private": False,
"location": """{"type":"Point","coordinates":[9.9401295,55.3881695]}""",
},
follow=True,
)
assert response.status_code == 200

content = response.content.decode()
soup = BeautifulSoup(content, "html.parser")
rows = soup.select("div.alert.alert-success")
matches = [s for s in rows if "Your village will be republished after the changes have been reviewed" in str(s)]
self.assertEqual(len(matches), 1, "failed to update a village.")

url = reverse(
"villages:village_update",
kwargs={
"camp_slug": self.camp.slug,
"slug": "the-camp",
},
)
response = self.client.get(path=url)
assert response.status_code == 404

def test_village_map_view(self) -> None:
"""Test the village map view."""
url = reverse(
"villages:village_map",
kwargs={
"camp_slug": self.camp.slug,
},
)
response = self.client.get(path=url)
assert response.status_code == 200

def test_village_detail_view(self) -> None:
"""Test the village detail view."""
url = reverse(
"villages:village_detail",
kwargs={
"camp_slug": self.camp.slug,
"slug": "baconsvin",
},
)
response = self.client.get(path=url)
assert response.status_code == 200

# Test 404 error page
url = reverse(
"villages:village_detail",
kwargs={
"camp_slug": self.camp.slug,
"slug": "404camp",
},
)
response = self.client.get(path=url)
assert response.status_code == 404

# Test accessing a not approved village.
url = reverse(
"villages:village_detail",
kwargs={
"camp_slug": self.camp.slug,
"slug": "the-camp",
},
)
response = self.client.get(path=url)
assert response.status_code == 404

def test_village_delete_view(self) -> None:
"""Test the village delete view."""
self.client.force_login(self.users[0])
url = reverse(
"villages:village_create",
kwargs={
"camp_slug": self.camp.slug,
},
)
response = self.client.get(path=url)
assert response.status_code == 200

response = self.client.post(
path=url,
data={
"name": "delete",
"description": "Some delete test village description.",
"private": False,
"location": """{"type":"Point","coordinates":[9.9401295,55.3881695]}""",
},
follow=True,
)
assert response.status_code == 200

url = reverse(
"villages:village_delete",
kwargs={
"camp_slug": self.camp.slug,
"slug": "delete",
},
)
response = self.client.get(path=url)
assert response.status_code == 200

response = self.client.post(path=url, follow=True)
assert response.status_code == 200
19 changes: 8 additions & 11 deletions src/villages/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def dump_features(self) -> list[object]:
deleted=False,
approved=True,
):
if village.location is None:
if not village.location:
continue
entry = {
"type": "Feature",
Expand Down Expand Up @@ -130,12 +130,13 @@ def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
def get_context_data(self, **kwargs) -> dict[str, str | bool]:
"""Add village map data to context."""
context = super().get_context_data(**kwargs)
context["mapData"] = {
"grid": static("json/grid.geojson"),
"x": context["village"].location.x,
"y": context["village"].location.y,
"loggedIn": self.request.user.is_authenticated,
}
if context["village"].location:
context["mapData"] = {
"grid": static("json/grid.geojson"),
"x": context["village"].location.x,
"y": context["village"].location.y,
"loggedIn": self.request.user.is_authenticated,
}
return context

def get_queryset(self) -> QuerySet[Village]:
Expand Down Expand Up @@ -179,8 +180,6 @@ def form_valid(self, form: ModelForm[Village]) -> HttpResponseRedirect:
village = form.save(commit=False)
village.contact = self.request.user
village.camp = self.camp
if not village.name:
village.name = "noname"
village.save()
messages.success(
self.request,
Expand Down Expand Up @@ -241,8 +240,6 @@ def form_valid(self, form: ModelForm[Village]) -> HttpResponse:
"""Set village as not approved before saving, send email."""
village = form.save(commit=False)
village.approved = False
if not village.name:
village.name = "noname"
messages.success(
self.request,
"Your village will be republished after the changes have been reviewed for CoC compliance.",
Expand Down
Loading