From e8c0ab194196550be193d9154d6d25a46cd0f081 Mon Sep 17 00:00:00 2001 From: Vidir Valberg Gudmundsson Date: Mon, 17 Jul 2017 11:25:57 +0200 Subject: [PATCH] Organized the code in a more sane manner. Also some websocket work. --- schedule/Main.elm | 523 ------------------- schedule/Makefile | 4 +- schedule/elm-package.json | 2 +- schedule/src/Decoders.elm | 95 ++++ schedule/src/Main.elm | 41 ++ schedule/src/Messages.elm | 19 + schedule/src/Models.elm | 126 +++++ schedule/src/Routing.elm | 30 ++ schedule/src/Update.elm | 110 ++++ schedule/src/Views.elm | 241 +++++++++ schedule/src/WebSocketCalls.elm | 42 ++ src/program/consumers.py | 29 +- src/program/models.py | 35 +- src/program/templates/schedule_overview.html | 2 +- 14 files changed, 751 insertions(+), 548 deletions(-) delete mode 100644 schedule/Main.elm create mode 100644 schedule/src/Decoders.elm create mode 100644 schedule/src/Main.elm create mode 100644 schedule/src/Messages.elm create mode 100644 schedule/src/Models.elm create mode 100644 schedule/src/Routing.elm create mode 100644 schedule/src/Update.elm create mode 100644 schedule/src/Views.elm create mode 100644 schedule/src/WebSocketCalls.elm diff --git a/schedule/Main.elm b/schedule/Main.elm deleted file mode 100644 index 7219dd56..00000000 --- a/schedule/Main.elm +++ /dev/null @@ -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) - ] - ] - ] diff --git a/schedule/Makefile b/schedule/Makefile index 5cfdeb9c..d093dced 100644 --- a/schedule/Makefile +++ b/schedule/Makefile @@ -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 diff --git a/schedule/elm-package.json b/schedule/elm-package.json index 8dbc336d..5b8b4855 100644 --- a/schedule/elm-package.json +++ b/schedule/elm-package.json @@ -4,7 +4,7 @@ "repository": "https://github.com/user/project.git", "license": "BSD3", "source-directories": [ - "." + "src/" ], "exposed-modules": [], "dependencies": { diff --git a/schedule/src/Decoders.elm b/schedule/src/Decoders.elm new file mode 100644 index 00000000..65c34d08 --- /dev/null +++ b/schedule/src/Decoders.elm @@ -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 [] diff --git a/schedule/src/Main.elm b/schedule/src/Main.elm new file mode 100644 index 00000000..acd95d73 --- /dev/null +++ b/schedule/src/Main.elm @@ -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 diff --git a/schedule/src/Messages.elm b/schedule/src/Messages.elm new file mode 100644 index 00000000..399914b3 --- /dev/null +++ b/schedule/src/Messages.elm @@ -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 diff --git a/schedule/src/Models.elm b/schedule/src/Models.elm new file mode 100644 index 00000000..f8afe6d4 --- /dev/null +++ b/schedule/src/Models.elm @@ -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" "" "" diff --git a/schedule/src/Routing.elm b/schedule/src/Routing.elm new file mode 100644 index 00000000..f3caeef5 --- /dev/null +++ b/schedule/src/Routing.elm @@ -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 diff --git a/schedule/src/Update.elm b/schedule/src/Update.elm new file mode 100644 index 00000000..70ca0818 --- /dev/null +++ b/schedule/src/Update.elm @@ -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 ] diff --git a/schedule/src/Views.elm b/schedule/src/Views.elm new file mode 100644 index 00000000..c199c5c4 --- /dev/null +++ b/schedule/src/Views.elm @@ -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) + ] + ] + ] diff --git a/schedule/src/WebSocketCalls.elm b/schedule/src/WebSocketCalls.elm new file mode 100644 index 00000000..cfde5396 --- /dev/null +++ b/schedule/src/WebSocketCalls.elm @@ -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 ) + ] + ) + ) diff --git a/src/program/consumers.py b/src/program/consumers.py index 725a849e..6d88cd56 100644 --- a/src/program/consumers.py +++ b/src/program/consumers.py @@ -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) diff --git a/src/program/models.py b/src/program/models.py index 7b3dbb9e..b6ac2626 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -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') diff --git a/src/program/templates/schedule_overview.html b/src/program/templates/schedule_overview.html index 064badb1..1fe22a2f 100644 --- a/src/program/templates/schedule_overview.html +++ b/src/program/templates/schedule_overview.html @@ -11,7 +11,7 @@