Initial work on the schedule written in elm.

This commit is contained in:
Vidir Valberg Gudmundsson 2017-07-16 01:31:00 +02:00
parent 477b1b85de
commit a6470d2ec9
8 changed files with 432 additions and 52 deletions

2
.gitignore vendored
View file

@ -6,4 +6,4 @@ db.sqlite3
*.pyc *.pyc
venv/ venv/
environment_settings.py environment_settings.py
elm-stuff/

355
schedule/Main.elm Normal file
View 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
View 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
View 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"
}

View file

@ -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,34 +10,43 @@ class ScheduleConsumer(JsonWebsocketConsumer):
def connection_groups(self, **kwargs): def connection_groups(self, **kwargs):
return ['schedule_users'] return ['schedule_users']
def connect(self, message, **kwargs):
camp_slug = message.http_session['campslug']
try:
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')
},
camp.get_days('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))
self.send({
"accept": True,
"event_instances": event_instances,
"days": days,
"action": "init"
})
except Camp.DoesNotExist:
pass
def raw_receive(self, message, **kwargs): def raw_receive(self, message, **kwargs):
content = self.decode_json(message['text']) content = self.decode_json(message['text'])
action = content.get('action') action = content.get('action')
data = {} data = {}
if action == 'init':
camp_slug = content.get('camp_slug')
try:
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')
},
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_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,
"event_instances": event_instances,
"days": days,
"action": "init"
}
except Camp.DoesNotExist:
pass
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,7 +61,8 @@ 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()
self.send(data) if data:
self.send(data)
def disconnect(self, message, **kwargs): def disconnect(self, message, **kwargs):
pass pass

View file

@ -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. """

View file

@ -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)

View file

@ -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;