Merge pull request #80 from bornhack/multicamp

Finally merge multicamp support
This commit is contained in:
Thomas Steen Rasmussen 2017-01-25 19:38:42 +01:00 committed by GitHub
commit b72cf60419
221 changed files with 2471 additions and 2649 deletions

View file

@ -1,17 +0,0 @@
SETTINGS = bornhack.settings.development
all: migrate superuser run
migrations:
./dev.sh makemigrations --settings=$(SETTINGS)
migrate:
./dev.sh migrate --settings=$(SETTINGS)
superuser:
./dev.sh createsuperuser --settings=$(SETTINGS)
run:
./dev.sh runserver --settings=$(SETTINGS)
.PHONY = all migrations migrate superuser run

View file

@ -6,31 +6,50 @@ Django project to power Bornhack. Features include news, villages, webshop, and
### Virtualenv
Create a Python 2.7 virtual environment and activate it:
$ virtualenv venv
$ source venv/bin/activate
```
$ virtualenv venv
$ source venv/bin/activate
```
### System libraries
Install system dependencies (method depends on OS):
- postgresql headers (for psychopg2)
- postgresql headers (for psychopg2):
- Debian: ?
- FreeBSD: ?
- libjpeg (for pdf generation)
- Debian: libjpeg-dev
- FreeBSD: ?
### Python packages
Install pip packages (pick either development or production):
(venv) $ pip install -r requirements/development.txt
(venv) $ pip install -r requirements/production.txt
Install pip packages:
```
(venv) $ pip install -r requirements.txt
```
### Configuration file
Copy environment file template and change settings:
Copy environment file template and change settings as needed:
```
(venv) $ cp bornhack/settings/env.dist bornhack/settings/.env
```
### Database
Is this a new installation? Initialize the database:
(venv) $ ./manage.py migrate --settings=bornhack.settings.development
(venv) $ ./manage.py createsuperuser --settings=bornhack.settings.development
```
(venv) $ ./manage.py migrate
```
Is this for local development? Bootstrap the database with dummy data and users:
```
(venv) $ ./manage.py bootstrap-devsite
```
### Done
Is this for local development? Start the Django devserver:
(venv) $ ./manage.py runserver --settings=bornhack.settings.development
```
(venv) $ ./manage.py runserver
```
Otherwise start uwsgi or similar to serve the application.
Enjoy!

View file

@ -1,6 +1,7 @@
### set database url and allowed_hosts here
DATABASE_URL=postgres://username:password@host:port/database
ALLOWED_HOSTS=localhost,127.0.0.1
DEBUG=True
### changes below here are only needed for production
SECRET_KEY=somethingverysecretandunique
@ -29,4 +30,7 @@ BANKACCOUNT_IBAN='123'
BANKACCOUNT_SWIFTBIC='123'
BANKACCOUNT_REG='123'
BANKACCOUNT_ACCOUNT='123'
TIME_ZONE='Europe/Copenhagen'
SCHEDULE_MIDNIGHT_OFFSET_HOURS=6
SCHEDULE_TIMESLOT_LENGTH_MINUTES=30

View file

@ -42,6 +42,8 @@ INSTALLED_APPS = [
'utils',
'villages',
'program',
'info',
'sponsors',
'allauth',
'allauth.account',
@ -53,10 +55,14 @@ STATIC_ROOT = local_dir('static')
STATICFILES_DIRS = [local_dir('static_src')]
MEDIA_ROOT = env('MEDIA_ROOT')
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
TIME_ZONE = env('TIME_ZONE')
#USE_I18N = True
#USE_L10N = True
USE_TZ = True
SHORT_DATE_FORMAT = 'd/m-Y'
DATE_FORMAT = 'd/m-Y'
DATETIME_FORMAT = 'd/m-Y H:i'
TIME_FORMAT = 'H:i'
TEMPLATES = [
{
@ -69,9 +75,9 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'camps.context_processors.current_camp',
'shop.context_processors.current_order',
'shop.context_processors.user_has_tickets',
'camps.context_processors.camp',
],
},
},
@ -92,8 +98,8 @@ MIDDLEWARE_CLASSES = [
LOGIN_REDIRECT_URL = 'profiles:detail'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # Login to admin with username
'allauth.account.auth_backends.AuthenticationBackend',
'django.contrib.auth.backends.ModelBackend', # Handles login to admin with username
'allauth.account.auth_backends.AuthenticationBackend', # Handles regular logins
)
ACCOUNT_AUTHENTICATION_METHOD = 'email'
@ -127,3 +133,46 @@ BANKACCOUNT_REG = env('BANKACCOUNT_REG')
BANKACCOUNT_ACCOUNT = env('BANKACCOUNT_ACCOUNT')
TICKET_CATEGORY_ID = env('TICKET_CATEGORY_ID')
DEBUG = env('DEBUG')
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
INSTALLED_APPS += ['debug_toolbar', ]
MIDDLEWARE_CLASSES += ['debug_toolbar.middleware.DebugToolbarMiddleware', ]
else:
EMAIL_HOST = env('EMAIL_HOST')
EMAIL_PORT = env('EMAIL_PORT')
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = env('EMAIL_USE_TLS')
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL')
SERVER_EMAIL = env('DEFAULT_FROM_EMAIL')
ARCHIVE_EMAIL = env('ARCHIVE_EMAIL')
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
},
'console': {
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
# schedule settings
SCHEDULE_MIDNIGHT_OFFSET_HOURS=int(env('SCHEDULE_MIDNIGHT_OFFSET_HOURS'))
SCHEDULE_TIMESLOT_LENGTH_MINUTES=int(env('SCHEDULE_TIMESLOT_LENGTH_MINUTES'))

View file

@ -1,11 +0,0 @@
from .base import *
import environ
env = environ.Env()
environ.Env.read_env()
DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# INSTALLED_APPS += ['debug_toolbar', ]

View file

@ -1,38 +0,0 @@
from .base import *
import environ
env = environ.Env()
environ.Env.read_env()
DEBUG = False
EMAIL_HOST = env('EMAIL_HOST')
EMAIL_PORT = env('EMAIL_PORT')
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = env('EMAIL_USE_TLS')
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL')
SERVER_EMAIL = env('DEFAULT_FROM_EMAIL')
ARCHIVE_EMAIL = env('ARCHIVE_EMAIL')
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
},
'console': {
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}

View file

@ -1,132 +0,0 @@
{% extends 'base.html' %}
{% load static from staticfiles %}
{% block content %}
<div class="row">
<div class="col-md-12">
<img src="{% static 'img/logo.png' %}" class="img-responsive"
id="front-logo" />
<br />
<div class="row">
<div class="col-md-8 text-container">
<div class="lead">
<strong>Bornhack</strong> is an annual 7 day outdoor tent camping festival
where people with an interest in technology, security and society
come together to celebrate technology, socialise, learn and <strong>have fun</strong>.
</div>
</div>
<div class="col-md-4">
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150812_162831.jpg">
<img class="img-thumbnail"
src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150812_162831_tn.jpg"
alt="Tents at CCCamp2015" title="Tents at CCCamp2015">
</a>
</div>
</div>
<br />
<div class="row">
<div class="col-md-4">
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/jarlsgaard.jpg">
<img class="img-thumbnail"
src="https://people.bornhack.org/tyk/ohm_cccamp_pics/jarlsgaard_tn.jpg"
alt="Jarlsgaard from above" title="Jarlsgaard from above">
</a>
</div>
<div class="col-md-8 text-container">
<div class="lead">
The first Bornhack took place from <strong>August 27 to September 3rd 2016</strong>
on the Danish island of <strong>Bornholm</strong>. We are preparing
<strong>BornHack 2017</strong> which will be from <strong>22nd to 27th of August 2017</strong>.
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-md-8 text-container">
<p class="lead">
Bornhack is a <strong>participatory</strong> event where we expect people
to come up with ideas and projects, and help make the content. We will reach out to
people to ensure interesting, high quality talks and workshops.
</p>
</div>
<div class="col-md-4">
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/_MG_5755.jpg">
<img class="img-thumbnail"
src="https://people.bornhack.org/tyk/ohm_cccamp_pics/_MG_5755_tn.jpg"
alt="Hacking in Baconsvin village at OHM2013" title="Hacking in Baconsvin village at OHM2013">
</a>
</div>
</div>
<br />
<p class="lead">
You are very welcome to ask questions and show your interest on our different channels:
</p>
{% include 'includes/contact.html' %}
<p class="lead">
Follow and like us to get updates about BornHack.
</p>
<p class="lead">
We hope you want to be a part of Bornhack. Ticket sales for 2017 will be open soon!
</p>
<div class="regards-and-button">
<p class="lead">
Sincerely<br />
The Bornhack Team
</p>
</div>
<p align="center">
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150815_141706.jpg">
<img class="img-thumbnail"
src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150815_141706_tn.jpg"
alt="Hacking sign at CCCamp2015" title="Hacking sign at CCCamp2015">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150812_163606.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150812_163606_tn.jpg"
alt="Entrance to Baconsvin village at CCCamp2015" title="Entrance to Baconsvin village at CCCamp2015">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150812_201635.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150812_201635_tn.jpg"
alt="Dome being built at CCCamp2015" title="Dome being built at CCCamp2015">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/_MG_5746.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/_MG_5746_tn.jpg"
alt="Person swinging lights in the night at OHM2013" title="Person swinging lights in the night at OHM2013">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150814_145204.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150814_145204_tn.jpg"
alt="Queer Feminist Geeks village at CCCamp2015" title="Queer Feminist Geeks village at CCCamp2015">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150815_154027.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150815_154027_tn.jpg"
alt="Dome at CCCamp2015" title="Dome at CCCamp2015">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/_MG_5736.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/_MG_5736_tn.jpg"
alt="Lights at night at OHM2013" title="Lights at night at OHM2013">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150815_154119.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150815_154119_tn.jpg"
alt="Free Chelsea Manning sign at CCCamp2015" title="Free Chelsea Manning sign at CCCamp2015">
</a>
<a href="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150816_153404.jpg">
<img class="img-thumbnail" src="https://people.bornhack.org/tyk/ohm_cccamp_pics/IMG_20150816_153404_tn.jpg"
alt="Swedish Embassy sign at CCCamp2015" title="Swedish Embassy sign at CCCamp2015">
</a>
</p>
</div>
{% endblock %}

