diff options
| author | Vlatka Pavisic | 2017-01-05 16:24:25 +0100 |
|---|---|---|
| committer | Vlatka Pavisic | 2017-01-05 16:24:25 +0100 |
| commit | 858a42cabbd96f3654b0b1b0c49225a7f1bcd4ae (patch) | |
| tree | e9aa20f898e5f1a1158d6595b6f9670569b77201 | |
| parent | 75cc7574b4d138aa93fa774507f58a7ee94a0944 (diff) | |
| parent | 89ab282c5737f7c0d723917ec989f028e196e9c5 (diff) | |
| download | chouette-core-858a42cabbd96f3654b0b1b0c49225a7f1bcd4ae.tar.bz2 | |
Merge calendars
29 files changed, 878 insertions, 8 deletions
diff --git a/app/assets/stylesheets/main/calendars.sass b/app/assets/stylesheets/main/calendars.sass new file mode 100644 index 000000000..298ce2a62 --- /dev/null +++ b/app/assets/stylesheets/main/calendars.sass @@ -0,0 +1,11 @@ +#calendar_form + .btn + margin: 5px + #delete-btn + margin-top: 23px + .well + padding-left: 50px + +#calendar_search_form + button + margin-top: 24px diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb new file mode 100644 index 000000000..9784820f9 --- /dev/null +++ b/app/controllers/calendars_controller.rb @@ -0,0 +1,47 @@ +class CalendarsController < BreadcrumbController + defaults resource_class: Calendar + before_action :check_policy, only: [:edit, :update, :destroy] + + respond_to :html + respond_to :js, only: :index + + private + def calendar_params + permitted_params = [:id, :name, :short_name, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] + permitted_params << :shared if policy(Calendar).share? + params.require(:calendar).permit(*permitted_params) + end + + def sort_column + Calendar.column_names.include?(params[:sort]) ? params[:sort] : 'short_name' + end + + def sort_direction + %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc' + end + + protected + def resource + @calendar = Calendar.where('organisation_id = ? OR shared = true', current_organisation.id).find_by_id(params[:id]) + end + + def build_resource + super.tap do |calendar| + calendar.organisation = current_organisation + end + end + + def collection + return @calendars if @calendars + + @q = Calendar.where('organisation_id = ? OR shared = true', current_organisation.id).search(params[:q]) + calendars = @q.result + calendars = calendars.order(sort_column + ' ' + sort_direction) if sort_column && sort_direction + @calendars = calendars.paginate(page: params[:page]) + end + + def check_policy + authorize resource + end +end + diff --git a/app/helpers/breadcrumb_helper.rb b/app/helpers/breadcrumb_helper.rb index dc68bd897..6ff4845f9 100644 --- a/app/helpers/breadcrumb_helper.rb +++ b/app/helpers/breadcrumb_helper.rb @@ -38,6 +38,8 @@ module BreadcrumbHelper timeband_breadcrumb action when 'Chouette::RoutingConstraintZone' routing_constraint_zone_breadcrumb action + when 'Calendar' + calendar_breadcrumb action when "StopAreaCopy" stop_area_copy_breadcrumb action when "Import" @@ -70,6 +72,11 @@ module BreadcrumbHelper end end + def calendar_breadcrumb(action) + add_breadcrumb I18n.t('calendars.index.title'), calendars_path + add_breadcrumb @calendar.name if %i(show edit).include? action + end + def workbench_breadcrumb(action) add_breadcrumb I18n.t("breadcrumbs.referentials"), referentials_path add_breadcrumb breadcrumb_label(@workbench), workbench_path(@workbench), :title => breadcrumb_tooltip(@workbench) diff --git a/app/helpers/newfront_helper.rb b/app/helpers/newfront_helper.rb index 2abfd589d..7bedbeea9 100644 --- a/app/helpers/newfront_helper.rb +++ b/app/helpers/newfront_helper.rb @@ -55,13 +55,15 @@ module NewfrontHelper polymorph_url << action end - if current_referential - polymorph_url << current_referential - polymorph_url << item.line if item.respond_to? :line - elsif item.respond_to? :referential - polymorph_url << item.referential - elsif item.respond_to? :line_referential - polymorph_url << item.line_referential + unless item.class.to_s == 'Calendar' + if current_referential + polymorph_url << current_referential + polymorph_url << item.line if item.respond_to? :line + elsif item.respond_to? :referential + polymorph_url << item.referential + elsif item.respond_to? :line_referential + polymorph_url << item.line_referential + end end polymorph_url << item diff --git a/app/models/calendar.rb b/app/models/calendar.rb new file mode 100644 index 000000000..54237e9cc --- /dev/null +++ b/app/models/calendar.rb @@ -0,0 +1,257 @@ +class Calendar < ActiveRecord::Base + belongs_to :organisation + + validates_presence_of :name, :short_name, :organisation + validates_uniqueness_of :short_name + + after_initialize :init_dates_and_date_ranges + + scope :contains_date, ->(date) { where('date ? = any (dates) OR date ? <@ any (date_ranges)', date, date) } + + def init_dates_and_date_ranges + self.dates ||= [] + self.date_ranges ||= [] + end + + def self.ransackable_scopes(auth_object = nil) + [:contains_date] + end + + class Period + include ActiveAttr::Model + + attribute :id, type: Integer + attribute :begin, type: Date + attribute :end, type: Date + + validates_presence_of :begin, :end + validate :check_end_greather_than_begin + + def check_end_greather_than_begin + if self.begin and self.end and self.begin > self.end + errors.add(:end, :invalid) + end + end + + def self.from_range(index, range) + Period.new id: index, begin: range.begin, end: range.end + end + + def range + if self.begin and self.end and self.begin <= self.end + Range.new self.begin, self.end + end + end + + def intersect?(*other) + return false if range.nil? + + other = other.flatten + other = other.delete_if { |o| o.id == id } if id + + other.any? do |period| + if other_range = period.range + (range & other_range).present? + end + end + end + + def cover? date + range.cover? date + end + + # Stuff required for coocon + def new_record? + !persisted? + end + + def persisted? + id.present? + end + + def mark_for_destruction + self._destroy = true + end + + attribute :_destroy, type: Boolean + alias_method :marked_for_destruction?, :_destroy + end + + # Required by coocon + def build_period + Period.new + end + + def periods + @periods ||= init_periods + end + + def init_periods + if date_ranges + date_ranges.each_with_index.map { |r, index| Period.from_range(index, r) } + else + [] + end + end + private :init_periods + + validate :validate_periods + + def validate_periods + periods_are_valid = true + + unless periods.all?(&:valid?) + periods_are_valid = false + end + + periods.each do |period| + if period.intersect?(periods) + period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) + periods_are_valid = false + end + end + + unless periods_are_valid + errors.add(:periods, :invalid) + end + end + + def periods_attributes=(attributes = {}) + @periods = [] + attributes.each do |index, period_attribute| + period = Period.new(period_attribute.merge(id: index)) + @periods << period unless period.marked_for_destruction? + end + + date_ranges_will_change! + end + + before_validation :fill_date_ranges + + def fill_date_ranges + if @periods + self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) + end + end + + after_save :clear_periods + + def clear_periods + @periods = nil + end + + private :clear_periods + +### dates + + class DateValue + include ActiveAttr::Model + + attribute :id, type: Integer + attribute :value, type: Date + + validates_presence_of :value + + def self.from_date(index, date) + DateValue.new id: index, value: date + end + + # Stuff required for coocon + def new_record? + !persisted? + end + + def persisted? + id.present? + end + + def mark_for_destruction + self._destroy = true + end + + attribute :_destroy, type: Boolean + alias_method :marked_for_destruction?, :_destroy + end + + # Required by coocon + def build_date_value + DateValue.new + end + + def date_values + @date_values ||= init_date_values + end + + def init_date_values + if dates + dates.each_with_index.map { |d, index| DateValue.from_date(index, d) } + else + [] + end + end + private :init_date_values + + validate :validate_date_values + + def validate_date_values + date_values_are_valid = true + + unless date_values.all?(&:valid?) + date_values_are_valid = false + end + + date_values.each do |date_value| + if date_values.count { |d| d.value == date_value.value } > 1 + date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) + date_values_are_valid = false + end + date_ranges.each do |date_range| + if date_range.cover? date_value.value + date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) + date_values_are_valid = false + end + end + end + + unless date_values_are_valid + errors.add(:date_values, :invalid) + end + end + + def date_values_attributes=(attributes = {}) + @date_values = [] + attributes.each do |index, date_value_attribute| + date_value = DateValue.new(date_value_attribute.merge(id: index)) + @date_values << date_value unless date_value.marked_for_destruction? + end + + dates_will_change! + end + + before_validation :fill_dates + + def fill_dates + if @date_values + self.dates = @date_values.map(&:value).compact.sort + end + end + + after_save :clear_date_values + + def clear_date_values + @date_values = nil + end + + private :clear_date_values + +### + +end + +class Range + def intersection(other) + return nil if (self.max < other.begin or other.max < self.begin) + [self.begin, other.begin].max..[self.max, other.max].min + end + alias_method :&, :intersection +end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index af1042081..8547ce5e1 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -13,6 +13,7 @@ class Organisation < ActiveRecord::Base has_many :line_referentials, through: :line_referential_memberships has_many :workbenches + has_many :calendars validates_presence_of :name validates_uniqueness_of :code diff --git a/app/policies/calendar_policy.rb b/app/policies/calendar_policy.rb new file mode 100644 index 000000000..1c455f391 --- /dev/null +++ b/app/policies/calendar_policy.rb @@ -0,0 +1,31 @@ +class CalendarPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def show? + organisation_match? || record.shared + end + + def new? ; modify? end + def create? ; new? end + + def edit? ; modify? end + def update? ; edit? end + + def destroy? ; modify? end + + def share? + user.organisation_id == 1 # FIXME + end + + def modify? + organisation_match? + end + + def organisation_match? + user.organisation == record.organisation + end +end diff --git a/app/views/calendars/_calendars.html.slim b/app/views/calendars/_calendars.html.slim new file mode 100644 index 000000000..261052baf --- /dev/null +++ b/app/views/calendars/_calendars.html.slim @@ -0,0 +1,12 @@ +- if @calendars.any? + = table_builder @calendars, + { @calendars.human_attribute_name(:short_name) => 'short_name', @calendars.human_attribute_name(:shared) => 'shared' }, + [:show, :edit, :delete], + 'table table-bordered' + + .text-center + = will_paginate @calendars, container: false, renderer: RemoteBootstrapPaginationLinkRenderer + +- else + = replacement_msg t('.search_no_results') + diff --git a/app/views/calendars/_date_value_fields.html.slim b/app/views/calendars/_date_value_fields.html.slim new file mode 100644 index 000000000..3a9cdb7bd --- /dev/null +++ b/app/views/calendars/_date_value_fields.html.slim @@ -0,0 +1,14 @@ +.nested-fields + - if f.object.errors.has_key? :base + .row + .col-lg-12 + .alert.alert-danger + - f.object.errors[:base].each do |message| + p.small = message + .row + .col-xs-3 + = f.input :value, as: :date, html5: true, label: t('simple_form.labels.calendar.date_value') + .col-xs-1.end.text-right#delete-btn + = link_to_remove_association f, class: 'btn btn-danger', data: { confirm: t('are_you_sure') } do + span.fa.fa-trash + diff --git a/app/views/calendars/_form.html.slim b/app/views/calendars/_form.html.slim new file mode 100644 index 000000000..a97c16565 --- /dev/null +++ b/app/views/calendars/_form.html.slim @@ -0,0 +1,27 @@ +#calendar_form + .row + .col-xs-8.col-xs-offset-2 + = simple_form_for @calendar, html: { class: 'form-horizontal' } do |f| + = f.input :name + = f.input :short_name + .form-group + = f.label :dates + .well + = f.simple_fields_for :date_values do |date_value| + = render 'date_value_fields', f: date_value + .links + = link_to_add_association t('simple_form.labels.calendar.add_a_date'), f, :date_values, class: 'btn btn-primary btn-xs' + .form-group + = f.label :date_ranges + .well + = f.simple_fields_for :periods do |period| + = render 'period_fields', f: period + .links + = link_to_add_association t('simple_form.labels.calendar.add_a_date_range'), f, :periods, class: 'btn btn-primary btn-xs' + - if policy(@calendar).share? + = f.input :shared + .form-actions + = f.button :submit, as: :button, class: 'btn btn-info' + = link_to t('cancel'), calendars_path, class: 'btn btn-default' + + diff --git a/app/views/calendars/_period_fields.html.slim b/app/views/calendars/_period_fields.html.slim new file mode 100644 index 000000000..024d09de2 --- /dev/null +++ b/app/views/calendars/_period_fields.html.slim @@ -0,0 +1,16 @@ +.nested-fields + - if f.object.errors.has_key? :base + .row + .col-lg-12 + .alert.alert-danger + - f.object.errors[:base].each do |message| + p.small = message + .row + .col-xs-4 + = f.input :begin, as: :date, html5: true, label: t('simple_form.labels.calendar.ranges.begin') + .col-xs-3 + = f.input :end, as: :date, html5: true, label: t('simple_form.labels.calendar.ranges.end') + .col-xs-1.text-right#delete-btn + = link_to_remove_association f, class: 'btn btn-danger', data: { confirm: t('are_you_sure') } do + span.fa.fa-trash + diff --git a/app/views/calendars/edit.html.slim b/app/views/calendars/edit.html.slim new file mode 100644 index 000000000..22645cf24 --- /dev/null +++ b/app/views/calendars/edit.html.slim @@ -0,0 +1,3 @@ += title_tag t('.title', calendar: @calendar.name) + += render 'form' diff --git a/app/views/calendars/index.html.slim b/app/views/calendars/index.html.slim new file mode 100644 index 000000000..41cd3d70c --- /dev/null +++ b/app/views/calendars/index.html.slim @@ -0,0 +1,29 @@ += title_tag t('.title') + +#calendar_search_form + = search_form_for @q, url: calendars_path, remote: true, html: { method: :get, class: 'form', id: 'search', role: 'form' } do |f| + .panel.panel-default + .panel-heading + .row + .col-md-3 + = f.label Calendar.human_attribute_name(:short_name) + = f.search_field :short_name_cont, class: 'form-control' + .col-md-3 + = f.label Calendar.human_attribute_name(:shared) + = f.select :shared_eq, [[t('.shared'), true], [t('.not_shared'), false]], { include_blank: '' }, { class: 'form-control', style: 'width: 100%', 'data-select2ed': 'true', 'data-select2ed-placeholder': t('.all') } + .col-md-3 + = f.label Calendar.human_attribute_name(:date) + = f.date_field :contains_date, class: 'form-control' + + .col-md-3 + button.btn.btn-primary#search-btn type='submit' + span.fa.fa-search + +#calendars + = render 'calendars' + +- content_for :sidebar do + ul.actions + li + = link_to t('calendars.actions.new'), new_calendar_path, class: 'add' + br diff --git a/app/views/calendars/index.js.slim b/app/views/calendars/index.js.slim new file mode 100644 index 000000000..936f93e5e --- /dev/null +++ b/app/views/calendars/index.js.slim @@ -0,0 +1 @@ +| $('#calendars').html("#{escape_javascript(render('calendars'))}"); diff --git a/app/views/calendars/new.html.slim b/app/views/calendars/new.html.slim new file mode 100644 index 000000000..f827e2eb6 --- /dev/null +++ b/app/views/calendars/new.html.slim @@ -0,0 +1,3 @@ += title_tag t('.title') + += render 'form' diff --git a/app/views/calendars/show.html.slim b/app/views/calendars/show.html.slim new file mode 100644 index 000000000..c0671fa94 --- /dev/null +++ b/app/views/calendars/show.html.slim @@ -0,0 +1,46 @@ += title_tag t('.title', calendar: @calendar.name) + +p + label => "#{Calendar.human_attribute_name('name')} : " + = @calendar.name +p + label => "#{Calendar.human_attribute_name('short_name')} : " + = @calendar.short_name + +.row + .col-xs-4 + p + label => "#{Calendar.human_attribute_name('dates')} : " + table.table.table-condensed + tbody + - @calendar.dates.each do |date| + tr + td= l date + p + label => "#{Calendar.human_attribute_name('date_ranges')} : " + table.table.table-condensed + thead + th= t('simple_form.labels.calendar.ranges.begin') + th= t('simple_form.labels.calendar.ranges.end') + tbody + - @calendar.date_ranges.each do |date_range| + tr + td= l date_range.begin + td= l date_range.end + +p + label => "#{Calendar.human_attribute_name('shared')} : " + = @calendar.shared +p + label => "#{Organisation.model_name.human} : " + = @calendar.organisation.name + + +- content_for(:sidebar) do + ul.actions + - if policy(@calendar).edit? + li + = link_to t('calendars.actions.edit'), edit_calendar_path(@calendar), class: 'edit' + - if policy(@calendar).destroy? + li + = link_to t('calendars.actions.destroy'), calendar_path(@calendar), method: :delete, data: { confirm: t('calendars.actions.destroy_confirm') }, class: 'remove' diff --git a/app/views/referentials/_period_fields.html.slim b/app/views/referentials/_period_fields.html.slim index 9d92f92ce..6658cd4aa 100644 --- a/app/views/referentials/_period_fields.html.slim +++ b/app/views/referentials/_period_fields.html.slim @@ -14,3 +14,4 @@ .col-lg-2.col-md-2.col-sm-2.col-xs-2.text-right style='margin-top:23px' = link_to_remove_association f, class: 'btn btn-danger', data: { confirm: 'Etes-vous sûr(e) ?' } do span.fa.fa-trash + diff --git a/config/locales/actions.en.yml b/config/locales/actions.en.yml index 1321e6761..ba5bc1506 100644 --- a/config/locales/actions.en.yml +++ b/config/locales/actions.en.yml @@ -10,4 +10,5 @@ en: search_hint: "Type in a search term" no_result_text: "No Results" searching_term: "Searching..." + are_you_sure: Are you sure? diff --git a/config/locales/actions.fr.yml b/config/locales/actions.fr.yml index 6c3e22933..16c687458 100644 --- a/config/locales/actions.fr.yml +++ b/config/locales/actions.fr.yml @@ -10,3 +10,4 @@ fr: search_hint: "Entrez un texte à rechercher" no_result_text: "Aucun résultat" searching_term: "Recherche en cours..." + are_you_sure: Etes vous sûr ? diff --git a/config/locales/calendars.en.yml b/config/locales/calendars.en.yml new file mode 100644 index 000000000..90ab12e76 --- /dev/null +++ b/config/locales/calendars.en.yml @@ -0,0 +1,48 @@ +en: + calendars: + actions: + new: Add a new calendar + edit: Edit this calendar + destroy: Remove this calendar + destroy_confirm: Are you sure you want destroy this calendar? + errors: + overlapped_periods: Another period is overlapped with this period + index: + title: Calendars + all: All + shared: Shared + not_shared: Not shared + search_no_results: No calendar matching your query + date: Date + new: + title: Add a new calendar + edit: + title: Update calendar %{calendar} + show: + title: Calendar %{calendar} + simple_form: + labels: + calendar: + date_value: Date + add_a_date: Add a date + add_a_date_range: Add a date range + ranges: + begin: Beginning + end: End + activerecord: + models: + calendar: Calendar + attributes: + calendar: + name: Name + short_name: Short name + date_ranges: Date ranges + dates: Dates + shared: Shared + errors: + models: + calendar: + attributes: + dates: + date_in_date_ranges: A date can not be in Dates and in Date ranges. + date_in_dates: A date can appear only once in the list of dates. diff --git a/config/locales/calendars.fr.yml b/config/locales/calendars.fr.yml new file mode 100644 index 000000000..ba45ad074 --- /dev/null +++ b/config/locales/calendars.fr.yml @@ -0,0 +1,49 @@ +fr: + calendars: + actions: + new: Ajouter un calendrier + edit: Modifier cet calendrier + destroy: Supprimer cet calendrier + destroy_confirm: Etes vous sûr de supprimer cet calendrier ? + errors: + overlapped_periods: Une autre période chevauche cette période + index: + title: Calendriers + all: Tous + shared: Partagées + not_shared: Non partagées + search_no_results: Aucun calendrier ne correspond à votre recherche + date: Date + new: + title: Ajouter un calendrier + edit: + title: Modifier le calendrier %{calendar} + show: + title: Calendrier %{calendar} + simple_form: + labels: + calendar: + date_value: Date + add_a_date: Ajouter une date + add_a_date_range: Ajouter un intervalle de dates + ranges: + begin: Début + end: Fin + activerecord: + models: + calendar: Calendrier + attributes: + calendar: + name: Nom + short_name: Nom court + date_ranges: Intervalles de dates + dates: Dates + shared: Partagé + errors: + models: + calendar: + attributes: + dates: + date_in_date_ranges: Une même date ne peut pas être incluse à la fois dans la liste et dans les intervalles de dates. + date_in_dates: Une même date ne peut pas être incluse plusieurs fois dans la liste. + diff --git a/config/locales/users.fr.yml b/config/locales/users.fr.yml index dcb81b808..f50218605 100644 --- a/config/locales/users.fr.yml +++ b/config/locales/users.fr.yml @@ -20,3 +20,5 @@ fr: user: name: "Nom complet" username: "Nom d'utilisateur" + + diff --git a/config/routes.rb b/config/routes.rb index 9d4141198..99effe3e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,8 @@ ChouetteIhm::Application.routes.draw do resources :networks end + resources :calendars + resources :referentials do resources :api_keys resources :autocomplete_stop_areas diff --git a/db/migrate/20161228092957_create_calendars.rb b/db/migrate/20161228092957_create_calendars.rb new file mode 100644 index 000000000..d8bc89103 --- /dev/null +++ b/db/migrate/20161228092957_create_calendars.rb @@ -0,0 +1,15 @@ +class CreateCalendars < ActiveRecord::Migration + def change + create_table :calendars do |t| + t.string :name + t.string :short_name + t.daterange :date_ranges, array: true + t.date :dates, array: true + t.boolean :shared + t.belongs_to :organisation, index: true + + t.timestamps + end + add_index :calendars, :short_name, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 0277cae91..562bf2ab6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161208120132) do +ActiveRecord::Schema.define(version: 20161228092957) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -77,6 +77,20 @@ ActiveRecord::Schema.define(version: 20161208120132) do t.datetime "updated_at" end + create_table "calendars", force: true do |t| + t.string "name" + t.string "short_name" + t.daterange "date_ranges", array: true + t.date "dates", array: true + t.boolean "shared" + t.integer "organisation_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "calendars", ["organisation_id"], :name => "index_calendars_on_organisation_id" + add_index "calendars", ["short_name"], :name => "index_calendars_on_short_name", :unique => true + create_table "clean_up_results", force: true do |t| t.string "message_key" t.hstore "message_attributs" diff --git a/lib/range_ext.rb b/lib/range_ext.rb new file mode 100644 index 000000000..5afb44dee --- /dev/null +++ b/lib/range_ext.rb @@ -0,0 +1,7 @@ +class Range + def intersection(other) + return nil if (self.max < other.begin or other.max < self.begin) + [self.begin, other.begin].max..[self.max, other.max].min + end + alias_method :&, :intersection +end diff --git a/spec/factories/calendars.rb b/spec/factories/calendars.rb new file mode 100644 index 000000000..5f3188bee --- /dev/null +++ b/spec/factories/calendars.rb @@ -0,0 +1,16 @@ +FactoryGirl.define do + factory :calendar do + sequence(:name) { |n| "Calendar #{n}" } + sequence(:short_name) { |n| "Cal #{n}" } + date_ranges { [generate(:date_range)] } + sequence(:dates) { |n| [ Date.yesterday - n, Date.yesterday - 2*n ] } + shared false + organisation + end + + sequence :date_range do |n| + date = Date.today + 2*n + date..(date+1) + end +end + diff --git a/spec/features/calendars_spec.rb b/spec/features/calendars_spec.rb new file mode 100644 index 000000000..c1701d7c7 --- /dev/null +++ b/spec/features/calendars_spec.rb @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +require 'spec_helper' + +describe 'Calendars', type: :feature do + login_user + + let!(:calendars) { Array.new(2) { create :calendar, organisation_id: 1 } } + let!(:shared_calendar_other_org) { create :calendar, shared: true } + let!(:unshared_calendar_other_org) { create :calendar } + + describe 'index' do + before(:each) { visit calendars_path } + + it 'displays calendars of the current organisation and shared calendars' do + expect(page).to have_content(calendars.first.short_name) + expect(page).to have_content(shared_calendar_other_org.short_name) + expect(page).not_to have_content(unshared_calendar_other_org.short_name) + end + + context 'filtering' do + it 'supports filtering by short name' do + fill_in 'q[short_name_cont]', with: calendars.first.short_name + click_button 'search-btn' + expect(page).to have_content(calendars.first.short_name) + expect(page).not_to have_content(calendars.last.short_name) + end + + it 'supports filtering by shared' do + shared_calendar = create :calendar, organisation_id: 1, shared: true + visit calendars_path + select I18n.t('calendars.index.shared'), from: 'q[shared_eq]' + click_button 'search-btn' + expect(page).to have_content(shared_calendar.short_name) + expect(page).not_to have_content(calendars.first.short_name) + end + + it 'supports filtering by date' do + july_calendar = create :calendar, dates: [Date.new(2017, 7, 7)], date_ranges: [Date.new(2017, 7, 15)..Date.new(2017, 7, 30)], organisation_id: 1 + visit calendars_path + fill_in 'q_contains_date', with: '2017/07/07' + click_button 'search-btn' + expect(page).to have_content(july_calendar.short_name) + expect(page).not_to have_content(calendars.first.short_name) + fill_in 'q_contains_date', with: '2017/07/18' + click_button 'search-btn' + expect(page).to have_content(july_calendar.short_name) + expect(page).not_to have_content(calendars.first.short_name) + end + end + end + + describe 'show' do + it 'displays calendar' do + visit calendar_path(calendars.first) + expect(page).to have_content(calendars.first.name) + end + end +end + diff --git a/spec/models/calendar_spec.rb b/spec/models/calendar_spec.rb new file mode 100644 index 000000000..36981961f --- /dev/null +++ b/spec/models/calendar_spec.rb @@ -0,0 +1,148 @@ +require 'rails_helper' + +RSpec.describe Calendar, :type => :model do + + it { should belong_to(:organisation) } + + it { is_expected.to validate_presence_of(:organisation) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:short_name) } + it { is_expected.to validate_uniqueness_of(:short_name) } + + describe 'validations' do + it 'validates that dates and date_ranges do not overlap' do + calendar = build(:calendar, dates: [Date.today], date_ranges: [Date.today..Date.tomorrow]) + expect { + calendar.save! + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'validates that there are no duplicates in dates' do + calendar = build(:calendar, dates: [Date.yesterday, Date.yesterday], date_ranges: [Date.today..Date.tomorrow]) + expect { + calendar.save! + }.to raise_error(ActiveRecord::RecordInvalid) + end + end + + describe 'Period' do + + subject { period } + + def period(attributes = {}) + return @period if attributes.empty? and @period + Calendar::Period.new(attributes).tap do |period| + @period = period if attributes.empty? + end + end + + it 'should support mark_for_destruction (required by cocoon)' do + period.mark_for_destruction + expect(period).to be_marked_for_destruction + end + + it 'should support _destroy attribute (required by coocon)' do + period._destroy = true + expect(period).to be_marked_for_destruction + end + + it 'should support new_record? (required by cocoon)' do + expect(Calendar::Period.new).to be_new_record + expect(period(id: 42)).not_to be_new_record + end + + it 'should cast begin as date attribute' do + expect(period(begin: '2016-11-22').begin).to eq(Date.new(2016,11,22)) + end + + it 'should cast end as date attribute' do + expect(period(end: '2016-11-22').end).to eq(Date.new(2016,11,22)) + end + + it { is_expected.to validate_presence_of(:begin) } + it { is_expected.to validate_presence_of(:end) } + + it 'should validate that end is greather than or equlals to begin' do + expect(period(begin: '2016-11-21', end: '2016-11-22')).to be_valid + expect(period(begin: '2016-11-21', end: '2016-11-21')).to be_valid + expect(period(begin: '2016-11-22', end: '2016-11-21')).to_not be_valid + end + + describe 'intersect?' do + + it 'should detect date in common with other date_ranges' do + november = period(begin: '2016-11-01', end: '2016-11-30') + mid_november_mid_december = period(begin: '2016-11-15', end: '2016-12-15') + expect(november.intersect?(mid_november_mid_december)).to be(true) + end + + it 'should not intersect when no date is in common' do + november = period(begin: '2016-11-01', end: '2016-11-30') + december = period(begin: '2016-12-01', end: '2016-12-31') + + expect(november.intersect?(december)).to be(false) + + january = period(begin: '2017-01-01', end: '2017-01-31') + expect(november.intersect?(december, january)).to be(false) + end + + it 'should not intersect itself' do + period = period(id: 42, begin: '2016-11-01', end: '2016-11-30') + expect(period.intersect?(period)).to be(false) + end + + end + end + + describe 'before_validation' do + let(:calendar) { create(:calendar, date_ranges: []) } + + it 'shoud fill date_ranges with date ranges' do + expected_ranges = [ + Range.new(Date.today, Date.tomorrow) + ] + expected_ranges.each_with_index do |range, index| + calendar.date_ranges << Calendar::Period.from_range(index, range) + end + calendar.valid? + + expect(calendar.date_ranges.map { |period| period.begin..period.end }).to eq(expected_ranges) + end + end + + describe 'DateValue' do + + subject { date_value } + + def date_value(attributes = {}) + return @date_value if attributes.empty? and @date_value + Calendar::DateValue.new(attributes).tap do |date_value| + @date_value = date_value if attributes.empty? + end + end + + it 'should support mark_for_destruction (required by cocoon)' do + date_value.mark_for_destruction + expect(date_value).to be_marked_for_destruction + end + + it 'should support _destroy attribute (required by coocon)' do + date_value._destroy = true + expect(date_value).to be_marked_for_destruction + end + + it 'should support new_record? (required by cocoon)' do + expect(Calendar::DateValue.new).to be_new_record + expect(date_value(id: 42)).not_to be_new_record + end + + it 'should cast value as date attribute' do + expect(date_value(value: '2017-01-03').value).to eq(Date.new(2017,01,03)) + end + + it { is_expected.to validate_presence_of(:value) } + + end + +end + |
