diff --git a/COMMENTS.md b/COMMENTS.md new file mode 100644 index 0000000..09014f3 --- /dev/null +++ b/COMMENTS.md @@ -0,0 +1,4 @@ +# on view specs +View specs are good for verifying page-elements and structure. However if we're doing page_objects as well we might as well use those rather than duplicating the effort. +After all we put most of the structure into the page-object. If we had a way to specify what elements are always there (default in site-prism is ALL) we could easily make 90% of our view +spec a simple verification of the page object `@page.all_there?`. This of course doesn't cover logic in the view but that's trivial with the page-object too. diff --git a/Gemfile b/Gemfile index 35e0fab..c8e7c33 100644 --- a/Gemfile +++ b/Gemfile @@ -50,9 +50,6 @@ group :development, :test do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 2.0' - # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring - gem 'spring' - gem 'rspec-rails', '~> 3.0' gem 'factory_girl_rails' # :development to help out in rails generators diff --git a/Gemfile.lock b/Gemfile.lock index 4b96e6a..c9fb452 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -232,7 +232,6 @@ GEM addressable (>= 2.3.3, < 3.0) capybara (>= 2.1, < 3.0) slop (3.6.0) - spring (1.3.6) sprockets (3.3.3) rack (~> 1.0) sprockets-rails (2.3.2) @@ -294,7 +293,6 @@ DEPENDENCIES selenium-webdriver shoulda-matchers site_prism - spring sqlite3 turbolinks uglifier (>= 1.3.0) diff --git a/Guardfile b/Guardfile index 60e3eae..9902825 100644 --- a/Guardfile +++ b/Guardfile @@ -24,7 +24,7 @@ # * zeus: 'zeus rspec' (requires the server to be started separately) # * 'just' rspec: 'rspec' -guard :rspec, cmd: "bundle exec rspec" do +guard :rspec, cmd: "zeus rspec" do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) @@ -72,7 +72,8 @@ guard :rspec, cmd: "bundle exec rspec" do end end -guard 'zeus' do +# see issue: https://github.com/guard/guard-zeus/issues/30 +guard 'zeus', run_all: false, rspec: false do require 'ostruct' rspec = OpenStruct.new @@ -86,11 +87,11 @@ guard 'zeus' do # Ruby apps ruby = OpenStruct.new ruby.lib_files = /^(lib\/.+)\.rb$/ - - watch(rspec.spec_files) - watch(rspec.spec_helper) { rspec.spec_dir } - watch(ruby.lib_files) { |m| rspec.spec.call(m[1]) } - +# see issue: https://github.com/guard/guard-zeus/issues/30 +# watch(rspec.spec_files) +# watch(rspec.spec_helper) { rspec.spec_dir } +# watch(ruby.lib_files) { |m| rspec.spec.call(m[1]) } + # Rails example rails = OpenStruct.new rails.app_files = /^app\/(.+)\.rb$/ diff --git a/README.rdoc b/README.md similarity index 83% rename from README.rdoc rename to README.md index a2f4fc9..551d0f2 100644 --- a/README.rdoc +++ b/README.md @@ -1,12 +1,14 @@ -== README +# README Helps teaching staff determine the best times for office hours by allowing students to input the times they cannot (or can) come to office hours. Teaching staff can then elect to hold their office hours at times when more students are available. This maximizes the number of students able to get help and decreases the time wasted in office hours when no students attend. -== OTHER +# OTHER This repo is also a repository for experiments with FullCalendar (http://fullcalendar.io/) and time conversion issues in Rails and moment.js. In addition it contains an example of using site-prism and capybara with fullcalendar. -== LICENSE +See also [comments](COMMENTS.md) for interesting discoveries made throughout the project. + +# LICENSE MIT diff --git a/app/assets/images/calendar.png b/app/assets/images/calendar.png new file mode 100644 index 0000000..a1b33f0 Binary files /dev/null and b/app/assets/images/calendar.png differ diff --git a/app/assets/stylesheets/courses.scss b/app/assets/stylesheets/courses.scss index 0cae8cc..e4795a5 100644 --- a/app/assets/stylesheets/courses.scss +++ b/app/assets/stylesheets/courses.scss @@ -1,3 +1,8 @@ // Place all the styles related to the courses controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ + +img.mini-cal { + height: 100px; + margin-left: 20px; +} diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index 2f56b18..a015684 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -10,6 +10,32 @@ def as_a_student(courses) as_a('student', courses) end + + #take a course and return something like: + # { students: [ { email: 'g@g.com', created_at ...} , ], + # educators: [ { email... } ], + # others: [] + # } + def user_courses_by_type(course) + students = [] + educators = [] + others = [] + course.course_participants.each do |cp| + case cp.role.role_name + when 'student' + students << cp.user + when 'educator' + educators << cp.user + else + others << cp.user + end + end + { students: students, + educators: educators, + others: others + } + end + private def as_a(role_name, courses) @@ -18,3 +44,5 @@ def as_a(role_name, courses) end end end + + diff --git a/app/views/courses/_participants_list.html.erb b/app/views/courses/_participants_list.html.erb new file mode 100644 index 0000000..6d1f786 --- /dev/null +++ b/app/views/courses/_participants_list.html.erb @@ -0,0 +1,20 @@ +<% users = user_courses_by_type(@course).delete_if { |k,v| v.blank? } %> +<% users.keys.each do |type| %> +

