EZCalendar

Turn your Ecto queries into calendar style data structures!

  • Includes view helpers for server side rendering in Phoenix
  • Tested with Ecto, Calecto and TimexEcto

Installation

This package is available in Hex.

Add ez_calendar to your mix.exs file.

def deps do
  [{:ez_calendar, "~> 0.1.0"}]
end

def application do
  [applications: [:ez_calendar]]
end

Add EZCalendar to your repo.ex:

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app
  use EZCalendar
end

Usage

Turn your queries into calendar structs

def calendar(params \\ DateTime.utc_now) do
  MyApp.Shift
  |> with_employee
  |> MyApp.Repo.month_calendar(params)
end

Month, week, day and biweekly calendars exist and can be called with their respective functions or by passing the module in as an argument to Repo.calendar/4. All Repo functions have a bang! variant that will raise an error rather than returning a tuple.

The params can be a Date, DateTime, erl style tuple or map containing the keys day month and year. The day can be ommited from the params, doing so will result in the first of the month being used. Tuple and map values will be cast as integers for easy integration with phoenix.

Repo.day_calendar(query, %{day: 1, month: 11, year: 2016})
Repo.week_calendar(query, {2016, 11, 27})
Repo.month_calendar(query, {2016, 11})
Repo.biweekly_calendar(query, {2016, 11, 3})
# Custom calendar modules can be defined.
Repo.calendar(query, MyApp.PayrollCalendar, params)
# Examples  lib/ez_calendar/calendars/
# Behaviour lib/ez_calendar/calendars/calendar.ex

The timezone and query field can be set with :tz and :field as options. If using a biweekly calendar the biweekly_start can also be set. Default values for the options can be set in the config.

Repo.biweekly_calendar(
  query, 
  params, 
  field: [:starting, :ending], 
  tz: "America/Vancouver", 
  biweekly_start: {2016, 1, 4}
)

The field option represents field name in your schema used to build the calendars. It can be a single field, or 2 item list or tuple representing a start/end range. Date or DateTime types are valid, if using a range the types must match.

The timezone defaults to UTC and must a be valid TZ data format. It’s used to appropriately set each dates today? boolean flag.

The biweekly_start must be an erl equal to the first day in any of the two week periods you want the calendar to represent. Can start on any day of the week.

Multiple Queries

You can use a keyword list for the query. Each query will use the default field(s) unless provided in the query list or in the options.

[
  shifts: [Shift, [:starting, :ending]],
  events: [Event, :starting_at],
  meetings: Meeting,
  deliveries: Delivery
]
|> Repo.month_calendar({2016, 11}, field: :scheduled_for)

Query results will be accessible by their key in each dates data field:

[
  # ...
  %{
    data: %{
      shifts: [%Shift{}, %Shift{}],
      events: [%Event{}],
      meetings: [],
      deliveries: [%Delivery{}]
    },
    day: 18, 
    month: 11, 
    today?: true, 
    weekday: "Friday", 
    year: 2016
  },
  # ...
]

Phoenix

Install one of the stylesheet formats.

$ mix ez_calendar.css
$ mix ez_calendar.scss
$ mix ez_calendar.sass

Build a calendar from the params.

def index(conn, params) do
  case MyApp.Repo.month_calendar(MyApp.Shift, params) do
    {:ok, calendar} ->
      render(conn, "index.html", calendar: calendar)

    {:error, reason} ->
      calendar = MyApp.Repo.month_calendar!(MyApp.Shift, DateTime.utc_now)

      conn
      |> put_flash(:error, reason)
      |> render("index.html", calendar: calendar)
  end
end

Import the view helpers.

defmodule MyApp.ShiftView do
  use MyApp.Web, :view
  import EZCalendar.HTML
end

Render the calendar with the view helpers.

<%= calendar_prev @calendar, "/shifts/:year/:month" %>
<%= @calendar.title %>
<%= calendar_next @calendar, "/shifts/:year/:month" %>

<%= render_calendar @calendar, fn(date)-> %>
  <!-- calendar date -->
  <%= for shift <- date.data do %>
    <!-- query results for date -->
  <% end %> 
<% end %> 

If you want to use an html module that is not the default for that calendar, you can also use the render_calendar/3 function to pass in the module as the first arugument.

<%= render_calendar MyApp.MonthCalendarHTML, @calendar, fn(date)-> %>
<% end %>
# Custom HTML modules can be defined.
# Examples  lib/ez_calendar/html/calendars/
# Behaviour lib/ez_calendar/html/calendars/calendar.ex

The calendar_next and calendar_prev functions accept the calendar struct and a string showing how to format the path. The correct parameters will replace the :day, :month and :year placeholders.

The links contents can also be passed in as an optional third argument.


<%= calendar_next @calendar, "/shifts/:year/:month/:day", "Next" %>

<%= calendar_next @calendar, "/shifts/:year/:month", fn()-> %>
  <!-- link content -->
<% end %>

<%= calendar_next @calendar, "/shifts?year=:year&month=:month", "Next" %>

Configuration

The following values are the defaults.

config :ez_calendar, 
  default_field: :date, 
  default_tz: "UTC", 
  default_next: ">",      
  default_prev: "<",   
  default_biweekly_start: {2016, 1, 3} 

Contributing

Help is very welcome! Please make sure all the tests are passing before submitting a pull request.

Setup the database

$ MIX_ENV=test mix ecto.create && mix ecto.migrate

Run the tests!

$ mix test