View file

@ -10,6 +10,11 @@ from django.conf.urls import include, url
from django.contrib import admin
from django.views.generic import TemplateView, RedirectView
from django.core.urlresolvers import reverse_lazy
from camps.views import *
from info.views import *
from villages.views import *
from program.views import *
from sponsors.views import *
urlpatterns = [
url(
@ -24,24 +29,11 @@ urlpatterns = [
r'^news/',
include('news.urls', namespace='news')
),
url(
r'^villages/',
include('villages.urls', namespace='villages')
),
url(
r'^schedule/',
include('program.urls', namespace='schedule')
),
url(
r'^$',
TemplateView.as_view(template_name='frontpage.html'),
name='frontpage'
),
url(
r'^info/',
TemplateView.as_view(template_name='info.html'),
name='info'
),
url(
r'^contact/',
TemplateView.as_view(template_name='contact.html'),
@ -52,16 +44,6 @@ urlpatterns = [
TemplateView.as_view(template_name='coc.html'),
name='conduct'
),
url(
r'^sponsors/',
TemplateView.as_view(template_name='sponsors.html'),
name='call-for-sponsors'
),
url(
r'^speakers/',
TemplateView.as_view(template_name='speakers.html'),
name='call-for-speakers'
),
url(
r'^login/$',
LoginView.as_view(),
@ -84,4 +66,105 @@ urlpatterns = [
),
url(r'^accounts/', include('allauth.urls')),
url(r'^admin/', include(admin.site.urls)),
url(
r'^camps/$',
CampListView.as_view(),
name='camp_list'
),
# camp specific urls below here
url(
r'(?P<camp_slug>[-_\w+]+)/', include([
url(
r'^$',
CampDetailView.as_view(),
name='camp_detail'
),
url(
r'^info/$',
CampInfoView.as_view(),
name='info'
),
url(
r'^schedule/', include([
url(
r'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/$',
ProgramDayView.as_view(),
name='schedule_day'
),
url(
r'^$',
ProgramOverviewView.as_view(),
name='schedule_index'
),
url(
r'^speakers/$',
SpeakerListView.as_view(),
name='speaker_index'
),
url(
r'^speakers/(?P<slug>[-_\w+]+)/$',
SpeakerDetailView.as_view(),
name='speaker_detail'
),
url(
r'^events/$',
EventListView.as_view(),
name='event_index'
),
url(
r'^call-for-speakers/$',
CallForSpeakersView.as_view(),
name='call_for_speakers'
),
url(
r'^(?P<slug>[-_\w+]+)/$',
EventDetailView.as_view(),
name='event_detail'
),
])
),
url(
r'^sponsors/$',
SponsorView.as_view(),
name='sponsors'
),
url(
r'^villages/', include([
url(
r'^$',
VillageListView.as_view(),
name='village_list'
),
url(
r'create/$',
VillageCreateView.as_view(),
name='village_create'
),
url(
r'(?P<slug>[-_\w+]+)/delete/$',
VillageDeleteView.as_view(),
name='village_delete'
),
url(
r'(?P<slug>[-_\w+]+)/edit/$',
VillageUpdateView.as_view(),
name='village_update'
),
url(
r'(?P<slug>[-_\w+]+)/$',
VillageDetailView.as_view(),
name='village_detail'
),
])
),
])
)
]

View file

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bornhack.settings.production")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bornhack.settings")
application = get_wsgi_application()

View file

@ -1,29 +1,8 @@
from django.contrib import admin
from .models import Camp, Day, Expense
from . import models
@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
@admin.register(models.Camp)
class CampModelAdmin(admin.ModelAdmin):
pass
class ExpenseInlineAdmin(admin.TabularInline):
model = Expense
@admin.register(Day)
class DayAdmin(admin.ModelAdmin):
pass
class DayInlineAdmin(admin.TabularInline):
model = Day
@admin.register(Camp)
class CampAdmin(admin.ModelAdmin):
inlines = [
DayInlineAdmin,
ExpenseInlineAdmin,
]

View file

@ -1,5 +1,23 @@
from django.conf import settings
from .models import Camp
from django.utils import timezone
def current_camp(request):
return {'current_camp': Camp.objects.current()}
def camp(request):
"""
if we have a camp_slug url component then get the "current" Camp object.
Return it after adding the slug to request.session along with a "camps"
queryset containing all camps (used to build the menu and such)
"""
if 'camp_slug' in request.resolver_match.kwargs:
camp = Camp.objects.get(slug=request.resolver_match.kwargs['camp_slug'])
request.session['campslug'] = camp.slug
else:
request.session['campslug'] = None
camp = None
return {
'camps': Camp.objects.all().order_by('-camp'),
'camp': camp
}

View file

@ -1,10 +0,0 @@
from django.utils import timezone
from django.db.models import QuerySet
class CampQuerySet(QuerySet):
def current(self):
now = timezone.now()
if self.filter(start__year=now.year).exists():
return self.get(start__year=now.year)
return None

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-12 18:03
from __future__ import unicode_literals
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('camps', '0006_auto_20160804_1705'),
]
operations = [
migrations.RemoveField(
model_name='day',
name='camp',
),
migrations.RemoveField(
model_name='camp',
name='shop_open',
),
migrations.RemoveField(
model_name='expense',
name='amount',
),
migrations.RemoveField(
model_name='expense',
name='camp',
),
migrations.RemoveField(
model_name='expense',
name='covered_by',
),
migrations.RemoveField(
model_name='expense',
name='currency',
),
migrations.AddField(
model_name='camp',
name='slug',
field=models.SlugField(default='', help_text=b'The url slug to use for this camp', verbose_name=b'Url Slug'),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='dkk_amount',
field=models.DecimalField(decimal_places=2, default=0, help_text=b'The DKK amount of the expense.', max_digits=7, verbose_name=b'DKK Amount'),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='payment_time',
field=models.DateTimeField(default=datetime.datetime(2016, 12, 12, 18, 3, 10, 378604, tzinfo=utc), help_text=b'The date and time this expense was paid.', verbose_name=b'Expense date/time'),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='receipt',
field=models.ImageField(default='', help_text=b'Upload a scan or image of the receipt', upload_to=b'', verbose_name=b'Image of receipt'),
preserve_default=False,
),
migrations.AddField(
model_name='expense',
name='refund_paid',
field=models.BooleanField(default=False, help_text=b'Has this expense been refunded to the user?', verbose_name=b'Refund paid?'),
),
migrations.AddField(
model_name='expense',
name='refund_user',
field=models.ForeignKey(blank=True, help_text=b'Which user, if any, covered this expense and should be refunded.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name=b'Refund user'),
),
migrations.AlterField(
model_name='camp',
name='end',
field=models.DateTimeField(help_text=b'When the camp ends.', verbose_name=b'End date'),
),
migrations.AlterField(
model_name='camp',
name='name',
field=models.CharField(help_text=b'Name of the camp, ie. Bornhack 2016.', max_length=255, verbose_name=b'Name'),
),
migrations.AlterField(
model_name='camp',
name='start',
field=models.DateTimeField(help_text=b'When the camp starts.', verbose_name=b'Start date'),
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-12 18:09
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('program', '0010_auto_20161212_1809'),
('camps', '0007_auto_20161212_1803'),
]
operations = [
migrations.DeleteModel(
name='Day',
),
]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 16:45
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('camps', '0008_delete_day'),
]
operations = [
migrations.RenameField(
model_name='camp',
old_name='end',
new_name='camp_end',
),
migrations.RenameField(
model_name='camp',
old_name='start',
new_name='camp_start',
),
migrations.AddField(
model_name='camp',
name='buildup_start',
field=models.DateTimeField(default=datetime.datetime(2016, 12, 20, 16, 45, 39, 609630, tzinfo=utc), help_text=b'When the camp buildup starts.', verbose_name=b'Buildup Start date'),
preserve_default=False,
),
migrations.AddField(
model_name='camp',
name='teardown_end',
field=models.DateTimeField(default=datetime.datetime(2016, 12, 20, 16, 45, 44, 532143, tzinfo=utc), help_text=b'When the camp teardown ends.', verbose_name=b'Start date'),
preserve_default=False,
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 17:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0009_auto_20161220_1645'),
]
operations = [
migrations.RemoveField(
model_name='camp',
name='name',
),
migrations.AddField(
model_name='camp',
name='tagline',
field=models.CharField(default='', help_text=b'Tagline of the camp, ie. "Initial Commit"', max_length=255, verbose_name=b'Tagline'),
preserve_default=False,
),
migrations.AddField(
model_name='camp',
name='title',
field=models.CharField(default='', help_text=b'Title of the camp, ie. Bornhack 2016.', max_length=255, verbose_name=b'Title'),
preserve_default=False,
),
]

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-28 17:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0010_auto_20161220_1714'),
]
operations = [
migrations.AddField(
model_name='camp',
name='logo_large',
field=models.CharField(default='', help_text=b'The filename of the large logo to use on the frontpage of this camp', max_length=100, verbose_name=b'Large logo for this camp'),
preserve_default=False,
),
migrations.AddField(
model_name='camp',
name='logo_small',
field=models.CharField(default='', help_text=b'The filename of the small logo to use in the top of the page for this camp', max_length=100, verbose_name=b'Small logo for this camp'),
preserve_default=False,
),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-28 23:12
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0011_auto_20161228_1750'),
]
operations = [
migrations.RemoveField(
model_name='camp',
name='logo_large',
),
migrations.RemoveField(
model_name='camp',
name='logo_small',
),
]

View file

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-29 22:01
from __future__ import unicode_literals
import django.contrib.postgres.fields.ranges
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0012_auto_20161228_2312'),
]
operations = [
migrations.RemoveField(
model_name='camp',
name='buildup_start',
),
migrations.RemoveField(
model_name='camp',
name='camp_end',
),
migrations.RemoveField(
model_name='camp',
name='camp_start',
),
migrations.RemoveField(
model_name='camp',
name='teardown_end',
),
migrations.AddField(
model_name='camp',
name='buildup',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp buildup period.', null=True, verbose_name=b'Buildup Period'),
),
migrations.AddField(
model_name='camp',
name='camp',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp period.', null=True, verbose_name=b'Camp Period'),
),
migrations.AddField(
model_name='camp',
name='teardown',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp teardown period.', null=True, verbose_name=b'Teardown period'),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-29 22:02
from __future__ import unicode_literals
import django.contrib.postgres.fields.ranges
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0013_auto_20161229_2201'),
]
operations = [
migrations.AlterField(
model_name='camp',
name='buildup',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp buildup period.', verbose_name=b'Buildup Period'),
),
migrations.AlterField(
model_name='camp',
name='camp',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp period.', verbose_name=b'Camp Period'),
),
migrations.AlterField(
model_name='camp',
name='teardown',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(help_text=b'The camp teardown period.', verbose_name=b'Teardown period'),
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-16 16:34
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0014_auto_20161229_2202'),
]
operations = [
migrations.RemoveField(
model_name='expense',
name='refund_user',
),
migrations.DeleteModel(
name='Expense',
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-16 16:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0015_auto_20170116_1634'),
]
operations = [
migrations.AddField(
model_name='camp',
name='description',
field=models.TextField(default=b'', help_text=b'Description of the camp, shown on the camp frontpage. HTML and markdown supported.', verbose_name=b'Description'),
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-16 21:59
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0016_camp_description'),
]
operations = [
migrations.RemoveField(
model_name='camp',
name='description',
),
]

14
camps/mixins.py Normal file
View file

@ -0,0 +1,14 @@
from camps.models import Camp
from django.shortcuts import get_object_or_404
class CampViewMixin(object):
def dispatch(self, request, *args, **kwargs):
self.camp = get_object_or_404(Camp, slug=self.kwargs['camp_slug'])
return super(CampViewMixin, self).dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = super(CampViewMixin, self).get_queryset()
return queryset.filter(camp=self.camp)

View file

@ -1,135 +1,158 @@
import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
from utils.models import UUIDModel, CreatedUpdatedModel
from .managers import CampQuerySet
from program.models import EventType
from django.contrib.postgres.fields import DateTimeRangeField
from psycopg2.extras import DateTimeTZRange
from django.core.exceptions import ValidationError
from datetime import timedelta
class Camp(CreatedUpdatedModel, UUIDModel):
class Meta:
verbose_name = _('Camp')
verbose_name_plural = _('Camps')
verbose_name = 'Camp'
verbose_name_plural = 'Camps'
name = models.CharField(
verbose_name=_('Name'),
help_text=_('Name of the camp, ie. Bornhack.'),
title = models.CharField(
verbose_name='Title',
help_text='Title of the camp, ie. Bornhack 2016.',
max_length=255,
)
start = models.DateTimeField(
verbose_name=_('Start date'),
help_text=_('When the camp starts.'),
unique=True,
)
end = models.DateTimeField(
verbose_name=_('End date'),
help_text=_('When the camp ends.'),
unique=True,
)
shop_open = models.BooleanField(
verbose_name=_('Shop open?'),
help_text=_('Whether the shop is open or not.'),
default=False,
)
objects = CampQuerySet.as_manager()
def __str__(self):
return _('{} {}').format(
self.name,
self.start.year,
)
def create_days(self):
delta = self.end - self.start
for day_offset in range(0, delta.days + 1):
day, created = self.days.get_or_create(
date=self.start + datetime.timedelta(days=day_offset)
)
def save(self, **kwargs):
super(Camp, self).save(**kwargs)
self.create_days()
class Day(CreatedUpdatedModel, UUIDModel):
class Meta:
verbose_name = _('Day')
verbose_name_plural = _('Days')
ordering = ['date']
camp = models.ForeignKey(
'camps.Camp',
verbose_name=_('Camp'),
help_text=_('Which camp does this day belong to.'),
related_name='days',
)
date = models.DateField(
verbose_name=_('Date'),
help_text=_('What date?')
)
def __str__(self):
return '{} ({})'.format(
self.date.strftime('%A'),
self.date
)
class Expense(CreatedUpdatedModel, UUIDModel):
class Meta:
verbose_name = _('Expense')
verbose_name_plural = _('Expenses')
camp = models.ForeignKey(
'camps.Camp',
verbose_name=_('Camp'),
help_text=_('The camp to which this expense relates to.'),
)
description = models.CharField(
verbose_name=_('Description'),
help_text=_('What this expense covers.'),
tagline = models.CharField(
verbose_name='Tagline',
help_text='Tagline of the camp, ie. "Initial Commit"',
max_length=255,
)
amount = models.DecimalField(
verbose_name=_('Amount'),
help_text=_('The amount of the expense.'),
max_digits=7,
decimal_places=2,
slug = models.SlugField(
verbose_name='Url Slug',
help_text='The url slug to use for this camp'
)
CURRENCIES = [
('btc', 'BTC'),
('dkk', 'DKK'),
('eur', 'EUR'),
('sek', 'SEK'),
]
currency = models.CharField(
verbose_name=_('Currency'),
help_text=_('What currency the amount is in.'),
choices=CURRENCIES,
max_length=3,
buildup = DateTimeRangeField(
verbose_name='Buildup Period',
help_text='The camp buildup period.',
)
covered_by = models.ForeignKey(
'auth.User',
verbose_name=_('Covered by'),
help_text=_('Which user, if any, covered this expense.'),
null=True,
blank=True,
camp = DateTimeRangeField(
verbose_name='Camp Period',
help_text='The camp period.',
)
def __str__(self):
return _('{} {} for {}').format(
self.amount,
self.get_currency_display(),
self.camp,
)
teardown = DateTimeRangeField(
verbose_name='Teardown period',
help_text='The camp teardown period.',
)
def clean(self):
''' Make sure the dates make sense - meaning no overlaps and buildup before camp before teardown '''
errors = []
# sanity checking for buildup
if self.buildup.lower > self.buildup.upper:
errors.append(ValidationError({'buildup', 'Start of buildup must be before end of buildup'}))
# sanity checking for camp
if self.camp.lower > self.camp.upper:
errors.append(ValidationError({'camp', 'Start of camp must be before end of camp'}))
# sanity checking for teardown
if self.teardown.lower > self.teardown.upper:
errors.append(ValidationError({'teardown', 'Start of teardown must be before end of teardown'}))
# check for overlaps buildup vs. camp
if self.buildup.upper > self.camp.lower:
msg = "End of buildup must not be after camp start"
errors.append(ValidationError({'buildup', msg}))
errors.append(ValidationError({'camp', msg}))
# check for overlaps camp vs. teardown
if self.camp.upper > self.teardown.lower:
msg = "End of camp must not be after teardown start"
errors.append(ValidationError({'camp', msg}))
errors.append(ValidationError({'teardown', msg}))
if errors:
raise ValidationError(errors)
def __unicode__(self):
return "%s - %s" % (self.title, self.tagline)
@property
def event_types(self):
# return all event types with at least one event in this camp
return EventType.objects.filter(event__instances__isnull=False, event__camp=self).distinct()
@property
def logo_small(self):
return 'img/%(slug)s/logo/%(slug)s-logo-small.png' % {'slug': self.slug}
@property
def logo_large(self):
return 'img/%(slug)s/logo/%(slug)s-logo-large.png' % {'slug': self.slug}
def get_days(self, camppart):
'''
Returns a list of DateTimeTZRanges representing the days during the specified part of the camp.
'''
if not hasattr(self, camppart):
print("nonexistant field/attribute")
return False
field = getattr(self, camppart)
if not hasattr(field, '__class__') or not hasattr(field.__class__, '__name__') or not field.__class__.__name__ == 'DateTimeTZRange':
print("this attribute is not a datetimetzrange field: %s" % field)
return False
daycount = (field.upper - field.lower).days
days = []
for i in range(0, daycount):
if i == 0:
# on the first day use actual start time instead of midnight
days.append(
DateTimeTZRange(
field.lower,
(field.lower+timedelta(days=i+1)).replace(hour=0)
)
)
elif i == daycount-1:
# on the last day use actual end time instead of midnight
days.append(
DateTimeTZRange(
(field.lower+timedelta(days=i)).replace(hour=0),
field.lower+timedelta(days=i+1)
)
)
else:
# neither first nor last day, goes from midnight to midnight
days.append(
DateTimeTZRange(
(field.lower+timedelta(days=i)).replace(hour=0),
(field.lower+timedelta(days=i+1)).replace(hour=0)
)
)
return days
@property
def buildup_days(self):
'''
Returns a list of DateTimeTZRanges representing the days during the buildup.
'''
return self.get_days('buildup')
@property
def camp_days(self):
'''
Returns a list of DateTimeTZRanges representing the days during the camp.
'''
return self.get_days('camp')
@property
def teardown_days(self):
'''
Returns a list of DateTimeTZRanges representing the days during the buildup.
'''
return self.get_days('teardown')

View file

@ -0,0 +1,61 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% block content %}
<div class="row">
<div class="col-md-12">
<img src="{% static camp.logo_large %}" class="img-responsive" id="front-logo" />
<br />
<div class="col-md-8 text-container">
<div class="lead">
<strong>Bornhack 2016</strong> was the first BornHack. It took place from <strong>August 27 to September 3rd 2016</strong> on the Danish island of Bornholm. The tagline of this event was <strong>Initial Commit</strong>.
</div>
</div>
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2612.jpg' 'A quiet moment in the chillout area by the bar' %}
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5128.JPG' 'Emma Holten speaking at BornHack 2016' %}
</div>
<div class="col-md-8 text-container">
<div class="lead">
With <strong>3 keynotes, 38 talks, and 9 workshops</strong> we had an ambitious program which was well received by participants. We also had an <strong>amazing list of sponsors</strong> making the event financially possible.
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-md-8 text-container">
<p class="lead">Many of the speakers and keynotes were present for the <strong>full week</strong>, providing participants with <strong>ample opportunity to discuss projects and exchange ideas</strong>.</p>
</div>
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
</div>
</div>
<br />
<div class="row">
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2631.jpg' 'The BornHack 2016 organiser team' %}
</div>
<div class="col-md-8 text-container">
<div class="lead">
The team behind BornHack 2016 had <strong>a lot of help</strong> from volunteer participants when it was needed. We are <strong>very grateful</strong> and will repay the kindness by continuing to organise awesome events.
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,61 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% block content %}
<div class="row">
<div class="col-md-12">
<img src="{% static camp.logo_large %}" class="img-responsive" id="front-logo" />
<br />
<div class="col-md-8 text-container">
<div class="lead">
<strong>Bornhack 2017</strong> will be the second BornHack. It will take place from <strong>August 22nd to August 29th 2017</strong> on the Danish island of Bornholm. The tagline of this event will be <strong>Make Tradition</strong>.
</div>
</div>
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2610.jpg' 'The family area at BornHack 2016' %}
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2631.jpg' 'The BornHack 2016 organiser team' %}
</div>
<div class="col-md-8 text-container">
<div class="lead">
The BornHack team looks forward to organising another great event for the hacker community. We <strong>still need volunteers</strong>, so please let us know if you want to help!
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-md-8 text-container">
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> and anyone else with an interest in <strong>technology and society</strong> to read our <a href="{% url 'call_for_speakers' camp_slug=camp.slug %}">call for speakers</a>.</div>
</div>
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
</div>
</div>
<br />
<div class="row">
<div class="col-md-4">
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5265.JPG' 'Organisers thanking the BornHack 2016 sponsors' %}
</div>
<div class="col-md-8 text-container">
<div class="lead">
BornHack aims to <strong>keep ticket prices affordable</strong> for everyone and to that end <strong>we need sponsors</strong>. Please see our <a href="{% url 'sponsors' camp_slug=camp.slug %}">call for sponsors</a> if you want to sponsor us, or if you work for a company you think might be able to help.
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% block content %}
<div class="row">
<div class="col-md-12">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Tagline</th>
<th>Buildup</th>
<th>Camp</th>
<th>Teardown</th>
</tr>
</thead>
<tbody>
{% for listcamp in camps %}
<tr>
<td><a href="{% url 'camp_detail' camp_slug=listcamp.slug %}">{{ listcamp.title }}</a></td>
<td>{{ listcamp.tagline }}</td>
<td>{{ listcamp.buildup.lower }} to {{ listcamp.buildup.upper }}</td>
<td>{{ listcamp.camp.lower }} to {{ listcamp.camp.upper }}</td>
<td>{{ listcamp.teardown.lower }} to {{ listcamp.teardown.upper }}</td>
</tr>
</a>
{% empty %}
<h3>No camps found, what gives?!</h3>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
$("table").on("click", "tr", function(e) {
if ($(e.target).is("a,input")) // anything else you don't want to trigger the click
return;
location.href = $(this).find("a").attr("href");
});
</script>
{% endblock content %}

View file

@ -1,4 +0,0 @@
<form method="POST">
{{ form }}
<button type="submit">Submit</button>
</form>

View file

@ -1,3 +0,0 @@
{% for camp in camps %}
{{ camp }}<br />
{% endfor %}

View file

@ -1,3 +1,17 @@
from . import models
from django.views.generic import ListView, DetailView
from django.utils import timezone
from .models import *
class CampDetailView(DetailView):
model = Camp
slug_url_kwarg = 'camp_slug'
def get_template_names(self):
return 'camp_detail_%s.html' % self.get_object().slug
class CampListView(ListView):
model = Camp
template_name = 'camp_list.html'

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 154 KiB

6
info/admin.py Normal file
View file

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import *
admin.site.register(InfoCategory)
admin.site.register(InfoItem)

7
info/apps.py Normal file
View file

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

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-24 22:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('camps', '0010_auto_20161220_1714'),
]
operations = [
migrations.CreateModel(
name='InfoCategory',
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)),
('headline', models.CharField(help_text='The headline of this info category', max_length=100)),
('anchor', models.SlugField(help_text='The HTML anchor to use for this info category.')),
('weight', models.PositiveIntegerField(help_text='Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically.')),
('camp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='infocategories', to='camps.Camp')),
],
options={
'ordering': ['-weight', 'headline'],
},
),
migrations.CreateModel(
name='InfoItem',
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)),
('headline', models.CharField(help_text='Headline of this info item.', max_length=100)),
('anchor', models.SlugField(help_text='The HTML anchor to use for this info item.')),
('body', models.TextField(help_text='Body of this info item. Markdown is supported.')),
('weight', models.PositiveIntegerField(help_text='Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically.')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='infoitems', to='info.InfoCategory')),
],
options={
'ordering': ['-weight', 'headline'],
},
),
migrations.AlterUniqueTogether(
name='infoitem',
unique_together=set([('headline', 'category'), ('anchor', 'category')]),
),
migrations.AlterUniqueTogether(
name='infocategory',
unique_together=set([('headline', 'camp'), ('anchor', 'camp')]),
),
]

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-28 23:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('info', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='infocategory',
options={'ordering': ['-weight', 'headline'], 'verbose_name_plural': 'Info Categories'},
),
migrations.AlterField(
model_name='infocategory',
name='weight',
field=models.PositiveIntegerField(default=100, help_text='Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically. Defaults to 100.'),
),
migrations.AlterField(
model_name='infoitem',
name='weight',
field=models.PositiveIntegerField(default=100, help_text='Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically. Defaults to 100.'),
),
]

