Merge pull request #222 from bornhack/feature/team_controlled_info
Make info categories team controlled
This commit is contained in:
commit
9bfdc714f0
|
@ -51,6 +51,7 @@ INSTALLED_APPS = [
|
||||||
'allauth.account',
|
'allauth.account',
|
||||||
'bootstrap3',
|
'bootstrap3',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
|
'reversion',
|
||||||
'betterforms',
|
'betterforms',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from reversion.admin import VersionAdmin
|
||||||
from .models import (
|
from .models import (
|
||||||
InfoItem,
|
InfoItem,
|
||||||
InfoCategory
|
InfoCategory
|
||||||
|
@ -6,20 +7,20 @@ from .models import (
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InfoItem)
|
@admin.register(InfoItem)
|
||||||
class InfoItemAdmin(admin.ModelAdmin):
|
class InfoItemAdmin(VersionAdmin):
|
||||||
list_filter = ['category', 'category__camp',]
|
list_filter = ['category', 'category__team__camp',]
|
||||||
list_display = ['headline',]
|
list_display = ['headline',]
|
||||||
|
|
||||||
|
|
||||||
class InfoItemInlineAdmin(admin.StackedInline):
|
class InfoItemInlineAdmin(admin.StackedInline):
|
||||||
model = InfoItem
|
model = InfoItem
|
||||||
list_filter = ['category', 'category__camp',]
|
list_filter = ['category', 'category__team__camp',]
|
||||||
list_display = ['headline',]
|
list_display = ['headline',]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InfoCategory)
|
@admin.register(InfoCategory)
|
||||||
class InfoCategorydmin(admin.ModelAdmin):
|
class InfoCategorydmin(admin.ModelAdmin):
|
||||||
list_filter = ['camp',]
|
list_filter = ['team__camp',]
|
||||||
list_display = ['headline',]
|
list_display = ['headline',]
|
||||||
search_fields = ['headline', 'body']
|
search_fields = ['headline', 'body']
|
||||||
inlines = [InfoItemInlineAdmin]
|
inlines = [InfoItemInlineAdmin]
|
||||||
|
|
20
src/info/migrations/0004_infocategory_team.py
Normal file
20
src/info/migrations/0004_infocategory_team.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-04 21:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('teams', '0042_auto_20180413_1933'),
|
||||||
|
('info', '0003_auto_20170218_1148'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='infocategory',
|
||||||
|
name='team',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='The team responsible for this info category.', null=True, on_delete=django.db.models.deletion.PROTECT, to='teams.Team'),
|
||||||
|
),
|
||||||
|
]
|
78
src/info/migrations/0005_add_teams_to_categories.py
Normal file
78
src/info/migrations/0005_add_teams_to_categories.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-08 07:42
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
|
||||||
|
def add_teams_to_categories(apps, schema_editor):
|
||||||
|
InfoCategory = apps.get_model("info", "InfoCategory")
|
||||||
|
Team = apps.get_model("teams", "Team")
|
||||||
|
Camp = apps.get_model("camps", "Camp")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2016 - Everything is orga team
|
||||||
|
camp2016 = Camp.objects.get(slug="bornhack-2016")
|
||||||
|
orga2016 = Team.objects.get(camp=camp2016, name="Orga")
|
||||||
|
InfoCategory.objects.filter(camp=camp2016).update(team=orga2016)
|
||||||
|
|
||||||
|
# 2017 - Everything is orga team
|
||||||
|
camp2017 = Camp.objects.get(slug="bornhack-2017")
|
||||||
|
orga2017 = Team.objects.get(camp=camp2017, name="Orga")
|
||||||
|
InfoCategory.objects.filter(camp=camp2017).update(team=orga2017)
|
||||||
|
|
||||||
|
# 2018 - Map categories to teams
|
||||||
|
camp2018 = Camp.objects.get(slug="bornhack-2018")
|
||||||
|
team2018 = Team.objects.filter(camp=camp2018)
|
||||||
|
infocategories2018 = InfoCategory.objects.filter(camp=camp2018)
|
||||||
|
|
||||||
|
# Info team
|
||||||
|
infoteam = team2018.get(name="Info")
|
||||||
|
info_anchors = [
|
||||||
|
"what",
|
||||||
|
"when",
|
||||||
|
"travel",
|
||||||
|
"where",
|
||||||
|
"sleep",
|
||||||
|
"bicycles",
|
||||||
|
"infodesk-and-cert",
|
||||||
|
"shower-and-toilets",
|
||||||
|
"venue-map",
|
||||||
|
"villages",
|
||||||
|
]
|
||||||
|
infocategories2018.filter(anchor__in=info_anchors).update(team=infoteam)
|
||||||
|
|
||||||
|
# Food team
|
||||||
|
food = team2018.get(name="Food")
|
||||||
|
infocategories2018.filter(anchor__in=["food-and-groceries"]).update(team=food)
|
||||||
|
|
||||||
|
# NOC team
|
||||||
|
noc = team2018.get(name="NOC")
|
||||||
|
infocategories2018.filter(anchor__in=["network"]).update(team=noc)
|
||||||
|
|
||||||
|
# Power team
|
||||||
|
power = team2018.get(name="Power")
|
||||||
|
infocategories2018.filter(anchor__in=["power"]).update(team=power)
|
||||||
|
|
||||||
|
# Shuttle bus
|
||||||
|
shuttle_bus = team2018.get(name="Shuttle Bus")
|
||||||
|
infocategories2018.filter(anchor__in=["shuttle-bus"]).update(team=shuttle_bus)
|
||||||
|
|
||||||
|
# Bar
|
||||||
|
bar = team2018.get(name="Bar")
|
||||||
|
infocategories2018.filter(anchor__in=["bar"]).update(team=bar)
|
||||||
|
|
||||||
|
# Make info team catch all remaining
|
||||||
|
infocategories2018.filter(team__isnull=True).update(team=infoteam)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('info', '0004_infocategory_team'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(add_teams_to_categories)
|
||||||
|
]
|
27
src/info/migrations/0006_auto_20180520_1113.py
Normal file
27
src/info/migrations/0006_auto_20180520_1113.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-20 16:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('info', '0005_add_teams_to_categories'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='infocategory',
|
||||||
|
name='team',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='The team responsible for this info category.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='info_categories', to='teams.Team'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='infocategory',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='infocategory',
|
||||||
|
name='camp',
|
||||||
|
),
|
||||||
|
]
|
19
src/info/migrations/0007_auto_20180520_1511.py
Normal file
19
src/info/migrations/0007_auto_20180520_1511.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-20 20:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('info', '0006_auto_20180520_1113'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='infocategory',
|
||||||
|
name='team',
|
||||||
|
field=models.ForeignKey(help_text='The team responsible for this info category.', on_delete=django.db.models.deletion.PROTECT, related_name='info_categories', to='teams.Team'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,19 +2,14 @@ from django.db import models
|
||||||
from utils.models import CampRelatedModel
|
from utils.models import CampRelatedModel
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
class InfoCategory(CampRelatedModel):
|
class InfoCategory(CampRelatedModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'headline']
|
ordering = ['weight', 'headline']
|
||||||
unique_together = (('anchor', 'camp'), ('headline', 'camp'))
|
|
||||||
verbose_name_plural = "Info Categories"
|
verbose_name_plural = "Info Categories"
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
|
||||||
'camps.Camp',
|
|
||||||
related_name='infocategories',
|
|
||||||
on_delete=models.PROTECT
|
|
||||||
)
|
|
||||||
|
|
||||||
headline = models.CharField(
|
headline = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
help_text="The headline of this info category"
|
help_text="The headline of this info category"
|
||||||
|
@ -29,15 +24,32 @@ class InfoCategory(CampRelatedModel):
|
||||||
default=100,
|
default=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
team = models.ForeignKey(
|
||||||
|
'teams.Team',
|
||||||
|
help_text='The team responsible for this info category.',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='info_categories'
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if InfoItem.objects.filter(category__camp=self.camp, anchor=self.anchor).exists():
|
if InfoItem.objects.filter(category__team__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)
|
# 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'})
|
raise ValidationError(
|
||||||
|
{'anchor': 'Anchor is already in use on an info item for this camp'}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.team.camp
|
||||||
|
|
||||||
|
camp_filter = 'team__camp'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%s)' % (self.headline, self.camp)
|
return '%s (%s)' % (self.headline, self.camp)
|
||||||
|
|
||||||
|
|
||||||
|
# We want to have info items under version control
|
||||||
|
@reversion.register()
|
||||||
class InfoItem(CampRelatedModel):
|
class InfoItem(CampRelatedModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'headline']
|
ordering = ['weight', 'headline']
|
||||||
|
@ -71,10 +83,10 @@ class InfoItem(CampRelatedModel):
|
||||||
def camp(self):
|
def camp(self):
|
||||||
return self.category.camp
|
return self.category.camp
|
||||||
|
|
||||||
camp_filter = 'category__camp'
|
camp_filter = 'category__team__camp'
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if InfoCategory.objects.filter(camp=self.category.camp, anchor=self.anchor).exists():
|
if hasattr(self, 'category') and InfoCategory.objects.filter(team__camp=self.category.team.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)
|
# 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'})
|
raise ValidationError({'anchor': 'Anchor is already in use on an info category for this camp'})
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ Info | {{ block.super }}
|
||||||
<span class="anchor" id="{{ category.anchor }}"></span>
|
<span class="anchor" id="{{ category.anchor }}"></span>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h2>{{ category.headline }}</h2>
|
<h2>{{ category.headline }} {% if category.team %}<small>Info by the {{ category.team.name }} team</small>{% endif %}</h2>
|
||||||
<div class="panel-group">
|
<div class="panel-group">
|
||||||
{% for item in category.infoitems.all %}
|
{% for item in category.infoitems.all %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -54,7 +54,14 @@ Info | {{ block.super }}
|
||||||
<a href="#{{ item.anchor }}">
|
<a href="#{{ item.anchor }}">
|
||||||
<i class="glyphicon glyphicon-link"></i>
|
<i class="glyphicon glyphicon-link"></i>
|
||||||
</a>
|
</a>
|
||||||
|
{% if request.user in category.team.responsible_members.all %}
|
||||||
|
<a href="{% url 'teams:info_item_update' camp_slug=camp.slug team_slug=category.team.slug category_anchor=category.anchor item_anchor=item.anchor %}?next={% url 'info' camp_slug=camp.slug %}#{{ item.anchor }}"
|
||||||
|
class="btn btn-xs btn-primary pull-right">
|
||||||
|
<i class="fa fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</h4>
|
</h4>
|
||||||
|
<small></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>{{ item.body|trustedcommonmark }}</p>
|
<p>{{ item.body|trustedcommonmark }}</p>
|
||||||
|
|
|
@ -15,6 +15,7 @@ django-bleach==0.3.0
|
||||||
django-bootstrap3==8.2.2
|
django-bootstrap3==8.2.2
|
||||||
django-extensions==1.7.7
|
django-extensions==1.7.7
|
||||||
django-wkhtmltopdf==3.1.0
|
django-wkhtmltopdf==3.1.0
|
||||||
|
django-reversion==2.0.13
|
||||||
django-betterforms==1.1.4
|
django-betterforms==1.1.4
|
||||||
docopt==0.6.2
|
docopt==0.6.2
|
||||||
future==0.16.0
|
future==0.16.0
|
||||||
|
|
|
@ -109,6 +109,9 @@ class Team(CampRelatedModel):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} ({})'.format(self.name, self.camp)
|
return '{} ({})'.format(self.name, self.camp)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse_lazy('teams:detail', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.slug})
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
# generate slug if needed
|
# generate slug if needed
|
||||||
if not self.pk or not self.slug:
|
if not self.pk or not self.slug:
|
||||||
|
|
30
src/teams/templates/info_item_delete_confirm.html
Normal file
30
src/teams/templates/info_item_delete_confirm.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if form.instance.id %}
|
||||||
|
Edit Info Item: {{ form.instance.headline }}
|
||||||
|
{% else %}
|
||||||
|
Create Info item
|
||||||
|
{% endif %}
|
||||||
|
in {{ form.instance.category.headline }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4>
|
||||||
|
Delete info item {{ object.name }}
|
||||||
|
in {{ form.instance.category.headline }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Are you sure you want to delete "{{ object.headline }}" in the "{{ object.category.headline }}" category.</p>
|
||||||
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
41
src/teams/templates/info_item_form.html
Normal file
41
src/teams/templates/info_item_form.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if object %}
|
||||||
|
Editing "{{ object.headline }}"
|
||||||
|
{% else %}
|
||||||
|
Create Info item
|
||||||
|
{% endif %}
|
||||||
|
in "{{ form.instance.category.headline }}"
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4>
|
||||||
|
{% if object %}
|
||||||
|
Editing "{{ object.headline }}"
|
||||||
|
{% else %}
|
||||||
|
Create Info Item
|
||||||
|
{% endif %}
|
||||||
|
in "{{ object.category.headline }}"
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
<button type="submit" class="btn btn-primary">{% if object.id %}Save{% else %}Create{% endif %}</button>
|
||||||
|
{% if object %}
|
||||||
|
<a href="{% url 'teams:info_item_delete' camp_slug=camp.slug team_slug=object.category.team.slug category_anchor=object.category.anchor item_anchor=object.anchor %}"
|
||||||
|
class="btn btn-danger">
|
||||||
|
<i class="fa fa-remove"></i> Delete
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer"><i>This info item belongs to the <a href="{% url 'teams:detail' team_slug=team.slug camp_slug=team.camp.slug %}">{{ team.name }} Team</a></i></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -8,14 +8,26 @@ Team: {{ team.name }} | {{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ team.name }} Team Details</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><h4>{{ team.name }} Team Details</h4></div>
|
<div class="panel-heading">
|
||||||
|
<h4>Description</h4>
|
||||||
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{{ team.description|untrustedcommonmark }}
|
{{ team.description|untrustedcommonmark }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
{# Team communications #}
|
||||||
|
<div class="panel panel-default">
|
||||||
<h4>{{ team.name }} Team Communications</h4>
|
<div class="panel-heading">
|
||||||
|
<h4>Communication Channels</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
{{ team.camp.title }} teams primarily use mailing lists and IRC to communicate. The <b>{{ team.name }} team</b> can be contacted in the following ways:</p>
|
{{ team.camp.title }} teams primarily use mailing lists and IRC to communicate. The <b>{{ team.name }} team</b> can be contacted in the following ways:</p>
|
||||||
|
|
||||||
<h5>Mailing List</h5>
|
<h5>Mailing List</h5>
|
||||||
|
@ -38,9 +50,51 @@ Team: {{ team.name }} | {{ block.super }}
|
||||||
<p>The {{ team.name }} Team private IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.private_irc_channel_name }}">{{ team.private_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.</p>
|
<p>The {{ team.name }} Team private IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.private_irc_channel_name }}">{{ team.private_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>{{ team.name }} Team Members</h4>
|
{# Team tasks #}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4>Tasks</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>The {{ team.name }} Team is responsible for the following tasks</p>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for task in team.tasks.all %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'teams:task_detail' slug=task.slug camp_slug=camp.slug team_slug=team.slug %}">{{ task.name }}</a></td>
|
||||||
|
<td>{{ task.description }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> Details</a>
|
||||||
|
{% if request.user in team.responsible_members.all %}
|
||||||
|
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fas fa-edit"></i> Edit Task</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if request.user in team.responsible_members.all %}
|
||||||
|
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create Task</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Team members #}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4>Members</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<p>The following <b>{{ team.approved_members.count }}</b> people {% if team.unapproved_members.count %}(and {{ team.unapproved_members.count }} pending){% endif %} are members of the <b>{{ team.name }} Team</b>:</p>
|
<p>The following <b>{{ team.approved_members.count }}</b> people {% if team.unapproved_members.count %}(and {{ team.unapproved_members.count }} pending){% endif %} are members of the <b>{{ team.name }} Team</b>:</p>
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -86,36 +140,50 @@ Team: {{ team.name }} | {{ block.super }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>{{ team.name }} Team Tasks</h4>
|
{# Team info categories section - only visible for team responsible #}
|
||||||
<p>The {{ team.name }} Team is responsible for the following tasks</p>
|
{% if request.user in team.responsible_members.all and team.info_categories.exists %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4>Info Categories</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
|
||||||
|
{% for info_category in team.info_categories.all %}
|
||||||
|
|
||||||
|
<h4>{{ info_category.headline }}</h4>
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Item name</th>
|
||||||
<th>Description</th>
|
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for task in team.tasks.all %}
|
{% for item in info_category.infoitems.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'teams:task_detail' slug=task.slug camp_slug=camp.slug team_slug=team.slug %}">{{ task.name }}</a></td>
|
<td>{{ item.headline }}</td>
|
||||||
<td>{{ task.description }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> Details</a>
|
<a href="{% url 'teams:info_item_update' camp_slug=camp.slug team_slug=team.slug category_anchor=info_category.anchor item_anchor=item.anchor %}"
|
||||||
{% if request.user in team.responsible_members.all %}
|
class="btn btn-primary btn-sm">
|
||||||
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fas fa-edit"></i> Edit Task</a>
|
<i class="fas fa-edit"></i> Edit
|
||||||
{% endif %}
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if request.user in team.responsible_members.all %}
|
|
||||||
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create Task</a>
|
<a href="{% url 'teams:info_item_create' camp_slug=camp.slug team_slug=team.slug category_anchor=info_category.anchor %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create Info Item</a>
|
||||||
{% endif %}
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from .views import *
|
|
||||||
|
|
||||||
|
from teams.views.base import (
|
||||||
|
TeamListView,
|
||||||
|
TeamMemberRemoveView,
|
||||||
|
TeamMemberApproveView,
|
||||||
|
TeamDetailView,
|
||||||
|
TeamJoinView,
|
||||||
|
TeamLeaveView,
|
||||||
|
TeamManageView,
|
||||||
|
FixIrcAclView,
|
||||||
|
)
|
||||||
|
from teams.views.info import InfoItemUpdateView, InfoItemCreateView, InfoItemDeleteView
|
||||||
|
|
||||||
|
from teams.views.tasks import (
|
||||||
|
TaskCreateView,
|
||||||
|
TaskDetailView,
|
||||||
|
TaskUpdateView,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = 'teams'
|
app_name = 'teams'
|
||||||
|
|
||||||
|
@ -75,6 +91,29 @@ urlpatterns = [
|
||||||
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
'info/<slug:category_anchor>/', include([
|
||||||
|
path(
|
||||||
|
'create/',
|
||||||
|
InfoItemCreateView.as_view(),
|
||||||
|
name='info_item_create',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<slug:item_anchor>/', include([
|
||||||
|
path(
|
||||||
|
'update/',
|
||||||
|
InfoItemUpdateView.as_view(),
|
||||||
|
name='info_item_update',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'delete/',
|
||||||
|
InfoItemDeleteView.as_view(),
|
||||||
|
name='info_item_delete',
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
)
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
0
src/teams/views/__init__.py
Normal file
0
src/teams/views/__init__.py
Normal file
|
@ -1,52 +1,22 @@
|
||||||
from django.views.generic import ListView, DetailView
|
from django.views.generic import ListView, DetailView
|
||||||
from django.views.generic.edit import CreateView, UpdateView
|
from django.views.generic.edit import UpdateView
|
||||||
from camps.mixins import CampViewMixin
|
from camps.mixins import CampViewMixin
|
||||||
from .models import Team, TeamMember, TeamTask
|
|
||||||
from .email import add_added_membership_email, add_removed_membership_email
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from profiles.models import Profile
|
from profiles.models import Profile
|
||||||
|
|
||||||
|
from .mixins import EnsureTeamResponsibleMixin, EnsureTeamMemberResponsibleMixin
|
||||||
|
from ..models import Team, TeamMember
|
||||||
|
from ..email import add_added_membership_email, add_removed_membership_email
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
class EnsureTeamResponsibleMixin(object):
|
|
||||||
"""
|
|
||||||
Use to make sure request.user is responsible for the team specified by kwargs['team_slug']
|
|
||||||
"""
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp)
|
|
||||||
if request.user not in self.team.responsible_members.all():
|
|
||||||
messages.error(request, 'No thanks')
|
|
||||||
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug)
|
|
||||||
|
|
||||||
return super().dispatch(
|
|
||||||
request, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
|
|
||||||
"""
|
|
||||||
Use to make sure request.user is responsible for the team which TeamMember belongs to
|
|
||||||
"""
|
|
||||||
model = TeamMember
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if request.user not in self.get_object().team.responsible_members.all():
|
|
||||||
messages.error(request, 'No thanks')
|
|
||||||
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
|
|
||||||
|
|
||||||
return super().dispatch(
|
|
||||||
request, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TeamListView(CampViewMixin, ListView):
|
class TeamListView(CampViewMixin, ListView):
|
||||||
template_name = "team_list.html"
|
template_name = "team_list.html"
|
||||||
model = Team
|
model = Team
|
||||||
|
@ -165,56 +135,6 @@ class TeamMemberApproveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberR
|
||||||
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=form.instance.team.slug)
|
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=form.instance.team.slug)
|
||||||
|
|
||||||
|
|
||||||
class TaskDetailView(CampViewMixin, DetailView):
|
|
||||||
template_name = "task_detail.html"
|
|
||||||
context_object_name = "task"
|
|
||||||
model = TeamTask
|
|
||||||
|
|
||||||
|
|
||||||
class TaskCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, CreateView):
|
|
||||||
model = TeamTask
|
|
||||||
template_name = "task_form.html"
|
|
||||||
fields = ['name', 'description']
|
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['team'] = self.team
|
|
||||||
return context
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
task = form.save(commit=False)
|
|
||||||
task.team = self.team
|
|
||||||
if not task.name:
|
|
||||||
task.name = "noname"
|
|
||||||
task.save()
|
|
||||||
return HttpResponseRedirect(task.get_absolute_url())
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return self.get_object().get_absolute_url()
|
|
||||||
|
|
||||||
|
|
||||||
class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
|
|
||||||
model = TeamTask
|
|
||||||
template_name = "task_form.html"
|
|
||||||
fields = ['name', 'description']
|
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['team'] = self.team
|
|
||||||
return context
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
task = form.save(commit=False)
|
|
||||||
task.team = self.team
|
|
||||||
if not task.name:
|
|
||||||
task.name = "noname"
|
|
||||||
task.save()
|
|
||||||
return HttpResponseRedirect(task.get_absolute_url())
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return self.get_object().get_absolute_url()
|
|
||||||
|
|
||||||
|
|
||||||
class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView):
|
class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView):
|
||||||
template_name = "fix_irc_acl.html"
|
template_name = "fix_irc_acl.html"
|
||||||
model = Team
|
model = Team
|
62
src/teams/views/info.py
Normal file
62
src/teams/views/info.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.views.generic import CreateView, UpdateView, DeleteView
|
||||||
|
from reversion.views import RevisionMixin
|
||||||
|
|
||||||
|
from camps.mixins import CampViewMixin
|
||||||
|
from info.models import InfoItem, InfoCategory
|
||||||
|
from teams.views.mixins import EnsureTeamResponsibleMixin
|
||||||
|
|
||||||
|
|
||||||
|
class InfoItemCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, CreateView):
|
||||||
|
model = InfoItem
|
||||||
|
template_name = "info_item_form.html"
|
||||||
|
fields = ['headline', 'body', 'anchor', 'weight']
|
||||||
|
slug_field = 'anchor'
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['team'] = self.team
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
info_item = form.save(commit=False)
|
||||||
|
category = InfoCategory.objects.get(camp=self.camp, anchor=self.kwargs.get('category_anchor'))
|
||||||
|
info_item.category = category
|
||||||
|
info_item.save()
|
||||||
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.team.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class InfoItemUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, RevisionMixin, UpdateView):
|
||||||
|
model = InfoItem
|
||||||
|
template_name = "info_item_form.html"
|
||||||
|
fields = ['headline', 'body', 'anchor', 'weight']
|
||||||
|
slug_field = 'anchor'
|
||||||
|
slug_url_kwarg = 'item_anchor'
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['team'] = self.team
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
next = self.request.GET.get('next')
|
||||||
|
if next:
|
||||||
|
return next
|
||||||
|
return self.team.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class InfoItemDeleteView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, RevisionMixin, DeleteView):
|
||||||
|
model = InfoItem
|
||||||
|
template_name = "info_item_delete_confirm.html"
|
||||||
|
slug_field = 'anchor'
|
||||||
|
slug_url_kwarg = 'item_anchor'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
next = self.request.GET.get('next')
|
||||||
|
if next:
|
||||||
|
return next
|
||||||
|
return self.team.get_absolute_url()
|
36
src/teams/views/mixins.py
Normal file
36
src/teams/views/mixins.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
|
from teams.models import Team, TeamMember
|
||||||
|
|
||||||
|
|
||||||
|
class EnsureTeamResponsibleMixin(object):
|
||||||
|
"""
|
||||||
|
Use to make sure request.user is responsible for the team specified by kwargs['team_slug']
|
||||||
|
"""
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp)
|
||||||
|
if request.user not in self.team.responsible_members.all():
|
||||||
|
messages.error(request, 'No thanks')
|
||||||
|
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug)
|
||||||
|
|
||||||
|
return super().dispatch(
|
||||||
|
request, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
|
||||||
|
"""
|
||||||
|
Use to make sure request.user is responsible for the team which TeamMember belongs to
|
||||||
|
"""
|
||||||
|
model = TeamMember
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if request.user not in self.get_object().team.responsible_members.all():
|
||||||
|
messages.error(request, 'No thanks')
|
||||||
|
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
|
||||||
|
|
||||||
|
return super().dispatch(
|
||||||
|
request, *args, **kwargs
|
||||||
|
)
|
57
src/teams/views/tasks.py
Normal file
57
src/teams/views/tasks.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.views.generic import DetailView, CreateView, UpdateView
|
||||||
|
|
||||||
|
from camps.mixins import CampViewMixin
|
||||||
|
from ..models import TeamTask
|
||||||
|
from .mixins import EnsureTeamResponsibleMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TaskDetailView(CampViewMixin, DetailView):
|
||||||
|
template_name = "task_detail.html"
|
||||||
|
context_object_name = "task"
|
||||||
|
model = TeamTask
|
||||||
|
|
||||||
|
|
||||||
|
class TaskCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, CreateView):
|
||||||
|
model = TeamTask
|
||||||
|
template_name = "task_form.html"
|
||||||
|
fields = ['name', 'description']
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['team'] = self.team
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
task = form.save(commit=False)
|
||||||
|
task.team = self.team
|
||||||
|
if not task.name:
|
||||||
|
task.name = "noname"
|
||||||
|
task.save()
|
||||||
|
return HttpResponseRedirect(task.get_absolute_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.get_object().get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
|
||||||
|
model = TeamTask
|
||||||
|
template_name = "task_form.html"
|
||||||
|
fields = ['name', 'description']
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['team'] = self.team
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
task = form.save(commit=False)
|
||||||
|
task.team = self.team
|
||||||
|
if not task.name:
|
||||||
|
task.name = "noname"
|
||||||
|
task.save()
|
||||||
|
return HttpResponseRedirect(task.get_absolute_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.get_object().get_absolute_url()
|
|
@ -1401,110 +1401,6 @@ programming for a danish startup.
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
self.output("Creating infocategories for {}...".format(year))
|
|
||||||
info_cat1 = InfoCategory.objects.create(
|
|
||||||
camp=camp,
|
|
||||||
headline='When is BornHack happening?',
|
|
||||||
anchor='when'
|
|
||||||
)
|
|
||||||
info_cat2 = InfoCategory.objects.create(
|
|
||||||
camp=camp,
|
|
||||||
headline='Travel Information',
|
|
||||||
anchor='travel'
|
|
||||||
)
|
|
||||||
info_cat3 = InfoCategory.objects.create(
|
|
||||||
camp=camp,
|
|
||||||
headline='Where do I sleep?',
|
|
||||||
anchor='sleep'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.output("Creating infoitems for {}...".format(year))
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat1,
|
|
||||||
headline='Opening',
|
|
||||||
anchor='opening',
|
|
||||||
body='BornHack 2016 starts saturday, august 27th, at noon (12:00). It will be possible to access the venue before noon if for example you arrive early in the morning with the ferry. But please dont expect everything to be ready before noon :)'
|
|
||||||
)
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat1,
|
|
||||||
headline='Closing',
|
|
||||||
anchor='closing',
|
|
||||||
body='BornHack 2016 ends saturday, september 3rd, at noon (12:00). Rented village tents must be empty and cleaned at this time, ready to take down. Participants must leave the site no later than 17:00 on the closing day (or stay and help us clean up).'
|
|
||||||
)
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat2,
|
|
||||||
headline='Public Transportation',
|
|
||||||
anchor='public-transportation',
|
|
||||||
body='''
|
|
||||||
From/Via Copenhagen
|
|
||||||
|
|
||||||
There are several ways to get to Bornholm from Copenhagen. A domestic plane departs from Copenhagen Airport, and you can get from Copenhagen Central station by either bus or train via Ystad or the Køge-Rønne ferry connection.
|
|
||||||
|
|
||||||
Plane (very fast, most expensive)
|
|
||||||
You can check plane departures and book tickets at dat.dk. There are multiple departures daily. The flight takes approximately 25 minutes.
|
|
||||||
Via Ystad (quick, cheap and easy, crosses Sweden border)
|
|
||||||
You can drive over Øresundsbroen to Ystad or you can take the train/bus from Copenhagen Central Station. You can buy train and ferry ticket at dsb.dk (Type in "København H" and "Rønne Havn"). More information about the crossing. The crossing takes 1 hour 20 minutes. In total about 3 hours 15 minutes. Due to recent developments an ID (passport, drivers license or similar) is required when crossing the Denmark/Sweden border.
|
|
||||||
Via Køge (cheap, slow)
|
|
||||||
Take the S-train to Køge Station (you need an "all zones" ticket) or travel by car. The ferry terminal is within walking distance from the station. You can check out prices here. It takes approximately 1 hour to get to Køge. The crossing takes 5 hours 30 minutes.
|
|
||||||
|
|
||||||
From Sweden/Malmö
|
|
||||||
|
|
||||||
To get to Bornholm from Malmö you may take a train from Malmö to Ystad and the ferry from Ystad to Bornholm.
|
|
||||||
|
|
||||||
Skånetrafiken runs trains from Malmö C to Ystad every 30 minutes. Trains leave at 08 and 38 minutes past the hour. Go to skanetrafiken for details.
|
|
||||||
|
|
||||||
The ferry from Ystad to Rønne leaves four times per day. Morning: 08:30-09:50 Noon: 12:30-13:50 Afternoon: 16:30-17:50 Evening: 20:30-21:50 Booking the ferry tickets prior to departure can drastically reduce the price. See "Getting from Rønne to the Venue" final step.
|
|
||||||
From Abroad
|
|
||||||
|
|
||||||
If you are going to BornHack from abroad you have different options as well.
|
|
||||||
|
|
||||||
Berlin (Germany)
|
|
||||||
There are no public transport routes from Berlin to Mukran, Sassnitz ferry terminal on Saturdays, including Aug 27 and Sept 03 the Bornhack start/end dates. Your best bet is to get a train to Dubnitz, Sassnitz station. Unfortunately it is still 1.7km to the actual ferry terminal: map of route. There is a bus, but it only goes once a weekday at 12:28 and not at all on Weekends. You can of course take a taxi. Search for routes Berlin ‐ Dubnitz on bahn.de. At the time of writing, the best route is:
|
|
||||||
08:45 Berlin Hbf → train with 2 changes, 50€ for a 2-way return ticket.
|
|
||||||
12:52 Sassnitz → taxi, ~14€, 10 min.
|
|
||||||
13:00 Mukran-Sassnitz ferry terminal
|
|
||||||
If you want to try your luck at a direct route to the ferry terminal, search for routes Berlin ‐ Sassnitz-Mukran Fährhafen on bahn.de or for routes Berlin ‐ Fährhafen Sassnitz on checkmybus.com.
|
|
||||||
Sassnitz (Germany)
|
|
||||||
There is a direct ferry taking cars going from Sassnitz ferry terminal which is 4km away from Sassnitz itself. The company is called BornholmerFærgen and the tickets cost 32€ (outgoing) and 25€ (return). It can also be booked from aferry.co.uk. The ferry departs for Bornholm on Aug 27 at 11:50, 13:30, and returns to Sassnitz on Sept 03 at 08:00, 09:00. Detailed timetable: English, Danish.
|
|
||||||
Kolobrzeg (Poland)
|
|
||||||
There is a passenger ferry from Kolobrzeg to Nexø havn.
|
|
||||||
|
|
||||||
Getting from Rønne to the Venue
|
|
||||||
|
|
||||||
The venue is 24km from Rønne. We will have a shuttle bus that will pick people up and take them to the venue. It is also possible to take a public bus to near the venue. Local taxis can also get you here. The company operating on Bornholm is called Dantaxi and the local phonenumber is +4556952301.
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat1,
|
|
||||||
headline='Bus to and from BornHack',
|
|
||||||
anchor='bus-to-and-from-bornhack',
|
|
||||||
body='PROSA, the union of IT-professionals in Denmark, has set up a great deal for BornHack attendees travelling from Copenhagen to BornHack. For only 125kr, about 17 euros, you can be transported to the camp on opening day, and back to Copenhagen at the end of the camp!'
|
|
||||||
|
|
||||||
)
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat1,
|
|
||||||
headline='Driving and Parking',
|
|
||||||
anchor='driving-and-parking',
|
|
||||||
body='''
|
|
||||||
A car is very convenient when bringing lots of equipment, and we know that hackers like to bring all the things. We welcome cars and vans at BornHack. We have ample parking space very close (less than 50 meters) to the main entrance.
|
|
||||||
|
|
||||||
Please note that sleeping in the parking lot is not permitted. If you want to sleep in your car/RV/autocamper/caravan please contact us, and we will work something out.
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat3,
|
|
||||||
headline='Camping',
|
|
||||||
anchor='camping',
|
|
||||||
body='BornHack is first and foremost a tent camp. You need to bring a tent to sleep in. Most people go with some friends and make a camp somewhere at the venue. See also the section on Villages - you might be able to find some likeminded people to camp with.'
|
|
||||||
)
|
|
||||||
InfoItem.objects.create(
|
|
||||||
category=info_cat3,
|
|
||||||
headline='Cabins',
|
|
||||||
anchor='cabins',
|
|
||||||
body='We rent out a few cabins at the venue with 8 beds each for people who don\'t want to sleep in tents for some reason. A tent is the cheapest sleeping option (you just need a ticket), but the cabins are there if you want them.'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.output("Creating villages for {}...".format(year))
|
self.output("Creating villages for {}...".format(year))
|
||||||
Village.objects.create(
|
Village.objects.create(
|
||||||
contact=user1,
|
contact=user1,
|
||||||
|
@ -1671,6 +1567,109 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
|
||||||
user=user4,
|
user=user4,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.output("Creating infocategories for {}...".format(year))
|
||||||
|
info_cat1 = InfoCategory.objects.create(
|
||||||
|
team=orga_team,
|
||||||
|
headline='When is BornHack happening?',
|
||||||
|
anchor='when'
|
||||||
|
)
|
||||||
|
info_cat2 = InfoCategory.objects.create(
|
||||||
|
team=orga_team,
|
||||||
|
headline='Travel Information',
|
||||||
|
anchor='travel'
|
||||||
|
)
|
||||||
|
info_cat3 = InfoCategory.objects.create(
|
||||||
|
team=orga_team,
|
||||||
|
headline='Where do I sleep?',
|
||||||
|
anchor='sleep'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.output("Creating infoitems for {}...".format(year))
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat1,
|
||||||
|
headline='Opening',
|
||||||
|
anchor='opening',
|
||||||
|
body='BornHack 2016 starts saturday, august 27th, at noon (12:00). It will be possible to access the venue before noon if for example you arrive early in the morning with the ferry. But please dont expect everything to be ready before noon :)'
|
||||||
|
)
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat1,
|
||||||
|
headline='Closing',
|
||||||
|
anchor='closing',
|
||||||
|
body='BornHack 2016 ends saturday, september 3rd, at noon (12:00). Rented village tents must be empty and cleaned at this time, ready to take down. Participants must leave the site no later than 17:00 on the closing day (or stay and help us clean up).'
|
||||||
|
)
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat2,
|
||||||
|
headline='Public Transportation',
|
||||||
|
anchor='public-transportation',
|
||||||
|
body='''
|
||||||
|
From/Via Copenhagen
|
||||||
|
|
||||||
|
There are several ways to get to Bornholm from Copenhagen. A domestic plane departs from Copenhagen Airport, and you can get from Copenhagen Central station by either bus or train via Ystad or the Køge-Rønne ferry connection.
|
||||||
|
|
||||||
|
Plane (very fast, most expensive)
|
||||||
|
You can check plane departures and book tickets at dat.dk. There are multiple departures daily. The flight takes approximately 25 minutes.
|
||||||
|
Via Ystad (quick, cheap and easy, crosses Sweden border)
|
||||||
|
You can drive over Øresundsbroen to Ystad or you can take the train/bus from Copenhagen Central Station. You can buy train and ferry ticket at dsb.dk (Type in "København H" and "Rønne Havn"). More information about the crossing. The crossing takes 1 hour 20 minutes. In total about 3 hours 15 minutes. Due to recent developments an ID (passport, drivers license or similar) is required when crossing the Denmark/Sweden border.
|
||||||
|
Via Køge (cheap, slow)
|
||||||
|
Take the S-train to Køge Station (you need an "all zones" ticket) or travel by car. The ferry terminal is within walking distance from the station. You can check out prices here. It takes approximately 1 hour to get to Køge. The crossing takes 5 hours 30 minutes.
|
||||||
|
|
||||||
|
From Sweden/Malmö
|
||||||
|
|
||||||
|
To get to Bornholm from Malmö you may take a train from Malmö to Ystad and the ferry from Ystad to Bornholm.
|
||||||
|
|
||||||
|
Skånetrafiken runs trains from Malmö C to Ystad every 30 minutes. Trains leave at 08 and 38 minutes past the hour. Go to skanetrafiken for details.
|
||||||
|
|
||||||
|
The ferry from Ystad to Rønne leaves four times per day. Morning: 08:30-09:50 Noon: 12:30-13:50 Afternoon: 16:30-17:50 Evening: 20:30-21:50 Booking the ferry tickets prior to departure can drastically reduce the price. See "Getting from Rønne to the Venue" final step.
|
||||||
|
From Abroad
|
||||||
|
|
||||||
|
If you are going to BornHack from abroad you have different options as well.
|
||||||
|
|
||||||
|
Berlin (Germany)
|
||||||
|
There are no public transport routes from Berlin to Mukran, Sassnitz ferry terminal on Saturdays, including Aug 27 and Sept 03 the Bornhack start/end dates. Your best bet is to get a train to Dubnitz, Sassnitz station. Unfortunately it is still 1.7km to the actual ferry terminal: map of route. There is a bus, but it only goes once a weekday at 12:28 and not at all on Weekends. You can of course take a taxi. Search for routes Berlin ‐ Dubnitz on bahn.de. At the time of writing, the best route is:
|
||||||
|
08:45 Berlin Hbf → train with 2 changes, 50€ for a 2-way return ticket.
|
||||||
|
12:52 Sassnitz → taxi, ~14€, 10 min.
|
||||||
|
13:00 Mukran-Sassnitz ferry terminal
|
||||||
|
If you want to try your luck at a direct route to the ferry terminal, search for routes Berlin ‐ Sassnitz-Mukran Fährhafen on bahn.de or for routes Berlin ‐ Fährhafen Sassnitz on checkmybus.com.
|
||||||
|
Sassnitz (Germany)
|
||||||
|
There is a direct ferry taking cars going from Sassnitz ferry terminal which is 4km away from Sassnitz itself. The company is called BornholmerFærgen and the tickets cost 32€ (outgoing) and 25€ (return). It can also be booked from aferry.co.uk. The ferry departs for Bornholm on Aug 27 at 11:50, 13:30, and returns to Sassnitz on Sept 03 at 08:00, 09:00. Detailed timetable: English, Danish.
|
||||||
|
Kolobrzeg (Poland)
|
||||||
|
There is a passenger ferry from Kolobrzeg to Nexø havn.
|
||||||
|
|
||||||
|
Getting from Rønne to the Venue
|
||||||
|
|
||||||
|
The venue is 24km from Rønne. We will have a shuttle bus that will pick people up and take them to the venue. It is also possible to take a public bus to near the venue. Local taxis can also get you here. The company operating on Bornholm is called Dantaxi and the local phonenumber is +4556952301.
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat1,
|
||||||
|
headline='Bus to and from BornHack',
|
||||||
|
anchor='bus-to-and-from-bornhack',
|
||||||
|
body='PROSA, the union of IT-professionals in Denmark, has set up a great deal for BornHack attendees travelling from Copenhagen to BornHack. For only 125kr, about 17 euros, you can be transported to the camp on opening day, and back to Copenhagen at the end of the camp!'
|
||||||
|
|
||||||
|
)
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat1,
|
||||||
|
headline='Driving and Parking',
|
||||||
|
anchor='driving-and-parking',
|
||||||
|
body='''
|
||||||
|
A car is very convenient when bringing lots of equipment, and we know that hackers like to bring all the things. We welcome cars and vans at BornHack. We have ample parking space very close (less than 50 meters) to the main entrance.
|
||||||
|
|
||||||
|
Please note that sleeping in the parking lot is not permitted. If you want to sleep in your car/RV/autocamper/caravan please contact us, and we will work something out.
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat3,
|
||||||
|
headline='Camping',
|
||||||
|
anchor='camping',
|
||||||
|
body='BornHack is first and foremost a tent camp. You need to bring a tent to sleep in. Most people go with some friends and make a camp somewhere at the venue. See also the section on Villages - you might be able to find some likeminded people to camp with.'
|
||||||
|
)
|
||||||
|
InfoItem.objects.create(
|
||||||
|
category=info_cat3,
|
||||||
|
headline='Cabins',
|
||||||
|
anchor='cabins',
|
||||||
|
body='We rent out a few cabins at the venue with 8 beds each for people who don\'t want to sleep in tents for some reason. A tent is the cheapest sleeping option (you just need a ticket), but the cabins are there if you want them.'
|
||||||
|
)
|
||||||
|
|
||||||
self.output("Adding event routing...")
|
self.output("Adding event routing...")
|
||||||
Routing.objects.create(
|
Routing.objects.create(
|
||||||
team=orga_team,
|
team=orga_team,
|
||||||
|
|
Loading…
Reference in a new issue