diff --git a/shop/templates/product_detail.html b/shop/templates/product_detail.html
index 8841123e..a5408361 100644
--- a/shop/templates/product_detail.html
+++ b/shop/templates/product_detail.html
@@ -3,6 +3,10 @@
{% load commonmark %}
{% load shop_tags %}
+{% block title %}
+{{ product.name }} | {{ block.super }}
+{% endblock %}
+
{% block shop_content %}
diff --git a/shop/templates/shop_index.html b/shop/templates/shop_index.html
index 45df7340..fc0c8ad7 100644
--- a/shop/templates/shop_index.html
+++ b/shop/templates/shop_index.html
@@ -2,6 +2,10 @@
{% load bootstrap3 %}
{% load shop_tags %}
+{% block title %}
+Shop | {{ block.super }}
+{% endblock %}
+
{% block shop_content %}
diff --git a/villages/__init__.py b/villages/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/villages/admin.py b/villages/admin.py
new file mode 100644
index 00000000..e752aaea
--- /dev/null
+++ b/villages/admin.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+
+from .models import Village
+
+
+@admin.register(Village)
+class VillageAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'private',
+ 'deleted',
+ ]
+
+ list_filter = [
+ 'private',
+ 'deleted',
+ ]
diff --git a/villages/apps.py b/villages/apps.py
new file mode 100644
index 00000000..fbf4978a
--- /dev/null
+++ b/villages/apps.py
@@ -0,0 +1,7 @@
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+
+
+class VillagesConfig(AppConfig):
+ name = 'villages'
diff --git a/villages/managers.py b/villages/managers.py
new file mode 100644
index 00000000..9966196d
--- /dev/null
+++ b/villages/managers.py
@@ -0,0 +1,9 @@
+from django.db.models import QuerySet
+
+
+class VillageQuerySet(QuerySet):
+
+ def not_deleted(self):
+ return self.filter(
+ deleted=False
+ )
diff --git a/villages/migrations/0001_initial.py b/villages/migrations/0001_initial.py
new file mode 100644
index 00000000..47c87f10
--- /dev/null
+++ b/villages/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-07-05 21:38
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('camps', '0005_auto_20160510_2011'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Village',
+ fields=[
+ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=255)),
+ ('slug', models.SlugField(blank=True, max_length=255)),
+ ('description', models.TextField()),
+ ('open', models.BooleanField(default=False, help_text='Is this village open for others to join?')),
+ ('camp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='camps.Camp')),
+ ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ['name'],
+ },
+ ),
+ ]
diff --git a/villages/migrations/0002_auto_20160705_2154.py b/villages/migrations/0002_auto_20160705_2154.py
new file mode 100644
index 00000000..020819be
--- /dev/null
+++ b/villages/migrations/0002_auto_20160705_2154.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-07-05 21:54
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('villages', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='village',
+ name='open',
+ ),
+ migrations.AddField(
+ model_name='village',
+ name='private',
+ field=models.BooleanField(default=True, help_text='Check if your village is privately organized'),
+ ),
+ ]
diff --git a/villages/migrations/0003_auto_20160705_2159.py b/villages/migrations/0003_auto_20160705_2159.py
new file mode 100644
index 00000000..af9a136c
--- /dev/null
+++ b/villages/migrations/0003_auto_20160705_2159.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-07-05 21:59
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('villages', '0002_auto_20160705_2154'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='village',
+ name='private',
+ field=models.BooleanField(default=False, help_text='Check if your village is privately organized'),
+ ),
+ ]
diff --git a/villages/migrations/0004_village_deleted.py b/villages/migrations/0004_village_deleted.py
new file mode 100644
index 00000000..3c1e5b3a
--- /dev/null
+++ b/villages/migrations/0004_village_deleted.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.6 on 2016-07-10 16:58
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('villages', '0003_auto_20160705_2159'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='village',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/villages/migrations/__init__.py b/villages/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/villages/models.py b/villages/models.py
new file mode 100644
index 00000000..5104b915
--- /dev/null
+++ b/villages/models.py
@@ -0,0 +1,72 @@
+from __future__ import unicode_literals
+
+from django.core.urlresolvers import reverse_lazy
+from django.db import models
+from django.utils.text import slugify
+
+from camps.models import Camp
+from utils.models import CreatedUpdatedModel, UUIDModel
+
+from .managers import VillageQuerySet
+
+
+class Village(CreatedUpdatedModel, UUIDModel):
+
+ class Meta:
+ ordering = ['name']
+
+ camp = models.ForeignKey('camps.Camp')
+ contact = models.ForeignKey('auth.User')
+
+ name = models.CharField(max_length=255)
+ slug = models.SlugField(max_length=255, blank=True)
+ description = models.TextField(
+ help_text="A descriptive text about your village. Markdown is supported."
+ )
+
+ private = models.BooleanField(
+ default=False,
+ help_text='Check if your village is privately organized'
+ )
+
+ deleted = models.BooleanField(
+ default=False,
+ )
+
+ objects = VillageQuerySet.as_manager()
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse_lazy('villages:detail', kwargs={'slug': self.slug})
+
+ def save(self, **kwargs):
+ if (
+ not self.pk or
+ not self.slug or
+ Village.objects.filter(slug=self.slug).count() > 1
+ ):
+ slug = slugify(self.name)
+ incrementer = 1
+
+ # We have to make sure that the slug won't clash with current slugs
+ while Village.objects.filter(slug=slug).exists():
+ if incrementer == 1:
+ slug = '{}-1'.format(slug)
+ else:
+ slug = '{}-{}'.format(
+ '-'.join(slug.split('-')[:-1]),
+ incrementer
+ )
+ incrementer += 1
+ self.slug = slug
+
+ if not hasattr(self, 'camp'):
+ self.camp = Camp.objects.current()
+
+ super(Village, self).save(**kwargs)
+
+ def delete(self, using=None, keep_parents=False):
+ self.deleted = True
+ self.save()
diff --git a/villages/templates/village_confirm_delete.html b/villages/templates/village_confirm_delete.html
new file mode 100644
index 00000000..d4fd816c
--- /dev/null
+++ b/villages/templates/village_confirm_delete.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+{% load commonmark %}
+
+{% block content %}
+
+
+{% endblock %}
diff --git a/villages/templates/village_detail.html b/villages/templates/village_detail.html
new file mode 100644
index 00000000..7ab7a9ed
--- /dev/null
+++ b/villages/templates/village_detail.html
@@ -0,0 +1,16 @@
+{% extends 'base.html' %}
+{% load commonmark %}
+
+{% block content %}
+
+
{{ village.name }}
+
+{{ village.description|commonmark }}
+
+{% if user == village.contact %}
+
+
Edit
+
Delete
+{% endif %}
+
+{% endblock %}
diff --git a/villages/templates/village_form.html b/villages/templates/village_form.html
new file mode 100644
index 00000000..3b5f8534
--- /dev/null
+++ b/villages/templates/village_form.html
@@ -0,0 +1,16 @@
+{% extends 'base.html' %}
+{% load bootstrap3 %}
+
+{% block content %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/villages/templates/village_list.html b/villages/templates/village_list.html
new file mode 100644
index 00000000..75a3015e
--- /dev/null
+++ b/villages/templates/village_list.html
@@ -0,0 +1,52 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+ If this is your first hackercamp the term 'Village' might be confusing but it
+ is fairly simple: a village is just a spot on the campsite where you and a
+ bunch of your friends/likeminded people camp together. Apart from peoples
+ individual tents which they sleep in, many villages bring a large common tent
+ where you can hack and hang out during the day.
+
+
+
+
+ It is also possible to rent a tent, chairs and tables for villages here.
+
+
+
+{% if user.is_authenticated %}
+
Create a village
+{% endif %}
+
+
+
+
+
+
+ Name |
+ Description |
+ Public |
+
+
+
+ {% for village in villages %}
+
+
+
+ {{ village.name }}
+
+ |
+
+ {{ village.description|truncatewords:15 }}
+ |
+
+
+ |
+
+ {% endfor %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/villages/tests.py b/villages/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/villages/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/villages/urls.py b/villages/urls.py
new file mode 100644
index 00000000..7a5d473f
--- /dev/null
+++ b/villages/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+from views import *
+
+urlpatterns = [
+ url(r'^$', VillageListView.as_view(), name='list'),
+ url(r'create/$', VillageCreateView.as_view(), name='create'),
+ url(r'(?P
[-_\w+]+)/delete/$', VillageDeleteView.as_view(), name='delete'),
+ url(r'(?P[-_\w+]+)/edit/$', VillageUpdateView.as_view(), name='update'),
+ url(r'(?P[-_\w+]+)/$', VillageDetailView.as_view(), name='detail'),
+]
diff --git a/villages/views.py b/villages/views.py
new file mode 100644
index 00000000..2bde344a
--- /dev/null
+++ b/villages/views.py
@@ -0,0 +1,50 @@
+from django.core.urlresolvers import reverse_lazy
+from django.http import HttpResponseRedirect
+from django.views.generic import (
+ ListView, DetailView, CreateView, UpdateView, DeleteView
+)
+from .models import (
+ Village,
+)
+
+
+class VillageListView(ListView):
+ queryset = Village.objects.not_deleted()
+ template_name = 'village_list.html'
+ context_object_name = 'villages'
+
+
+class VillageDetailView(DetailView):
+ queryset = Village.objects.not_deleted()
+ template_name = 'village_detail.html'
+ context_object_name = 'village'
+
+
+class VillageCreateView(CreateView):
+ model = Village
+ template_name = 'village_form.html'
+ fields = ['name', 'description', 'private']
+ success_url = reverse_lazy('villages:list')
+
+ def form_valid(self, form):
+ village = form.save(commit=False)
+ village.contact = self.request.user
+ village.save()
+ return HttpResponseRedirect(village.get_absolute_url())
+
+
+class VillageUpdateView(UpdateView):
+ model = Village
+ queryset = Village.objects.not_deleted()
+ template_name = 'village_form.html'
+ fields = ['name', 'description', 'private']
+
+ def get_success_url(self):
+ return self.get_object().get_absolute_url()
+
+
+class VillageDeleteView(DeleteView):
+ model = Village
+ success_url = reverse_lazy('villages:list')
+ template_name = 'village_confirm_delete.html'
+ context_object_name = 'village'