Organized the code in a more sane manner. Also some websocket work.
This commit is contained in:
parent
a5ef793dcf
commit
e8c0ab1941
|
@ -1,523 +0,0 @@
|
|||
module Main exposing (..)
|
||||
|
||||
import Html exposing (Html, Attribute, div, input, text, li, ul, a, h4, label, i, span, hr, small, p)
|
||||
import Html.Attributes exposing (class, classList, id, type_, for, style, href)
|
||||
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)
|
||||
import Markdown
|
||||
import Navigation exposing (Location)
|
||||
import UrlParser exposing ((</>))
|
||||
|
||||
|
||||
main : Program Flags Model Msg
|
||||
main =
|
||||
Navigation.programWithFlags
|
||||
OnLocationChange
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
scheduleServer : String
|
||||
scheduleServer =
|
||||
"ws://localhost:8000/schedule/"
|
||||
|
||||
|
||||
|
||||
-- ROUTING
|
||||
|
||||
|
||||
type Route
|
||||
= OverviewRoute
|
||||
| EventInstanceRoute EventInstanceSlug
|
||||
| NotFoundRoute
|
||||
|
||||
|
||||
matchers : UrlParser.Parser (Route -> a) a
|
||||
matchers =
|
||||
UrlParser.oneOf
|
||||
[ UrlParser.map OverviewRoute UrlParser.top
|
||||
, UrlParser.map EventInstanceRoute (UrlParser.s "event" </> UrlParser.string)
|
||||
]
|
||||
|
||||
|
||||
parseLocation : Location -> Route
|
||||
parseLocation location =
|
||||
case UrlParser.parseHash matchers location of
|
||||
Just route ->
|
||||
route
|
||||
|
||||
Nothing ->
|
||||
NotFoundRoute
|
||||
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ days : List Day
|
||||
, eventInstances : List EventInstance
|
||||
, eventLocations : List EventLocation
|
||||
, eventTypes : List EventType
|
||||
, flags : Flags
|
||||
, activeDay : Day
|
||||
, filter : Filter
|
||||
, route : Route
|
||||
}
|
||||
|
||||
|
||||
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 EventInstanceSlug =
|
||||
String
|
||||
|
||||
|
||||
type alias EventInstance =
|
||||
{ title : String
|
||||
, id : Int
|
||||
, url : String
|
||||
, abstract : String
|
||||
, eventSlug : EventInstanceSlug
|
||||
, eventType : String
|
||||
, backgroundColor : String
|
||||
, forgroundColor : String
|
||||
, from : String
|
||||
, to : String
|
||||
, timeslots : Float
|
||||
, location : String
|
||||
, locationIcon : String
|
||||
, speakers : List Speaker
|
||||
, videoRecording : Bool
|
||||
, videoUrl : String
|
||||
}
|
||||
|
||||
|
||||
emptyEventInstance : EventInstance
|
||||
emptyEventInstance =
|
||||
{ title = "This should not happen!"
|
||||
, id = 0
|
||||
, url = ""
|
||||
, abstract = ""
|
||||
, eventSlug = ""
|
||||
, eventType = ""
|
||||
, backgroundColor = ""
|
||||
, forgroundColor = ""
|
||||
, from = ""
|
||||
, to = ""
|
||||
, timeslots = 0.0
|
||||
, location = ""
|
||||
, locationIcon = ""
|
||||
, speakers = []
|
||||
, videoRecording = False
|
||||
, videoUrl = ""
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
allDaysDay =
|
||||
Day "All Days" "" ""
|
||||
|
||||
|
||||
init : Flags -> Location -> ( Model, Cmd Msg )
|
||||
init flags location =
|
||||
( Model [] [] [] [] flags allDaysDay (Filter [] []) (parseLocation location), 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
|
||||
| OnLocationChange Location
|
||||
|
||||
|
||||
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 [] []) model.route
|
||||
|
||||
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 } ! []
|
||||
|
||||
OnLocationChange location ->
|
||||
let
|
||||
newRoute =
|
||||
parseLocation location
|
||||
in
|
||||
{ model | route = newRoute } ! []
|
||||
|
||||
|
||||
|
||||
-- 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 -> Route -> 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))
|
||||
]
|
||||
, hr [] []
|
||||
, case model.route of
|
||||
OverviewRoute ->
|
||||
scheduleOverviewView model
|
||||
|
||||
EventInstanceRoute eventInstanceId ->
|
||||
eventInstanceDetailView eventInstanceId model.eventInstances
|
||||
|
||||
NotFoundRoute ->
|
||||
div [] [ text "Not found!" ]
|
||||
]
|
||||
|
||||
|
||||
eventInstanceDetailView : EventInstanceSlug -> List EventInstance -> Html Msg
|
||||
eventInstanceDetailView eventInstanceId eventInstances =
|
||||
let
|
||||
eventInstance =
|
||||
case List.head (List.filter (\e -> e.eventSlug == eventInstanceId) eventInstances) of
|
||||
Just eventInstance ->
|
||||
eventInstance
|
||||
|
||||
Nothing ->
|
||||
emptyEventInstance
|
||||
in
|
||||
div [ class "row" ]
|
||||
[ div [ class "col-sm-9" ]
|
||||
[ a [ href "#" ]
|
||||
[ text "Back"
|
||||
]
|
||||
, h4 [] [ text eventInstance.title ]
|
||||
, p [] [ Markdown.toHtml [] eventInstance.abstract ]
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "col-sm-3", True )
|
||||
, ( "schedule-sidebar", True )
|
||||
]
|
||||
]
|
||||
[ h4 [] [ text "Speakers" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
scheduleOverviewView : Model -> Html Msg
|
||||
scheduleOverviewView model =
|
||||
div [ class "row" ]
|
||||
[ div
|
||||
[ classList
|
||||
[ ( "col-sm-3", True )
|
||||
, ( "col-sm-push-9", True )
|
||||
, ( "schedule-sidebar", 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
|
||||
[ classList
|
||||
[ ( "col-sm-9", True )
|
||||
, ( "col-sm-pull-3", True )
|
||||
]
|
||||
]
|
||||
(List.map (\day -> dayRowView day model) model.days)
|
||||
]
|
||||
|
||||
|
||||
dayRowView : Day -> Model -> Html Msg
|
||||
dayRowView day model =
|
||||
let
|
||||
types =
|
||||
List.map (\eventType -> eventType.slug)
|
||||
(if List.isEmpty model.filter.eventTypes then
|
||||
model.eventTypes
|
||||
else
|
||||
model.filter.eventTypes
|
||||
)
|
||||
|
||||
locations =
|
||||
List.map (\eventLocation -> eventLocation.slug)
|
||||
(if List.isEmpty model.filter.eventLocations then
|
||||
model.eventLocations
|
||||
else
|
||||
model.filter.eventLocations
|
||||
)
|
||||
|
||||
filteredEventInstances =
|
||||
List.filter
|
||||
(\eventInstance ->
|
||||
((String.slice 0 10 eventInstance.from) == day.iso)
|
||||
&& List.member eventInstance.location locations
|
||||
&& List.member eventInstance.eventType types
|
||||
)
|
||||
model.eventInstances
|
||||
in
|
||||
div []
|
||||
[ h4 []
|
||||
[ text day.repr ]
|
||||
, div [ class "schedule-day-row" ]
|
||||
(List.map dayEventInstanceView filteredEventInstances)
|
||||
]
|
||||
|
||||
|
||||
dayEventInstanceView : EventInstance -> Html Msg
|
||||
dayEventInstanceView eventInstance =
|
||||
a
|
||||
[ class "event"
|
||||
, href ("#event/" ++ eventInstance.eventSlug)
|
||||
, style
|
||||
[ ( "background-color", eventInstance.backgroundColor )
|
||||
, ( "color", eventInstance.forgroundColor )
|
||||
]
|
||||
]
|
||||
[ small []
|
||||
[ text ((String.slice 11 16 eventInstance.from) ++ " - " ++ (String.slice 11 16 eventInstance.to)) ]
|
||||
, i [ classList [ ( "fa", True ), ( "fa-" ++ eventInstance.locationIcon, True ), ( "pull-right", True ) ] ] []
|
||||
, p
|
||||
[]
|
||||
[ text eventInstance.title ]
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
]
|
||||
]
|
|
@ -1,5 +1,5 @@
|
|||
all:
|
||||
elm-make Main.elm --warn --output ../src/program/static/js/elm_based_schedule.js
|
||||
elm-make src/Main.elm --warn --output ../src/program/static/js/elm_based_schedule.js
|
||||
|
||||
debug:
|
||||
elm-make Main.elm --debug --warn --output ../src/program/static/js/elm_based_schedule.js
|
||||
elm-make src/Main.elm --debug --warn --output ../src/program/static/js/elm_based_schedule.js
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"repository": "https://github.com/user/project.git",
|
||||
"license": "BSD3",
|
||||
"source-directories": [
|
||||
"."
|
||||
"src/"
|
||||
],
|
||||
"exposed-modules": [],
|
||||
"dependencies": {
|
||||
|
|
95
schedule/src/Decoders.elm
Normal file
95
schedule/src/Decoders.elm
Normal file
|
@ -0,0 +1,95 @@
|
|||
module Decoders exposing (..)
|
||||
|
||||
-- Local modules
|
||||
|
||||
import Models exposing (Day, Speaker, Event, EventInstance, EventLocation, EventType, Model, Flags, Filter, Route(..))
|
||||
|
||||
|
||||
-- Core modules
|
||||
|
||||
import Json.Decode exposing (int, string, float, list, bool, dict, Decoder)
|
||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
||||
|
||||
|
||||
-- DECODERS
|
||||
|
||||
|
||||
type alias WebSocketAction =
|
||||
{ action : String
|
||||
}
|
||||
|
||||
|
||||
webSocketActionDecoder : Decoder WebSocketAction
|
||||
webSocketActionDecoder =
|
||||
decode WebSocketAction
|
||||
|> required "action" string
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
eventDecoder : Decoder Event
|
||||
eventDecoder =
|
||||
decode Event
|
||||
|> required "title" string
|
||||
|> required "slug" string
|
||||
|> required "abstract" string
|
||||
|> required "speakers" (list speakerDecoder)
|
||||
|
||||
|
||||
eventInstanceDecoder : Decoder EventInstance
|
||||
eventInstanceDecoder =
|
||||
decode EventInstance
|
||||
|> required "title" string
|
||||
|> required "slug" string
|
||||
|> required "id" int
|
||||
|> required "url" 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 "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 -> Route -> Model)
|
||||
initDataDecoder =
|
||||
decode Model
|
||||
|> required "days" (list dayDecoder)
|
||||
|> required "event_instances" (list eventInstanceDecoder)
|
||||
|> required "event_locations" (list eventLocationDecoder)
|
||||
|> required "event_types" (list eventTypeDecoder)
|
||||
|> hardcoded []
|
41
schedule/src/Main.elm
Normal file
41
schedule/src/Main.elm
Normal file
|
@ -0,0 +1,41 @@
|
|||
module Main exposing (..)
|
||||
|
||||
-- Local modules
|
||||
|
||||
import Models exposing (..)
|
||||
import Routing exposing (parseLocation)
|
||||
import Update exposing (update)
|
||||
import Messages exposing (Msg(..))
|
||||
import WebSocketCalls exposing (scheduleServer, sendInitMessage)
|
||||
import Views exposing (view)
|
||||
|
||||
|
||||
-- External modules
|
||||
|
||||
import WebSocket exposing (listen)
|
||||
import Navigation exposing (Location)
|
||||
|
||||
|
||||
main : Program Flags Model Msg
|
||||
main =
|
||||
Navigation.programWithFlags
|
||||
OnLocationChange
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
init : Flags -> Location -> ( Model, Cmd Msg )
|
||||
init flags location =
|
||||
( Model [] [] [] [] [] flags allDaysDay (Filter [] []) (parseLocation location), sendInitMessage flags.camp_slug )
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
WebSocket.listen scheduleServer WebSocketPayload
|
19
schedule/src/Messages.elm
Normal file
19
schedule/src/Messages.elm
Normal file
|
@ -0,0 +1,19 @@
|
|||
module Messages exposing (Msg(..))
|
||||
|
||||
-- Local modules
|
||||
|
||||
import Models exposing (Day, EventType, EventLocation)
|
||||
|
||||
|
||||
-- External modules
|
||||
|
||||
import Navigation exposing (Location)
|
||||
|
||||
|
||||
type Msg
|
||||
= NoOp
|
||||
| WebSocketPayload String
|
||||
| MakeActiveday Day
|
||||
| ToggleEventTypeFilter EventType
|
||||
| ToggleEventLocationFilter EventLocation
|
||||
| OnLocationChange Location
|
126
schedule/src/Models.elm
Normal file
126
schedule/src/Models.elm
Normal file
|
@ -0,0 +1,126 @@
|
|||
module Models exposing (..)
|
||||
|
||||
|
||||
type Route
|
||||
= OverviewRoute
|
||||
| DayRoute DayIso
|
||||
| EventInstanceRoute EventInstanceSlug
|
||||
| NotFoundRoute
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ days : List Day
|
||||
, eventInstances : List EventInstance
|
||||
, eventLocations : List EventLocation
|
||||
, eventTypes : List EventType
|
||||
, events : List Event
|
||||
, flags : Flags
|
||||
, activeDay : Day
|
||||
, filter : Filter
|
||||
, route : Route
|
||||
}
|
||||
|
||||
|
||||
type alias Filter =
|
||||
{ eventTypes : List EventType
|
||||
, eventLocations : List EventLocation
|
||||
}
|
||||
|
||||
|
||||
type alias DayIso =
|
||||
String
|
||||
|
||||
|
||||
type alias Day =
|
||||
{ day_name : String
|
||||
, iso : DayIso
|
||||
, repr : String
|
||||
}
|
||||
|
||||
|
||||
type alias Speaker =
|
||||
{ name : String
|
||||
}
|
||||
|
||||
|
||||
type alias EventSlug =
|
||||
String
|
||||
|
||||
|
||||
type alias EventInstanceSlug =
|
||||
String
|
||||
|
||||
|
||||
type alias EventInstance =
|
||||
{ title : String
|
||||
, slug : EventInstanceSlug
|
||||
, id : Int
|
||||
, url : String
|
||||
, eventSlug : EventSlug
|
||||
, eventType : String
|
||||
, backgroundColor : String
|
||||
, forgroundColor : String
|
||||
, from : String
|
||||
, to : String
|
||||
, timeslots : Float
|
||||
, location : String
|
||||
, locationIcon : String
|
||||
, videoRecording : Bool
|
||||
, videoUrl : String
|
||||
}
|
||||
|
||||
|
||||
type alias Event =
|
||||
{ title : String
|
||||
, slug : EventSlug
|
||||
, abstract : String
|
||||
, speakers : List Speaker
|
||||
}
|
||||
|
||||
|
||||
emptyEventInstance : EventInstance
|
||||
emptyEventInstance =
|
||||
{ title = "This should not happen!"
|
||||
, slug = "this-should-not-happen"
|
||||
, id = 0
|
||||
, url = ""
|
||||
, eventSlug = ""
|
||||
, eventType = ""
|
||||
, backgroundColor = ""
|
||||
, forgroundColor = ""
|
||||
, from = ""
|
||||
, to = ""
|
||||
, timeslots = 0.0
|
||||
, location = ""
|
||||
, locationIcon = ""
|
||||
, videoRecording = False
|
||||
, videoUrl = ""
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
allDaysDay =
|
||||
Day "All Days" "" ""
|
30
schedule/src/Routing.elm
Normal file
30
schedule/src/Routing.elm
Normal file
|
@ -0,0 +1,30 @@
|
|||
module Routing exposing (..)
|
||||
|
||||
-- Local modules
|
||||
|
||||
import Models exposing (DayIso, EventInstanceSlug, Route(..))
|
||||
|
||||
|
||||
-- External modules
|
||||
|
||||
import Navigation exposing (Location)
|
||||
import UrlParser exposing ((</>))
|
||||
|
||||
|
||||
matchers : UrlParser.Parser (Route -> a) a
|
||||
matchers =
|
||||
UrlParser.oneOf
|
||||
[ UrlParser.map OverviewRoute UrlParser.top
|
||||
, UrlParser.map DayRoute (UrlParser.s "day" </> UrlParser.string)
|
||||
, UrlParser.map EventInstanceRoute (UrlParser.s "event" </> UrlParser.string)
|
||||
]
|
||||
|
||||
|
||||
parseLocation : Location -> Route
|
||||
parseLocation location =
|
||||
case UrlParser.parseHash matchers location of
|
||||
Just route ->
|
||||
route
|
||||
|
||||
Nothing ->
|
||||
NotFoundRoute
|
110
schedule/src/Update.elm
Normal file
110
schedule/src/Update.elm
Normal file
|
@ -0,0 +1,110 @@
|
|||
module Update exposing (update)
|
||||
|
||||
-- Local modules
|
||||
|
||||
import Models exposing (Model, Route(EventInstanceRoute), emptyEventInstance, allDaysDay, Filter)
|
||||
import Messages exposing (Msg(..))
|
||||
import Decoders exposing (webSocketActionDecoder, initDataDecoder, eventDecoder)
|
||||
import Routing exposing (parseLocation)
|
||||
import WebSocketCalls exposing (sendGetEventContent)
|
||||
|
||||
|
||||
-- Core modules
|
||||
|
||||
import Json.Decode
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
NoOp ->
|
||||
( model, Cmd.none )
|
||||
|
||||
WebSocketPayload str ->
|
||||
let
|
||||
newModel =
|
||||
case Json.Decode.decodeString webSocketActionDecoder str of
|
||||
Ok webSocketAction ->
|
||||
case webSocketAction.action of
|
||||
"init" ->
|
||||
case Json.Decode.decodeString initDataDecoder str of
|
||||
Ok m ->
|
||||
m model.flags allDaysDay (Filter [] []) model.route
|
||||
|
||||
Err error ->
|
||||
model
|
||||
|
||||
"get_event_content" ->
|
||||
case Json.Decode.decodeString eventDecoder str of
|
||||
Ok event ->
|
||||
{ model | events = event :: model.events }
|
||||
|
||||
Err error ->
|
||||
model
|
||||
|
||||
_ ->
|
||||
model
|
||||
|
||||
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 } ! []
|
||||
|
||||
OnLocationChange location ->
|
||||
let
|
||||
newRoute =
|
||||
parseLocation (Debug.log "location" location)
|
||||
|
||||
onLoadCmd =
|
||||
case newRoute of
|
||||
EventInstanceRoute eventInstanceSlug ->
|
||||
let
|
||||
eventInstance =
|
||||
case List.head (List.filter (\x -> x.slug == eventInstanceSlug) model.eventInstances) of
|
||||
Just eventInstance ->
|
||||
eventInstance
|
||||
|
||||
Nothing ->
|
||||
emptyEventInstance
|
||||
in
|
||||
sendGetEventContent model.flags.camp_slug eventInstance.eventSlug
|
||||
|
||||
_ ->
|
||||
Cmd.none
|
||||
in
|
||||
{ model | route = newRoute } ! [ onLoadCmd ]
|
241
schedule/src/Views.elm
Normal file
241
schedule/src/Views.elm
Normal file
|
@ -0,0 +1,241 @@
|
|||
module Views exposing (..)
|
||||
|
||||
-- Local modules
|
||||
|
||||
import Models exposing (..)
|
||||
import Messages exposing (Msg(..))
|
||||
|
||||
|
||||
-- Core modules
|
||||
|
||||
import Html exposing (Html, Attribute, div, input, text, li, ul, a, h4, label, i, span, hr, small, p)
|
||||
import Html.Attributes exposing (class, classList, id, type_, for, style, href)
|
||||
import Html.Events exposing (onClick)
|
||||
|
||||
|
||||
-- External modules
|
||||
|
||||
import Markdown
|
||||
|
||||
|
||||
dayButton : Day -> Day -> Html Msg
|
||||
dayButton day activeDay =
|
||||
a
|
||||
[ classList
|
||||
[ ( "btn", True )
|
||||
, ( "btn-default", day /= activeDay )
|
||||
, ( "btn-primary", day == activeDay )
|
||||
]
|
||||
, onClick (MakeActiveday day)
|
||||
, href
|
||||
("#"
|
||||
++ case day.iso of
|
||||
"" ->
|
||||
""
|
||||
|
||||
iso ->
|
||||
"day/" ++ iso
|
||||
)
|
||||
]
|
||||
[ 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))
|
||||
]
|
||||
, hr [] []
|
||||
, case model.route of
|
||||
OverviewRoute ->
|
||||
scheduleOverviewView model
|
||||
|
||||
DayRoute dayIso ->
|
||||
dayView dayIso model
|
||||
|
||||
EventInstanceRoute eventInstanceSlug ->
|
||||
eventInstanceDetailView eventInstanceSlug model
|
||||
|
||||
NotFoundRoute ->
|
||||
div [] [ text "Not found!" ]
|
||||
]
|
||||
|
||||
|
||||
dayView : DayIso -> Model -> Html Msg
|
||||
dayView dayIso model =
|
||||
div []
|
||||
[ filterSideBar model
|
||||
]
|
||||
|
||||
|
||||
eventInstanceDetailView : EventInstanceSlug -> Model -> Html Msg
|
||||
eventInstanceDetailView eventInstanceSlug model =
|
||||
let
|
||||
eventInstance =
|
||||
case List.head (List.filter (\e -> e.slug == eventInstanceSlug) model.eventInstances) of
|
||||
Just eventInstance ->
|
||||
eventInstance
|
||||
|
||||
Nothing ->
|
||||
emptyEventInstance
|
||||
|
||||
event =
|
||||
case List.head (List.filter (\e -> e.slug == eventInstance.eventSlug) model.events) of
|
||||
Just event ->
|
||||
event
|
||||
|
||||
Nothing ->
|
||||
{ title = "", slug = "", abstract = "", speakers = [] }
|
||||
in
|
||||
div [ class "row" ]
|
||||
[ div [ class "col-sm-9" ]
|
||||
[ a [ href "#" ]
|
||||
[ text "Back"
|
||||
]
|
||||
, h4 [] [ text eventInstance.title ]
|
||||
, p [] [ Markdown.toHtml [] event.abstract ]
|
||||
, hr [] []
|
||||
, h4 [] [ text "TODO: Show all instances here!" ]
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "col-sm-3", True )
|
||||
, ( "schedule-sidebar", True )
|
||||
]
|
||||
]
|
||||
[ h4 [] [ text "Speakers" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
filterSideBar : Model -> Html Msg
|
||||
filterSideBar model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "col-sm-3", True )
|
||||
, ( "col-sm-push-9", True )
|
||||
, ( "schedule-sidebar", 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
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
scheduleOverviewView : Model -> Html Msg
|
||||
scheduleOverviewView model =
|
||||
div [ class "row" ]
|
||||
[ filterSideBar model
|
||||
, div
|
||||
[ classList
|
||||
[ ( "col-sm-9", True )
|
||||
, ( "col-sm-pull-3", True )
|
||||
]
|
||||
]
|
||||
(List.map (\day -> dayRowView day model) model.days)
|
||||
]
|
||||
|
||||
|
||||
dayRowView : Day -> Model -> Html Msg
|
||||
dayRowView day model =
|
||||
let
|
||||
types =
|
||||
List.map (\eventType -> eventType.slug)
|
||||
(if List.isEmpty model.filter.eventTypes then
|
||||
model.eventTypes
|
||||
else
|
||||
model.filter.eventTypes
|
||||
)
|
||||
|
||||
locations =
|
||||
List.map (\eventLocation -> eventLocation.slug)
|
||||
(if List.isEmpty model.filter.eventLocations then
|
||||
model.eventLocations
|
||||
else
|
||||
model.filter.eventLocations
|
||||
)
|
||||
|
||||
filteredEventInstances =
|
||||
List.filter
|
||||
(\eventInstance ->
|
||||
((String.slice 0 10 eventInstance.from) == day.iso)
|
||||
&& List.member eventInstance.location locations
|
||||
&& List.member eventInstance.eventType types
|
||||
)
|
||||
model.eventInstances
|
||||
in
|
||||
div []
|
||||
[ h4 []
|
||||
[ text day.repr ]
|
||||
, div [ class "schedule-day-row" ]
|
||||
(List.map dayEventInstanceView filteredEventInstances)
|
||||
]
|
||||
|
||||
|
||||
dayEventInstanceView : EventInstance -> Html Msg
|
||||
dayEventInstanceView eventInstance =
|
||||
a
|
||||
[ class "event"
|
||||
, href ("#event/" ++ eventInstance.slug)
|
||||
, style
|
||||
[ ( "background-color", eventInstance.backgroundColor )
|
||||
, ( "color", eventInstance.forgroundColor )
|
||||
]
|
||||
]
|
||||
[ small []
|
||||
[ text ((String.slice 11 16 eventInstance.from) ++ " - " ++ (String.slice 11 16 eventInstance.to)) ]
|
||||
, i [ classList [ ( "fa", True ), ( "fa-" ++ eventInstance.locationIcon, True ), ( "pull-right", True ) ] ] []
|
||||
, p
|
||||
[]
|
||||
[ text eventInstance.title ]
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
]
|
||||
]
|
42
schedule/src/WebSocketCalls.elm
Normal file
42
schedule/src/WebSocketCalls.elm
Normal file
|
@ -0,0 +1,42 @@
|
|||
module WebSocketCalls exposing (scheduleServer, sendInitMessage, sendGetEventContent)
|
||||
|
||||
-- Internal modules
|
||||
|
||||
import Models exposing (EventSlug)
|
||||
import Messages exposing (Msg)
|
||||
|
||||
|
||||
-- External modules
|
||||
|
||||
import WebSocket
|
||||
import Json.Encode
|
||||
|
||||
|
||||
scheduleServer : String
|
||||
scheduleServer =
|
||||
"ws://localhost:8000/schedule/"
|
||||
|
||||
|
||||
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 )
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
sendGetEventContent : String -> EventSlug -> Cmd Msg
|
||||
sendGetEventContent campSlug eventSlug =
|
||||
WebSocket.send scheduleServer
|
||||
(Json.Encode.encode 0
|
||||
(Json.Encode.object
|
||||
[ ( "action", Json.Encode.string "get_event_content" )
|
||||
, ( "event_slug", Json.Encode.string eventSlug )
|
||||
, ( "camp_slug", Json.Encode.string campSlug )
|
||||
]
|
||||
)
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
from channels.generic.websockets import JsonWebsocketConsumer
|
||||
|
||||
from camps.models import Camp
|
||||
from .models import EventInstance, Favorite, EventLocation, EventType
|
||||
from .models import Event, EventInstance, Favorite, EventLocation, EventType
|
||||
|
||||
|
||||
class ScheduleConsumer(JsonWebsocketConsumer):
|
||||
|
@ -21,32 +21,45 @@ class ScheduleConsumer(JsonWebsocketConsumer):
|
|||
camp = Camp.objects.get(slug=camp_slug)
|
||||
days = list(map(
|
||||
lambda day:
|
||||
{ 'repr': day.lower.strftime('%A %Y-%m-%d')
|
||||
, 'iso': day.lower.strftime('%Y-%m-%d')
|
||||
, 'day_name': day.lower.strftime('%A')
|
||||
{
|
||||
'repr': day.lower.strftime('%A %Y-%m-%d'),
|
||||
'iso': day.lower.strftime('%Y-%m-%d'),
|
||||
'day_name': day.lower.strftime('%A'),
|
||||
},
|
||||
camp.get_days('camp')
|
||||
))
|
||||
event_instances_query_set = EventInstance.objects.filter(event__camp=camp)
|
||||
event_instances = list([x.to_json(user=message.user) for x in event_instances_query_set])
|
||||
event_instances = list([x.serialize(user=message.user) for x in event_instances_query_set])
|
||||
|
||||
event_locations_query_set = EventLocation.objects.filter(camp=camp)
|
||||
event_locations = list([x.to_json() for x in event_locations_query_set])
|
||||
event_locations = list([x.serialize() 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])
|
||||
event_types = list([x.serialize() for x in event_types_query_set])
|
||||
|
||||
data = {
|
||||
"action": "init",
|
||||
"event_locations": event_locations,
|
||||
"event_types": event_types,
|
||||
"accept": True,
|
||||
"event_instances": event_instances,
|
||||
"days": days,
|
||||
"action": "init"
|
||||
}
|
||||
except Camp.DoesNotExist:
|
||||
pass
|
||||
|
||||
if action == 'get_event_content':
|
||||
camp_slug = content.get('camp_slug')
|
||||
event_slug = content.get('event_slug')
|
||||
print(camp_slug)
|
||||
print(event_slug)
|
||||
event = Event.objects.get(
|
||||
slug=event_slug,
|
||||
camp__slug=camp_slug
|
||||
)
|
||||
data = event.serialize()
|
||||
data['action'] = "get_event_content"
|
||||
|
||||
if action == 'favorite':
|
||||
event_instance_id = content.get('event_instance_id')
|
||||
event_instance = EventInstance.objects.get(id=event_instance_id)
|
||||
|
|
|
@ -311,7 +311,7 @@ class EventLocation(CampRelatedModel):
|
|||
class Meta:
|
||||
unique_together = (('camp', 'slug'), ('camp', 'name'))
|
||||
|
||||
def to_json(self):
|
||||
def serialize(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"slug": self.slug,
|
||||
|
@ -357,7 +357,7 @@ class EventType(CreatedUpdatedModel):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def to_json(self):
|
||||
def serialize(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"slug": self.slug,
|
||||
|
@ -428,6 +428,18 @@ class Event(CampRelatedModel):
|
|||
def get_absolute_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
|
||||
|
||||
def serialize(self):
|
||||
data = {
|
||||
'title': self.title,
|
||||
'slug': self.slug,
|
||||
'abstract': self.abstract,
|
||||
'speakers': [
|
||||
speaker.serialize()
|
||||
for speaker in self.speakers.all()
|
||||
],
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class EventInstance(CampRelatedModel):
|
||||
""" An instance of an event """
|
||||
|
@ -490,24 +502,15 @@ class EventInstance(CampRelatedModel):
|
|||
ievent['location'] = icalendar.vText(self.location.name)
|
||||
return ievent
|
||||
|
||||
def to_json(self, user=None):
|
||||
parser = CommonMark.Parser()
|
||||
renderer = CommonMark.HtmlRenderer()
|
||||
ast = parser.parse(self.event.abstract)
|
||||
abstract = renderer.render(ast)
|
||||
|
||||
def serialize(self, user=None):
|
||||
data = {
|
||||
'title': self.event.title,
|
||||
'slug': self.event.slug + '-' + str(self.id),
|
||||
'event_slug': self.event.slug,
|
||||
'abstract': abstract,
|
||||
'from': self.when.lower.astimezone().isoformat(),
|
||||
'to': self.when.upper.astimezone().isoformat(),
|
||||
'url': str(self.event.get_absolute_url()),
|
||||
'id': self.id,
|
||||
'speakers': [
|
||||
{'name': speaker.name, 'url': str(speaker.get_absolute_url())}
|
||||
for speaker in self.event.speakers.all()
|
||||
],
|
||||
'bg-color': self.event.event_type.color,
|
||||
'fg-color': '#fff' if self.event.event_type.light_text else '#000',
|
||||
'event_type': self.event.event_type.slug,
|
||||
|
@ -604,6 +607,12 @@ class Speaker(CampRelatedModel):
|
|||
def get_absolute_url(self):
|
||||
return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
|
||||
|
||||
def serialize(self):
|
||||
data = {
|
||||
'name': self.name,
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class Favorite(models.Model):
|
||||
user = models.ForeignKey('auth.User', related_name='favorites')
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<script>
|
||||
var container = document.getElementById('schedule-container');
|
||||
Elm.Main.embed(
|
||||
var elm_app = Elm.Main.embed(
|
||||
container,
|
||||
{ 'schedule_timeslot_length_minutes': Number('{{ schedule_timeslot_length_minutes }}')
|
||||
, 'schedule_midnight_offset_hours': Number('{{ schedule_midnight_offset_hours }}')
|
||||
|
|
Loading…
Reference in a new issue