View file

80
info/models.py Normal file
View file

@ -0,0 +1,80 @@
from __future__ import unicode_literals
from django.contrib import messages
from django.db import models
from utils.models import CreatedUpdatedModel
from django.core.exceptions import ValidationError
class InfoCategory(CreatedUpdatedModel):
class Meta:
ordering = ['-weight', 'headline']
unique_together = (('anchor', 'camp'), ('headline', 'camp'))
verbose_name_plural = "Info Categories"
camp = models.ForeignKey(
'camps.Camp',
related_name = 'infocategories',
on_delete = models.PROTECT
)
headline = models.CharField(
max_length = 100,
help_text = "The headline of this info category"
)
anchor = models.SlugField(
help_text = "The HTML anchor to use for this info category."
)
weight = models.PositiveIntegerField(
help_text = 'Determines sorting/ordering. Heavier categories sink to the bottom. Categories with the same weight are ordered alphabetically. Defaults to 100.',
default = 100,
)
def clean(self):
if InfoItem.objects.filter(category__camp=self.camp, anchor=self.anchor).exists():
# this anchor is already in use on an item, so it cannot be used (must be unique on the page)
raise ValidationError({'anchor': 'Anchor is already in use on an info item for this camp'})
def __str__(self):
return '%s (%s)' % (self.headline, self.camp)
class InfoItem(CreatedUpdatedModel):
class Meta:
ordering = ['-weight', 'headline']
unique_together = (('anchor', 'category'), ('headline', 'category'))
category = models.ForeignKey(
'info.InfoCategory',
related_name = 'infoitems',
on_delete = models.PROTECT
)
headline = models.CharField(
max_length = 100,
help_text = "Headline of this info item."
)
anchor = models.SlugField(
help_text = "The HTML anchor to use for this info item."
)
body = models.TextField(
help_text = 'Body of this info item. Markdown is supported.'
)
weight = models.PositiveIntegerField(
help_text = 'Determines sorting/ordering. Heavier items sink to the bottom. Items with the same weight are ordered alphabetically. Defaults to 100.',
default = 100,
)
def clean(self):
if InfoCategory.objects.filter(camp=self.category.camp, anchor=self.anchor).exists():
# this anchor is already in use on a category, so it cannot be used here (they must be unique on the entire page)
raise ValidationError({'anchor': 'Anchor is already in use on an info category for this camp'})
def __str__(self):
return '%s (%s)' % (self.headline, self.category)

