Merge branch 'master' into feature/team_controlled_info
This commit is contained in:
commit
635e57b7f9
|
@ -46,8 +46,6 @@ speakerDecoder =
|
||||||
|> required "name" string
|
|> required "name" string
|
||||||
|> required "slug" string
|
|> required "slug" string
|
||||||
|> required "biography" string
|
|> required "biography" string
|
||||||
|> optional "large_picture_url" (nullable string) Nothing
|
|
||||||
|> optional "small_picture_url" (nullable string) Nothing
|
|
||||||
|
|
||||||
|
|
||||||
eventDecoder : Decoder Event
|
eventDecoder : Decoder Event
|
||||||
|
@ -82,6 +80,7 @@ eventInstanceDecoder =
|
||||||
|> required "url" string
|
|> required "url" string
|
||||||
|> required "event_slug" string
|
|> required "event_slug" string
|
||||||
|> required "event_type" string
|
|> required "event_type" string
|
||||||
|
|> required "event_track" string
|
||||||
|> required "bg-color" string
|
|> required "bg-color" string
|
||||||
|> required "fg-color" string
|
|> required "fg-color" string
|
||||||
|> required "from" dateDecoder
|
|> required "from" dateDecoder
|
||||||
|
@ -111,6 +110,13 @@ eventTypeDecoder =
|
||||||
|> required "light_text" bool
|
|> required "light_text" bool
|
||||||
|
|
||||||
|
|
||||||
|
eventTrackDecoder : Decoder FilterType
|
||||||
|
eventTrackDecoder =
|
||||||
|
decode TrackFilter
|
||||||
|
|> required "name" string
|
||||||
|
|> required "slug" string
|
||||||
|
|
||||||
|
|
||||||
initDataDecoder : Decoder (Flags -> Filter -> Location -> Route -> Bool -> Model)
|
initDataDecoder : Decoder (Flags -> Filter -> Location -> Route -> Bool -> Model)
|
||||||
initDataDecoder =
|
initDataDecoder =
|
||||||
decode Model
|
decode Model
|
||||||
|
@ -119,4 +125,5 @@ initDataDecoder =
|
||||||
|> required "event_instances" (list eventInstanceDecoder)
|
|> required "event_instances" (list eventInstanceDecoder)
|
||||||
|> required "event_locations" (list eventLocationDecoder)
|
|> required "event_locations" (list eventLocationDecoder)
|
||||||
|> required "event_types" (list eventTypeDecoder)
|
|> required "event_types" (list eventTypeDecoder)
|
||||||
|
|> required "event_tracks" (list eventTrackDecoder)
|
||||||
|> required "speakers" (list speakerDecoder)
|
|> required "speakers" (list speakerDecoder)
|
||||||
|
|
|
@ -34,10 +34,10 @@ init flags location =
|
||||||
parseLocation location
|
parseLocation location
|
||||||
|
|
||||||
emptyFilter =
|
emptyFilter =
|
||||||
Filter [] [] []
|
Filter [] [] [] []
|
||||||
|
|
||||||
model =
|
model =
|
||||||
Model [] [] [] [] [] [] flags emptyFilter location currentRoute False
|
Model [] [] [] [] [] [] [] flags emptyFilter location currentRoute False
|
||||||
in
|
in
|
||||||
model ! [ sendInitMessage flags.camp_slug flags.websocket_server ]
|
model ! [ sendInitMessage flags.camp_slug flags.websocket_server ]
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ type alias Model =
|
||||||
, eventInstances : List EventInstance
|
, eventInstances : List EventInstance
|
||||||
, eventLocations : List FilterType
|
, eventLocations : List FilterType
|
||||||
, eventTypes : List FilterType
|
, eventTypes : List FilterType
|
||||||
|
, eventTracks : List FilterType
|
||||||
, speakers : List Speaker
|
, speakers : List Speaker
|
||||||
, flags : Flags
|
, flags : Flags
|
||||||
, filter : Filter
|
, filter : Filter
|
||||||
|
@ -69,8 +70,6 @@ type alias Speaker =
|
||||||
{ name : String
|
{ name : String
|
||||||
, slug : SpeakerSlug
|
, slug : SpeakerSlug
|
||||||
, biography : String
|
, biography : String
|
||||||
, largePictureUrl : Maybe String
|
|
||||||
, smallPictureUrl : Maybe String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,6 +80,7 @@ type alias EventInstance =
|
||||||
, url : String
|
, url : String
|
||||||
, eventSlug : EventSlug
|
, eventSlug : EventSlug
|
||||||
, eventType : String
|
, eventType : String
|
||||||
|
, eventTrack : String
|
||||||
, backgroundColor : String
|
, backgroundColor : String
|
||||||
, forgroundColor : String
|
, forgroundColor : String
|
||||||
, from : Date
|
, from : Date
|
||||||
|
@ -142,11 +142,13 @@ type FilterType
|
||||||
= TypeFilter FilterName FilterSlug TypeColor TypeLightText
|
= TypeFilter FilterName FilterSlug TypeColor TypeLightText
|
||||||
| LocationFilter FilterName FilterSlug LocationIcon
|
| LocationFilter FilterName FilterSlug LocationIcon
|
||||||
| VideoFilter FilterName FilterSlug
|
| VideoFilter FilterName FilterSlug
|
||||||
|
| TrackFilter FilterName FilterSlug
|
||||||
|
|
||||||
|
|
||||||
type alias Filter =
|
type alias Filter =
|
||||||
{ eventTypes : List FilterType
|
{ eventTypes : List FilterType
|
||||||
, eventLocations : List FilterType
|
, eventLocations : List FilterType
|
||||||
|
, eventTracks : List FilterType
|
||||||
, videoRecording : List FilterType
|
, videoRecording : List FilterType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +164,9 @@ unpackFilterType filter =
|
||||||
VideoFilter name slug ->
|
VideoFilter name slug ->
|
||||||
( name, slug )
|
( name, slug )
|
||||||
|
|
||||||
|
TrackFilter name slug ->
|
||||||
|
( name, slug )
|
||||||
|
|
||||||
|
|
||||||
getSlugFromFilterType filter =
|
getSlugFromFilterType filter =
|
||||||
let
|
let
|
||||||
|
|
|
@ -96,6 +96,19 @@ update msg model =
|
||||||
videoRecording :: model.filter.videoRecording
|
videoRecording :: model.filter.videoRecording
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackFilter name slug ->
|
||||||
|
let
|
||||||
|
eventTrack =
|
||||||
|
TrackFilter name slug
|
||||||
|
in
|
||||||
|
{ currentFilter
|
||||||
|
| eventTracks =
|
||||||
|
if List.member eventTrack model.filter.eventTracks then
|
||||||
|
List.filter (\x -> x /= eventTrack) model.filter.videoRecording
|
||||||
|
else
|
||||||
|
eventTrack :: model.filter.eventTracks
|
||||||
|
}
|
||||||
|
|
||||||
query =
|
query =
|
||||||
filterToQuery newFilter
|
filterToQuery newFilter
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ applyFilters day model =
|
||||||
locations =
|
locations =
|
||||||
slugs model.eventLocations model.filter.eventLocations
|
slugs model.eventLocations model.filter.eventLocations
|
||||||
|
|
||||||
|
tracks =
|
||||||
|
slugs model.eventTracks model.filter.eventTracks
|
||||||
|
|
||||||
videoFilters =
|
videoFilters =
|
||||||
slugs videoRecordingFilters model.filter.videoRecording
|
slugs videoRecordingFilters model.filter.videoRecording
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ applyFilters day model =
|
||||||
&& (Date.Extra.equalBy Date.Extra.Day eventInstance.from day.date)
|
&& (Date.Extra.equalBy Date.Extra.Day eventInstance.from day.date)
|
||||||
&& List.member eventInstance.location locations
|
&& List.member eventInstance.location locations
|
||||||
&& List.member eventInstance.eventType types
|
&& List.member eventInstance.eventType types
|
||||||
|
&& List.member eventInstance.eventTrack tracks
|
||||||
&& List.member eventInstance.videoState videoFilters
|
&& List.member eventInstance.videoState videoFilters
|
||||||
)
|
)
|
||||||
model.eventInstances
|
model.eventInstances
|
||||||
|
@ -77,6 +81,12 @@ filterSidebar model =
|
||||||
model.filter.eventLocations
|
model.filter.eventLocations
|
||||||
model.eventInstances
|
model.eventInstances
|
||||||
.location
|
.location
|
||||||
|
, filterView
|
||||||
|
"Track"
|
||||||
|
model.eventTracks
|
||||||
|
model.filter.eventTracks
|
||||||
|
model.eventInstances
|
||||||
|
.eventTrack
|
||||||
, filterView
|
, filterView
|
||||||
"Video"
|
"Video"
|
||||||
videoRecordingFilters
|
videoRecordingFilters
|
||||||
|
@ -309,11 +319,15 @@ parseFilterFromQuery query model =
|
||||||
locations =
|
locations =
|
||||||
getFilter "location" model.eventLocations query
|
getFilter "location" model.eventLocations query
|
||||||
|
|
||||||
|
tracks =
|
||||||
|
getFilter "tracks" model.eventTracks query
|
||||||
|
|
||||||
videoFilters =
|
videoFilters =
|
||||||
getFilter "video" videoRecordingFilters query
|
getFilter "video" videoRecordingFilters query
|
||||||
in
|
in
|
||||||
{ eventTypes = types
|
{ eventTypes = types
|
||||||
, eventLocations = locations
|
, eventLocations = locations
|
||||||
|
, eventTracks = tracks
|
||||||
, videoRecording = videoFilters
|
, videoRecording = videoFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,33 +22,18 @@ speakerDetailView speakerSlug model =
|
||||||
model.speakers
|
model.speakers
|
||||||
|> List.filter (\speaker -> speaker.slug == speakerSlug)
|
|> List.filter (\speaker -> speaker.slug == speakerSlug)
|
||||||
|> List.head
|
|> List.head
|
||||||
|
|
||||||
image =
|
|
||||||
case speaker of
|
|
||||||
Just speaker ->
|
|
||||||
case speaker.smallPictureUrl of
|
|
||||||
Just smallPictureUrl ->
|
|
||||||
[ img [ src smallPictureUrl ] [] ]
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
[]
|
|
||||||
in
|
in
|
||||||
case speaker of
|
case speaker of
|
||||||
Just speaker ->
|
Just speaker ->
|
||||||
div []
|
div []
|
||||||
([ a [ onClick BackInHistory, classList [ ( "btn", True ), ( "btn-default", True ) ] ]
|
[ a [ onClick BackInHistory, classList [ ( "btn", True ), ( "btn-default", True ) ] ]
|
||||||
[ i [ classList [ ( "fa", True ), ( "fa-chevron-left", True ) ] ] []
|
[ i [ classList [ ( "fa", True ), ( "fa-chevron-left", True ) ] ] []
|
||||||
, text " Back"
|
, text " Back"
|
||||||
]
|
]
|
||||||
, h3 [] [ text speaker.name ]
|
, h3 [] [ text speaker.name ]
|
||||||
, div [] [ Markdown.toHtml [] speaker.biography ]
|
, div [] [ Markdown.toHtml [] speaker.biography ]
|
||||||
, speakerEvents speaker model
|
, speakerEvents speaker model
|
||||||
]
|
]
|
||||||
++ image
|
|
||||||
)
|
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
div [] [ text "Unknown speaker..." ]
|
div [] [ text "Unknown speaker..." ]
|
||||||
|
|
10
src/backoffice/mixins.py
Normal file
10
src/backoffice/mixins.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from camps.mixins import CampViewMixin
|
||||||
|
from utils.mixins import StaffMemberRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
|
class BackofficeViewMixin(CampViewMixin, StaffMemberRequiredMixin):
|
||||||
|
"""
|
||||||
|
Mixin used by all backoffice views. For now just uses CampViewMixin and StaffMemberRequiredMixin.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>Hand Out Badges</h2>
|
<h2>Hand Out Badges</h2>
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' %}">Ticket Checkin view</a> instead. To hand out merchandise and other products go to the <a href="{% url 'backoffice:product_handout' %}">Hand Out Products</a> view instead.
|
Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}">Ticket Checkin view</a> instead. To hand out merchandise and other products go to the <a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}">Hand Out Products</a> view instead.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
This table shows all (Shop|Discount|Sponsor)Tickets which are badge_handed_out=False. Tickets must be checked in before they are shown in this list.
|
This table shows all (Shop|Discount|Sponsor)Tickets which are badge_handed_out=False. Tickets must be checked in before they are shown in this list.
|
||||||
|
|
24
src/backoffice/templates/camp_select.html
Normal file
24
src/backoffice/templates/camp_select.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load imageutils %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h2>BornHack Backoffice Camp Picker</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p>
|
||||||
|
<div class="list-group">
|
||||||
|
{% for camp in camp_list %}
|
||||||
|
<a href="{% url 'backoffice:camp_index' camp_slug=camp.slug %}" class="list-group-item">
|
||||||
|
<h4 class="list-group-item-heading">{{ camp.title }}</h4>
|
||||||
|
<p class="list-group-item-text">Manage {{ camp.title }}</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>BornHack Backoffice</h2>
|
<h2>{{ camp.title }} Backoffice</h2>
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
Welcome to the promised land! Please select your desired action below:
|
Welcome to the promised land! Please select your desired action below:
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,22 +14,26 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a href="{% url 'backoffice:product_handout' %}" class="list-group-item">
|
<a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}" class="list-group-item">
|
||||||
<h4 class="list-group-item-heading">Hand Out Products</h4>
|
<h4 class="list-group-item-heading">Hand Out Products</h4>
|
||||||
<p class="list-group-item-text">Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.</p>
|
<p class="list-group-item-text">Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'backoffice:ticket_checkin' %}" class="list-group-item">
|
<a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}" class="list-group-item">
|
||||||
<h4 class="list-group-item-heading">Check-In Tickets</h4>
|
<h4 class="list-group-item-heading">Check-In Tickets</h4>
|
||||||
<p class="list-group-item-text">Use this view to check-in tickets when participants arrive.</p>
|
<p class="list-group-item-text">Use this view to check-in tickets when participants arrive.</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'backoffice:badge_handout' %}" class="list-group-item">
|
<a href="{% url 'backoffice:badge_handout' camp_slug=camp.slug %}" class="list-group-item">
|
||||||
<h4 class="list-group-item-heading">Hand Out Badges</h4>
|
<h4 class="list-group-item-heading">Hand Out Badges</h4>
|
||||||
<p class="list-group-item-text">Use this view to mark badges as handed out.</p>
|
<p class="list-group-item-text">Use this view to mark badges as handed out.</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'backoffice:public_credit_names' %}" class="list-group-item">
|
<a href="{% url 'backoffice:public_credit_names' camp_slug=camp.slug %}" class="list-group-item">
|
||||||
<h4 class="list-group-item-heading">Approve Public Credit Names</h4>
|
<h4 class="list-group-item-heading">Approve Public Credit Names</h4>
|
||||||
<p class="list-group-item-text">Use this view to check and approve users Public Credit Names</p>
|
<p class="list-group-item-text">Use this view to check and approve users Public Credit Names</p>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'backoffice:manage_proposals' camp_slug=camp.slug %}" class="list-group-item">
|
||||||
|
<h4 class="list-group-item-heading">Manage Proposals</h4>
|
||||||
|
<p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
15
src/backoffice/templates/manage_eventproposal.html
Normal file
15
src/backoffice/templates/manage_eventproposal.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>Manage {{ form.instance.event_type.name }} Proposal</h3>
|
||||||
|
{% include 'includes/eventproposal_detail.html' with camp=camp %}
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-check'></i> Approve" button_type="submit" button_class="btn-success" name="approve" %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-times'></i> Reject" button_type="submit" button_class="btn-danger" name="reject" %}
|
||||||
|
<a href="{% url 'backoffice:manage_proposals' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
87
src/backoffice/templates/manage_proposals.html
Normal file
87
src/backoffice/templates/manage_proposals.html
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load imageutils %}
|
||||||
|
{% load bornhack %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||||
|
{% endblock extra_head %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h2>BackOffice - Manage Speaker+EventProposals</h2>
|
||||||
|
<div class="lead">
|
||||||
|
The Content team can approve or reject pending SpeakerProposals and EventProposals from this page.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row">
|
||||||
|
<h3>SpeakerProposals</h3>
|
||||||
|
{% if not speakerproposals %}
|
||||||
|
<p class="lead">No pending SpeakerProposals found</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class="text-center">Ticket?</th>
|
||||||
|
<th class="text-center">Speaker?</th>
|
||||||
|
<th>Submitting User</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for proposal in speakerproposals %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ proposal.name }}</td>
|
||||||
|
<td class="text-center">{{ proposal.needs_oneday_ticket|truefalseicon }}</td>
|
||||||
|
<td class="text-center">{{ proposal.event|truefalseicon }}</td>
|
||||||
|
<td>{{ proposal.user }}</td>
|
||||||
|
<td><a href="{% url 'backoffice:speakerproposal_manage' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>EventProposals</h3>
|
||||||
|
{% if not eventproposals %}
|
||||||
|
<p class="lead">No pending SpeakerProposals found</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Speakers</th>
|
||||||
|
<th class="text-center">Event?</th>
|
||||||
|
<th>Submitting User</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for proposal in eventproposals %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ proposal.title }}</td>
|
||||||
|
<td>{{ proposal.track }}</td>
|
||||||
|
<td><i class="fas fa-{{ proposal.event_type.icon }} fa-lg" style="color: {{ proposal.event_type.color }};"></i> {{ proposal.event_type }}</td>
|
||||||
|
<td>{% for speaker in proposal.speakers.all %}<i class="fas fa-user" data-toggle="tooltip" title="{{ speaker.name }}"></i> {% endfor %}</td>
|
||||||
|
<td class="text-center">{{ proposal.speaker|truefalseicon }}</td>
|
||||||
|
<td>{{ proposal.user }}</td>
|
||||||
|
<td><a href="{% url 'backoffice:eventproposal_manage' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
$('.table').DataTable();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
15
src/backoffice/templates/manage_speakerproposal.html
Normal file
15
src/backoffice/templates/manage_speakerproposal.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>Manage Speaker Proposal</h3>
|
||||||
|
{% include 'includes/speakerproposal_detail.html' with camp=camp %}
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-check'></i> Approve" button_type="submit" button_class="btn-success" name="approve" %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-times'></i> Reject" button_type="submit" button_class="btn-danger" name="reject" %}
|
||||||
|
<a href="{% url 'backoffice:manage_proposals' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>Hand Out Products</h2>
|
<h2>Hand Out Products</h2>
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' %}">Ticket Checkin view</a> instead. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' %}">Badge Handout view</a> instead.
|
Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' camp_slug=camp.slug %}">Ticket Checkin view</a> instead. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' camp_slug=camp.slug %}">Badge Handout view</a> instead.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
This table shows all OrderProductRelations which are handed_out=False (not including unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled).
|
This table shows all OrderProductRelations which are handed_out=False (not including unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled).
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>Ticket Check-In</h2>
|
<h2>Ticket Check-In</h2>
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' %}">Badge Handout view</a> instead. To hand out other products go to the <a href="{% url 'backoffice:product_handout' %}">Hand Out Products</a> view instead.
|
Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' camp_slug=camp.slug %}">Badge Handout view</a> instead. To hand out other products go to the <a href="{% url 'backoffice:product_handout' camp_slug=camp.slug %}">Hand Out Products</a> view instead.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False.
|
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False.
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path, include
|
||||||
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'),
|
||||||
|
path('manage_proposals/', include([
|
||||||
|
path('', ManageProposalsView.as_view(), name='manage_proposals'),
|
||||||
|
path('speakers/<uuid:pk>/', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'),
|
||||||
|
path('events/<uuid:pk>/', EventProposalManageView.as_view(), name='eventproposal_manage'),
|
||||||
|
])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
|
import logging
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django.views.generic import TemplateView, ListView
|
from django.views.generic import TemplateView, ListView
|
||||||
from django.http import HttpResponseForbidden
|
from django.views.generic.edit import UpdateView
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from shop.models import OrderProductRelation
|
from shop.models import OrderProductRelation
|
||||||
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
|
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
|
||||||
from profiles.models import Profile
|
from profiles.models import Profile
|
||||||
from itertools import chain
|
from camps.models import Camp
|
||||||
import logging
|
from camps.mixins import CampViewMixin
|
||||||
|
from program.models import SpeakerProposal, EventProposal
|
||||||
|
|
||||||
|
from .mixins import BackofficeViewMixin
|
||||||
|
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
class StaffMemberRequiredMixin(object):
|
class BackofficeIndexView(BackofficeViewMixin, TemplateView):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
template_name = "index.html"
|
||||||
if not request.user.is_staff:
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView):
|
class ProductHandoutView(BackofficeViewMixin, ListView):
|
||||||
template_name = "backoffice_index.html"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductHandoutView(StaffMemberRequiredMixin, ListView):
|
|
||||||
template_name = "product_handout.html"
|
template_name = "product_handout.html"
|
||||||
queryset = OrderProductRelation.objects.filter(handed_out=False, order__paid=True, order__refunded=False, order__cancelled=False).order_by('order')
|
queryset = OrderProductRelation.objects.filter(
|
||||||
|
handed_out=False,
|
||||||
|
order__paid=True,
|
||||||
|
order__refunded=False,
|
||||||
|
order__cancelled=False
|
||||||
|
).order_by('order')
|
||||||
|
|
||||||
|
|
||||||
class BadgeHandoutView(StaffMemberRequiredMixin, ListView):
|
class BadgeHandoutView(BackofficeViewMixin, ListView):
|
||||||
template_name = "badge_handout.html"
|
template_name = "badge_handout.html"
|
||||||
context_object_name = 'tickets'
|
context_object_name = 'tickets'
|
||||||
|
|
||||||
|
@ -35,7 +45,7 @@ class BadgeHandoutView(StaffMemberRequiredMixin, ListView):
|
||||||
return list(chain(shoptickets, sponsortickets, discounttickets))
|
return list(chain(shoptickets, sponsortickets, discounttickets))
|
||||||
|
|
||||||
|
|
||||||
class TicketCheckinView(StaffMemberRequiredMixin, ListView):
|
class TicketCheckinView(BackofficeViewMixin, ListView):
|
||||||
template_name = "ticket_checkin.html"
|
template_name = "ticket_checkin.html"
|
||||||
context_object_name = 'tickets'
|
context_object_name = 'tickets'
|
||||||
|
|
||||||
|
@ -46,10 +56,70 @@ class TicketCheckinView(StaffMemberRequiredMixin, ListView):
|
||||||
return list(chain(shoptickets, sponsortickets, discounttickets))
|
return list(chain(shoptickets, sponsortickets, discounttickets))
|
||||||
|
|
||||||
|
|
||||||
class ApproveNamesView(StaffMemberRequiredMixin, ListView):
|
class ApproveNamesView(BackofficeViewMixin, ListView):
|
||||||
template_name = "approve_public_credit_names.html"
|
template_name = "approve_public_credit_names.html"
|
||||||
context_object_name = 'profiles'
|
context_object_name = 'profiles'
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return Profile.objects.filter(public_credit_name_approved=False).exclude(public_credit_name='')
|
return Profile.objects.filter(public_credit_name_approved=False).exclude(public_credit_name='')
|
||||||
|
|
||||||
|
|
||||||
|
class ManageProposalsView(BackofficeViewMixin, ListView):
|
||||||
|
"""
|
||||||
|
This view shows a list of pending SpeakerProposal and EventProposals.
|
||||||
|
"""
|
||||||
|
template_name = "manage_proposals.html"
|
||||||
|
context_object_name = 'speakerproposals'
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
return SpeakerProposal.objects.filter(
|
||||||
|
camp=self.camp,
|
||||||
|
proposal_status=SpeakerProposal.PROPOSAL_PENDING
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['eventproposals'] = EventProposal.objects.filter(
|
||||||
|
track__camp=self.camp,
|
||||||
|
proposal_status=EventProposal.PROPOSAL_PENDING
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalManageView(BackofficeViewMixin, UpdateView):
|
||||||
|
"""
|
||||||
|
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
|
||||||
|
"""
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""
|
||||||
|
We have two submit buttons in this form, Approve and Reject
|
||||||
|
"""
|
||||||
|
logger.debug(form.data)
|
||||||
|
if 'approve' in form.data:
|
||||||
|
# approve button was pressed
|
||||||
|
form.instance.mark_as_approved(self.request)
|
||||||
|
elif 'reject' in form.data:
|
||||||
|
# reject button was pressed
|
||||||
|
form.instance.mark_as_rejected(self.request)
|
||||||
|
else:
|
||||||
|
messages.error(self.request, "Unknown submit action")
|
||||||
|
return redirect(reverse('backoffice:manage_proposals', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakerProposalManageView(ProposalManageView):
|
||||||
|
"""
|
||||||
|
This view allows an admin to approve/reject SpeakerProposals
|
||||||
|
"""
|
||||||
|
model = SpeakerProposal
|
||||||
|
template_name = "manage_speakerproposal.html"
|
||||||
|
|
||||||
|
|
||||||
|
class EventProposalManageView(ProposalManageView):
|
||||||
|
"""
|
||||||
|
This view allows an admin to approve/reject EventProposals
|
||||||
|
"""
|
||||||
|
model = EventProposal
|
||||||
|
template_name = "manage_eventproposal.html"
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ INSTALLED_APPS = [
|
||||||
'bootstrap3',
|
'bootstrap3',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
'reversion',
|
'reversion',
|
||||||
|
'betterforms',
|
||||||
]
|
]
|
||||||
|
|
||||||
#MEDIA_URL = '/media/'
|
#MEDIA_URL = '/media/'
|
||||||
|
|
|
@ -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,301 +15,177 @@ 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(
|
|
||||||
r'^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/', include([
|
'program/',
|
||||||
url(
|
include('program.urls', namespace='program'),
|
||||||
r'^$',
|
|
||||||
ScheduleView.as_view(),
|
|
||||||
name='schedule_index'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^noscript/$',
|
|
||||||
NoScriptScheduleView.as_view(),
|
|
||||||
name='noscript_schedule_index'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^ics/', ICSView.as_view(), name="ics_view"
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^control/', ProgramControlCenter.as_view(), name="program_control_center"
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^proposals/', include([
|
|
||||||
url(
|
|
||||||
r'^$',
|
|
||||||
ProposalListView.as_view(),
|
|
||||||
name='proposal_list',
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^speakers/', include([
|
|
||||||
url(
|
|
||||||
r'^create/$',
|
|
||||||
SpeakerProposalCreateView.as_view(),
|
|
||||||
name='speakerproposal_create'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/$',
|
|
||||||
SpeakerProposalDetailView.as_view(),
|
|
||||||
name='speakerproposal_detail'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/edit/$',
|
|
||||||
SpeakerProposalUpdateView.as_view(),
|
|
||||||
name='speakerproposal_update'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/submit/$',
|
|
||||||
SpeakerProposalSubmitView.as_view(),
|
|
||||||
name='speakerproposal_submit'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/pictures/(?P<picture>[-_\w+]+)/$',
|
|
||||||
SpeakerProposalPictureView.as_view(),
|
|
||||||
name='speakerproposal_picture',
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^events/', include([
|
|
||||||
url(
|
|
||||||
r'^create/$',
|
|
||||||
EventProposalCreateView.as_view(),
|
|
||||||
name='eventproposal_create'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/$',
|
|
||||||
EventProposalDetailView.as_view(),
|
|
||||||
name='eventproposal_detail'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/edit/$',
|
|
||||||
EventProposalUpdateView.as_view(),
|
|
||||||
name='eventproposal_update'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<pk>[a-f0-9-]+)/submit/$',
|
|
||||||
EventProposalSubmitView.as_view(),
|
|
||||||
name='eventproposal_submit'
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^speakers/', include([
|
|
||||||
url(
|
|
||||||
r'^$',
|
|
||||||
SpeakerListView.as_view(),
|
|
||||||
name='speaker_index'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<slug>[-_\w+]+)/$',
|
|
||||||
SpeakerDetailView.as_view(),
|
|
||||||
name='speaker_detail'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^(?P<slug>[-_\w+]+)/pictures/(?P<picture>[-_\w+]+)/$',
|
|
||||||
SpeakerPictureView.as_view(),
|
|
||||||
name='speaker_picture',
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^events/$',
|
|
||||||
EventListView.as_view(),
|
|
||||||
name='event_index'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^call-for-speakers/$',
|
|
||||||
CallForSpeakersView.as_view(),
|
|
||||||
name='call_for_speakers'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^calendar/',
|
|
||||||
ICSView.as_view(),
|
|
||||||
name='ics_calendar'
|
|
||||||
),
|
|
||||||
# this has to be the last URL here
|
|
||||||
url(
|
|
||||||
r'^(?P<slug>[-_\w+]+)/$',
|
|
||||||
EventDetailView.as_view(),
|
|
||||||
name='event_detail'
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
),
|
||||||
|
|
||||||
url(
|
path(
|
||||||
r'^sponsors/call/$',
|
'sponsors/',
|
||||||
CallForSponsorsView.as_view(),
|
|
||||||
name='call-for-sponsors'
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r'^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')
|
||||||
),
|
),
|
||||||
|
|
||||||
|
path(
|
||||||
|
'backoffice/',
|
||||||
|
include('backoffice.urls', namespace='backoffice')
|
||||||
|
),
|
||||||
|
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
@ -318,5 +194,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
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Command(BaseCommand):
|
||||||
files = [
|
files = [
|
||||||
'sponsors/templates/{camp_slug}_sponsors.html',
|
'sponsors/templates/{camp_slug}_sponsors.html',
|
||||||
'camps/templates/{camp_slug}_camp_detail.html',
|
'camps/templates/{camp_slug}_camp_detail.html',
|
||||||
'program/templates/{camp_slug}_call_for_speakers.html'
|
'program/templates/{camp_slug}_call_for_participation.html'
|
||||||
]
|
]
|
||||||
|
|
||||||
# directories to create, relative to DJANGO_BASE_PATH
|
# directories to create, relative to DJANGO_BASE_PATH
|
||||||
|
@ -68,3 +68,4 @@ class Command(BaseCommand):
|
||||||
'static_src/img/{camp_slug}/logo/{camp_slug}-logo-small.png'.format(camp_slug=camp_slug)
|
'static_src/img/{camp_slug}/logo/{camp_slug}-logo-small.png'.format(camp_slug=camp_slug)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
23
src/camps/migrations/0026_auto_20180506_1633.py
Normal file
23
src/camps/migrations/0026_auto_20180506_1633.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-06 14:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0025_auto_20180318_1250'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='camp',
|
||||||
|
name='call_for_participation_open',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check if the Call for Participation is open for this camp'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='camp',
|
||||||
|
name='call_for_sponsors_open',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check if the Call for Sponsors is open for this camp'),
|
||||||
|
),
|
||||||
|
]
|
23
src/camps/migrations/0027_auto_20180525_1019.py
Normal file
23
src/camps/migrations/0027_auto_20180525_1019.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-25 08:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0026_auto_20180506_1633'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='camp',
|
||||||
|
name='call_for_participation',
|
||||||
|
field=models.TextField(blank=True, help_text='The CFP markdown for this Camp'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='camp',
|
||||||
|
name='call_for_sponsors',
|
||||||
|
field=models.TextField(blank=True, help_text='The CFS markdown for this Camp'),
|
||||||
|
),
|
||||||
|
]
|
23
src/camps/migrations/0028_auto_20180525_1025.py
Normal file
23
src/camps/migrations/0028_auto_20180525_1025.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-25 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0027_auto_20180525_1019'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='camp',
|
||||||
|
name='call_for_participation',
|
||||||
|
field=models.TextField(blank=True, default='The Call For Participation for this Camp has not been written yet', help_text='The CFP markdown for this Camp'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='camp',
|
||||||
|
name='call_for_sponsors',
|
||||||
|
field=models.TextField(blank=True, default='The Call For Sponsors for this Camp has not been written yet', help_text='The CFS markdown for this Camp'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,6 @@
|
||||||
from camps.models import Camp
|
from camps.models import Camp
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
class CampViewMixin(object):
|
class CampViewMixin(object):
|
||||||
|
@ -8,25 +9,16 @@ class CampViewMixin(object):
|
||||||
It also filters out objects that belong to other camps when the queryset has
|
It also filters out objects that belong to other camps when the queryset has
|
||||||
a direct relation to the Camp model.
|
a direct relation to the Camp model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.camp = get_object_or_404(Camp, slug=self.kwargs['camp_slug'])
|
self.camp = get_object_or_404(Camp, slug=self.kwargs["camp_slug"])
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super(CampViewMixin, self).get_queryset()
|
queryset = super(CampViewMixin, self).get_queryset()
|
||||||
if queryset:
|
if queryset:
|
||||||
# check if we have a foreignkey to Camp, filter if so
|
camp_filter = {self.model.get_camp_filter(): self.camp}
|
||||||
for field in queryset.model._meta.fields:
|
return queryset.filter(**camp_filter)
|
||||||
if field.name == "camp" and field.related_model._meta.label == "camps.Camp":
|
|
||||||
return queryset.filter(camp=self.camp)
|
|
||||||
|
|
||||||
# check if we have a camp property, filter if so
|
|
||||||
if hasattr(queryset[0], 'camp') and isinstance(getattr(type(queryset[0]), 'camp', None), property):
|
|
||||||
for item in queryset:
|
|
||||||
if item.camp != self.camp:
|
|
||||||
queryset = queryset.exclude(pk=item.pk)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
# Camp relation not found, or queryset is empty, return it unaltered
|
# Camp relation not found, or queryset is empty, return it unaltered
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,28 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
max_length=7
|
max_length=7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
call_for_participation_open = models.BooleanField(
|
||||||
|
help_text='Check if the Call for Participation is open for this camp',
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
call_for_participation = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text='The CFP markdown for this Camp',
|
||||||
|
default='The Call For Participation for this Camp has not been written yet',
|
||||||
|
)
|
||||||
|
|
||||||
|
call_for_sponsors_open = models.BooleanField(
|
||||||
|
help_text='Check if the Call for Sponsors is open for this camp',
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
call_for_sponsors = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text='The CFS markdown for this Camp',
|
||||||
|
default='The Call For Sponsors for this Camp has not been written yet',
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('camp_detail', kwargs={'camp_slug': self.slug})
|
return reverse('camp_detail', kwargs={'camp_slug': self.slug})
|
||||||
|
|
||||||
|
@ -91,7 +113,7 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event_types(self):
|
def event_types(self):
|
||||||
# return all event types with at least one event in this camp
|
""" Return all event types with at least one event in this camp """
|
||||||
return EventType.objects.filter(event__instances__isnull=False, event__camp=self).distinct()
|
return EventType.objects.filter(event__instances__isnull=False, event__camp=self).distinct()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -179,18 +201,3 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
||||||
'''
|
'''
|
||||||
return self.get_days('teardown')
|
return self.get_days('teardown')
|
||||||
|
|
||||||
@property
|
|
||||||
def call_for_speakers_open(self):
|
|
||||||
if self.camp.upper < timezone.now():
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def call_for_sponsors_open(self):
|
|
||||||
""" Keep call for sponsors open 30 days after camp end """
|
|
||||||
if self.camp.upper + timedelta(days=30) < timezone.now():
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 col-sm-9 text-container">
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'call_for_speakers' camp_slug=camp.slug %}">call for speakers</a>.</div>
|
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'program:call_for_participation' camp_slug=camp.slug %}">call for participation</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 col-sm-9 text-container">
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'call_for_speakers' camp_slug=camp.slug %}">call for speakers</a>.</div>
|
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'program:call_for_participation' camp_slug=camp.slug %}">call for participation</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 col-sm-9 text-container">
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
<strong>Bornhack 2019</strong> will be the third BornHack. It will take place from <strong>August 16th to August 23rd 2019</strong> on the Danish island of Bornholm.
|
<strong>Bornhack 2019</strong> will be the fourth BornHack. It will take place from <strong>August 13th to August 20th 2019</strong> on the Danish island of Bornholm.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 col-sm-9 text-container">
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'call_for_speakers' camp_slug=camp.slug %}">call for speakers</a>.</div>
|
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'program:call_for_participation' camp_slug=camp.slug %}">call for participation</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
92
src/camps/templates/bornhack-2020_camp_detail.html
Normal file
92
src/camps/templates/bornhack-2020_camp_detail.html
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load imageutils %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<img src="{% static camp.logo_large %}" height="350px" width="400px" class="img-responsive" id="front-logo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
|
<div class="lead">
|
||||||
|
<b>BornHack</b> is a 7 day outdoor tent camp where hackers, makers and people with an interest in technology or security come together to celebrate technology, socialise, learn and <b>have fun</b>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-3">
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2610.jpg' 'The family area at BornHack 2016' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-sm-3">
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x1000-B12A2398.jpg' 'A random hackers laptop' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
|
<div class="lead">
|
||||||
|
<strong>Bornhack 2020</strong> will be the fifth BornHack. It will take place from <strong>August 11th to August 18th 2020</strong> on the Danish island of Bornholm.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
|
<div class="lead">
|
||||||
|
The BornHack team looks forward to organising another great event for the hacker community. We <a href="{% url 'teams:list' camp_slug=camp.slug %}">still need volunteers</a>, so please let us know if you want to help!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2631.jpg' 'The BornHack 2016 organiser team' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-sm-3">
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
|
<div class="lead">We want to encourage <strong>hackers, makers, politicians, activists, developers, artists, sysadmins, engineers</strong> with something to say to read our <a href="{% url 'program:call_for_participation' camp_slug=camp.slug %}">call for participation</a>.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9 col-sm-9 text-container">
|
||||||
|
<div class="lead">
|
||||||
|
BornHack aims to <strong>keep ticket prices affordable</strong> for everyone and to that end <strong>we need sponsors</strong>. Please see our <a href="{% url 'sponsors' camp_slug=camp.slug %}">call for sponsors</a> if you want to sponsor us, or if you work for a company you think might be able to help.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-3">
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5265.JPG' 'Organisers thanking the BornHack 2016 sponsors' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 col-sm-12">
|
||||||
|
<p class="lead">You are very welcome to ask questions and show your interest on our different channels:</p>
|
||||||
|
{% include 'includes/contact.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FA0_1983.JPG' 'Happy organisers welcoming people at the entrance to BornHack 2016' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FA0_1986.JPG' 'A bus full of hackers arrive at BornHack 2016' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5126.JPG' 'Late night hacking at Baconsvin village at BornHack 2016' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5168.JPG' '#irl_bar by night at BornHack 2016' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2452.jpg' 'Soldering the BornHack 2016 badge' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2608.jpg' 'Colored lights at night' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/fonsmark' 'FA0_1961.JPG' 'BornHack' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2485.jpg' 'Colored light in the grass' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2624.jpg' 'Working on decorations' %}
|
||||||
|
{% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2604.jpg' 'Sitting around the campfire at BornHack 2016' %}
|
||||||
|
</p>
|
||||||
|
{% endblock content %}
|
|
@ -11,7 +11,7 @@ def handle_team_event(eventtype, irc_message=None, irc_timeout=60, email_templat
|
||||||
The type of event determines which teams receive notifications.
|
The type of event determines which teams receive notifications.
|
||||||
TODO: Add some sort of priority to messages
|
TODO: Add some sort of priority to messages
|
||||||
"""
|
"""
|
||||||
logger.info("Inside handle_team_event, eventtype %s" % eventtype)
|
#logger.info("Inside handle_team_event, eventtype %s" % eventtype)
|
||||||
|
|
||||||
# get event type from database
|
# get event type from database
|
||||||
from .models import Type
|
from .models import Type
|
||||||
|
|
|
@ -81,6 +81,8 @@ class InfoItem(CampRelatedModel):
|
||||||
def camp(self):
|
def camp(self):
|
||||||
return self.category.camp
|
return self.category.camp
|
||||||
|
|
||||||
|
camp_filter = 'category__camp'
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if hasattr(self, 'category') and InfoCategory.objects.filter(camp=self.category.camp, anchor=self.anchor).exists():
|
if hasattr(self, 'category') and InfoCategory.objects.filter(camp=self.category.camp, anchor=self.anchor).exists():
|
||||||
# this anchor is already in use on a category, so it cannot be used here (they must be unique on the entire page)
|
# this anchor is already in use on a category, so it cannot be used here (they must be unique on the entire page)
|
||||||
|
|
|
@ -64,7 +64,7 @@ Info | {{ block.super }}
|
||||||
<small></small>
|
<small></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>{{ item.body|commonmark }}</p>
|
<p>{{ item.body|trustedcommonmark }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -74,12 +74,12 @@ class Plugin(object):
|
||||||
@irc3.event(irc3.rfc.PRIVMSG)
|
@irc3.event(irc3.rfc.PRIVMSG)
|
||||||
def on_privmsg(self, **kwargs):
|
def on_privmsg(self, **kwargs):
|
||||||
"""triggered when a privmsg is sent to the bot or to a channel the bot is in"""
|
"""triggered when a privmsg is sent to the bot or to a channel the bot is in"""
|
||||||
logger.debug("inside on_privmsg(), kwargs: %s" % kwargs)
|
|
||||||
|
|
||||||
# we only handle NOTICEs for now
|
# we only handle NOTICEs for now
|
||||||
if kwargs['event'] != "NOTICE":
|
if kwargs['event'] != "NOTICE":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.debug("inside on_privmsg(), kwargs: %s" % kwargs)
|
||||||
|
|
||||||
# check if this is a message from nickserv
|
# check if this is a message from nickserv
|
||||||
if kwargs['mask'] == "NickServ!%s" % settings.IRCBOT_NICKSERV_MASK:
|
if kwargs['mask'] == "NickServ!%s" % settings.IRCBOT_NICKSERV_MASK:
|
||||||
self.bot.handle_nickserv_privmsg(**kwargs)
|
self.bot.handle_nickserv_privmsg(**kwargs)
|
||||||
|
|
|
@ -14,5 +14,5 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h3>{{ news_item.title }} <small>{{ news_item.published_at|date:"Y-m-d" }}</small></h3>
|
<h3>{{ news_item.title }} <small>{{ news_item.published_at|date:"Y-m-d" }}</small></h3>
|
||||||
</div>
|
</div>
|
||||||
{{ news_item.content|commonmark }}
|
{{ news_item.content|trustedcommonmark }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -13,7 +13,7 @@ News | {{ block.super }}
|
||||||
<div>
|
<div>
|
||||||
<h3><a href="{% url 'news:detail' slug=item.slug %}">{{ item.title }}</a> <small>{{ item.published_at|date:"Y-m-d" }}</small></h3>
|
<h3><a href="{% url 'news:detail' slug=item.slug %}">{{ item.title }}</a> <small>{{ item.published_at|date:"Y-m-d" }}</small></h3>
|
||||||
</div>
|
</div>
|
||||||
{{ item.content|commonmark }}
|
{{ item.content|trustedcommonmark }}
|
||||||
{% if not forloop.last %}
|
{% if not forloop.last %}
|
||||||
<hr />
|
<hr />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -22,5 +22,5 @@
|
||||||
<td>{{ profile.nickserv_username|default:"N/A" }}</td>
|
<td>{{ profile.nickserv_username|default:"N/A" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<a href="{% url 'profiles:update' %}" class="btn btn-black"><i class="fa fa-edit"></i> Edit Profile</a>
|
<a href="{% url 'profiles:update' %}" class="btn btn-black"><i class="fas fa-edit"></i> Edit Profile</a>
|
||||||
{% endblock profile_content %}
|
{% endblock profile_content %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form form %}
|
{% bootstrap_form form %}
|
||||||
<button type="submit" class="btn btn-black"><i class="fa fa-save"></i> Submit</button>
|
<button type="submit" class="btn btn-black"><i class="fas fa-save"></i> Submit</button>
|
||||||
<a href="{% url 'profiles:detail' %}" class="btn btn-black"><i class="fa fa-remove"></i> Cancel</a>
|
<a href="{% url 'profiles:detail' %}" class="btn btn-black"><i class="fas fa-times"></i> Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
{% endblock profile_content %}
|
{% endblock profile_content %}
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,9 +11,12 @@ from .models import (
|
||||||
EventType,
|
EventType,
|
||||||
EventInstance,
|
EventInstance,
|
||||||
EventLocation,
|
EventLocation,
|
||||||
|
EventTrack,
|
||||||
SpeakerProposal,
|
SpeakerProposal,
|
||||||
EventProposal,
|
EventProposal,
|
||||||
Favorite
|
Favorite,
|
||||||
|
UrlType,
|
||||||
|
Url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +24,7 @@ from .models import (
|
||||||
class SpeakerProposalAdmin(admin.ModelAdmin):
|
class SpeakerProposalAdmin(admin.ModelAdmin):
|
||||||
def mark_speakerproposal_as_approved(self, request, queryset):
|
def mark_speakerproposal_as_approved(self, request, queryset):
|
||||||
for sp in queryset:
|
for sp in queryset:
|
||||||
sp.mark_as_approved()
|
sp.mark_as_approved(request)
|
||||||
mark_speakerproposal_as_approved.description = 'Approve and create Speaker object(s)'
|
mark_speakerproposal_as_approved.description = 'Approve and create Speaker object(s)'
|
||||||
|
|
||||||
actions = ['mark_speakerproposal_as_approved']
|
actions = ['mark_speakerproposal_as_approved']
|
||||||
|
@ -40,14 +43,14 @@ class EventProposalAdmin(admin.ModelAdmin):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
ep.mark_as_approved()
|
ep.mark_as_approved(request)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
messages.error(request, e)
|
messages.error(request, e)
|
||||||
return False
|
return False
|
||||||
mark_eventproposal_as_approved.description = 'Approve and create Event object(s)'
|
mark_eventproposal_as_approved.description = 'Approve and create Event object(s)'
|
||||||
|
|
||||||
actions = ['mark_eventproposal_as_approved']
|
actions = ['mark_eventproposal_as_approved']
|
||||||
list_filter = ('camp', 'proposal_status', 'user')
|
list_filter = ('track', 'proposal_status', 'user')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(EventLocation)
|
@admin.register(EventLocation)
|
||||||
|
@ -56,6 +59,11 @@ class EventLocationAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'camp')
|
list_display = ('name', 'camp')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(EventTrack)
|
||||||
|
class EventTrackAdmin(admin.ModelAdmin):
|
||||||
|
list_filter = ('camp',)
|
||||||
|
list_display = ('name', 'camp')
|
||||||
|
|
||||||
@admin.register(EventInstance)
|
@admin.register(EventInstance)
|
||||||
class EventInstanceAdmin(admin.ModelAdmin):
|
class EventInstanceAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
@ -69,6 +77,7 @@ class EventTypeAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Speaker)
|
@admin.register(Speaker)
|
||||||
class SpeakerAdmin(admin.ModelAdmin):
|
class SpeakerAdmin(admin.ModelAdmin):
|
||||||
list_filter = ('camp',)
|
list_filter = ('camp',)
|
||||||
|
readonly_fields = ['proposal']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Favorite)
|
@admin.register(Favorite)
|
||||||
|
@ -82,7 +91,7 @@ class SpeakerInline(admin.StackedInline):
|
||||||
|
|
||||||
@admin.register(Event)
|
@admin.register(Event)
|
||||||
class EventAdmin(admin.ModelAdmin):
|
class EventAdmin(admin.ModelAdmin):
|
||||||
list_filter = ('camp', 'speakers')
|
list_filter = ('track', 'speakers')
|
||||||
list_display = [
|
list_display = [
|
||||||
'title',
|
'title',
|
||||||
'event_type',
|
'event_type',
|
||||||
|
@ -91,3 +100,14 @@ class EventAdmin(admin.ModelAdmin):
|
||||||
inlines = [
|
inlines = [
|
||||||
SpeakerInline
|
SpeakerInline
|
||||||
]
|
]
|
||||||
|
|
||||||
|
readonly_fields = ['proposal']
|
||||||
|
|
||||||
|
@admin.register(UrlType)
|
||||||
|
class UrlTypeAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@admin.register(Url)
|
||||||
|
class UrlAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from .models import (
|
||||||
Favorite,
|
Favorite,
|
||||||
EventLocation,
|
EventLocation,
|
||||||
EventType,
|
EventType,
|
||||||
|
EventTrack,
|
||||||
Speaker
|
Speaker
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,11 +35,11 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
camp.get_days('camp')
|
camp.get_days('camp')
|
||||||
))
|
))
|
||||||
|
|
||||||
events_query_set = Event.objects.filter(camp=camp)
|
events_query_set = Event.objects.filter(track__camp=camp)
|
||||||
events = list([x.serialize() for x in events_query_set])
|
events = list([x.serialize() for x in events_query_set])
|
||||||
|
|
||||||
event_instances_query_set = EventInstance.objects.filter(
|
event_instances_query_set = EventInstance.objects.filter(
|
||||||
event__camp=camp
|
event__track__camp=camp
|
||||||
)
|
)
|
||||||
event_instances = list([
|
event_instances = list([
|
||||||
x.serialize(user=user)
|
x.serialize(user=user)
|
||||||
|
@ -59,6 +60,14 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
for x in event_types_query_set
|
for x in event_types_query_set
|
||||||
])
|
])
|
||||||
|
|
||||||
|
event_tracks_query_set = EventTrack.objects.filter(
|
||||||
|
camp=camp
|
||||||
|
)
|
||||||
|
event_tracks = list([
|
||||||
|
x.serialize()
|
||||||
|
for x in event_tracks_query_set
|
||||||
|
])
|
||||||
|
|
||||||
speakers_query_set = Speaker.objects.filter(camp=camp)
|
speakers_query_set = Speaker.objects.filter(camp=camp)
|
||||||
speakers = list([x.serialize() for x in speakers_query_set])
|
speakers = list([x.serialize() for x in speakers_query_set])
|
||||||
|
|
||||||
|
@ -68,6 +77,7 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
"event_instances": event_instances,
|
"event_instances": event_instances,
|
||||||
"event_locations": event_locations,
|
"event_locations": event_locations,
|
||||||
"event_types": event_types,
|
"event_types": event_types,
|
||||||
|
"event_tracks": event_tracks,
|
||||||
"speakers": speakers,
|
"speakers": speakers,
|
||||||
"days": days,
|
"days": days,
|
||||||
}
|
}
|
||||||
|
|
250
src/program/forms.py
Normal file
250
src/program/forms.py
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
import logging
|
||||||
|
from betterforms.multiform import MultiModelForm
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.forms.widgets import TextInput
|
||||||
|
from django.utils.dateparse import parse_duration
|
||||||
|
|
||||||
|
from .models import SpeakerProposal, EventProposal, EventTrack
|
||||||
|
|
||||||
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakerProposalForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
The SpeakerProposalForm. Takes an EventType in __init__ and changes fields accordingly.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = SpeakerProposal
|
||||||
|
fields = ['name', 'biography', 'needs_oneday_ticket', 'submission_notes']
|
||||||
|
|
||||||
|
def __init__(self, camp, eventtype=None, *args, **kwargs):
|
||||||
|
# initialise the form
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# adapt form based on EventType?
|
||||||
|
if not eventtype:
|
||||||
|
return
|
||||||
|
|
||||||
|
if eventtype.name == 'Debate':
|
||||||
|
# fix label and help_text for the name field
|
||||||
|
self.fields['name'].label = 'Guest Name'
|
||||||
|
self.fields['name'].help_text = 'The name of a debate guest. Can be a real name or an alias.'
|
||||||
|
|
||||||
|
# fix label and help_text for the biograpy field
|
||||||
|
self.fields['biography'].label = 'Guest Biography'
|
||||||
|
self.fields['biography'].help_text = 'The biography of the guest.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Guest Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this guest. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no free tickets for workshops
|
||||||
|
del(self.fields['needs_oneday_ticket'])
|
||||||
|
|
||||||
|
elif eventtype.name == 'Lightning Talk':
|
||||||
|
# fix label and help_text for the name field
|
||||||
|
self.fields['name'].label = 'Speaker Name'
|
||||||
|
self.fields['name'].help_text = 'The name of the speaker. Can be a real name or an alias.'
|
||||||
|
|
||||||
|
# fix label and help_text for the biograpy field
|
||||||
|
self.fields['biography'].label = 'Speaker Biography'
|
||||||
|
self.fields['biography'].help_text = 'The biography of the speaker.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Speaker Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this speaker. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no free tickets for lightning talks
|
||||||
|
del(self.fields['needs_oneday_ticket'])
|
||||||
|
|
||||||
|
elif eventtype.name == 'Music Act':
|
||||||
|
# fix label and help_text for the name field
|
||||||
|
self.fields['name'].label = 'Artist Name'
|
||||||
|
self.fields['name'].help_text = 'The name of the artist. Can be a real name or artist alias.'
|
||||||
|
|
||||||
|
# fix label and help_text for the biograpy field
|
||||||
|
self.fields['biography'].label = 'Artist Description'
|
||||||
|
self.fields['biography'].help_text = 'The description of the artist.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Artist Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this artist. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no oneday tickets for music acts
|
||||||
|
del(self.fields['needs_oneday_ticket'])
|
||||||
|
|
||||||
|
elif eventtype.name == 'Recreational Event':
|
||||||
|
# fix label and help_text for the name field
|
||||||
|
self.fields['name'].label = 'Host Name'
|
||||||
|
self.fields['name'].help_text = 'The name of the event host. Can be a real name or an alias.'
|
||||||
|
|
||||||
|
# fix label and help_text for the biograpy field
|
||||||
|
self.fields['biography'].label = 'Host Biography'
|
||||||
|
self.fields['biography'].help_text = 'The biography of the host.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Host Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this host. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no oneday tickets for music acts
|
||||||
|
del(self.fields['needs_oneday_ticket'])
|
||||||
|
|
||||||
|
elif eventtype.name == 'Talk':
|
||||||
|
# fix label and help_text for the name field
|
||||||
|
self.fields['name'].label = 'Speaker Name'
|
||||||
|
self.fields['name'].help_text = 'The name of the speaker. Can be a real name or an alias.'
|
||||||
|
|
||||||
|
# fix label and help_text for the biograpy field
|
||||||
|
self.fields['biography'].label = 'Speaker Biography'
|
||||||
|
self.fields['biography'].help_text = 'The biography of the speaker.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Speaker Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this speaker. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
elif eventtype.name == 'Workshop':
|
||||||
|
# fix label and help_text for the name field
|
||||||
|
self.fields['name'].label = 'Host Name'
|
||||||
|
self.fields['name'].help_text = 'The name of the workshop host. Can be a real name or an alias.'
|
||||||
|
|
||||||
|
# fix label and help_text for the biograpy field
|
||||||
|
self.fields['biography'].label = 'Host Biography'
|
||||||
|
self.fields['biography'].help_text = 'The biography of the host.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Host Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this host. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no free tickets for workshops
|
||||||
|
del(self.fields['needs_oneday_ticket'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ImproperlyConfigured("Unsupported event type, don't know which form class to use")
|
||||||
|
|
||||||
|
|
||||||
|
class EventProposalForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
The EventProposalForm. Takes an EventType in __init__ and changes fields accordingly.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = EventProposal
|
||||||
|
fields = ['title', 'abstract', 'allow_video_recording', 'duration', 'submission_notes', 'track']
|
||||||
|
|
||||||
|
def clean_duration(self):
|
||||||
|
duration = self.cleaned_data['duration']
|
||||||
|
if not duration or duration < 60 or duration > 180:
|
||||||
|
raise forms.ValidationError("Please keep duration between 60 and 180 minutes.")
|
||||||
|
return duration
|
||||||
|
|
||||||
|
def clean_track(self):
|
||||||
|
track = self.cleaned_data['track']
|
||||||
|
# TODO: make sure the track is part of the current camp, needs camp as form kwarg to verify
|
||||||
|
return track
|
||||||
|
|
||||||
|
def __init__(self, camp, eventtype=None, *args, **kwargs):
|
||||||
|
# initialise form
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# disable the empty_label for the track select box
|
||||||
|
self.fields['track'].empty_label = None
|
||||||
|
self.fields['track'].queryset = EventTrack.objects.filter(camp=camp)
|
||||||
|
|
||||||
|
# make sure video_recording checkbox defaults to checked
|
||||||
|
self.fields['allow_video_recording'].initial = True
|
||||||
|
|
||||||
|
if eventtype.name == 'Debate':
|
||||||
|
# fix label and help_text for the title field
|
||||||
|
self.fields['title'].label = 'Title of debate'
|
||||||
|
self.fields['title'].help_text = 'The title of this debate'
|
||||||
|
|
||||||
|
# fix label and help_text for the abstract field
|
||||||
|
self.fields['abstract'].label = 'Description'
|
||||||
|
self.fields['abstract'].help_text = 'The description of this debate'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Debate Act Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this debate. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# better placeholder text for duration field
|
||||||
|
self.fields['duration'].widget.attrs['placeholder'] = 'Debate Duration (minutes)'
|
||||||
|
|
||||||
|
elif eventtype.name == 'Music Act':
|
||||||
|
# fix label and help_text for the title field
|
||||||
|
self.fields['title'].label = 'Title of music act'
|
||||||
|
self.fields['title'].help_text = 'The title of this music act/concert/set.'
|
||||||
|
|
||||||
|
# fix label and help_text for the abstract field
|
||||||
|
self.fields['abstract'].label = 'Description'
|
||||||
|
self.fields['abstract'].help_text = 'The description of this music act'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Music Act Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this music act. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no video recording for music acts
|
||||||
|
del(self.fields['allow_video_recording'])
|
||||||
|
|
||||||
|
# better placeholder text for duration field
|
||||||
|
self.fields['duration'].widget.attrs['placeholder'] = 'Duration (minutes)'
|
||||||
|
|
||||||
|
elif eventtype.name == 'Recreational Event':
|
||||||
|
# fix label and help_text for the title field
|
||||||
|
self.fields['title'].label = 'Event Title'
|
||||||
|
self.fields['title'].help_text = 'The title of this recreational event'
|
||||||
|
|
||||||
|
# fix label and help_text for the abstract field
|
||||||
|
self.fields['abstract'].label = 'Event Abstract'
|
||||||
|
self.fields['abstract'].help_text = 'The description/abstract of this recreational event.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Event Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this recreational event. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no video recording for music acts
|
||||||
|
del(self.fields['allow_video_recording'])
|
||||||
|
|
||||||
|
# better placeholder text for duration field
|
||||||
|
self.fields['duration'].label = 'Event Duration'
|
||||||
|
self.fields['duration'].widget.attrs['placeholder'] = 'Duration (minutes)'
|
||||||
|
|
||||||
|
elif eventtype.name == 'Talk' or eventtype.name == 'Lightning Talk':
|
||||||
|
# fix label and help_text for the title field
|
||||||
|
self.fields['title'].label = 'Title of Talk'
|
||||||
|
self.fields['title'].help_text = 'The title of this talk/presentation.'
|
||||||
|
|
||||||
|
# fix label and help_text for the abstract field
|
||||||
|
self.fields['abstract'].label = 'Abstract of Talk'
|
||||||
|
self.fields['abstract'].help_text = 'The description/abstract of this talk/presentation. Explain what the audience will experience.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Talk Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this talk. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# no duration for talks
|
||||||
|
del(self.fields['duration'])
|
||||||
|
|
||||||
|
elif eventtype.name == 'Workshop':
|
||||||
|
# fix label and help_text for the title field
|
||||||
|
self.fields['title'].label = 'Workshop Title'
|
||||||
|
self.fields['title'].help_text = 'The title of this workshop.'
|
||||||
|
|
||||||
|
# fix label and help_text for the submission_notes field
|
||||||
|
self.fields['submission_notes'].label = 'Workshop Notes'
|
||||||
|
self.fields['submission_notes'].help_text = 'Private notes regarding this workshop. Only visible to yourself and the BornHack organisers.'
|
||||||
|
|
||||||
|
# fix label and help_text for the abstract field
|
||||||
|
self.fields['abstract'].label = 'Workshop Abstract'
|
||||||
|
self.fields['abstract'].help_text = 'The description/abstract of this workshop. Explain what the participants will learn.'
|
||||||
|
|
||||||
|
# no video recording for workshops
|
||||||
|
del(self.fields['allow_video_recording'])
|
||||||
|
|
||||||
|
# duration field
|
||||||
|
self.fields['duration'].label = 'Workshop Duration'
|
||||||
|
self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this workshop? Please keep it between 60 and 180 minutes (1-3 hours).'
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ImproperlyConfigured("Unsupported event type, don't know which form class to use")
|
||||||
|
|
134
src/program/migrations/0048_auto_20180512_1625.py
Normal file
134
src/program/migrations/0048_auto_20180512_1625.py
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-12 14:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('camps', '0026_auto_20180506_1633'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('program', '0047_auto_20180415_1159'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventTrack',
|
||||||
|
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(max_length=100)),
|
||||||
|
('slug', models.SlugField()),
|
||||||
|
('camp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='eventtracks', to='camps.Camp')),
|
||||||
|
('managers', models.ManyToManyField(related_name='managed_tracks', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='speaker',
|
||||||
|
name='picture_large',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='speaker',
|
||||||
|
name='picture_small',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='picture_large',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='picture_small',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='duration',
|
||||||
|
field=models.IntegerField(blank=True, default=None, help_text='How much time (in minutes) should we set aside for this act? Please keep it between 60 and 180 minutes (1-3 hours).', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventtype',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, default='', help_text='The description of this type of event. Used in content submission flow.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventtype',
|
||||||
|
name='icon',
|
||||||
|
field=models.CharField(default='wrench', help_text="Name of the fontawesome icon to use, without the 'fa-' part", max_length=25),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventtype',
|
||||||
|
name='oneday_ticket_possible',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check if hosting an event of this type qualifies someone for a free oneday ticket'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='speaker',
|
||||||
|
name='needs_oneday_ticket',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check if BornHack needs to provide a free one-day ticket for this speaker'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='needs_oneday_ticket',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check if BornHack needs to provide a free one-day ticket for this speaker'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventlocation',
|
||||||
|
name='icon',
|
||||||
|
field=models.CharField(help_text="Name of the fontawesome icon to use without the 'fa-' part", max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='abstract',
|
||||||
|
field=models.TextField(blank=True, help_text='The abstract for this event. Describe what the audience can expect to see/hear.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='proposal_status',
|
||||||
|
field=models.CharField(choices=[('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='speakers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.', related_name='eventproposals', to='program.SpeakerProposal'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(help_text='The title of this event. Keep it short and memorable.', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='biography',
|
||||||
|
field=models.TextField(help_text='Biography of the speaker/artist/host. Markdown is supported.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Name or alias of the speaker/artist/host', max_length=150),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='proposal_status',
|
||||||
|
field=models.CharField(choices=[('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='submission_notes',
|
||||||
|
field=models.TextField(blank=True, help_text='Private notes for this speaker/artist/host. Only visible to the submitting user and the BornHack organisers.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='track',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='The track this event belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='events', to='program.EventTrack'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='track',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='The track this event belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='eventproposals', to='program.EventTrack'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='eventtrack',
|
||||||
|
unique_together={('camp', 'slug'), ('camp', 'name')},
|
||||||
|
),
|
||||||
|
]
|
30
src/program/migrations/0049_add_event_tracks.py
Normal file
30
src/program/migrations/0049_add_event_tracks.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-12 14:29
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def add_event_tracks(apps, schema_editor):
|
||||||
|
Camp = apps.get_model('camps', 'Camp')
|
||||||
|
EventTrack = apps.get_model('program', 'EventTrack')
|
||||||
|
EventProposal = apps.get_model('program', 'EventProposal')
|
||||||
|
Event = apps.get_model('program', 'Event')
|
||||||
|
for camp in Camp.objects.all():
|
||||||
|
# create the default track for this camp
|
||||||
|
track = EventTrack.objects.create(
|
||||||
|
name="BornHack",
|
||||||
|
slug="bornhack",
|
||||||
|
camp=camp
|
||||||
|
)
|
||||||
|
Event.objects.filter(camp=camp).update(track=track)
|
||||||
|
EventProposal.objects.filter(camp=camp).update(track=track)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0048_auto_20180512_1625'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(add_event_tracks),
|
||||||
|
]
|
||||||
|
|
25
src/program/migrations/0050_auto_20180512_1650.py
Normal file
25
src/program/migrations/0050_auto_20180512_1650.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-12 14:50
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0049_add_event_tracks'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='event',
|
||||||
|
unique_together={('track', 'title'), ('track', 'slug')},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='camp',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='camp',
|
||||||
|
),
|
||||||
|
]
|
24
src/program/migrations/0051_auto_20180512_1801.py
Normal file
24
src/program/migrations/0051_auto_20180512_1801.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-12 16:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0050_auto_20180512_1650'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='track',
|
||||||
|
field=models.ForeignKey(help_text='The track this event belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='events', to='program.EventTrack'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='track',
|
||||||
|
field=models.ForeignKey(help_text='The track this event belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='eventproposals', to='program.EventTrack'),
|
||||||
|
),
|
||||||
|
]
|
18
src/program/migrations/0052_auto_20180519_2324.py
Normal file
18
src/program/migrations/0052_auto_20180519_2324.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-19 21:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0051_auto_20180512_1801'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='allow_video_recording',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check if we can video record the event. Leave unchecked to avoid video recording.'),
|
||||||
|
),
|
||||||
|
]
|
18
src/program/migrations/0053_auto_20180519_2325.py
Normal file
18
src/program/migrations/0053_auto_20180519_2325.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-19 21:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0052_auto_20180519_2324'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventproposal',
|
||||||
|
name='allow_video_recording',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check to allow video recording of the event. Leave unchecked to avoid video recording.'),
|
||||||
|
),
|
||||||
|
]
|
22
src/program/migrations/0054_auto_20180520_1509.py
Normal file
22
src/program/migrations/0054_auto_20180520_1509.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-05-20 13:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0053_auto_20180519_2325'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventtype',
|
||||||
|
name='oneday_ticket_possible',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventtype',
|
||||||
|
name='host_title',
|
||||||
|
field=models.CharField(default='Person', help_text='What to call someone hosting this type of event. Like "Artist" for Music or "Speaker" for talks.', max_length=30),
|
||||||
|
),
|
||||||
|
]
|
48
src/program/migrations/0055_auto_20180521_2354.py
Normal file
48
src/program/migrations/0055_auto_20180521_2354.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
58
src/program/migrations/0056_add_urltypes.py
Normal file
58
src/program/migrations/0056_add_urltypes.py
Normal 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),
|
||||||
|
]
|
||||||
|
|
18
src/program/migrations/0057_auto_20180522_0659.py
Normal file
18
src/program/migrations/0057_auto_20180522_0659.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
22
src/program/migrations/0058_auto_20180523_0844.py
Normal file
22
src/program/migrations/0058_auto_20180523_0844.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
23
src/program/migrations/0059_auto_20180523_2241.py
Normal file
23
src/program/migrations/0059_auto_20180523_2241.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
40
src/program/migrations/0060_auto_20180603_1455.py
Normal file
40
src/program/migrations/0060_auto_20180603_1455.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-06-03 12:55
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0059_auto_20180523_2241'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventtrack',
|
||||||
|
name='camp',
|
||||||
|
field=models.ForeignKey(help_text='The Camp this Track belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='eventtracks', to='camps.Camp'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventtrack',
|
||||||
|
name='managers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='If this track is managed by someone other than the Content team pick the users here.', related_name='managed_tracks', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventtrack',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='The name of this Track', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventtrack',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(help_text='The url slug for this Track'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speakerproposal',
|
||||||
|
name='camp',
|
||||||
|
field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='speakerproposals', to='camps.Camp'),
|
||||||
|
),
|
||||||
|
]
|
24
src/program/migrations/0061_auto_20180603_1525.py
Normal file
24
src/program/migrations/0061_auto_20180603_1525.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.0.4 on 2018-06-03 13:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('program', '0060_auto_20180603_1455'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='proposal',
|
||||||
|
field=models.OneToOneField(blank=True, editable=False, help_text='The event proposal object this event was created from', null=True, on_delete=django.db.models.deletion.PROTECT, to='program.EventProposal'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='speaker',
|
||||||
|
name='proposal',
|
||||||
|
field=models.OneToOneField(blank=True, editable=False, help_text='The speaker proposal object this speaker was created from', null=True, on_delete=django.db.models.deletion.PROTECT, to='program.SpeakerProposal'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,54 +1,41 @@
|
||||||
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 EnsureCFSOpenMixin(SingleObjectMixin):
|
class EnsureCFPOpenMixin(object):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# do not permit editing if call for speakers is not open
|
# do not permit this action if call for participation is not open
|
||||||
if not self.camp.call_for_speakers_open:
|
if not self.camp.call_for_participation_open:
|
||||||
messages.error(request, "The Call for Speakers is not open.")
|
messages.error(request, "The Call for Participation is not open.")
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
)
|
)
|
||||||
|
|
||||||
# alright, continue with the request
|
# alright, continue with the request
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CreateProposalMixin(SingleObjectMixin):
|
|
||||||
def form_valid(self, form):
|
|
||||||
# set camp and user before saving
|
|
||||||
form.instance.camp = self.camp
|
|
||||||
form.instance.user = self.request.user
|
|
||||||
form.save()
|
|
||||||
return redirect(
|
|
||||||
reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EnsureUnapprovedProposalMixin(SingleObjectMixin):
|
class EnsureUnapprovedProposalMixin(SingleObjectMixin):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# do not permit editing if the proposal is already approved
|
# do not permit editing if the proposal is already approved
|
||||||
if self.get_object().proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED:
|
if self.get_object().proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED:
|
||||||
messages.error(request, "This proposal has already been approved. Please contact the organisers if you need to modify something.")
|
messages.error(request, "This proposal has already been approved. Please contact the organisers if you need to modify something.")
|
||||||
return redirect(reverse('proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
# alright, continue with the request
|
# alright, continue with the request
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class EnsureWritableCampMixin(SingleObjectMixin):
|
class EnsureWritableCampMixin(object):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# do not permit view if camp is in readonly mode
|
# do not permit view if camp is in readonly mode
|
||||||
if self.camp.read_only:
|
if self.camp.read_only:
|
||||||
messages.error(request, "No thanks")
|
messages.error(request, "No thanks")
|
||||||
return redirect(reverse('proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
# alright, continue with the request
|
# alright, continue with the request
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
@ -60,47 +47,48 @@ class EnsureUserOwnsProposalMixin(SingleObjectMixin):
|
||||||
if self.get_object().user.username != request.user.username:
|
if self.get_object().user.username != request.user.username:
|
||||||
messages.error(request, "No thanks")
|
messages.error(request, "No thanks")
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
)
|
)
|
||||||
|
|
||||||
# alright, continue with the request
|
# alright, continue with the request
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PictureViewMixin(SingleObjectMixin):
|
class UrlViewMixin(object):
|
||||||
|
"""
|
||||||
|
Mixin with code shared between all the Url views
|
||||||
|
"""
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# do we have the requested picture?
|
"""
|
||||||
if kwargs['picture'] == 'thumbnail':
|
Check that we have a valid SpeakerProposal or EventProposal and that it belongs to the current user
|
||||||
if self.get_object().picture_small:
|
"""
|
||||||
self.picture = self.get_object().picture_small
|
# get the proposal
|
||||||
else:
|
if 'event_uuid' in self.kwargs:
|
||||||
raise Http404()
|
self.eventproposal = get_object_or_404(models.EventProposal, uuid=self.kwargs['event_uuid'], user=request.user)
|
||||||
elif kwargs['picture'] == 'large':
|
elif 'speaker_uuid' in self.kwargs:
|
||||||
if self.get_object().picture_large:
|
self.speakerproposal = get_object_or_404(models.SpeakerProposal, uuid=self.kwargs['speaker_uuid'], user=request.user)
|
||||||
self.picture = self.get_object().picture_large
|
|
||||||
else:
|
|
||||||
raise Http404()
|
|
||||||
else:
|
else:
|
||||||
# only 'thumbnail' and 'large' pictures supported
|
# fuckery afoot
|
||||||
raise Http404()
|
raise Http404
|
||||||
|
|
||||||
# alright, continue with the request
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_picture_response(self, path):
|
def get_context_data(self, **kwargs):
|
||||||
if 'runserver' in sys.argv or 'runserver_plus' in sys.argv:
|
"""
|
||||||
# this is a local devserver situation, guess mimetype from extension and return picture directly
|
Include the proposal in the template context
|
||||||
response = HttpResponse(
|
"""
|
||||||
self.picture,
|
context = super().get_context_data(**kwargs)
|
||||||
content_type=mimetypes.types_map[".%s" % self.picture.name.split(".")[-1]]
|
if hasattr(self, 'eventproposal') and self.eventproposal:
|
||||||
)
|
context['eventproposal'] = self.eventproposal
|
||||||
else:
|
else:
|
||||||
# make nginx serve the picture using X-Accel-Redirect
|
context['speakerproposal'] = self.speakerproposal
|
||||||
# (this works for nginx only, other webservers use x-sendfile)
|
return context
|
||||||
# TODO: maybe make the header name configurable
|
|
||||||
response = HttpResponse()
|
|
||||||
response['X-Accel-Redirect'] = path
|
|
||||||
response['Content-Type'] = ''
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,9 @@ import uuid
|
||||||
import os
|
import os
|
||||||
import icalendar
|
import icalendar
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.contrib.postgres.fields import DateTimeRangeField
|
from django.contrib.postgres.fields import DateTimeRangeField, ArrayField
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
|
@ -16,46 +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 CustomUrlStorage(FileSystemStorage):
|
class UrlType(CreatedUpdatedModel):
|
||||||
def __init__(self, location=None):
|
"""
|
||||||
super(CustomUrlStorage, self).__init__(location)
|
Each Url object has a type.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=25,
|
||||||
|
help_text='The name of this type',
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
|
||||||
def url(self, name):
|
icon = models.CharField(
|
||||||
url = super(CustomUrlStorage, self).url(name)
|
max_length=100,
|
||||||
parts = url.split("/")
|
default='fas fa-link',
|
||||||
if parts[0] != "public":
|
help_text="Name of the fontawesome icon to use without the 'fa-' part"
|
||||||
# first bit should always be "public"
|
)
|
||||||
return False
|
|
||||||
|
|
||||||
if parts[1] == "speakerproposals":
|
class Meta:
|
||||||
# find speakerproposal
|
ordering = ['name']
|
||||||
speakerproposal_model = apps.get_model('program', 'speakerproposal')
|
|
||||||
try:
|
def __str__(self):
|
||||||
speakerproposal = speakerproposal_model.objects.get(picture_small=name)
|
return self.name
|
||||||
picture = "small"
|
|
||||||
except speakerproposal_model.DoesNotExist:
|
|
||||||
try:
|
class Url(CampRelatedModel):
|
||||||
speakerproposal = speakerproposal_model.objects.get(picture_large=name)
|
"""
|
||||||
picture = "large"
|
This model contains URLs related to
|
||||||
except speakerproposal_model.DoesNotExist:
|
- SpeakerProposals
|
||||||
return False
|
- EventProposals
|
||||||
url = reverse('speakerproposal_picture', kwargs={
|
- Speakers
|
||||||
'camp_slug': speakerproposal.camp.slug,
|
- Events
|
||||||
'pk': speakerproposal.pk,
|
Each URL has a UrlType and a GenericForeignKey to the model to which it belongs.
|
||||||
'picture': picture,
|
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:
|
else:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
return url
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.owner.camp
|
||||||
|
|
||||||
|
camp_filter = 'owner__camp'
|
||||||
|
|
||||||
|
|
||||||
storage = CustomUrlStorage()
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class UserSubmittedModel(CampRelatedModel):
|
class UserSubmittedModel(CampRelatedModel):
|
||||||
|
@ -78,157 +172,143 @@ class UserSubmittedModel(CampRelatedModel):
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
|
||||||
PROPOSAL_DRAFT = 'draft'
|
|
||||||
PROPOSAL_PENDING = 'pending'
|
PROPOSAL_PENDING = 'pending'
|
||||||
PROPOSAL_APPROVED = 'approved'
|
PROPOSAL_APPROVED = 'approved'
|
||||||
PROPOSAL_REJECTED = 'rejected'
|
PROPOSAL_REJECTED = 'rejected'
|
||||||
PROPOSAL_MODIFIED_AFTER_APPROVAL = 'modified after approval'
|
|
||||||
|
|
||||||
PROPOSAL_STATUSES = [
|
PROPOSAL_STATUSES = [
|
||||||
PROPOSAL_DRAFT,
|
|
||||||
PROPOSAL_PENDING,
|
PROPOSAL_PENDING,
|
||||||
PROPOSAL_APPROVED,
|
PROPOSAL_APPROVED,
|
||||||
PROPOSAL_REJECTED,
|
PROPOSAL_REJECTED,
|
||||||
PROPOSAL_MODIFIED_AFTER_APPROVAL
|
|
||||||
]
|
]
|
||||||
|
|
||||||
PROPOSAL_STATUS_CHOICES = [
|
PROPOSAL_STATUS_CHOICES = [
|
||||||
(PROPOSAL_DRAFT, 'Draft'),
|
|
||||||
(PROPOSAL_PENDING, 'Pending approval'),
|
(PROPOSAL_PENDING, 'Pending approval'),
|
||||||
(PROPOSAL_APPROVED, 'Approved'),
|
(PROPOSAL_APPROVED, 'Approved'),
|
||||||
(PROPOSAL_REJECTED, 'Rejected'),
|
(PROPOSAL_REJECTED, 'Rejected'),
|
||||||
(PROPOSAL_MODIFIED_AFTER_APPROVAL, 'Modified after approval'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
proposal_status = models.CharField(
|
proposal_status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PROPOSAL_STATUS_CHOICES,
|
choices=PROPOSAL_STATUS_CHOICES,
|
||||||
default=PROPOSAL_DRAFT,
|
default=PROPOSAL_PENDING,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (submitted by: %s, status: %s)' % (self.headline, self.user, self.proposal_status)
|
return '%s (submitted by: %s, status: %s)' % (self.headline, self.user, self.proposal_status)
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
if not self.camp.call_for_speakers_open:
|
if not self.camp.call_for_participation_open:
|
||||||
message = 'Call for speakers is not open'
|
message = 'Call for participation is not open'
|
||||||
if hasattr(self, 'request'):
|
if hasattr(self, 'request'):
|
||||||
messages.error(self.request, message)
|
messages.error(self.request, message)
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
super().save(**kwargs)
|
super().save(**kwargs)
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
if not self.camp.call_for_speakers_open:
|
if not self.camp.call_for_participation_open:
|
||||||
message = 'Call for speakers is not open'
|
message = 'Call for participation is not open'
|
||||||
if hasattr(self, 'request'):
|
if hasattr(self, 'request'):
|
||||||
messages.error(self.request, message)
|
messages.error(self.request, message)
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
super().delete(**kwargs)
|
super().delete(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_speakerproposal_picture_upload_path(instance, filename):
|
|
||||||
""" We want speakerproposal pictures saved as MEDIA_ROOT/public/speakerproposals/camp-slug/proposal-uuid/filename """
|
|
||||||
return 'public/speakerproposals/%(campslug)s/%(proposaluuid)s/%(filename)s' % {
|
|
||||||
'campslug': instance.camp.slug,
|
|
||||||
'proposaluuid': instance.uuid,
|
|
||||||
'filename': filename
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_speakersubmission_picture_upload_path(instance, filename):
|
|
||||||
""" We want speakerproposal pictures saved as MEDIA_ROOT/public/speakerproposals/camp-slug/proposal-uuid/filename """
|
|
||||||
return 'public/speakerproposals/%(campslug)s/%(proposaluuid)s/%(filename)s' % {
|
|
||||||
'campslug': instance.camp.slug,
|
|
||||||
'proposaluuidd': instance.uuid,
|
|
||||||
'filename': filename
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SpeakerProposal(UserSubmittedModel):
|
class SpeakerProposal(UserSubmittedModel):
|
||||||
""" A speaker proposal """
|
""" A speaker proposal """
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
camp = models.ForeignKey(
|
||||||
'camps.Camp',
|
'camps.Camp',
|
||||||
related_name='speakerproposals',
|
related_name='speakerproposals',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=150,
|
max_length=150,
|
||||||
help_text='Name or alias of the speaker',
|
help_text='Name or alias of the speaker/artist/host',
|
||||||
)
|
)
|
||||||
|
|
||||||
biography = models.TextField(
|
biography = models.TextField(
|
||||||
help_text='Markdown is supported.'
|
help_text='Biography of the speaker/artist/host. Markdown is supported.'
|
||||||
)
|
|
||||||
|
|
||||||
picture_large = models.ImageField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
upload_to=get_speakerproposal_picture_upload_path,
|
|
||||||
help_text='A picture of the speaker',
|
|
||||||
storage=storage,
|
|
||||||
max_length=255
|
|
||||||
)
|
|
||||||
|
|
||||||
picture_small = models.ImageField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
upload_to=get_speakerproposal_picture_upload_path,
|
|
||||||
help_text='A thumbnail of the speaker picture',
|
|
||||||
storage=storage,
|
|
||||||
max_length=255
|
|
||||||
)
|
)
|
||||||
|
|
||||||
submission_notes = models.TextField(
|
submission_notes = models.TextField(
|
||||||
help_text='Private notes for this speaker. Only visible to the submitting user and the BornHack organisers.',
|
help_text='Private notes for this speaker/artist/host. Only visible to the submitting user and the BornHack organisers.',
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
needs_oneday_ticket = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Check if BornHack needs to provide a free one-day ticket for this speaker',
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headline(self):
|
def headline(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid})
|
return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid})
|
||||||
|
|
||||||
def mark_as_approved(self):
|
def mark_as_approved(self, request):
|
||||||
speakermodel = apps.get_model('program', 'speaker')
|
""" Marks a SpeakerProposal as approved, including creating/updating the related Speaker object """
|
||||||
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
|
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
|
||||||
speaker = speakermodel()
|
# create a Speaker if we don't have one
|
||||||
|
if not hasattr(self, 'speaker'):
|
||||||
|
speakermodel = apps.get_model('program', 'speaker')
|
||||||
|
speaker = speakermodel()
|
||||||
|
speaker.proposal = self
|
||||||
|
else:
|
||||||
|
speaker = self.speaker
|
||||||
|
|
||||||
|
# set Speaker data
|
||||||
speaker.camp = self.camp
|
speaker.camp = self.camp
|
||||||
speaker.name = self.name
|
speaker.name = self.name
|
||||||
speaker.biography = self.biography
|
speaker.biography = self.biography
|
||||||
if self.picture_small and self.picture_large:
|
speaker.needs_oneday_ticket = self.needs_oneday_ticket
|
||||||
temp = ContentFile(self.picture_small.read())
|
|
||||||
temp.name = os.path.basename(self.picture_small.name)
|
|
||||||
speaker.picture_small = temp
|
|
||||||
temp = ContentFile(self.picture_large.read())
|
|
||||||
temp.name = os.path.basename(self.picture_large.name)
|
|
||||||
speaker.picture_large = temp
|
|
||||||
speaker.proposal = self
|
|
||||||
speaker.save()
|
speaker.save()
|
||||||
|
|
||||||
|
# mark as approved and save
|
||||||
self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED
|
self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# copy all the URLs to the speaker object
|
||||||
|
speaker.urls.clear()
|
||||||
|
for url in self.urls.all():
|
||||||
|
Url.objects.create(
|
||||||
|
url=url.url,
|
||||||
|
urltype=url.urltype,
|
||||||
|
speaker=speaker
|
||||||
|
)
|
||||||
|
|
||||||
|
# a message to the admin
|
||||||
|
messages.success(request, "Speaker object %s has been created/updated" % speaker)
|
||||||
|
|
||||||
|
def mark_as_rejected(self, request):
|
||||||
|
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
|
||||||
|
self.proposal_status = speakerproposalmodel.PROPOSAL_REJECTED
|
||||||
|
self.save()
|
||||||
|
messages.success(request, "SpeakerProposal %s has been rejected" % self.name)
|
||||||
|
|
||||||
|
|
||||||
class EventProposal(UserSubmittedModel):
|
class EventProposal(UserSubmittedModel):
|
||||||
""" An event proposal """
|
""" An event proposal """
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
track = models.ForeignKey(
|
||||||
'camps.Camp',
|
'program.EventTrack',
|
||||||
related_name='eventproposals',
|
related_name='eventproposals',
|
||||||
|
help_text='The track this event belongs to',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
help_text='The title of this event',
|
help_text='The title of this event. Keep it short and memorable.',
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract = models.TextField(
|
abstract = models.TextField(
|
||||||
help_text='The abstract for this event'
|
help_text='The abstract for this event. Describe what the audience can expect to see/hear.',
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
event_type = models.ForeignKey(
|
event_type = models.ForeignKey(
|
||||||
|
@ -241,11 +321,19 @@ class EventProposal(UserSubmittedModel):
|
||||||
'program.SpeakerProposal',
|
'program.SpeakerProposal',
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.',
|
help_text='Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.',
|
||||||
|
related_name='eventproposals',
|
||||||
)
|
)
|
||||||
|
|
||||||
allow_video_recording = models.BooleanField(
|
allow_video_recording = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text='If we can video record the event or not'
|
help_text='Check to allow video recording of the event. Leave unchecked to avoid video recording.'
|
||||||
|
)
|
||||||
|
|
||||||
|
duration = models.IntegerField(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text='How much time (in minutes) should we set aside for this act? Please keep it between 60 and 180 minutes (1-3 hours).'
|
||||||
)
|
)
|
||||||
|
|
||||||
submission_notes = models.TextField(
|
submission_notes = models.TextField(
|
||||||
|
@ -253,21 +341,37 @@ class EventProposal(UserSubmittedModel):
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.track.camp
|
||||||
|
|
||||||
|
camp_filter = 'track__camp'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headline(self):
|
def headline(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy(
|
return reverse_lazy(
|
||||||
'eventproposal_detail',
|
'program:eventproposal_detail',
|
||||||
kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}
|
kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}
|
||||||
)
|
)
|
||||||
|
|
||||||
def mark_as_approved(self):
|
def get_available_speakerproposals(self):
|
||||||
|
"""
|
||||||
|
Return all SpeakerProposals submitted by the user who submitted this EventProposal,
|
||||||
|
which are not already added to this EventProposal
|
||||||
|
"""
|
||||||
|
return SpeakerProposal.objects.filter(
|
||||||
|
camp=self.track.camp,
|
||||||
|
user=self.user
|
||||||
|
).exclude(uuid__in=self.speakers.all().values_list('uuid'))
|
||||||
|
|
||||||
|
def mark_as_approved(self, request):
|
||||||
eventmodel = apps.get_model('program', 'event')
|
eventmodel = apps.get_model('program', 'event')
|
||||||
eventproposalmodel = apps.get_model('program', 'eventproposal')
|
eventproposalmodel = apps.get_model('program', 'eventproposal')
|
||||||
event = eventmodel()
|
event = eventmodel()
|
||||||
event.camp = self.camp
|
event.track = self.track
|
||||||
event.title = self.title
|
event.title = self.title
|
||||||
event.abstract = self.abstract
|
event.abstract = self.abstract
|
||||||
event.event_type = self.event_type
|
event.event_type = self.event_type
|
||||||
|
@ -285,9 +389,65 @@ class EventProposal(UserSubmittedModel):
|
||||||
self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED
|
self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# copy all the URLs too
|
||||||
|
for url in self.urls.all():
|
||||||
|
Url.objects.create(
|
||||||
|
url=url.url,
|
||||||
|
urltype=url.urltype,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.success(request, "Event object %s has been created" % event)
|
||||||
|
|
||||||
|
def mark_as_rejected(self, request):
|
||||||
|
eventproposalmodel = apps.get_model('program', 'eventproposal')
|
||||||
|
self.proposal_status = eventproposalmodel.PROPOSAL_REJECTED
|
||||||
|
self.save()
|
||||||
|
messages.success(request, "EventProposal %s has been rejected" % self.title)
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class EventTrack(CampRelatedModel):
|
||||||
|
""" All events belong to a track. Administration of a track can be delegated to one or more users. """
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
help_text='The name of this Track',
|
||||||
|
)
|
||||||
|
|
||||||
|
slug = models.SlugField(
|
||||||
|
help_text='The url slug for this Track'
|
||||||
|
)
|
||||||
|
|
||||||
|
camp = models.ForeignKey(
|
||||||
|
'camps.Camp',
|
||||||
|
related_name='eventtracks',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
help_text='The Camp this Track belongs to',
|
||||||
|
)
|
||||||
|
|
||||||
|
managers = models.ManyToManyField(
|
||||||
|
'auth.User',
|
||||||
|
related_name='managed_tracks',
|
||||||
|
blank=True,
|
||||||
|
help_text='If this track is managed by someone other than the Content team pick the users here.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('camp', 'slug'), ('camp', 'name'))
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"slug": self.slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventLocation(CampRelatedModel):
|
class EventLocation(CampRelatedModel):
|
||||||
""" The places where stuff happens """
|
""" The places where stuff happens """
|
||||||
|
|
||||||
|
@ -299,7 +459,7 @@ class EventLocation(CampRelatedModel):
|
||||||
|
|
||||||
icon = models.CharField(
|
icon = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
help_text="hex for the unicode character in the fontawesome icon set to use, like 'f000' for 'fa-glass'"
|
help_text="Name of the fontawesome icon to use without the 'fa-' part"
|
||||||
)
|
)
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
camp = models.ForeignKey(
|
||||||
|
@ -332,6 +492,12 @@ class EventType(CreatedUpdatedModel):
|
||||||
|
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
default='',
|
||||||
|
help_text='The description of this type of event. Used in content submission flow.',
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
color = models.CharField(
|
color = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
help_text='The background color of this event type',
|
help_text='The background color of this event type',
|
||||||
|
@ -342,6 +508,12 @@ class EventType(CreatedUpdatedModel):
|
||||||
help_text='Check if this event type should use white text color',
|
help_text='Check if this event type should use white text color',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
icon = models.CharField(
|
||||||
|
max_length=25,
|
||||||
|
help_text="Name of the fontawesome icon to use, without the 'fa-' part",
|
||||||
|
default='wrench',
|
||||||
|
)
|
||||||
|
|
||||||
notifications = models.BooleanField(
|
notifications = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text='Check to send notifications for this event type',
|
help_text='Check to send notifications for this event type',
|
||||||
|
@ -357,6 +529,12 @@ class EventType(CreatedUpdatedModel):
|
||||||
help_text='Include events of this type in the event list?',
|
help_text='Include events of this type in the event list?',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
host_title = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
help_text='What to call someone hosting this type of event. Like "Artist" for Music or "Speaker" for talks.',
|
||||||
|
default='Person',
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -393,10 +571,10 @@ class Event(CampRelatedModel):
|
||||||
help_text='The slug for this event, created automatically',
|
help_text='The slug for this event, created automatically',
|
||||||
)
|
)
|
||||||
|
|
||||||
camp = models.ForeignKey(
|
track = models.ForeignKey(
|
||||||
'camps.Camp',
|
'program.EventTrack',
|
||||||
related_name='events',
|
related_name='events',
|
||||||
help_text='The camp this event belongs to',
|
help_text='The track this event belongs to',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -417,12 +595,13 @@ class Event(CampRelatedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='The event proposal object this event was created from',
|
help_text='The event proposal object this event was created from',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['title']
|
ordering = ['title']
|
||||||
unique_together = (('camp', 'slug'), ('camp', 'title'))
|
unique_together = (('track', 'slug'), ('track', 'title'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%s)' % (self.title, self.camp.title)
|
return '%s (%s)' % (self.title, self.camp.title)
|
||||||
|
@ -432,6 +611,12 @@ class Event(CampRelatedModel):
|
||||||
self.slug = slugify(self.title)
|
self.slug = slugify(self.title)
|
||||||
super(Event, self).save(**kwargs)
|
super(Event, self).save(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.track.camp
|
||||||
|
|
||||||
|
camp_filter = 'track__camp'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speakers_list(self):
|
def speakers_list(self):
|
||||||
if self.speakers.exists():
|
if self.speakers.exists():
|
||||||
|
@ -439,7 +624,7 @@ class Event(CampRelatedModel):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
|
return reverse_lazy('program:event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
data = {
|
data = {
|
||||||
|
@ -501,6 +686,8 @@ class EventInstance(CampRelatedModel):
|
||||||
def camp(self):
|
def camp(self):
|
||||||
return self.event.camp
|
return self.event.camp
|
||||||
|
|
||||||
|
camp_filter = 'event__track__camp'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schedule_date(self):
|
def schedule_date(self):
|
||||||
"""
|
"""
|
||||||
|
@ -514,9 +701,7 @@ class EventInstance(CampRelatedModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timeslots(self):
|
def timeslots(self):
|
||||||
"""
|
""" Find the number of timeslots this eventinstance takes up """
|
||||||
Find the number of timeslots this eventinstance takes up
|
|
||||||
"""
|
|
||||||
seconds = (self.when.upper-self.when.lower).seconds
|
seconds = (self.when.upper-self.when.lower).seconds
|
||||||
minutes = seconds / 60
|
minutes = seconds / 60
|
||||||
return minutes / settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES
|
return minutes / settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES
|
||||||
|
@ -542,6 +727,7 @@ class EventInstance(CampRelatedModel):
|
||||||
'bg-color': self.event.event_type.color,
|
'bg-color': self.event.event_type.color,
|
||||||
'fg-color': '#fff' if self.event.event_type.light_text else '#000',
|
'fg-color': '#fff' if self.event.event_type.light_text else '#000',
|
||||||
'event_type': self.event.event_type.slug,
|
'event_type': self.event.event_type.slug,
|
||||||
|
'event_track': self.event.track.slug,
|
||||||
'location': self.location.slug,
|
'location': self.location.slug,
|
||||||
'location_icon': self.location.icon,
|
'location_icon': self.location.icon,
|
||||||
'timeslots': self.timeslots,
|
'timeslots': self.timeslots,
|
||||||
|
@ -564,15 +750,6 @@ class EventInstance(CampRelatedModel):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def get_speaker_picture_upload_path(instance, filename):
|
|
||||||
""" We want speaker pictures are saved as MEDIA_ROOT/public/speakers/camp-slug/speaker-slug/filename """
|
|
||||||
return 'public/speakers/%(campslug)s/%(speakerslug)s/%(filename)s' % {
|
|
||||||
'campslug': instance.camp.slug,
|
|
||||||
'speakerslug': instance.slug,
|
|
||||||
'filename': filename
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Speaker(CampRelatedModel):
|
class Speaker(CampRelatedModel):
|
||||||
""" A Person (co)anchoring one or more events on a camp. """
|
""" A Person (co)anchoring one or more events on a camp. """
|
||||||
|
|
||||||
|
@ -585,20 +762,6 @@ class Speaker(CampRelatedModel):
|
||||||
help_text='Markdown is supported.'
|
help_text='Markdown is supported.'
|
||||||
)
|
)
|
||||||
|
|
||||||
picture_small = models.ImageField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
upload_to=get_speaker_picture_upload_path,
|
|
||||||
help_text='A thumbnail of the speaker picture'
|
|
||||||
)
|
|
||||||
|
|
||||||
picture_large = models.ImageField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
upload_to=get_speaker_picture_upload_path,
|
|
||||||
help_text='A picture of the speaker'
|
|
||||||
)
|
|
||||||
|
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
blank=True,
|
blank=True,
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
@ -625,7 +788,13 @@ class Speaker(CampRelatedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='The speaker proposal object this speaker was created from',
|
help_text='The speaker proposal object this speaker was created from',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
needs_oneday_ticket = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Check if BornHack needs to provide a free one-day ticket for this speaker',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -641,16 +810,7 @@ class Speaker(CampRelatedModel):
|
||||||
super(Speaker, self).save(**kwargs)
|
super(Speaker, self).save(**kwargs)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
|
return reverse_lazy('program:speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
|
||||||
|
|
||||||
def get_picture_url(self, size):
|
|
||||||
return reverse('speaker_picture', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug, 'picture': size})
|
|
||||||
|
|
||||||
def get_small_picture_url(self):
|
|
||||||
return self.get_picture_url('thumbnail')
|
|
||||||
|
|
||||||
def get_large_picture_url(self):
|
|
||||||
return self.get_picture_url('large')
|
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
data = {
|
data = {
|
||||||
|
@ -658,11 +818,6 @@ class Speaker(CampRelatedModel):
|
||||||
'slug': self.slug,
|
'slug': self.slug,
|
||||||
'biography': self.biography,
|
'biography': self.biography,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.picture_small and self.picture_large:
|
|
||||||
data['large_picture_url'] = self.get_large_picture_url()
|
|
||||||
data['small_picture_url'] = self.get_small_picture_url()
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -680,3 +835,33 @@ class Favorite(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['user', 'event_instance']
|
unique_together = ['user', 'event_instance']
|
||||||
|
|
||||||
|
# classes and functions below here was used by picture handling for speakers before it was removed in May 2018 by tyk
|
||||||
|
|
||||||
|
class CustomUrlStorage(FileSystemStorage):
|
||||||
|
"""
|
||||||
|
Must exist because it is mentioned in old migrations.
|
||||||
|
Can be removed when we clean up old migrations at some point
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_speaker_picture_upload_path():
|
||||||
|
"""
|
||||||
|
Must exist because it is mentioned in old migrations.
|
||||||
|
Can be removed when we clean up old migrations at some point
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_speakerproposal_picture_upload_path():
|
||||||
|
"""
|
||||||
|
Must exist because it is mentioned in old migrations.
|
||||||
|
Can be removed when we clean up old migrations at some point
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_speakersubmission_picture_upload_path():
|
||||||
|
"""
|
||||||
|
Must exist because it is mentioned in old migrations.
|
||||||
|
Can be removed when we clean up old migrations at some point
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
|
@ -13879,6 +13879,8 @@ var _user$project$Models$unpackFilterType = function (filter) {
|
||||||
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
||||||
case 'LocationFilter':
|
case 'LocationFilter':
|
||||||
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
||||||
|
case 'VideoFilter':
|
||||||
|
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
||||||
default:
|
default:
|
||||||
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1};
|
||||||
}
|
}
|
||||||
|
@ -13905,7 +13907,9 @@ var _user$project$Models$Model = function (a) {
|
||||||
return function (i) {
|
return function (i) {
|
||||||
return function (j) {
|
return function (j) {
|
||||||
return function (k) {
|
return function (k) {
|
||||||
return {days: a, events: b, eventInstances: c, eventLocations: d, eventTypes: e, speakers: f, flags: g, filter: h, location: i, route: j, dataLoaded: k};
|
return function (l) {
|
||||||
|
return {days: a, events: b, eventInstances: c, eventLocations: d, eventTypes: e, eventTracks: f, speakers: g, flags: h, filter: i, location: j, route: k, dataLoaded: l};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13921,9 +13925,9 @@ var _user$project$Models$Day = F3(
|
||||||
function (a, b, c) {
|
function (a, b, c) {
|
||||||
return {day_name: a, date: b, repr: c};
|
return {day_name: a, date: b, repr: c};
|
||||||
});
|
});
|
||||||
var _user$project$Models$Speaker = F5(
|
var _user$project$Models$Speaker = F3(
|
||||||
function (a, b, c, d, e) {
|
function (a, b, c) {
|
||||||
return {name: a, slug: b, biography: c, largePictureUrl: d, smallPictureUrl: e};
|
return {name: a, slug: b, biography: c};
|
||||||
});
|
});
|
||||||
var _user$project$Models$EventInstance = function (a) {
|
var _user$project$Models$EventInstance = function (a) {
|
||||||
return function (b) {
|
return function (b) {
|
||||||
|
@ -13941,7 +13945,9 @@ var _user$project$Models$EventInstance = function (a) {
|
||||||
return function (n) {
|
return function (n) {
|
||||||
return function (o) {
|
return function (o) {
|
||||||
return function (p) {
|
return function (p) {
|
||||||
return {title: a, slug: b, id: c, url: d, eventSlug: e, eventType: f, backgroundColor: g, forgroundColor: h, from: i, to: j, timeslots: k, location: l, locationIcon: m, videoState: n, videoUrl: o, isFavorited: p};
|
return function (q) {
|
||||||
|
return {title: a, slug: b, id: c, url: d, eventSlug: e, eventType: f, eventTrack: g, backgroundColor: h, forgroundColor: i, from: j, to: k, timeslots: l, location: m, locationIcon: n, videoState: o, videoUrl: p, isFavorited: q};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13966,9 +13972,9 @@ var _user$project$Models$Flags = F5(
|
||||||
function (a, b, c, d, e) {
|
function (a, b, c, d, e) {
|
||||||
return {schedule_timeslot_length_minutes: a, schedule_midnight_offset_hours: b, ics_button_href: c, camp_slug: d, websocket_server: e};
|
return {schedule_timeslot_length_minutes: a, schedule_midnight_offset_hours: b, ics_button_href: c, camp_slug: d, websocket_server: e};
|
||||||
});
|
});
|
||||||
var _user$project$Models$Filter = F3(
|
var _user$project$Models$Filter = F4(
|
||||||
function (a, b, c) {
|
function (a, b, c, d) {
|
||||||
return {eventTypes: a, eventLocations: b, videoRecording: c};
|
return {eventTypes: a, eventLocations: b, eventTracks: c, videoRecording: d};
|
||||||
});
|
});
|
||||||
var _user$project$Models$NotFoundRoute = {ctor: 'NotFoundRoute'};
|
var _user$project$Models$NotFoundRoute = {ctor: 'NotFoundRoute'};
|
||||||
var _user$project$Models$SpeakerRoute = function (a) {
|
var _user$project$Models$SpeakerRoute = function (a) {
|
||||||
|
@ -13984,6 +13990,10 @@ var _user$project$Models$OverviewFilteredRoute = function (a) {
|
||||||
return {ctor: 'OverviewFilteredRoute', _0: a};
|
return {ctor: 'OverviewFilteredRoute', _0: a};
|
||||||
};
|
};
|
||||||
var _user$project$Models$OverviewRoute = {ctor: 'OverviewRoute'};
|
var _user$project$Models$OverviewRoute = {ctor: 'OverviewRoute'};
|
||||||
|
var _user$project$Models$TrackFilter = F2(
|
||||||
|
function (a, b) {
|
||||||
|
return {ctor: 'TrackFilter', _0: a, _1: b};
|
||||||
|
});
|
||||||
var _user$project$Models$VideoFilter = F2(
|
var _user$project$Models$VideoFilter = F2(
|
||||||
function (a, b) {
|
function (a, b) {
|
||||||
return {ctor: 'VideoFilter', _0: a, _1: b};
|
return {ctor: 'VideoFilter', _0: a, _1: b};
|
||||||
|
@ -13997,6 +14007,15 @@ var _user$project$Models$TypeFilter = F4(
|
||||||
return {ctor: 'TypeFilter', _0: a, _1: b, _2: c, _3: d};
|
return {ctor: 'TypeFilter', _0: a, _1: b, _2: c, _3: d};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var _user$project$Decoders$eventTrackDecoder = A3(
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
|
'slug',
|
||||||
|
_elm_lang$core$Json_Decode$string,
|
||||||
|
A3(
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
|
'name',
|
||||||
|
_elm_lang$core$Json_Decode$string,
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$TrackFilter)));
|
||||||
var _user$project$Decoders$eventTypeDecoder = A3(
|
var _user$project$Decoders$eventTypeDecoder = A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'light_text',
|
'light_text',
|
||||||
|
@ -14080,29 +14099,33 @@ var _user$project$Decoders$eventInstanceDecoder = A4(
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'event_type',
|
'event_track',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'event_slug',
|
'event_type',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'url',
|
'event_slug',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'id',
|
'url',
|
||||||
_elm_lang$core$Json_Decode$int,
|
_elm_lang$core$Json_Decode$string,
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'slug',
|
'id',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$int,
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'title',
|
'slug',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$EventInstance)))))))))))))))));
|
A3(
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
|
'title',
|
||||||
|
_elm_lang$core$Json_Decode$string,
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$EventInstance))))))))))))))))));
|
||||||
var _user$project$Decoders$eventDecoder = A3(
|
var _user$project$Decoders$eventDecoder = A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'event_type',
|
'event_type',
|
||||||
|
@ -14133,29 +14156,19 @@ var _user$project$Decoders$eventDecoder = A3(
|
||||||
'title',
|
'title',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Event))))))));
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Event))))))));
|
||||||
var _user$project$Decoders$speakerDecoder = A4(
|
var _user$project$Decoders$speakerDecoder = A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$optional,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'small_picture_url',
|
'biography',
|
||||||
_elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string),
|
_elm_lang$core$Json_Decode$string,
|
||||||
_elm_lang$core$Maybe$Nothing,
|
A3(
|
||||||
A4(
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$optional,
|
'slug',
|
||||||
'large_picture_url',
|
_elm_lang$core$Json_Decode$string,
|
||||||
_elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string),
|
|
||||||
_elm_lang$core$Maybe$Nothing,
|
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'biography',
|
'name',
|
||||||
_elm_lang$core$Json_Decode$string,
|
_elm_lang$core$Json_Decode$string,
|
||||||
A3(
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Speaker))));
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
|
||||||
'slug',
|
|
||||||
_elm_lang$core$Json_Decode$string,
|
|
||||||
A3(
|
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
|
||||||
'name',
|
|
||||||
_elm_lang$core$Json_Decode$string,
|
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Speaker))))));
|
|
||||||
var _user$project$Decoders$dayDecoder = A3(
|
var _user$project$Decoders$dayDecoder = A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'repr',
|
'repr',
|
||||||
|
@ -14175,25 +14188,29 @@ var _user$project$Decoders$initDataDecoder = A3(
|
||||||
_elm_lang$core$Json_Decode$list(_user$project$Decoders$speakerDecoder),
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$speakerDecoder),
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'event_types',
|
'event_tracks',
|
||||||
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventTypeDecoder),
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventTrackDecoder),
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'event_locations',
|
'event_types',
|
||||||
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventLocationDecoder),
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventTypeDecoder),
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'event_instances',
|
'event_locations',
|
||||||
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventInstanceDecoder),
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventLocationDecoder),
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'events',
|
'event_instances',
|
||||||
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventDecoder),
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventInstanceDecoder),
|
||||||
A3(
|
A3(
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
'days',
|
'events',
|
||||||
_elm_lang$core$Json_Decode$list(_user$project$Decoders$dayDecoder),
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$eventDecoder),
|
||||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Model)))))));
|
A3(
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||||
|
'days',
|
||||||
|
_elm_lang$core$Json_Decode$list(_user$project$Decoders$dayDecoder),
|
||||||
|
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Model))))))));
|
||||||
var _user$project$Decoders$WebSocketAction = function (a) {
|
var _user$project$Decoders$WebSocketAction = function (a) {
|
||||||
return {action: a};
|
return {action: a};
|
||||||
};
|
};
|
||||||
|
@ -14709,9 +14726,10 @@ var _user$project$Views_FilterView$videoRecordingFilters = {
|
||||||
var _user$project$Views_FilterView$parseFilterFromQuery = F2(
|
var _user$project$Views_FilterView$parseFilterFromQuery = F2(
|
||||||
function (query, model) {
|
function (query, model) {
|
||||||
var videoFilters = A3(_user$project$Views_FilterView$getFilter, 'video', _user$project$Views_FilterView$videoRecordingFilters, query);
|
var videoFilters = A3(_user$project$Views_FilterView$getFilter, 'video', _user$project$Views_FilterView$videoRecordingFilters, query);
|
||||||
|
var tracks = A3(_user$project$Views_FilterView$getFilter, 'tracks', model.eventTracks, query);
|
||||||
var locations = A3(_user$project$Views_FilterView$getFilter, 'location', model.eventLocations, query);
|
var locations = A3(_user$project$Views_FilterView$getFilter, 'location', model.eventLocations, query);
|
||||||
var types = A3(_user$project$Views_FilterView$getFilter, 'type', model.eventTypes, query);
|
var types = A3(_user$project$Views_FilterView$getFilter, 'type', model.eventTypes, query);
|
||||||
return {eventTypes: types, eventLocations: locations, videoRecording: videoFilters};
|
return {eventTypes: types, eventLocations: locations, eventTracks: tracks, videoRecording: videoFilters};
|
||||||
});
|
});
|
||||||
var _user$project$Views_FilterView$icsButton = function (model) {
|
var _user$project$Views_FilterView$icsButton = function (model) {
|
||||||
var filterString = function () {
|
var filterString = function () {
|
||||||
|
@ -14835,14 +14853,26 @@ var _user$project$Views_FilterView$filterSidebar = function (model) {
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: A5(
|
_0: A5(
|
||||||
_user$project$Views_FilterView$filterView,
|
_user$project$Views_FilterView$filterView,
|
||||||
'Video',
|
'Track',
|
||||||
_user$project$Views_FilterView$videoRecordingFilters,
|
model.eventTracks,
|
||||||
model.filter.videoRecording,
|
model.filter.eventTracks,
|
||||||
model.eventInstances,
|
model.eventInstances,
|
||||||
function (_) {
|
function (_) {
|
||||||
return _.videoState;
|
return _.eventTrack;
|
||||||
}),
|
}),
|
||||||
_1: {ctor: '[]'}
|
_1: {
|
||||||
|
ctor: '::',
|
||||||
|
_0: A5(
|
||||||
|
_user$project$Views_FilterView$filterView,
|
||||||
|
'Video',
|
||||||
|
_user$project$Views_FilterView$videoRecordingFilters,
|
||||||
|
model.filter.videoRecording,
|
||||||
|
model.eventInstances,
|
||||||
|
function (_) {
|
||||||
|
return _.videoState;
|
||||||
|
}),
|
||||||
|
_1: {ctor: '[]'}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -14865,11 +14895,12 @@ var _user$project$Views_FilterView$applyFilters = F2(
|
||||||
});
|
});
|
||||||
var types = A2(slugs, model.eventTypes, model.filter.eventTypes);
|
var types = A2(slugs, model.eventTypes, model.filter.eventTypes);
|
||||||
var locations = A2(slugs, model.eventLocations, model.filter.eventLocations);
|
var locations = A2(slugs, model.eventLocations, model.filter.eventLocations);
|
||||||
|
var tracks = A2(slugs, model.eventTracks, model.filter.eventTracks);
|
||||||
var videoFilters = A2(slugs, _user$project$Views_FilterView$videoRecordingFilters, model.filter.videoRecording);
|
var videoFilters = A2(slugs, _user$project$Views_FilterView$videoRecordingFilters, model.filter.videoRecording);
|
||||||
var filteredEventInstances = A2(
|
var filteredEventInstances = A2(
|
||||||
_elm_lang$core$List$filter,
|
_elm_lang$core$List$filter,
|
||||||
function (eventInstance) {
|
function (eventInstance) {
|
||||||
return A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Month, eventInstance.from, day.date) && (A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Day, eventInstance.from, day.date) && (A2(_elm_lang$core$List$member, eventInstance.location, locations) && (A2(_elm_lang$core$List$member, eventInstance.eventType, types) && A2(_elm_lang$core$List$member, eventInstance.videoState, videoFilters))));
|
return A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Month, eventInstance.from, day.date) && (A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Day, eventInstance.from, day.date) && (A2(_elm_lang$core$List$member, eventInstance.location, locations) && (A2(_elm_lang$core$List$member, eventInstance.eventType, types) && (A2(_elm_lang$core$List$member, eventInstance.eventTrack, tracks) && A2(_elm_lang$core$List$member, eventInstance.videoState, videoFilters)))));
|
||||||
},
|
},
|
||||||
model.eventInstances);
|
model.eventInstances);
|
||||||
return filteredEventInstances;
|
return filteredEventInstances;
|
||||||
|
@ -14939,7 +14970,7 @@ var _user$project$Update$update = F2(
|
||||||
},
|
},
|
||||||
model.filter.eventLocations) : {ctor: '::', _0: eventLocation, _1: model.filter.eventLocations}
|
model.filter.eventLocations) : {ctor: '::', _0: eventLocation, _1: model.filter.eventLocations}
|
||||||
});
|
});
|
||||||
default:
|
case 'VideoFilter':
|
||||||
var videoRecording = A2(_user$project$Models$VideoFilter, _p6._0, _p6._1);
|
var videoRecording = A2(_user$project$Models$VideoFilter, _p6._0, _p6._1);
|
||||||
return _elm_lang$core$Native_Utils.update(
|
return _elm_lang$core$Native_Utils.update(
|
||||||
currentFilter,
|
currentFilter,
|
||||||
|
@ -14951,6 +14982,18 @@ var _user$project$Update$update = F2(
|
||||||
},
|
},
|
||||||
model.filter.videoRecording) : {ctor: '::', _0: videoRecording, _1: model.filter.videoRecording}
|
model.filter.videoRecording) : {ctor: '::', _0: videoRecording, _1: model.filter.videoRecording}
|
||||||
});
|
});
|
||||||
|
default:
|
||||||
|
var eventTrack = A2(_user$project$Models$TrackFilter, _p6._0, _p6._1);
|
||||||
|
return _elm_lang$core$Native_Utils.update(
|
||||||
|
currentFilter,
|
||||||
|
{
|
||||||
|
eventTracks: A2(_elm_lang$core$List$member, eventTrack, model.filter.eventTracks) ? A2(
|
||||||
|
_elm_lang$core$List$filter,
|
||||||
|
function (x) {
|
||||||
|
return !_elm_lang$core$Native_Utils.eq(x, eventTrack);
|
||||||
|
},
|
||||||
|
model.filter.videoRecording) : {ctor: '::', _0: eventTrack, _1: model.filter.eventTracks}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
var query = _user$project$Views_FilterView$filterToQuery(newFilter);
|
var query = _user$project$Views_FilterView$filterToQuery(newFilter);
|
||||||
|
@ -16224,117 +16267,90 @@ var _user$project$Views_SpeakerDetail$speakerDetailView = F2(
|
||||||
return _elm_lang$core$Native_Utils.eq(speaker.slug, speakerSlug);
|
return _elm_lang$core$Native_Utils.eq(speaker.slug, speakerSlug);
|
||||||
},
|
},
|
||||||
model.speakers));
|
model.speakers));
|
||||||
var image = function () {
|
var _p1 = speaker;
|
||||||
var _p1 = speaker;
|
if (_p1.ctor === 'Just') {
|
||||||
if (_p1.ctor === 'Just') {
|
var _p2 = _p1._0;
|
||||||
var _p2 = _p1._0.smallPictureUrl;
|
|
||||||
if (_p2.ctor === 'Just') {
|
|
||||||
return {
|
|
||||||
ctor: '::',
|
|
||||||
_0: A2(
|
|
||||||
_elm_lang$html$Html$img,
|
|
||||||
{
|
|
||||||
ctor: '::',
|
|
||||||
_0: _elm_lang$html$Html_Attributes$src(_p2._0),
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
},
|
|
||||||
{ctor: '[]'}),
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {ctor: '[]'};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {ctor: '[]'};
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
var _p3 = speaker;
|
|
||||||
if (_p3.ctor === 'Just') {
|
|
||||||
var _p4 = _p3._0;
|
|
||||||
return A2(
|
return A2(
|
||||||
_elm_lang$html$Html$div,
|
_elm_lang$html$Html$div,
|
||||||
{ctor: '[]'},
|
{ctor: '[]'},
|
||||||
A2(
|
{
|
||||||
_elm_lang$core$Basics_ops['++'],
|
ctor: '::',
|
||||||
{
|
_0: A2(
|
||||||
ctor: '::',
|
_elm_lang$html$Html$a,
|
||||||
_0: A2(
|
{
|
||||||
_elm_lang$html$Html$a,
|
ctor: '::',
|
||||||
{
|
_0: _elm_lang$html$Html_Events$onClick(_user$project$Messages$BackInHistory),
|
||||||
|
_1: {
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: _elm_lang$html$Html_Events$onClick(_user$project$Messages$BackInHistory),
|
_0: _elm_lang$html$Html_Attributes$classList(
|
||||||
_1: {
|
{
|
||||||
|
ctor: '::',
|
||||||
|
_0: {ctor: '_Tuple2', _0: 'btn', _1: true},
|
||||||
|
_1: {
|
||||||
|
ctor: '::',
|
||||||
|
_0: {ctor: '_Tuple2', _0: 'btn-default', _1: true},
|
||||||
|
_1: {ctor: '[]'}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_1: {ctor: '[]'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctor: '::',
|
||||||
|
_0: A2(
|
||||||
|
_elm_lang$html$Html$i,
|
||||||
|
{
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: _elm_lang$html$Html_Attributes$classList(
|
_0: _elm_lang$html$Html_Attributes$classList(
|
||||||
{
|
{
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: {ctor: '_Tuple2', _0: 'btn', _1: true},
|
_0: {ctor: '_Tuple2', _0: 'fa', _1: true},
|
||||||
_1: {
|
_1: {
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: {ctor: '_Tuple2', _0: 'btn-default', _1: true},
|
_0: {ctor: '_Tuple2', _0: 'fa-chevron-left', _1: true},
|
||||||
_1: {ctor: '[]'}
|
_1: {ctor: '[]'}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
_1: {ctor: '[]'}
|
_1: {ctor: '[]'}
|
||||||
}
|
},
|
||||||
},
|
{ctor: '[]'}),
|
||||||
|
_1: {
|
||||||
|
ctor: '::',
|
||||||
|
_0: _elm_lang$html$Html$text(' Back'),
|
||||||
|
_1: {ctor: '[]'}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_1: {
|
||||||
|
ctor: '::',
|
||||||
|
_0: A2(
|
||||||
|
_elm_lang$html$Html$h3,
|
||||||
|
{ctor: '[]'},
|
||||||
{
|
{
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: A2(
|
_0: _elm_lang$html$Html$text(_p2.name),
|
||||||
_elm_lang$html$Html$i,
|
_1: {ctor: '[]'}
|
||||||
{
|
|
||||||
ctor: '::',
|
|
||||||
_0: _elm_lang$html$Html_Attributes$classList(
|
|
||||||
{
|
|
||||||
ctor: '::',
|
|
||||||
_0: {ctor: '_Tuple2', _0: 'fa', _1: true},
|
|
||||||
_1: {
|
|
||||||
ctor: '::',
|
|
||||||
_0: {ctor: '_Tuple2', _0: 'fa-chevron-left', _1: true},
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
},
|
|
||||||
{ctor: '[]'}),
|
|
||||||
_1: {
|
|
||||||
ctor: '::',
|
|
||||||
_0: _elm_lang$html$Html$text(' Back'),
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
_1: {
|
_1: {
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: A2(
|
_0: A2(
|
||||||
_elm_lang$html$Html$h3,
|
_elm_lang$html$Html$div,
|
||||||
{ctor: '[]'},
|
{ctor: '[]'},
|
||||||
{
|
{
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: _elm_lang$html$Html$text(_p4.name),
|
_0: A2(
|
||||||
|
_evancz$elm_markdown$Markdown$toHtml,
|
||||||
|
{ctor: '[]'},
|
||||||
|
_p2.biography),
|
||||||
_1: {ctor: '[]'}
|
_1: {ctor: '[]'}
|
||||||
}),
|
}),
|
||||||
_1: {
|
_1: {
|
||||||
ctor: '::',
|
ctor: '::',
|
||||||
_0: A2(
|
_0: A2(_user$project$Views_SpeakerDetail$speakerEvents, _p2, model),
|
||||||
_elm_lang$html$Html$div,
|
_1: {ctor: '[]'}
|
||||||
{ctor: '[]'},
|
|
||||||
{
|
|
||||||
ctor: '::',
|
|
||||||
_0: A2(
|
|
||||||
_evancz$elm_markdown$Markdown$toHtml,
|
|
||||||
{ctor: '[]'},
|
|
||||||
_p4.biography),
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
}),
|
|
||||||
_1: {
|
|
||||||
ctor: '::',
|
|
||||||
_0: A2(_user$project$Views_SpeakerDetail$speakerEvents, _p4, model),
|
|
||||||
_1: {ctor: '[]'}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
image));
|
});
|
||||||
} else {
|
} else {
|
||||||
return A2(
|
return A2(
|
||||||
_elm_lang$html$Html$div,
|
_elm_lang$html$Html$div,
|
||||||
|
@ -16690,10 +16706,11 @@ var _user$project$Main$subscriptions = function (model) {
|
||||||
};
|
};
|
||||||
var _user$project$Main$init = F2(
|
var _user$project$Main$init = F2(
|
||||||
function (flags, location) {
|
function (flags, location) {
|
||||||
var emptyFilter = A3(
|
var emptyFilter = A4(
|
||||||
_user$project$Models$Filter,
|
_user$project$Models$Filter,
|
||||||
{ctor: '[]'},
|
{ctor: '[]'},
|
||||||
{ctor: '[]'},
|
{ctor: '[]'},
|
||||||
|
{ctor: '[]'},
|
||||||
{ctor: '[]'});
|
{ctor: '[]'});
|
||||||
var currentRoute = _user$project$Routing$parseLocation(location);
|
var currentRoute = _user$project$Routing$parseLocation(location);
|
||||||
var model = _user$project$Models$Model(
|
var model = _user$project$Models$Model(
|
||||||
|
@ -16702,6 +16719,7 @@ var _user$project$Main$init = F2(
|
||||||
{ctor: '[]'})(
|
{ctor: '[]'})(
|
||||||
{ctor: '[]'})(
|
{ctor: '[]'})(
|
||||||
{ctor: '[]'})(
|
{ctor: '[]'})(
|
||||||
|
{ctor: '[]'})(
|
||||||
{ctor: '[]'})(flags)(emptyFilter)(location)(currentRoute)(false);
|
{ctor: '[]'})(flags)(emptyFilter)(location)(currentRoute)(false);
|
||||||
return A2(
|
return A2(
|
||||||
_elm_lang$core$Platform_Cmd_ops['!'],
|
_elm_lang$core$Platform_Cmd_ops['!'],
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
{% extends 'program_base.html' %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
Call for Speakers | {{ block.super }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
|
||||||
|
|
||||||
{% if not camp.call_for_speakers_open %}
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<strong>Note!</strong> This Call for Speakers is no longer relevant. It is kept here for historic purposes.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>BornHack 2016: Call for Speakers</h2>
|
|
||||||
|
|
||||||
<p>BornHack 2016 is a 7 days outdoor technology tent camping festival that will take place from the 27th of August to the 3rd of September 2016 on the island of Bornholm in Denmark. It is first time that BornHack will take place and it is our goal to make BornHack a yearly recurring event with 100 to 350 participants.</p>
|
|
||||||
|
|
||||||
<p>We are looking for gifted, entertaining and technically enlightening speakers to host talks, lightning talks and workshops at BornHack.</p>
|
|
||||||
|
|
||||||
<p>Please reach out to us on speakers@bornhack.dk with a title, abstract, biography, an optional picture of yourself and whether it is a regular talk, lightning talk, workshop or something entirely different. Please ensure that all information is in English. The submitted information will be published both as a news entry and in the official event program on our website, if the submission is accepted.</p>
|
|
||||||
|
|
||||||
<p>We are very open to different topics. We expect that the majority of the presentation at BornHack will be on security, networking, programming, distributed systems, privacy, and how these technologies relate to society.</p>
|
|
||||||
|
|
||||||
<p>The ticket shop for BornHack 2016 is already open and available at <a href="{% url 'shop:index' %}">https://bornhack.dk/shop/</a> - please make sure you have also read our <a href="{% url 'conduct' %}">Code of Conduct</a>.</p>
|
|
||||||
|
|
||||||
<h3>Regular Talk</h3>
|
|
||||||
|
|
||||||
<p>Regular talks are 45 minutes of presentation, 10 minutes of questions from the audience followed by 5 minutes of preparation for setting up the next speaker.</p>
|
|
||||||
|
|
||||||
<p>Please bring your own laptop with your presentation on; it should have an HDMI socket and we will provide the cable to the projector. We do not guarantee that audio will work, even if your laptop supports that.</p>
|
|
||||||
|
|
||||||
<p>We will provide you with a one-day entrance ticket free of charge, but due to our limited funds, you would have to pay for transportation to and from the event yourself. We also encourage you to participate for the entire week, but you would also have to pay for the ticket yourself.</p>
|
|
||||||
|
|
||||||
<h3>Lightning Talk</h3>
|
|
||||||
|
|
||||||
<p>Lightning talks are 10 minutes of presentation. A laptop will be connected to the projector at the location of the presentations.</p>
|
|
||||||
|
|
||||||
<p>A lightning talk is an excellent opportunity for inexperienced speakers to present a topic that you find interesting.</p>
|
|
||||||
|
|
||||||
<p>You MUST buy yourself an entrance ticket to host a lightning talk; we are unable to offer free tickets for everyone that gives a lightning talk.</p>
|
|
||||||
|
|
||||||
<h3>Workshop</h3>
|
|
||||||
|
|
||||||
<p>We have two workshop areas that will be able to host workshops for approximately 20 people per room. Workshops can be up to 3 hours per slot and can be extended for daily workshops.</p>
|
|
||||||
|
|
||||||
<p>You MUST buy yourself an entrance ticket to host a workshop; we are unable to offer free tickets for everyone that hosts a workshop.</p>
|
|
||||||
|
|
||||||
<h2>Contact Information</h2>
|
|
||||||
|
|
||||||
<p>The BornHack speakers team can be contacted via speakers@bornhack.dk - for general information reach out to the info team via info@bornhack.dk</p>
|
|
||||||
|
|
||||||
<p>We are also reachable via IRC in #BornHack on irc.baconsvin.org or 6nbtgccn5nbcodn3.onion - both listening for TLS connections on port 6697.</p>
|
|
||||||
|
|
||||||
<p>For more information, please have a look at <a href="{% url 'camp_detail' camp_slug='bornhack-2016' %}">https://bornhack.dk/</a> or follow us on Twitter at <a href="https://twitter.com/bornhax">@bornhax</a>.</p>
|
|
||||||
{% endblock %}
|
|
|
@ -1,58 +0,0 @@
|
||||||
{% extends 'program_base.html' %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
Call for Speakers | {{ block.super }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
|
||||||
|
|
||||||
{% if not camp.call_for_speakers_open %}
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<strong>Note!</strong> This Call for Speakers is no longer relevant. It is kept here for historic purposes.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>Call for Speakers</h2>
|
|
||||||
<p>We are looking for gifted, talented, humourous, technically enlightened speakers to host talks, lightning talks, and workshops at BornHack.</p>
|
|
||||||
|
|
||||||
<p>We are very open to different topics. We expect that the majority of the presentation at BornHack will be on security, networking, programming, distributed systems, privacy, and how these technologies relate to society.</p>
|
|
||||||
|
|
||||||
<p>BornHack is trying to be an inclusive event so please make sure you have read and understood our <a href="{% url 'conduct' %}">Code of Conduct</a>.</p>
|
|
||||||
|
|
||||||
<h3>Regular Talk</h3>
|
|
||||||
<p>Regular talks are 45 minutes of presentation, 10 minutes of questions from the audience followed by 5 minutes of preparation for setting up the next speaker.</p>
|
|
||||||
|
|
||||||
<p>Please bring your own laptop with your presentation on; it should have an ordinary HDMI output and we will provide the cable to the projector. We do not guarantee that audio will work, even if your laptop supports it - please reach out to us early if this is a requirement.</p>
|
|
||||||
|
|
||||||
<p>We will provide speakers with a one-day entrance ticket free of charge, but due to our limited funds, you would have to pay for transportation to and from the event yourself. We also encourage speakers to participate for the entire week, but you will have to pay for the full ticket yourself.</p>
|
|
||||||
|
|
||||||
<h3>Lightning Talk</h3>
|
|
||||||
<p>Lightning talks are 10 minutes of presentation. A laptop will be connected to the projector at the location of the presentations.</p>
|
|
||||||
|
|
||||||
<p>A lightning talk is an excellent opportunity for inexperienced speakers to share an interesting idea, presentation, or maybe just a small story.</p>
|
|
||||||
|
|
||||||
<p>You must buy an entrance ticket to host a lightning talk; we are unable to offer free tickets for lightning talks.</p>
|
|
||||||
|
|
||||||
<h3>Workshops</h3>
|
|
||||||
<p>We have two workshop areas that will be able to host workshops for approximately 20 people per room. Workshops can be up to 3 hours per slot and can be extended to full day workshops.</p>
|
|
||||||
|
|
||||||
<p>You must buy an entrance ticket to host a workshop; we are unable to offer free tickets for workshops.</p>
|
|
||||||
|
|
||||||
<h3>Submitting Content</h3>
|
|
||||||
<p>Please submit content for BornHack 2017 as early as possible. You can submit content via our website:</p>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>Create a <a href="{% url 'account_signup' %}">user account</a> on the BornHack website</li>
|
|
||||||
<li>Visit <a href="{% url 'proposal_list' camp_slug='bornhack-2017' %}">the proposals page</a></li>
|
|
||||||
<li>Propose a new speaker</li>
|
|
||||||
<li>Propose a new event</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<p>We will review incoming proposals and notify you as early as possible on whether the proposal was accepted or not. Proposals submitted before 1st of July will be notified by us no later than the 16th of July. Late submissions are welcome, but we might be running low on available slots at that time.</p>
|
|
||||||
|
|
||||||
<h3>Contact Information</h3>
|
|
||||||
<p>The BornHack content team can be reached at content@bornhack.dk - for general questions regarding the event please reach out to the info team at info@bornhack.dk</p>
|
|
||||||
|
|
||||||
<p>We are reachable via IRC in #BornHack on irc.baconsvin.org (6nbtgccn5nbcodn3.onion) on port 6697 with TLS, you can also follow us on Twitter at @bornhax.</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1 +0,0 @@
|
||||||
program/templates/bornhack-2019_call_for_speakers.html
|
|
22
src/program/templates/call_for_participation.html
Normal file
22
src/program/templates/call_for_participation.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Call for Participation | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
|
||||||
|
{% if not camp.call_for_participation_open %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>Note!</strong> This Call for Particilation is not open.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not camp.call_for_participation %}
|
||||||
|
<p class='lead'>This CFP has not been written yet.</p>
|
||||||
|
{% else %}
|
||||||
|
{{ camp.call_for_participation|trustedcommonmark }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
33
src/program/templates/combined_proposal_select_person.html
Normal file
33
src/program/templates/combined_proposal_select_person.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Use Existing {{ eventtype.host_title }} or Add New? | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
|
||||||
|
<h3>Use Existing {{ eventtype.host_title }}?</h3>
|
||||||
|
|
||||||
|
<p class="lead">Pick a {{ eventtype.host_title }} from the list below, or press the button at the bottom to add a new {{ eventtype.host_title }} for this {{ eventtype.name }}.</p>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Use an Existing {{ eventtype.host_title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for speakerproposal in speakerproposal_list %}
|
||||||
|
<a href="{% url 'program:eventproposal_create' camp_slug=camp.slug event_type_slug=eventtype.slug speaker_uuid=speakerproposal.uuid %}" class="list-group-item">
|
||||||
|
<h4 class="list-group-item-heading">
|
||||||
|
Use {{ speakerproposal.name }} as {{ eventtype.host_title }}
|
||||||
|
</h4>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'program:proposal_combined_submit' camp_slug=camp.slug event_type_slug=eventtype.slug %}" class="btn btn-primary btn-success"><i class="fas fa-plus"></i> Add New {{ eventtype.host_title }}</a>
|
||||||
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
16
src/program/templates/combined_proposal_submit.html
Normal file
16
src/program/templates/combined_proposal_submit.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
<h3>Submit {{ camp.title }} {{ eventtype.name }} <i class="fas fa-{{ eventtype.icon }}" style="color: {{ eventtype.color }};"></i></h3>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% bootstrap_field field %}
|
||||||
|
{% endfor %}
|
||||||
|
{% bootstrap_button "Submit for Review" button_type="submit" button_class="btn-primary" %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock program_content %}
|
||||||
|
|
|
@ -20,17 +20,17 @@
|
||||||
{% for event in event_list %}
|
{% for event in event_list %}
|
||||||
{% if event.event_type.include_in_event_list %}
|
{% if event.event_type.include_in_event_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background-color: {{ event.event_type.color }}; ">
|
<td>
|
||||||
<a href="{% url 'schedule_index' camp_slug=camp.slug %}?type={{ event.event_type.slug }}" style="color: {% if event.event_type.light_text %}white{% else %}black{% endif %};">
|
<a href="{% url 'program:schedule_index' camp_slug=camp.slug %}?type={{ event.event_type.slug }}">
|
||||||
{{ event.event_type.name }}
|
<i class="fas fa-{{ event.event_type.icon }} fa-lg" style="color: {{ event.event_type.color }};"></i> <span style="font-size: larger">{{ event.event_type.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a>
|
<a href="{% url 'program:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% for speaker in event.speakers.all %}
|
{% for speaker in event.speakers.all %}
|
||||||
<a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}">{{ speaker.name }}</a><br>
|
<a href="{% url 'program:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}">{{ speaker.name }}</a><br>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
N/A
|
N/A
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
15
src/program/templates/event_proposal_add_person.html
Normal file
15
src/program/templates/event_proposal_add_person.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
<h3>Add {{ eventproposal.event_type.host_title }} {{ speakerproposal.name }} to {{ eventproposal.title }}</h3>
|
||||||
|
|
||||||
|
<p class="lead">Really add <b>{{ speakerproposal.name }}</b> as {{ eventproposal.event_type.host_title }} for <b>{{ eventproposal.title }}</b>?
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-check'></i> Yes" button_type="submit" button_class="btn-success" %}
|
||||||
|
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock program_content %}
|
||||||
|
|
15
src/program/templates/event_proposal_remove_person.html
Normal file
15
src/program/templates/event_proposal_remove_person.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
<h3>Remove "{{ speakerproposal.name }}" from "{{ eventproposal.title }}"?</h3>
|
||||||
|
<p class="lead">Really remove this {{ eventproposal.event_type.host_title }} from this event?</p>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-times'></i> Remove" button_type="submit" button_class="btn-danger" %}
|
||||||
|
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary"><i class='fas fa-undo'></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock program_content %}
|
||||||
|
|
33
src/program/templates/event_proposal_select_person.html
Normal file
33
src/program/templates/event_proposal_select_person.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Add {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }} | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
|
||||||
|
<h3>Add New {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }}</h3>
|
||||||
|
|
||||||
|
<p class="lead">You are adding a new {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }}. Either pick an existing {{ eventproposal.event_type.host_title }} from the list below, or press the button to create a new {{ eventproposal.event_type.host_title }}.</p>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Existing Artists</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for speakerproposal in speakerproposal_list %}
|
||||||
|
<a href="{% url 'program:eventproposal_addperson' camp_slug=camp.slug event_uuid=eventproposal.uuid speaker_uuid=speakerproposal.uuid %}" class="list-group-item">
|
||||||
|
<h4 class="list-group-item-heading">
|
||||||
|
Add <b>{{ speakerproposal.name }}</b> to <b>{{ eventproposal.title }}</b>
|
||||||
|
</h4>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'program:speakerproposal_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-primary btn-success"><i class="fas fa-plus"></i> Add New {{ eventproposal.event_type.host_title }}</a>
|
||||||
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
10
src/program/templates/event_proposal_type_select.html
Normal file
10
src/program/templates/event_proposal_type_select.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Select Event Type | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
{% include 'includes/event_proposal_type_select.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
10
src/program/templates/event_type_select.html
Normal file
10
src/program/templates/event_type_select.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Select Event Type | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
{% include 'includes/event_proposal_type_select.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
{% extends 'program_base.html' %}
|
{% extends 'program_base.html' %}
|
||||||
{% load commonmark %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
|
|
||||||
<h2>{{ camp.title }} Event Proposal Details</h2>
|
{% if not camp.call_for_participation_open %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
<ul>
|
<strong>Note!</strong> This Call for Particilation is not open.
|
||||||
<li class="list">Status: <span class="badge">{{ eventproposal.proposal_status }}</span></li>
|
|
||||||
<li class="list">ID: <span class="badge">{{ eventproposal.uuid }}</span></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">{{ eventproposal.title }}</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{{ eventproposal.abstract|commonmark }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>Details for {{ eventproposal.title }}</h2>
|
||||||
|
|
||||||
|
{% include 'includes/eventproposal_detail.html' %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only %}
|
||||||
|
<a href="{% url 'program:eventproposal_delete' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-danger pull-right"><i class="fas fa-times"></i><span class="h5"> Delete</span></a>
|
||||||
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% endblock program_content %}
|
{% endblock program_content %}
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Event Proposal</h3>
|
{% if speaker %}
|
||||||
|
<h3>Submit new {{ event_type.name }} by {{ speaker.name }}</h3>
|
||||||
|
{% else %}
|
||||||
|
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} {{ event_type.name }}</h3>
|
||||||
|
{% endif %}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form form %}
|
{% bootstrap_form form %}
|
||||||
{% bootstrap_button "Save draft" button_type="submit" button_class="btn-primary" %}
|
{% bootstrap_button "Submit for Review" button_type="submit" button_class="btn-primary" %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock program_content %}
|
{% endblock program_content %}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends 'program_base.html' %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
|
||||||
<h3>Confirm Submission</h3>
|
|
||||||
<form method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% bootstrap_form form %}
|
|
||||||
<p class="lead">Really submit this event proposal for approval?</p>
|
|
||||||
{% bootstrap_button "Submit" button_type="submit" button_class="btn-primary" %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock program_content %}
|
|
||||||
|
|
42
src/program/templates/includes/event_proposal_table.html
Normal file
42
src/program/templates/includes/event_proposal_table.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>URLs</th>
|
||||||
|
<th>People</th>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Status</th>
|
||||||
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
|
<th class='text-right'>Available Actions</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for eventproposal in eventproposals %}
|
||||||
|
<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 }}" target="_blank"><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 %}
|
||||||
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
|
<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>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-user" data-toggle="tooltip" title="{{ person.name }}"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</span></td>
|
||||||
|
<td><span class="h4">{{ eventproposal.track.name }}</span></td>
|
||||||
|
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
|
||||||
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
|
<td class='text-right'>
|
||||||
|
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye"></i><span class="h5"> Details</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Submit New Proposal{% if speaker %} for {{ speaker.name }}{% endif %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<h4>What would {% if speaker %}{{ speaker.name }}{% else %}you{% endif %} like to host?</h4>
|
||||||
|
{% if speaker %}
|
||||||
|
<p>You are submitting a new proposal for {{ speaker.name }}. Please begin by selecting the type of proposal below:</p>
|
||||||
|
{% else %}
|
||||||
|
<p>To submit content for {{ camp.title }} please begin by selecting the type of event below:</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for eventtype in eventtype_list %}
|
||||||
|
{% if speaker %}
|
||||||
|
<a href="{%url 'program:eventproposal_create' camp_slug=camp.slug event_type_slug=eventtype.slug speaker_uuid=speaker.uuid %}" class="list-group-item">
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'program:proposal_combined_person_select' camp_slug=camp.slug event_type_slug=eventtype.slug %}" class="list-group-item">
|
||||||
|
{% endif %}
|
||||||
|
<h4 class="list-group-item-heading">
|
||||||
|
<i class="fas fa-{{ eventtype.icon }} fa-2x fa-pull-left fa-fw" style="color: {{ eventtype.color }};"></i>
|
||||||
|
{{ eventtype.name }}<span class="pull-right"><i class="fas fa-plus fa-2x fa-pull-right" style="color: {{ eventtype.color }};"></i></span>
|
||||||
|
</h4>
|
||||||
|
{% if eventtype.description %}<p class="list-group-item-text">{{ eventtype.description }}</p>{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p><i>If you have questions or experience problems submitting proposals here please let us know on IRC or by mail. You can also send an email with your proposal and the Content team will take care of creating it in the system.</i></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
45
src/program/templates/includes/eventproposal_detail.html
Normal file
45
src/program/templates/includes/eventproposal_detail.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{% load commonmark %}
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">{{ eventproposal.title }}</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ eventproposal.abstract|untrustedcommonmark }}
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<a href="{% url 'program:eventproposal_update' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm pull-right"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">Status: <span class="badge">{{ eventproposal.proposal_status }}</span> ID: <span class="badge">{{ eventproposal.uuid }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<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 %}
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">{{ eventproposal.event_type.host_title }} List for {{ eventproposal.title }}</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if eventproposal.speakers.exists %}
|
||||||
|
{% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %}
|
||||||
|
{% else %}
|
||||||
|
<i>Nothing found.</i>
|
||||||
|
{% endif %}
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
{% if eventproposal.get_available_speakerproposals.exists %}
|
||||||
|
<a href="{% url 'program:eventproposal_selectperson' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'program:speakerproposal_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
23
src/program/templates/includes/eventproposalurl_table.html
Normal file
23
src/program/templates/includes/eventproposalurl_table.html
Normal 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 }}" target="_blank">{{ 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>
|
12
src/program/templates/includes/program_menu.html
Normal file
12
src/program/templates/includes/program_menu.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<a href="{% url 'program:schedule_index' camp_slug=camp.slug %}" class="btn {% if url_name == "schedule_index" or urlyear %}btn-primary{% else %}btn-default{% endif %}">Schedule</a>
|
||||||
|
<a href="{% url 'program:event_index' camp_slug=camp.slug %}" class="btn {% if url_name == "event_index" %}btn-primary{% else %}btn-default{% endif %}">Events</a>
|
||||||
|
<a href="{% url 'program:speaker_index' camp_slug=camp.slug %}" class="btn {% if url_name == "speaker_index" %}btn-primary{% else %}btn-default{% endif %}">Speakers</a>
|
||||||
|
<a href="{% url 'program:call_for_participation' camp_slug=camp.slug %}" class="btn {% if url_name == "call_for_participation" %}btn-primary{% else %}btn-default{% endif %}">Call for Participation</a>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
{% if camp.call_for_participation_open %}
|
||||||
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn {% if url_name in proposal_urls %}btn-primary{% else %}btn-default{% endif %}">Submit Proposal</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn {% if url_name in proposal_urls %}btn-primary{% else %}btn-default{% endif %}">View Proposals</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
47
src/program/templates/includes/speaker_proposal_table.html
Normal file
47
src/program/templates/includes/speaker_proposal_table.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class="text-center">Events</th>
|
||||||
|
<th class="text-center">URLs</th>
|
||||||
|
<th>Status</th>
|
||||||
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
|
<th class="text-right">Available Actions</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for speakerproposal in speakerproposals %}
|
||||||
|
<tr>
|
||||||
|
<td><span class="h4">{{ speakerproposal.name }}</span></td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if speakerproposal.eventproposals.all %}
|
||||||
|
{% for ep in speakerproposal.eventproposals.all %}
|
||||||
|
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=ep.uuid %}"><i class="fas fa-{{ ep.event_type.icon }} fa-lg" style="color: {{ ep.event_type.color }};" data-toggle="tooltip" title="{{ ep.title }} ({{ ep.event_type.name }})"></i></a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% for url in speakerproposal.urls.all %}
|
||||||
|
<a href="{{ url.url }}" target="_blank" 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>
|
||||||
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
|
<td class="text-right">
|
||||||
|
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye"></i><span class="h5"> Details</span>
|
||||||
|
</a>
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and eventproposal and eventproposal.speakers.count > 1 %}
|
||||||
|
<a href="{% url 'program:eventproposal_removeperson' camp_slug=camp.slug event_uuid=eventproposal.uuid speaker_uuid=speakerproposal.uuid %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i><span class="h5"> Remove {{ eventproposal.event_type.host_title }}</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
40
src/program/templates/includes/speakerproposal_detail.html
Normal file
40
src/program/templates/includes/speakerproposal_detail.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{% load commonmark %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">{{ speakerproposal.name }}</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ speakerproposal.biography|untrustedcommonmark }}
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<a href="{% url 'program:speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm pull-right"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">Status: <span class="badge">{{ speakerproposal.proposal_status }}</span> | ID: <span class="badge">{{ speakerproposal.uuid }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<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 %}
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</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 %}
|
||||||
|
{% else %}
|
||||||
|
<i>Nothing found.</i>
|
||||||
|
{% endif %}
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<a href="{% url 'program:eventproposal_typeselect' 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 New Event</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
25
src/program/templates/includes/speakerproposalurl_table.html
Normal file
25
src/program/templates/includes/speakerproposalurl_table.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>URLs</th>
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<th class='text-right'>Available Actions</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for url in speakerproposal.urls.all %}
|
||||||
|
<tr>
|
||||||
|
<td><i class="{{ url.urltype.icon }} fa-lg"></i><span class="h4"> {{ url.urltype.name }}</span></td>
|
||||||
|
<td><span class="h4"><a href="{{ url.url }}" target="_blank">{{ url }}</a></span></td>
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
|
||||||
|
<td class='text-right'>
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ instance.when.lower|date:"H:i" }}-{{ instance.when.upper|date:"H:i" }}</td>
|
<td>{{ instance.when.lower|date:"H:i" }}-{{ instance.when.upper|date:"H:i" }}</td>
|
||||||
<td><a href="{% url 'event_detail' camp_slug=camp.slug slug=instance.event.slug %}">{{ instance.event.title }}</a></td>
|
<td><a href="{% url 'program:event_detail' camp_slug=camp.slug slug=instance.event.slug %}">{{ instance.event.title }}</a></td>
|
||||||
<td>{{ instance.location.name }}</td>
|
<td>{{ instance.location.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -6,20 +6,10 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="btn-group btn-group-justified hidden-xs">
|
<div class="btn-group btn-group-justified hidden-xs">
|
||||||
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" class="btn {% if url_name == "schedule_index" or urlyear %}btn-primary{% else %}btn-default{% endif %}">Schedule</a>
|
{% include 'includes/program_menu.html' %}
|
||||||
<a href="{% url 'event_index' camp_slug=camp.slug %}" class="btn {% if url_name == "event_index" %}btn-primary{% else %}btn-default{% endif %}">Events</a>
|
|
||||||
<a href="{% url 'speaker_index' camp_slug=camp.slug %}" class="btn {% if url_name == "speaker_index" %}btn-primary{% else %}btn-default{% endif %}">Speakers</a>
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<a href="{% url 'proposal_list' camp_slug=camp.slug %}" class="btn {% if url_name in proposal_urls %}btn-primary{% else %}btn-default{% endif %}">Your Proposals</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-vertical visible-xs">
|
<div class="btn-group-vertical visible-xs">
|
||||||
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" class="btn {% if url_name == "schedule_index" or urlyear %}btn-primary{% else %}btn-default{% endif %}">Schedule</a>
|
{% include 'includes/program_menu.html' %}
|
||||||
<a href="{% url 'event_index' camp_slug=camp.slug %}" class="btn {% if url_name == "event_index" %}btn-primary{% else %}btn-default{% endif %}">Events</a>
|
|
||||||
<a href="{% url 'speaker_index' camp_slug=camp.slug %}" class="btn {% if url_name == "speaker_index" %}btn-primary{% else %}btn-default{% endif %}">Speakers</a>
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<a href="{% url 'proposal_list' camp_slug=camp.slug %}" class="btn {% if url_name in proposal_urls %}btn-primary{% else %}btn-default{% endif %}">Your Proposals</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
|
19
src/program/templates/proposal_delete.html
Normal file
19
src/program/templates/proposal_delete.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
{% if object.name %}
|
||||||
|
<h3>Delete "{{ object.name }}"</h3>
|
||||||
|
{% else %}
|
||||||
|
<h3>Delete "{{ object.title }}"</h3>
|
||||||
|
{% endif %}
|
||||||
|
<p class="lead">Really delete this proposal? This action cannot be undone.</p>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_button "<i class='fas fa-times'></i> Delete" button_type="submit" button_class="btn-danger" %}
|
||||||
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary" ><i class='fas fa-undo'></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock program_content %}
|
||||||
|
|
|
@ -5,98 +5,39 @@ Proposals | {{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
<h3>Submitting</h3>
|
|
||||||
<p>To submit a talk or other event for {{ camp.title }} you need to to the following:</p>
|
|
||||||
|
|
||||||
<ol>
|
{% if camp.call_for_participation_open %}
|
||||||
<li>First you propose one or more speakers. Most events just have one speaker, but some events might have two or more. Be sure to create everyone before going on to step 2.</li>
|
{% include 'includes/event_proposal_type_select.html' %}
|
||||||
<li>Then you propose one or more events. The <i>Propose New Event</i> form will allow you to choose the speaker(s) you proposed.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<p>If you experience problems submitting proposals here please let us know on IRC or by mail. You can also send an email with your proposal and the Content team will take care of creating it in the system.</p>
|
|
||||||
|
|
||||||
<h3>Your {{ camp.title }} Speaker Proposals</h3>
|
|
||||||
{% if speakerproposal_list %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for speakerproposal in speakerproposal_list %}
|
|
||||||
<tr>
|
|
||||||
<td><b>{{ speakerproposal.name }}</b></td>
|
|
||||||
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Details</a>
|
|
||||||
{% if not camp.read_only %}
|
|
||||||
<a href="{% url 'speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Modify</a>
|
|
||||||
{% if speakerproposal.proposal_status == "pending" or speakerproposal.proposal_status == "approved" %}
|
|
||||||
<a href="{% url 'speakerproposal_submit' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs btn-disabled" disabled>Submit</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'speakerproposal_submit' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Submit</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="#" class="btn btn-danger btn-xs btn-disabled" disabled>Delete</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<h4>No speaker proposals found</h4>
|
<div class="alert alert-danger">
|
||||||
|
<strong>Note!</strong> This Call for Particilation is not open.
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not camp.read_only and camp.call_for_speakers_open %}
|
|
||||||
<a href="{% url 'speakerproposal_create' camp_slug=camp.slug %}" class="btn btn-primary btn-sm">Propose New Speaker</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>
|
{% if speakerproposal_list or eventproposal_list %}
|
||||||
<br>
|
<div class="panel panel-default">
|
||||||
</p>
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title"><i class="fas fa-pencil"></i> Existing Proposals</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<h4>People</h4>
|
||||||
|
{% if speakerproposal_list %}
|
||||||
|
{% include 'includes/speaker_proposal_table.html' with speakerproposals=speakerproposal_list %}
|
||||||
|
{% else %}
|
||||||
|
<i>Nothing found.</i>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h3>Your {{ camp.title }} Event Proposals</h3>
|
<p><hr></p>
|
||||||
{% if eventproposal_list %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for eventproposal in eventproposal_list %}
|
|
||||||
<tr>
|
|
||||||
<td><b>{{ eventproposal.title }}</b></td>
|
|
||||||
<td><b>{{ eventproposal.event_type }}</b></td>
|
|
||||||
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-xs">Details</a>
|
|
||||||
{% if not camp.read_only %}
|
|
||||||
<a href="{% url 'eventproposal_update' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-xs">Modify</a>
|
|
||||||
{% if eventproposal.proposal_status == "pending" %}
|
|
||||||
<a href="{% url 'eventproposal_submit' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-xs btn-disabled" disabled>Submit</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'eventproposal_submit' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-xs">Submit</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="#" class="btn btn-danger btn-xs btn-disabled" disabled>Delete</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<h4>No event proposals found</h4>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not camp.read_only and camp.call_for_speakers_open %}
|
<h4>Events</h4>
|
||||||
<a href="{% url 'eventproposal_create' camp_slug=camp.slug %}" class="btn btn-primary btn-sm">Propose New Event</a>
|
{% if eventproposal_list %}
|
||||||
|
{% include 'includes/event_proposal_table.html' with eventproposals=eventproposal_list %}
|
||||||
|
{% else %}
|
||||||
|
<i>Nothing found.</i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -31,8 +31,8 @@
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<label for="event-type-{{ type.slug }}" class="btn btn-default" style="min-width: 200px; text-align: left;">
|
<label for="event-type-{{ type.slug }}" class="btn btn-default" style="min-width: 200px; text-align: left;">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-minus"></i>
|
<i class="fas fa-minus"></i>
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
|
|
||||||
{{ type.name }}
|
{{ type.name }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -59,12 +59,12 @@
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<label for="location-{{ location.slug }}" class="btn btn-default" style="min-width: 200px; text-align: left;">
|
<label for="location-{{ location.slug }}" class="btn btn-default" style="min-width: 200px; text-align: left;">
|
||||||
<span class="pull-left">
|
<span class="pull-left">
|
||||||
<i class="fa fa-minus"></i>
|
<i class="fas fa-minus"></i>
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
|
|
||||||
{{ location.name }}
|
{{ location.name }}
|
||||||
</span>
|
</span>
|
||||||
<i class="pull-right fa fa-{{ location.icon }}"></i>
|
<i class="pull-right fas fa-{{ location.icon }}"></i>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a id="ics-button" class="btn btn-default form-control filter-control">
|
<a id="ics-button" class="btn btn-default form-control filter-control">
|
||||||
<i class="fa fa-calendar"></i> ICS
|
<i class="fas fa-calendar"></i> ICS
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
|
||||||
{% url 'schedule_index' camp_slug=camp.slug as baseurl %}
|
{% url 'program:schedule_index' camp_slug=camp.slug as baseurl %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$('.filter-control').on('change', function() {
|
$('.filter-control').on('change', function() {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{% for eventinstance in eventinstances %}
|
{% for eventinstance in eventinstances %}
|
||||||
{% if eventinstance.when.lower.time == timeslot.time %}
|
{% if eventinstance.when.lower.time == timeslot.time %}
|
||||||
<td style="background-color: {{ eventinstance.event.event_type.color }}; color: {% if eventinstance.event.event_type.light_text %}white{% else %}black{% endif %};" class="event-td" rowspan={{ eventinstance.timeslots }} data-eventinstance-id="{{ eventinstance.id }}">
|
<td style="background-color: {{ eventinstance.event.event_type.color }}; color: {% if eventinstance.event.event_type.light_text %}white{% else %}black{% endif %};" class="event-td" rowspan={{ eventinstance.timeslots }} data-eventinstance-id="{{ eventinstance.id }}">
|
||||||
<a style="color:inherit;" href="{% url 'event_detail' camp_slug=camp.slug slug=eventinstance.event.slug %}">
|
<a style="color:inherit;" href="{% url 'program:event_detail' camp_slug=camp.slug slug=eventinstance.event.slug %}">
|
||||||
{{ eventinstance.event.title }}<br>
|
{{ eventinstance.event.title }}<br>
|
||||||
{{ eventinstance.when.lower.time }}-{{ eventinstance.when.upper.time }}
|
{{ eventinstance.when.lower.time }}-{{ eventinstance.when.upper.time }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<noscript>
|
<noscript>
|
||||||
<a href="{% url "noscript_schedule_index" camp_slug=camp.slug %}" class="btn btn-primary">
|
<a href="{% url "program:noscript_schedule_index" camp_slug=camp.slug %}" class="btn btn-primary">
|
||||||
Back to noscript schedule
|
Back to noscript schedule
|
||||||
</a>
|
</a>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -14,10 +14,10 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading" ><span style="font-size: x-large"><span style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">{{ event.event_type.name }}</span> {{ event.title }}</span></div>
|
<div class="panel-heading" ><span style="font-size: x-large"><i class="fas fa-{{ event.event_type.icon }} fa-lg" style="color: {{ event.event_type.color }};"></i> {{ event.title }}</span></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>
|
<p>
|
||||||
{{ event.abstract|commonmark }}
|
{{ event.abstract|untrustedcommonmark }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<h4>Speakers</h4>
|
<h4>Speakers</h4>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for speaker in event.speakers.all %}
|
{% for speaker in event.speakers.all %}
|
||||||
<h4><a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item">{{ speaker.name }}</a></h4>
|
<h4><a href="{% url 'program:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item">{{ speaker.name }}</a></h4>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<noscript>
|
<noscript>
|
||||||
<meta http-equiv="refresh" content="0; url={% url "noscript_schedule_index" camp_slug=camp.slug %}" />
|
<meta http-equiv="refresh" content="0; url={% url "program:noscript_schedule_index" camp_slug=camp.slug %}" />
|
||||||
</noscript>
|
</noscript>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
No javascript? Don't worry, we have a HTML only version of the schedule! Redirecting you there now.
|
No javascript? Don't worry, we have a HTML only version of the schedule! Redirecting you there now.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url "noscript_schedule_index" camp_slug=camp.slug %}">
|
<a href="{% url "program:noscript_schedule_index" camp_slug=camp.slug %}">
|
||||||
Click here if you are not redirected.
|
Click here if you are not redirected.
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -34,7 +34,7 @@ var elm_app = Elm.Main.embed(
|
||||||
container,
|
container,
|
||||||
{ 'schedule_timeslot_length_minutes': Number('{{ schedule_timeslot_length_minutes }}')
|
{ 'schedule_timeslot_length_minutes': Number('{{ schedule_timeslot_length_minutes }}')
|
||||||
, 'schedule_midnight_offset_hours': Number('{{ schedule_midnight_offset_hours }}')
|
, 'schedule_midnight_offset_hours': Number('{{ schedule_midnight_offset_hours }}')
|
||||||
, 'ics_button_href': "{% url 'ics_view' camp_slug=camp.slug %}"
|
, 'ics_button_href': "{% url 'program:ics_view' camp_slug=camp.slug %}"
|
||||||
, 'camp_slug': "{{ camp.slug }}"
|
, 'camp_slug': "{{ camp.slug }}"
|
||||||
, 'websocket_server': "ws://" + window.location.host + "/schedule/"
|
, 'websocket_server': "ws://" + window.location.host + "/schedule/"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,11 @@
|
||||||
|
|
||||||
<h3>{{ speaker.name }}</h3>
|
<h3>{{ speaker.name }}</h3>
|
||||||
|
|
||||||
{% if speaker.picture_large and speaker.picture_small %}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 text-container">
|
<div class="col-md-12 text-container">
|
||||||
{{ speaker.biography|commonmark }}
|
{{ speaker.biography|untrustedcommonmark }}
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<a href="{% url 'speaker_picture' camp_slug=camp.slug slug=speaker.slug picture='large' %}" >
|
|
||||||
<img src="{% url 'speaker_picture' camp_slug=camp.slug slug=speaker.slug picture='thumbnail' %}" alt="{{ camp.title }} speaker picture of {{ speaker.name }}" width="200px">
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
{{ speaker.biography|commonmark }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
@ -28,9 +19,9 @@
|
||||||
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
|
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
|
||||||
{{ event.event_type.name }}
|
{{ event.event_type.name }}
|
||||||
</small>
|
</small>
|
||||||
<a href="{% url 'event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a>
|
<a href="{% url 'program:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a>
|
||||||
</h3>
|
</h3>
|
||||||
{{ event.abstract|commonmark }}
|
{{ event.abstract|untrustedcommonmark }}
|
||||||
|
|
||||||
<h4>Instances</h4>
|
<h4>Instances</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
|
|
@ -13,12 +13,10 @@
|
||||||
|
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for speaker in speaker_list %}
|
{% for speaker in speaker_list %}
|
||||||
<a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item">
|
<a href="{% url 'program:speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item">
|
||||||
{{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }})
|
{{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }})
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p><a href="{% url 'call_for_speakers' camp_slug=camp.slug %}" class="btn btn-primary"><span {% if not camp.call_for_speakers_open %}style="text-decoration: line-through;"{% endif %}>Call for Speakers</span></a></p>
|
|
||||||
{% endblock program_content %}
|
{% endblock program_content %}
|
||||||
|
|
15
src/program/templates/speakerproposal_delete.html
Normal file
15
src/program/templates/speakerproposal_delete.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block program_content %}
|
||||||
|
<h3>Delete {{ object.name }}</h3>
|
||||||
|
<p class="lead">Really delete this proposal? This action cannot be undone.</p>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_button "Delete" button_type="submit" button_class="btn-danger" %}
|
||||||
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}">{% bootstrap_button "Cancel" button_type="link" button_class="btn-primary" %}</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock program_content %}
|
||||||
|
|
|
@ -1,37 +1,24 @@
|
||||||
{% extends 'program_base.html' %}
|
{% extends 'program_base.html' %}
|
||||||
{% load commonmark %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
|
|
||||||
<h2>{{ camp.title }} Speaker Proposal Details</h2>
|
{% if not camp.call_for_participation_open %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
<ul>
|
<strong>Note!</strong> This Call for Particilation is not open.
|
||||||
<li class="list">Status: <span class="badge">{{ speakerproposal.proposal_status }}</span></li>
|
|
||||||
<li class="list">ID: <span class="badge">{{ speakerproposal.uuid }}</span></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">{{ speakerproposal.name }}</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% if speakerproposal.picture_large and speakerproposal.picture_small %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8 text-container">
|
|
||||||
{{ speakerproposal.biography|commonmark }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<a href="{% url 'speakerproposal_picture' camp_slug=camp.slug pk=speakerproposal.pk picture='large' %}" >
|
|
||||||
<img src="{% url 'speakerproposal_picture' camp_slug=camp.slug pk=speakerproposal.pk picture='thumbnail' %}" alt="{{ camp.title }} speaker picture of {{ speakerproposal.name }}">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{{ speakerproposal.biography|commonmark }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>Details for {{ speakerproposal.name }}</h2>
|
||||||
|
|
||||||
|
{% include 'includes/speakerproposal_detail.html' %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
||||||
|
{% if camp.call_for_participation_open and not camp.read_only %}
|
||||||
|
{% if not speakerproposal.eventproposals.all %}
|
||||||
|
<a href="{% url 'program:speakerproposal_delete' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-danger pull-right"><i class="fas fa-times"></i><span class="h5"> Delete Person</span></a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% endblock program_content %}
|
{% endblock program_content %}
|
||||||
|
|
|
@ -2,11 +2,19 @@
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Speaker Proposal</h3>
|
|
||||||
|
<h3>
|
||||||
|
{% if object %}
|
||||||
|
Update {{ object.name }} Details
|
||||||
|
{% else %}
|
||||||
|
Add {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }}
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form form %}
|
{% bootstrap_form form %}
|
||||||
{% bootstrap_button "Save draft" button_type="submit" button_class="btn-primary" %}
|
{% bootstrap_button "Submit for review" button_type="submit" button_class="btn-primary" %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock program_content %}
|
{% endblock program_content %}
|
||||||
|
|
19
src/program/templates/url_delete.html
Normal file
19
src/program/templates/url_delete.html
Normal 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 %}
|
||||||
|
|
21
src/program/templates/url_form.html
Normal file
21
src/program/templates/url_form.html
Normal 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 %}
|
||||||
|
|
191
src/program/urls.py
Normal file
191
src/program/urls.py
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
from django.urls import path, include
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
app_name = 'program'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
'',
|
||||||
|
ScheduleView.as_view(),
|
||||||
|
name='schedule_index'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'noscript/',
|
||||||
|
NoScriptScheduleView.as_view(),
|
||||||
|
name='noscript_schedule_index'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'ics/', ICSView.as_view(), name="ics_view"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'control/', ProgramControlCenter.as_view(), name="program_control_center"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'proposals/', include([
|
||||||
|
path(
|
||||||
|
'',
|
||||||
|
ProposalListView.as_view(),
|
||||||
|
name='proposal_list',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'submit/', include([
|
||||||
|
path(
|
||||||
|
'',
|
||||||
|
CombinedProposalTypeSelectView.as_view(),
|
||||||
|
name='proposal_combined_type_select',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<slug:event_type_slug>/',
|
||||||
|
CombinedProposalSubmitView.as_view(),
|
||||||
|
name='proposal_combined_submit',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<slug:event_type_slug>/select_person/',
|
||||||
|
CombinedProposalPersonSelectView.as_view(),
|
||||||
|
name='proposal_combined_person_select',
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'people/', include([
|
||||||
|
path(
|
||||||
|
'<uuid:pk>/',
|
||||||
|
SpeakerProposalDetailView.as_view(),
|
||||||
|
name='speakerproposal_detail'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:pk>/update/',
|
||||||
|
SpeakerProposalUpdateView.as_view(),
|
||||||
|
name='speakerproposal_update'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:pk>/delete/',
|
||||||
|
SpeakerProposalDeleteView.as_view(),
|
||||||
|
name='speakerproposal_delete'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:speaker_uuid>/add_event/',
|
||||||
|
EventProposalTypeSelectView.as_view(),
|
||||||
|
name='eventproposal_typeselect'
|
||||||
|
),
|
||||||
|
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'
|
||||||
|
),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'events/', include([
|
||||||
|
path(
|
||||||
|
'<uuid:pk>/',
|
||||||
|
EventProposalDetailView.as_view(),
|
||||||
|
name='eventproposal_detail'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:pk>/update/',
|
||||||
|
EventProposalUpdateView.as_view(),
|
||||||
|
name='eventproposal_update'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:pk>/delete/',
|
||||||
|
EventProposalDeleteView.as_view(),
|
||||||
|
name='eventproposal_delete'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:event_uuid>/add_person/',
|
||||||
|
EventProposalSelectPersonView.as_view(),
|
||||||
|
name='eventproposal_selectperson'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:event_uuid>/add_person/new/',
|
||||||
|
SpeakerProposalCreateView.as_view(),
|
||||||
|
name='speakerproposal_create'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:event_uuid>/add_person/<uuid:speaker_uuid>/',
|
||||||
|
EventProposalAddPersonView.as_view(),
|
||||||
|
name='eventproposal_addperson'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<uuid:event_uuid>/remove_person/<uuid:speaker_uuid>/',
|
||||||
|
EventProposalRemovePersonView.as_view(),
|
||||||
|
name='eventproposal_removeperson'
|
||||||
|
),
|
||||||
|
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'
|
||||||
|
),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'speakers/', include([
|
||||||
|
path(
|
||||||
|
'',
|
||||||
|
SpeakerListView.as_view(),
|
||||||
|
name='speaker_index'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<slug:slug>/',
|
||||||
|
SpeakerDetailView.as_view(),
|
||||||
|
name='speaker_detail'
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'events/',
|
||||||
|
EventListView.as_view(),
|
||||||
|
name='event_index'
|
||||||
|
),
|
||||||
|
# legacy CFS url kept on purpose to keep old links functional
|
||||||
|
path(
|
||||||
|
'call-for-speakers/',
|
||||||
|
CallForParticipationView.as_view(),
|
||||||
|
name='call_for_speakers'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'call-for-participation/',
|
||||||
|
CallForParticipationView.as_view(),
|
||||||
|
name='call_for_participation'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'calendar',
|
||||||
|
ICSView.as_view(),
|
||||||
|
name='ics_calendar'
|
||||||
|
),
|
||||||
|
# this must be the last URL here or the regex will overrule the others
|
||||||
|
path(
|
||||||
|
'<slug:slug>',
|
||||||
|
EventDetailView.as_view(),
|
||||||
|
name='event_detail'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.views.generic import ListView, TemplateView, DetailView, View
|
from django.views.generic import ListView, TemplateView, DetailView, View
|
||||||
from django.views.generic.edit import CreateView, UpdateView
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.decorators.http import require_safe
|
from django.views.decorators.http import require_safe
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
|
@ -10,34 +11,44 @@ from django.utils.decorators import method_decorator
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.urls import reverse
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.template import Engine, Context
|
from django.template import Engine, Context
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from betterforms.multiform import MultiModelForm
|
||||||
import icalendar
|
import icalendar
|
||||||
|
|
||||||
from camps.mixins import CampViewMixin
|
from camps.mixins import CampViewMixin
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
CreateProposalMixin,
|
|
||||||
EnsureUnapprovedProposalMixin,
|
EnsureUnapprovedProposalMixin,
|
||||||
EnsureUserOwnsProposalMixin,
|
EnsureUserOwnsProposalMixin,
|
||||||
EnsureWritableCampMixin,
|
EnsureWritableCampMixin,
|
||||||
PictureViewMixin,
|
EnsureCFPOpenMixin,
|
||||||
EnsureCFSOpenMixin
|
UrlViewMixin,
|
||||||
)
|
)
|
||||||
from .email import (
|
from .email import (
|
||||||
|
add_new_eventproposal_email,
|
||||||
|
add_new_speakerproposal_email,
|
||||||
add_speakerproposal_updated_email,
|
add_speakerproposal_updated_email,
|
||||||
add_eventproposal_updated_email
|
add_eventproposal_updated_email
|
||||||
)
|
)
|
||||||
from . import models
|
from . import models
|
||||||
|
from .forms import SpeakerProposalForm, EventProposalForm
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################################
|
||||||
# ical calendar
|
# ical calendar
|
||||||
|
|
||||||
|
|
||||||
class ICSView(CampViewMixin, View):
|
class ICSView(CampViewMixin, View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
eventinstances = models.EventInstance.objects.filter(event__camp=self.camp)
|
eventinstances = models.EventInstance.objects.filter(
|
||||||
|
event__track__camp=self.camp
|
||||||
|
)
|
||||||
|
|
||||||
# Type query
|
# Type query
|
||||||
type_query = request.GET.get('type', None)
|
type_query = request.GET.get('type', None)
|
||||||
|
@ -88,7 +99,8 @@ class ICSView(CampViewMixin, View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# proposals
|
###################################################################################################
|
||||||
|
# proposals list view
|
||||||
|
|
||||||
|
|
||||||
class ProposalListView(LoginRequiredMixin, CampViewMixin, ListView):
|
class ProposalListView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||||
|
@ -103,55 +115,135 @@ class ProposalListView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# also add eventproposals to the context
|
# also add eventproposals to the context
|
||||||
context['eventproposal_list'] = models.EventProposal.objects.filter(camp=self.camp, user=self.request.user)
|
context['eventproposal_list'] = models.EventProposal.objects.filter(track__camp=self.camp, user=self.request.user)
|
||||||
|
context['eventtype_list'] = models.EventType.objects.filter(public=True)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, CreateProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, CreateView):
|
###################################################################################################
|
||||||
|
# speakerproposal views
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, CreateView):
|
||||||
|
""" This view allows a user to create a new SpeakerProposal linked to an existing EventProposal """
|
||||||
model = models.SpeakerProposal
|
model = models.SpeakerProposal
|
||||||
fields = ['name', 'biography', 'picture_small', 'picture_large', 'submission_notes']
|
|
||||||
template_name = 'speakerproposal_form.html'
|
template_name = 'speakerproposal_form.html'
|
||||||
|
form_class = SpeakerProposalForm
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
""" Get the eventproposal object """
|
||||||
|
self.eventproposal = get_object_or_404(models.EventProposal, pk=kwargs['event_uuid'])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
"""
|
||||||
|
Set camp and eventtype for the form
|
||||||
|
"""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs.update({
|
||||||
|
'camp': self.camp,
|
||||||
|
'eventtype': self.eventproposal.event_type
|
||||||
|
})
|
||||||
|
return kwargs
|
||||||
|
|
||||||
class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
def get_context_data(self, **kwargs):
|
||||||
model = models.SpeakerProposal
|
context = super().get_context_data(**kwargs)
|
||||||
fields = ['name', 'biography', 'picture_small', 'picture_large', 'submission_notes']
|
context['eventproposal'] = self.eventproposal
|
||||||
template_name = 'speakerproposal_form.html'
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_PENDING:
|
# set user before saving
|
||||||
messages.warning(self.request, "Your speaker proposal has been reverted to status draft. Please submit it again when you are ready.")
|
form.instance.user = self.request.user
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_DRAFT
|
form.instance.camp = self.camp
|
||||||
|
speakerproposal = form.save()
|
||||||
|
|
||||||
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED:
|
# add speakerproposal to eventproposal
|
||||||
messages.warning(self.request, "Your speaker proposal has been set to modified after approval. Please await approval of the changes.")
|
self.eventproposal.speakers.add(speakerproposal)
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_MODIFIED_AFTER_APPROVAL
|
|
||||||
if not add_speakerproposal_updated_email(form.instance):
|
|
||||||
logger.error(
|
|
||||||
'Unable to add update email to queue for speaker: {}'.format(form.instance)
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
# send mail to content team
|
||||||
|
if not add_new_speakerproposal_email(speakerproposal):
|
||||||
|
logger.error("Unable to send email to content team after new speakerproposal")
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SpeakerProposalSubmitView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureUnapprovedProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, UpdateView):
|
||||||
|
"""
|
||||||
|
This view allows a user to update an existing SpeakerProposal.
|
||||||
|
"""
|
||||||
model = models.SpeakerProposal
|
model = models.SpeakerProposal
|
||||||
fields = []
|
template_name = 'speakerproposal_form.html'
|
||||||
template_name = 'speakerproposal_submit.html'
|
form_class = SpeakerProposalForm
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_form_kwargs(self):
|
||||||
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
"""
|
||||||
|
Set camp and eventtype for the form
|
||||||
|
"""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
if self.get_object().eventproposals.count() == 1:
|
||||||
|
# determine which form to use based on the type of event associated with the proposal
|
||||||
|
eventtype = self.get_object().eventproposals.get().event_type
|
||||||
|
else:
|
||||||
|
# more than one eventproposal. If all events are the same type we can still show a non-generic form here
|
||||||
|
eventtypes = set()
|
||||||
|
for ep in self.get_object().eventproposals.all():
|
||||||
|
eventtypes.add(ep.event_type)
|
||||||
|
if len(eventtypes) == 1:
|
||||||
|
eventtype = self.get_object().eventproposals.get().event_type
|
||||||
|
else:
|
||||||
|
# more than one type of event for this person, return the generic speakerproposal form
|
||||||
|
eventtype = None
|
||||||
|
|
||||||
|
# add camp and eventtype to form kwargs
|
||||||
|
kwargs.update({
|
||||||
|
'camp': self.camp,
|
||||||
|
'eventtype': eventtype
|
||||||
|
})
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_PENDING
|
"""
|
||||||
messages.info(self.request, "Your proposal has been submitted and is now pending approval")
|
Change the speakerproposal status to pending
|
||||||
return super().form_valid(form)
|
"""
|
||||||
|
# set proposal status to pending
|
||||||
|
form.instance.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
||||||
|
speakerproposal = form.save()
|
||||||
|
|
||||||
|
# send mail to content team
|
||||||
|
if not add_speakerproposal_updated_email(speakerproposal):
|
||||||
|
logger.error("Unable to send email to content team after speakerproposal update")
|
||||||
|
|
||||||
|
# message user and redirect
|
||||||
|
messages.info(self.request, "Your proposal is now pending approval by the content team.")
|
||||||
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakerProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DeleteView):
|
||||||
|
"""
|
||||||
|
This view allows a user to delete an existing SpeakerProposal object, as long as it is not linked to any EventProposals
|
||||||
|
"""
|
||||||
|
model = models.SpeakerProposal
|
||||||
|
template_name = 'proposal_delete.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# do not permit deleting if this speakerproposal is linked to any eventproposals
|
||||||
|
if self.get_object().eventproposals.exists():
|
||||||
|
messages.error(request, "Cannot delete a person while it is associated with one or more eventproposals. Delete those first.")
|
||||||
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
|
# continue with the request
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
messages.success(self.request, "Proposal '%s' has been deleted." % self.object.name)
|
||||||
|
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, DetailView):
|
class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, DetailView):
|
||||||
|
@ -159,101 +251,383 @@ class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwn
|
||||||
template_name = 'speakerproposal_detail.html'
|
template_name = 'speakerproposal_detail.html'
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(require_safe, name='dispatch')
|
###################################################################################################
|
||||||
class SpeakerProposalPictureView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, PictureViewMixin, DetailView):
|
# eventproposal views
|
||||||
|
|
||||||
|
|
||||||
|
class EventProposalTypeSelectView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, ListView):
|
||||||
|
"""
|
||||||
|
This view is for selecting the type of event to submit (when adding a new eventproposal to an existing speakerproposal)
|
||||||
|
"""
|
||||||
|
model = models.EventType
|
||||||
|
template_name = 'event_type_select.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
""" Get the speakerproposal object """
|
||||||
|
self.speaker = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
""" We only allow submissions of events with EventTypes where public=True """
|
||||||
|
return super().get_queryset().filter(public=True)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
""" Make speakerproposal object available in template """
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['speaker'] = self.speaker
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventProposalSelectPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, ListView):
|
||||||
|
"""
|
||||||
|
This view is for selecting an existing speakerproposal to add to an existing eventproposal
|
||||||
|
"""
|
||||||
model = models.SpeakerProposal
|
model = models.SpeakerProposal
|
||||||
|
template_name = 'event_proposal_select_person.html'
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# is the proposal owned by current user?
|
""" Get EventProposal from url kwargs """
|
||||||
if self.get_object().user != request.user:
|
self.eventproposal = get_object_or_404(models.EventProposal, pk=kwargs['event_uuid'], user=request.user)
|
||||||
raise Http404()
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
# get and return the response
|
def get_queryset(self, **kwargs):
|
||||||
response = self.get_picture_response('/public/speakerproposals/%(campslug)s/%(proposaluuid)s/%(filename)s' % {
|
""" Filter out any speakerproposals already added to this eventproposal """
|
||||||
'campslug': self.camp.slug,
|
return self.eventproposal.get_available_speakerproposals().all()
|
||||||
'proposaluuid': self.get_object().uuid,
|
|
||||||
'filename': os.path.basename(self.picture.name),
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
""" Make eventproposal object available in template """
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['eventproposal'] = self.eventproposal
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventProposalAddPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView):
|
||||||
|
"""
|
||||||
|
This view is for adding an existing speakerproposal to an existing eventproposal
|
||||||
|
"""
|
||||||
|
model = models.EventProposal
|
||||||
|
template_name = 'event_proposal_add_person.html'
|
||||||
|
fields = []
|
||||||
|
pk_url_kwarg = 'event_uuid'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
""" Get the speakerproposal object """
|
||||||
|
self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'], user=request.user)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
""" Make speakerproposal object available in template """
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['speakerproposal'] = self.speakerproposal
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.speakers.add(self.speakerproposal)
|
||||||
|
messages.success(self.request, "%s has been added as %s for %s" % (
|
||||||
|
self.speakerproposal.name,
|
||||||
|
form.instance.event_type.host_title,
|
||||||
|
form.instance.title
|
||||||
|
))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
||||||
|
class EventProposalRemovePersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView):
|
||||||
|
"""
|
||||||
|
This view is for removing a speakerproposal from an existing eventproposal
|
||||||
|
"""
|
||||||
|
model = models.EventProposal
|
||||||
|
template_name = 'event_proposal_remove_person.html'
|
||||||
|
fields = []
|
||||||
|
pk_url_kwarg = 'event_uuid'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
""" Get the speakerproposal object and check a few things """
|
||||||
|
# get the speakerproposal object from URL kwargs
|
||||||
|
self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'], user=request.user)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
""" Make speakerproposal object available in template """
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['speakerproposal'] = self.speakerproposal
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
""" Remove the speaker from the event """
|
||||||
|
if self.speakerproposal not in self.get_object().speakers.all():
|
||||||
|
# this speaker is not associated with this event
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
if self.get_object().speakers.count() == 1:
|
||||||
|
messages.error(self.request, "Cannot delete the last person associalted with event!")
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
# remove speakerproposal from eventproposal
|
||||||
|
form.instance.speakers.remove(self.speakerproposal)
|
||||||
|
messages.success(self.request, "%s has been removed from %s" % (
|
||||||
|
self.speakerproposal.name,
|
||||||
|
self.get_object().title
|
||||||
|
))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse(
|
||||||
|
'program:eventproposal_detail', kwargs={
|
||||||
|
'camp_slug': self.camp.slug,
|
||||||
|
'pk': self.get_object().uuid
|
||||||
})
|
})
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, CreateView):
|
||||||
class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, CreateProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, CreateView):
|
"""
|
||||||
|
This view allows a user to create a new eventproposal linked to an existing speakerproposal
|
||||||
|
"""
|
||||||
model = models.EventProposal
|
model = models.EventProposal
|
||||||
fields = ['title', 'abstract', 'event_type', 'speakers', 'allow_video_recording', 'submission_notes']
|
|
||||||
template_name = 'eventproposal_form.html'
|
template_name = 'eventproposal_form.html'
|
||||||
|
form_class = EventProposalForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
""" Get the speakerproposal object """
|
||||||
|
self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=self.kwargs['speaker_uuid'])
|
||||||
|
self.event_type = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug'])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
""" Make speakerproposal object available in template """
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['form'].fields['speakers'].queryset = models.SpeakerProposal.objects.filter(camp=self.camp, user=self.request.user)
|
context['speaker'] = self.speakerproposal
|
||||||
context['form'].fields['event_type'].queryset = models.EventType.objects.filter(public=True)
|
context['event_type'] = self.event_type
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
"""
|
||||||
|
Set camp and eventtype for the form
|
||||||
|
"""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs.update({
|
||||||
|
'camp': self.camp,
|
||||||
|
'eventtype': self.event_type
|
||||||
|
})
|
||||||
|
return kwargs
|
||||||
|
|
||||||
class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
def form_valid(self, form):
|
||||||
model = models.EventProposal
|
# set camp and user for this eventproposal
|
||||||
fields = ['title', 'abstract', 'event_type', 'speakers', 'allow_video_recording', 'submission_notes']
|
eventproposal = form.save(commit=False)
|
||||||
template_name = 'eventproposal_form.html'
|
eventproposal.user = self.request.user
|
||||||
|
eventproposal.event_type = self.event_type
|
||||||
|
eventproposal.save()
|
||||||
|
|
||||||
|
# add the speakerproposal to the eventproposal
|
||||||
|
eventproposal.speakers.add(self.speakerproposal)
|
||||||
|
|
||||||
|
# send mail to content team
|
||||||
|
if not add_new_eventproposal_email(eventproposal):
|
||||||
|
logger.error("Unable to send email to content team after new eventproposal")
|
||||||
|
|
||||||
|
# all good
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
|
class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, UpdateView):
|
||||||
|
model = models.EventProposal
|
||||||
|
template_name = 'eventproposal_form.html'
|
||||||
|
form_class = EventProposalForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
"""
|
||||||
|
Set camp and eventtype for the form
|
||||||
|
"""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs.update({
|
||||||
|
'camp': self.camp,
|
||||||
|
'eventtype': self.get_object().event_type
|
||||||
|
})
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
""" Make speakerproposal and eventtype objects available in the template """
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['form'].fields['speakers'].queryset = models.SpeakerProposal.objects.filter(camp=self.camp, user=self.request.user)
|
context['event_type'] = self.get_object().event_type
|
||||||
context['form'].fields['event_type'].queryset = models.EventType.objects.filter(public=True)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_PENDING:
|
# set status to pending and save eventproposal
|
||||||
messages.warning(self.request, "Your event proposal has been reverted to status draft. Please submit it again when you are ready.")
|
form.instance.proposal_status = models.EventProposal.PROPOSAL_PENDING
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_DRAFT
|
eventproposal = form.save()
|
||||||
|
|
||||||
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED:
|
# send email to content team
|
||||||
messages.warning(self.request, "Your event proposal has been set to status modified after approval. Please await approval of the changes.")
|
if not add_eventproposal_updated_email(eventproposal):
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_MODIFIED_AFTER_APPROVAL
|
logger.error("Unable to send email to content team after eventproposal update")
|
||||||
if not add_eventproposal_updated_email(form.instance):
|
|
||||||
logger.error(
|
|
||||||
'Unable to add update email to queue for event: {}'.format(form.instance)
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
# message for the user and redirect
|
||||||
|
messages.info(self.request, "Your proposal is now pending approval by the content team.")
|
||||||
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
|
|
||||||
class EventProposalSubmitView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureUnapprovedProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
class EventProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DeleteView):
|
||||||
model = models.EventProposal
|
model = models.EventProposal
|
||||||
fields = []
|
template_name = 'proposal_delete.html'
|
||||||
template_name = 'eventproposal_submit.html'
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
messages.success(self.request, "Proposal '%s' has been deleted." % self.object.title)
|
||||||
|
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
def form_valid(self, form):
|
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_PENDING
|
|
||||||
messages.info(self.request, "Your proposal has been submitted and is now pending approval")
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, DetailView):
|
class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, DetailView):
|
||||||
model = models.EventProposal
|
model = models.EventProposal
|
||||||
template_name = 'eventproposal_detail.html'
|
template_name = 'eventproposal_detail.html'
|
||||||
|
|
||||||
|
###################################################################################################
|
||||||
# speakers
|
# combined proposal views
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(require_safe, name='dispatch')
|
class CombinedProposalTypeSelectView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||||
class SpeakerPictureView(CampViewMixin, PictureViewMixin, DetailView):
|
"""
|
||||||
model = models.Speaker
|
A view which allows the user to select event type without anything else on the page
|
||||||
|
"""
|
||||||
|
model = models.EventType
|
||||||
|
template_name = 'event_type_select.html'
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
""" We only allow submissions of events with EventTypes where public=True """
|
||||||
|
return super().get_queryset().filter(public=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CombinedProposalPersonSelectView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||||
|
"""
|
||||||
|
A view which allows the user to 1) choose between existing SpeakerProposals or
|
||||||
|
2) pressing a button to create a new SpeakerProposal.
|
||||||
|
Redirect straight to 2) if no existing SpeakerProposals exist.
|
||||||
|
"""
|
||||||
|
model = models.SpeakerProposal
|
||||||
|
template_name = 'combined_proposal_select_person.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Check that we have a valid EventType
|
||||||
|
"""
|
||||||
|
# get EventType from url kwargs
|
||||||
|
self.eventtype = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug'])
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
# only show speaker proposals for the current user
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Add EventType to template context
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['eventtype'] = self.eventtype
|
||||||
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# get and return the response
|
""" If we don't have any existing SpeakerProposals just redirect directly to the combined submit view """
|
||||||
response = self.get_picture_response(path='/public/speakers/%(campslug)s/%(slug)s/%(filename)s' % {
|
if not self.get_queryset().exists():
|
||||||
'campslug': self.camp.slug,
|
return redirect(reverse_lazy('program:proposal_combined_submit', kwargs={'camp_slug': self.camp.slug, 'event_type_slug': self.eventtype.slug}))
|
||||||
'slug': self.get_object().slug,
|
return super().get(request, *args, **kwargs)
|
||||||
'filename': os.path.basename(self.picture.name),
|
|
||||||
|
|
||||||
|
class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView):
|
||||||
|
"""
|
||||||
|
This view is used by users to submit CFP proposals.
|
||||||
|
It allows the user to submit an EventProposal and a SpeakerProposal together.
|
||||||
|
It can also be used with a preselected SpeakerProposal uuid in url kwargs
|
||||||
|
"""
|
||||||
|
template_name = 'combined_proposal_submit.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Check that we have a valid EventType
|
||||||
|
"""
|
||||||
|
# get EventType from url kwargs
|
||||||
|
self.eventtype = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug'])
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Add EventType to template context
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['eventtype'] = self.eventtype
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""
|
||||||
|
Save the object(s) here before redirecting
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'speakerproposal'):
|
||||||
|
eventproposal = form.save(commit=False)
|
||||||
|
eventproposal.user = self.request.user
|
||||||
|
eventproposal.event_type = self.eventtype
|
||||||
|
eventproposal.save()
|
||||||
|
eventproposal.speakers.add(self.speakerproposal)
|
||||||
|
else:
|
||||||
|
# first save the SpeakerProposal
|
||||||
|
speakerproposal = form['speakerproposal'].save(commit=False)
|
||||||
|
speakerproposal.camp = self.camp
|
||||||
|
speakerproposal.user = self.request.user
|
||||||
|
speakerproposal.save()
|
||||||
|
|
||||||
|
# then save the eventproposal
|
||||||
|
eventproposal = form['eventproposal'].save(commit=False)
|
||||||
|
eventproposal.user = self.request.user
|
||||||
|
eventproposal.event_type = self.eventtype
|
||||||
|
eventproposal.save()
|
||||||
|
|
||||||
|
# add the speakerproposal to the eventproposal
|
||||||
|
eventproposal.speakers.add(speakerproposal)
|
||||||
|
|
||||||
|
# send mail(s) to content team
|
||||||
|
if not add_new_eventproposal_email(eventproposal):
|
||||||
|
logger.error("Unable to send email to content team after new eventproposal")
|
||||||
|
if not hasattr(self, 'speakerproposal'):
|
||||||
|
if not add_new_speakerproposal_email(speakerproposal):
|
||||||
|
logger.error("Unable to send email to content team after new speakerproposal")
|
||||||
|
|
||||||
|
# all good
|
||||||
|
return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
"""
|
||||||
|
Unless we have an existing SpeakerProposal we must show two forms on the page.
|
||||||
|
We use betterforms.MultiModelForm to combine two forms on the page
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'speakerproposal'):
|
||||||
|
# we already have a speakerproposal, just show an eventproposal form
|
||||||
|
return EventProposalForm
|
||||||
|
|
||||||
|
# build our MultiModelForm
|
||||||
|
class CombinedProposalSubmitForm(MultiModelForm):
|
||||||
|
form_classes = OrderedDict((
|
||||||
|
('speakerproposal', SpeakerProposalForm),
|
||||||
|
('eventproposal', EventProposalForm),
|
||||||
|
))
|
||||||
|
|
||||||
|
# return the form class
|
||||||
|
return CombinedProposalSubmitForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
"""
|
||||||
|
Set camp and eventtype for the form
|
||||||
|
"""
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs.update({
|
||||||
|
'camp': self.camp,
|
||||||
|
'eventtype': self.eventtype
|
||||||
})
|
})
|
||||||
return response
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################################
|
||||||
|
# speaker views
|
||||||
|
|
||||||
|
|
||||||
class SpeakerDetailView(CampViewMixin, DetailView):
|
class SpeakerDetailView(CampViewMixin, DetailView):
|
||||||
|
@ -266,7 +640,8 @@ class SpeakerListView(CampViewMixin, ListView):
|
||||||
template_name = 'speaker_list.html'
|
template_name = 'speaker_list.html'
|
||||||
|
|
||||||
|
|
||||||
# events
|
###################################################################################################
|
||||||
|
# event views
|
||||||
|
|
||||||
|
|
||||||
class EventListView(CampViewMixin, ListView):
|
class EventListView(CampViewMixin, ListView):
|
||||||
|
@ -279,7 +654,8 @@ class EventDetailView(CampViewMixin, DetailView):
|
||||||
template_name = 'schedule_event_detail.html'
|
template_name = 'schedule_event_detail.html'
|
||||||
|
|
||||||
|
|
||||||
# schedule
|
###################################################################################################
|
||||||
|
# schedule views
|
||||||
|
|
||||||
|
|
||||||
class NoScriptScheduleView(CampViewMixin, TemplateView):
|
class NoScriptScheduleView(CampViewMixin, TemplateView):
|
||||||
|
@ -287,7 +663,9 @@ class NoScriptScheduleView(CampViewMixin, TemplateView):
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['eventinstances'] = models.EventInstance.objects.filter(event__camp=self.camp).order_by('when')
|
context['eventinstances'] = models.EventInstance.objects.filter(
|
||||||
|
event__track__camp=self.camp
|
||||||
|
).order_by('when')
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -300,12 +678,12 @@ class ScheduleView(CampViewMixin, TemplateView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CallForSpeakersView(CampViewMixin, TemplateView):
|
class CallForParticipationView(CampViewMixin, TemplateView):
|
||||||
def get_template_names(self):
|
template_name = 'call_for_participation.html'
|
||||||
return '%s_call_for_speakers.html' % self.camp.slug
|
|
||||||
|
|
||||||
|
|
||||||
# control center
|
###################################################################################################
|
||||||
|
# control center csv
|
||||||
|
|
||||||
|
|
||||||
class ProgramControlCenter(CampViewMixin, TemplateView):
|
class ProgramControlCenter(CampViewMixin, TemplateView):
|
||||||
|
@ -328,3 +706,41 @@ class ProgramControlCenter(CampViewMixin, TemplateView):
|
||||||
context['csv'] = csv
|
context['csv'] = csv
|
||||||
|
|
||||||
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'
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue