Initial work on the schedule written in elm.
This commit is contained in:
parent
477b1b85de
commit
a6470d2ec9
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,4 +6,4 @@ db.sqlite3
|
||||||
*.pyc
|
*.pyc
|
||||||
venv/
|
venv/
|
||||||
environment_settings.py
|
environment_settings.py
|
||||||
|
elm-stuff/
|
||||||
|
|
355
schedule/Main.elm
Normal file
355
schedule/Main.elm
Normal file
|
@ -0,0 +1,355 @@
|
||||||
|
module Main exposing (..)
|
||||||
|
|
||||||
|
import Html exposing (Html, Attribute, div, input, text, li, ul, a, h4, label, i, span)
|
||||||
|
import Html.Attributes exposing (class, classList, id, type_, for)
|
||||||
|
import Html.Events exposing (onClick)
|
||||||
|
import WebSocket exposing (listen)
|
||||||
|
import Json.Decode exposing (int, string, float, list, bool, Decoder)
|
||||||
|
import Json.Encode
|
||||||
|
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
||||||
|
|
||||||
|
|
||||||
|
main : Program Flags Model Msg
|
||||||
|
main =
|
||||||
|
Html.programWithFlags
|
||||||
|
{ init = init
|
||||||
|
, view = view
|
||||||
|
, update = update
|
||||||
|
, subscriptions = subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
scheduleServer : String
|
||||||
|
scheduleServer =
|
||||||
|
"ws://localhost:8000/schedule/"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- MODEL
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ days : List Day
|
||||||
|
, eventInstances : List EventInstance
|
||||||
|
, eventLocations : List EventLocation
|
||||||
|
, eventTypes : List EventType
|
||||||
|
, flags : Flags
|
||||||
|
, activeDay : Day
|
||||||
|
, filter : Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Filter =
|
||||||
|
{ eventTypes : List EventType
|
||||||
|
, eventLocations : List EventLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Day =
|
||||||
|
{ day_name : String
|
||||||
|
, iso : String
|
||||||
|
, repr : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Speaker =
|
||||||
|
{ name : String
|
||||||
|
, url : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias EventInstance =
|
||||||
|
{ title : String
|
||||||
|
, id : Int
|
||||||
|
, url : String
|
||||||
|
, abstract : String
|
||||||
|
, eventSlug : String
|
||||||
|
, eventType : String
|
||||||
|
, backgroundColor : String
|
||||||
|
, forgroundColor : String
|
||||||
|
, from : String
|
||||||
|
, to : String
|
||||||
|
, timeslots : Float
|
||||||
|
, location : String
|
||||||
|
, locationIcon : String
|
||||||
|
, speakers : List Speaker
|
||||||
|
, videoRecording : Bool
|
||||||
|
, videoUrl : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias EventLocation =
|
||||||
|
{ name : String
|
||||||
|
, slug : String
|
||||||
|
, icon : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias EventType =
|
||||||
|
{ name : String
|
||||||
|
, slug : String
|
||||||
|
, color : String
|
||||||
|
, lightText : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Flags =
|
||||||
|
{ schedule_timeslot_length_minutes : Int
|
||||||
|
, schedule_midnight_offset_hours : Int
|
||||||
|
, ics_button_href : String
|
||||||
|
, camp_slug : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
allDaysDay =
|
||||||
|
Day "All Days" "" ""
|
||||||
|
|
||||||
|
|
||||||
|
init : Flags -> ( Model, Cmd Msg )
|
||||||
|
init flags =
|
||||||
|
( Model [] [] [] [] flags allDaysDay (Filter [] []), sendInitMessage flags.camp_slug )
|
||||||
|
|
||||||
|
|
||||||
|
sendInitMessage : String -> Cmd Msg
|
||||||
|
sendInitMessage camp_slug =
|
||||||
|
WebSocket.send scheduleServer
|
||||||
|
(Json.Encode.encode 0
|
||||||
|
(Json.Encode.object
|
||||||
|
[ ( "action", Json.Encode.string "init" )
|
||||||
|
, ( "camp_slug", Json.Encode.string camp_slug )
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- UPDATE
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= NoOp
|
||||||
|
| WebSocketPayload String
|
||||||
|
| MakeActiveday Day
|
||||||
|
| ToggleEventTypeFilter EventType
|
||||||
|
| ToggleEventLocationFilter EventLocation
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
NoOp ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
WebSocketPayload str ->
|
||||||
|
let
|
||||||
|
newModel =
|
||||||
|
case Json.Decode.decodeString initDataDecoder str of
|
||||||
|
Ok m ->
|
||||||
|
m model.flags allDaysDay (Filter [] [])
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
model
|
||||||
|
in
|
||||||
|
newModel ! []
|
||||||
|
|
||||||
|
MakeActiveday day ->
|
||||||
|
{ model | activeDay = day } ! []
|
||||||
|
|
||||||
|
ToggleEventTypeFilter eventType ->
|
||||||
|
let
|
||||||
|
eventTypesFilter =
|
||||||
|
if List.member eventType model.filter.eventTypes then
|
||||||
|
List.filter (\x -> x /= eventType) model.filter.eventTypes
|
||||||
|
else
|
||||||
|
eventType :: model.filter.eventTypes
|
||||||
|
|
||||||
|
currentFilter =
|
||||||
|
model.filter
|
||||||
|
|
||||||
|
newFilter =
|
||||||
|
{ currentFilter | eventTypes = eventTypesFilter }
|
||||||
|
in
|
||||||
|
{ model | filter = newFilter } ! []
|
||||||
|
|
||||||
|
ToggleEventLocationFilter eventLocation ->
|
||||||
|
let
|
||||||
|
eventLocationsFilter =
|
||||||
|
if List.member eventLocation model.filter.eventLocations then
|
||||||
|
List.filter (\x -> x /= eventLocation) model.filter.eventLocations
|
||||||
|
else
|
||||||
|
eventLocation :: model.filter.eventLocations
|
||||||
|
|
||||||
|
currentFilter =
|
||||||
|
model.filter
|
||||||
|
|
||||||
|
newFilter =
|
||||||
|
{ currentFilter | eventLocations = eventLocationsFilter }
|
||||||
|
in
|
||||||
|
{ model | filter = newFilter } ! []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- SUBSCRIPTIONS
|
||||||
|
|
||||||
|
|
||||||
|
subscriptions : Model -> Sub Msg
|
||||||
|
subscriptions model =
|
||||||
|
WebSocket.listen scheduleServer WebSocketPayload
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- DECODERS
|
||||||
|
|
||||||
|
|
||||||
|
dayDecoder : Decoder Day
|
||||||
|
dayDecoder =
|
||||||
|
decode Day
|
||||||
|
|> required "day_name" string
|
||||||
|
|> required "iso" string
|
||||||
|
|> required "repr" string
|
||||||
|
|
||||||
|
|
||||||
|
speakerDecoder : Decoder Speaker
|
||||||
|
speakerDecoder =
|
||||||
|
decode Speaker
|
||||||
|
|> required "name" string
|
||||||
|
|> required "url" string
|
||||||
|
|
||||||
|
|
||||||
|
eventInstanceDecoder : Decoder EventInstance
|
||||||
|
eventInstanceDecoder =
|
||||||
|
decode EventInstance
|
||||||
|
|> required "title" string
|
||||||
|
|> required "id" int
|
||||||
|
|> required "url" string
|
||||||
|
|> required "abstract" string
|
||||||
|
|> required "event_slug" string
|
||||||
|
|> required "event_type" string
|
||||||
|
|> required "bg-color" string
|
||||||
|
|> required "fg-color" string
|
||||||
|
|> required "from" string
|
||||||
|
|> required "to" string
|
||||||
|
|> required "timeslots" float
|
||||||
|
|> required "location" string
|
||||||
|
|> required "location_icon" string
|
||||||
|
|> required "speakers" (list speakerDecoder)
|
||||||
|
|> required "video_recording" bool
|
||||||
|
|> optional "video_url" string ""
|
||||||
|
|
||||||
|
|
||||||
|
eventLocationDecoder : Decoder EventLocation
|
||||||
|
eventLocationDecoder =
|
||||||
|
decode EventLocation
|
||||||
|
|> required "name" string
|
||||||
|
|> required "slug" string
|
||||||
|
|> required "icon" string
|
||||||
|
|
||||||
|
|
||||||
|
eventTypeDecoder : Decoder EventType
|
||||||
|
eventTypeDecoder =
|
||||||
|
decode EventType
|
||||||
|
|> required "name" string
|
||||||
|
|> required "slug" string
|
||||||
|
|> required "color" string
|
||||||
|
|> required "light_text" bool
|
||||||
|
|
||||||
|
|
||||||
|
initDataDecoder : Decoder (Flags -> Day -> Filter -> Model)
|
||||||
|
initDataDecoder =
|
||||||
|
decode Model
|
||||||
|
|> required "days" (list dayDecoder)
|
||||||
|
|> required "event_instances" (list eventInstanceDecoder)
|
||||||
|
|> required "event_locations" (list eventLocationDecoder)
|
||||||
|
|> required "event_types" (list eventTypeDecoder)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
|
dayButton : Day -> Day -> Html Msg
|
||||||
|
dayButton day activeDay =
|
||||||
|
a
|
||||||
|
[ classList
|
||||||
|
[ ( "btn", True )
|
||||||
|
, ( "btn-default", day /= activeDay )
|
||||||
|
, ( "btn-primary", day == activeDay )
|
||||||
|
]
|
||||||
|
, onClick (MakeActiveday day)
|
||||||
|
]
|
||||||
|
[ text day.day_name
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
view : Model -> Html Msg
|
||||||
|
view model =
|
||||||
|
div []
|
||||||
|
[ div [ class "row" ]
|
||||||
|
[ div [ id "schedule-days", class "btn-group" ]
|
||||||
|
(List.map (\day -> dayButton day model.activeDay) (allDaysDay :: model.days))
|
||||||
|
]
|
||||||
|
, div [ class "row" ]
|
||||||
|
[ div
|
||||||
|
[ classList
|
||||||
|
[ ( "col-sm-3", True )
|
||||||
|
, ( "col-sm-9", True )
|
||||||
|
, ( "schedule-filter", True )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ h4 [] [ text "Filter" ]
|
||||||
|
, div [ class "form-group" ]
|
||||||
|
[ filterView "Type" model.eventTypes model.filter.eventTypes ToggleEventTypeFilter
|
||||||
|
, filterView "Location" model.eventLocations model.filter.eventLocations ToggleEventLocationFilter
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
filterView :
|
||||||
|
String
|
||||||
|
-> List { a | name : String }
|
||||||
|
-> List { a | name : String }
|
||||||
|
-> ({ a | name : String } -> Msg)
|
||||||
|
-> Html Msg
|
||||||
|
filterView name possibleFilters currentFilters action =
|
||||||
|
div []
|
||||||
|
[ text (name ++ ":")
|
||||||
|
, ul [] (List.map (\filter -> filterChoiceView filter currentFilters action) possibleFilters)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
filterChoiceView :
|
||||||
|
{ a | name : String }
|
||||||
|
-> List { a | name : String }
|
||||||
|
-> ({ a | name : String } -> Msg)
|
||||||
|
-> Html Msg
|
||||||
|
filterChoiceView filter currentFilters action =
|
||||||
|
let
|
||||||
|
active =
|
||||||
|
List.member filter currentFilters
|
||||||
|
|
||||||
|
notActive =
|
||||||
|
not active
|
||||||
|
in
|
||||||
|
li []
|
||||||
|
[ div
|
||||||
|
[ classList
|
||||||
|
[ ( "btn", True )
|
||||||
|
, ( "btn-default", True )
|
||||||
|
, ( "filter-choice-active", active )
|
||||||
|
]
|
||||||
|
, onClick (action filter)
|
||||||
|
]
|
||||||
|
[ span []
|
||||||
|
[ i [ classList [ ( "fa", True ), ( "fa-minus", active ), ( "fa-plus", notActive ) ] ] []
|
||||||
|
, text (" " ++ filter.name)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
locationFilter : List EventLocation -> Html Msg
|
||||||
|
locationFilter eventLocations =
|
||||||
|
div [] [ text "Location:" ]
|
2
schedule/Makefile
Normal file
2
schedule/Makefile
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
all:
|
||||||
|
elm-make Main.elm --debug --output ../src/program/static/js/elm_based_schedule.js
|
17
schedule/elm-package.json
Normal file
17
schedule/elm-package.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"summary": "helpful summary of your project, less than 80 characters",
|
||||||
|
"repository": "https://github.com/user/project.git",
|
||||||
|
"license": "BSD3",
|
||||||
|
"source-directories": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"exposed-modules": [],
|
||||||
|
"dependencies": {
|
||||||
|
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
|
||||||
|
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
||||||
|
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
||||||
|
"elm-lang/websocket": "1.0.2 <= v < 2.0.0"
|
||||||
|
},
|
||||||
|
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
from channels.generic.websockets import JsonWebsocketConsumer
|
from channels.generic.websockets import JsonWebsocketConsumer
|
||||||
|
|
||||||
from camps.models import Camp
|
from camps.models import Camp
|
||||||
from .models import EventInstance, Favorite
|
from .models import EventInstance, Favorite, EventLocation, EventType
|
||||||
|
|
||||||
|
|
||||||
class ScheduleConsumer(JsonWebsocketConsumer):
|
class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
|
@ -10,8 +10,13 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
def connection_groups(self, **kwargs):
|
def connection_groups(self, **kwargs):
|
||||||
return ['schedule_users']
|
return ['schedule_users']
|
||||||
|
|
||||||
def connect(self, message, **kwargs):
|
def raw_receive(self, message, **kwargs):
|
||||||
camp_slug = message.http_session['campslug']
|
content = self.decode_json(message['text'])
|
||||||
|
action = content.get('action')
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if action == 'init':
|
||||||
|
camp_slug = content.get('camp_slug')
|
||||||
try:
|
try:
|
||||||
camp = Camp.objects.get(slug=camp_slug)
|
camp = Camp.objects.get(slug=camp_slug)
|
||||||
days = list(map(
|
days = list(map(
|
||||||
|
@ -23,21 +28,25 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
camp.get_days('camp')
|
camp.get_days('camp')
|
||||||
))
|
))
|
||||||
event_instances_query_set = EventInstance.objects.filter(event__camp=camp)
|
event_instances_query_set = EventInstance.objects.filter(event__camp=camp)
|
||||||
event_instances = list(map(lambda x: x.to_json(user=message.user), event_instances_query_set))
|
event_instances = list([x.to_json(user=message.user) for x in event_instances_query_set])
|
||||||
self.send({
|
|
||||||
|
event_locations_query_set = EventLocation.objects.filter(camp=camp)
|
||||||
|
event_locations = list([x.to_json() for x in event_locations_query_set])
|
||||||
|
|
||||||
|
event_types_query_set = EventType.objects.filter()
|
||||||
|
event_types = list([x.to_json() for x in event_types_query_set])
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"event_locations": event_locations,
|
||||||
|
"event_types": event_types,
|
||||||
"accept": True,
|
"accept": True,
|
||||||
"event_instances": event_instances,
|
"event_instances": event_instances,
|
||||||
"days": days,
|
"days": days,
|
||||||
"action": "init"
|
"action": "init"
|
||||||
})
|
}
|
||||||
except Camp.DoesNotExist:
|
except Camp.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def raw_receive(self, message, **kwargs):
|
|
||||||
content = self.decode_json(message['text'])
|
|
||||||
action = content.get('action')
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
if action == 'favorite':
|
if action == 'favorite':
|
||||||
event_instance_id = content.get('event_instance_id')
|
event_instance_id = content.get('event_instance_id')
|
||||||
event_instance = EventInstance.objects.get(id=event_instance_id)
|
event_instance = EventInstance.objects.get(id=event_instance_id)
|
||||||
|
@ -52,6 +61,7 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
||||||
favorite = Favorite.objects.get(event_instance=event_instance, user=message.user)
|
favorite = Favorite.objects.get(event_instance=event_instance, user=message.user)
|
||||||
favorite.delete()
|
favorite.delete()
|
||||||
|
|
||||||
|
if data:
|
||||||
self.send(data)
|
self.send(data)
|
||||||
|
|
||||||
def disconnect(self, message, **kwargs):
|
def disconnect(self, message, **kwargs):
|
||||||
|
|
|
@ -311,6 +311,13 @@ class EventLocation(CampRelatedModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (('camp', 'slug'), ('camp', 'name'))
|
unique_together = (('camp', 'slug'), ('camp', 'name'))
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"slug": self.slug,
|
||||||
|
"icon": self.icon,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventType(CreatedUpdatedModel):
|
class EventType(CreatedUpdatedModel):
|
||||||
""" Every event needs to have a type. """
|
""" Every event needs to have a type. """
|
||||||
|
@ -350,6 +357,14 @@ class EventType(CreatedUpdatedModel):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"slug": self.slug,
|
||||||
|
"color": self.color,
|
||||||
|
"light_text": self.light_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Event(CampRelatedModel):
|
class Event(CampRelatedModel):
|
||||||
""" Something that is on the program one or more times. """
|
""" Something that is on the program one or more times. """
|
||||||
|
|
|
@ -264,10 +264,7 @@ class EventDetailView(CampViewMixin, DetailView):
|
||||||
|
|
||||||
|
|
||||||
class ScheduleView(CampViewMixin, TemplateView):
|
class ScheduleView(CampViewMixin, TemplateView):
|
||||||
def get_template_names(self):
|
template_name = 'schedule_overview_elm.html'
|
||||||
if 'day' in self.kwargs:
|
|
||||||
return 'schedule_day.html'
|
|
||||||
return 'schedule_overview.html'
|
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super(ScheduleView, self).get_context_data(**kwargs)
|
context = super(ScheduleView, self).get_context_data(**kwargs)
|
||||||
|
|
|
@ -208,6 +208,11 @@ footer {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schedule-filter .btn {
|
||||||
|
min-width: 200px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 520px) {
|
@media (min-width: 520px) {
|
||||||
.schedule-filter {
|
.schedule-filter {
|
||||||
|
@ -231,28 +236,7 @@ footer {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group .schedule-checkbox {
|
.filter-choice-active {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="checkbox"] + .btn-group > label span {
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="checkbox"] + .btn-group > label span i:first-child {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.form-group input[type="checkbox"] + .btn-group > label span i:last-child {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="checkbox"]:checked + .btn-group > label span i:first-child {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.form-group input[type="checkbox"]:checked + .btn-group > label span i:last-child {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.form-group input[type="checkbox"]:checked + .btn-group > label {
|
|
||||||
color: #333;
|
color: #333;
|
||||||
background-color: #e6e6e6;
|
background-color: #e6e6e6;
|
||||||
border-color: #adadad;
|
border-color: #adadad;
|
||||||
|
|
Loading…
Reference in a new issue