Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions config/widgets.exs
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,18 @@ config :neoboard, Neoboard.Widgets.Time,
# ],
# every: 10_000

#config :neoboard, Neoboard.Widgets.Youtube,
# url: "https://www.youtube.com/embed/uNYEZXvRlB8?&autoplay=1"
# config :neoboard, Neoboard.Widgets.Youtube,
# url: "https://www.youtube.com/embed/uNYEZXvRlB8?&autoplay=1"

# config :neoboard, Neoboard.Widgets.Calendar,
# calendars: [
# %{
# url: "https://calendar.company.com/projects.ics",
# colors: ["#0B467F", "#62B2FF", "#158CFF", "#31597F", "#1170CC"]
# },
# %{
# url: "https://calendar.company.com/meetings.ics",
# colors: ["#AF1E7F"]
# },
# ],
# every: 600_000
52 changes: 52 additions & 0 deletions lib/neoboard/widgets/calendar.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule Neoboard.Widgets.Calendar do
alias Neoboard.Widgets.Calendar.Parser
alias Neoboard.Widgets.Calendar.Data
alias Neoboard.TimeService
use GenServer
use Neoboard.Pusher
use Neoboard.Config

def start_link do
{:ok, pid} = GenServer.start_link(__MODULE__, nil)
send(pid, :tick)
:timer.send_interval(config()[:every], pid, :tick)
{:ok, pid}
end

def handle_info(:tick, _) do
events = fetch_calendars(config()[:calendars])
|> Enum.map(fn(data) -> data.events end)
|> List.flatten

push! %{events: events, current: TimeService.now_as_iso}
{:noreply, nil}
end

defp fetch_calendars([]), do: []
defp fetch_calendars([configuration | rest]) do
data = struct(Data, configuration)
[fetch(data)] ++ fetch_calendars(rest)
end

