Add schedule interval as attribute of a program

Also, the part of the schedule event is deleted and the length of the event types
changes to the nearest suitable after changing the length of the interval.

This closes #1220
This commit is contained in:
JewelSam 2017-03-02 20:53:20 +03:00
parent d1bf19867c
commit f6e74461ba
19 changed files with 104 additions and 35 deletions

View file

@ -43,7 +43,6 @@ There are a couple of environment variables you can set to configure OSEM. Check
| OSEM_FACEBOOK_SECRET | *string* | OMNIAUTH Developer Secret for Facebook | OSEM_FACEBOOK_SECRET | *string* | OMNIAUTH Developer Secret for Facebook
| OSEM_GITHUB_KEY | *string* | OMNIAUTH Developer Key for GitHub | OSEM_GITHUB_KEY | *string* | OMNIAUTH Developer Key for GitHub
| OSEM_GITHUB_SECRET | *string* | OMNIAUTH Developer Secret for GitHub | OSEM_GITHUB_SECRET | *string* | OMNIAUTH Developer Secret for GitHub
| OSEM_SCHEDULE_CELL_SIZE | *integer* | Schedule timeslot size to use (in minutes), should be greater than zero, should be divisor of 60
| OSEM_SMTP_ADDRESS | smtp.opensuse.org | The smtp server to use | OSEM_SMTP_ADDRESS | smtp.opensuse.org | The smtp server to use
| OSEM_SMTP_PORT | *int* | The port on the smtp server | OSEM_SMTP_PORT | *int* | The port on the smtp server
| OSEM_SMTP_USERNAME | *string* | The user for the smtp server | OSEM_SMTP_USERNAME | *string* | The user for the smtp server

View file

@ -54,10 +54,6 @@
"description": "The user for the smtp server", "description": "The user for the smtp server",
"required": false "required": false
}, },
"OSEM_SCHEDULE_CELL_SIZE": {
"description": "Schedule timeslot size in minutes",
"required": false
},
"RACK_ENV": { "RACK_ENV": {
"required": false "required": false
}, },

View file

@ -12,13 +12,15 @@ module Admin
@program = @conference.program @program = @conference.program
@program.assign_attributes(program_params) @program.assign_attributes(program_params)
send_mail_on_schedule_public = @program.notify_on_schedule_public? send_mail_on_schedule_public = @program.notify_on_schedule_public?
event_schedules_count_was = @program.event_schedules.count
if @program.update_attributes(program_params) if @program.update_attributes(program_params)
ConferenceScheduleUpdateMailJob.perform_later(@conference) if send_mail_on_schedule_public ConferenceScheduleUpdateMailJob.perform_later(@conference) if send_mail_on_schedule_public
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to admin_conference_program_path(@conference.short_title), notice = 'The program was successfully updated.'
notice: 'The program was successfully updated.' notice += ' You changed schedule interval and some events were unscheduled.' if @program.event_schedules.count != event_schedules_count_was
redirect_to admin_conference_program_path(@conference.short_title), notice: notice
end end
format.js { render json: {} } format.js { render json: {} }
end end
@ -36,7 +38,7 @@ module Admin
private private
def program_params def program_params
params.require(:program).permit(:rating, :schedule_public, :schedule_fluid, :languages, :blind_voting, :voting_start_date, :voting_end_date, :selected_schedule_id) params.require(:program).permit(:rating, :schedule_public, :schedule_interval, :schedule_fluid, :languages, :blind_voting, :voting_start_date, :voting_end_date, :selected_schedule_id)
end end
end end
end end

View file

@ -13,7 +13,7 @@ class SchedulesController < ApplicationController
@events_xml = schedules.map(&:event).group_by{ |event| event.time.to_date } if schedules @events_xml = schedules.map(&:event).group_by{ |event| event.time.to_date } if schedules
@dates = @conference.start_date..@conference.end_date @dates = @conference.start_date..@conference.end_date
@step_minutes = EventType::LENGTH_STEP.minutes @step_minutes = @program.schedule_interval.minutes
@conf_start = @conference.start_hour @conf_start = @conference.start_hour
@conf_period = @conference.end_hour - @conf_start @conf_period = @conference.end_hour - @conf_start

