Merge pull request #222 from bornhack/feature/team_controlled_info

Make info categories team controlled
This commit is contained in:
Víðir Valberg Guðmundsson 2018-07-01 17:34:22 +02:00 committed by GitHub
commit 9bfdc714f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 665 additions and 244 deletions

View file

@ -51,6 +51,7 @@ INSTALLED_APPS = [
'allauth.account', 'allauth.account',
'bootstrap3', 'bootstrap3',
'django_extensions', 'django_extensions',
'reversion',
'betterforms', 'betterforms',
] ]

View file

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

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

View 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)
]

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

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

View file

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

View file

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

View file

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

View file

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

View 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 %}

View 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 %}

View file

@ -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 %}

View file

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

View file

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

View file

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