qcourses 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/.rspec +1 -0
  2. data/Gemfile +23 -0
  3. data/Gemfile.lock +75 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.rdoc +19 -0
  6. data/Rakefile +52 -0
  7. data/VERSION +1 -0
  8. data/config.ru +3 -0
  9. data/db/migrations/001_initial_database.rb +25 -0
  10. data/db/migrations/002_create_registrations.rb +29 -0
  11. data/demo_app.rb +11 -0
  12. data/lib/factories.rb +111 -0
  13. data/lib/qcourses/configuration.rb +35 -0
  14. data/lib/qcourses/controllers/base.rb +20 -0
  15. data/lib/qcourses/controllers/courses_controller.rb +16 -0
  16. data/lib/qcourses/controllers/events_controller.rb +54 -0
  17. data/lib/qcourses/controllers/registrations_controller.rb +51 -0
  18. data/lib/qcourses/controllers.rb +5 -0
  19. data/lib/qcourses/date_ex.rb +18 -0
  20. data/lib/qcourses/models/course.rb +30 -0
  21. data/lib/qcourses/models/course_repository.rb +90 -0
  22. data/lib/qcourses/models/event.rb +54 -0
  23. data/lib/qcourses/models/location.rb +17 -0
  24. data/lib/qcourses/models/registration.rb +36 -0
  25. data/lib/qcourses/models.rb +28 -0
  26. data/lib/qcourses/qcourses.rake.rb +21 -0
  27. data/lib/qcourses/renderers.rb +26 -0
  28. data/lib/qcourses/resource_paths.rb +45 -0
  29. data/lib/qcourses/string_ex.rb +5 -0
  30. data/lib/qcourses/view_helpers.rb +70 -0
  31. data/lib/qcourses/web_app.rb +29 -0
  32. data/lib/qcourses.rb +21 -0
  33. data/public/css/default.css +91 -0
  34. data/public/img/delete.gif +0 -0
  35. data/public/javascript/jquery-1.7.js +9300 -0
  36. data/public/javascript/jquery-1.7.min.js +4 -0
  37. data/public/javascript/qcourses.js +20 -0
  38. data/spec/factories_spec.rb +131 -0
  39. data/spec/qcourses/configuration_spec.rb +37 -0
  40. data/spec/qcourses/controllers/courses_controller_spec.rb +43 -0
  41. data/spec/qcourses/controllers/events_controller_spec.rb +100 -0
  42. data/spec/qcourses/controllers/registrations_controller_spec.rb +100 -0
  43. data/spec/qcourses/models/course_repository_spec.rb +147 -0
  44. data/spec/qcourses/models/event_spec.rb +74 -0
  45. data/spec/qcourses/models/location_spec.rb +19 -0
  46. data/spec/qcourses/models/registration_spec.rb +55 -0
  47. data/spec/qcourses/renderers_spec.rb +82 -0
  48. data/spec/qcourses/string_ex_spec.rb +11 -0
  49. data/spec/qcourses/view_helpers_spec.rb +90 -0
  50. data/spec/spec_helper.rb +38 -0
  51. data/spec/support/factories.rb +24 -0
  52. data/spec/support/matchers.rb +61 -0
  53. data/spec/support/request_specs.rb +4 -0
  54. data/spec/support/test_files.rb +20 -0
  55. data/views/admin.haml +9 -0
  56. data/views/events/index.haml +18 -0
  57. data/views/events/new.haml +34 -0
  58. data/views/layout.haml +9 -0
  59. data/views/registration.haml +9 -0
  60. data/views/registrations/new.haml +37 -0
  61. data/views/registrations/participant.haml +12 -0
  62. data/views/registrations/success.haml +4 -0
  63. data/views/trainings/error.haml +1 -0
  64. data/views/trainings/index.haml +9 -0
  65. metadata +306 -0