70
info/templates/info.html Normal file
View file

@ -0,0 +1,70 @@
{% 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 %}
{% block content %}
<style>
.anchor{
display: block;
height: 80px; /*same height as header*/
margin-top: -80px; /*same height as header*/
visibility: hidden;
}
.sub-anchor{
display: block;
height: 94px; /*same height as header*/
margin-top: -94px; /*same height as header*/
visibility: hidden;
}
</style>
{% if categories %}
<div class="row">
<div class="col-md-12">
<h3>Table of Contents</h3>
<p class="list-group">
{% for category in categories %}
<a href="#{{ category.anchor }}" class="list-group-item">{{ category.headline }}</a>
{% endfor %}
</p>
</div>
</div>
{% for category in categories %}
<span class="anchor" id="{{ category.anchor }}"></span>
<div class="row">
<div class="col-md-12">
<h2>{{ category.title }}</h2>
<div class="panel-group">
{% for item in category.infoitems.all %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="sub-anchor" id="{{ item.anchor }}"></span>
<h4 class="panel-title">{{ item.headline }}
<a href="#{{ item.anchor }}">
<i class="glyphicon glyphicon-link"></i>
</a>
</h4>
</div>
<div class="panel-body">
<p>{{ item.body }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<h3>No info found for {{ camp.title }}</h3>
{% endif %}
{% endblock %}

16
info/views.py Normal file
View file

@ -0,0 +1,16 @@
from django.shortcuts import render
from django.views.generic import ListView, DetailView
from django.utils import timezone
from .models import *
from camps.mixins import CampViewMixin
class CampInfoView(CampViewMixin, ListView):
model = InfoCategory
template_name = 'info.html'
context_object_name = 'categories'
def get_queryset(self):
queryset = super(CampInfoView, self).get_queryset()
# do not show categories with 0 items
return queryset.exclude(infoitems__isnull=True)

View file

@ -5,7 +5,7 @@ import sys
if __name__ == "__main__":
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
"bornhack.settings.production"
"bornhack.settings"
)
from django.core.management import execute_from_command_line

View file

@ -5,5 +5,15 @@ from . import models
@admin.register(models.NewsItem)
class NewsItemModelAdmin(admin.ModelAdmin):
list_display = ['title', 'public', 'published_at']
list_filter = ['public']
list_display = ['title', 'published_at', 'archived']
actions = ['archive_news_items', 'unarchive_news_items']
def archive_news_items(self, request, queryset):
queryset.filter(archived=False).update(archived=True)
archive_news_items.description = 'Mark newsitem(s) as archived'
def unarchive_news_items(self, request, queryset):
queryset.filter(archived=True).update(archived=False)
unarchive_news_items.description = 'Mark newsitem(s) as not archived'

View file

@ -1,11 +0,0 @@
from django.db.models import QuerySet
from django.utils import timezone
class NewsItemQuerySet(QuerySet):
def public(self):
return self.filter(
public=True,
published_at__lt=timezone.now()
)

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 11:27
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0005_auto_20160618_1902'),
]
operations = [
migrations.RemoveField(
model_name='newsitem',
name='public',
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 11:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0006_remove_newsitem_public'),
]
operations = [
migrations.AlterField(
model_name='newsitem',
name='published_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-20 13:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0007_auto_20161220_1136'),
]
operations = [
migrations.AddField(
model_name='newsitem',
name='archived',
field=models.BooleanField(default=False),
),
]

View file

@ -5,7 +5,6 @@ from django.utils import encoding
from django.utils.text import slugify
from utils.models import CreatedUpdatedModel
from news.managers import NewsItemQuerySet
@encoding.python_2_unicode_compatible
@ -15,36 +14,33 @@ class NewsItem(CreatedUpdatedModel):
title = models.CharField(max_length=100)
content = models.TextField()
public = models.BooleanField(default=False)
published_at = models.DateTimeField()
published_at = models.DateTimeField(null=True, blank=True)
slug = models.SlugField(max_length=255, blank=True)
archived = models.BooleanField(default=False)
def __str__(self):
return self.title
def save(self, **kwargs):
if (
not self.pk or
not self.slug or
NewsItem.objects.filter(slug=self.slug).count() > 1
):
published_at_string = self.published_at.strftime('%Y-%m-%d')
base_slug = slugify(self.title)
slug = '{}-{}'.format(published_at_string, base_slug)
incrementer = 1
if self.published_at:
# if this is a new newsitem, or it doesn't have a slug, or the slug is in use on another item, create a new slug
if (not self.pk or not self.slug or NewsItem.objects.filter(slug=self.slug).count() > 1):
published_at_string = self.published_at.strftime('%Y-%m-%d')
base_slug = slugify(self.title)
slug = '{}-{}'.format(published_at_string, base_slug)
incrementer = 1
# We have to make sure that the slug won't clash with current slugs
while NewsItem.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
# We have to make sure that the slug won't clash with current slugs
while NewsItem.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
super(NewsItem, self).save(**kwargs)
objects = NewsItemQuerySet.as_manager()

View file

@ -6,6 +6,9 @@ News | {{ block.super }}
{% endblock %}
{% block content %}
{% if request.resolver_match.kwargs.archived %}
<i>Showing archived news items. <a href="{% url 'news:index' archived=False %}">Show regular news items</a></i>
{% endif %}
{% for item in news_items %}
<div>
<h3><a href="{% url 'news:detail' slug=item.slug %}">{{ item.title }}</a> <small>{{ item.published_at|date:"Y-m-d" }}</small></h3>
@ -17,4 +20,9 @@ News | {{ block.super }}
{% empty %}
<h3>No news yet. Stay tuned!</h3>
{% endfor %}
{% if not request.resolver_match.kwargs.archived %}
<hr />
<i><a href="{% url 'news:archive' archived=True %}">Show archived news items</a></i>
{% endif %}
{% endblock %}

View file

@ -3,6 +3,8 @@ from . import views
urlpatterns = [
url(r'^$', views.NewsIndex.as_view(), name='index'),
url(r'^$', views.NewsIndex.as_view(), kwargs={'archived': False}, name='index'),
url(r'^archive/$', views.NewsIndex.as_view(), kwargs={'archived': True}, name='archive'),
url(r'(?P<slug>[-_\w+]+)/$', views.NewsDetail.as_view(), name='detail'),
]

View file

@ -1,33 +1,22 @@
from django.views.generic import ListView, DetailView
from django.utils import timezone
from . import models
from .models import *
class NewsIndex(ListView):
model = models.NewsItem
model = NewsItem
template_name = 'news_index.html'
context_object_name = 'news_items'
def get_queryset(self):
return self.model.objects.public()
return NewsItem.objects.filter(
published_at__isnull=False,
published_at__lt=timezone.now(),
archived=self.kwargs['archived']
)
class NewsDetail(DetailView):
model = models.NewsItem
model = NewsItem
template_name = 'news_detail.html'
context_object_name = 'news_item'
def get_context_data(self, **kwargs):
context = super(NewsDetail, self).get_context_data(**kwargs)
news_item = self.get_object()
timed = news_item.published_at > timezone.now()
if news_item.public and timed:
context['not_public'] = True
context['timed'] = True
elif not news_item.public:
context['not_public'] = True
context['timed'] = False
return context

View file

@ -30,3 +30,4 @@ class Profile(CreatedUpdatedModel, UUIDModel):
def create_profile(sender, created, instance, **kwargs):
if created:
Profile.objects.create(user=instance)

View file

@ -0,0 +1,2 @@
default_app_config = 'program.apps.ProgramConfig'

View file

@ -1,8 +1,12 @@
from django.contrib import admin
from .models import Event, Speaker, EventType
from .models import Event, Speaker, EventType, EventInstance
@admin.register(EventInstance)
class EventInstanceAdmin(admin.ModelAdmin):
pass
@admin.register(EventType)
class EventTypeAdmin(admin.ModelAdmin):
pass
@ -22,17 +26,8 @@ class EventAdmin(admin.ModelAdmin):
list_display = [
'title',
'event_type',
'get_days',
'start',
'end',
]
def get_days(self, obj):
return ', '.join([
str(day.date.strftime('%a'))
for day in obj.days.all()
])
inlines = [
SpeakerInline
]

View file

@ -1,7 +1,12 @@
from __future__ import unicode_literals
from django.apps import AppConfig
from django.db.models.signals import m2m_changed, pre_save
from .signal_handlers import check_speaker_event_camp_consistency, check_speaker_camp_change
class ProgramConfig(AppConfig):
name = 'program'
def ready(self):
from .models import Speaker
m2m_changed.connect(check_speaker_event_camp_consistency, sender=Speaker.events.through)
pre_save.connect(check_speaker_camp_change, sender=Speaker)

View file

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-12 18:09
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('camps', '0007_auto_20161212_1803'),
('program', '0009_auto_20160827_0752'),
]
operations = [
migrations.CreateModel(
name='EventInstance',
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)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
],
options={
'ordering': ['start'],
},
),
migrations.RemoveField(
model_name='event',
name='days',
),
migrations.RemoveField(
model_name='event',
name='end',
),
migrations.RemoveField(
model_name='event',
name='start',
),
migrations.AddField(
model_name='event',
name='camp',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='camps.Camp'),
),
migrations.AddField(
model_name='eventinstance',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='program.Event'),
),
]

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-29 21:49
from __future__ import unicode_literals
import django.contrib.postgres.fields.ranges
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('program', '0010_auto_20161212_1809'),
]
operations = [
migrations.AlterModelOptions(
name='eventinstance',
options={'ordering': ['when']},
),
migrations.RemoveField(
model_name='eventinstance',
name='end',
),
migrations.RemoveField(
model_name='eventinstance',
name='start',
),
migrations.AddField(
model_name='eventinstance',
name='when',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(null=True),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-29 21:50
from __future__ import unicode_literals
import django.contrib.postgres.fields.ranges
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('program', '0011_auto_20161229_2149'),
]
operations = [
migrations.AlterField(
model_name='eventinstance',
name='when',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-21 12:12
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('program', '0012_auto_20161229_2150'),
]
operations = [
migrations.AlterField(
model_name='event',
name='camp',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='camps.Camp'),
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-22 13:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('camps', '0017_remove_camp_description'),
('program', '0013_auto_20170121_1312'),
]
operations = [
migrations.AddField(
model_name='speaker',
name='camp',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='speakers', to='camps.Camp'),
),
]

View file

@ -1,9 +1,11 @@
from __future__ import unicode_literals
from django.contrib.postgres.fields import DateTimeRangeField
from django.db import models
from django.utils.text import slugify
from django.conf import settings
from utils.models import CreatedUpdatedModel
from django.core.exceptions import ValidationError
from datetime import timedelta
class EventType(CreatedUpdatedModel):
@ -18,33 +20,83 @@ class EventType(CreatedUpdatedModel):
class Event(CreatedUpdatedModel):
""" Something that is on the program. """
""" Something that is on the program one or more times. """
title = models.CharField(max_length=255)
slug = models.SlugField(blank=True, max_length=255)
abstract = models.TextField()
event_type = models.ForeignKey(EventType)
days = models.ManyToManyField('camps.Day', blank=True)
start = models.TimeField(null=True, blank=True)
end = models.TimeField(null=True, blank=True)
camp = models.ForeignKey('camps.Camp', null=True, related_name="events")
class Meta:
ordering = ['title']
def __unicode__(self):
return self.title
return '%s (%s)' % (self.title, self.camp.title)
def save(self, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Event, self).save(**kwargs)
@property
def speakers_list(self):
if self.speakers.exists():
return ", ".join(self.speakers.all().values_list('name', flat=True))
return False
class EventInstance(CreatedUpdatedModel):
""" An instance of an event """
event = models.ForeignKey('program.event', related_name='instances')
when = DateTimeRangeField()
class Meta:
ordering = ['when']
def __unicode__(self):
return '%s (%s)' % (self.event, self.when)
def __clean__(self):
errors = []
if self.when.lower > self.when.upper:
errors.append(ValidationError({'when', "Start should be earlier than finish"}))
if self.when.lower.time().minute != 0 and self.when.lower.time().minute != 30:
errors.append(ValidationError({'when', "Start time minute should be 0 or 30."}))
if self.when.upper.time().minute != 0 and self.when.upper.time().minute != 30:
errors.append(ValidationError({'when', "End time minute should be 0 or 30."}))
if errors:
raise ValidationError(errors)
@property
def schedule_date(self):
"""
Returns the schedule date of this eventinstance. Schedule date is determined by substracting
settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS from the eventinstance start time. This means that if
an event is scheduled for 00:30 wednesday evening (technically thursday) then the date
after substracting 5 hours would be wednesdays date, not thursdays.
"""
return (self.when.lower-timedelta(hours=settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS)).date()
@property
def timeslots(self):
"""
Find the number of timeslots this eventinstance takes up
"""
seconds = (self.when.upper-self.when.lower).seconds
minutes = seconds / 60
return minutes / settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES
class Speaker(CreatedUpdatedModel):
""" Person anchoring an event. """
""" A Person anchoring an event. """
name = models.CharField(max_length=150)
biography = models.TextField()
picture = models.ImageField(null=True, blank=True)
slug = models.SlugField(blank=True, max_length=255)
camp = models.ForeignKey('camps.Camp', null=True, related_name="speakers")
events = models.ManyToManyField(
Event,
related_name='speakers',
@ -56,7 +108,7 @@ class Speaker(CreatedUpdatedModel):
ordering = ['name']
def __unicode__(self):
return self.name
return '%s (%s)' % (self.name, self.camp)
def save(self, **kwargs):
if not self.slug:

View file

@ -0,0 +1,16 @@
from django.core.exceptions import ValidationError
def check_speaker_event_camp_consistency(sender, instance, **kwargs):
if kwargs['action'] == 'pre_add':
for pk in kwargs['pk_set']:
# check if this event belongs to a different event than the speaker does
from program.models import Event
event = Event.objects.get(id=pk)
if event.camp != instance.camp:
raise ValidationError({'events': 'One or more events belong to a different camp (%s) than the speaker (%s) does' % (event.camp, instance.camp)})
def check_speaker_camp_change(sender, instance, **kwargs):
for event in instance.events.all():
if event.camp != instance.camp:
raise ValidationError({'camp': 'You cannot change the camp a speaker belongs to if the speaker is associated with one or more events.'})

View file

@ -9,7 +9,7 @@
<div class="list-group">
{% for event in event_list %}
{% if event.event_type.name != "Facilities" %}
<a href="{% url 'schedule:event' slug=event.slug %}" class="list-group-item">
<a href="{% url 'event_detail' camp_slug=camp.slug slug=event.slug %}" class="list-group-item">
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }}
</small>

