====== Legacy Records Import ======
===== Objective =======
To facilitate an upload of legacy data into Learnexa in back-end. A simple interactive task will upload legacy data such as User, Course & Enrolment into Learnexa. Uploaded records will automatically be activated or qualified upon creation.
===== Assumptions =======
* No web interface to upload the file
* An interactive rake task will take inputs to decide upon upload type and data
* Each row's execution status get recorded in a status file
* Any invalid or error records will be skipped in the import process
* All existing business rules will be followed while importing legacy records
* No email notifications in the process of uploading
===== Purpose =======
A back-end task to upload legacy User/Course/Enrolment records into Learnexa. The legacy CSV data file is used as a source list for the import process. Uploaded records will get validated as per existing business rules and automatically be activated. Learnexa will not send the usual activation email notification to the user.
Status of each imported rows gets recorded in an output file for reference. The output status will be Completed or Failed.
=== Input File Format ===
Input files should be a valid CSV file. Having column header as the first row.
**User fields:**
* Email Address
* First Name
* Last Name
* Password
* Member Status
Valid literals for Member Status are Member or NonMember.
**Course fields:**
* Title
* Description
* Type
* Product Code
* Start Date
* End Date
* Credits
* Location
Valid literals for Type are Course, Certification, LiveEvent, InPersonEvent.
**Enrolment fields:**
* Email Address
* Product Code
* Enrolled Date
* Completed Date
Valid literals for Type are Course, Certification, LiveEvent, InPersonEvent.
=== Output File Format ===
The output file is also a CSV file. With indication of row-wise status. Each row displays all fields as is in the input file with two additional fields
* Output Status
* Error/Exception Description
Valid literals for output Status are Completed or Failed. Sensitive data such as Password will be avoided in the output file.
**Sample User output file:**
^ Email Address ^ First Name ^ Last Name ^ Member Status ^ Output Status ^ Error Description ^
|tom@mu.com | Tom | Brady | Member | Completed | |
| |Ram | Kumar | NonMember | Failed | Email address is missing |
| tom@mu.com | Tom | Hawk | Member | Failed | Email address has already been taken |
| ganesh@mu.com | Ganesh | M | NonMember | Completed | |
| | Amit | Raj | Nmember | Failed | Email address is missing. Members status should be Member or NonMember only.|
Like User output, Course & Enrolment output files will have additional fields output status & error description.
===== Processing ====
**Common:**
* Rake task accept legacy records in a CSV format as input
* Rake task prompt for Client/Customer ID to associate
* Input file format will be validated. The first row in the file should indicate the column header
* The input source will be validated row by row
* Valid inputs will be imported and blank rows skipped.
**User:**
* Associate the user records to the given Company/Client
* Successfully created user will be activated immediately
* No email notifications to user in the process of uploading
* Each row status will be logged into the status/output file
===== Pseudocode ====
A Factory implementing with a superclass specifies all standard and generic behavior and then delegates the creation details to sub-classes that are supplied by the client.
require 'csvlint'
# A factory implementation
class Uploader
def initialize(source, cid)
@cid = cid
@source = source
@output = CSV.generate "output.csv"
end
# Validating source CSV. Method takes validation schema as argument. It's defaulted to User schema.
def validate(schema='User')
@csv = Csvlint::Validator.new(File.new(@source), nil, schema)
#invoke the validation
@csv.validate
unless @csv.valid?
#access array of errors, each is an Csvlint::ErrorMessage object
output @csv.errors
end
end
def upload
@csv do |row|
begin
load(row)
rescue MalformedCSVError
output(row, err)
end
end
end
def output(row, status = 'Success')
@output << row << status
end
end
The client is totally decoupled from the implementation details of derived classes. Polymorphic creation is now possible.
module User
def load(row)
user = User.new
user.firstname = row['firstname']
user.lastname = row['lastname']
user.email = row['email']
user.status = User::ACTIVE
user.action = User::ACTIONS[:activated]
user.company_id = @cid
user.skip_making_activation_code = true
# To avoid all post user creation actions
User.skip_callback(:set_status) do
output(row) if user.save!
end
end
end
Factory Method enforces that encapsulation and allows an object to be requested without inextricable coupling to the act of creation.
module Course
def validate
super 'Course'
end
def load(row)
crse = Course.new
crse.save!
end
end
module Enrolment
def validate
super 'Enrolment'
end
def load(row)
enrl = Enrolment.new
enrl.save!
end
end
An interactive Rack task to direct the execution flow.
namespace :legacy_data do
desc "Legacy User data import"
task :import => :environment do
def crash_exit
puts "Invalid entry"
exit
end
def get_client cid
return "Madras University"
end
puts "Choose import category:"
puts "1. User"
puts "2. Course"
puts "3. Enrollment"
category = gets.chomp
puts "Enter CSV file source:"
src_file = gets.chomp
while true do
puts "Enter Client ID:"
cid = gets.chomp
puts "Confirm Client:" + get_client(cid)
confirm = gets.chomp
break if confirm
end
# Polymorphic creation
import = case category
when '1'
Uploader.new(src_file, cid).extend(User)
when '2'
Uploader.new(src_file, cid).extend(Course)
when '3'
Uploader.new(src_file, cid).extend(Enrollment)
else
crash_exit
end
import.upload if import.validate
end
end
**Reference:** \\
**CSV Lint** - A ruby gem to validate CSV files to check their syntax and contents. The library supports validating data against a schema. A schema configuration can be provided as a Hash or parsed from JSON. https://github.com/theodi/csvlint.rb
===== Entity Relationship ====
The following entities are used as part of the implementation
class Company < ActiveRecord::Base
has_many :users, :dependent => :destroy
end
class User < ActiveRecord::Base
belongs_to :company
end