add url support for speakerproposals and eventproposals, including new models Url and UrlType. Also switch to Django 2.0 path() syntax in various urls.py files getting rid of a lot of ugly regex \o/

This commit is contained in:
Thomas Steen Rasmussen 2018-05-23 23:28:27 +02:00
parent df783168c6
commit 18c33383b7
26 changed files with 751 additions and 214 deletions

View File

@ -1,14 +1,14 @@
from django.conf.urls import url
from django.urls import path
from .views import *
app_name = 'backoffice'
urlpatterns = [
url(r'^$', BackofficeIndexView.as_view(), name='index'),
url(r'product_handout/$', ProductHandoutView.as_view(), name='product_handout'),
url(r'badge_handout/$', BadgeHandoutView.as_view(), name='badge_handout'),
url(r'ticket_checkin/$', TicketCheckinView.as_view(), name='ticket_checkin'),
url(r'public_credit_names/$', ApproveNamesView.as_view(), name='public_credit_names'),
path('', BackofficeIndexView.as_view(), name='index'),
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
]

View File

@ -3,7 +3,7 @@ from allauth.account.views import (
LogoutView,
)
from django.conf import settings
from django.conf.urls import include, url
from django.urls import include, path
from django.contrib import admin
from camps.views import *
from info.views import *
@ -15,180 +15,180 @@ from people.views import *
from bar.views import MenuView
urlpatterns = [
url(
r'^profile/',
path(
'profile/',
include('profiles.urls', namespace='profiles')
),
url(
r'^tickets/',
path(
'tickets/',
include('tickets.urls', namespace='tickets')
),
url(
r'^shop/',
path(
'shop/',
include('shop.urls', namespace='shop')
),
url(
r'^news/',
path(
'news/',
include('news.urls', namespace='news')
),
url(
r'^contact/',
path(
'contact/',
TemplateView.as_view(template_name='contact.html'),
name='contact'
),
url(
r'^conduct/',
path(
'conduct/',
TemplateView.as_view(template_name='coc.html'),
name='conduct'
),
url(
r'^login/$',
path(
'login/',
LoginView.as_view(),
name='account_login',
),
url(
r'^logout/$',
path(
'logout/',
LogoutView.as_view(),
name='account_logout',
),
url(
r'^privacy-policy/$',
path(
'privacy-policy/',
TemplateView.as_view(template_name='legal/privacy_policy.html'),
name='privacy-policy'
),
url(
r'^general-terms-and-conditions/$',
path(
'general-terms-and-conditions/',
TemplateView.as_view(template_name='legal/general_terms_and_conditions.html'),
name='general-terms'
),
url(r'^accounts/', include('allauth.urls')),
url(r'^admin/', admin.site.urls),
path('accounts/', include('allauth.urls')),
path('admin/', admin.site.urls),
url(
r'^camps/$',
path(
'camps/',
CampListView.as_view(),
name='camp_list'
),
# camp redirect views here
url(
r'^$',
path(
'',
CampRedirectView.as_view(),
kwargs={'page': 'camp_detail'},
name='camp_detail_redirect',
),
url(
r'^program/$',
path(
'program/',
CampRedirectView.as_view(),
kwargs={'page': 'schedule_index'},
name='schedule_index_redirect',
),
url(
r'^info/$',
path(
'info/',
CampRedirectView.as_view(),
kwargs={'page': 'info'},
name='info_redirect',
),
url(
r'^sponsors/$',
path(
'sponsors/',
CampRedirectView.as_view(),
kwargs={'page': 'sponsors'},
name='sponsors_redirect',
),
url(
r'^villages/$',
path(
'villages/',
CampRedirectView.as_view(),
kwargs={'page': 'village_list'},
name='village_list_redirect',
),
url(
r'^people/$',
path(
'people/',
PeopleView.as_view(),
name='people',
),
url(
r'^backoffice/',
path(
'backoffice/',
include('backoffice.urls', namespace='backoffice')
),
# camp specific urls below here
url(
r'(?P<camp_slug>[-_\w+]+)/', include([
url(
r'^$',
path(
'<slug:camp_slug>/', include([
path(
'',
CampDetailView.as_view(),
name='camp_detail'
),
url(
r'^info/$',
path(
'info/',
CampInfoView.as_view(),
name='info'
),
url(
r'^program/',
path(
'program/',
include('program.urls', namespace='program'),
),
url(
r'^sponsors/call/$',
path(
'sponsors/call/',
CallForSponsorsView.as_view(),
name='call-for-sponsors'
),
url(
r'^sponsors/$',
path(
'sponsors/',
SponsorsView.as_view(),
name='sponsors'
),
url(
r'^bar/menu$',
path(
'bar/menu',
MenuView.as_view(),
name='menu'
),
url(
r'^villages/', include([
url(
r'^$',
path(
'villages/', include([
path(
'',
VillageListView.as_view(),
name='village_list'
),
url(
r'create/$',
path(
'create/',
VillageCreateView.as_view(),
name='village_create'
),
url(
r'(?P<slug>[-_\w+]+)/delete/$',
path(
'<slug:slug>/delete/',
VillageDeleteView.as_view(),
name='village_delete'
),
url(
r'(?P<slug>[-_\w+]+)/edit/$',
path(
'<slug:slug>/edit/',
VillageUpdateView.as_view(),
name='village_update'
),
# this has to be the last url in the list
url(
r'(?P<slug>[-_\w+]+)/$',
path(
'<slug:slug>/',
VillageDetailView.as_view(),
name='village_detail'
),
])
),
url(
r'^teams/',
path(
'teams/',
include('teams.urls', namespace='teams')
),
@ -200,5 +200,6 @@ urlpatterns = [
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

View File

@ -1,10 +1,10 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'news'
urlpatterns = [
url(r'^$', views.NewsIndex.as_view(), kwargs={'archived': False}, name='index'),
url(r'^archive/$', views.NewsIndex.as_view(), kwargs={'archived': True}, name='archive'),
url(r'(?P<slug>[-_\w+]+)/$', views.NewsDetail.as_view(), name='detail'),
path('', views.NewsIndex.as_view(), kwargs={'archived': False}, name='index'),
path('archive/', views.NewsIndex.as_view(), kwargs={'archived': True}, name='archive'),
path('<slug:slug>/', views.NewsDetail.as_view(), name='detail'),
]

View File

@ -1,10 +1,10 @@
from django.conf.urls import url
from django.urls import path
from .views import ProfileDetail, ProfileUpdate
app_name = 'profiles'
urlpatterns = [
url(r'^$', ProfileDetail.as_view(), name='detail'),
url(r'^edit$', ProfileUpdate.as_view(), name='update'),
path('', ProfileDetail.as_view(), name='detail'),
path('edit', ProfileUpdate.as_view(), name='update'),
]

View File

@ -14,7 +14,9 @@ from .models import (
EventTrack,
SpeakerProposal,
EventProposal,
Favorite
Favorite,
UrlType,
Url
)
@ -98,3 +100,11 @@ class EventAdmin(admin.ModelAdmin):
SpeakerInline
]
@admin.register(UrlType)
class UrlTypeAdmin(admin.ModelAdmin):
pass
@admin.register(Url)
class UrlAdmin(admin.ModelAdmin):
pass

View File

@ -0,0 +1,48 @@
# Generated by Django 2.0.4 on 2018-05-21 21:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('program', '0054_auto_20180520_1509'),
]
operations = [
migrations.CreateModel(
name='Url',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('url', models.URLField(help_text='The actual URL')),
('event', models.ForeignKey(blank=True, help_text='The event proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.Event')),
('eventproposal', models.ForeignKey(blank=True, help_text='The event proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.EventProposal')),
('speaker', models.ForeignKey(blank=True, help_text='The speaker proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.Speaker')),
('speakerproposal', models.ForeignKey(blank=True, help_text='The speaker proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.SpeakerProposal')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='UrlType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(help_text='The name of this type', max_length=25)),
('icon', models.CharField(help_text="Name of the fontawesome icon to use without the 'fa-' part", max_length=100)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='url',
name='urltype',
field=models.ForeignKey(help_text='The type of this URL', on_delete=django.db.models.deletion.PROTECT, to='program.UrlType'),
),
]

View File

@ -0,0 +1,58 @@
# Generated by Django 2.0.4 on 2018-05-21 21:55
from django.db import migrations
def add_urltypes(apps, schema_editor):
UrlType = apps.get_model('program', 'UrlType')
UrlType.objects.create(
name='Other',
icon='link',
)
UrlType.objects.create(
name='Homepage',
icon='link',
)
UrlType.objects.create(
name='Slides',
icon='link',
)
UrlType.objects.create(
name='Twitter',
icon='link',
)
UrlType.objects.create(
name='Mastodon',
icon='link',
)
UrlType.objects.create(
name='Facebook',
icon='link',
)
UrlType.objects.create(
name='Project',
icon='link',
)
UrlType.objects.create(
name='Blog',
icon='link',
)
class Migration(migrations.Migration):
dependencies = [
('program', '0055_auto_20180521_2354'),
]
operations = [
migrations.RunPython(add_urltypes),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.4 on 2018-05-22 04:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('program', '0056_add_urltypes'),
]
operations = [
migrations.AlterField(
model_name='urltype',
name='icon',
field=models.CharField(default='link', help_text="Name of the fontawesome icon to use without the 'fa-' part", max_length=100),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.0.4 on 2018-05-23 06:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('program', '0057_auto_20180522_0659'),
]
operations = [
migrations.AlterModelOptions(
name='urltype',
options={'ordering': ['name']},
),
migrations.AlterField(
model_name='urltype',
name='name',
field=models.CharField(help_text='The name of this type', max_length=25, unique=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.0.4 on 2018-05-23 20:41
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('program', '0058_auto_20180523_0844'),
]
operations = [
migrations.RemoveField(
model_name='url',
name='id',
),
migrations.AddField(
model_name='url',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
]

View File

@ -1,11 +1,9 @@
from django.views.generic.detail import SingleObjectMixin
from django.shortcuts import redirect
from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse
from . import models
from django.contrib import messages
from django.http import Http404, HttpResponse
import sys
import mimetypes
class EnsureCFPOpenMixin(object):
@ -55,3 +53,42 @@ class EnsureUserOwnsProposalMixin(SingleObjectMixin):
# alright, continue with the request
return super().dispatch(request, *args, **kwargs)
class UrlViewMixin(object):
"""
Mixin with code shared between all the Url views
"""
def dispatch(self, request, *args, **kwargs):
"""
Check that we have a valid SpeakerProposal or EventProposal and that it belongs to the current user
"""
# get the proposal
if 'event_uuid' in self.kwargs:
self.eventproposal = get_object_or_404(models.EventProposal, uuid=self.kwargs['event_uuid'], user=request.user)
elif 'speaker_uuid' in self.kwargs:
self.speakerproposal = get_object_or_404(models.SpeakerProposal, uuid=self.kwargs['speaker_uuid'], user=request.user)
else:
# fuckery afoot
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
Include the proposal in the template context
"""
context = super().get_context_data(**kwargs)
if hasattr(self, 'eventproposal') and self.eventproposal:
context['eventproposal'] = self.eventproposal
else:
context['speakerproposal'] = self.speakerproposal
return context
def get_success_url(self):
"""
Return to the detail view of the proposal
"""
if hasattr(self, 'eventproposal'):
return self.eventproposal.get_absolute_url()
else:
return self.speakerproposal.get_absolute_url()

View File

@ -15,11 +15,141 @@ from django.core.files.storage import FileSystemStorage
from django.urls import reverse
from django.apps import apps
from django.core.files.base import ContentFile
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from utils.models import CreatedUpdatedModel, CampRelatedModel
logger = logging.getLogger("bornhack.%s" % __name__)
class UrlType(CreatedUpdatedModel):
"""
Each Url object has a type.
"""
name = models.CharField(
max_length=25,
help_text='The name of this type',
unique=True,
)
icon = models.CharField(
max_length=100,
default='link',
help_text="Name of the fontawesome icon to use without the 'fa-' part"
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Url(CampRelatedModel):
"""
This model contains URLs related to
- SpeakerProposals
- EventProposals
- Speakers
- Events
Each URL has a UrlType and a GenericForeignKey to the model to which it belongs.
When a SpeakerProposal or EventProposal is approved the related URLs will be copied with FK to the new Speaker/Event objects.
"""
uuid = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
url = models.URLField(
help_text='The actual URL'
)
urltype = models.ForeignKey(
'program.UrlType',
help_text='The type of this URL',
on_delete=models.PROTECT,
)
speakerproposal = models.ForeignKey(
'program.SpeakerProposal',
null=True,
blank=True,
help_text='The speaker proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
eventproposal = models.ForeignKey(
'program.EventProposal',
null=True,
blank=True,
help_text='The event proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
speaker = models.ForeignKey(
'program.Speaker',
null=True,
blank=True,
help_text='The speaker proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
event = models.ForeignKey(
'program.Event',
null=True,
blank=True,
help_text='The event proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
def __str__(self):
return self.url
def clean(self):
''' Make sure we have exactly one FK '''
fks = 0
if self.speakerproposal:
fks += 1
if self.eventproposal:
fks += 1
if self.speaker:
fks += 1
if self.event:
fks += 1
if fks > 1:
raise(ValidationError("Url objects must have maximum one FK, this has %s" % fks))
@property
def owner(self):
"""
Return the object this Url belongs to
"""
if self.speakerproposal:
return self.speakerproposal
elif self.eventproposal:
return self.eventproposal
elif self.speaker:
return self.speaker
elif self.event:
return self.event
else:
return None
@property
def camp(self):
return self.owner.camp
###############################################################################
class UserSubmittedModel(CampRelatedModel):
"""
An abstract model containing the stuff that is shared

View File

@ -15,7 +15,19 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">Events</div>
<div class="panel-heading">URLs for {{ eventproposal.title }}</div>
<div class="panel-body">
{% if eventproposal.urls.exists %}
{% include 'includes/eventproposalurl_table.html' %}
{% else %}
<i>Nothing found.</i>
{% endif %}
<a href="{% url 'program:eventproposalurl_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">{{ eventproposal.event_type.host_title }} List</div>
<div class="panel-body">
{% if eventproposal.speakers.exists %}
{% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %}

View File

@ -3,6 +3,7 @@
<tr>
<th>Title</th>
<th>Type</th>
<th>URLs</th>
<th>People</th>
<th>Track</th>
<th>Status</th>
@ -14,6 +15,7 @@
<tr>
<td><span class="h4">{{ eventproposal.title }}</span></td>
<td><i class="fas fa-{{ eventproposal.event_type.icon }} fa-lg" style="color: {{ eventproposal.event_type.color }};"></i><span class="h4"> {{ eventproposal.event_type }}</span></td>
<td><span class="h4">{% for url in eventproposal.urls.all %}<a href="{{ url.url }}"><i class="fas fa-{{ url.urltype.icon }}" data-toggle="tooltip" title="{{ url.urltype.name }}"></i></a> {% empty %}N/A{% endfor %}</span></td>
<td><span class="h4">{% for person in eventproposal.speakers.all %}<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=person.uuid %}"><i class="fas fa-user" data-toggle="tooltip" title="{{ person.name }}"></i></a> {% endfor %}</span></td>
<td><span class="h4">{{ eventproposal.track.name }}</span></td>
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
@ -23,6 +25,7 @@
<i class="fas fa-eye"></i><span class="h5"> Detail</span></a>
{% if not camp.read_only %}
<a href="{% url 'program:eventproposal_update' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
<a href="{% url 'program:eventproposalurl_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success btn-sm"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
{% if eventproposal.get_available_speakerproposals.exists %}
<a href="{% url 'program:eventproposal_selectperson' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success btn-sm"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
{% else %}

View File

@ -0,0 +1,23 @@
<table class="table table-striped">
<thead>
<tr>
<th>Type</th>
<th>URLs</th>
<th class='text-right'>Available Actions</th>
</tr>
</thead>
<tbody>
{% for url in eventproposal.urls.all %}
<tr>
<td><i class="fas fa-{{ url.urltype.icon }} fa-lg"></i><span class="h4"> {{ url.urltype.name }}</span></td>
<td><span class="h4"><a href="{{ url.url }}">{{ url }}</a></span></td>
<td class='text-right'>
{% if not camp.read_only %}
<a href="{% url 'program:eventproposalurl_update' camp_slug=camp.slug event_uuid=eventproposal.uuid url_uuid=url.uuid %}" class="btn btn-success btn-sm"><i class="fas fa-edit"></i><span class="h5"> Update</span></a>
<a href="{% url 'program:eventproposalurl_delete' camp_slug=camp.slug event_uuid=eventproposal.uuid url_uuid=url.uuid %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i><span class="h5"> Delete</span></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -3,6 +3,7 @@
<tr>
<th>Name</th>
<th class="text-center">Events</th>
<th class="text-center">URLs</th>
<th>Status</th>
<th class="text-right">Available Actions</th>
</tr>
@ -20,6 +21,13 @@
N/A
{% endif %}
</td>
<td class="text-center">
{% for url in speakerproposal.urls.all %}
<a href="{{ url.url }}" data-toggle="tooltip" title="{{ url.urltype }}"><i class="fas fa-{{ url.urltype.icon }}"></i></a>
{% empty %}
N/A
{% endfor %}
</td>
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
<td class="text-right">
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}"
@ -27,7 +35,7 @@
<i class="fas fa-eye"></i><span class="h5"> Detail</span></a>
{% if not camp.read_only %}
<a href="{% url 'program:speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
<a href="{% url 'program:eventproposal_typeselect' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm"><i class="fas fa-plus"></i><span class="h5"> Add Event</span></a>
<a href="{% url 'program:speakerproposalurl_create' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
{% if not speakerproposal.eventproposals.all %}
<a href="{% url 'program:speakerproposal_delete' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i><span class="h5"> Delete</span></a>
{% endif %}

View File

@ -0,0 +1,23 @@
<table class="table table-striped">
<thead>
<tr>
<th>Type</th>
<th>URLs</th>
<th class='text-right'>Available Actions</th>
</tr>
</thead>
<tbody>
{% for url in speakerproposal.urls.all %}
<tr>
<td><i class="fas fa-{{ url.urltype.icon }} fa-lg"></i><span class="h4"> {{ url.urltype.name }}</span></td>
<td><span class="h4"><a href="{{ url.url }}">{{ url }}</a></span></td>
<td class='text-right'>
{% if not camp.read_only %}
<a href="{% url 'program:speakerproposalurl_update' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid url_uuid=url.uuid %}" class="btn btn-success btn-sm"><i class="fas fa-edit"></i><span class="h5"> Update</span></a>
<a href="{% url 'program:speakerproposalurl_delete' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid url_uuid=url.uuid %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i><span class="h5"> Delete</span></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -15,7 +15,19 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">Events</div>
<div class="panel-heading">URLs for {{ speakerproposal.name }}</div>
<div class="panel-body">
{% if speakerproposal.urls.exists %}
{% include 'includes/speakerproposalurl_table.html' %}
{% else %}
<i>Nothing found.</i>
{% endif %}
<a href="{% url 'program:speakerproposalurl_create' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Events for {{ speakerproposal.name }}</div>
<div class="panel-body">
{% if speakerproposal.eventproposals.exists %}
{% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %}

View File

@ -0,0 +1,19 @@
{% extends 'program_base.html' %}
{% load bootstrap3 %}
{% block program_content %}
<h3>Delete URL</h3>
<p class="lead">Really delete this URL? This action cannot be undone.</p>
<form method="POST">
{% csrf_token %}
{% bootstrap_button "<i class='fas fa-times'></i> Delete" button_type="submit" button_class="btn-danger" %}
{% if speakerproposal %}
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary">
{% else %}
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary">
{% endif %}
<i class='fas fa-undo'></i> Cancel</a>
</form>
{% endblock program_content %}

View File

@ -0,0 +1,21 @@
{% extends 'program_base.html' %}
{% load bootstrap3 %}
{% block program_content %}
<h3>
{% if object %}
Update URL
{% else %}
Add URL to {% if speakerproposal %}{{ speakerproposal.name }}{% else %}{{ eventproposal.title }}{% endif %}
{% endif %}
</h3>
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button "Save URL" button_type="submit" button_class="btn-primary" %}
</form>
{% endblock program_content %}

View File

@ -1,154 +1,184 @@
from django.conf.urls import include, url
from django.urls import path, include
from .views import *
app_name = 'program'
urlpatterns = [
url(
r'^$',
path(
'',
ScheduleView.as_view(),
name='schedule_index'
),
url(
r'^noscript/$',
path(
'noscript/',
NoScriptScheduleView.as_view(),
name='noscript_schedule_index'
),
url(
r'^ics/', ICSView.as_view(), name="ics_view"
path(
'ics/', ICSView.as_view(), name="ics_view"
),
url(
r'^control/', ProgramControlCenter.as_view(), name="program_control_center"
path(
'control/', ProgramControlCenter.as_view(), name="program_control_center"
),
url(
r'^proposals/', include([
url(
r'^$',
path(
'proposals/', include([
path(
'',
ProposalListView.as_view(),
name='proposal_list',
),
url(
r'^submit/', include([
url(
r'^$',
path(
'submit/', include([
path(
'',
CombinedProposalTypeSelectView.as_view(),
name='proposal_combined_type_select',
),
url(
r'^(?P<event_type_slug>[-_\w+]+)/$',
path(
'<slug:event_type_slug>/',
CombinedProposalSubmitView.as_view(),
name='proposal_combined_submit',
),
url(
r'^(?P<event_type_slug>[-_\w+]+)/select_person/$',
path(
'<slug:event_type_slug>/select_person/',
CombinedProposalPersonSelectView.as_view(),
name='proposal_combined_person_select',
),
]),
),
url(
r'^people/', include([
url(
r'^(?P<pk>[a-f0-9-]+)/$',
path(
'people/', include([
path(
'<uuid:pk>/',
SpeakerProposalDetailView.as_view(),
name='speakerproposal_detail'
),
url(
r'^(?P<pk>[a-f0-9-]+)/update/$',
path(
'<uuid:pk>/update/',
SpeakerProposalUpdateView.as_view(),
name='speakerproposal_update'
),
url(
r'^(?P<pk>[a-f0-9-]+)/delete/$',
path(
'<uuid:pk>/delete/',
SpeakerProposalDeleteView.as_view(),
name='speakerproposal_delete'
),
url(
r'^(?P<speaker_uuid>[a-f0-9-]+)/add_event/$',
path(
'<uuid:speaker_uuid>/add_event/',
EventProposalTypeSelectView.as_view(),
name='eventproposal_typeselect'
),
url(
r'^(?P<speaker_uuid>[a-f0-9-]+)/add_event/(?P<event_type_slug>[-_\w+]+)/$',
path(
'<uuid:speaker_uuid>/add_event/<slug:event_type_slug>/',
EventProposalCreateView.as_view(),
name='eventproposal_create'
),
path(
'<uuid:speaker_uuid>/add_url/',
UrlCreateView.as_view(),
name='speakerproposalurl_create'
),
path(
'<uuid:speaker_uuid>/urls/<uuid:url_uuid>/update/',
UrlUpdateView.as_view(),
name='speakerproposalurl_update'
),
path(
'<uuid:speaker_uuid>/urls/<uuid:url_uuid>/delete/',
UrlDeleteView.as_view(),
name='speakerproposalurl_delete'
),
])
),
url(
r'^events/', include([
url(
r'^(?P<pk>[a-f0-9-]+)/$',
path(
'events/', include([
path(
'<uuid:pk>/',
EventProposalDetailView.as_view(),
name='eventproposal_detail'
),
url(
r'^(?P<pk>[a-f0-9-]+)/edit/$',
path(
'<uuid:pk>/update/',
EventProposalUpdateView.as_view(),
name='eventproposal_update'
),
url(
r'^(?P<pk>[a-f0-9-]+)/delete/$',
path(
'<uuid:pk>/delete/',
EventProposalDeleteView.as_view(),
name='eventproposal_delete'
),
url(
r'^(?P<event_uuid>[a-f0-9-]+)/add_person/$',
path(
'<uuid:event_uuid>/add_person/',
EventProposalSelectPersonView.as_view(),
name='eventproposal_selectperson'
),
url(
r'^(?P<event_uuid>[a-f0-9-]+)/add_person/new/$',
path(
'<uuid:event_uuid>/add_person/new/',
SpeakerProposalCreateView.as_view(),
name='speakerproposal_create'
),
url(
r'^(?P<event_uuid>[a-f0-9-]+)/add_person/(?P<speaker_uuid>[a-f0-9-]+)/$',
path(
'<uuid:event_uuid>/add_person/<uuid:speaker_uuid>/',
EventProposalAddPersonView.as_view(),
name='eventproposal_addperson'
),
path(
'<uuid:event_uuid>/add_url/',
UrlCreateView.as_view(),
name='eventproposalurl_create'
),
path(
'<uuid:event_uuid>/urls/<uuid:url_uuid>/update/',
UrlUpdateView.as_view(),
name='eventproposalurl_update'
),
path(
'<uuid:event_uuid>/urls/<uuid:url_uuid>/delete/',
UrlDeleteView.as_view(),
name='eventproposalurl_delete'
),
])
),
])
),
url(
r'^speakers/', include([
url(
r'^$',
path(
'speakers/', include([
path(
'',
SpeakerListView.as_view(),
name='speaker_index'
),
url(
r'^(?P<slug>[-_\w+]+)/$',
path(
'<slug:slug>/',
SpeakerDetailView.as_view(),
name='speaker_detail'
),
]),
),
url(
r'^events/$',
path(
'events/',
EventListView.as_view(),
name='event_index'
),
# legacy CFS url kept on purpose to keep old links functional
url(
r'^call-for-speakers/$',
path(
'call-for-speakers/',
CallForParticipationView.as_view(),
name='call_for_speakers'
),
url(
r'^call-for-participation/$',
path(
'call-for-participation/',
CallForParticipationView.as_view(),
name='call_for_participation'
),
url(
r'^calendar/',
path(
'calendar',
ICSView.as_view(),
name='ics_calendar'
),
# this must be the last URL here or the regex will overrule the others
url(
r'^(?P<slug>[-_\w+]+)/$',
path(
'<slug:slug>',
EventDetailView.as_view(),
name='event_detail'
),

View File

@ -24,7 +24,8 @@ from .mixins import (
EnsureUnapprovedProposalMixin,
EnsureUserOwnsProposalMixin,
EnsureWritableCampMixin,
EnsureCFPOpenMixin
EnsureCFPOpenMixin,
UrlViewMixin,
)
from .email import (
add_speakerproposal_updated_email,
@ -600,3 +601,40 @@ class ProgramControlCenter(CampViewMixin, TemplateView):
return context
###################################################################################################
# URL views
class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, CreateView):
model = models.Url
template_name = 'url_form.html'
fields = ['urltype', 'url']
def form_valid(self, form):
"""
Set the proposal FK before saving
"""
if hasattr(self, 'eventproposal') and self.eventproposal:
form.instance.eventproposal = self.eventproposal
url = form.save()
else:
form.instance.speakerproposal = self.speakerproposal
url = form.save()
messages.success(self.request, "URL saved.")
# all good
return redirect(self.get_success_url())
class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, UpdateView):
model = models.Url
template_name = 'url_form.html'
fields = ['urltype', 'url']
pk_url_kwarg = 'url_uuid'
class UrlDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, DeleteView):
model = models.Url
template_name = 'url_delete.html'
pk_url_kwarg = 'url_uuid'

View File

@ -1,30 +1,31 @@
from django.conf.urls import url
from django.urls import path, include
from .views import *
app_name = 'shop'
urlpatterns = [
url(r'^$', ShopIndexView.as_view(), name='index'),
path('', ShopIndexView.as_view(), name='index'),
url(r'products/(?P<slug>[-_\w+]+)/$', ProductDetailView.as_view(), name='product_detail'),
path('products/<slug:slug>/', ProductDetailView.as_view(), name='product_detail'),
url(r'orders/$', OrderListView.as_view(), name='order_list'),
url(r'orders/(?P<pk>[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'),
url(r'orders/(?P<pk>[0-9]+)/invoice/$', DownloadInvoiceView.as_view(), name='download_invoice'),
url(r'orders/(?P<pk>[0-9]+)/mark_as_paid/$', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'),
path('orders/', OrderListView.as_view(), name='order_list'),
path('orders/<int:pk>/', include([
path('', OrderDetailView.as_view(), name='order_detail'),
path('invoice/', DownloadInvoiceView.as_view(), name='download_invoice'),
path('mark_as_paid/', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'),
url(r'orders/(?P<pk>[0-9]+)/pay/creditcard/$', EpayFormView.as_view(), name='epay_form'),
url(r'orders/(?P<pk>[0-9]+)/pay/creditcard/callback/$',EpayCallbackView.as_view(), name='epay_callback'),
url(r'orders/(?P<pk>[0-9]+)/pay/creditcard/thanks/$', EpayThanksView.as_view(), name='epay_thanks'),
path('pay/creditcard/', EpayFormView.as_view(), name='epay_form'),
path('pay/creditcard/callback/',EpayCallbackView.as_view(), name='epay_callback'),
path('pay/creditcard/thanks/', EpayThanksView.as_view(), name='epay_thanks'),
url(r'orders/(?P<pk>[0-9]+)/pay/blockchain/$', CoinifyRedirectView.as_view(), name='coinify_pay'),
url(r'orders/(?P<pk>[0-9]+)/pay/blockchain/callback/$', CoinifyCallbackView.as_view(), name='coinify_callback'),
url(r'orders/(?P<pk>[0-9]+)/pay/blockchain/thanks/$', CoinifyThanksView.as_view(), name='coinify_thanks'),
path('pay/blockchain/', CoinifyRedirectView.as_view(), name='coinify_pay'),
path('pay/blockchain/callback/', CoinifyCallbackView.as_view(), name='coinify_callback'),
path('pay/blockchain/thanks/', CoinifyThanksView.as_view(), name='coinify_thanks'),
url(r'orders/(?P<pk>[0-9]+)/pay/banktransfer/$', BankTransferView.as_view(), name='bank_transfer'),
path('pay/banktransfer/', BankTransferView.as_view(), name='bank_transfer'),
url(r'orders/(?P<pk>[0-9]+)/pay/cash/$', CashView.as_view(), name='cash'),
url(r'creditnotes/$', CreditNoteListView.as_view(), name='creditnote_list'),
url(r'creditnotes/(?P<pk>[0-9]+)/pdf/$', DownloadCreditNoteView.as_view(), name='download_creditnote'),
path('pay/cash/', CashView.as_view(), name='cash'),
])),
path('creditnotes/', CreditNoteListView.as_view(), name='creditnote_list'),
path('creditnotes/<int:pk>/pdf/', DownloadCreditNoteView.as_view(), name='download_creditnote'),
]

View File

@ -1,72 +1,72 @@
from django.conf.urls import url, include
from django.urls import path, include
from .views import *
app_name = 'teams'
urlpatterns = [
url(
r'^$',
path(
'',
TeamListView.as_view(),
name='list'
),
url(
r'^members/', include([
url(
r'^(?P<pk>[0-9]+)/remove/$',
path(
'members/', include([
path(
'<int:pk>/remove/',
TeamMemberRemoveView.as_view(),
name='teammember_remove',
),
url(
r'^(?P<pk>[0-9]+)/approve/$',
path(
'<int:pk>/approve/',
TeamMemberApproveView.as_view(),
name='teammember_approve',
),
]),
),
url(
r'^(?P<team_slug>[-_\w+]+)/', include([
url(
r'^$',
path(
'<slug:team_slug>/', include([
path(
'',
TeamDetailView.as_view(),
name='detail'
),
url(
r'^join/$',
path(
'join/',
TeamJoinView.as_view(),
name='join'
),
url(
r'^leave/$',
path(
'leave/',
TeamLeaveView.as_view(),
name='leave'
),
url(
r'^manage/$',
path(
'manage/',
TeamManageView.as_view(),
name='manage'
),
url(
r'^fix_irc_acl/$',
path(
'fix_irc_acl/',
FixIrcAclView.as_view(),
name='fix_irc_acl',
),
url(
r'^tasks/', include([
url(
r'^create/$',
path(
'tasks/', include([
path(
'create/',
TaskCreateView.as_view(),
name='task_create',
),
url(
r'^(?P<slug>[-_\w+]+)/', include([
url(
r'^$',
path(
'<slug:slug>/', include([
path(
'',
TaskDetailView.as_view(),
name='task_detail',
),
url(
r'^update/$',
path(
'update/',
TaskUpdateView.as_view(),
name='task_update',
),

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from .views import (
ShopTicketListView,
@ -9,18 +9,18 @@ from .views import (
app_name = 'tickets'
urlpatterns = [
url(
r'^$',
path(
'',
ShopTicketListView.as_view(),
name='shopticket_list'
),
url(
r'^(?P<pk>\b[0-9A-Fa-f]{8}\b(-\b[0-9A-Fa-f]{4}\b){3}-\b[0-9A-Fa-f]{12}\b)/download/$',
path(
'<uuid:pk>/download/',
ShopTicketDownloadView.as_view(),
name='shopticket_download'
),
url(
r'^(?P<pk>\b[0-9A-Fa-f]{8}\b(-\b[0-9A-Fa-f]{4}\b){3}-\b[0-9A-Fa-f]{12}\b)/edit/$',
path(
'<uuid:pk>/edit/',
ShopTicketDetailView.as_view(),
name='shopticket_edit'
),

View File

@ -1,13 +1,13 @@
from django.conf.urls import url
from django.urls import path
from .views import *
app_name = 'villages'
urlpatterns = [
url(r'^$', VillageListView.as_view(), name='list'),
url(r'create/$', VillageCreateView.as_view(), name='create'),
url(r'(?P<slug>[-_\w+]+)/delete/$', VillageDeleteView.as_view(), name='delete'),
url(r'(?P<slug>[-_\w+]+)/edit/$', VillageUpdateView.as_view(), name='update'),
url(r'(?P<slug>[-_\w+]+)/$', VillageDetailView.as_view(), name='detail'),
path('', VillageListView.as_view(), name='list'),
path('create/', VillageCreateView.as_view(), name='create'),
path('<slug:slug>/delete/', VillageDeleteView.as_view(), name='delete'),
path('<slug:slug>/edit/', VillageUpdateView.as_view(), name='update'),
path('<slug:slug>/', VillageDetailView.as_view(), name='detail'),
]