View file

@ -1,19 +1,49 @@
{% extends 'schedule_base.html' %}
{% block schedule_content %}
<div class="row">
<div class="btn-group btn-group-justified">
<a href="{% url 'schedule_index' camp_slug=camp.slug %}{% if eventtype %}?type={{ eventtype.slug }}{% endif %}" class="btn {% if urlyear %}btn-default{% else %}btn-primary{% endif %}">Overview</a>
{% for day in camp.camp_days %}
{% with day.lower.date|date:"m" as month_padded %}
{% with day.lower.date|date:"d" as day_padded %}
<a href="{% url 'schedule_day' camp_slug=camp.slug year=day.lower.date.year month=month_padded day=day_padded %}{% if eventtype %}?type={{ eventtype.slug }}{% endif %}" class="btn btn-sm {% if urlyear and urlyear|add:"0" == day.lower.date.year and urlmonth == month_padded and urlday == day_padded %}btn-primary{% else %}btn-default{% endif %}">
{{ day.lower.date|date:"l" }}
</a>
{% endwith %}
{% endwith %}
{% endfor %}
</div>
</div>
<a href="{% url 'schedule:index' %}" class="btn btn-default" style="display: inline-block; padding: 5px;">
Overview
</a>
{% for day in days %}
{% with day.date|date:"m" as month_padded %}
{% with day.date|date:"d" as day_padded %}
<a href="{% url 'schedule:day' year=day.date.year month=month_padded day=day_padded %}" class="btn btn-default" style="display: inline-block; padding: 5px;">
{{ day.date|date:"l" }}
</a>
{% endwith %}
{% endwith %}
{% endfor %}
<p>
<div class="row">
<div class="btn-group btn-group-justified">
{% if not urlyear %}
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" style="background-color: black; border: 0; color: white;" class="btn">
{% if eventtype %}All{% else %}<b>All</b>{% endif %}
</a>
{% else %}
<a href="{% url 'schedule_day' camp_slug=camp.slug year=urlyear month=urlmonth day=urlday %}" style="background-color: black; border: 0; color: white;" class="btn">
{% if eventtype %}All{% else %}<b>All</b>{% endif %}
</a>
{% endif %}
{% for event_type in camp.event_types %}
{% if not urlyear %}
<a href="{% url 'schedule_index' camp_slug=camp.slug %}?type={{ event_type.slug }}" style="background-color: {{ event_type.color }}; border: 0; color: {% if event_type.light_text %}white{% else %}black{% endif %};" class="btn">
{% if eventtype and eventtype == event_type %}<b>{{ event_type.name }}</b>{% else %}{{ event_type.name }}{% endif %}
</a>
{% else %}
<a href="{% url 'schedule_day' camp_slug=camp.slug year=urlyear month=urlmonth day=urlday %}?type={{ event_type.slug }}" style="background-color: {{ event_type.color }}; border: 0; color: {% if event_type.light_text %}white{% else %}black{% endif %};" class="btn">
{% if eventtype and eventtype == event_type %}<b>{{ event_type.name }}</b>{% else %}{{ event_type.name }}{% endif %}
</a>
{% endif %}
{% endfor %}
</div>
</div>
<hr />

