Skip to content

Latest commit

 

History

History
320 lines (230 loc) · 8.05 KB

step15.md

File metadata and controls

320 lines (230 loc) · 8.05 KB

Adding Students

Students will be added as a separate class by uploading a datafile. Each student can be assigned to multiple Deliveries and thus be issued several Certificates.

A user story:

As a course administrator
In order to be able to issue course certificates
I want to be able to import a datafile with student information

We start by creating a new feature file in the features folder. Call it student_data_import.feature.

We will be doing this in small increments so we will not define all steps at once but rather work our way through the scenario in parts, doing new steps as we move along. Let's start with these initial steps:

# features/student_data_import.feature

Scenario: Data file upload
  Given the delivery for the course "Basic" is set to "2015-12-01"
  And I am on the Course index page
  And I click on "2015-12-01" for the "Basic programming" Course
  Then I should be on 2015-12-01 show page

Add the following step to your application_steps.rb

# features/step_definitions/application_steps.rb

Given(/^the delivery for the course "([^"]*)" is set to "([^"]*)"$/) do |name, date|
  steps %(
    Given the course "#{name}" is created
    And I am on the Course index page
    And I click on "Add Delivery date" for the "#{name}" Course
    And I fill in "Start" with "#{date}"
    And I click "Submit" link
  )
end

And in the support/paths.rb we have to configure the path for Cucumber to know where to go.

# features/support/paths.rb

...
when /^(.*) show page$/i
  d = Delivery.find(date: $1).first
  "/courses/deliveries/show/#{d.id}"
...

And lastly update your course/index.erb file as follows:

# lib/views/courses/index.erb

<!-- # replace  -->
<%= date.start_date %>

<!-- # with  -->
<%= link_to date.start_date, "/courses/deliveries/show/#{date.id}" %>

In our controller, we need to define that route as well:

# lib/application.rb

...
get '/courses/deliveries/show/:id', auth: :user do
  @delivery = Delivery.get(params[:id].to_i)
  erb :'courses/deliveries/show'
end

post '/courses/deliveries/file_upload' do
  # TODO: Add method to parse csv
end
...

In that route we are pointing to an erb template that is still missing (you'll see that when you run cucumber, right?)

We need to create a new folder and a new erb file. Can you figure out how?

# lib/views/courses/deliveries/show.erb

<h2><%= @delivery.course.title %></h2>

<% form_tag '/courses/deliveries/file_upload', method: :post, multipart: true do %>
  <%= hidden_field_tag :id, value: @delivery.id %>
  <%= file_field_tag :file %>
  <%= submit_tag 'Submit' %>
<% end %>

If you run cucumber at this point, all the steps should be green.

Moving on. Let's add some more steps to our scenario.

# features/student_data_import.feature

Scenario: Data file upload
  Given the delivery for the course "Basic" is set to "2015-12-01"
  And I am on the Course index page
  And I click on "2015-12-01" for the "Basic programming" Course
  Then I should be on 2015-12-01 show page
  When I select the "students.csv" file
  And I click "Submit" link
  Then 3 instances of "Student" should be created

We need to add two new step definitions:

# features/step_definitions/application_steps.rb

...

When(/^I select the "([^"]*)" file$/) do |file_name|
  attach_file('file', File.absolute_path("./features/fixtures/#{file_name}"))
end

Then(/^([^"]*) instances of "([^"]*)" should be created$/) do |count, model|
  expect(Object.const_get(model).count).to eq count.to_i
end

To save some time, use the following two commands to create a fixture folder (where we will store some dummy data) and an empty students.csv:

$ mkdir features/fixtures
$ touch features/fixtures/students.csv

Now run your tests again and you'll see that you have moved forward a bit but also that you can not move any further without defining a new Student class. Time for some RSpec and unit testing.

We start by creating a student_spec.rb file in the spec folder and add a few expectations:

# spec/student_spec.rb

describe Student do

  it { is_expected.to have_property :id }
  it { is_expected.to have_property :full_name }
  it { is_expected.to have_property :email }

  it { is_expected.to have_many_and_belong_to :deliveries }
end

And we need to, of course, create a Student class:

# lib/student.rb

class Student
  include DataMapper::Resource

  property :id, Serial
  property :full_name, String
  property :email, String

  has n, :deliveries, through: Resource
end

Don't forget to include the Student class in your controller:

# lib/application.rb

...
require './lib/student'
...

Run the student_spec.rb:

$ rspec spec/student_spec.rb

The specs should pass.

Time to add a module to parse the uploaded file. In your lib folder, create a file named csv_parse.rb. There we will place some logic on how to parse a data file and create Student objects.

# lib/csv_parse.rb

require 'csv'

module CSVParse
  def self.import(file, obj, parent)
    import = CSV.read(file, quote_char: '"',
                      col_sep: ';',
                      row_sep: :auto,
                      headers: true,
                      header_converters: :symbol,
                      converters: :all).collect do |row|
      Hash[row.collect { |c, r| [c, r] }]
    end
    CSVParse.create_instance(parent, obj, import)
  end

  def self.create_instance(parent, obj, dataset)
    dataset.each do |data|
      student = obj.first_or_create({full_name: data[:full_name]}, {full_name: data[:full_name], email: data[:email]})
      student.deliveries << parent unless student.deliveries.include? parent
      student.save
    end
  end
end

Now, let's go back to our controller.

You have to require and include the module (look closely at the code below) and update your post request route with this code:

# lib/application.rb

...
require './lib/csv_parse'

class WorkshopApp < Sinatra::Base
  include CSVParse
...

...
post '/courses/deliveries/file_upload' do
  @delivery = Delivery.get(params[:id])
  CSVParse.import(params[:file][:tempfile], Student, @delivery)
  redirect "/courses/deliveries/show/#{@delivery.id}"
end
...

Add some content to your fixture file, the students.csv:

# features/fixtures/students.csv

full_name; email
Thomas Ochman;[email protected]
Anders Andersson;[email protected]
Kalle Karlsson;[email protected]

Now, if you run the scenario, all the steps should pass.

Add another few steps to your scenario:

# features/student_data_import.feature

Scenario: Data file upload
  Given the delivery for the course "Basic" is set to "2015-12-01"
  And I am on the Course index page
  And I click on "2015-12-01" for the "Basic programming" Course
  Then I should be on 2015-12-01 show page
  When I select the "students.csv" file
  And I click "Submit" link
  Then 3 instances of "Student" should be created
  Then I should be on 2015-12-01 show page
  And I should see "Students:"
  And I should see "Thomas Ochman"
  And I should see "Anders Andersson"
  And I should see "Kalle Karlsson"

We need to add an relationship between Delivery and Student for the Delivery to know what Students it is associated with. It needs to do that so we can display that information on the page after we've uploaded and parsed the data file.

We start by adding a spec for that relation:

# spec/delivery_spec.rb

it { is_expected.to have_many_and_belong_to :students }

And we add that relation to our Delivery class:

# lib/delivery.rb

...
has n, :students, through: Resource

Finally, add the following code to the show.erb template:

# lib/views/courses/deliveries/show.erb

...
<div>
  <% if @delivery.students.any? %>
    Students:
    <% @delivery.students.each do |student| %>
      <%= [student.full_name, ''].join(' ') %>
    <% end %>
  <% end %>
</div>

Run all your specs and features. You should see a lot of green on your screen. That is always a good sign. ;-)

Next, we will be creating the actual certificates. Exciting?