defp fetch(data) do
case HTTPoison.get(data.url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
process_body(data, body)
end
end

defp process_body(data, body) do
Parser.deserialize(data, body)
|> Data.to_export
|> inject_calendar_data_into_events
end

defp inject_calendar_data_into_events(data) do
events = Stream.cycle(data.colors)
|> Enum.zip(data.events)
|> Enum.map(fn({color, event}) ->
Map.merge(event, %{color: color, calendar: data.title})
end)
Map.put(data, :events, events)
end
end
10 changes: 10 additions & 0 deletions lib/neoboard/widgets/calendar/data.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Neoboard.Widgets.Calendar.Data do
alias Neoboard.Widgets.Calendar.Data
alias Neoboard.Widgets.Calendar.Event

defstruct url: nil, title: nil, colors: ["#CCCCCC"], events: [], tzid: "Etc/UTC"

def to_export(%Data{} = data) do
Map.put(data, :events, Enum.map(data.events, &Event.to_export/1))
end
end
21 changes: 21 additions & 0 deletions lib/neoboard/widgets/calendar/date_time_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Neoboard.Widgets.Calendar.DateTimeHelper do
alias Timex.Timezone

def parse(string, %{"VALUE" => "DATE"}, _) do
Timex.parse!(string, "{YYYY}{0M}{0D}") |> NaiveDateTime.to_date
end
def parse(string, %{"TZID" => tzid}, _), do: to_datetime(string, tzid)
def parse(string, _, nil), do: to_datetime(string, "Etc/UTC")
def parse(string, _, tzid), do: to_datetime(string, tzid)

defp to_datetime(string, timezone) do
datetime = case String.last(string) do
"Z" -> string <> timezone
_ -> string <> "Z" <> timezone
end

datetime
|> Timex.parse!("{YYYY}{0M}{0D}T{h24}{m}{s}Z{Zname}")
|> Timezone.convert("Etc/UTC")
end
end
22 changes: 22 additions & 0 deletions lib/neoboard/widgets/calendar/event.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Neoboard.Widgets.Calendar.Event do
alias Timex
alias Neoboard.Widgets.Calendar.Event

defstruct [:id, :title, :start, :end, :categories, :location, :description]

def all_day?(%Event{start: %Date{}, end: %Date{}}), do: true
def all_day?(_), do: false

def to_export(%Event{} = event) do
event
|> Map.merge(%{
start: format_as_iso(event.start),
end: format_as_iso(event.end),
allDay: all_day?(event)
})
end

defp format_as_iso(datetime) do
Timex.format!(datetime, "{ISO:Extended}")
end
end
77 changes: 77 additions & 0 deletions lib/neoboard/widgets/calendar/parser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule Neoboard.Widgets.Calendar.Parser do
alias Neoboard.Widgets.Calendar.Data
alias Neoboard.Widgets.Calendar.Event
alias Neoboard.Widgets.Calendar.DateTimeHelper

def deserialize(%Data{} = data, input) do
input
|> String.split("\n")
|> Enum.map(&extract_properties/1)
|> Enum.reject(&(&1 == nil))
|> Enum.reduce(data, &parse/2)
|> reverse_events
end

defp parse({"TZID", tzid, _}, data), do: Map.put(data, :tzid, tzid)
defp parse({"BEGIN", "VEVENT", _}, data) do
add_event(data)
end
defp parse({"UID", value, _}, data) do
update_event(data, :id, value)
end
defp parse({"SUMMARY", value, _}, data) do
update_event(data, :title, value)
end
defp parse({"DTSTART", value, params}, data) do
update_event(data, :start, DateTimeHelper.parse(value, params, data.tzid))
end
defp parse({"DTEND", value, params}, data) do
update_event(data, :end, DateTimeHelper.parse(value, params, data.tzid))
end
defp parse({"CATEGORIES", value, _}, data) do
update_event(data, :categories, value)
end
defp parse({"LOCATION", value, _}, data) do
update_event(data, :location, value)
end
defp parse({"DESCRIPTION", value, _}, data) do
update_event(data, :description, value)
end
defp parse({"X-WR-CALNAME", value, _}, data) do
Map.put(data, :title, value)
end
defp parse(_, data), do: data

defp add_event(%{events: events} = data) do
Map.put(data, :events, [%Event{} | events])
end

defp update_event(%{events: [event | rest]} = data, key, value) do
updated_event = %{event | key => value}
Map.put(data, :events, [updated_event | rest])
end
defp update_event(data, _, _), do: data

defp extract_properties(""), do: nil
defp extract_properties(line) do
[key, value] = String.split(line, ":", parts: 2, trim: true)
{key, params} = extract_params_from_key(key)
{key, String.trim(value), params}
end

defp extract_params_from_key(key) do
{key, rest} = case String.split(key, ";", trim: true) do
[key] -> {key, []}
[key | rest] -> {key, rest}
end
params = Enum.reduce(rest, %{}, fn(param, acc) ->
[name, value] = String.split(param, "=", parts: 2, trim: true)
Map.put(acc, name, value)
end)
{key, params}
end

defp reverse_events(%{events: events} = data) do
%{data | events: Enum.reverse(events)}
end
end
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
"classnames": "~2.2.5",
"emojify.js": "^1.1.0",
"lodash": "^4.17.4",
"moment": "^2.17.1",
"phoenix": "^1.2.1",
"react": "^15.6.1",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.1",
"react-grid-layout": "^0.14.6",
"react-intl": "^2.3.0",
"react-markdown": "^2.5.0"
"react-markdown": "^2.5.0",
"react-big-calendar": "^0.12.3"
},
"devDependencies": {
"babel-core": "^6.22.1",
Expand Down
62 changes: 62 additions & 0 deletions test/neoboard/widgets/calendar/calendar_fixture.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
BEGIN:VCALENDAR
PRODID:-//Inverse inc./SOGo 3.2.4//EN
VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:UUUU-UUUUUUUU-1-00000001
SUMMARY:Demo A
CLASS:PUBLIC
CATEGORIES:Category A
TRANSP:OPAQUE
DTSTART;VALUE=DATE:20170208
DTEND;VALUE=DATE:20170218
CREATED:20170216T110228Z
DTSTAMP:20170216T110228Z
LAST-MODIFIED:20170216T110720Z
LOCATION:Ort A
DESCRIPTION:Description A
SEQUENCE:1
END:VEVENT
BEGIN:VEVENT
UID:UUUU-UUUUUUUU-1-00000002
SUMMARY:Demo B
CLASS:PUBLIC
TRANSP:OPAQUE
DTSTART;VALUE=DATE:20170216
DTEND;VALUE=DATE:20170217
CREATED:20170216T110325Z
DTSTAMP:20170216T110325Z
LAST-MODIFIED:20170216T110605Z
CATEGORIES:Category B
END:VEVENT
BEGIN:VEVENT
UID:UUUU-UUUUUUUU-1-00000003
SUMMARY:Demo C
CLASS:PUBLIC
TRANSP:OPAQUE
DTSTART;TZID=Europe/Berlin:20170218T120000
DTEND;TZID=Europe/Berlin:20170218T130000
CREATED:20170216T112347Z
DTSTAMP:20170216T112347Z
LAST-MODIFIED:20170216T112347Z
END:VEVENT
X-WR-CALNAME:Some Calendar
END:VCALENDAR
24 changes: 24 additions & 0 deletions test/neoboard/widgets/calendar/date_time_helper_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Neoboard.Widgets.Calendar.DateTimeHelperTest do
use ExUnit.Case, async: true
alias Neoboard.Widgets.Calendar.DateTimeHelper

test "parses dates" do
input = "20170210"
got = DateTimeHelper.parse(input, %{"VALUE" => "DATE"}, nil)
assert ~D[2017-02-10] == got
end

test "uses tzid from params and converts to UTC" do
input = "20170218T120000"
got = DateTimeHelper.parse(input, %{"TZID" => "Europe/Berlin"}, nil)
{:ok, expected, _} = DateTime.from_iso8601("2017-02-18T11:00:00+00:00")
assert expected == got
end

test "uses tzid from fallback and converts to UTC" do
input = "20170218T120000"
got = DateTimeHelper.parse(input, nil, "Europe/Berlin")
{:ok, expected, _} = DateTime.from_iso8601("2017-02-18T11:00:00+00:00")
assert expected == got
end
end
44 changes: 44 additions & 0 deletions test/neoboard/widgets/calendar/event_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule Neoboard.Widgets.Calendar.EventTest do
use ExUnit.Case, async: true
alias Neoboard.Widgets.Calendar.Event

test "isn't all_day? if a start/end is DateTime" do
event = %Event{}
datetime = DateTime.from_naive!(~N[2017-02-01 10:10:10], "Etc/UTC")

assert false == Event.all_day?(event)

event = %{event | start: datetime}
assert false == Event.all_day?(event)

event = %{event | end: datetime}
assert false == Event.all_day?(event)
end

test "is all_day when both a start/end is Date" do
event = %Event{
start: ~D[2017-02-01],
end: ~D[2017-02-01],
}

assert true == Event.all_day?(event)
end

test "to_export" do
event = %Event{
id: "some_id",
title: "the title",
start: ~D[2017-02-01],
end: ~D[2017-02-01],
categories: "the_categories"
}

expected = Map.merge(event, %{
start: "2017-02-01T00:00:00",
end: "2017-02-01T00:00:00",
allDay: true
})

assert expected == Event.to_export(event)
end
end
Loading