View file

@ -3,17 +3,14 @@
{% block program_content %}
<h2>{{ date|date:"l, F jS" }}</h2>
{% for event in events %}
{% ifchanged event.event_type %}
{% if not forloop.first %}</div>{% endif %}
<h3>{{ event.event_type }}</h3>
<div style="display: flex; flex-wrap: wrap;">
{% endifchanged %}
<a class="event"
href="{% url 'schedule:event' slug=event.slug %}"
href="{% url 'event_detail' camp_slut=camp.slug slug=event.slug %}"
style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %};">
<small>{{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }}</small>
<br />
@ -25,6 +22,27 @@
</a>
{% endfor %}
</div>
<table>
<tbody>
<table>
{% for timeslot in timeslots %}
<tr>
<td style="height: 50px; padding: 5px;">{{ timeslot.time }}</td>
{% for eventinstance in eventinstances %}
{% if eventinstance.when.lower.time == timeslot.time %}
<td style="background-color: {{ eventinstance.event.event_type.color }}; color: {% if event.event_type.light_text %}white{% else %}black{% endif %};" class="event" rowspan={{ eventinstance.timeslots }}>
<a style="color:inherit;" href="{% url 'event_detail' camp_slug=camp.slug slug=eventinstance.event.slug %}">
{{ eventinstance.event.title }}<br>
{{ eventinstance.when.lower.time }}-{{ eventinstance.when.upper.time }}
</a>
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
</tbody>
</table>
{% endblock program_content %}