<%= type %>

+ + + + + + + + + <% users[type.to_sym].each do |participant| %> + + + + <% end %> + +
email
<%= participant[:email] %>
+<% end %> + diff --git a/app/views/courses/edit.html.erb b/app/views/courses/edit.html.erb index 4e3e3a1..5873b2d 100644 --- a/app/views/courses/edit.html.erb +++ b/app/views/courses/edit.html.erb @@ -1,6 +1,23 @@

Editing Course

+

Course information

<%= render 'form' %> +
+

Scheduling

+

Optimal meeting times (calculated)

+ <%=image_tag('calendar.png', alt: 'optimal meeting times', class: 'mini-cal') %> +

Edit my available times

+ <%= link_to course_calendar_path(@course) do %> + <%=image_tag('calendar.png', alt: 'optimal meeting times', class: 'mini-cal') %> + <% end %> +
+ +
+

Course Participants

+ <%= render 'participants_list' %> +
<%= link_to 'Show', @course %> | <%= link_to 'Back', courses_path %> + + diff --git a/bin/rails b/bin/rails index 4d608ed..5191e69 100755 --- a/bin/rails +++ b/bin/rails @@ -1,8 +1,4 @@ #!/usr/bin/env ruby -begin - load File.expand_path("../spring", __FILE__) -rescue LoadError -end APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/rake b/bin/rake index 8017a02..1724048 100755 --- a/bin/rake +++ b/bin/rake @@ -1,8 +1,4 @@ #!/usr/bin/env ruby -begin - load File.expand_path("../spring", __FILE__) -rescue LoadError -end require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/bin/spring b/bin/spring deleted file mode 100755 index 7b45d37..0000000 --- a/bin/spring +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env ruby - -# This file loads spring without using Bundler, in order to be fast. -# It gets overwritten when you run the `spring binstub` command. - -unless defined?(Spring) - require "rubygems" - require "bundler" - - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) - Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } - gem "spring", match[1] - require "spring/binstub" - end -end diff --git a/spec/factories/course_participants.rb b/spec/factories/course_participants.rb index 0b4c630..0f86d9d 100644 --- a/spec/factories/course_participants.rb +++ b/spec/factories/course_participants.rb @@ -2,6 +2,7 @@ factory :course_participant do role course + user trait :student do association :role, factory: :student_role @@ -11,6 +12,10 @@ association :role, factory: :educator_role end + trait :with_new_user do + association :user, factory: :user + end + factory :student_participant, traits: [:student] factory :educator_participant, traits: [:educator] end diff --git a/spec/factories/courses.rb b/spec/factories/courses.rb index 9d69bac..738ecb3 100644 --- a/spec/factories/courses.rb +++ b/spec/factories/courses.rb @@ -18,5 +18,39 @@ cp.save! end end + + # huge factory that generates an entire stack of courses with users and roles + # please only use when really needed and in integration tests + factory :course_with_many_users do + transient { educators 2 } + transient { students 3 } + + after(:create) do |course, evaluator| + educator_users = create_list(:educator_participant, + evaluator.educators.to_i, + :with_new_user, + course: course + ) + student_users = create_list(:student_participant, + evaluator.students.to_i, + :with_new_user, + course: course + ) + end + end + end + # build up a full deep hash from courses to users and roles + factory :course_with_many_users_hash, class: Hash do + transient { title 'Rocket Science' } + after(:build) do |course, evaluator| + participants = FactoryGirl.attributes_for_list(:student_role, 7) + participants += FactoryGirl.attributes_for_list(:educator_role, 2) + users = attributes_for_list(:user,9) + participants.each_with_index do |part, index| + part[:user] = users[index] + end + course[:course_participants] = participants + course[:title] = evaluator.title + end end end diff --git a/spec/features/courses_editing_feature_spec.rb b/spec/features/courses_editing_feature_spec.rb new file mode 100644 index 0000000..ce199b5 --- /dev/null +++ b/spec/features/courses_editing_feature_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +RSpec.feature "Editing courses", + %q{ + As a teacher or student, + I want to edit the courses I'm involved in, +}, + :js do + + + given!(:course) { FactoryGirl.create(:course_with_many_users, students: 3) } # a huge factory + given(:teacher) { course.course_participants.where( role: Role.educator ).first.user } + given(:one_student) { course.course_participants.where(role: Role.student)[2].user } + + before do + UsersSignInPage.sign_in_user(teacher.email) + @p = CoursesIndexPage.new + @p.load + @p.teaching_courses.first.edit_link.click + @p = CoursesEditPage.new + end + + it 'displays the page elements' do # verify the page-object ? + expect(@p).to be_displayed + expect(@p).to be_all_there + end + + scenario "changing the course name" do + @p.title_field.set 'Saving Christmas' + @p.update_button.click + @p = CoursesIndexPage.new + # below is not implemented + #expect(@p).to be_displayed + #expect(@p.teaching_courses_table).to have_content 'Saving Christmas' + # instead we'll just check this for now: + expect(page).to have_content 'Saving Christmas' + end + + scenario 'shows us the course participants broken into educators and students' do + expect(@p.students.count).to eq 3 + expect(@p.educators.count).to eq 2 + end + + scenario 'can proceed to view my calendar' do + @p.edit_calendar_link.click + @p = CourseParticipantsPage.new + expect(@p).to be_displayed + end +end diff --git a/spec/helpers/courses_helper_spec.rb b/spec/helpers/courses_helper_spec.rb index 7434d9e..98cfdb6 100644 --- a/spec/helpers/courses_helper_spec.rb +++ b/spec/helpers/courses_helper_spec.rb @@ -71,4 +71,26 @@ expect(res.count).to eq 3 end end + + describe '#user_courses_by_role' do + let(:student) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:user) } + let(:course) { FactoryGirl.create(:course_for_user, user: student) } + before { FactoryGirl.create(:educator_participant, course: course, user: teacher) } + subject(:result) { helper.user_courses_by_type(course) } + it 'returns a hash with keys :students, :teachers, :other' do + expect(result).to be_a Hash + expect(result.keys) =~ [ :students, :teachers, :other ] + end + it 'has an array of students in the students hash' do + expect(result[:students]).to be_an Array + expect(result[:students].first.email).to eq student.email + end + + it 'has an array of educators under the educators keys' do + expect(result[:educators]).to be_an Array + expect(result[:educators].first.email).to eq teacher.email + end + + end end diff --git a/spec/page_objects/pages/course_participants_page.rb b/spec/page_objects/pages/course_participants_page.rb index 3bc36cf..81b7f7d 100644 --- a/spec/page_objects/pages/course_participants_page.rb +++ b/spec/page_objects/pages/course_participants_page.rb @@ -1,6 +1,8 @@ class CourseParticipantsPage < SitePrism::Page # set_url 'course_participants/{:id}/' set_url '/courses/{:course_id}/calendar' + set_url_matcher /courses.*\/calendar/ + section :calendar, WeekCalendarSection, '#calendar' element :save_button, 'input[name="commit"]' @@ -17,3 +19,4 @@ def self.navigate_here page.navigate_here end end + diff --git a/spec/page_objects/pages/courses_edit_page.rb b/spec/page_objects/pages/courses_edit_page.rb new file mode 100644 index 0000000..4704857 --- /dev/null +++ b/spec/page_objects/pages/courses_edit_page.rb @@ -0,0 +1,14 @@ +class CoursesEditPage < SitePrism::Page + set_url '/courses/{course_id}/edit' + set_url_matcher /courses.*\/edit/ + element :title_field, 'input#course_title' + element :update_button, 'input[name="commit"]' + + element :edit_calendar_link, 'div#scheduling a' + element :educators_table, 'table.type-educators' + elements :educators, 'table.type-educators tbody tr' + + element :students_table, 'table.type-students' + elements :students, 'table.type-students tbody tr' + +end diff --git a/spec/support/capybara_string.rb b/spec/support/capybara_string.rb new file mode 100644 index 0000000..a58d54d --- /dev/null +++ b/spec/support/capybara_string.rb @@ -0,0 +1,4 @@ +def capybara(some_str=nil) + some_str = rendered if some_str.nil? + Capybara.string(some_str) +end diff --git a/spec/views/courses/edit.html.erb_spec.rb b/spec/views/courses/edit.html.erb_spec.rb index 78d974d..b9402a0 100644 --- a/spec/views/courses/edit.html.erb_spec.rb +++ b/spec/views/courses/edit.html.erb_spec.rb @@ -2,17 +2,69 @@ RSpec.describe "courses/edit", type: :view do before(:each) do - @course = assign(:course, Course.create!( - :title => "MyString" - )) + @course = FactoryGirl.build_stubbed(:course, title: 'MyString') + assign :course, @course + users = { students: FactoryGirl.attributes_for_list(:user, 7), + educators: FactoryGirl.attributes_for_list(:user, 2) + } + allow(view).to receive(:user_courses_by_type).and_return(users) end it "renders the edit course form" do render - assert_select "form[action=?][method=?]", course_path(@course), "post" do - assert_select "input#course_title[name=?]", "course[title]" end end + + it 'has a Scheduling section' do + render + assert_select 'div#scheduling' do + assert_select 'h2', text: 'Scheduling' + end + end + + it 'has a Participants list' do + render + + assert_select 'div#participants' do + assert_select 'h2', text: 'Course Participants' + end + end + + describe 'scheduling section' do + it 'has two calendar images' do + render + assert_select 'div#scheduling img', count: 2 + end + it 'has a link to calendar page' do + render + expect(capybara.find('div#scheduling a')['href']).to match course_calendar_path(@course) + end + end + + describe 'participants section' do + + it 'has a list of students' do + render + assert_select 'div#participants' do + assert_select 'h3', text: 'students' + assert_select 'table.type-students tbody tr', count: 7 + end + end + it 'has a list of teachers' do + render + assert_select 'div#participants' do + assert_select 'h3', text: 'educators' + assert_select 'table.type-educators tbody tr', count: 2 + end + end + it "doesn't display sections for which there are no entries" do + allow(view).to receive(:user_courses_by_type).and_return({students: [], other: []}) + render + + expect(rendered).to_not have_selector('table.type-students') + expect(rendered).to_not have_selector('table.type-educators') + end + end end