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 * from .views import *
app_name = 'backoffice' app_name = 'backoffice'
urlpatterns = [ urlpatterns = [
url(r'^$', BackofficeIndexView.as_view(), name='index'), path('', BackofficeIndexView.as_view(), name='index'),
url(r'product_handout/$', ProductHandoutView.as_view(), name='product_handout'), path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
url(r'badge_handout/$', BadgeHandoutView.as_view(), name='badge_handout'), path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
url(r'ticket_checkin/$', TicketCheckinView.as_view(), name='ticket_checkin'), path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
url(r'public_credit_names/$', ApproveNamesView.as_view(), name='public_credit_names'), path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
] ]

View file

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

View file

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

View file

@ -14,7 +14,9 @@ from .models import (
EventTrack, EventTrack,
SpeakerProposal, SpeakerProposal,
EventProposal, EventProposal,
Favorite Favorite,
UrlType,
Url
) )
@ -98,3 +100,11 @@ class EventAdmin(admin.ModelAdmin):
SpeakerInline 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.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 django.urls import reverse
from . import models from . import models
from django.contrib import messages from django.contrib import messages
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
import sys
import mimetypes
class EnsureCFPOpenMixin(object): class EnsureCFPOpenMixin(object):
@ -55,3 +53,42 @@ class EnsureUserOwnsProposalMixin(SingleObjectMixin):
# alright, continue with the request # alright, continue with the request
return super().dispatch(request, *args, **kwargs) 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.urls import reverse
from django.apps import apps from django.apps import apps
from django.core.files.base import ContentFile 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 from utils.models import CreatedUpdatedModel, CampRelatedModel
logger = logging.getLogger("bornhack.%s" % __name__) 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): class UserSubmittedModel(CampRelatedModel):
""" """
An abstract model containing the stuff that is shared An abstract model containing the stuff that is shared

View file

@ -15,7 +15,19 @@
</div> </div>
<div class="panel panel-default"> <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"> <div class="panel-body">
{% if eventproposal.speakers.exists %} {% if eventproposal.speakers.exists %}
{% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %} {% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %}

View file