View file

@ -2,35 +2,32 @@
{% load commonmark %}
{% block schedule_content %}
<div class="panel panel-default">
<div class="panel-heading" ><span style="font-size: x-large"><span style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">{{ event.event_type.name }}</span> {{ event.title }}</span></div>
<div class="panel-body">
<p>
{{ event.abstract|commonmark }}
{% if event.speakers.exists %}
<hr>
{% for speaker in event.speakers.all %}
<h4><a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}">{{ speaker }}</a></h4>
{{ speaker.biography|commonmark }}
{% endfor %}
{% endif %}
</p>
<h3>
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }}
</small>
{{ event.title }}
</h3>
<h4>
{% if event.start and event.end and event.days.all.exists %}
{{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }} on
{% for day in event.days.all %}{{ day.date|date:"l" }}{% if not forloop.last %}, {% endif %}{% endfor %}<br />
{% else %}
Not scheduled yet
{% endif %}
</h4>
{{ event.abstract|commonmark }}
<hr />
{% if event.speakers.exists %}
{% for speaker in event.speakers.all %}
<h3><a href="{% url 'schedule:speaker_detail' slug=speaker.slug %}">{{ speaker }}</a></h3>
{{ speaker.biography|commonmark }}
{% endfor %}
{% endif %}
<hr>
<h4>Instances</h4>
<ul class="list-group">
{% for ei in event.instances.all %}
<li class="list-group-item">{{ ei.when.lower|date:"l M. d H:i" }} - {{ ei.when.upper|date:"H:i" }}</li>
{% empty %}
No instances scheduled yet
{% endfor %}
</ul>
</h4>
</div>
</div>
{% endblock schedule_content %}

View file

@ -1,36 +1,28 @@
{% extends 'program_base.html' %}
{% block program_content %}
<a href="{% url 'schedule:index' %}" style="background-color: black; border: 0; color: white; display: inline-block; padding: 5px;">
All
</a>
{% for event_type in event_types %}
<a href="{% url 'schedule:index' %}?type={{ event_type.slug }}" style="background-color: {{ event_type.color }}; border: 0; color: {% if event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event_type.name }}
</a>
{% endfor %}
<hr />
{% for day, events in day_events.items %}
{{ day.date|date:"D d/m" }} <br />
{% for day in camp.camp_days %}
{{ day.lower.date|date:"D d/m" }} <br />
<div style="display: flex; flex-wrap: wrap;">
{% for event in events %}
{% for event in camp.events.all %}
{% for eventinstance in event.instances.all %}
{% if eventinstance.schedule_date == day.lower.date %}
{% if not eventtype or eventtype == eventinstance.event.event_type %}
<a class="event"
href="{% url 'schedule:event' slug=event.slug %}"
style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %};">
<small>{{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }}</small>
href="{% url 'event_detail' camp_slug=camp.slug slug=eventinstance.event.slug %}"
style="background-color: {{ eventinstance.event.event_type.color }}; border: 0; color: {% if eveninstance.event.event_type.light_text %}white{% else %}black{% endif %};">
<small>{{ eventinstance.when.lower|date:"H:i" }} - {{ eventinstance.when.upper|date:"H:i" }}</small>
<br />
{{ event }}
<br />
{% if event.speakers.exists %}
<i>by {{ event.speakers.all|join:", " }}
{% endif %}</i>
{% if event.speakers.exists %}<i>by {{ event.speakers_list }}{% endif %}</i>
</a>
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
</div>
<hr />
{% endfor %}
{% endfor %}
{% endblock program_content %}

View file

@ -1,15 +1,16 @@
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="btn-group btn-group-justified">
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" class="btn {% if request.resolver_match.url_name == "schedule_index" or urlyear %}btn-primary{% else %}btn-default{% endif %}">Schedule</a>
<a href="{% url 'call_for_speakers' camp_slug=camp.slug %}" class="btn {% if request.resolver_match.url_name == "call_for_speakers" %}btn-primary{% else %}btn-default{% endif %}">Call for Speakers</a>
<a href="{% url 'speaker_index' camp_slug=camp.slug %}" class="btn {% if request.resolver_match.url_name == "speaker_index" %}btn-primary{% else %}btn-default{% endif %}">Speakers</a>
<a href="{% url 'event_index' camp_slug=camp.slug %}" class="btn {% if request.resolver_match.url_name == "event_index" %}btn-primary{% else %}btn-default{% endif %}">Talks &amp; Events</a>
</div>
</div>
<p>
<a href="{% url 'schedule:index' %}" class="btn btn-default">Schedule</a>
<a href="{% url 'call-for-speakers' %}" class="btn btn-default">Call for Speakers</a>
<a href="{% url 'schedule:speaker_index' %}" class="btn btn-default">Speakers</a>
<a href="{% url 'schedule:event_index' %}" class="btn btn-default">Talks &amp; Events</a>
</p>
<hr />
{% block schedule_content %}
{% endblock schedule_content %}

View file

@ -9,23 +9,27 @@
<hr />
{% if speaker.events.exists %}
{% for event in speaker.events.all %}
{% for event in speaker.events.all %}
<h3>
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }}
</small>
<a href="{% url 'event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a>
</h3>
{{ event.abstract|commonmark }}
<h3>
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }}
</small><br>
<a href="{% url 'schedule:event' slug=event.slug %}">{{ event.title }}</a></h3>
{{ event.abstract|commonmark }}
{% if event.start and event.end and event.days.all.exists %}
At {{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }} on
{% for day in event.days.all %}{{ day.date|date:"l" }}{% if not forloop.last %}, {% endif %}{% endfor %}<br />
{% else %}
Not scheduled yet
<h4>Instances</h4>
<ul class="list-group">
{% for ei in event.instances.all %}
<li class="list-group-item">{{ ei.when.lower|date:"l M. d H:i" }} - {{ ei.when.upper|date:"H:i" }}</li>
{% empty %}
No instances scheduled yet
{% endfor %}
</ul>
</h4>
<hr>
{% empty %}
No events registered for this speaker yet
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% endblock schedule_content %}

View file

@ -8,7 +8,7 @@
<div class="list-group">
{% for speaker in speaker_list %}
<a href="{% url 'schedule:speaker_detail' slug=speaker.slug %}" class="list-group-item">
<a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item">
{{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }})
</a>
{% endfor %}

View file

@ -1,11 +0,0 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/$', views.ProgramDayView.as_view(), name='day'),
url(r'^$', views.ProgramOverviewView.as_view(), name='index'),
url(r'^speakers/$', views.SpeakerListView.as_view(), name='speaker_index'),
url(r'^speakers/(?P<slug>[-_\w+]+)/$', views.SpeakerDetailView.as_view(), name='speaker_detail'),
url(r'^events/$', views.EventListView.as_view(), name='event_index'),
url(r'^(?P<slug>[-_\w+]+)/$', views.EventDetailView.as_view(), name='event'),
]

View file