View file

@ -15,17 +15,13 @@ class EventType < ActiveRecord::Base
alias_attribute :name, :title alias_attribute :name, :title
# If LENGTH_STEP must be divisor of 60, otherwise the schedule wont be displayed properly
LENGTH_STEP = defined?(SCHEDULE_CELL_SIZE) ? SCHEDULE_CELL_SIZE : 15
private private
## ##
# Check if length is multiple of LENGTH_STEP. Used as validation. # Check if length is a divisor of program schedule cell size. Used as validation.
# #
def length_step def length_step
errors.add(:length, "must be multiple of #{LENGTH_STEP}") if length % LENGTH_STEP != 0 errors.add(:length, "must be a divisor of #{program.schedule_interval}") if program && length % program.schedule_interval != 0
end end
def capitalize_color def capitalize_color

View file

@ -38,6 +38,7 @@ class Program < ActiveRecord::Base
where(state: :confirmed, is_highlight: true) where(state: :confirmed, is_highlight: true)
end end
end end
has_many :event_schedules, through: :events
has_many :event_users, through: :events has_many :event_users, through: :events
has_many :speakers, -> { distinct }, through: :event_users, source: :user do has_many :speakers, -> { distinct }, through: :event_users, source: :user do
@ -52,11 +53,15 @@ class Program < ActiveRecord::Base
# validates :conference_id, presence: true, uniqueness: true # validates :conference_id, presence: true, uniqueness: true
validates :rating, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 10, only_integer: true } validates :rating, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 10, only_integer: true }
validates :schedule_interval, numericality: { greater_than_or_equal_to: 5, less_than_or_equal_to: 60 }, presence: true
validate :schedule_interval_divisor_60
validate :voting_start_date_before_end_date validate :voting_start_date_before_end_date
validate :voting_dates_exist validate :voting_dates_exist
after_create :create_event_types after_create :create_event_types
after_create :create_difficulty_levels after_create :create_difficulty_levels
after_save :unschedule_unfit_events, if: :schedule_interval_changed?
after_save :normalize_event_types_length, if: :schedule_interval_changed?
validate :check_languages_format validate :check_languages_format
# Returns all event_schedules for the selected schedule ordered by start_time # Returns all event_schedules for the selected schedule ordered by start_time
@ -199,4 +204,31 @@ class Program < ActiveRecord::Base
# We check if every language is a valid ISO 639-1 language # We check if every language is a valid ISO 639-1 language
errors.add(:languages, 'must be ISO 639-1 valid codes') unless languages_array.select{ |x| ISO_639.find(x).nil? }.empty? errors.add(:languages, 'must be ISO 639-1 valid codes') unless languages_array.select{ |x| ISO_639.find(x).nil? }.empty?
end end
##
# Check if schedule_interval is a divisor of 60 minutes
#
def schedule_interval_divisor_60
errors.add(:schedule_interval, 'must be a divisor of 60') if schedule_interval > 0 && 60 % schedule_interval > 0
end
##
# Unschedule all the events which don't fit
#
def unschedule_unfit_events
unfit_schedules = event_schedules.select do |event_schedule|
event_schedule.start_time.min % schedule_interval > 0
end
EventSchedule.where(id: unfit_schedules.map(&:id)).destroy_all
end
##
# Change event type length according schedule interval
#
def normalize_event_types_length
event_types.each do |event_type|
new_length = event_type.length > schedule_interval ? event_type.length - (event_type.length % schedule_interval) : schedule_interval
event_type.update_attributes length: new_length
end
end
end end

View file

@ -26,6 +26,6 @@ class EventSerializer < ActiveModel::Serializer
end end
def length def length
object.event_type.try(:length) || EventType::LENGTH_STEP object.event_type.try(:length) || object.event_type.program.schedule_interval
end end
end end

View file

