Merge branch 'master' into feature/speakers-app

This commit is contained in:
Víðir Valberg Guðmundsson 2016-07-13 21:41:35 +02:00
commit 4381dbd2bd
40 changed files with 725 additions and 46 deletions

View File

@ -29,6 +29,7 @@ INSTALLED_APPS = [
'shop',
'news',
'utils',
'villages',
'allauth',
'allauth.account',

View File

@ -7,7 +7,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Bornhack</title>
<title>{% block title %}BornHack{% endblock %}</title>
<!-- Bootstrap core CSS -->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
@ -42,19 +42,8 @@
{% if current_camp.shop_open %}
<li><a href="{% url 'shop:index' %}">Shop</a></li>
{% if current_order and current_order.get_number_of_items %}
<li>
<a href="{% url 'shop:order_detail' pk=current_order.pk %}">
<i class="glyphicon glyphicon-shopping-cart"></i>
<span class="badge">
{{ current_order.get_number_of_items }}
</span>
</a>
</li>
{% endif %}
{% endif %}
<li><a href="{% url 'villages:list' %}">Villages</a></li>
<li><a href="{% url 'call-for-speakers' %}">Speakers</a></li>
<li><a href="{% url 'call-for-sponsors' %}">Sponsors</a></li>
<li><a href="{% url 'contact' %}">Contact</a></li>

View File

@ -1,5 +1,9 @@
{% extends 'base.html' %}
{% block title %}
Code of Conduct | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Code of Conduct</h2>

View File

@ -1,5 +1,9 @@
{% extends 'base.html' %}
{% block title %}
Contact | {{ block.super }}
{% endblock %}
{% block content %}
<p class="lead">

View File

@ -1,6 +1,10 @@
{% extends 'base.html' %}
{% load static from staticfiles %}
{% block title %}
Info | {{ block.super }}
{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'css/leaflet.css' %}" />
{% endblock %}
@ -201,16 +205,22 @@
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.
villages bring a large common tent where you can hack and hang out
during the day.
</p>
<p>Villages can also rent village tents. The details are not in place yet,
but stay tuned for the announcement. The idea is that you order and pay
for a large tent which will then be setup and ready when you arrive. The
tents have wooden floors and you can rent folding tables and chairs as needed.
<p>Villages can also rent village tents via us, head over to the
<a href="{% url 'shop:index' %}?category=villages">
villages section of the shop</a>.
The tents will be ready for when you arrive and will be teared down
again saturday the 3rd of September at 12:00. The tents have optional
floors and you can rent folding tables and chairs as needed. You do not
have to register a village or have a concept to rent a big tent.
</p>
<p><a href="{% url 'villages:list' %}">You can register your village registration here</a>!
If you don't want to make your own village you will possibly be able to
find and join one that suits your interests.
Get in touch if you have any questions!
</p>
<p>Village registration is not finished yet, but stay tuned!
If you don't want to make your own village you will likely
be able to find and join one that suits your interests.</p>
</div>
</div>
@ -231,7 +241,6 @@
<dt>Kiosk</dt>
<dd>We sell a few beverages and sweets as well as practical things like coal for the barbeque. We will accept the same payment methods at the venue as we do on the website: cash, cards, and blockchain.</dd>
</dl>
</p>
</div>
</div>

View File

@ -1,5 +1,9 @@
{% extends 'base.html' %}
{% block title %}
General Terms and Conditions | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Terms and Conditions for BornHack IvS sale and delivery of (i) merchandise (ii) tickets and/or (iii) accommodation</h2>

View File

@ -1,5 +1,9 @@
{% extends 'base.html' %}
{% block title %}
Privacy Policy | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Privacy and cookie policy</h2>

View File

@ -1,5 +1,9 @@
{% extends 'base.html' %}
{% block title %}
Call for Speakers | {{ block.super }}
{% endblock %}
{% block content %}
<h2>BornHack 2016: Call for Speakers</h2>

View File

@ -1,5 +1,9 @@
{% extends 'base.html' %}
{% block title %}
Call for Sponsors | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Sponsoring</h2>

View File

@ -74,6 +74,10 @@ urlpatterns = [
r'^news/',
include('news.urls', namespace='news')
),
url(
r'^villages/',
include('villages.urls', namespace='villages')
),
url(r'^accounts/', include('allauth.urls')),
url(r'^admin/', include(admin.site.urls)),
]

