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_GITHUB_KEY | *string* | OMNIAUTH Developer Key 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_PORT | *int* | The port on 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",
"required": false
},
"OSEM_SCHEDULE_CELL_SIZE": {
"description": "Schedule timeslot size in minutes",
"required": false
},
"RACK_ENV": {
"required": false
},

View File

@@ -12,13 +12,15 @@ module Admin
@program = @conference.program
@program.assign_attributes(program_params)
send_mail_on_schedule_public = @program.notify_on_schedule_public?
event_schedules_count_was = @program.event_schedules.count
if @program.update_attributes(program_params)
ConferenceScheduleUpdateMailJob.perform_later(@conference) if send_mail_on_schedule_public
respond_to do |format|
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
format.js { render json: {} }
end
@@ -36,7 +38,7 @@ module Admin
private
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

View File

@@ -13,7 +13,7 @@ class SchedulesController < ApplicationController
@events_xml = schedules.map(&:event).group_by{ |event| event.time.to_date } if schedules
@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_period = @conference.end_hour - @conf_start

View File

@@ -15,17 +15,13 @@ class EventType < ActiveRecord::Base
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
##
# 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
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
def capitalize_color

View File

@@ -38,6 +38,7 @@ class Program < ActiveRecord::Base
where(state: :confirmed, is_highlight: true)
end
end
has_many :event_schedules, through: :events
has_many :event_users, through: :events
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 :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_dates_exist
after_create :create_event_types
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
# 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
errors.add(:languages, 'must be ISO 639-1 valid codes') unless languages_array.select{ |x| ISO_639.find(x).nil? }.empty?
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

View File

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

View File

@@ -10,7 +10,7 @@
.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|
= 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 :minimum_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 :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 :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 :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?) }

View File

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

View File

@@ -1,5 +1,5 @@
- compact_grid = EventType::LENGTH_STEP < 15
- cells_per_hour = 60 / EventType::LENGTH_STEP
- compact_grid = @program.schedule_interval < 15
- cells_per_hour = 60 / @program.schedule_interval
/ use smaller cell heights for more compact grids
- cell_height = compact_grid ? 32 : 58
- date_event_schedules = @event_schedules.select{ |e| e.start_time.to_date.eql? date }
@@ -11,7 +11,7 @@
= room.name
- (@conference.start_hour * cells_per_hour..@conference.end_hour * cells_per_hour).each do |slot|
- 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}"
.schedule-room-slot{ id: "schedule-room-#{room.guid}-#{hour}-#{minutes}", |
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
- compact_grid = EventType::LENGTH_STEP < 15
- compact_grid = @program.schedule_interval < 15
- single_cell_height = compact_grid ? 32 : 58
- height = (cells_length * single_cell_height)
- 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
- carousel_number = (@conf_period / hrs_per_slide.to_f).ceil
.carousel.slide{ id: "carousel-#{ date }-#{ hrs_per_slide }", |
@@ -41,7 +41,7 @@
- if event_schedule
/ 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)
= render partial: 'schedule_item', locals: {event: event_schedule.event, event_schedule: event_schedule, span: span, width: width}
- else

View File

@@ -6,7 +6,7 @@
%start= @conference.start_date
%end= @conference.end_date
%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?
- @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.
ActiveRecord::Schema.define(version: 20170213145807) do
ActiveRecord::Schema.define(version: 20170302145716) do
create_table "ahoy_events", force: :cascade do |t|
t.uuid "visit_id", limit: 16
@@ -289,6 +289,7 @@ ActiveRecord::Schema.define(version: 20170213145807) do
t.datetime "voting_start_date"
t.datetime "voting_end_date"
t.integer "selected_schedule_id"
t.integer "schedule_interval", default: 15, null: false
end
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
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(:difficulty_levels).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(: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(: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
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
@@ -167,6 +180,34 @@ describe Program do
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
it "is not valid if languages aren't two letters separated by commas" do
program.languages = 'eng, De es'