@ -3,6 +3,7 @@
<tr> <tr>
<th>Title</th> <th>Title</th>
<th>Type</th> <th>Type</th>
<th>URLs</th>
<th>People</th> <th>People</th>
<th>Track</th> <th>Track</th>
<th>Status</th> <th>Status</th>
@ -14,6 +15,7 @@
<tr> <tr>
<td><span class="h4">{{ eventproposal.title }}</span></td> <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><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">{% 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="h4">{{ eventproposal.track.name }}</span></td>
<td><span class="badge">{{ eventproposal.proposal_status }}</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> <i class="fas fa-eye"></i><span class="h5"> Detail</span></a>
{% if not camp.read_only %} {% 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: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 %} {% 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> <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 %} {% 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> <tr>
<th>Name</th> <th>Name</th>
<th class="text-center">Events</th> <th class="text-center">Events</th>
<th class="text-center">URLs</th>
<th>Status</th> <th>Status</th>
<th class="text-right">Available Actions</th> <th class="text-right">Available Actions</th>
</tr> </tr>
@ -20,6 +21,13 @@
N/A N/A
{% endif %} {% endif %}
</td> </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><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
<td class="text-right"> <td class="text-right">
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" <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> <i class="fas fa-eye"></i><span class="h5"> Detail</span></a>
{% if not camp.read_only %} {% 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: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 %} {% 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> <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 %} {% 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>
<div class="panel panel-default"> <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"> <div class="panel-body">
{% if speakerproposal.eventproposals.exists %} {% if speakerproposal.eventproposals.exists %}
{% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %} {% 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 * from .views import *
app_name = 'program' app_name = 'program'
urlpatterns = [ urlpatterns = [
url( path(
r'^$', '',
ScheduleView.as_view(), ScheduleView.as_view(),
name='schedule_index' name='schedule_index'
), ),
url( path(
r'^noscript/$', 'noscript/',
NoScriptScheduleView.as_view(), NoScriptScheduleView.as_view(),
name='noscript_schedule_index' name='noscript_schedule_index'
), ),
url( path(
r'^ics/', ICSView.as_view(), name="ics_view" 'ics/', ICSView.as_view(), name="ics_view"
), ),
url( path(
r'^control/', ProgramControlCenter.as_view(), name="program_control_center" 'control/', ProgramControlCenter.as_view(), name="program_control_center"
), ),
url( path(
r'^proposals/', include([ 'proposals/', include([
url( path(
r'^$', '',
ProposalListView.as_view(), ProposalListView.as_view(),
name='proposal_list', name='proposal_list',
), ),
url( path(
r'^submit/', include([ 'submit/', include([
url( path(
r'^$', '',
CombinedProposalTypeSelectView.as_view(), CombinedProposalTypeSelectView.as_view(),
name='proposal_combined_type_select', name='proposal_combined_type_select',
), ),
url( path(
r'^(?P<event_type_slug>[-_\w+]+)/$', '<slug:event_type_slug>/',
CombinedProposalSubmitView.as_view(), CombinedProposalSubmitView.as_view(),
name='proposal_combined_submit', name='proposal_combined_submit',
), ),
url( path(
r'^(?P<event_type_slug>[-_\w+]+)/select_person/$', '<slug:event_type_slug>/select_person/',
CombinedProposalPersonSelectView.as_view(), CombinedProposalPersonSelectView.as_view(),
name='proposal_combined_person_select', name='proposal_combined_person_select',
), ),
]), ]),
), ),
url( path(
r'^people/', include([ 'people/', include([
url( path(
r'^(?P<pk>[a-f0-9-]+)/$', '<uuid:pk>/',
SpeakerProposalDetailView.as_view(), SpeakerProposalDetailView.as_view(),
name='speakerproposal_detail' name='speakerproposal_detail'
), ),
url( path(
r'^(?P<pk>[a-f0-9-]+)/update/$', '<uuid:pk>/update/',
SpeakerProposalUpdateView.as_view(), SpeakerProposalUpdateView.as_view(),
name='speakerproposal_update' name='speakerproposal_update'
), ),
url( path(
r'^(?P<pk>[a-f0-9-]+)/delete/$', '<uuid:pk>/delete/',
SpeakerProposalDeleteView.as_view(), SpeakerProposalDeleteView.as_view(),
name='speakerproposal_delete' name='speakerproposal_delete'
), ),
url( path(
r'^(?P<speaker_uuid>[a-f0-9-]+)/add_event/$', '<uuid:speaker_uuid>/add_event/',
EventProposalTypeSelectView.as_view(), EventProposalTypeSelectView.as_view(),
name='eventproposal_typeselect' name='eventproposal_typeselect'
), ),
url( path(
r'^(?P<speaker_uuid>[a-f0-9-]+)/add_event/(?P<event_type_slug>[-_\w+]+)/$', '<uuid:speaker_uuid>/add_event/<slug:event_type_slug>/',
EventProposalCreateView.as_view(), EventProposalCreateView.as_view(),
name='eventproposal_create' 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( path(
r'^events/', include([ 'events/', include([
url( path(
r'^(?P<pk>[a-f0-9-]+)/$', '<uuid:pk>/',
EventProposalDetailView.as_view(), EventProposalDetailView.as_view(),
name='eventproposal_detail' name='eventproposal_detail'
), ),
url( path(
r'^(?P<pk>[a-f0-9-]+)/edit/$', '<uuid:pk>/update/',
EventProposalUpdateView.as_view(), EventProposalUpdateView.as_view(),
name='eventproposal_update' name='eventproposal_update'
), ),
url( path(
r'^(?P<pk>[a-f0-9-]+)/delete/$', '<uuid:pk>/delete/',
EventProposalDeleteView.as_view(), EventProposalDeleteView.as_view(),
name='eventproposal_delete' name='eventproposal_delete'
), ),
url( path(
r'^(?P<event_uuid>[a-f0-9-]+)/add_person/$', '<uuid:event_uuid>/add_person/',
EventProposalSelectPersonView.as_view(), EventProposalSelectPersonView.as_view(),
name='eventproposal_selectperson' name='eventproposal_selectperson'
), ),
url( path(
r'^(?P<event_uuid>[a-f0-9-]+)/add_person/new/$', '<uuid:event_uuid>/add_person/new/',
SpeakerProposalCreateView.as_view(), SpeakerProposalCreateView.as_view(),
name='speakerproposal_create' name='speakerproposal_create'
), ),
url( path(
r'^(?P<event_uuid>[a-f0-9-]+)/add_person/(?P<speaker_uuid>[a-f0-9-]+)/$', '<uuid:event_uuid>/add_person/<uuid:speaker_uuid>/',
EventProposalAddPersonView.as_view(), EventProposalAddPersonView.as_view(),
name='eventproposal_addperson' 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( path(
r'^speakers/', include([ 'speakers/', include([
url( path(
r'^$', '',
SpeakerListView.as_view(), SpeakerListView.as_view(),
name='speaker_index' name='speaker_index'
), ),
url( path(
r'^(?P<slug>[-_\w+]+)/$', '<slug:slug>/',
SpeakerDetailView.as_view(), SpeakerDetailView.as_view(),
name='speaker_detail' name='speaker_detail'
), ),
]), ]),
), ),
url( path(
r'^events/$', 'events/',
EventListView.as_view(), EventListView.as_view(),
name='event_index' name='event_index'
), ),
# legacy CFS url kept on purpose to keep old links functional # legacy CFS url kept on purpose to keep old links functional
url( path(
r'^call-for-speakers/$', 'call-for-speakers/',
CallForParticipationView.as_view(), CallForParticipationView.as_view(),
name='call_for_speakers' name='call_for_speakers'
), ),
url( path(
r'^call-for-participation/$', 'call-for-participation/',
CallForParticipationView.as_view(), CallForParticipationView.as_view(),
name='call_for_participation' name='call_for_participation'
), ),
url( path(
r'^calendar/', 'calendar',
ICSView.as_view(), ICSView.as_view(),
name='ics_calendar' name='ics_calendar'
), ),
# this must be the last URL here or the regex will overrule the others # this must be the last URL here or the regex will overrule the others
url( path(
r'^(?P<slug>[-_\w+]+)/$', '<slug:slug>',
EventDetailView.as_view(), EventDetailView.as_view(),
name='event_detail' name='event_detail'
), ),

View file

@ -24,7 +24,8 @@ from .mixins import (
EnsureUnapprovedProposalMixin, EnsureUnapprovedProposalMixin,
EnsureUserOwnsProposalMixin, EnsureUserOwnsProposalMixin,
EnsureWritableCampMixin, EnsureWritableCampMixin,
EnsureCFPOpenMixin EnsureCFPOpenMixin,
UrlViewMixin,
) )
from .email import ( from .email import (
add_speakerproposal_updated_email, add_speakerproposal_updated_email,
@ -600,3 +601,40 @@ class ProgramControlCenter(CampViewMixin, TemplateView):
return context 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 * from .views import *
app_name = 'shop' app_name = 'shop'
urlpatterns = [ 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'), path('orders/', OrderListView.as_view(), name='order_list'),
url(r'orders/(?P<pk>[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'), path('orders/<int:pk>/', include([
url(r'orders/(?P<pk>[0-9]+)/invoice/$', DownloadInvoiceView.as_view(), name='download_invoice'), path('', OrderDetailView.as_view(), name='order_detail'),
url(r'orders/(?P<pk>[0-9]+)/mark_as_paid/$', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'), 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'), path('pay/creditcard/', EpayFormView.as_view(), name='epay_form'),
url(r'orders/(?P<pk>[0-9]+)/pay/creditcard/callback/$',EpayCallbackView.as_view(), name='epay_callback'), path('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/thanks/', EpayThanksView.as_view(), name='epay_thanks'),
url(r'orders/(?P<pk>[0-9]+)/pay/blockchain/$', CoinifyRedirectView.as_view(), name='coinify_pay'), path('pay/blockchain/', CoinifyRedirectView.as_view(), name='coinify_pay'),
url(r'orders/(?P<pk>[0-9]+)/pay/blockchain/callback/$', CoinifyCallbackView.as_view(), name='coinify_callback'), path('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/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'), path('pay/cash/', CashView.as_view(), name='cash'),
])),
url(r'creditnotes/$', CreditNoteListView.as_view(), name='creditnote_list'), path('creditnotes/', CreditNoteListView.as_view(), name='creditnote_list'),
url(r'creditnotes/(?P<pk>[0-9]+)/pdf/$', DownloadCreditNoteView.as_view(), name='download_creditnote'), 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 * from .views import *
app_name = 'teams' app_name = 'teams'
urlpatterns = [ urlpatterns = [
url( path(
r'^$', '',
TeamListView.as_view(), TeamListView.as_view(),
name='list' name='list'
), ),
url( path(
r'^members/', include([ 'members/', include([
url( path(
r'^(?P<pk>[0-9]+)/remove/$', '<int:pk>/remove/',
TeamMemberRemoveView.as_view(), TeamMemberRemoveView.as_view(),
name='teammember_remove', name='teammember_remove',
), ),
url( path(
r'^(?P<pk>[0-9]+)/approve/$', '<int:pk>/approve/',
TeamMemberApproveView.as_view(), TeamMemberApproveView.as_view(),
name='teammember_approve', name='teammember_approve',
), ),
]), ]),
), ),
url( path(
r'^(?P<team_slug>[-_\w+]+)/', include([ '<slug:team_slug>/', include([
url( path(
r'^$', '',
TeamDetailView.as_view(), TeamDetailView.as_view(),
name='detail' name='detail'
), ),
url( path(
r'^join/$', 'join/',
TeamJoinView.as_view(), TeamJoinView.as_view(),
name='join' name='join'
), ),
url( path(
r'^leave/$', 'leave/',
TeamLeaveView.as_view(), TeamLeaveView.as_view(),
name='leave' name='leave'
), ),
url( path(
r'^manage/$', 'manage/',
TeamManageView.as_view(), TeamManageView.as_view(),
name='manage' name='manage'
), ),
url( path(
r'^fix_irc_acl/$', 'fix_irc_acl/',
FixIrcAclView.as_view(), FixIrcAclView.as_view(),
name='fix_irc_acl', name='fix_irc_acl',
), ),
url( path(
r'^tasks/', include([ 'tasks/', include([
url( path(
r'^create/$', 'create/',
TaskCreateView.as_view(), TaskCreateView.as_view(),
name='task_create', name='task_create',
), ),
url( path(
r'^(?P<slug>[-_\w+]+)/', include([ '<slug:slug>/', include([
url( path(
r'^$', '',
TaskDetailView.as_view(), TaskDetailView.as_view(),
name='task_detail', name='task_detail',
), ),
url( path(
r'^update/$', 'update/',
TaskUpdateView.as_view(), TaskUpdateView.as_view(),
name='task_update', name='task_update',
), ),

View file

@ -1,4 +1,4 @@
from django.conf.urls import url from django.urls import path
from .views import ( from .views import (
ShopTicketListView, ShopTicketListView,
@ -9,18 +9,18 @@ from .views import (
app_name = 'tickets' app_name = 'tickets'
urlpatterns = [ urlpatterns = [
url( path(
r'^$', '',
ShopTicketListView.as_view(), ShopTicketListView.as_view(),
name='shopticket_list' name='shopticket_list'
), ),
url( path(
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/$', '<uuid:pk>/download/',
ShopTicketDownloadView.as_view(), ShopTicketDownloadView.as_view(),
name='shopticket_download' name='shopticket_download'
), ),
url( path(
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/$', '<uuid:pk>/edit/',
ShopTicketDetailView.as_view(), ShopTicketDetailView.as_view(),
name='shopticket_edit' name='shopticket_edit'
), ),

View file

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