@@ -0,0 +1,90 @@
1
+ module Qcourses
2
+
3
+ class MemoryCourseRepository
4
+ def initialize
5
+ @courses = {}
6
+ end
7
+ def create_course(attributes)
8
+ raise "should supply course identification" unless attributes.has_key?(:identification)
9
+ @courses[attributes[:identification]] = Course.new(attributes)
10
+ end
11
+ def all()
12
+ @courses.values
13
+ end
14
+ def find(course_id)
15
+ @courses[course_id]
16
+ end
17
+ end
18
+
19
+ class CourseRepository
20
+ class Error < Exception; end
21
+
22
+ @@instance = nil
23
+ def self.configure(instance = CourseRepository.new)
24
+ @@instance = instance
25
+ end
26
+
27
+ def self.destroy
28
+ @@instance = nil
29
+ end
30
+
31
+ def self.in_memory
32
+ configure MemoryCourseRepository.new
33
+ end
34
+
35
+ def self.on_file_system
36
+ configure CourseRepository.new
37
+ end
38
+
39
+ def self.method_missing(method, *args)
40
+ raise Error.new "CourseRepository not configured while calling #{method}" unless @@instance
41
+ @@instance.send(method, *args)
42
+ end
43
+
44
+ KEY_VALUE_SEPARATOR = ":"
45
+
46
+ def initialize(directory = Dir, file_opener = File)
47
+ @directory = directory
48
+ @file_opener = file_opener
49
+ end
50
+
51
+ def all
52
+ courses.values
53
+ end
54
+
55
+ def find(course_id)
56
+ courses[course_id]
57
+ end
58
+
59
+ private
60
+ def courses
61
+ @courses ||= Hash[ parse_courses.map {|course| [course.identification, course] }]
62
+ end
63
+
64
+ def parse_courses
65
+ course_files.collect { |file| parse_course(file) }.uniq
66
+ end
67
+ def course_files
68
+ files = (@directory.glob("**/courses/*.mdown") + @directory.glob("**/cursussen/*.mdown"))
69
+ files.delete_if {|file| file.end_with?('index.mdown')}
70
+ end
71
+
72
+ def parse_course(filename)
73
+ @file_opener.open(filename) do |file|
74
+ course_identification = File.basename(filename, '.mdown')
75
+ Course.new parse_attributes(file, Course.default_attributes(course_identification))
76
+ end
77
+ end
78
+
79
+ def parse_attributes(file, attributes)
80
+ file.each_line do |line|
81
+ line.strip!
82
+ return attributes unless line.include?(KEY_VALUE_SEPARATOR)
83
+ key, value = line.split(KEY_VALUE_SEPARATOR, 2)
84
+ attributes[:name] = value.strip if key.strip == 'Name'
85
+ attributes[:subtitle] = value.strip if key.strip == 'Subtitle'
86
+ end
87
+ return attributes
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,54 @@
1
+ module Qcourses
2
+
3
+ class Event < Sequel::Model
4
+ plugin :validation_helpers
5
+ many_to_one :location
6
+
7
+ def self.opens(object_id)
8
+ first{ ({id => object_id}) & (from > Time.now) }
9
+ end
10
+
11
+ def validate
12
+ super
13
+ validates_presence :from
14
+ errors.add(:from, "must be a time") unless is_a_time?(from)
15
+ errors.add(:location, "must be an associated location") unless location && location.valid?
16
+ end
17
+
18
+ def location=(new_location)
19
+ if new_location.is_a?(String)
20
+ new_location = Location.find_or_create_bij_insensitive_name(new_location)
21
+ end
22
+ super(new_location)
23
+ end
24
+
25
+ def name
26
+ return "removed course (#{course_id})" unless course
27
+ course.name
28
+ end
29
+
30
+ def subtitle
31
+ return '' unless course
32
+ course.subtitle
33
+ end
34
+
35
+ def location_name
36
+ return 'unknown' unless location
37
+ location.name
38
+ end
39
+
40
+ def course
41
+ @course ||= CourseRepository.find(course_id)
42
+ end
43
+
44
+ private
45
+ def is_a_time?(date)
46
+ return true if date.is_a?(Time)
47
+ return false unless date.is_a?(String)
48
+ return true
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,17 @@
1
+ module Qcourses
2
+
3
+ class Location < Sequel::Model
4
+ plugin :validation_helpers
5
+ one_to_many :events
6
+
7
+ def validate
8
+ validates_presence :name
9
+ end
10
+
11
+ def self.find_or_create_bij_insensitive_name(new_location)
12
+ return nil if new_location.empty?
13
+ filter(:name.ilike("#{new_location}%") ).first || create(name: new_location)
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,36 @@
1
+ module Qcourses
2
+ class Company < Sequel::Model
3
+ plugin :validation_helpers
4
+ one_to_many :employees
5
+ def validate
6
+ validates_presence :name
7
+ validates_presence :contact_person
8
+ validates_presence :contact_email
9
+ validates_presence :invoice_address
10
+ validates_presence :postal_code
11
+ validates_presence :city
12
+ validates_format EMAIL_REGEXP, :contact_email
13
+ end
14
+ end
15
+ class Employee < Sequel::Model
16
+ plugin :validation_helpers
17
+ many_to_one :company
18
+ def validate
19
+ validates_presence :name
20
+ validates_presence :email
21
+ validates_format EMAIL_REGEXP, :email
22
+ end
23
+
24
+ end
25
+
26
+ class Registration < Sequel::Model
27
+ plugin :validation_helpers
28
+ many_to_one :employee
29
+ many_to_one :event
30
+
31
+ def validate
32
+ validates_presence :employee
33
+ validates_presence :event
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ require 'sequel'
2
+ require 'sequel/extensions/migration'
3
+ require 'yaml'
4
+
5
+ module Qcourses
6
+ def self.db
7
+ @@db ||= create_connection
8
+ end
9
+ def self.create_connection(environment = Qcourses.env)
10
+ environment = environment.to_s
11
+ connection = Sequel.connect(YAML::load(File.read(File.join(config.root, 'config', 'database.yml')))[environment])
12
+ schema_definition.apply(connection, :up) if environment == 'test'
13
+ connection
14
+ end
15
+
16
+ def self.schema_definition
17
+ eval(`sequel -d sqlite://data/development.sqlite3`)
18
+ end
19
+ EMAIL_REGEXP = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
20
+ end
21
+
22
+ Qcourses.create_connection
23
+
24
+ require_relative 'models/course_repository'
25
+ require_relative 'models/course'
26
+ require_relative 'models/location'
27
+ require_relative 'models/event'
28
+ require_relative 'models/registration'
@@ -0,0 +1,21 @@
1
+
2
+ desc "project console"
3
+ task :console => :environment do
4
+ sh "irb -I #{File.expand_path('lib', File.dirname(__FILE__)) } -r qcourses"
5
+ end
6
+
7
+ desc "project environment"
8
+ task :environment do
9
+ ENV['RACK_ENV'] ||= 'development'
10
+ end
11
+
12
+ namespace :db do
13
+ desc "migrate the database"
14
+ task :migrate, [:version] => :environment do |t, args|
15
+ migration_dir = File.expand_path('../../db/migrations', File.dirname(__FILE__))
16
+ command = ["sequel -E config/database.yml -m #{migration_dir}"]
17
+ command << "-M #{args[:version]}" if args[:version]
18
+ sh command.join(' ')
19
+ end
20
+ end
21
+
@@ -0,0 +1,26 @@
1
+ module Qcourses
2
+ module Renderers
3
+ private
4
+ def render(engine, data, options={}, locals={}, &block)
5
+ super(engine, data, override_options(options, data, engine), locals, &block)
6
+ end
7
+
8
+ def override_options(options,template, engine)
9
+ options = options.merge(views:file_path_for_template(template, engine))
10
+ end
11
+
12
+ def file_path_for_template(template, engine)
13
+ views_path = File.join(root, views)
14
+ if template_exists?(template, engine, views_path)
15
+ views_path
16
+ else
17
+ File.join(local_root, 'views')
18
+ end
19
+ end
20
+
21
+ def template_exists?(template, engine, root_dir)
22
+ path = File.expand_path("#{template}.#{engine}", root_dir)
23
+ File.exists?(path)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ module Qcourses
2
+ module ResourcePaths
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ base.helpers do
6
+ include UrlHelpers
7
+ end
8
+ end
9
+ module UrlHelpers
10
+ def adminprefix
11
+ settings.admin_url
12
+ end
13
+ def basepath
14
+ settings.basepath
15
+ end
16
+ def request_url(param = nil)
17
+ [basepath, param_symbol(param)].compact.join('/')
18
+ end
19
+ def new_request_url(param = nil)
20
+ [request_url, 'new', param_symbol(param)].compact.join('/')
21
+ end
22
+ def admin_request_url
23
+ adminprefix + request_url
24
+ end
25
+ def admin_new_request_url
26
+ adminprefix + new_request_url
27
+ end
28
+ private
29
+ def param_symbol(param)
30
+ param && param.inspect || param
31
+ end
32
+ end
33
+ module ClassMethods
34
+ def resource(name)
35
+ set :basepath, name
36
+ end
37
+ def resource_admin_on(url)
38
+ set :admin_url, url
39
+ end
40
+ include UrlHelpers
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def underscore
3
+ self.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z])([A-Z][a-z])/, '\1_\2').downcase
4
+ end
5
+ end
@@ -0,0 +1,70 @@
1
+ module Qcourses
2
+ module ViewHelpers
3
+ def options_for_select options
4
+ options.collect {|option| haml "%option(value='#{option[0]}') #{option[1]}", :layout => false}.join
5
+ end
6
+ def course_options
7
+ options_for_select CourseRepository.all.collect { |course| [course.identification, course.name] }
8
+ end
9
+ def location_options
10
+ options_for_select(Location.all.collect { |location| [location.id, location.name] } + [['0', 'new location']])
11
+ end
12
+ def display_if_locations
13
+ Location.all.empty? && "display:none;" || "display:block"
14
+ end
15
+ def display_unless_locations
16
+ Location.all.empty? && "display:block;" || "display:none"
17
+ end
18
+
19
+ def human_period(from, to)
20
+ if (from.year != to.year)
21
+ "#{from.strftime('%e/%m/%Y').strip} - #{to.strftime('%e/%m/%Y').strip}"
22
+ elsif (from.month != to.month)
23
+ "#{from.strftime('%e/%m')} - #{to.strftime('%e/%m %Y').strip}"
24
+ else
25
+ "#{from.day} - #{to.strftime('%e %B %Y').strip}"
26
+ end
27
+ end
28
+
29
+ def input_for(object, attribute_name, options={})
30
+ object = instance_variable_get("@#{object}") if object.is_a?(Symbol)
31
+ return '' unless object
32
+ tag_attributes = {:type => 'text', :name => input_name(object, attribute_name, options[:array_element])}
33
+ tag_attributes[:value] = object.send(attribute_name) || ''
34
+ tag_classes = []
35
+ tag_classes << 'error' if object.errors.on(attribute_name)
36
+ tag_classes << options[:class] if options[:class]
37
+ tag_attributes[:class] = tag_classes.join(' ') unless tag_classes.empty?
38
+ result = haml("%input#{tag_attributes.inspect}")
39
+ result += haml("%span{:class => 'error'} #{object.errors.on(attribute_name).first}" ) if object.errors.on(attribute_name) && !options[:suppress_error_messages]
40
+ result
41
+ end
42
+
43
+ def textarea_for(object, attribute_name, options={})
44
+ object = instance_variable_get("@#{object}") if object.is_a?(Symbol)
45
+ return '' unless object
46
+ tag_attributes = {:name => input_name(object, attribute_name, options[:array_element])}
47
+ value = object.send(attribute_name) || ''
48
+ tag_classes = []
49
+ tag_classes << 'error' if object.errors.on(attribute_name)
50
+ tag_classes << options[:class] if options[:class]
51
+ tag_attributes[:class] = tag_classes.join(' ') unless tag_classes.empty?
52
+ "<textarea #{text_hash(tag_attributes)}>#{value}</textarea>"
53
+ end
54
+
55
+ private
56
+ def text_hash(hash)
57
+ hash.to_a.map {|e| "#{e[0]}=\"#{e[1]}\"" }.join(' ')
58
+ end
59
+ def input_name(object, attribute_name, array)
60
+ if (array)
61
+ "#{class_name(object)}[][#{attribute_name}]"
62
+ else
63
+ "#{class_name(object)}[#{attribute_name}]"
64
+ end
65
+ end
66
+ def class_name(object)
67
+ object.class.name.split('::').last.underscore
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ require 'sinatra/base'
2
+ require 'haml'
3
+ require 'qcourses'
4
+
5
+ module Qcourses
6
+ class WebApp < Sinatra::Application
7
+ use Qcourses::CoursesController
8
+ use Qcourses::EventsController
9
+ use Qcourses::RegistrationsController
10
+
11
+ enable :sessions
12
+ configure :production do
13
+ end
14
+ configure :development do
15
+ enable :logging
16
+ end
17
+ helpers do
18
+ include Rack::Utils
19
+ alias_method :h, :escape_html
20
+ end
21
+ end
22
+ module Helpers
23
+ def qcourses_get(url)
24
+ Qcourses::WebApp.call(env.merge("PATH_INFO" => url)).last.join
25
+ end
26
+ end
27
+ end
28
+
29
+ #Dir.glob('lib/controllers/*_controller.rb').each { |controller| require controller }
data/lib/qcourses.rb ADDED
@@ -0,0 +1,21 @@
1
+ require_relative 'qcourses/configuration'
2
+ module Qcourses
3
+ def self.env
4
+ @@env ||= (ENV['RACK_ENV'] || ENV['QCOURSES_ENV'] || 'development')
5
+ end
6
+ def self.config
7
+ @@config
8
+ end
9
+ def self.configure(&configuration_block)
10
+ @@config = Configuration.instance
11
+ @@config.local_root = File.expand_path('..', File.dirname(__FILE__))
12
+ self.config.configure(&configuration_block)
13
+ require 'qcourses/date_ex'
14
+ require 'qcourses/string_ex'
15
+ require 'qcourses/models'
16
+ end
17
+ end
18
+ require 'qcourses/renderers'
19
+ require 'qcourses/view_helpers'
20
+ require 'qcourses/controllers'
21
+
@@ -0,0 +1,91 @@
1
+ body {
2
+ width:800px;
3
+ margin: 0 auto;
4
+ font-family: Helvetica, Arial, sans-serif;
5
+ color: #737373;
6
+ }
7
+
8
+ .clear {
9
+ display: block;
10
+ clear: both;
11
+ }
12
+
13
+
14
+ .form {
15
+ background: #F4F4F4;
16
+ margin: 10px 10px;
17
+ padding: 10px 10px;
18
+ border-radius: 6px;
19
+ }
20
+
21
+ .form .label {
22
+ clear:left;
23
+ float:left;
24
+ width: 200px;
25
+ position:relative;
26
+ padding-top: 4px;
27
+ }
28
+
29
+ .form .input {
30
+ float:left;
31
+ position:relative;
32
+ }
33
+
34
+ #add-participant {
35
+ margin-left:215px;
36
+ }
37
+
38
+ span.error {
39
+ color: red;
40
+ }
41
+
42
+ fieldset {
43
+ border: 1px solid #e3e3e3;
44
+ border-radius: 3px;
45
+ }
46
+ legend {
47
+ font-size: smaller;
48
+ }
49
+
50
+ textarea, input[type="text"] {
51
+ border: 1px solid #e3e3e3;
52
+ padding: 5px 0;
53
+ width: 348px;
54
+ }
55
+
56
+ input.error {
57
+ border-color:red;
58
+ }
59
+
60
+ input.postal-code {
61
+ width: 98px;
62
+ }
63
+
64
+ input.city {
65
+ padding-left: 3px;
66
+ width: 241px;
67
+ }
68
+
69
+ textarea {
70
+ height: 3em;
71
+ }
72
+
73
+ select {
74
+ border: 1px solid #e3e3e3;
75
+ padding: 5px 0 5px 3px;
76
+ width: 200px;
77
+ }
78
+ input.date {
79
+ }
80
+
81
+ input.submit {
82
+ }
83
+
84
+ .event {border-bottom: solid lightgrey 1px; padding:3px 0px; color: #8B8B89; font-family: Lucida Grande, Lucida Sans Unicode,Arial,Helvetica,Sans,FreeSans,Jamrul,Garuda,Kalimati; line-height: 120%;}
85
+ .event .event_name {font-size: 15px; font-weight:bold; cursor: pointer; }
86
+ .event .event_subtitle {font-size:12px; margin:2px; color: #6B6B69; cursor: pointer; }
87
+ .event .event_registration {text-align:right; font-size: 12px; font-style: italic;}
88
+ .event .event_date_location {float:right; width:190px;}
89
+ .event .event_registration {float:right; width:60px;}
90
+ .event .clear {clear:both;}
91
+
Binary file