diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 9f5494c0..4644a6bb 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -172,6 +172,9 @@ linters: - "app/views/users/edit.html.haml" - "app/views/users/show.html.haml" - "app/views/admin/cfps/index.html.haml" + - "app/views/tracks/_form.html.haml" + - "app/views/tracks/index.html.haml" + - "app/views/tracks/show.html.haml" # Offense count: 223 InstanceVariables: @@ -232,6 +235,7 @@ linters: - "app/views/schedules/_schedule_item.html.haml" - "app/views/schedules/_schedule_tabs.html.haml" - "app/views/admin/cfps/_events_cfp.html.haml" + - "app/views/tracks/_form.html.haml" # Offense count: 32 IdNames: @@ -331,6 +335,8 @@ linters: - "app/views/shared/_object_changes.html.haml" - "app/views/tickets/_ticket.html.haml" - "app/views/tickets/index.html.haml" + - "app/views/tracks/index.html.haml" + - "app/views/tracks/show.html.haml" # Offense count: 23 ClassesBeforeIds: diff --git a/.rubocop.yml b/.rubocop.yml index 22e17db0..5d24032d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -30,3 +30,4 @@ Metrics/BlockLength: Exclude: - 'spec/models/conference_spec.rb' - 'spec/features/ability_spec.rb' + - 'spec/models/ability_spec.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 68360907..9523593a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -94,6 +94,8 @@ Metrics/ModuleLength: # Offense count: 14 Metrics/PerceivedComplexity: Max: 15 + Exclude: + - 'app/controllers/admin/roles_controller.rb' # Offense count: 11 # Cop supports --auto-correct. @@ -467,17 +469,6 @@ Style/IndentationConsistency: - 'app/models/event.rb' - 'spec/controllers/subscriptions_controller_spec.rb' -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: Width, IgnoredPatterns. -Style/IndentationWidth: - Exclude: - - 'app/helpers/format_helper.rb' - - 'app/serializers/conference_serializer.rb' - - 'db/migrate/20140701123203_add_events_per_week_to_conference.rb' - - 'lib/tasks/demo_data_for_development.rake' - - 'spec/models/ability_spec.rb' - # Offense count: 4 # Cop supports --auto-correct. Style/LeadingCommentSpace: @@ -850,6 +841,7 @@ Style/SymbolProc: - 'app/controllers/admin/questions_controller.rb' - 'app/helpers/application_helper.rb' - 'app/models/ability.rb' + - 'app/models/admin_ability.rb' - 'db/migrate/20140730104658_migrate_roles_for_cancancan.rb' - 'spec/controllers/admin/conferences_controller_spec.rb' - 'spec/support/flash.rb' diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 3b28a230..631dd4d8 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -15,7 +15,8 @@ module Admin end unless (current_user.has_role? :organizer, :any) || (current_user.has_role? :cfp, :any) || (current_user.has_role? :info_desk, :any) || (current_user.has_role? :organization_admin, :any) || - (current_user.has_role? :volunteers_coordinator, :any) || current_user.is_admin + (current_user.has_role? :volunteers_coordinator, :any) || + (current_user.has_role? :track_organizer, :any) || current_user.is_admin raise CanCan::AccessDenied.new('You are not authorized to access this page.') end end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 3bc37bc3..e4b83241 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -8,14 +8,26 @@ module Admin def index @roles = Role.where(resource: @conference) + tracks = @conference.program.tracks.where.not(submitter: nil) + @roles += Role.where(resource: tracks) authorize! :index, @role end def show + @url = if @track + toggle_user_track_admin_conference_role_path(@conference.short_title, @role.name, @track) + else + toggle_user_admin_conference_role_path(@conference.short_title, @role.name) + end @users = @role.users end def edit + @url = if @track + track_admin_conference_role_path(@conference.short_title, @role.name, @track) + else + admin_conference_role_path(@conference.short_title, @role.name) + end @users = @role.users end @@ -23,7 +35,13 @@ module Admin role_name = @role.name if @role.update_attributes(role_params) - redirect_to admin_conference_role_path(@conference.short_title, @role.name), + url = if @track + track_admin_conference_role_path(@conference.short_title, @role.name, @track) + else + admin_conference_role_path(@conference.short_title, @role.name) + end + + redirect_to url, notice: 'Successfully updated role ' + @role.name else @role.name = role_name @@ -36,8 +54,14 @@ module Admin user = User.find_by(email: user_params[:email]) state = user_params[:state] + url = if @track + track_admin_conference_role_path(@conference.short_title, @role.name, @track) + else + admin_conference_role_path(@conference.short_title, @role.name) + end + unless user - redirect_to admin_conference_role_path(@conference.short_title, @role.name), + redirect_to url, error: 'Could not find user. Please provide a valid email!' return end @@ -49,17 +73,23 @@ module Admin return end + if @role.resource_type == 'Conference' + role_resource = @conference + elsif @role.resource_type == 'Track' + role_resource = @track + end + # Remove user if state == 'false' - if user.remove_role @role.name, @conference + if user.remove_role @role.name, role_resource flash[:notice] = "Successfully removed role #{@role.name} from user #{user.email}" else flash[:error] = "Could not remove role #{@role.name} from user #{user.email}" end - elsif user.has_role? @role.name, @conference + elsif user.has_role? @role.name, role_resource flash[:error] = "User #{user.email} already has the role #{@role.name}" # Add user - elsif user.add_role @role.name, @conference + elsif user.add_role @role.name, role_resource flash[:notice] = "Successfully added role #{@role.name} to user #{user.email}" else flash[:error] = "Coud not add role #{@role.name} to #{user.email}" @@ -67,7 +97,7 @@ module Admin respond_to do |format| format.js - format.html { redirect_to admin_conference_role_path(@conference.short_title, @role.name) } + format.html { redirect_to url } end end @@ -77,7 +107,12 @@ module Admin # Set 'organizer' as default role, when there is no other selection @selection = params[:id] ? params[:id].parameterize.underscore : 'organizer' - @role = Role.find_by(name: @selection, resource: @conference) + if @selection == 'track_organizer' + @track = @conference.program.tracks.find_by(short_name: params[:track_name]) + @role = Role.find_by(name: @selection, resource: @track) + else + @role = Role.find_by(name: @selection, resource: @conference) + end end def role_params diff --git a/app/controllers/admin/tracks_controller.rb b/app/controllers/admin/tracks_controller.rb index 4fc9d9c7..c229b9b8 100644 --- a/app/controllers/admin/tracks_controller.rb +++ b/app/controllers/admin/tracks_controller.rb @@ -50,10 +50,19 @@ module Admin end end + def toggle_cfp_inclusion + @track.cfp_active = !@track.cfp_active + if @track.save + head :ok + else + head :unprocessable_entity + end + end + private def track_params - params.require(:track).permit(:name, :description, :color, :short_name) + params.require(:track).permit(:name, :description, :color, :short_name, :cfp_active) end end end diff --git a/app/controllers/tracks_controller.rb b/app/controllers/tracks_controller.rb new file mode 100644 index 00000000..529767cd --- /dev/null +++ b/app/controllers/tracks_controller.rb @@ -0,0 +1,47 @@ +class TracksController < ApplicationController + load_resource :conference, find_by: :short_title + load_resource :program, through: :conference, singleton: true + load_and_authorize_resource through: :program, find_by: :short_name + + def index + @tracks = current_user.tracks.where(program: @program) + end + + def show; end + + def new + @track = @program.tracks.new(color: @conference.next_color_for_collection(:tracks)) + end + + def edit; end + + def create + @track = @program.tracks.new(track_params) + @track.submitter = current_user + @track.state = 'new' + @track.cfp_active = false + if @track.save + redirect_to conference_program_tracks_path(conference_id: @conference.short_title), + notice: 'Track request successfully created.' + else + flash.now[:error] = "Creating Track request failed: #{@track.errors.full_messages.join('. ')}." + render :new + end + end + + def update + if @track.update_attributes(track_params) + redirect_to conference_program_tracks_path(conference_id: @conference.short_title), + notice: 'Track request successfully updated.' + else + flash.now[:error] = "Track request update failed: #{@track.errors.full_messages.join('. ')}." + render :edit + end + end + + private + + def track_params + params.require(:track).permit(:name, :description, :color, :short_name) + end +end diff --git a/app/helpers/format_helper.rb b/app/helpers/format_helper.rb index 472edc38..334c70bf 100644 --- a/app/helpers/format_helper.rb +++ b/app/helpers/format_helper.rb @@ -102,13 +102,12 @@ module FormatHelper end end - # rubocop:disable Lint/EndAlignment def word_pluralize(count, singular, plural = nil) word = if (count == 1 || count =~ /^1(\.0+)?$/) - singular - else - plural || singular.pluralize - end + singular + else + plural || singular.pluralize + end "#{word}" end diff --git a/app/models/admin_ability.rb b/app/models/admin_ability.rb index 006e919a..d43683fd 100644 --- a/app/models/admin_ability.rb +++ b/app/models/admin_ability.rb @@ -70,6 +70,11 @@ class AdminAbility cannot :destroy, Venue do |venue| venue.conference.program.events.where.not(room_id: nil).any? end + + # Prevent requests for tracks from being destroyed + cannot :destroy, Track do |track| + track.self_organized? + end end # Abilities for signed in users with roles @@ -79,6 +84,7 @@ class AdminAbility signed_in_with_cfp_role(user) if user.has_role? :cfp, :any signed_in_with_info_desk_role(user) if user.has_role? :info_desk, :any signed_in_with_volunteers_coordinator_role(user) if user.has_role? :volunteers_coordinator, :any + signed_in_with_track_organizer_role(user) if user.has_role? :track_organizer, :any common_abilities_for_roles(user) end @@ -100,6 +106,9 @@ class AdminAbility # ids of all the conferences for which the user has the 'organizer' role and # conferences that belong to organizations for which user is 'organization_admin' conf_ids = conf_ids_for_organization_admin.concat(Conference.with_role(:organizer, user).pluck(:id)).uniq + # ids of all the tracks that belong to the programs of the above conferences + track_ids = Track.joins(:program).where('programs.conference_id IN (?)', conf_ids).pluck(:id) + can :manage, Resource, conference_id: conf_ids can [:read, :update, :destroy], Conference, id: conf_ids can :manage, Splashpage, conference_id: conf_ids @@ -140,11 +149,12 @@ class AdminAbility # Abilities for Role (Conference resource) can [:index, :show], Role do |role| - role.resource_type == 'Conference' + role.resource_type == 'Conference' || role.resource_type == 'Track' end can [:edit, :update, :toggle_user], Role do |role| - role.resource_type == 'Conference' && (conf_ids.include? role.resource_id) + role.resource_type == 'Conference' && (conf_ids.include? role.resource_id) || + role.resource_type == 'Track' && (track_ids.include? role.resource_id) end can [:index, :revert_object, :revert_attribute], PaperTrail::Version do |version| @@ -178,7 +188,7 @@ class AdminAbility # Abilities for Role (Conference resource) can [:index, :show], Role do |role| - role.resource_type == 'Conference' + role.resource_type == 'Conference' || role.resource_type == 'Track' end # Can add or remove users from role, when user has that same role for the conference # Eg. If you are member of the CfP team, you can add more CfP team members (add users to the role 'CfP') @@ -211,7 +221,7 @@ class AdminAbility # Abilities for Role (Conference resource) can [:index, :show], Role do |role| - role.resource_type == 'Conference' + role.resource_type == 'Conference' || role.resource_type == 'Track' end # Can add or remove users from role, when user has that same role for the conference # Eg. If you are member of the CfP team, you can add more CfP team members (add users to the role 'CfP') @@ -234,7 +244,7 @@ class AdminAbility # Abilities for Role (Conference resource) can [:index, :show], Role do |role| - role.resource_type == 'Conference' + role.resource_type == 'Conference' || role.resource_type == 'Track' end # Can add or remove users from role, when user has that same role for the conference # Eg. If you are member of the CfP team, you can add more CfP team members (add users to the role 'CfP') @@ -243,4 +253,34 @@ class AdminAbility (Conference.with_role(:volunteers_coordinator, user).pluck(:id).include? role.resource_id) end end + + def signed_in_with_track_organizer_role(user) + # ids of all the conferences for which the user has the 'track organizer' role + conf_ids_for_track_organizer = Track.with_role(:track_organizer, user).joins(:program).pluck(:conference_id) + # ids of all the tracks for which the user has the 'track_organizer' role + track_ids_for_track_organizer = Track.with_role(:track_organizer, user).pluck(:id) + + can :show, Conference do |conf| + conf_ids_for_track_organizer.include?(conf.id) + end + + # Show Program in the admin sidebar + can :show, Program, conference_id: conf_ids_for_track_organizer + + # Show Tracks in the admin sidebar + can :update, Track do |track| + track.new_record? && conf_ids_for_track_organizer.include?(track.program.conference_id) + end + + can :manage, Track, id: track_ids_for_track_organizer + + # Show Roles in the admin sidebar and allow authorization of the index action + can [:index, :show], Role do |role| + role.resource_type == 'Conference' || role.resource_type == 'Track' + end + + can :toggle_user, Role do |role| + role.resource_type == 'Track' && track_ids_for_track_organizer.include?(role.resource_id) + end + end end diff --git a/app/models/track.rb b/app/models/track.rb index f65506d8..2b37d1bb 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -1,6 +1,10 @@ class Track < ActiveRecord::Base include RevisionCount + + resourcify :roles, dependent: :delete_all + belongs_to :program + belongs_to :submitter, class_name: 'User' has_many :events, dependent: :nullify has_paper_trail only: [:name, :description, :color], meta: { conference_id: :conference_id } @@ -14,13 +18,31 @@ class Track < ActiveRecord::Base uniqueness: { scope: :program } + validates :state, presence: true, if: :self_organized? + validates :cfp_active, inclusion: { in: [true, false] }, if: :self_organized? before_validation :capitalize_color + after_create :create_organizer_role, if: :self_organized? + def conference program.conference end + ## + # Checks if the track is self-organized + # ====Returns + # * +true+ -> If the track has a submitter + # * +false+ -> if the track doesn't have a submitter + def self_organized? + return true if submitter + false + end + + def to_param + short_name + end + private def generate_guid @@ -38,4 +60,10 @@ class Track < ActiveRecord::Base def conference_id program.conference_id end + + ## + # Creates the role of the track organizer + def create_organizer_role + Role.where(name: 'track_organizer', resource: self).first_or_create(description: 'For the organizers of the Track') + end end diff --git a/app/models/user.rb b/app/models/user.rb index 6ffb739f..e79d0b2a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -55,6 +55,7 @@ class User < ActiveRecord::Base has_many :votes, dependent: :destroy has_many :voted_events, through: :votes, source: :events has_many :subscriptions, dependent: :destroy + has_many :tracks, foreign_key: 'submitter_id' accepts_nested_attributes_for :roles scope :admin, -> { where(is_admin: true) } diff --git a/app/serializers/conference_serializer.rb b/app/serializers/conference_serializer.rb index a7e0c1da..7bb96131 100644 --- a/app/serializers/conference_serializer.rb +++ b/app/serializers/conference_serializer.rb @@ -60,7 +60,7 @@ class ConferenceSerializer < ActiveModel::Serializer def date_range if defined? date_string(object.start_date, object.end_date) - date_string(object.start_date, object.end_date).try(:split, ',').try(:first) + date_string(object.start_date, object.end_date).try(:split, ',').try(:first) end end end diff --git a/app/views/admin/roles/_form.html.haml b/app/views/admin/roles/_form.html.haml index eb10b23f..2204ef89 100644 --- a/app/views/admin/roles/_form.html.haml +++ b/app/views/admin/roles/_form.html.haml @@ -7,7 +7,7 @@ .text-muted = @role.description -= semantic_form_for @role, url: admin_conference_role_path(@conference.short_title, @role.name) do |f| += semantic_form_for @role, url: @url do |f| .row .col-md-5 = f.input :description diff --git a/app/views/admin/roles/_users.html.haml b/app/views/admin/roles/_users.html.haml index 9d73421b..f32c9a99 100644 --- a/app/views/admin/roles/_users.html.haml +++ b/app/views/admin/roles/_users.html.haml @@ -14,7 +14,7 @@ - if ( can? :toggle_user, @role ) %td.text-right = hidden_field_tag "role[user_ids][]", nil - = check_box_tag @conference.short_title, @role.id, (@role.user_ids.include? user.id), method: :post, url: "/admin/conferences/#{@conference.short_title}/roles/#{@role.name}/toggle_user?user[email]=#{user.email}&user[state]=", class: 'switch-checkbox', data: { size: 'small', off_color: 'warning', on_text: 'Yes', off_text: 'No' } + = check_box_tag @conference.short_title, @role.id, (@role.user_ids.include? user.id), method: :post, url: "#{@url}?user[email]=#{user.email}&user[state]=", class: 'switch-checkbox', data: { size: 'small', off_color: 'warning', on_text: 'Yes', off_text: 'No' } %td= user.id %td= user.name %td= user.email diff --git a/app/views/admin/roles/index.html.haml b/app/views/admin/roles/index.html.haml index c6e4c773..4a100025 100644 --- a/app/views/admin/roles/index.html.haml +++ b/app/views/admin/roles/index.html.haml @@ -19,13 +19,24 @@ %tr %td= role.id %td= role.name.titleize - %td= role.description + %td + = role.description + - if role.resource_type == 'Track' + = link_to role.resource.name, admin_conference_program_track_path(@conference.short_title, role.resource) %td = role.users.pluck(:name).first(5).join ', ' - if role.users.count > 5 - = link_to '...', admin_conference_role_path(@conference.short_title, role.name) + - if role.resource_type == 'Track' + = link_to '...', track_admin_conference_role_path(@conference.short_title, role.name, role.resource) + - else + = link_to '...', admin_conference_role_path(@conference.short_title, role.name) %td .btn-group - = link_to 'Users', admin_conference_role_path(@conference.short_title, role.name), class: 'btn btn-success' - - if can? :edit, role - = link_to 'Edit', edit_admin_conference_role_path(@conference.short_title, role.name), class: 'btn btn-primary' + - if role.resource_type == 'Track' + = link_to 'Users', track_admin_conference_role_path(@conference.short_title, role.name, role.resource), class: 'btn btn-success' + - if can? :edit, role + = link_to 'Edit', track_edit_admin_conference_role_path(@conference.short_title, role.name, role.resource), class: 'btn btn-primary' + - else + = link_to 'Users', admin_conference_role_path(@conference.short_title, role.name), class: 'btn btn-success' + - if can? :edit, role + = link_to 'Edit', edit_admin_conference_role_path(@conference.short_title, role.name), class: 'btn btn-primary' diff --git a/app/views/admin/roles/show.html.haml b/app/views/admin/roles/show.html.haml index 69d02f6b..ee249975 100644 --- a/app/views/admin/roles/show.html.haml +++ b/app/views/admin/roles/show.html.haml @@ -6,14 +6,20 @@ Role = @role.name.titleize - if can? :edit, @role - = link_to 'Edit', edit_admin_conference_role_path(@conference.short_title, @role.name), class: 'btn btn-primary pull-right' + - if @track + = link_to 'Edit', track_edit_admin_conference_role_path(@conference.short_title, @role.name, @track), class: 'btn btn-primary pull-right' + - else + = link_to 'Edit', edit_admin_conference_role_path(@conference.short_title, @role.name), class: 'btn btn-primary pull-right' .text-muted = @role.description + - if @track + = link_to @track.name, admin_conference_program_track_path(@conference.short_title, @track) + .row.col-md-3 - if ( can? :toggle_user, @role ) && !@role.new_record? - = semantic_form_for :user, url: toggle_user_admin_conference_role_path(@conference.short_title, @role.name), method: :post do |u| + = semantic_form_for :user, url: @url, method: :post do |u| = u.label 'Add user by email: ' .input-group diff --git a/app/views/admin/tracks/_form.html.haml b/app/views/admin/tracks/_form.html.haml index c3c7cf26..fe7f014a 100644 --- a/app/views/admin/tracks/_form.html.haml +++ b/app/views/admin/tracks/_form.html.haml @@ -8,9 +8,11 @@ Track .row .col-md-12 - = semantic_form_for(@track, url: (@track.new_record? ? admin_conference_program_tracks_path : admin_conference_program_track_path(@conference.short_title, @track.short_name))) do |f| + = semantic_form_for(@track, url: (@track.new_record? ? admin_conference_program_tracks_path : admin_conference_program_track_path(@conference.short_title, @track))) do |f| = f.input :name = f.input :short_name, hint: "A short and unique handle for the track, using only letters, numbers, underscores, and dashes. This will be used to identify the track in URLs etc. Example: 'my_awesome_track'", input_html: { required: 'required', pattern: '[a-zA-Z0-9_-]+', title: 'Only letters, numbers, underscores, and dashes.' } = f.input :color, input_html: {size: 6, type: 'color'}, required: true = f.input :description, input_html: {rows: 2, data: { provide: 'markdown-editable' } }, hint: markdown_hint + - if @track.self_organized? + = f.input :cfp_active, label: 'Allow event submitters to select this track for their proposal' = f.action :submit, as: :button, button_html: { class: 'btn btn-primary' } diff --git a/app/views/admin/tracks/index.html.haml b/app/views/admin/tracks/index.html.haml index f9ed9036..1cda360a 100644 --- a/app/views/admin/tracks/index.html.haml +++ b/app/views/admin/tracks/index.html.haml @@ -11,29 +11,55 @@ %th Name %th Short name %th Description + %th Submitter %th Color + %th State + %th Included in the Cfp %th Actions %tbody - @tracks.each do |track| %tr %td - = link_to(admin_conference_program_track_path(@conference.short_title, track.short_name)) do + = link_to(admin_conference_program_track_path(@conference.short_title, track)) do = track.name %td = track.short_name %td %p = truncate(track.description) + %td + - if track.self_organized? + = link_to track.submitter.name, admin_user_path(track.submitter) + - else + N/A %td %span.label{style: "background-color: #{track.color}; color: #{ contrast_color(track.color) }"} = track.color + %td + - if track.self_organized? + = track.state + - else + N/A + %td + - if track.self_organized? + = check_box_tag "#{@conference.short_title}_#{track.short_name}", track.id, track.cfp_active, + class: 'switch-checkbox', method: :patch, + url: toggle_cfp_inclusion_admin_conference_program_track_path(@conference.short_title, id: track.short_name)+"?included=", + data: { size: 'small', + on_color: 'success', + off_color: 'warning', + on_text: 'Yes', + off_text: 'No' } + - else + %i.fa.fa-check %td .btn-group{role: "group"} - = link_to 'Edit', edit_admin_conference_program_track_path(@conference.short_title, track.short_name), + = link_to 'Edit', edit_admin_conference_program_track_path(@conference.short_title, track), method: :get, class: 'btn btn-primary' - = link_to 'Delete', admin_conference_program_track_path(@conference.short_title, track.short_name), - method: :delete, class: 'btn btn-danger', - data: { confirm: "Do you really want to delete #{track.name}? Attention: This track will be removed from all Events that have it set" } + - if can? :destroy, track + = link_to 'Delete', admin_conference_program_track_path(@conference.short_title, track), + method: :delete, class: 'btn btn-danger', + data: { confirm: "Do you really want to delete #{track.name}? Attention: This track will be removed from all Events that have it set" } .row .col-md-12.text-right = link_to 'New Track', new_admin_conference_program_track_path(@conference.short_title), class: 'btn btn-success' diff --git a/app/views/tracks/_form.html.haml b/app/views/tracks/_form.html.haml new file mode 100644 index 00000000..0e48a851 --- /dev/null +++ b/app/views/tracks/_form.html.haml @@ -0,0 +1,17 @@ +.container + .row + .col-md-12 + .page-header + %h1 + - if @track.new_record? + New + = @track.name + Track + .row + .col-md-12 + = semantic_form_for(@track, url: (@track.new_record? ? conference_program_tracks_path : conference_program_track_path(@conference.short_title, @track))) do |f| + = f.input :name + = f.input :short_name, hint: "A short and unique handle for the track, using only letters, numbers, underscores, and dashes. This will be used to identify the track in URLs etc. Example: 'my_awesome_track'", input_html: { required: 'required', pattern: '[a-zA-Z0-9_-]+', title: 'Only letters, numbers, underscores, and dashes.' } + = f.input :color, input_html: {size: 6, type: 'color'}, required: true + = f.input :description, input_html: {rows: 2, data: { provide: 'markdown-editable' } }, required: true, hint: markdown_hint + = f.action :submit, as: :button, button_html: { class: 'btn btn-primary' } diff --git a/app/views/tracks/index.html.haml b/app/views/tracks/index.html.haml new file mode 100644 index 00000000..d4670bf4 --- /dev/null +++ b/app/views/tracks/index.html.haml @@ -0,0 +1,43 @@ +.container + .row + .col-md-12.page-header + %h1 + Track requests for + %span.notranslate + = @conference.title + + - if @tracks.any? + .row + .col-md-12 + %table.table.table-hover#tracks + %thead + %th Name + %th Short name + %th Description + %th Color + %th State + %th Actions + %tbody + - @tracks.each do |track| + %tr + %td + = link_to(conference_program_track_path(@conference.short_title, track)) do + = track.name + %td + = track.short_name + %td + %p + = truncate(track.description) + %td + %span.label{style: "background-color: #{track.color}; color: #{ contrast_color(track.color) }"} + = track.color + %td + = track.state + %td + = link_to 'Edit', edit_conference_program_track_path(@conference.short_title, track), + method: :get, class: 'btn btn-primary' + + .row + .col-md-12 + - if can? :create, @track + = link_to "New Track request", new_conference_program_track_path(@conference.short_title), class: 'btn btn-success pull-right' diff --git a/app/views/tracks/show.html.haml b/app/views/tracks/show.html.haml new file mode 100644 index 00000000..c08818eb --- /dev/null +++ b/app/views/tracks/show.html.haml @@ -0,0 +1,26 @@ +.container + .row + .col-md-12 + .page-header + %h1 + = @track.name + Track + .row + .col-md-8 + %dl.dl-horizontal + %dt + Color: + %dd + %span.label{style: "background-color: #{@track.color}; color: #{ contrast_color(@track.color) }"} + = @track.color + %dt + State: + %dd + = @track.state + %dt + Description + %dd + = @track.description + .row + .col-md-12.text-right + = link_to 'Edit Track request', edit_conference_program_track_path(@conference.short_title, @track), class: 'btn btn-primary' diff --git a/config/routes.rb b/config/routes.rb index db88613f..1643fc6b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,7 +54,11 @@ Osem::Application.routes.draw do resource :registration_period resource :program do resources :cfps - resources :tracks + resources :tracks do + member do + patch :toggle_cfp_inclusion + end + end resources :event_types resources :difficulty_levels resources :events do @@ -82,9 +86,15 @@ Osem::Application.routes.draw do resources :campaigns, except: [:show] resources :emails, only: [:show, :update, :index] resources :physical_ticket, only: [:index] - resources :roles, except: [ :new, :create ] do + resources :roles, only: [:edit] + resources :roles, except: [ :new, :create, :edit ] do member do post :toggle_user + get ':track_name' => 'roles#show', as: 'track' + get ':track_name/edit' => 'roles#edit', as: 'track_edit' + patch ':track_name' => 'roles#update' + put ':track_name' => 'roles#update' + post ':track_name/toggle_user' => 'roles#toggle_user', as: 'toggle_user_track' end end @@ -120,6 +130,7 @@ Osem::Application.routes.draw do patch '/restart' => 'proposals#restart' end end + resources :tracks, except: :destroy end # TODO: change conference_registrations to singular resource diff --git a/db/migrate/20140701123203_add_events_per_week_to_conference.rb b/db/migrate/20140701123203_add_events_per_week_to_conference.rb index 5c2eb9d4..36bbf40d 100644 --- a/db/migrate/20140701123203_add_events_per_week_to_conference.rb +++ b/db/migrate/20140701123203_add_events_per_week_to_conference.rb @@ -22,50 +22,50 @@ class AddEventsPerWeekToConference < ActiveRecord::Migration if event conference = TempConference.find_by_id(event.conference_id) if conference - week = event_version.created_at.end_of_week + week = event_version.created_at.end_of_week - no_events = { - new: 0, - withdrawn: 0, - unconfirmed: 0, - confirmed: 0, - canceled: 0, - rejected: 0, - } - - if !conference.events_per_week - conference.events_per_week = { - week => no_events + no_events = { + new: 0, + withdrawn: 0, + unconfirmed: 0, + confirmed: 0, + canceled: 0, + rejected: 0, } - elsif !conference.events_per_week[week] - conference.events_per_week[week] = no_events - end - if event_version.object_changes && - event_version.event == 'create' - - # Increment the new state - conference.events_per_week[week][:new] += 1 - elsif event_version.object_changes && - event_version.object_changes[:state] - - prev_state = event_version.object_changes[:state][0].to_sym - next_state = event_version.object_changes[:state][1].to_sym - - # Backward compatibility: deprecated state :review now :new - if prev_state == :review - prev_state = :new - elsif next_state == :review - next_state = :new + if !conference.events_per_week + conference.events_per_week = { + week => no_events + } + elsif !conference.events_per_week[week] + conference.events_per_week[week] = no_events end - # Increment the next state - conference.events_per_week[week][next_state] += 1 + if event_version.object_changes && + event_version.event == 'create' - # Decrement the previous state - conference.events_per_week[week][prev_state] -= 1 - end - conference.save + # Increment the new state + conference.events_per_week[week][:new] += 1 + elsif event_version.object_changes && + event_version.object_changes[:state] + + prev_state = event_version.object_changes[:state][0].to_sym + next_state = event_version.object_changes[:state][1].to_sym + + # Backward compatibility: deprecated state :review now :new + if prev_state == :review + prev_state = :new + elsif next_state == :review + next_state = :new + end + + # Increment the next state + conference.events_per_week[week][next_state] += 1 + + # Decrement the previous state + conference.events_per_week[week][prev_state] -= 1 + end + conference.save end end end diff --git a/db/migrate/20170705075039_add_state_cfp_active_and_submitter_reference_to_tracks.rb b/db/migrate/20170705075039_add_state_cfp_active_and_submitter_reference_to_tracks.rb new file mode 100644 index 00000000..9cb7f7de --- /dev/null +++ b/db/migrate/20170705075039_add_state_cfp_active_and_submitter_reference_to_tracks.rb @@ -0,0 +1,8 @@ +class AddStateCfpActiveAndSubmitterReferenceToTracks < ActiveRecord::Migration + def change + add_column :tracks, :state, :string + add_column :tracks, :cfp_active, :boolean + add_column :tracks, :submitter_id, :integer + add_index :tracks, :submitter_id + end +end diff --git a/db/schema.rb b/db/schema.rb index b9f8e3f4..f1a213a5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -483,16 +483,21 @@ ActiveRecord::Schema.define(version: 20170711102511) do end create_table "tracks", force: :cascade do |t| - t.string "guid", null: false - t.string "name", null: false + t.string "guid", null: false + t.string "name", null: false t.text "description" t.string "color" t.datetime "created_at" t.datetime "updated_at" t.integer "program_id" - t.string "short_name", null: false + t.string "short_name", null: false + t.string "state" + t.boolean "cfp_active" + t.integer "submitter_id" end + add_index "tracks", ["submitter_id"], name: "index_tracks_on_submitter_id" + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false diff --git a/lib/tasks/demo_data_for_development.rake b/lib/tasks/demo_data_for_development.rake index a86371b3..5be30c8e 100644 --- a/lib/tasks/demo_data_for_development.rake +++ b/lib/tasks/demo_data_for_development.rake @@ -4,37 +4,37 @@ namespace :data do include FactoryGirl::Syntax::Methods def generate_program conference - program = conference.program - user1 = create(:user) - user2 = create(:user) + program = conference.program + user1 = create(:user) + user2 = create(:user) - conference_rooms = conference.venue.rooms + conference_rooms = conference.venue.rooms - selected_schedule = create(:schedule, program: program) - demo_schedule = create(:schedule, program: program) - program.update_attributes!(selected_schedule: selected_schedule) + selected_schedule = create(:schedule, program: program) + demo_schedule = create(:schedule, program: program) + program.update_attributes!(selected_schedule: selected_schedule) - create(:event, program: program, title: 'Demo Event', abstract: 'This is a demo event instance whose state not defined.') - create(:event, program: program, title: 'Demo Rejected Event', state: 'rejected', abstract: 'This is demo event instance in a rejected state.') - create(:event, program: program, title: 'Demo Unconfirmed Event', state: 'unconfirmed', abstract: 'This is a demo event instance in unconfirmed state.') - create(:event, program: program, title: 'Demo Confirmed Unscheduled Event', state: 'confirmed', abstract: 'This is a demo event instance in a confirmed state.') + create(:event, program: program, title: 'Demo Event', abstract: 'This is a demo event instance whose state not defined.') + create(:event, program: program, title: 'Demo Rejected Event', state: 'rejected', abstract: 'This is demo event instance in a rejected state.') + create(:event, program: program, title: 'Demo Unconfirmed Event', state: 'unconfirmed', abstract: 'This is a demo event instance in unconfirmed state.') + create(:event, program: program, title: 'Demo Confirmed Unscheduled Event', state: 'confirmed', abstract: 'This is a demo event instance in a confirmed state.') - first_scheduled_event = create(:event, program: program, title: 'first_scheduled_event', state: 'confirmed', abstract: 'This is a demo scheduled event instance.') - second_scheduled_event = create(:event, program: program, title: 'second_scheduled_event', state: 'confirmed', abstract: 'This is a demo scheduled event instance.') - multiple_speaker_event = create(:event, program: program, title: 'multiple_speaker_event', state: 'confirmed', abstract: 'This is a demo scheduled event instance having multiple speakers.') + first_scheduled_event = create(:event, program: program, title: 'first_scheduled_event', state: 'confirmed', abstract: 'This is a demo scheduled event instance.') + second_scheduled_event = create(:event, program: program, title: 'second_scheduled_event', state: 'confirmed', abstract: 'This is a demo scheduled event instance.') + multiple_speaker_event = create(:event, program: program, title: 'multiple_speaker_event', state: 'confirmed', abstract: 'This is a demo scheduled event instance having multiple speakers.') - create(:event_user, event: multiple_speaker_event, user: user1, event_role: 'speaker') - create(:event_user, event: multiple_speaker_event, user: user2, event_role: 'speaker') + create(:event_user, event: multiple_speaker_event, user: user1, event_role: 'speaker') + create(:event_user, event: multiple_speaker_event, user: user2, event_role: 'speaker') - create(:event_schedule, event: first_scheduled_event, schedule: selected_schedule, start_time: conference.start_date + conference.start_hour.hours, room: conference_rooms.first) - create(:event_schedule, event: second_scheduled_event, schedule: selected_schedule, start_time: conference.start_date + conference.start_hour.hours + 15.minutes, room: conference_rooms.second) - create(:event_schedule, event: multiple_speaker_event, schedule: selected_schedule, start_time: conference.start_date + conference.start_hour.hours + 30.minutes, room: conference_rooms.third) - create(:event_schedule, event: first_scheduled_event, schedule: demo_schedule, start_time: conference.start_date + conference.start_hour.hours + 15.minutes, room: conference_rooms.third) - create(:event_schedule, event: second_scheduled_event, schedule: demo_schedule, start_time: conference.start_date + conference.start_hour.hours + 30.minutes, room: conference_rooms.third) - create(:event_schedule, event: multiple_speaker_event, schedule: demo_schedule, start_time: conference.start_date + conference.start_hour.hours, room: conference_rooms.first) + create(:event_schedule, event: first_scheduled_event, schedule: selected_schedule, start_time: conference.start_date + conference.start_hour.hours, room: conference_rooms.first) + create(:event_schedule, event: second_scheduled_event, schedule: selected_schedule, start_time: conference.start_date + conference.start_hour.hours + 15.minutes, room: conference_rooms.second) + create(:event_schedule, event: multiple_speaker_event, schedule: selected_schedule, start_time: conference.start_date + conference.start_hour.hours + 30.minutes, room: conference_rooms.third) + create(:event_schedule, event: first_scheduled_event, schedule: demo_schedule, start_time: conference.start_date + conference.start_hour.hours + 15.minutes, room: conference_rooms.third) + create(:event_schedule, event: second_scheduled_event, schedule: demo_schedule, start_time: conference.start_date + conference.start_hour.hours + 30.minutes, room: conference_rooms.third) + create(:event_schedule, event: multiple_speaker_event, schedule: demo_schedule, start_time: conference.start_date + conference.start_hour.hours, room: conference_rooms.first) - create(:registration, user: user1, conference: conference) - create(:registration, user: user2, conference: conference) + create(:registration, user: user1, conference: conference) + create(:registration, user: user2, conference: conference) end # This is a full conference demo instance that will happen in the future. @@ -73,5 +73,5 @@ namespace :data do # Registration for this conference has reached its limit. conference = create(:full_conference, title: 'Zypper Docker Conference', short_title: 'zypper', registration_limit: 2, start_date: 3.days.from_now, end_date: 7.days.from_now, start_hour: 7, end_hour: 19, description: 'This is a full conference demo instance. Its registrations has reached the limit.') generate_program conference - end + end end diff --git a/spec/controllers/admin/tracks_controller_spec.rb b/spec/controllers/admin/tracks_controller_spec.rb new file mode 100644 index 00000000..2177af2f --- /dev/null +++ b/spec/controllers/admin/tracks_controller_spec.rb @@ -0,0 +1,256 @@ +require 'spec_helper' + +describe Admin::TracksController do + let(:admin) { create(:admin) } + + let(:conference) { create(:conference) } + let!(:track) { create(:track, program: conference.program, color: '#800080') } + let!(:self_organized_track) { create(:track, :self_organized, program: conference.program) } + + before :each do + sign_in(admin) + end + + describe 'GET #index' do + before :each do + get :index, conference_id: conference.short_title + end + + it 'assigns @tracks with the correct values' do + expect(assigns(:tracks).length).to eq 2 + expect(assigns(:tracks).include?(track)).to eq true + expect(assigns(:tracks).include?(self_organized_track)).to eq true + end + + it 'renders the index template' do + expect(response).to render_template :index + end + end + + describe 'GET #show' do + before :each do + get :show, conference_id: conference.short_title, id: track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq track + end + + it 'renders the show template' do + expect(response).to render_template :show + end + end + + describe 'GET #new' do + before :each do + get :new, conference_id: conference.short_title + end + + it 'assigns a new track with the correct conference' do + expect(assigns(:track)).to be_a Track + expect(assigns(:track).new_record?).to eq true + expect(assigns(:track).program_id).to eq conference.program.id + end + + it 'renders the new template' do + expect(response).to render_template :new + end + end + + describe 'POST #create' do + context 'saves successfuly' do + before :each do + post :create, track: attributes_for(:track), conference_id: conference.short_title + end + + it 'assigns a new track with the correct conference' do + expect(assigns(:track)).to be_a Track + expect(assigns(:track).new_record?).to eq false + expect(assigns(:track).program_id).to eq conference.program.id + end + + it 'redirects to admin tracks index path' do + expect(response).to redirect_to admin_conference_program_tracks_path(conference_id: conference.short_title) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match('Track successfully created.') + end + + it 'creates new track' do + expect(Track.find(assigns(:track).id)).to be_a Track + end + end + + context 'save fails' do + before :each do + allow_any_instance_of(Track).to receive(:save).and_return(false) + post :create, track: attributes_for(:track, short_name: 'my_track'), conference_id: conference.short_title + end + + it 'assigns a new track with the correct conference' do + expect(assigns(:track)).to be_a Track + expect(assigns(:track).new_record?).to eq true + expect(assigns(:track).program_id).to eq conference.program.id + end + + it 'renders the new template' do + expect(response).to render_template :new + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Creating Track failed: #{assigns(:track).errors.full_messages.join('. ')}.") + end + + it 'does not create a new track' do + expect(conference.program.tracks.find_by(short_name: 'my_track')).to eq nil + end + end + end + + describe 'GET #edit' do + before :each do + get :edit, conference_id: conference.short_title, id: track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq track + end + + it 'renders the show template' do + expect(response).to render_template :edit + end + end + + describe 'PATCH #update' do + context 'updates successfully' do + before :each do + patch :update, track: attributes_for(:track, color: '#FF0000'), + conference_id: conference.short_title, + id: track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq track + end + + it 'redirects to admin tracks index path' do + expect(response).to redirect_to admin_conference_program_tracks_path(conference_id: conference.short_title) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match('Track successfully updated.') + end + + it 'updates the track' do + track.reload + expect(track.color).to eq '#FF0000' + end + end + + context 'update fails' do + before :each do + allow_any_instance_of(Track).to receive(:save).and_return(false) + patch :update, track: attributes_for(:track, color: '#FF0000'), + conference_id: conference.short_title, + id: track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq track + end + + it 'renders edit template' do + expect(response).to render_template :edit + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Track update failed: #{assigns(:track).errors.full_messages.join('. ')}.") + end + + it 'does not update the track' do + track.reload + expect(track.color).to eq '#800080' + end + end + end + + describe 'DELETE #destroy' do + context 'deletes successfully' do + before :each do + delete :destroy, conference_id: conference.short_title, id: track.short_name + end + + it 'redirects to admin tracks index path' do + expect(response).to redirect_to admin_conference_program_tracks_path(conference_id: conference.short_title) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match('Track successfully deleted.') + end + + it 'deletes the track' do + expect(Track.find_by(id: track)).to eq nil + end + end + + context 'delete fails' do + before :each do + allow_any_instance_of(Track).to receive(:destroy).and_return(false) + delete :destroy, conference_id: conference.short_title, id: track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq track + end + + it 'redirects to admin tracks index path' do + expect(response).to redirect_to admin_conference_program_tracks_path(conference_id: conference.short_title) + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Track couldn't be deleted. #{track.errors.full_messages.join('. ')}.") + end + + it 'does not delete the track' do + expect(Track.find(track.id)).to eq track + end + end + end + + describe 'PATCH #toggle_cfp_inclusion' do + context 'cfp_active is false' do + before :each do + self_organized_track.cfp_active = false + self_organized_track.save! + patch :toggle_cfp_inclusion, conference_id: conference.short_title, id: self_organized_track.short_name + self_organized_track.reload + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq self_organized_track + end + + it 'becomes true' do + expect(self_organized_track.cfp_active).to eq true + end + end + + context 'cfp_active is true' do + before :each do + self_organized_track.cfp_active = true + self_organized_track.save! + patch :toggle_cfp_inclusion, conference_id: conference.short_title, id: self_organized_track.short_name + self_organized_track.reload + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq self_organized_track + end + + it 'becomes false' do + expect(self_organized_track.cfp_active).to eq false + end + end + end +end diff --git a/spec/controllers/tracks_controller_spec.rb b/spec/controllers/tracks_controller_spec.rb new file mode 100644 index 00000000..373435c2 --- /dev/null +++ b/spec/controllers/tracks_controller_spec.rb @@ -0,0 +1,179 @@ +require 'spec_helper' + +describe TracksController do + # A regular user should be used when the track requests have been enabled + let(:user) { create(:admin) } + + let(:conference) { create(:conference) } + let!(:regular_track) { create(:track, program: conference.program) } + let!(:self_organized_track) { create(:track, :self_organized, program: conference.program, submitter: user, color: '#800080') } + + before :each do + sign_in(user) + end + + describe 'GET #index' do + before :each do + get :index, conference_id: conference.short_title + end + + it 'assigns @tracks with the correct values' do + expect(assigns(:tracks).length).to eq 1 + expect(assigns(:tracks).include?(regular_track)).to eq false + expect(assigns(:tracks).include?(self_organized_track)).to eq true + end + + it 'renders the index template' do + expect(response).to render_template :index + end + end + + describe 'GET #show' do + before :each do + get :show, conference_id: conference.short_title, id: self_organized_track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq self_organized_track + end + + it 'renders the show template' do + expect(response).to render_template :show + end + end + + describe 'GET #new' do + before :each do + get :new, conference_id: conference.short_title + end + + it 'assigns a new track with the correct conference' do + expect(assigns(:track)).to be_a Track + expect(assigns(:track).new_record?).to eq true + expect(assigns(:track).program_id).to eq conference.program.id + end + + it 'renders the new template' do + expect(response).to render_template :new + end + end + + describe 'POST #create' do + context 'saves successfuly' do + before :each do + post :create, track: attributes_for(:track, short_name: 'my_track'), conference_id: conference.short_title + end + + it 'redirects to tracks index path' do + expect(response).to redirect_to conference_program_tracks_path(conference_id: conference.short_title) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match('Track request successfully created.') + end + + it 'creates new track' do + expect(assigns(:track).new_record?).to eq false + end + + it 'the new tracks has the correct attributes' do + expect(assigns(:track).program_id).to eq conference.program.id + expect(assigns(:track).submitter).to eq user + expect(assigns(:track).state).to eq 'new' + expect(assigns(:track).cfp_active).to eq false + end + end + + context 'save fails' do + before :each do + allow_any_instance_of(Track).to receive(:save).and_return(false) + post :create, track: attributes_for(:track, short_name: 'my_track'), conference_id: conference.short_title + end + + it 'assigns a new track with the correct conference' do + expect(assigns(:track)).to be_a Track + expect(assigns(:track).new_record?).to eq true + expect(assigns(:track).program_id).to eq conference.program.id + end + + it 'renders the new template' do + expect(response).to render_template :new + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Creating Track request failed: #{assigns(:track).errors.full_messages.join('. ')}.") + end + + it 'does not create a new track' do + expect(conference.program.tracks.find_by(short_name: 'my_track')).to eq nil + end + end + end + + describe 'GET #edit' do + before :each do + get :edit, conference_id: conference.short_title, id: self_organized_track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq self_organized_track + end + + it 'renders the show template' do + expect(response).to render_template :edit + end + end + + describe 'PATCH #update' do + context 'updates successfully' do + before :each do + patch :update, track: attributes_for(:track, color: '#FF0000'), + conference_id: conference.short_title, + id: self_organized_track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq self_organized_track + end + + it 'redirects to tracks index path' do + expect(response).to redirect_to conference_program_tracks_path(conference_id: conference.short_title) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match('Track request successfully updated.') + end + + it 'updates the track' do + self_organized_track.reload + expect(self_organized_track.color).to eq '#FF0000' + end + end + + context 'update fails' do + before :each do + allow_any_instance_of(Track).to receive(:save).and_return(false) + patch :update, track: attributes_for(:track, color: '#FF0000'), + conference_id: conference.short_title, + id: self_organized_track.short_name + end + + it 'assigns the correct track' do + expect(assigns(:track)).to eq self_organized_track + end + + it 'renders edit template' do + expect(response).to render_template :edit + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Track request update failed: #{assigns(:track).errors.full_messages.join('. ')}.") + end + + it 'does not update the track' do + self_organized_track.reload + expect(self_organized_track.color).to eq '#800080' + end + end + end +end diff --git a/spec/factories/tracks.rb b/spec/factories/tracks.rb index 4334cb4c..091d8d07 100644 --- a/spec/factories/tracks.rb +++ b/spec/factories/tracks.rb @@ -5,5 +5,11 @@ FactoryGirl.define do color { Faker::Color.hex_color } short_name { SecureRandom.urlsafe_base64(5) } program + + trait :self_organized do + association :submitter, factory: :user + state 'new' + cfp_active false + end end end diff --git a/spec/features/track_organizer_ability_spec.rb b/spec/features/track_organizer_ability_spec.rb new file mode 100644 index 00000000..f79eba05 --- /dev/null +++ b/spec/features/track_organizer_ability_spec.rb @@ -0,0 +1,244 @@ +require 'spec_helper' + +feature 'Has correct abilities' do + + let(:organization) { create(:organization) } + let(:conference) { create(:full_conference, organization: organization) } + let(:self_organized_track) { create(:track, :self_organized, program: conference.program) } + let(:role_track_organizer) { Role.find_by(name: 'track_organizer', resource: self_organized_track) } + let(:user_track_organizer) { create(:user, role_ids: [role_track_organizer.id]) } + + context 'when user is info desk' do + before do + sign_in user_track_organizer + end + + scenario 'for organization and conference attributes' do + visit admin_conference_path(conference.short_title) + expect(current_path).to eq(admin_conference_path(conference.short_title)) + + expect(page).to have_selector('li.nav-header.nav-header-bigger a', text: 'Dashboard') + expect(page).to_not have_link('Basics', href: "/admin/conferences/#{conference.short_title}/edit") + expect(page).to have_text('Basics') + expect(page).to_not have_link('Contact', href: "/admin/conferences/#{conference.short_title}/contact/edit") + expect(page).to have_link('Commercials', href: "/admin/conferences/#{conference.short_title}/commercials") + expect(page).to_not have_link('Splashpage', href: "/admin/conferences/#{conference.short_title}/splashpage") + expect(page).to_not have_link('Venue', href: "/admin/conferences/#{conference.short_title}/venue") + expect(page).to_not have_link('Rooms', href: "/admin/conferences/#{conference.short_title}/venue/rooms") + expect(page).to_not have_link('Lodgings', href: "/admin/conferences/#{conference.short_title}/lodgings") + expect(page).to have_link('Program', href: "/admin/conferences/#{conference.short_title}/program") + expect(page).to_not have_link('Call for Papers', href: "/admin/conferences/#{conference.short_title}/program/cfps") + expect(page).to_not have_link('Events', href: "/admin/conferences/#{conference.short_title}/program/events") + expect(page).to have_link('Tracks', href: "/admin/conferences/#{conference.short_title}/program/tracks") + expect(page).to_not have_link('Event Types', href: "/admin/conferences/#{conference.short_title}/program/event_types") + expect(page).to_not have_link('Difficulty Levels', href: "/admin/conferences/#{conference.short_title}/program/difficulty_levels") + expect(page).to_not have_link('Schedules', href: "/admin/conferences/#{conference.short_title}/schedules") + expect(page).to_not have_link('Reports', href: "/admin/conferences/#{conference.short_title}/program/reports") + expect(page).to_not have_link('Registrations', href: "/admin/conferences/#{conference.short_title}/registrations") + expect(page).to_not have_link('Registration Period', href: "/admin/conferences/#{conference.short_title}/registration_period") + expect(page).to_not have_link('Questions', href: "/admin/conferences/#{conference.short_title}/questions") + expect(page).to_not have_text('Donations') + expect(page).to_not have_link('Sponsorship Levels', href: "/admin/conferences/#{conference.short_title}/sponsorship_levels") + expect(page).to_not have_link('Sponsors', href: "/admin/conferences/#{conference.short_title}/sponsors") + expect(page).to_not have_link('Tickets', href: "/admin/conferences/#{conference.short_title}/tickets") + expect(page).to_not have_text('Objectives') + expect(page).to_not have_link('Campaigns', href: "/admin/conferences/#{conference.short_title}/campaigns") + expect(page).to_not have_link('Goals', href: "/admin/conferences/#{conference.short_title}/targets") + expect(page).to_not have_link('E-Mails', href: "/admin/conferences/#{conference.short_title}/emails") + expect(page).to have_link('Roles', href: "/admin/conferences/#{conference.short_title}/roles") + expect(page).to_not have_link('Resources', href: "/admin/conferences/#{conference.short_title}/resources") + expect(page).to_not have_link('New Conference', href: '/admin/conferences/new') + + visit edit_admin_conference_path(conference.short_title) + expect(current_path).to eq root_path + + visit edit_admin_conference_contact_path(conference.short_title) + expect(current_path).to eq root_path + + visit admin_conference_commercials_path(conference.short_title) + expect(current_path).to eq admin_conference_commercials_path(conference.short_title) + + visit new_admin_conference_splashpage_path(conference.short_title) + expect(current_path).to eq root_path + + visit edit_admin_conference_splashpage_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_venue_path(conference.short_title) + expect(current_path).to eq root_path + + conference.venue = create(:venue) + visit edit_admin_conference_venue_path(conference.short_title) + expect(current_path).to eq root_path + + visit admin_conference_venue_rooms_path(conference.short_title) + expect(current_path).to eq root_path + + create(:room, venue: conference.venue) + visit edit_admin_conference_venue_room_path(conference.short_title, conference.venue.rooms.first) + expect(current_path).to eq root_path + + visit admin_conference_lodgings_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_lodging_path(conference.short_title) + expect(current_path).to eq root_path + + create(:lodging, conference: conference) + visit edit_admin_conference_lodging_path(conference.short_title, conference.lodgings.first) + expect(current_path).to eq root_path + + visit new_admin_conference_program_path(conference.short_title) + expect(current_path).to eq root_path + + visit edit_admin_conference_program_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_program_cfp_path(conference.short_title) + expect(current_path).to eq root_path + + visit edit_admin_conference_program_cfp_path(conference.short_title, conference.program.cfp) + expect(current_path).to eq root_path + + visit admin_conference_program_events_path(conference.short_title) + expect(current_path).to eq admin_conference_program_events_path(conference.short_title) + + create(:event, program: conference.program) + visit edit_admin_conference_program_event_path(conference.short_title, conference.program.events.first) + expect(current_path).to eq root_path + + visit admin_conference_program_event_types_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_program_event_type_path(conference.short_title) + expect(current_path).to eq root_path + + visit edit_admin_conference_program_event_type_path(conference.short_title, conference.program.event_types.first) + expect(current_path).to eq root_path + + visit admin_conference_program_difficulty_levels_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_program_difficulty_level_path(conference.short_title) + expect(current_path).to eq root_path + + visit edit_admin_conference_program_difficulty_level_path(conference.short_title, conference.program.difficulty_levels.first) + expect(current_path).to eq root_path + + visit admin_conference_schedules_path(conference.short_title) + expect(current_path).to eq root_path + + create(:schedule, program: conference.program) + visit admin_conference_schedule_path(conference.short_title, conference.program.schedules.first) + expect(current_path).to eq root_path + + visit admin_conference_program_reports_path(conference.short_title) + expect(current_path).to eq admin_conference_program_reports_path(conference.short_title) + + visit admin_conference_registrations_path(conference.short_title) + expect(current_path).to eq admin_conference_registrations_path(conference.short_title) + + create(:registration, user: create(:user), conference: conference) + visit edit_admin_conference_registration_path(conference.short_title, conference.registrations.first) + expect(current_path).to eq root_path + + visit new_admin_conference_registration_period_path(conference.short_title) + expect(current_path).to eq root_path + + create(:registration_period, conference: conference) + visit edit_admin_conference_registration_period_path(conference.short_title) + expect(current_path).to eq root_path + + visit admin_conference_questions_path(conference.short_title) + expect(current_path).to eq root_path + + visit admin_conference_sponsorship_levels_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_sponsorship_level_path(conference.short_title) + expect(current_path).to eq root_path + + create(:sponsorship_level, conference: conference) + visit edit_admin_conference_sponsorship_level_path(conference.short_title, conference.sponsorship_levels.first) + expect(current_path).to eq root_path + + visit admin_conference_sponsors_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_sponsor_path(conference.short_title) + expect(current_path).to eq root_path + + create(:sponsor, conference: conference, sponsorship_level: conference.sponsorship_levels.first) + visit edit_admin_conference_sponsor_path(conference.short_title, conference.sponsors.first) + expect(current_path).to eq root_path + + visit admin_conference_tickets_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_ticket_path(conference.short_title) + expect(current_path).to eq root_path + + create(:ticket, conference: conference) + visit edit_admin_conference_ticket_path(conference.short_title, conference.tickets.first) + expect(current_path).to eq root_path + + visit admin_conference_campaigns_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_campaign_path(conference.short_title) + expect(current_path).to eq root_path + + create(:campaign, conference: conference) + visit edit_admin_conference_campaign_path(conference.short_title, conference.campaigns.first) + expect(current_path).to eq root_path + + visit admin_conference_targets_path(conference.short_title) + expect(current_path).to eq root_path + + visit new_admin_conference_target_path(conference.short_title) + expect(current_path).to eq root_path + + create(:target, conference: conference) + visit edit_admin_conference_target_path(conference.short_title, conference.targets.first) + expect(current_path).to eq root_path + + visit admin_conference_program_tracks_path(conference.short_title) + expect(current_path).to eq admin_conference_program_tracks_path(conference.short_title) + + visit new_admin_conference_program_track_path(conference.short_title) + expect(current_path).to eq root_path + + other_track = create(:track, program: conference.program) + visit admin_conference_program_track_path(conference.short_title, other_track) + expect(current_path).to eq root_path + + visit edit_admin_conference_program_track_path(conference.short_title, other_track) + expect(current_path).to eq root_path + + visit admin_conference_program_track_path(conference.short_title, self_organized_track) + expect(current_path).to eq admin_conference_program_track_path(conference.short_title, self_organized_track) + + visit edit_admin_conference_program_track_path(conference.short_title, self_organized_track) + expect(current_path).to eq edit_admin_conference_program_track_path(conference.short_title, self_organized_track) + + visit admin_conference_roles_path(conference.short_title) + expect(current_path).to eq admin_conference_roles_path(conference.short_title) + + visit admin_conference_emails_path(conference.short_title) + expect(current_path).to eq root_path + + visit admin_conference_resources_path(conference.short_title) + expect(current_path).to eq admin_conference_resources_path(conference.short_title) + + visit new_admin_conference_resource_path(conference.short_title) + expect(current_path).to eq new_admin_conference_resource_path(conference.short_title) + + create(:resource, conference: conference) + visit edit_admin_conference_resource_path(conference.short_title, conference.resources.first) + expect(current_path).to eq root_path + + visit admin_revision_history_path + expect(current_path).to eq root_path + end + end +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 55f430f2..2144cec6 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -40,9 +40,9 @@ describe 'User' do it{ should_not be_able_to(:show, conference_not_public)} it do - conference_public.program.schedule_public = true - conference_public.program.save - should be_able_to(:schedule, conference_public) + conference_public.program.schedule_public = true + conference_public.program.save + should be_able_to(:schedule, conference_public) end it{ should_not be_able_to(:schedule, conference_not_public)} diff --git a/spec/models/admin_ability_spec.rb b/spec/models/admin_ability_spec.rb index 486ff295..9baae4a9 100644 --- a/spec/models/admin_ability_spec.rb +++ b/spec/models/admin_ability_spec.rb @@ -44,6 +44,8 @@ describe 'User with admin role' do let!(:my_event_schedule) { create(:event_schedule, schedule: my_schedule) } let!(:other_event_schedule) { create(:event_schedule, schedule: other_schedule) } + let!(:my_self_organized_track) { create(:track, :self_organized, program: my_conference.program) } + context 'user #is_admin?' do let(:venue) { my_conference.venue } let(:room) { create(:room, venue: venue) } @@ -69,6 +71,19 @@ describe 'User with admin role' do it{ should be_able_to(:show, Role.find_by(name: role, resource: other_conference)) } it{ should be_able_to(:index, Role.find_by(name: role, resource: other_conference)) } end + + context 'accesses track organizers' do + before :each do + other_self_organized_track = create(:track, :self_organized) + @other_track_organizer_role = Role.find_by(name: 'track_organizer', resource: other_self_organized_track) + end + + it{ should_not be_able_to(:toggle_user, @other_track_organizer_role) } + it{ should_not be_able_to(:update, @other_track_organizer_role) } + it{ should_not be_able_to(:edit, @other_track_organizer_role) } + it{ should be_able_to(:show, @other_track_organizer_role) } + it{ should be_able_to(:index, @other_track_organizer_role) } + end end shared_examples 'user with non-organizer role' do |role_name| @@ -83,6 +98,22 @@ describe 'User with admin role' do it{ should be_able_to(:show, Role.find_by(name: role, resource: my_conference)) } it{ should be_able_to(:index, Role.find_by(name: role, resource: my_conference)) } end + + context 'accesses track organizers' do + before :each do + @track_organizer_role = Role.find_by(name: 'track_organizer', resource: my_self_organized_track) + end + + if role_name == 'track_organizer' + it{ should be_able_to(:toggle_user, @track_organizer_role) } + else + it{ should_not be_able_to(:toggle_user, @track_organizer_role) } + end + it{ should_not be_able_to(:update, @track_organizer_role) } + it{ should_not be_able_to(:edit, @track_organizer_role) } + it{ should be_able_to(:show, @track_organizer_role) } + it{ should be_able_to(:index, @track_organizer_role) } + end end context 'when user has the role organization_admin' do @@ -186,6 +217,18 @@ describe 'User with admin role' do it{ should be_able_to(:index, Role.find_by(name: role, resource: my_conference)) } end + context 'can manage track organizers' do + before :each do + @track_organizer_role = Role.find_by(name: 'track_organizer', resource: my_self_organized_track) + end + + it{ should be_able_to(:toggle_user, @track_organizer_role) } + it{ should be_able_to(:edit, @track_organizer_role) } + it{ should be_able_to(:update, @track_organizer_role) } + it{ should be_able_to(:show, @track_organizer_role) } + it{ should be_able_to(:index, @track_organizer_role) } + end + it_behaves_like 'user with any role' end @@ -392,5 +435,74 @@ describe 'User with admin role' do it_behaves_like 'user with any role' it_behaves_like 'user with non-organizer role', 'volunteers_coordinator' end + + context 'when user has the role track_organizer' do + let(:role) { Role.find_by(name: 'track_organizer', resource: my_self_organized_track) } + let(:user) { create(:user, role_ids: [role.id]) } + let(:new_track) { build(:track, program: my_conference.program) } + + it{ should_not be_able_to(:new, Conference.new) } + it{ should_not be_able_to(:create, Conference.new) } + it{ should_not be_able_to(:manage, my_conference) } + it{ should_not be_able_to(:manage, conference_public) } + it{ should_not be_able_to(:manage, my_conference.splashpage) } + it{ should_not be_able_to(:manage, conference_public.splashpage) } + it{ should_not be_able_to(:manage, my_conference.contact) } + it{ should_not be_able_to(:manage, conference_public.contact) } + it{ should_not be_able_to(:manage, my_conference.email_settings) } + it{ should_not be_able_to(:manage, conference_public.email_settings) } + it{ should_not be_able_to(:manage, my_conference.campaigns.first) } + it{ should_not be_able_to(:manage, conference_public.campaigns.first) } + it{ should_not be_able_to(:manage, my_conference.targets.first) } + it{ should_not be_able_to(:manage, conference_public.targets.first) } + it{ should_not be_able_to(:manage, my_conference.commercials.first) } + it{ should_not be_able_to(:manage, conference_public.commercials.first) } + it{ should_not be_able_to(:manage, my_conference.registration_period) } + it{ should_not be_able_to(:manage, conference_public.registration_period) } + it{ should_not be_able_to(:manage, my_conference.questions.first) } + it{ should_not be_able_to(:manage, conference_public.questions.first) } + it{ should_not be_able_to(:manage, my_conference.program.cfp) } + it{ should_not be_able_to(:manage, conference_public.program.cfp) } + it{ should_not be_able_to(:manage, my_schedule) } + it{ should_not be_able_to(:manage, other_schedule) } + it{ should_not be_able_to(:manage, my_event_schedule) } + it{ should_not be_able_to(:manage, other_event_schedule) } + it{ should_not be_able_to(:manage, my_conference.venue) } + it{ should_not be_able_to(:show, my_conference.venue) } + it{ should_not be_able_to(:manage, conference_public.venue) } + it{ should_not be_able_to(:manage, my_conference.lodgings.first) } + it{ should_not be_able_to(:manage, conference_public.lodgings.first) } + it{ should_not be_able_to(:manage, my_conference.sponsors.first) } + it{ should_not be_able_to(:manage, conference_public.sponsors.first) } + it{ should_not be_able_to(:manage, my_conference.sponsorship_levels.first) } + it{ should_not be_able_to(:manage, conference_public.sponsorship_levels.first) } + it{ should_not be_able_to(:manage, my_conference.tickets.first) } + it{ should_not be_able_to(:manage, conference_public.tickets.first) } + + it{ should_not be_able_to(:manage, registration) } + it{ should_not be_able_to(:manage, other_registration) } + + it{ should_not be_able_to(:manage, my_event) } + it{ should_not be_able_to(:manage, other_event) } + it{ should_not be_able_to(:manage, my_event.event_type) } + it{ should_not be_able_to(:manage, other_event.event_type) } + it{ should_not be_able_to(:manage, my_event.track) } + it{ should_not be_able_to(:manage, other_event.track) } + it{ should_not be_able_to(:manage, my_event.difficulty_level) } + it{ should_not be_able_to(:manage, other_event.difficulty_level) } + it{ should_not be_able_to(:manage, my_event.commercials.first) } + it{ should_not be_able_to(:manage, other_event.commercials.first) } + it{ should_not be_able_to(:index, my_event.comment_threads.first) } + it{ should_not be_able_to(:index, other_event.comment_threads.first) } + + it{ should_not be_able_to(:manage, resource) } + + it{ should be_able_to(:show, my_conference.program) } + it{ should be_able_to(:update, new_track) } + it{ should be_able_to(:manage, my_self_organized_track) } + + it_behaves_like 'user with any role' + it_behaves_like 'user with non-organizer role', 'track_organizer' + end end end diff --git a/spec/models/track_spec.rb b/spec/models/track_spec.rb new file mode 100644 index 00000000..840641cb --- /dev/null +++ b/spec/models/track_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Track do + subject { create(:track) } + let(:track) { create(:track) } + let(:self_organized_track) { create(:track, :self_organized) } + + describe 'association' do + it { is_expected.to belong_to(:program) } + it { is_expected.to belong_to(:submitter).class_name('User') } + it { is_expected.to have_many(:events) } + end + + describe 'validation' do + it 'has a valid factory' do + expect(build(:track)).to be_valid + end + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('#ABCDEF').for(:color) } + it { is_expected.to allow_value('#124689').for(:color) } + it { is_expected.to validate_presence_of(:short_name) } + it { is_expected.to allow_value('My_track_name').for(:short_name) } + it { is_expected.to_not allow_value('My track name').for(:short_name) } + it { is_expected.to validate_uniqueness_of(:short_name).scoped_to(:program_id) } + + context 'when self-organized' do + before :each do + allow(subject).to receive(:self_organized?).and_return(true) + end + + it { is_expected.to validate_presence_of(:state) } + it { is_expected.to validate_inclusion_of(:cfp_active).in_array([true, false]) } + end + + context 'when regular' do + before :each do + allow(subject).to receive(:self_organized?).and_return(false) + end + + it { is_expected.to_not validate_presence_of(:state) } + it { is_expected.to_not validate_inclusion_of(:cfp_active) } + end + end + + describe '#self_organized?' do + it 'returns true when it has a submitter' do + expect(self_organized_track.submitter).to be_a User + expect(self_organized_track.self_organized?).to eq true + end + + it 'returns false when it doesn\'t have a submitter' do + expect(track.submitter).to eq nil + expect(track.self_organized?).to eq false + end + end +end