@ -10,7 +10,7 @@
.col-md-12 .col-md-12
= semantic_form_for(@event_type, url: (@event_type.new_record? ? admin_conference_program_event_types_path : admin_conference_program_event_type_path(@conference.short_title, @event_type))) do |f| = semantic_form_for(@event_type, url: (@event_type.new_record? ? admin_conference_program_event_types_path : admin_conference_program_event_type_path(@conference.short_title, @event_type))) do |f|
= f.input :title = f.input :title
= f.input :length, input_html: {size: 3, type: 'number', step: EventType::LENGTH_STEP, min: EventType::LENGTH_STEP} = f.input :length, input_html: {size: 3, type: 'number', step: @event_type.program.schedule_interval, min: @event_type.program.schedule_interval}
= f.input :description = f.input :description
= f.input :minimum_abstract_length, input_html: {size: 3} = f.input :minimum_abstract_length, input_html: {size: 3}
= f.input :maximum_abstract_length, input_html: {size: 3} = f.input :maximum_abstract_length, input_html: {size: 3}

View file

@ -9,6 +9,7 @@
= f.input :schedule_fluid, label: 'Allow submitters to change their event after it is scheduled' = f.input :schedule_fluid, label: 'Allow submitters to change their event after it is scheduled'
= f.input :rating, hint: 'Enter the number of different rating levels you want to have for voting on proposals. Enter 0 if you do not want to vote on proposals.' = f.input :rating, hint: 'Enter the number of different rating levels you want to have for voting on proposals. Enter 0 if you do not want to vote on proposals.'
= f.input :languages, hint: "Enter the languages allowed for events as values of #{link_to('ISO 639-1', 'http://www.loc.gov/standards/iso639-2/php/code_list.php', target: "_blank")} language codes separated with commas. The first language would be the default language. Leave it blank if you do not want to specify languages.".html_safe = f.input :languages, hint: "Enter the languages allowed for events as values of #{link_to('ISO 639-1', 'http://www.loc.gov/standards/iso639-2/php/code_list.php', target: "_blank")} language codes separated with commas. The first language would be the default language. Leave it blank if you do not want to specify languages.".html_safe
= f.input :schedule_interval, hint: "It is the minimal time interval of your schedule. The value should be 5, 6, 10, 12, 15, 20, 30 or 60. Warning! Some events could be unscheduled when changing this value."
= f.input :blind_voting, hint: 'Enable this feature if you do not want to show voting results and voters prior to user submitting a vote. For the feature to work you need to set the voting dates below as well' = f.input :blind_voting, hint: 'Enable this feature if you do not want to show voting results and voters prior to user submitting a vote. For the feature to work you need to set the voting dates below as well'
= f.input :voting_start_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_start_date.to_formatted_s(:db_without_seconds) unless f.object.voting_start_date.nil?) } = f.input :voting_start_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_start_date.to_formatted_s(:db_without_seconds) unless f.object.voting_start_date.nil?) }
= f.input :voting_end_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_end_date.to_formatted_s(:db_without_seconds) unless f.object.voting_end_date.nil?) } = f.input :voting_end_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_end_date.to_formatted_s(:db_without_seconds) unless f.object.voting_end_date.nil?) }

View file

@ -49,6 +49,11 @@
Yes Yes
- else - else
No No
%dt
Schedule interval
%dd
= @program.schedule_interval
minutes
%h3 Voting Options %h3 Voting Options
%hr %hr

View file