@ -1,82 +1,111 @@
from collections import OrderedDict
import datetime
from django.views.generic import ListView, TemplateView, DetailView
from camps.models import Day
from camps.mixins import CampViewMixin
from . import models
from django.http import Http404
import datetime
from django.conf import settings
class SpeakerDetailView(DetailView):
class SpeakerDetailView(CampViewMixin, DetailView):
model = models.Speaker
template_name = 'speaker_detail.html'
class SpeakerListView(ListView):
class SpeakerListView(CampViewMixin, ListView):
model = models.Speaker
template_name = 'speaker_list.html'
class EventListView(ListView):
def get_queryset(self, *args, **kwargs):
return models.Speaker.objects.filter(camp=self.camp)
class EventListView(CampViewMixin, ListView):
model = models.Event
template_name = 'event_list.html'
class ProgramOverviewView(ListView):
class ProgramOverviewView(CampViewMixin, ListView):
model = models.Event
template_name = 'program_overview.html'
def get_context_data(self, **kwargs):
context = super(
ProgramOverviewView, self
).get_context_data(**kwargs)
days = Day.objects.all()
context['days'] = days
filter = {}
def dispatch(self, *args, **kwargs):
""" If an event type has been supplied check if it is valid """
if 'type' in self.request.GET:
event_type = self.request.GET['type']
filter["event_type__slug"] = event_type
context['day_events'] = OrderedDict([
(
day,
self.get_queryset().filter(
days__in=[day],
**filter
).order_by(
'start'
try:
eventtype = models.EventType.objects.get(
slug=self.request.GET['type']
)
)
for day in days
])
context['event_types'] = models.EventType.objects.all()
except models.EventType.DoesNotExist:
raise Http404
return super(ProgramOverviewView, self).dispatch(*args, **kwargs)
def get_context_data(self, *args, **kwargs):
context = super(ProgramOverviewView, self).get_context_data(**kwargs)
if 'type' in self.request.GET:
context['eventtype'] = models.EventType.objects.get(slug=self.request.GET['type'])
return context
class ProgramDayView(TemplateView):
class ProgramDayView(CampViewMixin, TemplateView):
template_name = 'program_day.html'
def dispatch(self, *args, **kwargs):
""" If an event type has been supplied check if it is valid """
if 'type' in self.request.GET:
try:
eventtype = models.EventType.objects.get(
slug=self.request.GET['type']
)
except models.EventType.DoesNotExist:
raise Http404
return super(ProgramDayView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
def get_context_data(self, *args, **kwargs):
context = super(ProgramDayView, self).get_context_data(**kwargs)
year = int(kwargs['year'])
month = int(kwargs['month'])
day = int(kwargs['day'])
date = datetime.date(year=year, month=month, day=day)
day = Day.objects.filter(date=date)
context['date'] = date
context['events'] = models.Event.objects.filter(days=day).order_by('event_type', 'start')
context['event_types'] = models.EventType.objects.all()
context['days'] = Day.objects.filter(date__year=year)
when = datetime.datetime(year=int(self.kwargs['year']), month=int(self.kwargs['month']), day=int(self.kwargs['day']))
eventinstances = models.EventInstance.objects.filter(event__in=self.camp.events.all())
skip = []
for ei in eventinstances:
if ei.schedule_date != when.date():
skip.append(ei.id)
else:
if 'type' in self.request.GET:
eventtype = models.EventType.objects.get(
slug=self.request.GET['type']
)
if ei.event.event_type != eventtype:
skip.append(ei.id)
context['eventinstances'] = eventinstances.exclude(id__in=skip).order_by('event__event_type')
start = when + datetime.timedelta(hours=settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS)
timeslots = []
# calculate how many timeslots we have in the schedule based on the lenght of the timeslots in minutes,
# and the number of minutes in 24 hours
for i in range(0,(24*60)/settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES):
timeslot = start + datetime.timedelta(minutes=i*settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES)
timeslots.append(timeslot)
context['timeslots'] = timeslots
# include the components to make the urls
context['urlyear'] = self.kwargs['year']
context['urlmonth'] = self.kwargs['month']
context['urlday'] = self.kwargs['day']
if 'type' in self.request.GET:
context['eventtype'] = models.EventType.objects.get(slug=self.request.GET['type'])
return context
class EventDetailView(DetailView):
class EventDetailView(CampViewMixin, DetailView):
model = models.Event
template_name = 'program_event_detail.html'
def get_context_data(self, **kwargs):
context = super(EventDetailView, self).get_context_data(**kwargs)
# TODO: date__year is hardcoded here - need fix for 2017 :P
context['days'] = Day.objects.filter(date__year=2016)
return context
class CallForSpeakersView(CampViewMixin, TemplateView):
def get_template_names(self):
return 'call_for_speakers_%s.html' % self.camp.slug

27
requirements.txt Normal file
View file

@ -0,0 +1,27 @@
CommonMark==0.7.3
Django==1.10.5
Pillow==4.0.0
PyPDF2==1.26.0
Unidecode==0.04.20
argparse==1.2.1
bleach==1.5.0
django-allauth==0.30.0
django-bleach==0.3.0
django-bootstrap3==8.1.0
django-debug-toolbar==1.6
django-environ==0.4.1
django-wkhtmltopdf==3.1.0
future==0.16.0
html5lib==0.9999999
oauthlib==2.0.1
olefile==0.44
psycopg2==2.6.2
python-openid==2.2.5
pytz==2016.10
qrcode==5.3
requests==2.12.5
requests-oauthlib==0.7.0
six==1.10.0
sqlparse==0.2.2
webencodings==0.5
wsgiref==0.1.2

View file

@ -1,12 +0,0 @@
django>=1.9
django-allauth>=0.23.0
django-bootstrap3>=7.0.0
django-environ>=0.4.0
psycopg2>=2.6.1
PyPDF2>=1.25.1
django-wkhtmltopdf>=3.0.0
Pillow==3.2.0
qrcode==5.3
CommonMark==0.6.4
django-bleach==0.3.0
Unidecode==0.4.19

View file

@ -1,2 +0,0 @@
-r base.txt
django-debug-toolbar==1.3.2

View file

@ -1 +0,0 @@
-r base.txt

View file

@ -58,7 +58,6 @@ class OrderAdmin(admin.ModelAdmin):
]
list_filter = [
'camp',
'payment_method',
'open',
'paid',

View file

@ -20,3 +20,4 @@ def user_has_tickets(request):
).exists():
has_tickets = True
return {'has_tickets': has_tickets}

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-12 17:56
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('shop', '0032_order_customer_comment'),
]
operations = [
migrations.RemoveField(
model_name='customorder',
name='camp',
),
migrations.RemoveField(
model_name='order',
name='camp',
),
]

View file

@ -17,12 +17,6 @@ from unidecode import unidecode
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')
)
@ -78,12 +72,6 @@ class Order(CreatedUpdatedModel):
default=True,
)
camp = models.ForeignKey(
'camps.Camp',
verbose_name=_('Camp'),
help_text=_('The camp this shop order is for.'),
)
CREDIT_CARD = 'credit_card'
BLOCKCHAIN = 'blockchain'
BANK_TRANSFER = 'bank_transfer'
@ -168,7 +156,7 @@ class Order(CreatedUpdatedModel):
@property
def description(self):
return "BornHack %s order #%s" % (self.camp.start.year, self.pk)
return "Order #%s" % self.pk
def get_absolute_url(self):
return str(reverse_lazy('shop:order_detail', kwargs={'pk': self.pk}))

View file

@ -20,7 +20,6 @@ from django.views.decorators.csrf import csrf_exempt
from django.utils.dateparse import parse_datetime
from django.utils import timezone
from camps.models import Camp
from shop.models import (
Order,
Product,
@ -234,7 +233,6 @@ class ProductDetailView(FormView, DetailView):
# no open order - open a new one
order = Order.objects.create(
user=self.request.user,
camp=Camp.objects.current()
)
# get product from kwargs

0
sponsors/__init__.py Normal file
View file

3
sponsors/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

7
sponsors/apps.py Normal file
View file

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

View file

5
sponsors/models.py Normal file
View file

@ -0,0 +1,5 @@
from __future__ import unicode_literals
from django.db import models
# Create your models here.

View file

@ -6,11 +6,10 @@ Call for Sponsors | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Our Sponsors</h2>
<h2>BornHack 2016 Sponsors</h2>
<p class="lead">
This is an alphabetical list of our current sponsors. We are immensely
grateful for all the help we are getting, and we are looking forward to
adding more names to this list.
This is an alphabetical list of the BornHack 2016 sponsors. An event like BornHack can not be built on hard work and
good intentions alone - it would simply not have been possible without the financial help from these organisations. Thank you, we are immensely grateful!
</p>
<div class="text-center">
<hr>
@ -116,7 +115,7 @@ Call for Sponsors | {{ block.super }}
<a href="mailto:thomas@tyktech.dk">
<img src="{% static 'img/sponsors/tyktech_logo.png' %}" class="img-responsive center-block" alt="tyktech logo">
</a>
Badges, lanyards, wristbands
Badges, wristbands
</p>
<hr>
<p class="lead">

3
sponsors/tests.py Normal file
View file

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

9
sponsors/views.py Normal file
View file

@ -0,0 +1,9 @@
from django.views.generic import TemplateView
from camps.mixins import CampViewMixin
class SponsorView(CampViewMixin, TemplateView):
def get_template_names(self):
return '%s-sponsors.html' % self.camp.slug

View file

@ -1,5 +1,5 @@
body {
margin-top: 90px;
margin-top: 85px;
margin-bottom: 35px;
}
@ -42,23 +42,7 @@ a, a:active, a:focus {
}
.navbar-fixed-top {
min-height: 80px;
}
@media (min-width: 768px) {
.nav li a {
padding: 30px 15px;
}
.nav {
float: right!important;
}
}
@media (max-width: 767px) {
.nav li {
text-align: center;
font-size: 20px;
}
min-height: 70px;
}
.navbar-toggle .icon-bar {
@ -121,13 +105,8 @@ footer {
}
.event {
max-width: 200px;
width: 200px;
height: 150px;
display: inline-block;
margin: 5px 5px;
padding: 5px;
flex: 1 1 auto;
}
.event:hover {
@ -135,3 +114,4 @@ footer {
color: white !important;
text-decoration: none;
}

Some files were not shown because too many files have changed in this diff Show more