View File

@ -1,6 +1,10 @@
{% extends 'base.html' %}
{% load commonmark %}
{% block title %}
{{ news_item.title }} | {{ block.super }}
{% endblock %}
{% block content %}
<div>
{% if not_public %}

View File

@ -1,6 +1,10 @@
{% extends 'base.html' %}
{% load commonmark %}
{% block title %}
News | {{ block.super }}
{% endblock %}
{% block content %}
{% for item in news_items %}
<div>

View File

@ -9,7 +9,7 @@ admin.site.register(models.CoinifyAPICallback)
admin.site.register(models.Invoice)
admin.site.register(models.CreditNote)
admin.site.register(models.Ticket)
admin.site.register(models.CustomOrder)
@admin.register(models.ProductCategory)
class ProductCategoryAdmin(admin.ModelAdmin):
@ -41,6 +41,7 @@ class TicketInline(admin.TabularInline):
@admin.register(models.Order)
class OrderAdmin(admin.ModelAdmin):
change_form_template = 'admin/change_order_form.html'
readonly_fields = ('paid',)
def get_email(self, obj):
return obj.user.email

View File

@ -4,7 +4,7 @@ from django.conf import settings
from django.utils import timezone
from shop.pdf import generate_pdf_letter
from shop.email import send_invoice_email, send_creditnote_email
from shop.models import Order, Invoice, CreditNote
from shop.models import Order, CustomOrder, Invoice, CreditNote
from time import sleep
from decimal import Decimal
@ -19,11 +19,17 @@ class Command(BaseCommand):
def handle(self, *args, **options):
self.output('Invoice worker running...')
while True:
# check if we need to generate any invoices
# check if we need to generate any invoices for shop orders
for order in Order.objects.filter(paid=True, invoice__isnull=True):
# generate invoice for this Order
Invoice.objects.create(order=order)
self.output('Generated Invoice object for order %s' % order)
self.output('Generated Invoice object for %s' % order)
# check if we need to generate any invoices for custom orders
for customorder in CustomOrder.objects.filter(invoice__isnull=True):
# generate invoice for this CustomOrder
Invoice.objects.create(customorder=customorder)
self.output('Generated Invoice object for %s' % customorder)
# check if we need to generate any pdf invoices
for invoice in Invoice.objects.filter(pdf=''):
@ -34,9 +40,13 @@ class Command(BaseCommand):
# generate the pdf
try:
if invoice.customorder:
template='pdf/custominvoice.html'
else:
template='pdf/invoice.html'
pdffile = generate_pdf_letter(
filename=invoice.filename,
template='pdf/invoice.html',
template=template,
formatdict=formatdict,
)
self.output('Generated pdf for invoice %s' % invoice)
@ -54,8 +64,8 @@ class Command(BaseCommand):
invoice.save()
###############################################################
# check if we need to send out any invoices (only where pdf has been generated)
for invoice in Invoice.objects.filter(sent_to_customer=False).exclude(pdf=''):
# check if we need to send out any invoices (only for shop orders, and only where pdf has been generated)
for invoice in Invoice.objects.filter(order__isnull=False, sent_to_customer=False).exclude(pdf=''):
# send the email
if send_invoice_email(invoice=invoice):
self.output('OK: Invoice email sent to %s' % invoice.order.user.email)

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-07-12 20:36
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('camps', '0005_auto_20160510_2011'),
('shop', '0026_order_refunded'),
]
operations = [
migrations.CreateModel(
name='CustomOrder',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('text', models.TextField()),
('amount', models.IntegerField(help_text='Amount of this custom order (in DKK, including VAT).')),
('paid', models.BooleanField(default=False, help_text='Whether this custom order has been paid.', verbose_name='Paid?')),
('camp', models.ForeignKey(help_text='The camp this custom order is for.', on_delete=django.db.models.deletion.CASCADE, to='camps.Camp', verbose_name='Camp')),
],
options={
'abstract': False,
},
),
migrations.AlterModelOptions(
name='creditnote',
options={'ordering': ['-created']},
),
migrations.AlterField(
model_name='creditnote',
name='paid',
field=models.BooleanField(default=False, help_text='Whether the amount in this creditnote has been paid back to the customer.', verbose_name='Paid?'),
),
migrations.AlterField(
model_name='invoice',
name='order',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shop.Order'),
),
migrations.AlterField(
model_name='order',
name='camp',
field=models.ForeignKey(help_text='The camp this shop order is for.', on_delete=django.db.models.deletion.CASCADE, to='camps.Camp', verbose_name='Camp'),
),
migrations.AlterField(
model_name='order',
name='open',
field=models.NullBooleanField(default=True, help_text='Whether this shop order is open or not. "None" means closed.', verbose_name='Open?'),
),
migrations.AlterField(
model_name='order',
name='paid',
field=models.BooleanField(default=False, help_text='Whether this shop order has been paid.', verbose_name='Paid?'),
),
migrations.AlterField(
model_name='order',
name='user',
field=models.ForeignKey(help_text='The user this shop order belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AlterField(
model_name='product',
name='price',
field=models.IntegerField(help_text='Price of the product (in DKK, including VAT).'),
),
migrations.AddField(
model_name='invoice',
name='customorder',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shop.CustomOrder'),
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-07-12 21:19
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0027_auto_20160712_2036'),
]
operations = [
migrations.AddField(
model_name='customorder',
name='customer',
field=models.TextField(default='', help_text='The customer info for this order, use <br> for newlines'),
preserve_default=False,
),
migrations.AlterField(
model_name='customorder',
name='text',
field=models.TextField(help_text='The invoice text'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-07-12 21:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0028_auto_20160712_2119'),
]
operations = [
migrations.AlterField(
model_name='customorder',
name='customer',
field=models.TextField(help_text='The customer info for this order'),
),
]

View File

@ -15,8 +15,40 @@ from decimal import Decimal
from datetime import timedelta
class Order(CreatedUpdatedModel):
class CustomOrder(CreatedUpdatedModel):
camp = models.ForeignKey(
'camps.Camp',
verbose_name=_('Camp'),
help_text=_('The camp this custom order is for.'),
)
text = models.TextField(
help_text=_('The invoice text')
)
customer = models.TextField(
help_text=_('The customer info for this order')
)
amount = models.IntegerField(
help_text=_('Amount of this custom order (in DKK, including VAT).')
)
paid = models.BooleanField(
verbose_name=_('Paid?'),
help_text=_('Whether this custom order has been paid.'),
default=False,
)
def __str__(self):
return 'custom order id #%s' % self.pk
@property
def vat(self):
return Decimal(self.amount*Decimal(0.2))
class Order(CreatedUpdatedModel):
class Meta:
unique_together = ('user', 'open')
ordering = ['-created']
@ -29,26 +61,26 @@ class Order(CreatedUpdatedModel):
user = models.ForeignKey(
'auth.User',
verbose_name=_('User'),
help_text=_('The user this order belongs to.'),
help_text=_('The user this shop order belongs to.'),
related_name='orders',
)
paid = models.BooleanField(
verbose_name=_('Paid?'),
help_text=_('Whether this order has been paid.'),
help_text=_('Whether this shop order has been paid.'),
default=False,
)
open = models.NullBooleanField(
verbose_name=_('Open?'),
help_text=_('Whether this order is open or not. "None" means closed.'),
help_text=_('Whether this shop order is open or not. "None" means closed.'),
default=True,
)
camp = models.ForeignKey(
'camps.Camp',
verbose_name=_('Camp'),
help_text=_('The camp this order is for.'),
help_text=_('The camp this shop order is for.'),
)
CREDIT_CARD = 'credit_card'
@ -84,7 +116,7 @@ class Order(CreatedUpdatedModel):
objects = OrderQuerySet.as_manager()
def __str__(self):
return 'order id #%s' % self.pk
return 'shop order id #%s' % self.pk
def get_number_of_items(self):
return self.products.aggregate(
@ -225,7 +257,7 @@ class Product(CreatedUpdatedModel, UUIDModel):
slug = models.SlugField()
price = models.IntegerField(
help_text=_('Price of the product (in DKK).')
help_text=_('Price of the product (in DKK, including VAT).')
)
description = models.TextField()
@ -326,19 +358,29 @@ class CreditNote(CreatedUpdatedModel):
class Invoice(CreatedUpdatedModel):
order = models.OneToOneField('shop.Order')
order = models.OneToOneField('shop.Order', null=True, blank=True)
customorder = models.OneToOneField('shop.CustomOrder', null=True, blank=True)
pdf = models.FileField(null=True, blank=True, upload_to='invoices/')
sent_to_customer = models.BooleanField(default=False)
def __str__(self):
return 'invoice#%s - order %s - %s - total %s DKK (sent to %s: %s)' % (
self.id,
self.order.id,
self.order.created,
self.order.total,
self.order.user.email,
self.sent_to_customer,
)
if self.order:
return 'invoice#%s - shop order %s - %s - total %s DKK (sent to %s: %s)' % (
self.id,
self.order.id,
self.order.created,
self.order.total,
self.order.user.email,
self.sent_to_customer,
)
elif self.customorder:
return 'invoice#%s - custom order %s - %s - amount %s DKK (customer: %s)' % (
self.id,
self.customorder.id,
self.customorder.created,
self.customorder.amount,
self.customorder.customer,
)
@property
def filename(self):

View File

@ -0,0 +1,51 @@
{% load static from staticfiles %}
{% load shop_tags %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<table style="width:100%;">
<tr>
<td style="width: 75%;">&nbsp;</td>
<td>
<h3>
{{ invoice.created|date:"b jS, Y" }}<br>
Custom Order #{{ invoice.customorder.pk }}<br>
Invoice #{{ invoice.pk }}
</h3>
</td>
</tr>
</table>
<h2>CUSTOMER</h2>
<p class="lead">{{ invoice.customorder.customer|linebreaks }}</p>
<br>
<h2>INVOICE</h2>
<table style="width:90%">
<tr>
<td>
<b>Text
<td align="center" style="width: 20%">
<b>Price
<tr>
<td>
{{ invoice.customorder.text|linebreaks }}
<td align="right">
{{ invoice.customorder.amount|currency }}
<tr>
<td align="right">
<strong>Danish VAT (25%)</strong>
<td align="right">
{{ invoice.customorder.vat|currency }}
<tr>
<td align="right">
<strong>Total</strong>
<td align="right">
{{ invoice.customorder.amount|currency }}
</table>
<br>
<p class="lead">
Payment should be made by bank transfer to our account in Arbejdernes Landsbank reg. 5371 account no. 0244504 within two weeks from {{ invoice.created|date:"b jS, Y" }} please. Thank you!
</p>

View File

@ -3,6 +3,10 @@
{% load commonmark %}
{% load shop_tags %}
{% block title %}
{{ product.name }} | {{ block.super }}
{% endblock %}
{% block shop_content %}
<div class="row">

View File

@ -19,7 +19,21 @@
{% if has_tickets %}
<li class="pull-right"><a href="{% url 'shop:ticket_list' %}">Tickets</a></li>
{% endif %}
<li class="pull-right no-before"><a href="{% url 'shop:order_list' %}">Orders</a></li>
<li class="pull-right"><a href="{% url 'shop:order_list' %}">Orders</a></li>
<li class="pull-right no-before">
{% if current_order and current_order.get_number_of_items %}
<a href="{% url 'shop:order_detail' pk=current_order.pk %}">
{% endif %}
<i class="glyphicon glyphicon-shopping-cart"></i>
<span class="badge">
{{ current_order.get_number_of_items|default:0 }}
</span>
{% if current_order and current_order.get_number_of_items %}
</a>
{% endif %}
</li>
{% endif %}
</ol>
</div>

View File

@ -2,6 +2,10 @@
{% load bootstrap3 %}
{% load shop_tags %}
{% block title %}
Shop | {{ block.super }}
{% endblock %}
{% block shop_content %}
<div class="row">

0
villages/__init__.py Normal file
View File

17
villages/admin.py Normal file
View File

@ -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',
]

7
villages/apps.py Normal file
View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class VillagesConfig(AppConfig):
name = 'villages'

9
villages/managers.py Normal file
View File

@ -0,0 +1,9 @@
from django.db.models import QuerySet
class VillageQuerySet(QuerySet):
def not_deleted(self):
return self.filter(
deleted=False
)

View File

@ -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'],
},
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-07-12 20:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('villages', '0004_village_deleted'),
]
operations = [
migrations.AlterField(
model_name='village',
name='description',
field=models.TextField(help_text='A descriptive text about your village. Markdown is supported.'),
),
]

View File

72
villages/models.py Normal file
View File

@ -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()

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% load commonmark %}
{% block content %}
<form action="" method="post" class="col-md-6 col-md-offset-3">{% csrf_token %}
<p>Are you sure you want to delete the village "{{ village }}"?</p>
<button type="submit" class="btn btn-danger form-control">Confirm</button>
<br />
<br />
<a href="{% url 'villages:detail' slug=village.slug %}" class="btn btn-default form-control">Cancel</a>
</form>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load commonmark %}
{% block title %}
Village: {{ village.name }} | {{ block.super }}
{% endblock %}
{% block content %}
<h3>{{ village.name }}</h3>
{{ village.description|commonmark }}
{% if user == village.contact %}
<hr />
<a href="{% url 'villages:update' slug=village.slug %}" class="btn btn-primary">Edit</a>
<a href="{% url 'villages:delete' slug=village.slug %}" class="btn btn-danger">Delete</a>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<hr />
<button class="btn btn-primary pull-right" type="submit">Save</button>
</form>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}
Villages | {{ block.super }}
{% endblock %}
{% block content %}
<p>
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.
</p>
<p class="lead">
<a href="https://bornhack.dk/shop/?category=villages">
It is also possible to rent a tent, chairs and tables for villages here.
</a>
</p>
{% if user.is_authenticated %}
<a href="{% url 'villages:create' %}" class="btn btn-primary">Create a village</a>
{% endif %}
<hr />
<table class="table table-hover table-condensed table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Public</th>
</tr>
</thead>
<tbody>
{% for village in villages %}
<tr>
<td>
<a href="{% url 'villages:detail' slug=village.slug %}">
{{ village.name }}
</a>
</td>
<td>
{{ village.description|truncatewords:50 }}
</td>
<td>
<i class="glyphicon glyphicon-{% if village.private %}remove{% else %}ok{% endif %}"></i>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

3
villages/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
villages/urls.py Normal file
View File

@ -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<slug>[-_\w+]+)/delete/$', VillageDeleteView.as_view(), name='delete'),
url(r'(?P<slug>[-_\w+]+)/edit/$', VillageUpdateView.as_view(), name='update'),
url(r'(?P<slug>[-_\w+]+)/$', VillageDetailView.as_view(), name='detail'),
]

50
villages/views.py Normal file
View File

@ -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'