@ -1,5 +1,5 @@
- compact_grid = EventType::LENGTH_STEP < 15 - compact_grid = @program.schedule_interval < 15
- cells_per_hour = 60 / EventType::LENGTH_STEP - cells_per_hour = 60 / @program.schedule_interval
/ use smaller cell heights for more compact grids / use smaller cell heights for more compact grids
- cell_height = compact_grid ? 32 : 58 - cell_height = compact_grid ? 32 : 58
- date_event_schedules = @event_schedules.select{ |e| e.start_time.to_date.eql? date } - date_event_schedules = @event_schedules.select{ |e| e.start_time.to_date.eql? date }
@ -11,7 +11,7 @@
= room.name = room.name
- (@conference.start_hour * cells_per_hour..@conference.end_hour * cells_per_hour).each do |slot| - (@conference.start_hour * cells_per_hour..@conference.end_hour * cells_per_hour).each do |slot|
- hour = slot / cells_per_hour - hour = slot / cells_per_hour
- minutes = (EventType::LENGTH_STEP * (slot % cells_per_hour)).to_s.rjust(2, '0') - minutes = (@program.schedule_interval * (slot % cells_per_hour)).to_s.rjust(2, '0')
- time = "#{hour}:#{minutes}" - time = "#{hour}:#{minutes}"
.schedule-room-slot{ id: "schedule-room-#{room.guid}-#{hour}-#{minutes}", | .schedule-room-slot{ id: "schedule-room-#{room.guid}-#{hour}-#{minutes}", |
room_id: room.id, | room_id: room.id, |

View file

@ -1,6 +1,6 @@
- cells_length = event.event_type.length / EventType::LENGTH_STEP - cells_length = event.event_type.length / @program.schedule_interval
/ this height fits the room cells / this height fits the room cells
- compact_grid = EventType::LENGTH_STEP < 15 - compact_grid = @program.schedule_interval < 15
- single_cell_height = compact_grid ? 32 : 58 - single_cell_height = compact_grid ? 32 : 58
- height = (cells_length * single_cell_height) - height = (cells_length * single_cell_height)
- height -= 23 unless compact_grid - height -= 23 unless compact_grid

View file

@ -1,4 +1,4 @@
- intervals = hrs_per_slide * 60 / EventType::LENGTH_STEP + 1 - intervals = hrs_per_slide * 60 / @conference.program.schedule_interval + 1
- width = 85 / intervals - width = 85 / intervals
- carousel_number = (@conf_period / hrs_per_slide.to_f).ceil - carousel_number = (@conf_period / hrs_per_slide.to_f).ceil
.carousel.slide{ id: "carousel-#{ date }-#{ hrs_per_slide }", | .carousel.slide{ id: "carousel-#{ date }-#{ hrs_per_slide }", |
@ -41,7 +41,7 @@
- if event_schedule - if event_schedule
/ There is an event, calculate the span and show it / There is an event, calculate the span and show it
- event_span = (event_schedule.end_time.to_i - start_room_time.to_i) / 60 / EventType::LENGTH_STEP - event_span = (event_schedule.end_time.to_i - start_room_time.to_i) / 60 / @conference.program.schedule_interval
- span = ((event_span + i - 1 ) > intervals ? intervals + 1 - i : event_span) - span = ((event_span + i - 1 ) > intervals ? intervals + 1 - i : event_span)
= render partial: 'schedule_item', locals: {event: event_schedule.event, event_schedule: event_schedule, span: span, width: width} = render partial: 'schedule_item', locals: {event: event_schedule.event, event_schedule: event_schedule, span: span, width: width}
- else - else

View file

@ -6,7 +6,7 @@
%start= @conference.start_date %start= @conference.start_date
%end= @conference.end_date %end= @conference.end_date
%days= (@conference.end_date - @conference.start_date).to_i + 1 %days= (@conference.end_date - @conference.start_date).to_i + 1
%timeslot_duration= length_timestamp(EventType::LENGTH_STEP) %timeslot_duration= length_timestamp(@conference.program.schedule_interval)
- if @events_xml.any? - if @events_xml.any?
- @events_xml.keys.each.with_index(1) do |day, index| - @events_xml.keys.each.with_index(1) do |day, index|

View file

@ -1,6 +0,0 @@
#sanitize the OSEM_SCHEUDLE_CELL_SIZE to be used for EventType::LENGTH_STEP
sched_cell_size = ENV['OSEM_SCHEDULE_CELL_SIZE'].to_i
if (sched_cell_size > 0 and 60 % sched_cell_size == 0)
SCHEDULE_CELL_SIZE = sched_cell_size
end

View file

@ -0,0 +1,5 @@
class AddScheduleIntervalToPrograms < ActiveRecord::Migration
def change
add_column :programs, :schedule_interval, :integer, default: 15, null: false
end
end

View file

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170213145807) do ActiveRecord::Schema.define(version: 20170302145716) do
create_table "ahoy_events", force: :cascade do |t| create_table "ahoy_events", force: :cascade do |t|
t.uuid "visit_id", limit: 16 t.uuid "visit_id", limit: 16
@ -289,6 +289,7 @@ ActiveRecord::Schema.define(version: 20170213145807) do
t.datetime "voting_start_date" t.datetime "voting_start_date"
t.datetime "voting_end_date" t.datetime "voting_end_date"
t.integer "selected_schedule_id" t.integer "selected_schedule_id"
t.integer "schedule_interval", default: 15, null: false
end end
add_index "programs", ["selected_schedule_id"], name: "index_programs_on_selected_schedule_id" add_index "programs", ["selected_schedule_id"], name: "index_programs_on_selected_schedule_id"

View file

@ -57,6 +57,3 @@ OSEM_SMTP_DOMAIN=""
# Enable the usage of the devise ichain plugin # Enable the usage of the devise ichain plugin
OSEM_ICHAIN_ENABLED=false OSEM_ICHAIN_ENABLED=false
# Schedule grid parameters, cell size in minutes
OSEM_SCHEDULE_CELL_SIZE=15

View file

@ -13,6 +13,7 @@ describe Program do
it { is_expected.to have_many(:tracks).dependent(:destroy) } it { is_expected.to have_many(:tracks).dependent(:destroy) }
it { is_expected.to have_many(:difficulty_levels).dependent(:destroy) } it { is_expected.to have_many(:difficulty_levels).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:event_schedules).through(:events) }
it { is_expected.to have_many(:event_users).through(:events) } it { is_expected.to have_many(:event_users).through(:events) }
it { is_expected.to have_many(:speakers).through(:event_users).source(:user) } it { is_expected.to have_many(:speakers).through(:event_users).source(:user) }
@ -32,6 +33,18 @@ describe Program do
it { is_expected.to validate_numericality_of(:rating).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(10).only_integer } it { is_expected.to validate_numericality_of(:rating).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(10).only_integer }
it { is_expected.to validate_numericality_of(:schedule_interval).is_greater_than_or_equal_to(5).is_less_than_or_equal_to(60) }
describe 'schedule_interval_divisor_60' do
it 'is valid, when schedule_interval is divisor of 60' do
expect(build(:program, schedule_interval: 20)).to be_valid
end
it 'is not valid, when schedule_interval is not divisor of 60' do
expect(build(:program, schedule_interval: 35)).to_not be_valid
end
end
describe 'voting_start_date_before_end_date' do describe 'voting_start_date_before_end_date' do
it 'is valid, when voting_start_date is the same day as voting_end_date' do it 'is valid, when voting_start_date is the same day as voting_end_date' do
expect(build(:program, voting_start_date: Date.today, voting_end_date: Date.today)).to be_valid expect(build(:program, voting_start_date: Date.today, voting_end_date: Date.today)).to be_valid
@ -167,6 +180,34 @@ describe Program do
end end
end end
describe 'excecutes after_save functions' do
it 'and unschedule unfit events if schedule interval was changed' do
start_date = program.conference.start_date.to_datetime.change(hour: program.conference.start_hour)
create(:event_schedule, event: create(:event, program: program), start_time: start_date.change(min: program.schedule_interval))
create(:event_schedule, event: create(:event, program: program), start_time: start_date)
expect(program.event_schedules.count).to eq 2
program.schedule_interval = 10
program.save!
program.reload
expect(program.event_schedules.count).to eq 1
expect(program.event_schedules.first.start_time).to eq start_date
end
it 'and change event type length if schedule interval was changed' do
program.schedule_interval = 5
program.save!
program.event_types.first.update_attributes length: 5
program.event_types.last.update_attributes length: 25
create(:event_type, program: program, length: 30)
program.schedule_interval = 10
program.save!
expect(program.event_types.pluck(:length).sort).to eq [10, 20, 30]
end
end
describe 'languages' do describe 'languages' do
it "is not valid if languages aren't two letters separated by commas" do it "is not valid if languages aren't two letters separated by commas" do
program.languages = 'eng, De es' program.languages = 'eng, De es'