├── log └── .gitkeep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── app ├── mailers │ └── .gitkeep ├── assets │ ├── images │ │ └── rails.png │ ├── javascripts │ │ ├── bootstrap.js.coffee │ │ ├── application.js │ │ └── bootstrap-datetimepicker.min.js │ └── stylesheets │ │ ├── custom │ │ ├── header_adjust.css │ │ └── bootstrap-datetimepicker.min.css │ │ ├── application.css │ │ └── bootstrap_and_overrides.css.less ├── controllers │ ├── application_controller.rb │ ├── bookings_controller.rb │ ├── cargo_inspection_service.rb │ ├── handling_event_registration.rb │ ├── handling_events_controller.rb │ └── tracking_cargos_controller.rb ├── views │ ├── handling_events │ │ ├── show.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── bookings │ │ ├── show.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb │ ├── tracking_cargos │ │ └── show.html.erb │ └── layouts │ │ └── application.html.erb ├── helpers │ └── application_helper.rb └── models │ └── booking.rb ├── spec ├── domain │ ├── cargo │ │ ├── itinerary_spec.rb │ │ ├── tracking_id_spec.rb │ │ ├── handling_activity_spec.rb │ │ ├── routing_status_spec.rb │ │ ├── transport_status_spec.rb │ │ ├── leg_spec.rb │ │ ├── route_specification_spec.rb │ │ ├── cargo_spec.rb │ │ ├── delivery_spec.rb │ │ └── delivery_spec.rb_dan │ ├── location │ │ ├── location_spec.rb │ │ └── unlocode_spec.rb │ ├── voyage │ │ ├── schedule_spec.rb │ │ ├── carrier_movement_spec.rb │ │ ├── transport_leg_spec.rb │ │ ├── voyage_number_spec.orb │ │ └── voyage_spec.rb │ ├── handling │ │ ├── handling_event_spec.rb │ │ ├── handling_event_type_spec.rb │ │ └── handling_history_spec.rb │ └── itinerary_spec.rb ├── support │ └── models_require.rb ├── spec_helper.rb ├── integration │ └── cargo_spec.rb ├── infrastructure │ ├── location_repository_spec.rb │ ├── handling_event_repository_spec.rb │ ├── sample_data_spec.rb │ └── cargo_repository_spec.rb └── lib │ └── value_object_spec.rb ├── config ├── mongoid.yml ├── environment.rb ├── routes.rb ├── boot.rb ├── initializers │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── secret_token.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── loader.rb ├── locales │ ├── en.yml │ └── en.bootstrap.yml ├── database.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── config.ru ├── domain ├── cargo │ ├── tracking_id.rb │ ├── routing_status.rb │ ├── transport_status.rb │ ├── handling_activity.rb │ ├── route_specification.rb │ ├── leg.rb │ ├── itinerary.rb │ ├── cargo.rb │ └── delivery.rb ├── voyage │ ├── voyage_number.rb │ ├── transport_leg.rb │ ├── voyage.rb │ ├── schedule.rb │ └── carrier_movement.rb ├── handling │ ├── handling_event_type.rb │ ├── handling_event.rb │ └── handling_history.rb └── location │ ├── unlocode.rb │ └── location.rb ├── Rakefile ├── script └── rails ├── .gitignore ├── ports └── persistence │ └── mongodb_adaptor │ ├── mongoid_readme.ad │ ├── location_repository.rb │ ├── handling_event_repository.rb │ └── cargo_repository.rb ├── Guardfile ├── lib └── value_object.rb ├── Gemfile ├── LICENSE ├── Notes.ad ├── Gemfile.lock └── README.adoc /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/cargo/itinerary_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/cargo/tracking_id_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/location/location_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/location/unlocode_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/voyage/schedule_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/cargo/handling_activity_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/cargo/routing_status_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/cargo/transport_status_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/handling/handling_event_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/voyage/carrier_movement_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/voyage/transport_leg_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/voyage/voyage_number_spec.orb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/handling/handling_event_type_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/domain/handling/handling_history_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulrayner/ddd_sample_app_ruby/HEAD/app/assets/images/rails.png -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/bootstrap.js.coffee: -------------------------------------------------------------------------------- 1 | jQuery -> 2 | $("a[rel=popover]").popover() 3 | $(".tooltip").tooltip() 4 | $("a[rel=tooltip]").tooltip() -------------------------------------------------------------------------------- /config/mongoid.yml: -------------------------------------------------------------------------------- 1 | development: 2 | sessions: 3 | default: 4 | database: container_shipping 5 | hosts: 6 | - localhost:27017 -------------------------------------------------------------------------------- /app/assets/stylesheets/custom/header_adjust.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | } 4 | @media (max-width: 980px) { 5 | body { 6 | padding-top: 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run DddSampleAppRuby::Application 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | DddSampleAppRuby::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | DddSampleAppRuby::Application.routes.draw do 2 | resources :handling_events 3 | resources :tracking_cargos 4 | resources :bookings 5 | 6 | root :to => 'bookings#index' 7 | end 8 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /domain/cargo/tracking_id.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class TrackingId < ValueObject 5 | attr_reader :id 6 | 7 | def initialize(id) 8 | @id = id 9 | 10 | IceNine.deep_freeze(self) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /domain/cargo/routing_status.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-enum' 2 | 3 | # Describes status of cargo routing 4 | class RoutingStatus 5 | include Ruby::Enum 6 | 7 | define :NotRouted, 'Not Routed' 8 | define :Misrouted, 'Misrouted' 9 | define :Routed, 'Routed' 10 | end 11 | -------------------------------------------------------------------------------- /domain/voyage/voyage_number.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class VoyageNumber < ValueObject 5 | attr_reader :number 6 | 7 | def initialize(number) 8 | @number = number 9 | 10 | IceNine.deep_freeze(self) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /domain/handling/handling_event_type.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-enum' 2 | 3 | class HandlingEventType 4 | include Ruby::Enum 5 | 6 | define :Load, 'Load' 7 | define :Unload, 'Unload' 8 | define :Receive, 'Receive' 9 | define :Claim, 'Claim' 10 | define :Customs, 'Customs' 11 | end 12 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | date: 7 | formats: 8 | default: "%d/%m/%Y" -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | DddSampleAppRuby::Application.load_tasks 8 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /domain/cargo/transport_status.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-enum' 2 | 3 | # Describes status of cargo transportation 4 | class TransportStatus 5 | include Ruby::Enum 6 | 7 | define :NotReceived, 'Not Received' 8 | define :OnboardCarrier, 'Onboard Carrier' 9 | define :InPort, 'In Port' 10 | define :Claimed, 'Claimed' 11 | define :Unknown, 'Unknown' 12 | end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore bundler config 3 | /.bundle 4 | 5 | # Ignore all logfiles and tempfiles. 6 | /log/*.log 7 | /tmp 8 | .ackrc 9 | .rvmrc 10 | .ruby-* 11 | 12 | # These should be gitignored so that each person can have their own. 13 | # Use .yml.sample as a sample template 14 | 15 | config/mongoid.yml 16 | .bundle 17 | tmp/ 18 | *.DS_Store 19 | coverage 20 | .tags* 21 | .idea* 22 | -------------------------------------------------------------------------------- /domain/cargo/handling_activity.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class HandlingActivity < ValueObject 5 | attr_reader :handling_event_type 6 | attr_reader :location 7 | 8 | def initialize(handling_event_type, location) 9 | # TODO Check valid values 10 | 11 | @handling_event_type = handling_event_type 12 | @location = location 13 | 14 | IceNine.deep_freeze(self) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/models_require.rb: -------------------------------------------------------------------------------- 1 | # helper file to require all models 2 | require 'routing_status' 3 | require 'transport_status' 4 | 5 | require 'cargo' 6 | require 'leg' 7 | require 'itinerary' 8 | require 'tracking_id' 9 | require 'route_specification' 10 | require 'location' 11 | require 'unlocode' 12 | require 'delivery' 13 | require 'handling_activity' 14 | require 'handling_event' 15 | require 'handling_event_type' 16 | 17 | require 'date' 18 | -------------------------------------------------------------------------------- /domain/voyage/transport_leg.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class TransportLeg < ValueObject 5 | attr_reader :departure_location 6 | attr_reader :arrival_location 7 | 8 | def initialize(departure_location, arrival_location) 9 | # TODO Check valid values 10 | 11 | @departure_location = departure_location 12 | @arrival_location = arrival_location 13 | 14 | IceNine.deep_freeze(self) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | DddSampleAppRuby::Application.config.session_store :cookie_store, key: '_ddd_sample_app_ruby_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # DddSampleAppRuby::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /domain/voyage/voyage.rb: -------------------------------------------------------------------------------- 1 | class Voyage 2 | attr_reader :id # unique id of this voyage 3 | attr_accessor :number # voyage number (non-unique) associated with this voyage 4 | attr_accessor :schedule # schedule associated with this voyage 5 | 6 | def initialize (number, schedule) 7 | # TODO: add exception checking for invalid (null) values 8 | 9 | @number = number 10 | @schedule = schedule 11 | end 12 | 13 | def ==(other) 14 | self.number == other.number 15 | end 16 | end -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | DddSampleAppRuby::Application.config.secret_token = '8753660a1624f772f2b569b6293e908b8efa49ae0a344c9a6aa79b3a798f6f5fa93cff66aab3f3a67c8d1e45e4378e7a67017cd95d3bb96dc757b064495f0ea9' 8 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /config/locales/en.bootstrap.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | helpers: 6 | actions: "Actions" 7 | links: 8 | back: "Back" 9 | cancel: "Cancel" 10 | confirm: "Are you sure?" 11 | destroy: "Delete" 12 | new: "New" 13 | edit: "Edit" 14 | titles: 15 | edit: "Edit" 16 | save: "Save" 17 | new: "New" 18 | delete: "Delete" 19 | -------------------------------------------------------------------------------- /ports/persistence/mongodb_adaptor/mongoid_readme.ad: -------------------------------------------------------------------------------- 1 | On the command line: 2 | 3 | ``` 4 | export MONGOID_ENV=development 5 | ``` 6 | 7 | == Resources 8 | 9 | * https://github.com/evansagge/mongoid-rspec[Mongoid-RSpec] - RSpec matchers and macros for Mongoid 3.x 10 | * http://stackoverflow.com/questions/9868323/is-there-a-convention-to-name-collection-in-mongo-db?lq=1[Naming conventions for Mongo artifacts] 11 | * http://docs.mongodb.org/manual/tutorial/model-embedded-one-to-one-relationships-between-documents/[Model Embedded One-to-One Relationships Between Documents] -------------------------------------------------------------------------------- /domain/voyage/schedule.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class Schedule < ValueObject 5 | attr_reader :carrier_movements 6 | attr_reader :departure_time 7 | attr_reader :arrival_time 8 | 9 | def initialize(carrier_movements, departure_time, arrival_time, price_per_cargo) 10 | # TODO Check valid values 11 | 12 | @carrier_movements = carrier_movements 13 | @departure_time = carrier_movements.first.departure_time 14 | @arrival_time = carrier_movements.last.arrival_time 15 | 16 | IceNine.deep_freeze(self) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/domain/voyage/voyage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'voyage' 3 | 4 | describe Voyage do 5 | 6 | context "entity equality" do 7 | it "should equal a voyage with the same voyage number" do 8 | @voyage = Voyage.new('9999', 'fake schedule') 9 | @voyage.should == Voyage.new('9999', 'another fake schedule') 10 | end 11 | 12 | it "should not equal a voyage with a different voyage number" do 13 | @voyage = Voyage.new('9999', 'fake schedule') 14 | @voyage.should_not == Voyage.new('8888', 'fake schedule') 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /app/views/handling_events/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to 'All handling events', handling_events_path %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @handling_events_history.each do |handling_event| %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% end %> 19 |
TypeLocationCompletion DateTracking ID
<%= handling_event.event_type %><%= handling_event.location.name %><%= handling_event.completion_date %><%= handling_event.tracking_id %>
20 | -------------------------------------------------------------------------------- /app/views/handling_events/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to 'New handling event', new_handling_event_path %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @handling_events_history.each do |handling_event| %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% end %> 19 |
TypeLocationCompletion DateTracking ID
<%= handling_event.event_type %><%= handling_event.location.name %><%= handling_event.completion_date %><%= handling_event.tracking_id %>
20 | -------------------------------------------------------------------------------- /app/views/handling_events/new.html.erb: -------------------------------------------------------------------------------- 1 | <%=form_tag handling_events_path do %> 2 | <% fields_for :handling do |f| %> 3 | <%= f.label :event_type %> 4 | <%= f.text_field :event_type %> 5 |
6 | <%= f.label :location_code %> 7 | <%= f.text_field :location_code %> 8 |
9 | <%= f.label :completion_date %> 10 | <%= select_date Date.today, :prefix => :completion_date %> 11 |
12 | <%= f.label :tracking_id, 'Tracking Id' %> 13 | <%= f.text_field :tracking_id %> 14 |
15 | <%= f.submit "Create handling event" %> 16 | <% end %> 17 | <% end %> -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | def bootstrap_class_for flash_type 4 | { success: "alert-success", error: "alert-danger", alert: "alert-warning", notice: "alert-info" }[flash_type] || flash_type.to_s 5 | end 6 | 7 | def flash_messages(opts = {}) 8 | flash.each do |msg_type, message| 9 | concat(content_tag(:div, message, class: "alert #{bootstrap_class_for(msg_type)} fade in") do 10 | concat content_tag(:button, 'x', class: "close", data: { dismiss: 'alert' }) 11 | concat raw(message) 12 | end) 13 | end 14 | nil 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /domain/voyage/carrier_movement.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | # Based on .NET implementation 5 | class CarrierMovement < ValueObject 6 | attr_reader :transport_leg 7 | attr_reader :departure_time 8 | attr_reader :arrival_time 9 | attr_reader :price_per_cargo 10 | 11 | def initialize(transport_leg, departure_time, arrival_time, price_per_cargo) 12 | # TODO Check valid values 13 | 14 | @transport_leg = transport_leg 15 | @departure_time = departure_time 16 | @arrival_time = arrival_time 17 | @price_per_cargo = price_per_cargo 18 | 19 | IceNine.deep_freeze(self) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /domain/handling/handling_event.rb: -------------------------------------------------------------------------------- 1 | require 'uuidtools' 2 | 3 | class HandlingEvent 4 | attr_accessor :event_type 5 | attr_accessor :location 6 | attr_accessor :registration_date 7 | attr_accessor :completion_date 8 | attr_accessor :tracking_id 9 | attr_accessor :id 10 | 11 | def initialize(event_type, location, registration_date, completion_date, tracking_id, id) 12 | @event_type = event_type 13 | @location = location 14 | @registration_date = registration_date 15 | @completion_date = completion_date 16 | @tracking_id = tracking_id 17 | @id = id 18 | end 19 | 20 | def self.new_id 21 | UUIDTools::UUID.timestamp_create.to_s 22 | end 23 | end -------------------------------------------------------------------------------- /domain/handling/handling_history.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'hamster' 3 | require 'value_object' 4 | 5 | class HandlingHistory < ValueObject 6 | attr_reader :handling_events 7 | 8 | # TODO Handle empty values for attributes by returning UNKNOWN location 9 | # TODO Add is_empty method to supporting checking for this in is_empty 10 | 11 | def initialize(handling_events) 12 | # TODO Check valid values 13 | 14 | @handling_events = Hamster.list(handling_events) 15 | 16 | IceNine.deep_freeze(self) 17 | end 18 | 19 | # TODO Implement this (shouldn't it be the default?) 20 | def events_by_completion_time(event) 21 | handling_events 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /domain/location/unlocode.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | # United nations location code. 5 | # 6 | # http://www.unece.org/cefact/locode/service/location.html 7 | # http://www.unece.org/fileadmin/DAM/cefact/locode/Service/LocodeColumn.htm 8 | # 9 | # Returns a string representation of this UnLocode consisting of 5 characters (all upper): 10 | # 2 chars of ISO country code and 3 describing location. 11 | 12 | class UnLocode < ValueObject 13 | attr_reader :code 14 | 15 | # TODO: Add regex check for valid code 16 | 17 | def initialize(code) 18 | @code = code 19 | 20 | IceNine.deep_freeze(self) 21 | end 22 | 23 | def to_s 24 | "#{@code}" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require twitter/bootstrap 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /config/initializers/loader.rb: -------------------------------------------------------------------------------- 1 | puts "Loading common objects..." 2 | Dir.glob("#{Rails.root.to_s}/lib/*.rb").each do |f| 3 | puts "Loading: " + f 4 | require f 5 | end 6 | 7 | puts "Loading domain objects..." 8 | Dir.glob("#{Rails.root.to_s}/domain/**/*.rb").each do |f| 9 | puts "Loading: " + f 10 | require f 11 | end 12 | 13 | puts "Loading ports and adaptors..." 14 | Dir.glob("#{Rails.root.to_s}/ports/**/*.rb").each do |f| 15 | puts "Loading: " + f 16 | require f 17 | end 18 | 19 | puts "Loading models..." 20 | Dir.glob("#{Rails.root.to_s}/app/models/**/*.rb").each do |f| 21 | puts "Loading: " + f 22 | require f 23 | end 24 | 25 | 26 | puts "Subscribing cargo inspection service to handling event registrations..." 27 | Wisper::GlobalListeners.add_listener(CargoInspectionService.new, :async => true) 28 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # Run 'guard' in the terminal 2 | 3 | guard :rspec do 4 | watch(%r{^spec/.+_spec\.rb$}) 5 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 6 | watch('spec/spec_helper.rb') { "spec" } 7 | 8 | 9 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 10 | watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 11 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 12 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 13 | watch('config/routes.rb') { "spec/routing" } 14 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 15 | end 16 | 17 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/controllers/bookings_controller.rb: -------------------------------------------------------------------------------- 1 | class BookingsController < ApplicationController 2 | def index 3 | cargo_repository = CargoRepository.new 4 | @cargo_documents = cargo_repository.find_all 5 | end 6 | 7 | def show 8 | tracking_id = TrackingId.new(params[:id]) 9 | cargo_repository = CargoRepository.new 10 | @cargo = cargo_repository.find_by_tracking_id(tracking_id) 11 | end 12 | 13 | def new 14 | @booking = Booking.new 15 | end 16 | 17 | 18 | def create 19 | @booking = Booking.new(params[:booking]) 20 | if @booking.valid? 21 | cargo_repository = CargoRepository.new 22 | cargo_repository.store(@booking.as_cargo) 23 | redirect_to bookings_path, :notice => "Booking #{@booking.to_flash} was successfully created." 24 | else 25 | render :new 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /domain/location/location.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class Location < ValueObject 5 | attr_reader :unlocode 6 | attr_reader :name 7 | 8 | CODES = { 9 | 'USCHI' => 'Chicago', 10 | 'USDAL' => 'Dallas', 11 | 'DEHAM' => 'Hamburg', 12 | 'CNHGH' => 'Hangzhou', 13 | 'FIHEL' => 'Helsinki', 14 | 'HKHKG' => 'Hongkong', 15 | 'AUMEL' => 'Melbourne', 16 | 'USLGB' => 'Long Beach', 17 | 'USNYC' => 'New York', 18 | 'NLRTM' => 'Rotterdam', 19 | 'USSEA' => 'Seattle', 20 | 'CNSHA' => 'Shanghai', 21 | 'SESTO' => 'Stockholm', 22 | 'JNTKO' => 'Tokyo' 23 | }.freeze 24 | 25 | def initialize(unlocode, name) 26 | @unlocode = unlocode 27 | @name = name 28 | 29 | IceNine.deep_freeze(self) 30 | end 31 | 32 | # TODO Handle unknown location 33 | 34 | def to_s 35 | "#{@name} \[#{@unlocode}]" 36 | end 37 | end -------------------------------------------------------------------------------- /lib/value_object.rb: -------------------------------------------------------------------------------- 1 | class ValueObject 2 | 3 | # Class methods 4 | class << self 5 | 6 | # @equality_list is a class instance variable that is defined 7 | # inside the child class. It persists throughout the life of 8 | # that class. 9 | 10 | def attr_reader(*symbols) 11 | @equality_list ||= [] 12 | 13 | symbols.each do |symbol| 14 | super(symbol) 15 | @equality_list << symbol 16 | end 17 | end 18 | 19 | 20 | def equality_list 21 | @equality_list.dup 22 | end 23 | 24 | end 25 | 26 | 27 | # Instance methods 28 | 29 | def ==(other) 30 | if equality_list.empty? 31 | super(other) 32 | else 33 | equality_list.all? do |symbol| 34 | send(symbol) == other.send(symbol) 35 | end 36 | end 37 | end 38 | 39 | 40 | def equality_list 41 | self.class.equality_list 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /app/controllers/cargo_inspection_service.rb: -------------------------------------------------------------------------------- 1 | class CargoInspectionService 2 | include Wisper::Publisher 3 | 4 | def cargo_was_handled(tracking_id, last_handling_event) 5 | cargo_repository = CargoRepository.new 6 | cargo = cargo_repository.find_by_tracking_id(tracking_id) 7 | puts "Old delivery ", cargo.delivery.inspect 8 | cargo.derive_delivery_progress(last_handling_event) 9 | puts "New delivery ", cargo.delivery.inspect 10 | publish(:cargo_is_misdirected, tracking_id) if cargo.delivery.is_misdirected 11 | publish(:cargo_is_unloaded_at_destination, tracking_id) if cargo.delivery.is_unloaded_at_destination 12 | cargo_repository.store(cargo) 13 | end 14 | 15 | def cargo_is_misdirected(tracking_id) 16 | puts "Cargo is misdirected - need to reroute it! ", tracking_id.inspect 17 | end 18 | 19 | def is_unloaded_at_destination(tracking_id) 20 | puts "Cargo has arrived at the destination - notify the customer.", tracking_id.inspect 21 | end 22 | end -------------------------------------------------------------------------------- /domain/cargo/route_specification.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class RouteSpecification < ValueObject 5 | attr_reader :origin 6 | attr_reader :destination 7 | attr_reader :arrival_deadline 8 | 9 | class InitializationError < RuntimeError; end 10 | 11 | def initialize(origin, destination, arrival_deadline) 12 | raise InitializationError unless origin && destination && arrival_deadline 13 | 14 | @origin = origin 15 | @destination = destination 16 | @arrival_deadline = arrival_deadline 17 | 18 | IceNine.deep_freeze(self) 19 | end 20 | 21 | def is_satisfied_by(itinerary) 22 | @origin == itinerary.initial_departure_location && 23 | @destination == itinerary.final_arrival_location && 24 | @arrival_deadline >= itinerary.final_arrival_date 25 | end 26 | 27 | def ==(other) 28 | self.origin == other.origin && 29 | self.destination == other.destination && 30 | self.arrival_deadline == other.arrival_deadline 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | source 'http://gems.github.com' 3 | 4 | gem 'rails', '3.2.12' 5 | 6 | 7 | # Gems used only for assets and not required 8 | # in production environments by default. 9 | group :assets do 10 | gem 'sass-rails', '~> 3.2.3' 11 | gem 'coffee-rails', '~> 3.2.1' 12 | 13 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 14 | # gem 'therubyracer', :platforms => :ruby 15 | 16 | gem 'uglifier', '>= 1.0.3' 17 | gem 'twitter-bootstrap-rails' 18 | gem 'less-rails' 19 | end 20 | 21 | gem 'jquery-rails' 22 | gem 'rspec-rails' 23 | 24 | gem 'mongo', '1.7.0' 25 | gem 'bson_ext', '1.7.0' 26 | gem 'mongoid' 27 | 28 | group :development do 29 | gem 'pry' 30 | gem 'pry-byebug' 31 | gem 'pry-stack_explorer' 32 | 33 | gem 'guard' 34 | gem 'guard-rspec' 35 | gem 'terminal-notifier-guard' 36 | end 37 | 38 | gem 'ice_nine' 39 | gem 'hamster' 40 | gem 'ruby-enum' 41 | 42 | gem 'therubyracer' 43 | 44 | gem 'wisper-async' 45 | gem 'uuidtools' -------------------------------------------------------------------------------- /app/controllers/handling_event_registration.rb: -------------------------------------------------------------------------------- 1 | class HandlingEventRegistration 2 | include Wisper::Publisher 3 | 4 | def handle(register_handling_event) 5 | location_repository = LocationRepository.new 6 | 7 | # TODO Make this a conversion to an enum when it is implemented 8 | event_type = register_handling_event[:event_type] 9 | completed = register_handling_event[:completion_date] 10 | completion_date = DateTime.new(completed[:year].to_i, completed[:month].to_i, completed[:day].to_i) 11 | location = location_repository.find(UnLocode.new(register_handling_event[:location_code])) 12 | tracking_id = TrackingId.new(register_handling_event[:tracking_id]) 13 | registration_date = DateTime.now 14 | handling_event = HandlingEvent.new(event_type, location, registration_date, completion_date, tracking_id, HandlingEvent.new_id) 15 | 16 | handling_event_repository = HandlingEventRepository.new 17 | handling_event_repository.store(handling_event) 18 | 19 | publish(:cargo_was_handled, tracking_id, handling_event) 20 | end 21 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | 8 | 9 | # Add all folders and subfolders to load path 10 | $LOAD_PATH.unshift *Dir.glob("{app,config,domain,lib,ports}/**/*/") 11 | $LOAD_PATH.unshift *Dir.glob("spec/support") 12 | 13 | RSpec.configure do |config| 14 | config.treat_symbols_as_metadata_keys_with_true_values = true 15 | config.run_all_when_everything_filtered = true 16 | config.filter_run :focus 17 | config.color_enabled = true 18 | 19 | # Run specs in random order to surface order dependencies. If you find an 20 | # order dependency and want to debug it, you can fix the order by providing 21 | # the seed, which is printed after each run. 22 | # --seed 1234 23 | config.order = 'random' 24 | end 25 | 26 | require 'pry' # allows any spec to be debuggable 27 | -------------------------------------------------------------------------------- /domain/cargo/leg.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class Leg < ValueObject 5 | attr_reader :voyage 6 | attr_reader :load_location 7 | attr_reader :unload_location 8 | attr_reader :load_date 9 | attr_reader :unload_date 10 | 11 | # TODO Handle empty values for attributes by returning UNKNOWN location 12 | # TODO Add is_empty method to supporting checking for this in is_empty 13 | 14 | def initialize(voyage, load_location, load_date, unload_location, unload_date) 15 | # TODO Check valid values 16 | 17 | @voyage = voyage 18 | @load_location = load_location 19 | @unload_location = unload_location 20 | @load_date = load_date 21 | @unload_date = unload_date 22 | 23 | IceNine.deep_freeze(self) 24 | end 25 | 26 | # Checks whether provided event is expected according to this itinerary specification. 27 | def is_expected(event) 28 | # TODO Implement this 29 | end 30 | 31 | def to_s 32 | "Loading on voyage #{@voyage} in #{@load_location} on #{@load_date}, unloading in #{@unload_location} on #{@unload_date}" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2012-2013 Paul Rayner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /app/views/bookings/show.html.erb: -------------------------------------------------------------------------------- 1 |

Details for cargo: <%= @cargo.tracking_id.id %>

2 |

3 | Origin: <%= @cargo.route_specification.origin.name %>
4 | Destination: <%= @cargo.route_specification.destination.name %>
5 | Arrival deadline: <%= @cargo.route_specification.arrival_deadline.strftime("%m/%d/%Y") %>
6 |

7 |

8 | 9 | <% if @cargo.delivery.routing_status == RoutingStatus::Routed %> 10 |


11 |

12 | Itinerary
13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% @cargo.itinerary.legs.each do | leg | %> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | <% end %> 31 |
VoyageFromLoad dateToUnload date
<%= leg.voyage %><%= leg.load_location.name %><%= leg.load_date.strftime("%m/%d/%Y") %><%= leg.unload_location.name %><%= leg.unload_date.strftime("%m/%d/%Y") %>
32 | <% else %> 33 | 34 | <%= @cargo.delivery.is_misdirected ? "Misdirected Needs rerouting" : "Not misdirected" =%>
35 | <%= link_to "Route this cargo", "bookings/assignToRoute/" + cargo_document.tracking_id %> 36 | <% end %> 37 | 38 |

39 | 40 | 41 | -------------------------------------------------------------------------------- /app/controllers/handling_events_controller.rb: -------------------------------------------------------------------------------- 1 | class HandlingEventsController < ApplicationController 2 | def index 3 | handling_event_repository = HandlingEventRepository.new 4 | # TODO this doesn't belong here...obviously! 5 | tracking_id = TrackingId.new('cargo_1234') 6 | @handling_events_history = handling_event_repository.lookup_handling_history_of_cargo(tracking_id) 7 | end 8 | 9 | def show 10 | tracking_id = TrackingId.new(params[:id]) 11 | handling_event_repository = HandlingEventRepository.new 12 | @handling_events_history = handling_event_repository.lookup_handling_history_of_cargo(tracking_id) 13 | end 14 | 15 | def create 16 | handling_event_registration = HandlingEventRegistration.new 17 | handling_event_registration.handle(register_handling_event(params)) 18 | redirect_to handling_events_path 19 | end 20 | 21 | # TODO Create command hash from params - not sure how to do this in one line 22 | def register_handling_event(params) 23 | command = Hash.new 24 | command[:event_type] = params[:handling][:event_type] 25 | command[:location_code] = params[:handling][:location_code] 26 | command[:completion_date] = params[:completion_date] 27 | command[:tracking_id] = params[:handling][:tracking_id] 28 | command 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/bookings/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @cargo_documents.each do |cargo_document| -%> 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | <% end -%> 29 | 30 |
Tracking IDOriginDestinationRouted?
<%= link_to cargo_document.tracking_id, "bookings/" + cargo_document.tracking_id, :class => 'btn btn-mini btn-success' %><%= cargo_document.origin_name %><%= cargo_document.destination_name %><%= cargo_document.leg_documents.count > 0 ? "Yes" : "No" %> 22 | 23 | <%= link_to "Track", tracking_cargos_path + "/" + cargo_document.tracking_id, :class => 'btn btn-mini btn-info' %> 24 | 25 | <%= link_to "New Handling Event", handling_events_path + "/" + cargo_document.tracking_id, :class => 'btn btn-mini btn-info' %> 26 |
31 | 32 | <%= link_to 'Create New Booking', new_booking_path, :class => 'btn btn-primary' %> -------------------------------------------------------------------------------- /app/views/tracking_cargos/show.html.erb: -------------------------------------------------------------------------------- 1 |

Details for cargo: <%= @cargo.tracking_id.id %>

2 |

3 | Routing Status: <%= @cargo.delivery.routing_status %>
4 | Transport Status: <%= @cargo.delivery.transport_status %>
5 |

6 | <% if @cargo.delivery.is_misdirected %> 7 |

Cargo is misdirected and requires rerouting

8 | <% else %> 9 |

Estimated time of arrival in <%= @cargo.route_specification.destination %>: <%= @cargo.delivery.eta.strftime("%m/%d/%Y") + " at " + @cargo.delivery.eta.strftime("%H:%M") %>

10 | <% end %> 11 | <% if @cargo.delivery.next_expected_activity %> 12 |

Next expected activity is to <%= @cargo.delivery.next_expected_activity.handling_event_type.camelize(:lower) %> in <%= @cargo.delivery.next_expected_activity.location.name %> 13 |


14 | Cargo last updated at <%= @cargo.delivery.calculated_at %> 15 |

16 | <% end %> 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <% @handling_events_history.each do |handling_event| %> 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% end %> 35 |
TypeLocationCompletion DateTracking ID
<%= handling_event.event_type %><%= handling_event.location.name %><%= handling_event.completion_date.strftime("%m/%d/%Y") %><%= handling_event.tracking_id %>
36 | -------------------------------------------------------------------------------- /domain/cargo/itinerary.rb: -------------------------------------------------------------------------------- 1 | require 'ice_nine' 2 | require 'value_object' 3 | 4 | class Itinerary < ValueObject 5 | attr_reader :legs 6 | 7 | # TODO Handle empty values for attributes by returning UNKNOWN location 8 | # TODO Add is_empty method to supporting checking for this in is_empty 9 | 10 | def initialize(legs) 11 | # TODO Check valid values 12 | @legs = legs.dup 13 | 14 | IceNine.deep_freeze(self) 15 | end 16 | 17 | def initial_departure_location 18 | legs.first.load_location 19 | end 20 | 21 | def final_arrival_location 22 | legs.last.unload_location 23 | end 24 | 25 | def final_arrival_date 26 | legs.last.unload_date 27 | end 28 | 29 | # Checks whether provided event is expected according to this itinerary specification. 30 | def is_expected(handling_event) 31 | if (handling_event.event_type == HandlingEventType::Receive) 32 | return legs.first.load_location == handling_event.location 33 | end 34 | if (handling_event.event_type == HandlingEventType::Unload) 35 | return legs.any? { |leg| leg.unload_location == handling_event.location } 36 | end 37 | if (handling_event.event_type == HandlingEventType::Load) 38 | return legs.any? { |leg| leg.load_location == handling_event.location } 39 | end 40 | if (handling_event.event_type == HandlingEventType::Claim) 41 | return legs.last.unload_location == handling_event.location 42 | end 43 | false 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/tracking_cargos_controller.rb: -------------------------------------------------------------------------------- 1 | class TrackingCargosController < ApplicationController 2 | def index 3 | handling_event_repository = HandlingEventRepository.new 4 | # TODO this doesn't belong here...obviously! 5 | tracking_id = TrackingId.new('cargo_1234') 6 | @handling_events_history = handling_event_repository.lookup_handling_history_of_cargo(tracking_id) 7 | end 8 | 9 | def show 10 | tracking_id = TrackingId.new(params[:id]) 11 | cargo_repository = CargoRepository.new 12 | # TODO use cargo_tracking_report object here...see branch for this 13 | @cargo = cargo_repository.find_by_tracking_id(tracking_id) 14 | handling_event_repository = HandlingEventRepository.new 15 | @handling_events_history = handling_event_repository.lookup_handling_history_of_cargo(tracking_id) 16 | end 17 | 18 | def create 19 | handling_event_registration = HandlingEventRegistration.new 20 | handling_event_registration.handle(register_handling_event(params)) 21 | redirect_to handling_events_path 22 | end 23 | 24 | # TODO Create command hash from params - not sure how to do this in one line 25 | def register_handling_event(params) 26 | command = Hash.new 27 | command[:event_type] = params[:handling][:event_type] 28 | command[:location_code] = params[:handling][:location_code] 29 | command[:completion_date] = params[:completion_date] 30 | command[:tracking_id] = params[:handling][:tracking_id] 31 | command 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | DddSampleAppRuby::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | # config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/domain/cargo/leg_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'leg' 3 | 4 | describe Leg do 5 | 6 | # Has to be a method def instead of the usual RSpec let() 7 | # because the latter acts more like a variable that persists 8 | # throught an example no matter how many times you call it 9 | def random_string 10 | (0...8).map { (65 + rand(26)).chr }.join 11 | end 12 | 13 | # This is probably no longer necessary in the final code since 14 | # it just duplicates the expectations in value_object_spec.rb. 15 | # I just put it here in the meantime to show that it really works! :-) 16 | context "#==" do 17 | 18 | it "returns true if all attributes in the equality list are equal" do 19 | value1 = random_string 20 | value2 = random_string 21 | value3 = random_string 22 | value4 = random_string 23 | value5 = random_string 24 | 25 | leg1 = Leg.new(value1, value2, value3, value4, value5) 26 | leg2 = Leg.new(value1, value2, value3, value4, value5) 27 | 28 | (leg1 == leg2).should be_true 29 | end 30 | 31 | 32 | it "returns false if at least one attribute in the equality list doesn't match" do 33 | value1 = random_string 34 | value2 = random_string 35 | value3 = random_string 36 | value4 = random_string 37 | value5 = random_string 38 | 39 | leg1 = Leg.new(value1, value2, value3, value4, value5) 40 | leg2 = Leg.new(value1, value2, value3, value4, value1) 41 | 42 | (leg1 == leg2).should be_false 43 | end 44 | 45 | end # context #== 46 | 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/integration/cargo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | 4 | # TODO Move 5 | describe "Cargo" do 6 | 7 | xit "should have a transport status of Not Received" do 8 | hkg = Location.new(UnLocode.new('HKG'), 'Hong Kong') 9 | lgb = Location.new(UnLocode.new('LGB'), 'Long Beach') 10 | arrival_deadline = Date.new(2013, 2, 3) 11 | route_spec = RouteSpecification.new(hkg, lgb, arrival_deadline) 12 | cargo = Cargo.new(TrackingId.new('blah'), route_spec) 13 | 14 | cargo.transport_status.should_not be_true # not received 15 | end 16 | 17 | # TODO Make this test the correct thing 18 | it "Cargo is not considered unloaded at destination if there are no recorded handling events" do 19 | true 20 | end 21 | 22 | xit "Cargo is not considered unloaded at destination after handling unload event but not at destination" do 23 | hkg = Location.new(UnLocode.new('HKG'), 'Hong Kong') 24 | lgb = Location.new(UnLocode.new('LGB'), 'Long Beach') 25 | dal = Location.new(UnLocode.new('DAL'), 'Dallas') 26 | arrival_deadline = Date.new(2013, 7, 1) 27 | 28 | route_spec = RouteSpecification.new(hkg, lgb, arrival_deadline) 29 | cargo = Cargo.new(TrackingId.new('blah'), route_spec) 30 | 31 | legs = Array.new 32 | legs << Leg.new(nil, hkg, Date.new(2013, 6, 14), lgb, Date.new(2013, 6, 18)) 33 | legs << Leg.new(nil, lgb, Date.new(2013, 6, 19), dal, Date.new(2013, 6, 21)) 34 | itinerary = Itinerary.new(legs) 35 | 36 | # Delivery.derived_from 37 | 38 | cargo.transport_status.should_not be_true # not received 39 | end 40 | end -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DddSampleAppRuby 8 | <%= stylesheet_link_tag "application", :media => "all" %> 9 | <%= javascript_include_tag "application" %> 10 | <%= csrf_meta_tags %> 11 | 12 | 13 | 14 | 15 | 34 | 35 |
36 | <%= flash_messages %> 37 | 38 |
39 |
<%= yield %>
40 |
41 | 42 | This DDD Sample App is a Ruby version of the .NET / Java Sample App. 43 |
44 |
45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/assets/stylesheets/bootstrap_and_overrides.css.less: -------------------------------------------------------------------------------- 1 | @import "twitter/bootstrap/bootstrap"; 2 | @import "twitter/bootstrap/responsive"; 3 | @import "custom/bootstrap.cerulean.min.css"; 4 | @import "custom/bootstrap-datetimepicker.min.css"; 5 | @import "custom/header_adjust.css"; 6 | 7 | 8 | // Set the correct sprite paths 9 | @iconSpritePath: asset-path("twitter/bootstrap/glyphicons-halflings"); 10 | @iconWhiteSpritePath: asset-path("twitter/bootstrap/glyphicons-halflings-white"); 11 | 12 | // Set the Font Awesome (Font Awesome is default. You can disable by commenting below lines) 13 | @fontAwesomeEotPath: asset-url("fontawesome-webfont.eot"); 14 | @fontAwesomeEotPath_iefix: asset-url("fontawesome-webfont.eot#iefix"); 15 | @fontAwesomeWoffPath: asset-url("fontawesome-webfont.woff"); 16 | @fontAwesomeTtfPath: asset-url("fontawesome-webfont.ttf"); 17 | @fontAwesomeSvgPath: asset-url("fontawesome-webfont.svg#fontawesomeregular"); 18 | 19 | // Font Awesome 20 | @import "fontawesome/font-awesome"; 21 | 22 | // Glyphicons 23 | //@import "twitter/bootstrap/sprites.less"; 24 | 25 | // Your custom LESS stylesheets goes here 26 | // 27 | // Since bootstrap was imported above you have access to its mixins which 28 | // you may use and inherit here 29 | // 30 | // If you'd like to override bootstrap's own variables, you can do so here as well 31 | // See http://twitter.github.com/bootstrap/customize.html#variables for their names and documentation 32 | // 33 | // Example: 34 | // @linkColor: #ff0000; 35 | 36 | #error_explanation { 37 | .alert(); 38 | .alert-error(); 39 | .alert-block(); 40 | } 41 | 42 | .field_with_errors { 43 | .control-group.error(); 44 | display:inline; 45 | } -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | DddSampleAppRuby::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /ports/persistence/mongodb_adaptor/location_repository.rb: -------------------------------------------------------------------------------- 1 | require 'mongoid' 2 | 3 | class LocationRepository 4 | 5 | def initialize 6 | # TODO Move this somewhere (base class?) for all Mongoid-based repositories 7 | Mongoid.load!("#{File.dirname(__FILE__)}/../../../config/mongoid.yml", :development) 8 | end 9 | 10 | def store(location) 11 | location_document = LocationDocumentAdaptor.new.transform_to_mongoid_document(location) 12 | location_document.save 13 | end 14 | 15 | def find(unlocode) 16 | location_document = LocationDocument.find_by(location_code: unlocode.code) 17 | LocationDocumentAdaptor.new.transform_to_location(location_document) 18 | end 19 | 20 | def find_all() 21 | locations = Array.new() 22 | LocationDocument.each do | location_document | 23 | locations << LocationDocumentAdaptor.new.transform_to_location(location_document) 24 | end 25 | locations 26 | end 27 | 28 | # TODO Do something cleaner than this for data setup/teardown - yikes! 29 | def nuke_all_locations 30 | LocationDocument.delete_all 31 | end 32 | end 33 | 34 | class LocationDocument 35 | include Mongoid::Document 36 | 37 | field :location_code, type: String 38 | field :location_name, type: String 39 | end 40 | 41 | class LocationDocumentAdaptor 42 | def transform_to_mongoid_document(location) 43 | location_document = LocationDocument.new( 44 | location_code: location.unlocode.code, 45 | location_name: location.name 46 | ) 47 | location_document 48 | end 49 | 50 | def transform_to_location(location_document) 51 | Location.new(UnLocode.new(location_document[:location_code]), location_document[:location_name]) 52 | end 53 | end -------------------------------------------------------------------------------- /app/models/booking.rb: -------------------------------------------------------------------------------- 1 | class Booking 2 | include ActiveModel::Validations 3 | 4 | attr_accessor :origin, :destination, :arrival_deadline 5 | 6 | def initialize(attributes = {}) 7 | attributes.each do |name, value| 8 | send("#{name}=", value) 9 | end 10 | end 11 | 12 | validates_presence_of :origin, :destination, :arrival_deadline 13 | validate :origin_must_not_equal_destination 14 | validate :arrival_deadline_must_be_in_the_future 15 | 16 | def origin_must_not_equal_destination 17 | errors.add(:destination, "cannot equal origin") if (self.origin == self.destination) 18 | end 19 | 20 | def arrival_deadline_must_be_in_the_future 21 | return if arrival_deadline.blank? 22 | errors.add(:arrival_deadline, "must be in the future") if Date.parse(arrival_deadline) <= Date.today 23 | end 24 | 25 | def as_cargo # convert booking to a cargo 26 | origin_location = Location.new(UnLocode.new(origin), Location::CODES[origin]) 27 | destination_location = Location.new(UnLocode.new(destination), Location::CODES[destination]) 28 | deadline = Date.parse(arrival_deadline) 29 | 30 | route_specification = RouteSpecification.new(origin_location, destination_location, deadline) 31 | 32 | itinerary = Itinerary.new([ 33 | Leg.new('Sharp Shipping', origin_location, deadline-5.days, destination_location, deadline-3.days) 34 | ]) 35 | 36 | cargo = Cargo.new(TrackingId.new("cargo_#{rand(36**6).to_s(36)}"), route_specification) 37 | cargo.assign_to_route(itinerary) 38 | cargo 39 | end 40 | 41 | def to_flash 42 | "#{Location::CODES[origin]} -> #{Location::CODES[destination]} by #{arrival_deadline}" 43 | end 44 | 45 | # this is here because of some Rails bug 46 | def to_key; nil end 47 | end -------------------------------------------------------------------------------- /spec/infrastructure/location_repository_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | require 'location_repository' 4 | 5 | describe "LocationRepository" do 6 | it "Location can be stored and then found again by UN/LOCODE" do 7 | location_repository = LocationRepository.new 8 | 9 | # TODO Replace this quick-and-dirty data teardown... 10 | location_repository.nuke_all_locations 11 | 12 | location = Location.new(UnLocode.new('HKG'), 'Hong Kong') 13 | location_repository.store(location) 14 | 15 | found_location = location_repository.find(UnLocode.new('HKG')) 16 | found_location.should == location 17 | end 18 | 19 | it "All locations can be found" do 20 | locations = { 21 | 'USCHI' => 'Chicago', 22 | 'USDAL' => 'Dallas', 23 | 'DEHAM' => 'Hamburg', 24 | 'CNHGH' => 'Hangzhou', 25 | 'FIHEL' => 'Helsinki', 26 | 'HKHKG' => 'Hongkong', 27 | 'AUMEL' => 'Melbourne', 28 | 'USLGB' => 'Long Beach', 29 | 'USNYC' => 'New York', 30 | 'NLRTM' => 'Rotterdam', 31 | 'USSEA' => 'Seattle', 32 | 'CNSHA' => 'Shanghai', 33 | 'SESTO' => 'Stockholm', 34 | 'JNTKO' => 'Tokyo' 35 | } 36 | location_repository = LocationRepository.new 37 | 38 | # TODO Replace this quick-and-dirty data teardown... 39 | location_repository.nuke_all_locations 40 | 41 | locations.each do | code, name | 42 | location_repository.store(Location.new(UnLocode.new(code), name)) 43 | end 44 | 45 | found_locations = location_repository.find_all() 46 | found_locations.size.should == 14 47 | # TODO Add comparing each individual location 48 | end 49 | end -------------------------------------------------------------------------------- /domain/cargo/cargo.rb: -------------------------------------------------------------------------------- 1 | class Cargo 2 | attr_accessor :tracking_id 3 | attr_accessor :route_specification 4 | attr_accessor :itinerary 5 | attr_accessor :delivery 6 | 7 | class InitializationError < RuntimeError; end 8 | 9 | def initialize (tracking_id, route_specification) 10 | raise InitializationError unless tracking_id && route_specification 11 | 12 | @tracking_id = tracking_id 13 | @route_specification = route_specification 14 | @delivery = Delivery.new(@route_specification, @itinerary, nil) 15 | end 16 | 17 | # cf. https://github.com/SzymonPobiega/DDDSample.Net/blob/master/DDDSample-Vanilla/Domain/Cargo/Cargo.cs#L55 18 | def specify_new_route (route_specification) 19 | # TODO: add exception checking for invalid (null) values 20 | @route_specification = route_specification 21 | # TODO: Change to @delivery = Delivery.update_on_routing(@route_specification, @itinerary) 22 | @delivery = Delivery.new(@route_specification, @itinerary, @delivery.last_handling_event) 23 | end 24 | 25 | # cf. https://github.com/SzymonPobiega/DDDSample.Net/blob/master/DDDSample-Vanilla/Domain/Cargo/Cargo.cs#L69 26 | def assign_to_route (itinerary) 27 | # TODO: add exception checking for invalid (null) values 28 | @itinerary = itinerary 29 | # TODO: Change to @delivery = Delivery.update_on_routing(@route_specification, @itinerary) 30 | # @delivery = Delivery.new(@route_specification, @itinerary, @delivery.last_handling_event) 31 | end 32 | 33 | # cf. https://github.com/SzymonPobiega/DDDSample.Net/blob/master/DDDSample-Vanilla/Domain/Cargo/Cargo.cs#L83 34 | def derive_delivery_progress (last_handling_event) 35 | # TODO: Change to @delivery = Delivery.derived_from(@route_specification, @itinerary)? 36 | @delivery = Delivery.new(@route_specification, @itinerary, last_handling_event) 37 | end 38 | 39 | def ==(other) 40 | self.tracking_id == other.tracking_id 41 | end 42 | end -------------------------------------------------------------------------------- /app/views/bookings/new.html.erb: -------------------------------------------------------------------------------- 1 |

Create a New Booking for a Cargo

2 | 3 | <%= form_for(@booking, :url => { :action => "create" }, :html => { :class => 'form-horizontal' }) do |f| %> 4 | 5 | <% if @booking.errors.any? %> 6 |
7 |

8 | <%= pluralize(@booking.errors.count, "error") %> 9 | prohibited this Booking from being saved: 10 |

11 |

There were problems with the following fields:

12 | 17 |

Please correct these errors and try again.

18 |
19 | <% end %> 20 | 21 |
22 | Enter Booking Details 23 | 24 |
25 | <%= f.label :origin, :class => 'control-label' %> 26 |
27 | <%= f.select :origin, options_for_select(Location::CODES.invert), {}, {:class => 'text_field'} %> 28 |
29 |
30 | 31 |
32 | <%= f.label :destination, :class => 'control-label' %> 33 |
34 | <%= f.select :destination, options_for_select(Location::CODES.invert), {}, {:class => 'text_field'} %> 35 |
36 |
37 | 38 |
39 | <%= f.label :arrival_deadline, :class => 'control-label' %> 40 |
41 |
42 | <%= f.text_field :arrival_deadline, :class => 'text_field', 'data-format' => 'yyyy-MM-dd' %> 43 | 44 | 45 | 46 |
47 |
48 |
49 | 50 |
51 | <%= f.submit 'Save Booking', :class => 'btn btn-primary' %> 52 | <%= link_to 'Cancel', bookings_path, :class => 'btn' %> 53 |
54 |
55 | <% end %> 56 | 57 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | DddSampleAppRuby::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /spec/domain/cargo/route_specification_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'route_specification' 3 | 4 | describe RouteSpecification do 5 | 6 | context "initialize()" do 7 | {'origin is nil' => [nil, 'something', 'something'], 8 | 'destination is nil' => ['something', nil, 'something'], 9 | 'arrival_deadline is nil' => ['something', 'something', nil], 10 | }.each do |test, params| 11 | it "should raise an error if #{test}" do 12 | expect { 13 | RouteSpecification.new(*params) 14 | }.to raise_error(RouteSpecification::InitializationError) 15 | end 16 | end # loop 17 | 18 | it "should not raise an error if all three are passed in" do 19 | expect { 20 | RouteSpecification.new('x', 'x', 'x') 21 | }.to_not raise_error 22 | end 23 | end # context initialize() 24 | 25 | 26 | 27 | context "is_satisfied_by()" do 28 | require 'location' 29 | require 'itinerary' 30 | require 'leg' 31 | 32 | before do 33 | @krakow = Location.new('PLKRK', 'Krakow') 34 | @warszawa = Location.new('PLWAW', 'Warszawa') 35 | @wroclaw = Location.new('PLWRC', 'Wroclaw') 36 | @arrival_deadline = Date.new(2011,12,24) 37 | @route_specification = RouteSpecification.new(@krakow, @wroclaw, @arrival_deadline) 38 | end 39 | 40 | it "should be satisfied if origin and destination match and arrival deadline not missed" do 41 | itinerary = Itinerary.new([ 42 | Leg.new(nil, @krakow, Date.new(2011,12,1), @warszawa, Date.new(2011,12,2)), 43 | Leg.new(nil, @warszawa, Date.new(2011,12,13), @wroclaw, @arrival_deadline) 44 | ]) 45 | @route_specification.is_satisfied_by(itinerary).should be_true 46 | end 47 | 48 | it "should not be satisfied if arrival deadline is missed" do 49 | itinerary = Itinerary.new([ 50 | Leg.new(nil, @krakow, Date.new(2011,12,1), @warszawa, Date.new(2011,12,2)), 51 | Leg.new(nil, @warszawa, Date.new(2011,12,13), @wroclaw, Date.new(2011,12,25)) 52 | ]) 53 | @route_specification.is_satisfied_by(itinerary).should be_false 54 | end 55 | 56 | it "should not be satisfied if origin does not match" do 57 | itinerary = Itinerary.new([ 58 | Leg.new(nil, @warszawa, Date.new(2011,12,13), @wroclaw, Date.new(2011,12,15)), 59 | ]) 60 | @route_specification.is_satisfied_by(itinerary).should be_false 61 | end 62 | 63 | it "should not be satisfied if destination does not match" do 64 | itinerary = Itinerary.new([ 65 | Leg.new(nil, @krakow, Date.new(2011,12,1), @warszawa, Date.new(2011,12,2)), 66 | ]) 67 | @route_specification.is_satisfied_by(itinerary).should be_false 68 | end 69 | 70 | end # context is_satisfied_by() 71 | 72 | end 73 | 74 | 75 | -------------------------------------------------------------------------------- /ports/persistence/mongodb_adaptor/handling_event_repository.rb: -------------------------------------------------------------------------------- 1 | require 'mongoid' 2 | 3 | class HandlingEventRepository 4 | 5 | def initialize 6 | # TODO Move this somewhere (base class?) for all Mongoid-based repositories 7 | Mongoid.load!("#{File.dirname(__FILE__)}/../../../config/mongoid.yml", :development) 8 | end 9 | 10 | def store(handling_event) 11 | handling_event_document = HandlingEventDocumentAdaptor.new.transform_to_mongoid_document(handling_event) 12 | handling_event_document.save 13 | end 14 | 15 | def lookup_handling_history_of_cargo(tracking_id) 16 | handling_event_history = Array.new() 17 | HandlingEventDocument.where(tracking_id: tracking_id.id).each do | handling_event_document | 18 | handling_event_history << HandlingEventDocumentAdaptor.new.transform_to_handling_event(handling_event_document) 19 | end 20 | handling_event_history 21 | end 22 | 23 | def find(event_id) 24 | handling_event_document = HandlingEventDocument.find_by(event_id: event_id) 25 | HandlingEventDocumentAdaptor.new.transform_to_handling_event(handling_event_document) 26 | end 27 | 28 | # TODO Do something cleaner than this for data setup/teardown - yikes! 29 | def nuke_all_handling_events 30 | HandlingEventDocument.delete_all 31 | end 32 | end 33 | 34 | class HandlingEventDocument 35 | include Mongoid::Document 36 | 37 | field :event_id, type: String 38 | field :tracking_id, type: String 39 | field :event_type, type: String 40 | field :location_code, type: String 41 | field :location_name, type: String 42 | field :registration_date, type: DateTime 43 | field :completion_date, type: DateTime 44 | end 45 | 46 | class HandlingEventDocumentAdaptor 47 | def transform_to_mongoid_document(handling_event) 48 | handling_event_document = HandlingEventDocument.new( 49 | event_id: handling_event.id, 50 | tracking_id: handling_event.tracking_id.id, 51 | event_type: handling_event.event_type, 52 | location_code: handling_event.location.unlocode.code, 53 | location_name: handling_event.location.name, 54 | registration_date: handling_event.registration_date, 55 | completion_date: handling_event.completion_date 56 | ) 57 | handling_event_document 58 | end 59 | 60 | def transform_to_handling_event(handling_event_document) 61 | id = handling_event_document[:event_id] 62 | tracking_id = handling_event_document[:tracking_id] 63 | event_type = HandlingEventType.parse(handling_event_document[:event_type]) 64 | location = Location.new(UnLocode.new(handling_event_document[:location_code]), handling_event_document[:location_name]) 65 | registration_date = handling_event_document[:registration_date] 66 | completion_date = handling_event_document[:completion_date] 67 | HandlingEvent.new(event_type, location, registration_date, completion_date, tracking_id, id) 68 | end 69 | end -------------------------------------------------------------------------------- /spec/infrastructure/handling_event_repository_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | require 'handling_event_repository' 4 | 5 | describe "HandlingEventRepository" do 6 | it "Handling events can be persisted and retrieved by id" do 7 | handling_event_repository = HandlingEventRepository.new 8 | 9 | # TODO Replace this quick-and-dirty data teardown... 10 | handling_event_repository.nuke_all_handling_events 11 | 12 | origin = Location.new(UnLocode.new('HKG'), 'Hong Kong') 13 | port = Location.new(UnLocode.new('LGB'), 'Long Beach') 14 | tracking_id = TrackingId.new('cargo_1234') 15 | handling_event = HandlingEvent.new(HandlingEventType::Load, origin, DateTime.new(2013, 6, 14), DateTime.new(2013, 6, 15), tracking_id, HandlingEvent.new_id) 16 | handling_event_repository.store(handling_event) 17 | 18 | found_handling_event = handling_event_repository.find(handling_event.id) 19 | 20 | found_handling_event.id.should == handling_event.id 21 | found_handling_event.event_type.should == HandlingEventType::Load 22 | found_handling_event.location.should == origin 23 | found_handling_event.registration_date.should == DateTime.new(2013, 6, 14) 24 | found_handling_event.completion_date.should == DateTime.new(2013, 6, 15) 25 | found_handling_event.tracking_id.should == 'cargo_1234' 26 | end 27 | 28 | it "Multiple handling events can be persisted and retrieved for a cargo" do 29 | handling_event_repository = HandlingEventRepository.new 30 | 31 | # TODO Replace this quick-and-dirty data teardown... 32 | handling_event_repository.nuke_all_handling_events 33 | 34 | origin = Location.new(UnLocode.new('HKG'), 'Hong Kong') 35 | port = Location.new(UnLocode.new('LGB'), 'Long Beach') 36 | tracking_id = TrackingId.new('cargo_1234') 37 | handling_event1 = HandlingEvent.new(HandlingEventType::Load, origin, DateTime.new(2013, 6, 14), DateTime.new(2013, 6, 15), tracking_id, HandlingEvent.new_id) 38 | handling_event_repository.store(handling_event1) 39 | handling_event2 = HandlingEvent.new(HandlingEventType::Unload, port, DateTime.new(2013, 6, 18), DateTime.new(2013, 6, 18), tracking_id, HandlingEvent.new_id) 40 | handling_event_repository.store(handling_event2) 41 | 42 | handling_event_history = handling_event_repository.lookup_handling_history_of_cargo(tracking_id) 43 | 44 | handling_event_history.count.should == 2 45 | 46 | first_handling_event = handling_event_history[0] 47 | first_handling_event.id.should == handling_event1.id 48 | first_handling_event.event_type.should == HandlingEventType::Load 49 | first_handling_event.location.should == origin 50 | first_handling_event.registration_date.should == DateTime.new(2013, 6, 14) 51 | first_handling_event.completion_date.should == DateTime.new(2013, 6, 15) 52 | first_handling_event.tracking_id.should == 'cargo_1234' 53 | 54 | second_handling_event = handling_event_history[1] 55 | second_handling_event.id.should == handling_event2.id 56 | second_handling_event.event_type.should == HandlingEventType::Unload 57 | second_handling_event.location.should == port 58 | second_handling_event.registration_date.should == DateTime.new(2013, 6, 18) 59 | second_handling_event.completion_date.should == DateTime.new(2013, 6, 18) 60 | second_handling_event.tracking_id.should == 'cargo_1234' 61 | end 62 | end -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "action_controller/railtie" 4 | require "action_mailer/railtie" 5 | require "active_resource/railtie" 6 | require "rails/test_unit/railtie" 7 | 8 | if defined?(Bundler) 9 | # If you precompile assets before deploying to production, use this line 10 | Bundler.require(*Rails.groups(:assets => %w(development test))) 11 | # If you want your assets lazily compiled in production, use this line 12 | # Bundler.require(:default, :assets, Rails.env) 13 | end 14 | 15 | module DddSampleAppRuby 16 | class Application < Rails::Application 17 | # Settings in config/environments/* take precedence over those specified here. 18 | # Application configuration should go into files in config/initializers 19 | # -- all .rb files in that directory are automatically loaded. 20 | 21 | # Custom directories with classes and modules you want to be autoloadable. 22 | # config.autoload_paths += %W(#{config.root}/domain/location ) 23 | # config.autoload_paths += %W(#{config.root}/domain/cargo ) 24 | # config.autoload_paths += %W(#{config.root}/domain/handling ) 25 | # config.autoload_paths += %W(#{config.root}/domain/voyage ) 26 | # config.autoload_paths += %W(#{config.root}/ports/persistence/mongodb_adaptor) 27 | 28 | # Only load the plugins named here, in the order given (default is alphabetical). 29 | # :all can be used as a placeholder for all plugins not explicitly named. 30 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 31 | 32 | # Activate observers that should always be running. 33 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 34 | 35 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 36 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 37 | # config.time_zone = 'Central Time (US & Canada)' 38 | 39 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 40 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 41 | # config.i18n.default_locale = :de 42 | 43 | # Configure the default encoding used in templates for Ruby 1.9. 44 | config.encoding = "utf-8" 45 | 46 | # Configure sensitive parameters which will be filtered from the log file. 47 | config.filter_parameters += [:password] 48 | 49 | # Enable escaping HTML in JSON. 50 | config.active_support.escape_html_entities_in_json = true 51 | 52 | # Use SQL instead of Active Record's schema dumper when creating the database. 53 | # This is necessary if your schema can't be completely dumped by the schema dumper, 54 | # like if you have constraints or database-specific column types 55 | # config.active_record.schema_format = :sql 56 | 57 | # Enforce whitelist mode for mass assignment. 58 | # This will create an empty whitelist of attributes available for mass-assignment for all models 59 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 60 | # parameters by using an attr_accessible or attr_protected declaration. 61 | # config.active_record.whitelist_attributes = true 62 | 63 | # Enable the asset pipeline 64 | config.assets.enabled = true 65 | 66 | # Version of your assets, change this if you want to expire all your assets 67 | config.assets.version = '1.0' 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/domain/itinerary_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | 4 | # TODO Implement itinerary specs - probably need to be renamed to fit rspec idiom 5 | if false 6 | 7 | def handling_event_fake(location, handling_event_type) 8 | registration_date = Date.new(2013, 6, 21) 9 | completion_date = Date.new(2013, 6, 21) 10 | 11 | # TODO How is it possible to have a HandlingEvent with a nil Cargo? 12 | #unload_handling_event = HandlingEvent.new(unloaded, @port, registration_date, completion_date, nil, , HandlingEvent.new_id) 13 | HandlingEvent.new(handling_event_type, location, registration_date, completion_date, nil, HandlingEvent.new_id) 14 | end 15 | 16 | 17 | describe "Itinerary" do 18 | 19 | before(:each) do 20 | @origin = Location.new(UnLocode.new('HKG'), 'Hong Kong') 21 | @destination = Location.new(UnLocode.new('DAL'), 'Dallas') 22 | arrival_deadline = Date.new(2013, 7, 1) 23 | @route_spec = RouteSpecification.new(@origin, @destination, arrival_deadline) 24 | 25 | @port = Location.new(UnLocode.new('LGB'), 'Long Beach') 26 | legs = Array.new 27 | legs << Leg.new('Voyage ABC', @origin, Date.new(2013, 6, 14), @port, Date.new(2013, 6, 19)) 28 | legs << Leg.new('Voyage DEF', @port, Date.new(2013, 6, 21), @destination, Date.new(2013, 6, 24)) 29 | @itinerary = Itinerary.new(legs) 30 | end 31 | 32 | # TODO .NET version does var cargoWithEmptyItinerary = new Itinerary(new Leg[] { }); 33 | # How is this even a valid Itinerary? How can an Itinerary be "empty"? In other 34 | # words, if an Itinerary by definition is an ordered set of Legs, how is the notion 35 | # of an empty Itinerary even coherent? The Java version throws an exception for an 36 | # empty Itinerary. 37 | # it "Claim event is not expected by an empty itinerary" do 38 | # end 39 | 40 | it "Receive event is expected when first leg load location matches event location" do 41 | @itinerary.is_expected(handling_event_fake(@origin, "Receive")).should be_true 42 | end 43 | 44 | it "Receive event is not expected when first leg load location doesn't match event location" do 45 | @itinerary.is_expected(handling_event_fake(@port, "Receive")).should be_false 46 | end 47 | 48 | it "Claim event is expected when last leg unload location matches event location" do 49 | @itinerary.is_expected(handling_event_fake(@destination, "Claim")).should be_true 50 | end 51 | 52 | it "Claim event is not expected when last leg unload location doesnt match event location" do 53 | @itinerary.is_expected(handling_event_fake(@port, "Claim")).should be_false 54 | end 55 | 56 | it "Load event is expected when first leg load location matches event location" do 57 | @itinerary.is_expected(handling_event_fake(@origin, "Load")).should be_true 58 | end 59 | 60 | it "Load event is expected when second leg load location matches event location" do 61 | @itinerary.is_expected(handling_event_fake(@port, "Load")).should be_true 62 | end 63 | 64 | it "Load event is not expected when event location doesn't match any legs load location" do 65 | @itinerary.is_expected(handling_event_fake(@destination, "Load")).should be_false 66 | end 67 | 68 | it "Unload event is expected when first leg unload location matches event location" do 69 | @itinerary.is_expected(handling_event_fake(@port, "Unload")).should be_true 70 | end 71 | 72 | it "Unload event is expected when second leg unload location matches event location" do 73 | @itinerary.is_expected(handling_event_fake(@destination, "Unload")).should be_true 74 | end 75 | 76 | it "Unload event is not expected when event location doesn't match any legs unload location" do 77 | @itinerary.is_expected(handling_event_fake(@origin, "Unload")).should be_false 78 | end 79 | end 80 | 81 | end -------------------------------------------------------------------------------- /spec/infrastructure/sample_data_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | require 'cargo_repository' 4 | require 'handling_event_repository' 5 | require 'location_repository' 6 | 7 | 8 | # TODO Massive hack to get sample data into MongoDB 9 | # for manual testing purposes. Remove ASAP. 10 | describe "Sample Data" do 11 | it "Sample data can be set up" do 12 | DemoData.new.create_sample_data 13 | true.should == true 14 | end 15 | end 16 | 17 | class DemoData 18 | def initialize 19 | 20 | # TODO Add missing locations to support these Maersk routes 21 | # CN:Xingang,CN:Dalian,CN:Qingdao, US:Longbeach, US:Oakland - Maersk Transpacific 8 (eastbound) 22 | # Taiwan:Kaohsiung, HKHKG, CN:Xiamen, CNSHA, CN:Ningbo, USLGB - Transpacific 2 (eastbound) 23 | 24 | # Transpacific 6 - eastbound 25 | # Tanjung Pelepas, Malaysia FRI SUN -- 26 | # Ho Chi Minh Ci􏰀 (Vungtau), Vietnam TUE TUE 27 | # Nansha, Mainland China FRI SAT 28 | # Yantian, Mainland China SAT SUN 29 | # Hong Kong, Hong Kong SUN MON 30 | # Los Angeles, CA, USA SUN THU 31 | 32 | @locations = { 33 | 'USCHI' => 'Chicago', 34 | 'USDAL' => 'Dallas', 35 | 'DEHAM' => 'Hamburg', 36 | 'CNHGH' => 'Hangzhou', 37 | 'FIHEL' => 'Helsinki', 38 | 'HKHKG' => 'Hongkong', 39 | 'AUMEL' => 'Melbourne', 40 | 'USLGB' => 'Long Beach', 41 | 'USNYC' => 'New York', 42 | 'NLRTM' => 'Rotterdam', 43 | 'USSEA' => 'Seattle', 44 | 'CNSHA' => 'Shanghai', 45 | 'SESTO' => 'Stockholm', 46 | 'JNTKO' => 'Tokyo' 47 | } 48 | 49 | @location_repository = LocationRepository.new 50 | @cargo_repository = CargoRepository.new 51 | @handling_event_repository = HandlingEventRepository.new 52 | 53 | end 54 | 55 | def create_sample_data 56 | # TODO Replace quick-and-dirty data teardown... 57 | @cargo_repository.nuke_all_cargo 58 | @handling_event_repository.nuke_all_handling_events 59 | @location_repository.nuke_all_locations 60 | 61 | @locations.each do | code, name | 62 | @location_repository.store(Location.new(UnLocode.new(code), name)) 63 | end 64 | 65 | # Cargo 1 66 | cargo_factory(TrackingId.new('cargo_1234'), 'HKHKG', 'USLGB', 'USDAL', DateTime.new(2013, 7, 1)) 67 | # Cargo 2 68 | cargo_factory(TrackingId.new('cargo_5678'), 'HKHKG', 'USSEA', 'USCHI', DateTime.new(2013, 7, 2)) 69 | # Cargo 3 70 | cargo_factory(TrackingId.new('cargo_9012'), 'CNSHA', 'USSEA', 'USNYC', DateTime.new(2013, 7, 5)) 71 | end 72 | 73 | def cargo_factory(tracking_id, origin_code, port_code, destination_code, arrival_deadline) 74 | origin = Location.new(UnLocode.new(origin_code), @locations[origin_code]) 75 | port = Location.new(UnLocode.new(port_code), @locations[port_code]) 76 | destination = Location.new(UnLocode.new(destination_code), @locations[destination_code]) 77 | route_spec = RouteSpecification.new(origin, destination, arrival_deadline) 78 | cargo = Cargo.new(tracking_id, route_spec) 79 | 80 | legs = Array.new 81 | legs << Leg.new('Voyage GHI', origin, DateTime.new(2013, 6, 14), port, DateTime.new(2013, 6, 19)) 82 | legs << Leg.new('Voyage JKL', port, DateTime.new(2013, 6, 21), destination, DateTime.new(2013, 6, 24)) 83 | itinerary = Itinerary.new(legs) 84 | cargo.assign_to_route(itinerary) 85 | @cargo_repository.store(cargo) 86 | 87 | handling_event = HandlingEvent.new(HandlingEventType::Load, origin, DateTime.new(2013, 6, 14), DateTime.new(2013, 6, 15), tracking_id, HandlingEvent.new_id) 88 | @handling_event_repository.store(handling_event) 89 | 90 | cargo.derive_delivery_progress(handling_event) 91 | @cargo_repository.store(cargo) 92 | end 93 | end -------------------------------------------------------------------------------- /spec/lib/value_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'value_object' 3 | 4 | describe ValueObject do 5 | 6 | # Use an anonymous class to test ValueObject 7 | let(:klass) do 8 | Class.new(ValueObject) do 9 | attr_reader :attr1 10 | attr_reader :attr2 11 | 12 | def initialize(attr1, attr2) 13 | @attr1 = attr1 14 | @attr2 = attr2 15 | end 16 | end 17 | end 18 | 19 | # Has to be a method def instead of the usual RSpec let() 20 | # because the latter acts more like a variable that persists 21 | # throught an example no matter how many times you call it 22 | def random_string 23 | (0...8).map { (65 + rand(26)).chr }.join 24 | end 25 | 26 | 27 | context "::attr_reader" do 28 | 29 | it "adds a new attribute to the object" do 30 | value1 = random_string 31 | value2 = random_string 32 | 33 | obj = klass.new(value1, value2) 34 | 35 | obj.attr1.should == value1 36 | obj.attr2.should == value2 37 | end 38 | 39 | it "adds the new attribute to the equality list" do 40 | klass.send(:equality_list).should == [:attr1, :attr2] 41 | end 42 | 43 | 44 | it "does not share equality lists between child classes" do 45 | klass2 = Class.new(ValueObject) do 46 | attr_reader :klass1_attr1 47 | attr_reader :klass2_attr2 48 | 49 | def initialize(attr1, attr2) 50 | @attr1 = attr1 51 | @attr2 = attr2 52 | end 53 | end 54 | 55 | klass.send(:equality_list).should_not == klass2.send(:equality_list) 56 | end 57 | 58 | end # context ::attr_reader 59 | 60 | 61 | context "#==" do 62 | 63 | it "returns true if all attributes in the equality list are equal" do 64 | value1 = random_string 65 | value2 = random_string 66 | 67 | obj1 = klass.new(value1, value2) 68 | obj2 = klass.new(value1, value2) 69 | 70 | (obj1 == obj2).should be_true 71 | end 72 | 73 | it "returns false if at least one attribute in the equality list doesn't match" do 74 | value1 = random_string 75 | value2 = random_string 76 | 77 | obj1 = klass.new(value1, value2) 78 | obj2 = klass.new(value1, value1) 79 | 80 | (obj1 == obj2).should be_false 81 | end 82 | 83 | it "returns true if its attribute containing array of value objects are equal" do 84 | child_klass = klass 85 | 86 | parent_klass = Class.new(ValueObject) do 87 | attr_reader :children 88 | 89 | def initialize(children) 90 | @children = children 91 | end 92 | end 93 | 94 | value1 = random_string 95 | value2 = random_string 96 | 97 | child1a = child_klass.new(value1, value2) 98 | child1b = child1a.dup 99 | 100 | child2a = child_klass.new(value2, value1) 101 | child2b = child2a.dup 102 | 103 | parent1 = parent_klass.new([child1a, child2a]) 104 | parent2 = parent_klass.new([child1b, child2b]) 105 | 106 | (parent1 == parent2).should be_true 107 | end 108 | 109 | it "returns false if its attribute containing array of value objects are not equal" do 110 | child_klass = klass 111 | 112 | parent_klass = Class.new(ValueObject) do 113 | attr_reader :children 114 | 115 | def initialize(children) 116 | @children = children 117 | end 118 | end 119 | 120 | value1 = random_string 121 | value2 = random_string 122 | 123 | child1a = child_klass.new(value1, value2) 124 | child1b = child1a.dup 125 | 126 | child2a = child_klass.new(value2, value1) 127 | child2b = child2a.dup 128 | 129 | parent1 = parent_klass.new([child1a, child2a]) 130 | parent2 = parent_klass.new([child2b, child2b]) 131 | 132 | (parent1 == parent2).should be_false 133 | end 134 | 135 | end # context #== 136 | 137 | end 138 | -------------------------------------------------------------------------------- /spec/domain/cargo/cargo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'cargo' 3 | require 'delivery' 4 | 5 | # # this doesn't work when running with all specs 6 | # class Delivery < Struct.new(:one, :two, :three); end 7 | 8 | # reopen Delivery to allow stubbing (otherwise it's frozen) 9 | # class Delivery 10 | # def initialize(route_specification='x', itinerary=nil, last_handled_event=nil); end 11 | # end 12 | 13 | 14 | describe Cargo do 15 | 16 | context "initialize()" do 17 | it "should raise an error if no tracking_id passed in" do 18 | expect { 19 | Cargo.new(nil, 'something') 20 | }.to raise_error(Cargo::InitializationError) 21 | end 22 | 23 | it "should raise an error if no route_specification passed in" do 24 | expect { 25 | Cargo.new('something', nil) 26 | }.to raise_error(Cargo::InitializationError) 27 | end 28 | 29 | it "should not raise an error if a tracking_id and route_specification are passed in" do 30 | expect { 31 | Cargo.new('something', 'something') 32 | }.to_not raise_error 33 | end 34 | 35 | it "should create a delivery object" do 36 | Delivery.stub(:new 37 | ).and_return('something') 38 | cargo = Cargo.new('something', 'something') 39 | cargo.delivery.should_not be_nil 40 | cargo.delivery.should == 'something' 41 | end 42 | end # context initialize() 43 | 44 | 45 | context "entity equality" do 46 | it "should equal a cargo with the same tracking id" do 47 | @cargo = Cargo.new(TrackingId.new('999'), 'fake route') 48 | @cargo.should == Cargo.new(TrackingId.new('999'), 'another fake route') 49 | end 50 | 51 | it "should not equal a cargo with a different cargo number" do 52 | @cargo = Cargo.new(TrackingId.new('999'), 'fake route') 53 | @cargo.should_not == Cargo.new(TrackingId.new('555'), 'fake route') 54 | end 55 | end 56 | 57 | context "specify_new_route()" do 58 | before do 59 | @cargo = Cargo.new('tracking', 'route') 60 | @delivery = @cargo.delivery 61 | @cargo.specify_new_route('new_route') 62 | end 63 | 64 | it "should update the route_specification with the passed in value" do 65 | @cargo.route_specification.should == 'new_route' 66 | end 67 | 68 | it "should update the delivery object" do 69 | @cargo.delivery.should_not == @delivery 70 | end 71 | end # context specify_new_route() 72 | 73 | context "assign_to_route()" do 74 | before do 75 | @cargo = Cargo.new('tracking', 'route') 76 | @itinerary = @cargo.itinerary 77 | @cargo.assign_to_route('new_itinerary') 78 | end 79 | 80 | it "should update the itinerary with the passed in value" do 81 | @cargo.itinerary.should == 'new_itinerary' 82 | end 83 | 84 | it "should have a different itinerary" do 85 | @cargo.itinerary.should_not == @itinerary 86 | end 87 | end # context assign_to_route() 88 | 89 | context "derive_delivery_progress()" do 90 | before do 91 | Delivery.stub(:new).and_return(double('delivery', :last_handling_event => 'last_event')) 92 | @cargo = Cargo.new('tracking', 'route') 93 | @delivery = @cargo.delivery 94 | # stub it again to have it return a different double 95 | Delivery.stub(:new).and_return(double('delivery', :last_handling_event => 'last_event')) 96 | @cargo.derive_delivery_progress('last_event') 97 | end 98 | 99 | it "should have a delivery" do 100 | @cargo.delivery.should_not be_nil 101 | end 102 | 103 | it "should be a different delivery" do 104 | @cargo.delivery.should_not == @delivery 105 | end 106 | 107 | end # context derive_delivery_progress() 108 | 109 | context "checking that Delivery value objects are created" do 110 | it "should create new Delivery on cargo initialization" do 111 | Delivery.should_receive(:new) 112 | Cargo.new('tracking', 'route') 113 | end 114 | 115 | it "should create new Delivery on specify_new_route" do 116 | cargo = Cargo.new('tracking', 'route') 117 | Delivery.should_receive(:new) 118 | cargo.specify_new_route('something') 119 | end 120 | 121 | it "should create new Delivery on derive_delivery_progress" do 122 | cargo = Cargo.new('tracking', 'route') 123 | Delivery.should_receive(:new) 124 | cargo.derive_delivery_progress('something') 125 | end 126 | 127 | it "should not create new Delivery on assign_to_route" do 128 | cargo = Cargo.new('tracking', 'route') 129 | Delivery.should_not_receive(:new) 130 | cargo.assign_to_route('something') 131 | end 132 | end # context checking Delivery object creation 133 | end -------------------------------------------------------------------------------- /domain/cargo/delivery.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'ice_nine' 3 | require 'value_object' 4 | 5 | class Delivery < ValueObject 6 | attr_reader :transport_status 7 | attr_reader :last_known_location 8 | attr_reader :is_misdirected 9 | attr_reader :eta 10 | attr_reader :is_unloaded_at_destination 11 | attr_reader :routing_status 12 | attr_reader :calculated_at 13 | attr_reader :last_handling_event 14 | attr_reader :next_expected_activity 15 | 16 | class InitializationError < RuntimeError; end 17 | 18 | def initialize(route_specification, itinerary, last_handling_event) 19 | raise InitializationError unless route_specification 20 | 21 | @last_handling_event = last_handling_event 22 | @routing_status = calculate_routing_status(itinerary, route_specification) 23 | @transport_status = calculate_transport_status(last_handling_event) 24 | @last_known_location = calculate_last_known_location(last_handling_event) 25 | @is_misdirected = calculate_misdirection_status(last_handling_event, itinerary) 26 | @is_unloaded_at_destination = calculate_unloaded_at_destination(last_handling_event, route_specification) 27 | @eta = calculate_eta(itinerary) 28 | @next_expected_activity = calculate_next_expected_activity(last_handling_event, route_specification, itinerary) 29 | @calculated_at = DateTime.now 30 | 31 | IceNine.deep_freeze(self) 32 | end 33 | 34 | def self.derived_from(route_specification, itinerary, last_handling_event) 35 | Delivery.new(route_specification, itinerary, last_handling_event) 36 | end 37 | 38 | def on_track? 39 | @routing_status == RoutingStatus::Routed && is_misdirected == false 40 | end 41 | 42 | private 43 | 44 | def calculate_last_known_location(last_handling_event) 45 | if last_handling_event.nil? 46 | return nil 47 | end 48 | last_handling_event.location 49 | end 50 | 51 | def calculate_unloaded_at_destination(last_handling_event, route_specification) 52 | if last_handling_event.nil? 53 | return false 54 | end 55 | last_handling_event.event_type == HandlingEventType::Unload && 56 | last_handling_event.location == route_specification.destination 57 | end 58 | 59 | def calculate_misdirection_status(last_handling_event, itinerary) 60 | if itinerary.nil? 61 | return false 62 | end 63 | if last_handling_event.nil? 64 | return false 65 | end 66 | !itinerary.is_expected(last_handling_event) 67 | end 68 | 69 | def calculate_routing_status(itinerary, route_specification) 70 | if itinerary.nil? 71 | return RoutingStatus::NotRouted 72 | end 73 | route_specification.is_satisfied_by(itinerary) ? RoutingStatus::Routed : RoutingStatus::Misrouted 74 | end 75 | 76 | def calculate_transport_status(last_handling_event) 77 | if last_handling_event.nil? 78 | return TransportStatus::NotReceived 79 | end 80 | case last_handling_event.event_type 81 | when HandlingEventType::Load 82 | TransportStatus::OnboardCarrier 83 | when HandlingEventType::Unload, HandlingEventType::Receive 84 | TransportStatus::InPort 85 | when HandlingEventType::Claim 86 | TransportStatus::Claimed 87 | else 88 | TransportStatus::Unknown 89 | end 90 | end 91 | 92 | def calculate_eta(itinerary) 93 | on_track? ? itinerary.final_arrival_date : nil 94 | end 95 | 96 | def calculate_next_expected_activity(last_handling_event, route_specification, itinerary) 97 | unless on_track? 98 | return nil 99 | end 100 | if (last_handling_event.nil?) 101 | return HandlingActivity.new(HandlingEventType::Receive, route_specification.origin) 102 | end 103 | case last_handling_event.event_type 104 | when HandlingEventType::Load 105 | last_leg_index = itinerary.legs.index { |x| x.load_location == last_handling_event.location } 106 | return last_leg_index.nil? == false ? HandlingActivity.new(HandlingEventType::Unload, itinerary.legs[last_leg_index].unload_location) : nil 107 | when HandlingEventType::Unload 108 | itinerary.legs.each_cons(2) do |leg, next_leg| 109 | if (leg.unload_location == last_handling_event.location) 110 | return HandlingActivity.new(HandlingEventType::Load, next_leg.load_location) if next_leg 111 | end 112 | return HandlingActivity.new(HandlingEventType::Claim, next_leg.unload_location) 113 | end 114 | when HandlingEventType::Receive 115 | return HandlingActivity.new(HandlingEventType::Load, itinerary.legs.first.load_location) 116 | when HandlingEventType::Claim 117 | nil # TODO What to do here? .NET doesn't handle this case at all 118 | else 119 | nil # TODO What to do here? .NET returns null 120 | end 121 | end 122 | 123 | end 124 | -------------------------------------------------------------------------------- /spec/infrastructure/cargo_repository_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | require 'cargo_repository' 4 | 5 | describe "CargoRepository" do 6 | specify "Cargo assigned to route but with no delivery history can be persisted" do 7 | cargo_repository = CargoRepository.new 8 | 9 | # TODO Replace this quick-and-dirty data teardown... 10 | cargo_repository.nuke_all_cargo 11 | 12 | origin = Location.new(UnLocode.new('HKG'), 'Hong Kong') 13 | destination = Location.new(UnLocode.new('DAL'), 'Dallas') 14 | arrival_deadline = DateTime.new(2013, 7, 1) 15 | 16 | route_spec = RouteSpecification.new(origin, destination, arrival_deadline) 17 | tracking_id = TrackingId.new('cargo_1234') 18 | port = Location.new(UnLocode.new('LGB'), 'Long Beach') 19 | legs = Array.new 20 | legs << Leg.new('Voyage ABC', origin, DateTime.new(2013, 6, 14), port, DateTime.new(2013, 6, 19)) 21 | legs << Leg.new('Voyage DEF', port, DateTime.new(2013, 6, 21), destination, DateTime.new(2013, 6, 24)) 22 | itinerary = Itinerary.new(legs) 23 | cargo = Cargo.new(tracking_id, route_spec) 24 | cargo.assign_to_route(itinerary) 25 | 26 | cargo_repository.store(cargo) 27 | 28 | found_cargo = cargo_repository.find_by_tracking_id(tracking_id) 29 | 30 | found_cargo.tracking_id.should == tracking_id 31 | found_cargo.route_specification.should == route_spec 32 | # TODO Get itinerary equality passing. Seems to be bombing on date comparison...UTC? 33 | # - [Loading on voyage Voyage ABC in Hong Kong [HKG] on 2013-06-14, unloading in Hong Kong [HKG] on 2013-06-14, 34 | # - Loading on voyage Voyage DEF in Long Beach [LGB] on 2013-06-21, unloading in Long Beach [LGB] on 2013-06-21]> 35 | # + [Loading on voyage Voyage ABC in Hong Kong [HKG] on 2013-06-14 00:00:00 UTC, unloading in Hong Kong [HKG] on 2013-06-14 00:00:00 UTC, 36 | # + Loading on voyage Voyage DEF in Long Beach [LGB] on 2013-06-21 00:00:00 UTC, unloading in Long Beach [LGB] on 2013-06-21 00:00:00 UTC]> 37 | #found_cargo.itinerary.should == itinerary 38 | 39 | # TODO create test that checks for these values in the actual MongoDB document. Since 40 | # all the values are calculated when Delivery is created, these tests don't mean much 41 | # right now 42 | # found_cargo.delivery.transport_status.should == "Not Received" 43 | # found_cargo.delivery.last_known_location.should be_nil 44 | # found_cargo.delivery.is_misdirected.should be_false 45 | # found_cargo.delivery.eta.should be_nil #== DateTime.new(2013, 6, 24) 46 | # found_cargo.delivery.is_unloaded_at_destination.should be_false 47 | # found_cargo.delivery.routing_status.should be_nil 48 | # found_cargo.delivery.calculated_at.should == "junk" 49 | # found_cargo.delivery.last_handled_event.should == "junk" 50 | # found_cargo.delivery.next_expected_activity.should == "junk" 51 | end 52 | 53 | specify "Cargo with delivery history can be persisted" do 54 | cargo_repository = CargoRepository.new 55 | 56 | # TODO Replace this quick-and-dirty data teardown... 57 | cargo_repository.nuke_all_cargo 58 | 59 | origin = Location.new(UnLocode.new('HKG'), 'Hong Kong') 60 | destination = Location.new(UnLocode.new('DAL'), 'Dallas') 61 | arrival_deadline = DateTime.new(2013, 7, 1) 62 | 63 | route_spec = RouteSpecification.new(origin, destination, arrival_deadline) 64 | tracking_id = TrackingId.new('cargo_1234') 65 | port = Location.new(UnLocode.new('LGB'), 'Long Beach') 66 | legs = Array.new 67 | legs << Leg.new('Voyage ABC', origin, DateTime.new(2013, 6, 14), port, DateTime.new(2013, 6, 19)) 68 | legs << Leg.new('Voyage DEF', port, DateTime.new(2013, 6, 21), destination, DateTime.new(2013, 6, 24)) 69 | itinerary = Itinerary.new(legs) 70 | 71 | cargo = Cargo.new(tracking_id, route_spec) 72 | cargo.assign_to_route(itinerary) 73 | handling_event = HandlingEvent.new(HandlingEventType::Load, origin, DateTime.new(2013, 6, 14), DateTime.new(2013, 6, 15), tracking_id, HandlingEvent.new_id) 74 | handling_event_repository = HandlingEventRepository.new 75 | handling_event_repository.store(handling_event) 76 | cargo.derive_delivery_progress(handling_event) 77 | 78 | cargo_repository.store(cargo) 79 | 80 | found_cargo = cargo_repository.find_by_tracking_id(tracking_id) 81 | 82 | found_cargo.tracking_id.should == tracking_id 83 | found_cargo.route_specification.should == route_spec 84 | 85 | found_cargo.delivery.last_handling_event.id.should == handling_event.id 86 | found_cargo.delivery.transport_status.should == TransportStatus::OnboardCarrier 87 | found_cargo.delivery.last_known_location.should == origin 88 | found_cargo.delivery.is_misdirected.should be_false 89 | found_cargo.delivery.eta.should == DateTime.new(2013, 6, 24) 90 | found_cargo.delivery.is_unloaded_at_destination.should be_false 91 | found_cargo.delivery.routing_status.should == RoutingStatus::Routed 92 | # found_cargo.delivery.calculated_at.should == "junk" # TODO Need to fake the date 93 | found_cargo.delivery.last_handling_event.event_type.should == HandlingEventType::Load 94 | found_cargo.delivery.next_expected_activity.handling_event_type.should == HandlingEventType::Unload 95 | found_cargo.delivery.next_expected_activity.location.should == port 96 | end 97 | end -------------------------------------------------------------------------------- /Notes.ad: -------------------------------------------------------------------------------- 1 | * http://solnic.eu/2011/08/01/making-activerecord-models-thin.html[Making ActiveRecord Models Thin] - great blog post (and comment stream) from August 1, 2011 discussing separating behavior and state in Rails apps. 2 | * http://solnic.eu/2013/01/23/mutation-testing-with-mutant.html 3 | * http://solnic.eu/2012/12/20/datamapper-2-status-and-roadmap.html[DataMapper 2 Status and Roadmap] - mentions the following cool Gems that have been extraced from DataMapper: 4 | ** https://github.com/dkubb/adamantium[Adamantium] - helps in building immutable objects 5 | ** https://github.com/dkubb/ice_nine[IceNine] - deep freezing objects 6 | ** https://github.com/dkubb/equalizer[Equalizer] - builds equality methods for you 7 | ** DescendantsTracker 8 | 9 | * Sample usage for DataMapper 2. Kinda cool: 10 | 11 | ``` 12 | # In DataMapper 2 we don't pollute global constants with shared state. 13 | # That's why we decided to use an environment object that *you* 14 | # create and use do build mappers and configure everything. 15 | 16 | # Let's call it "datamapper" :) 17 | 18 | datamapper = DataMapper::Environment.new 19 | 20 | # You use environment object to establish connection with a db 21 | datamapper.setup :postgres, :uri => "postgres://localhost/test" 22 | 23 | # So let's say we have 2 domain objects Page and Book 24 | 25 | class Page 26 | include DataMapper::Model 27 | 28 | attribute :id, Integer 29 | attribute :content, String 30 | end 31 | 32 | class Book 33 | include DataMapper::Model 34 | 35 | attribute :isbn, String 36 | attribute :title, String 37 | attribute :author, String 38 | attribute :pages, Array[Page] 39 | end 40 | 41 | # You use environment object to build mappers 42 | 43 | datamapper.build(Page, :postgres) do 44 | key(:id) 45 | end 46 | 47 | datamapper.build(Book, :postgres) do 48 | key(:isbn) 49 | 50 | # here we establish a relationship between page and books 51 | has 0..n, :pages, Page 52 | end 53 | 54 | # we need to finalize the env now 55 | datamapper.finalize 56 | 57 | # to access a mapper you use #[] method and model constant 58 | # so to fetch all books with their pages you just do: 59 | 60 | datamapper[Book].include(:pages).all 61 | 62 | # Query API is pretty similar to what you already know 63 | 64 | datamapper[Book].find(:author => 'John Doe').limit(10).offset(2) 65 | ``` 66 | 67 | == Data Mapper Pattern 68 | 69 | * http://stackoverflow.com/questions/13550690/how-is-the-data-mapper-pattern-different-from-the-repository-pattern 70 | 71 | ---- 72 | http://stackoverflow.com/questions/13550690/how-is-the-data-mapper-pattern-different-from-the-repository-pattern 73 | ---- 74 | 75 | http://www.martinfowler.com/eaaCatalog/dataMapper.html[Data Mapper -] - A layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself. 76 | 77 | == UIs in Ruby 78 | 79 | What about DTOs? 80 | 81 | 82 | [quote, http://stackoverflow.com/questions/3284917/ruby-on-rails-dto-objects-where-do-you-store-them] 83 | ____ 84 | The Rails convention is not to use distributed tiers for controller and view layers. The separation is there, but it is logical and relatively thin/lightweight compared to the types of frameworks you see in Java land. 85 | 86 | The basic architecture is that the controller sets instance variables that are available in the corresponding view. In the general case, the instance variables will be model instances or collections of model instances (coming from the database). Models should be the core of your business logic. Controllers coordinate flows of data. Views display it. Helpers are used to format display values in the view ... anything that takes a model value and does something just for display purposes (you may find that a helper method used repeatedly may actually be better off on the model itself). 87 | 88 | However, if you find that a view needs knowledge of many different models, you might find it easier to wrap models into another object at a higher-level of abstraction. Nothing prevents you from creating non-active-record objects that collect and coordinate your actual AR models. You can then instantiate these objects in the controller, and have them available to the view. You generally have to be at a pretty dense level of complexity in the controller to need this type of thing. 89 | 90 | I would tend to throw such objects into apps/models - Rails already loads everything in this directory, keeps things easy from a config/expectation point of view. 91 | ____ 92 | 93 | === Padrino 94 | 95 | * Guides - http://www.padrinorb.com/guides 96 | * https://leanpub.com/padrino - content at https://github.com/matthias-guenther/padrino-book 97 | * Recipes - https://github.com/padrino/padrino-recipes 98 | * http://www.slideshare.net/victorbstan/introduction-to-padrino-9450994 99 | 100 | === Padrino Bugs (found so far) 101 | 102 | * http://stackoverflow.com/questions/16368016/bundle-update-for-padrino-app-could-not-find-compatible-versions-for-tilt[Gemfile: gem 'tilt', '1.3.7'] 103 | * post.rb - Put Text in quotes per http://rubyflewtoo.blogspot.com/2012/08/padrino-datamapper-rake-uninitialized.html 104 | 105 | # Sinatra 106 | 107 | * Sample apps - http://www.sinatrarb.com/wild.html 108 | 109 | == Specification Pattern 110 | 111 | * http://www.lukeredpath.co.uk/blog/introduction-to-activespec.html[ActiveSpec] - Ruby implementation of the Specification pattern from DDD. Written in 2006! 112 | * http://robuye.blogspot.com/2013/05/specification-pattern-with-ruby-basics.html[Specification Pattern with Ruby - basics] -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | remote: http://gems.github.com/ 4 | specs: 5 | actionmailer (3.2.12) 6 | actionpack (= 3.2.12) 7 | mail (~> 2.4.4) 8 | actionpack (3.2.12) 9 | activemodel (= 3.2.12) 10 | activesupport (= 3.2.12) 11 | builder (~> 3.0.0) 12 | erubis (~> 2.7.0) 13 | journey (~> 1.0.4) 14 | rack (~> 1.4.5) 15 | rack-cache (~> 1.2) 16 | rack-test (~> 0.6.1) 17 | sprockets (~> 2.2.1) 18 | activemodel (3.2.12) 19 | activesupport (= 3.2.12) 20 | builder (~> 3.0.0) 21 | activerecord (3.2.12) 22 | activemodel (= 3.2.12) 23 | activesupport (= 3.2.12) 24 | arel (~> 3.0.2) 25 | tzinfo (~> 0.3.29) 26 | activeresource (3.2.12) 27 | activemodel (= 3.2.12) 28 | activesupport (= 3.2.12) 29 | activesupport (3.2.12) 30 | i18n (~> 0.6) 31 | multi_json (~> 1.0) 32 | arel (3.0.2) 33 | binding_of_caller (0.7.1) 34 | debug_inspector (>= 0.0.1) 35 | bson (1.7.1) 36 | bson_ext (1.7.0) 37 | bson (~> 1.7.0) 38 | builder (3.0.4) 39 | byebug (2.7.0) 40 | columnize (~> 0.3) 41 | debugger-linecache (~> 1.2) 42 | celluloid (0.14.1) 43 | timers (>= 1.0.0) 44 | coderay (1.0.9) 45 | coffee-rails (3.2.2) 46 | coffee-script (>= 2.2.0) 47 | railties (~> 3.2.0) 48 | coffee-script (2.2.0) 49 | coffee-script-source 50 | execjs 51 | coffee-script-source (1.6.2) 52 | columnize (0.8.9) 53 | commonjs (0.2.6) 54 | debug_inspector (0.0.2) 55 | debugger-linecache (1.2.0) 56 | diff-lcs (1.2.4) 57 | erubis (2.7.0) 58 | execjs (1.4.0) 59 | multi_json (~> 1.0) 60 | ffi (1.9.3) 61 | formatador (0.2.4) 62 | guard (1.8.3) 63 | formatador (>= 0.2.4) 64 | listen (~> 1.3) 65 | lumberjack (>= 1.0.2) 66 | pry (>= 0.9.10) 67 | thor (>= 0.14.6) 68 | guard-rspec (3.1.0) 69 | guard (>= 1.8) 70 | rspec (~> 2.13) 71 | hamster (0.4.3) 72 | hike (1.2.3) 73 | i18n (0.6.4) 74 | ice_nine (0.7.0) 75 | journey (1.0.4) 76 | jquery-rails (3.0.0) 77 | railties (>= 3.0, < 5.0) 78 | thor (>= 0.14, < 2.0) 79 | json (1.8.0) 80 | less (2.3.2) 81 | commonjs (~> 0.2.6) 82 | less-rails (2.3.3) 83 | actionpack (>= 3.1) 84 | less (~> 2.3.1) 85 | libv8 (3.11.8.17) 86 | listen (1.3.1) 87 | rb-fsevent (>= 0.9.3) 88 | rb-inotify (>= 0.9) 89 | rb-kqueue (>= 0.2) 90 | lumberjack (1.0.4) 91 | mail (2.4.4) 92 | i18n (>= 0.4.0) 93 | mime-types (~> 1.16) 94 | treetop (~> 1.4.8) 95 | method_source (0.8.1) 96 | mime-types (1.23) 97 | mongo (1.7.0) 98 | bson (~> 1.7.0) 99 | mongoid (3.1.4) 100 | activemodel (~> 3.2) 101 | moped (~> 1.4) 102 | origin (~> 1.0) 103 | tzinfo (~> 0.3.22) 104 | moped (1.5.0) 105 | multi_json (1.7.6) 106 | origin (1.1.0) 107 | polyglot (0.3.3) 108 | pry (0.9.12.2) 109 | coderay (~> 1.0.5) 110 | method_source (~> 0.8) 111 | slop (~> 3.4) 112 | pry-byebug (1.3.2) 113 | byebug (~> 2.7) 114 | pry (~> 0.9.12) 115 | pry-stack_explorer (0.4.9) 116 | binding_of_caller (>= 0.7) 117 | pry (~> 0.9.11) 118 | rack (1.4.5) 119 | rack-cache (1.2) 120 | rack (>= 0.4) 121 | rack-ssl (1.3.3) 122 | rack 123 | rack-test (0.6.2) 124 | rack (>= 1.0) 125 | rails (3.2.12) 126 | actionmailer (= 3.2.12) 127 | actionpack (= 3.2.12) 128 | activerecord (= 3.2.12) 129 | activeresource (= 3.2.12) 130 | activesupport (= 3.2.12) 131 | bundler (~> 1.0) 132 | railties (= 3.2.12) 133 | railties (3.2.12) 134 | actionpack (= 3.2.12) 135 | activesupport (= 3.2.12) 136 | rack-ssl (~> 1.3.2) 137 | rake (>= 0.8.7) 138 | rdoc (~> 3.4) 139 | thor (>= 0.14.6, < 2.0) 140 | rake (10.0.4) 141 | rb-fsevent (0.9.3) 142 | rb-inotify (0.9.3) 143 | ffi (>= 0.5.0) 144 | rb-kqueue (0.2.0) 145 | ffi (>= 0.5.0) 146 | rdoc (3.12.2) 147 | json (~> 1.4) 148 | ref (1.0.5) 149 | rspec (2.13.0) 150 | rspec-core (~> 2.13.0) 151 | rspec-expectations (~> 2.13.0) 152 | rspec-mocks (~> 2.13.0) 153 | rspec-core (2.13.1) 154 | rspec-expectations (2.13.0) 155 | diff-lcs (>= 1.1.3, < 2.0) 156 | rspec-mocks (2.13.1) 157 | rspec-rails (2.13.2) 158 | actionpack (>= 3.0) 159 | activesupport (>= 3.0) 160 | railties (>= 3.0) 161 | rspec-core (~> 2.13.0) 162 | rspec-expectations (~> 2.13.0) 163 | rspec-mocks (~> 2.13.0) 164 | ruby-enum (0.4.0) 165 | i18n 166 | sass (3.2.9) 167 | sass-rails (3.2.6) 168 | railties (~> 3.2.0) 169 | sass (>= 3.1.10) 170 | tilt (~> 1.3) 171 | slop (3.4.5) 172 | sprockets (2.2.2) 173 | hike (~> 1.2) 174 | multi_json (~> 1.0) 175 | rack (~> 1.0) 176 | tilt (~> 1.1, != 1.3.0) 177 | terminal-notifier-guard (1.5.3) 178 | therubyracer (0.11.4) 179 | libv8 (~> 3.11.8.12) 180 | ref 181 | thor (0.18.1) 182 | tilt (1.4.1) 183 | timers (1.1.0) 184 | treetop (1.4.14) 185 | polyglot 186 | polyglot (>= 0.3.1) 187 | twitter-bootstrap-rails (2.2.7) 188 | actionpack (>= 3.1) 189 | execjs 190 | rails (>= 3.1) 191 | railties (>= 3.1) 192 | tzinfo (0.3.37) 193 | uglifier (2.1.1) 194 | execjs (>= 0.3.0) 195 | multi_json (~> 1.0, >= 1.0.2) 196 | uuidtools (2.1.4) 197 | wisper (1.1.0) 198 | wisper-async (0.0.1) 199 | celluloid 200 | wisper (~> 1.0) 201 | 202 | PLATFORMS 203 | ruby 204 | 205 | DEPENDENCIES 206 | bson_ext (= 1.7.0) 207 | coffee-rails (~> 3.2.1) 208 | guard 209 | guard-rspec 210 | hamster 211 | ice_nine 212 | jquery-rails 213 | less-rails 214 | mongo (= 1.7.0) 215 | mongoid 216 | pry 217 | pry-byebug 218 | pry-stack_explorer 219 | rails (= 3.2.12) 220 | rspec-rails 221 | ruby-enum 222 | sass-rails (~> 3.2.3) 223 | terminal-notifier-guard 224 | therubyracer 225 | twitter-bootstrap-rails 226 | uglifier (>= 1.0.3) 227 | uuidtools 228 | wisper-async 229 | -------------------------------------------------------------------------------- /app/assets/stylesheets/custom/bootstrap-datetimepicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Datepicker for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-datetimepicker-widget{top:0;left:0;width:250px;padding:4px;margin-top:1px;z-index:3000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);position:absolute;top:-7px;left:6px}.bootstrap-datetimepicker-widget:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:7px}.bootstrap-datetimepicker-widget.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget>ul{list-style-type:none;margin:0}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:100%;font-weight:bold;font-size:1.2em}.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator{width:4px;padding:0;margin:0}.bootstrap-datetimepicker-widget .datepicker>div{display:none}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget td,.bootstrap-datetimepicker-widget th{text-align:center;width:20px;height:20px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget td.day:hover,.bootstrap-datetimepicker-widget td.hour:hover,.bootstrap-datetimepicker-widget td.minute:hover,.bootstrap-datetimepicker-widget td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget td.old,.bootstrap-datetimepicker-widget td.new{color:#999}.bootstrap-datetimepicker-widget td.active,.bootstrap-datetimepicker-widget td.active:hover{color:#fff;background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td.active:hover,.bootstrap-datetimepicker-widget td.active:hover:hover,.bootstrap-datetimepicker-widget td.active:active,.bootstrap-datetimepicker-widget td.active:hover:active,.bootstrap-datetimepicker-widget td.active.active,.bootstrap-datetimepicker-widget td.active:hover.active,.bootstrap-datetimepicker-widget td.active.disabled,.bootstrap-datetimepicker-widget td.active:hover.disabled,.bootstrap-datetimepicker-widget td.active[disabled],.bootstrap-datetimepicker-widget td.active:hover[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.bootstrap-datetimepicker-widget td.active:active,.bootstrap-datetimepicker-widget td.active:hover:active,.bootstrap-datetimepicker-widget td.active.active,.bootstrap-datetimepicker-widget td.active:hover.active{background-color:#039 \9}.bootstrap-datetimepicker-widget td.disabled,.bootstrap-datetimepicker-widget td.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget td span{display:block;width:47px;height:54px;line-height:54px;float:left;margin:2px;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget td span:hover{background:#eee}.bootstrap-datetimepicker-widget td span.active{color:#fff;background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td span.active:hover,.bootstrap-datetimepicker-widget td span.active:active,.bootstrap-datetimepicker-widget td span.active.active,.bootstrap-datetimepicker-widget td span.active.disabled,.bootstrap-datetimepicker-widget td span.active[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.bootstrap-datetimepicker-widget td span.active:active,.bootstrap-datetimepicker-widget td span.active.active{background-color:#039 \9}.bootstrap-datetimepicker-widget td span.old{color:#999}.bootstrap-datetimepicker-widget td span.disabled,.bootstrap-datetimepicker-widget td span.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget th.switch{width:145px}.bootstrap-datetimepicker-widget th.next,.bootstrap-datetimepicker-widget th.prev{font-size:21px}.bootstrap-datetimepicker-widget th.disabled,.bootstrap-datetimepicker-widget th.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget thead tr:first-child th:hover{background:#eee}.input-append.date .add-on i,.input-prepend.date .add-on i{display:block;cursor:pointer;width:16px;height:16px}.bootstrap-datetimepicker-widget.left-oriented:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.left-oriented:after{left:auto;right:7px} -------------------------------------------------------------------------------- /ports/persistence/mongodb_adaptor/cargo_repository.rb: -------------------------------------------------------------------------------- 1 | require 'mongoid' 2 | require_relative 'handling_event_repository' 3 | 4 | class CargoRepository 5 | 6 | def initialize 7 | # TODO Move this somewhere (base class?) for all Mongoid-based repositories 8 | Mongoid.load!("#{File.dirname(__FILE__)}/../../../config/mongoid.yml", :development) 9 | end 10 | 11 | def store(cargo) 12 | # TODO Figure out how to update existing document 13 | # when the delivery progress is updated, rather than 14 | # create a new one. 15 | cargo_doc = CargoDocument.where(tracking_id: cargo.tracking_id.id) 16 | if cargo_doc.first 17 | puts "Cargo already saved...removing existing document..." 18 | cargo_doc.delete 19 | end 20 | cargo_document = CargoDocumentAdaptor.new.transform_to_mongoid_document(cargo) 21 | # Upsert didn't work. Change back to save? 22 | cargo_document.save 23 | end 24 | 25 | def find_by_tracking_id(tracking_id) 26 | cargo_doc = CargoDocument.find_by(tracking_id: tracking_id.id) 27 | CargoDocumentAdaptor.new.transform_to_cargo(cargo_doc) 28 | end 29 | 30 | def find_all() 31 | CargoDocument.all 32 | end 33 | 34 | # TODO Implement (return GUID) 35 | def next_tracking_id() 36 | # Using Banksimplistic approach... 37 | # UUIDTools::UUID.timestamp_create.to_s 38 | end 39 | 40 | # TODO Do something cleaner than this for data setup/teardown - yikes! 41 | def nuke_all_cargo 42 | CargoDocument.delete_all 43 | end 44 | end 45 | 46 | class CargoDocument 47 | include Mongoid::Document 48 | 49 | field :tracking_id, type: String 50 | field :origin_code, type: String 51 | field :destination_code, type: String 52 | field :origin_name, type: String 53 | field :destination_name, type: String 54 | field :arrival_deadline, type: DateTime 55 | #----- 56 | # Decide whether we need to persist these, since they are derived from legs. They might 57 | # make reporting from MongoDB easier...treating them like a cache of useful values... 58 | field :initial_departure_location_code, type: String 59 | field :initial_departure_location_name, type: String 60 | field :final_arrival_location_code, type: String 61 | field :final_arrival_location_name, type: String 62 | field :final_arrival_date, type: DateTime 63 | field :last_handling_event_id, type: String 64 | #----- 65 | embeds_many :leg_documents 66 | 67 | index({ tracking_id: 1 }, { unique: true, name: "tracking_id" }) 68 | end 69 | 70 | class LegDocument 71 | include Mongoid::Document 72 | 73 | # TODO Decide whether Location should be its own document. It's kinda a pain to keep writing both code 74 | # and name for each location, plus prone to error. 75 | field :voyage, type: String 76 | field :load_location_code, type: String 77 | field :load_location_name, type: String 78 | field :unload_location_code, type: String 79 | field :unload_location_name, type: String 80 | field :load_date, type: DateTime 81 | field :unload_date, type: DateTime 82 | 83 | embedded_in :cargo_document 84 | end 85 | 86 | class CargoDocumentAdaptor 87 | def transform_to_mongoid_document(cargo) 88 | cargo_document = CargoDocument.new( 89 | tracking_id: cargo.tracking_id.id, 90 | origin_code: cargo.route_specification.origin.unlocode.code, 91 | origin_name: cargo.route_specification.origin.name, 92 | destination_code: cargo.route_specification.destination.unlocode.code, 93 | destination_name: cargo.route_specification.destination.name, 94 | arrival_deadline: cargo.route_specification.arrival_deadline 95 | ) 96 | if cargo.delivery.last_handling_event 97 | cargo_document.last_handling_event_id = cargo.delivery.last_handling_event.id 98 | end 99 | cargo_document.leg_documents.concat(transform_to_leg_documents(cargo.itinerary.legs)) 100 | cargo_document 101 | end 102 | 103 | def transform_to_cargo(cargo_document) 104 | legs = transform_to_legs(cargo_document.leg_documents) 105 | itinerary = Itinerary.new(legs) 106 | origin = Location.new(UnLocode.new(cargo_document[:origin_code]), cargo_document[:origin_name]) 107 | destination = Location.new(UnLocode.new(cargo_document[:destination_code]), cargo_document[:destination_name]) 108 | route_spec = RouteSpecification.new(origin, destination, cargo_document[:arrival_deadline]) 109 | tracking_id = TrackingId.new(cargo_document[:tracking_id]) 110 | 111 | cargo = Cargo.new(tracking_id, route_spec) 112 | cargo.assign_to_route(itinerary) 113 | if cargo_document.last_handling_event_id 114 | handling_event_repository = HandlingEventRepository.new 115 | last_handling_event = handling_event_repository.find(cargo_document.last_handling_event_id) 116 | cargo.derive_delivery_progress(last_handling_event) 117 | end 118 | 119 | cargo 120 | end 121 | 122 | def transform_to_leg_documents(legs) 123 | leg_documents = Array.new 124 | legs.each do |leg| 125 | leg_document = LegDocument.new( 126 | voyage: leg.voyage, 127 | load_location_code: leg.load_location.unlocode.code, 128 | load_location_name: leg.load_location.name, 129 | unload_location_code: leg.unload_location.unlocode.code, 130 | unload_location_name: leg.unload_location.name, 131 | load_date: leg.load_date, 132 | unload_date: leg.unload_date 133 | ) 134 | leg_documents << leg_document 135 | end 136 | leg_documents 137 | end 138 | 139 | def transform_to_legs(leg_documents) 140 | legs = Array.new 141 | leg_documents.each do |leg_document| 142 | load_location = Location.new(UnLocode.new(leg_document[:load_location_code]), leg_document[:load_location_name]) 143 | unload_location = Location.new(UnLocode.new(leg_document[:unload_location_code]), leg_document[:unload_location_name]) 144 | legs << Leg.new( 145 | leg_document[:voyage], 146 | load_location, 147 | leg_document[:load_date], 148 | unload_location, 149 | leg_document[:unload_date] 150 | ) 151 | end 152 | legs 153 | end 154 | end 155 | 156 | 157 | # TODO Decide whether to break out value objects (Delivery, RouteSpecification 158 | # and Itinerary) into embedded documents, such as: 159 | # 160 | # In CargoDocument: 161 | # 162 | # embeds_one :route_specification_document 163 | 164 | # class RouteSpecificationDocument 165 | # include Mongoid::Document 166 | # field :arrival_deadline, type: Date 167 | # ... 168 | # embedded_in :cargo_documents 169 | # end -------------------------------------------------------------------------------- /spec/domain/cargo/delivery_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'models_require' 3 | 4 | def handling_event_fake(location, handling_event_type) 5 | registration_date = Date.new(2013, 6, 21) 6 | completion_date = Date.new(2013, 6, 21) 7 | 8 | # TODO Set it to fake tracking id for now 9 | HandlingEvent.new(handling_event_type, location, registration_date, completion_date, 999, HandlingEvent.new_id) 10 | end 11 | 12 | describe "Delivery" do 13 | before(:each) do 14 | @origin = Location.new(UnLocode.new('HKG'), 'Hong Kong') 15 | @destination = Location.new(UnLocode.new('DAL'), 'Dallas') 16 | arrival_deadline = Date.new(2013, 7, 1) 17 | @route_spec = RouteSpecification.new(@origin, @destination, arrival_deadline) 18 | 19 | @port = Location.new(UnLocode.new('LGB'), 'Long Beach') 20 | legs = Array.new 21 | legs << Leg.new('Voyage ABC', @origin, Date.new(2013, 6, 14), @port, Date.new(2013, 6, 19)) 22 | legs << Leg.new('Voyage DEF', @port, Date.new(2013, 6, 21), @destination, Date.new(2013, 6, 24)) 23 | @itinerary = Itinerary.new(legs) 24 | end 25 | 26 | it "Cargo is not considered unloaded at destination when there are no recorded handling events" do 27 | last_event = nil 28 | 29 | # TODO Implement derived_from once I work out static method, and calling constructor from 30 | # this static method (then delete the direct call to the constructor) 31 | delivery = Delivery.new(@route_spec, @itinerary, nil) 32 | # @delivery = @old_delivery.derived_from(@route_spec, itinerary, last_event); 33 | delivery.is_unloaded_at_destination.should be_false 34 | end 35 | 36 | it "Cargo is not considered unloaded at destination after handling unload event but not at destination" do 37 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@port, HandlingEventType::Unload)) 38 | delivery.is_unloaded_at_destination.should be_false 39 | end 40 | 41 | it "Cargo is not considered unloaded at destination after handling other event at destination" do 42 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Customs)) 43 | delivery.is_unloaded_at_destination.should be_false 44 | end 45 | 46 | it "Cargo is considered unloaded at destination after handling unload event at destination" do 47 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Unload)) 48 | delivery.is_unloaded_at_destination.should be_true 49 | end 50 | 51 | # TODO I really don't like the presence of nil here! Should have something like 52 | # an 'Unknown' location object rather than nil 53 | it "Cargo has unknown location when there are no recorded handling events" do 54 | delivery = Delivery.new(@route_spec, @itinerary, nil) 55 | delivery.last_known_location.should be_nil 56 | end 57 | 58 | it "Cargo has correct last known location based on most recent handling event" do 59 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Unload)) 60 | delivery.last_known_location.should == @destination 61 | end 62 | 63 | # TODO I really don't like the presence of nil here! Should have something like 64 | # an 'Unknown' location object rather than nil 65 | it "Cargo is not misdirected when there are no recorded handling events" do 66 | delivery = Delivery.new(@route_spec, @itinerary, nil) 67 | delivery.is_misdirected.should be_false 68 | end 69 | 70 | # TODO I really don't like the presence of nil here! Should have something like 71 | # an 'Unknown' itinerary object rather than nil 72 | it "Cargo is not misdirected when it has no itinerary" do 73 | delivery = Delivery.new(@route_spec, nil, handling_event_fake(@destination, HandlingEventType::Unload)) 74 | delivery.is_misdirected.should be_false 75 | end 76 | 77 | it "Cargo is not misdirected when the last recorded handling event is a load in the origin which matches the itinerary" do 78 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@origin, HandlingEventType::Load)) 79 | delivery.is_misdirected.should be_false 80 | end 81 | 82 | it "Cargo is not misdirected when the last recorded handling event matches the itinerary" do 83 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Unload)) 84 | delivery.is_misdirected.should be_false 85 | end 86 | 87 | it "Cargo is misdirected when the last recorded handling event does not match the itinerary" do 88 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Load)) 89 | delivery.is_misdirected.should be_true 90 | end 91 | 92 | it "Cargo is not routed when it doesn't have an itinerary" do 93 | delivery = Delivery.new(@route_spec, nil, handling_event_fake(@destination, HandlingEventType::Load)) 94 | delivery.routing_status.should == RoutingStatus::NotRouted 95 | end 96 | 97 | it "Cargo is routed when specification is satisfied by itinerary" do 98 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Load)) 99 | delivery.routing_status.should == RoutingStatus::Routed 100 | end 101 | 102 | it "Cargo is on track when the cargo has been routed and is not misdirected" do 103 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Unload)) 104 | delivery.on_track?.should be_true 105 | end 106 | 107 | it "Cargo is not on track when the cargo has been routed and is misdirected" do 108 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Load)) 109 | delivery.on_track?.should be_false 110 | end 111 | 112 | it "Cargo transport status is not received when there are no recorded handling events" do 113 | delivery = Delivery.new(@route_spec, @itinerary, nil) 114 | delivery.transport_status.should == TransportStatus::NotReceived 115 | end 116 | 117 | it "Cargo transport status is in port when the last recorded handling event is an unload" do 118 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Unload)) 119 | delivery.transport_status.should == TransportStatus::InPort 120 | end 121 | 122 | it "Cargo transport status is in port when the last recorded handling event is a receive" do 123 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Receive)) 124 | delivery.transport_status.should == TransportStatus::InPort 125 | end 126 | 127 | it "Cargo transport status is onboard carrier when the last recorded handling event is a load" do 128 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Load)) 129 | delivery.transport_status.should == TransportStatus::OnboardCarrier 130 | end 131 | 132 | it "Cargo transport status is claimed when the last recorded handling event is a claim" do 133 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Claim)) 134 | delivery.transport_status.should == TransportStatus::Claimed 135 | end 136 | 137 | it "Cargo has correct eta based on itinerary when on track" do 138 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@origin, HandlingEventType::Load)) 139 | delivery.eta.should == @itinerary.final_arrival_date 140 | end 141 | 142 | it "Cargo has no eta when not on track" do 143 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@origin, HandlingEventType::Unload)) 144 | delivery.eta.should be_nil 145 | end 146 | 147 | it "Cargo has no next expected activity when not on track" do 148 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@origin, HandlingEventType::Unload)) 149 | delivery.next_expected_activity.should be_nil 150 | end 151 | 152 | it "Cargo has next expected activity of receive at origin when there are no recorded handling events" do 153 | delivery = Delivery.new(@route_spec, @itinerary, nil) 154 | delivery.next_expected_activity.should == HandlingActivity.new(HandlingEventType::Receive, @origin) 155 | end 156 | 157 | it "Cargo has next expected activity of load at origin when when the last recorded handling event is a receive" do 158 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@origin, HandlingEventType::Receive)) 159 | delivery.next_expected_activity.should == HandlingActivity.new(HandlingEventType::Load, @origin) 160 | end 161 | 162 | it "Cargo has next expected activity of unload at next port when the last recorded handling event is a load at origin" do 163 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@origin, HandlingEventType::Load)) 164 | delivery.next_expected_activity.should == HandlingActivity.new(HandlingEventType::Unload, @port) 165 | end 166 | 167 | it "Cargo has next expected activity of load at port when the last recorded handling event is an unload at the port" do 168 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@port, HandlingEventType::Unload)) 169 | delivery.next_expected_activity.should == HandlingActivity.new(HandlingEventType::Load, @port) 170 | end 171 | 172 | it "Cargo has next expected activity of unload at destination when the last recorded handling event is a load at the previous port" do 173 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@port, HandlingEventType::Load)) 174 | delivery.next_expected_activity.should == HandlingActivity.new(HandlingEventType::Unload, @destination) 175 | end 176 | 177 | it "Cargo has next expected activity of claim at destination when the last recorded handling event is an unload at the destination" do 178 | delivery = Delivery.new(@route_spec, @itinerary, handling_event_fake(@destination, HandlingEventType::Unload)) 179 | delivery.next_expected_activity.should == HandlingActivity.new(HandlingEventType::Claim, @destination) 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | == Ruby DDD Sample App 2 | 3 | === Goal 4 | 5 | The goal of this sample app is to provide an idiomatic Ruby port of the DDD sample application. It should remain faithful to the intention of the purposes of the original DDD sample application. 6 | 7 | === Why a Ruby port? 8 | 9 | Because Paul wanted to learn Ruby and see how well it supports DDD. Also, interested in questions like: 10 | 11 | * How does the choice of Ruby affect the implementation of the DDD building block patterns? 12 | * How well does an opinionated MVC framework like Rails support doing DDD? 13 | * What are implications of choosing a document store like MongoDB for aggregate design and eventual consistency? 14 | 15 | === Why a DDD Sample App? 16 | 17 | 1. Provide a how-to example for implementing a typical DDD application 18 | 19 | * Descriptive, not prescriptive 20 | * Idiomatic 21 | * Update sample app with latest ideas 22 | 23 | 2. Support discussion of implementation practices 24 | 25 | * Engage Ruby community in dialog and learning about DDD 26 | * Show design and implementation tradeoffs 27 | * Teach DDD (tend to be Java & .NET) community more about Ruby 28 | 29 | 3. Lab mouse for controlled experiments 30 | 31 | * Learn Ruby as a language and ecosystem 32 | * Can Rails + MongoDB carry the weight of a complex domain model? 33 | * Tradeoffs between Sinatra vs Rails for Ruby web apps 34 | 35 | === Problems with Sample Apps 36 | 37 | * http://lostechies.com/jimmybogard/2008/10/22/where-are-the-ddd-sample-applications/[Against DDD sample applications] - Wise words from Jimmy Bogard 38 | 39 | === DDD Sample Application 40 | 41 | * link:https://github.com/patrikfr/dddsample[Written in Java in 2009]. 42 | 43 | *History* 44 | 45 | * Sep 2008 First public release: 1.0. 46 | * Jan 2009 Sample application tutorial at the JFokus conference in Stockholm. 47 | * Mar 2009 Sample application tutorial at the QCon conference in London. 48 | * Mar 2009 New public release: 1.1.0. See changelog for details. 49 | * 2010/2011 https://github.com/SzymonPobiega/DDDSample.Net[Ported to .NET in several flavors]. 50 | * May 8, 2013 Begin porting to Ruby at https://github.com/paulrayner/ddd_sample_app_ruby 51 | * May 13, 2013 Presentation of early work on port of sample app to Ruby at DDD Denver. Slides are http://virtual-genius.com/presentations/ddd_with_ruby_20130613.html[available online]. 52 | 53 | _The .NET port is being used as the primary basis for this Ruby port._ 54 | 55 | == Implementation Stack 56 | 57 | * Persistence: MongoDB 58 | * Data access: Mongoid-backed repositories 59 | * Domain model: Plain Ruby objects 60 | * UI: Rails stack (w/ Twitter bootstrap) 61 | * Aggregate eventual consistency: Wisper-async leveraging Celluloid 62 | * _GOAL: Before DDDx London - June 14_ DONE! 63 | 64 | _The focus of this version is to see how implementing a domain model within Rails affects the implementation. 65 | 66 | == Aggregates 67 | 68 | The aggregate roots are: 69 | 70 | * Cargo 71 | * HandlingEvent 72 | * Location 73 | * Voyage 74 | 75 | == Design Decisions 76 | 77 | Here you'll find information on design choices made, and the relative tradeoffs. Plus resources for further reading. Actually, mostly resources right now. 78 | 79 | === Value Objects 80 | 81 | ==== Immutability in Ruby 82 | 83 | * link:https://deveo.com/blog/2013/03/22/immutability-in-ruby-part-1/[Immutability in Ruby - part 1 of 2] 84 | * link:https://deveo.com/blog/2013/03/28/immutability-in-ruby-part-2/[Entities and value objects in Ruby - part 2 of 2] 85 | 86 | * http://voormedia.com/blog/2013/02/creating-immutable-tree-data-structures-in-ruby[Creating immutable tree data structures in Ruby - Feb 2013] 87 | * http://www.confreaks.com/videos/2337-mwrc2013-immutable-ruby[Immutable Ruby presentation video (25 mins) - Michael Fairley @ MountainWest RubyConf 2013] 88 | * http://blog.rubybestpractices.com/posts/rklemme/017-Struct.html[Ruby structs inside-out] 89 | 90 | * http://functionalruby.com/blog/2012/02/23/hamster-immutable-data-structures-for-ruby[Blog post on Functional Ruby blog about Hamster] 91 | * http://www.harukizaemon.com/blog/2010/03/01/functional-programming-in-object-oriented-languages/[Functional programming in object oriented languages] - Blog post by Simon Harris, author of Hamster. 92 | 93 | ==== Libraries/Gems Supporting Immutability in Ruby 94 | 95 | * https://rubygems.org/gems/ice_nine[Ice Nine (for deep freezing objects)] 96 | * https://github.com/harukizaemon/hamster[Hamster - Efficient, Immutable, Thread-Safe Collection classes for Ruby] 97 | * https://github.com/tcrayford/values 98 | * https://github.com/solnic/virtus 99 | * https://github.com/hdgarrood/value_object 100 | * https://github.com/rouge-lang/rouge[Ruby + Clojure = Rouge] 101 | 102 | === Enums in Ruby 103 | 104 | * http://stackoverflow.com/questions/75759/enums-in-ruby 105 | * http://www.lesismore.co.za/rubyenums.html 106 | * http://gistflow.com/posts/682-ruby-enums-approaches 107 | 108 | === Equality in Ruby 109 | 110 | * http://woss.name/2011/01/20/equality-comparison-and-ordering-in-ruby/[Equality, Comparison and Uniqueness in Ruby] 111 | * http://stackoverflow.com/questions/11247000/which-equality-test-does-rubys-hash-use-when-comparing-keys[SO: Which equality test does Ruby's Hash use when comparing keys?] 112 | * http://pivotallabs.com/equality-and-sameness-in-ruby/[Equality and sameness in RubyConf] 113 | * http://kentreis.wordpress.com/2007/02/08/identity-and-equality-in-ruby-and-smalltalk/[Identity and Equality in Ruby and Smalltalk] 114 | 115 | == Persistence 116 | 117 | === MongoDB 118 | 119 | * link:http://speakerdeck.com/u/mongodb/p/domain-driven-design-with-mongodb-chris-hafey-on-point-medical-diagnostics[Presentation on Domain Driven Design with MongoDB] 120 | * link:http://wiki.basho.com/Riak-Compared-to-MongoDB.html[Riak Compared to MongoDB] 121 | * https://github.com/basho/ripple/wiki[Ripple is a rich Ruby client for Riak, Basho’s distributed database] 122 | * http://docs.mongodb.org/ecosystem/drivers/ruby/[Mongo Ruby driver] 123 | 124 | ==== Mongo ORMs 125 | 126 | * http://mongoid.org/en/mongoid/index.html[Mongoid] - Object-Document-Mapper (ODM) for MongoDB written in Ruby. Has Echo sample app - take a look at `application.rb` - it's using Sidekiq and Kiqstand (not sure what for...maybe could be used for aggregate updates?) 127 | * https://github.com/mongomatic/mongomatic[Mongomatic] - A MongoDB super-set that adds nice features over the traditional Ruby Driver. Map your Ruby objects to Mongo documents. It is designed to be fast and simple. 128 | * http://mongomapper.com/[MongoMapper] - ODM for MongoDB written in Ruby. 129 | 130 | === Repository Pattern in Ruby 131 | 132 | * http://mattbriggs.net/blog/2012/02/23/repository-pattern-in-ruby/ 133 | * https://github.com/nfedyashev/repository[A Ruby implementation of the Repository Pattern - In memory only], developed from https://github.com/alexch/treasury[Repository Pattern for Ruby - 3 years old]. 134 | * https://github.com/playlouder/persistence[A set of interfaces for, and implementations of, the Repository pattern in Ruby.] This one looks promising. 135 | * https://github.com/brandonweiss/collector[Collector is an implementation of the Repository Pattern for MongoDB] 136 | * https://github.com/braintree/curator[Curator is a model and repository framework for Ruby].Currently, curator supports Riak, MongoDB and an in-memory data store for persistence. 137 | * https://github.com/braintree/curator_rails_example[Curator Rails example] 138 | * http://www.pgrs.net/2012/02/21/untangle-domain-and-persistence-logic-with-curator[Good blog post by Paul Gross: "Untangle Domain and Persistence Logic with Curator"] 139 | * http://www.pgrs.net/2012/03/08/data-migrations-for-nosql-with-curator/[Data migrations for NoSQL with Curator]. "Curator migrations are lazy, so at any given time you might have documents with different versions in the data store." 140 | * https://gist.github.com/bokmann/2217602[ActiveRepository "Strawman" gist by David Bock]. Proposal for what a good Repository pattern implementation should look like in Ruby. Comment thread is excellent value. 141 | * http://datamapper.org/[DataMapper 2] - goal is to create an ORM which is fast, thread-safe and feature rich. Last release was 1.2, but active development on v2 seems to be progressing. 142 | * https://github.com/fredwu/datamappify[Datamappify] - is built using Virtus and existing ORMs (ActiveRecord and Sequel, etc). Compose and manage domain logic and data persistence separately and intelligently, Datamappify is loosely based on the Repository Pattern and Entity Aggregation. _Datamappify is current in Proof-of-Concept stage, do NOT use it for anything other than experimentation._ 143 | 144 | Have not yet found a repository implementation that supports aggregates. Rather, each implementation follows a repository-per-object approach, which is not what we need. 145 | 146 | There is an on issue for Curator regarding https://github.com/braintree/curator/issues/16[ supporting foreign keys and embedded objects], and some experimentation in a branch with adding a https://github.com/braintree/curator/commit/repository_mapping[mapping API] which may do what I need. 147 | 148 | https://github.com/ifesdjeen/entrepot[Entrepot] looks promising. It uses Virtus for the objects and has this kinda weird approach of referencing a repository from a repository: 149 | 150 | [source, ruby] 151 | ---- 152 | class Address 153 | include Virtus 154 | include Entrepot::Model 155 | 156 | attribute :street, String 157 | attribute :city, String 158 | attribute :country, String 159 | end 160 | 161 | class Person 162 | include Virtus 163 | include Entrepot::Mongo::Model 164 | 165 | attribute :name, String 166 | attribute :address, Address 167 | end 168 | 169 | class PersonRepository 170 | include Entrepot::Repository 171 | 172 | has_many :articles, :repository => :ArticleRepository 173 | end 174 | ---- 175 | 176 | === Aggregates 177 | 178 | ==== Concurrency in Ruby 179 | 180 | * http://www.slideshare.net/ThoughtWorks0ffshore/concurrency-patterns-in-ruby-3547211[Concurrency patterns in Ruby - Thoughtworks presentation] 181 | * https://github.com/tenderlove/tusk[Message busses with Observable API] 182 | * http://www.slideshare.net/KyleDrake/hybrid-concurrency-patterns[Presentation on NOT using Eventmachine], advocates Celluloid 183 | * http://blog.paracode.com/2012/09/07/pragmatic-concurrency-with-ruby/[Pragmatic Concurrency With Ruby] - great article, which also discusses how Celluloid uses `mutex` to thread-safe its mailboxes. 184 | 185 | ==== Eventual Consistency 186 | 187 | Resources for implementing eventual consistency (i.e. performing asynchronous updates) between aggregate instances. 188 | 189 | ===== Worker Queues 190 | 191 | * http://rubylearning.com/blog/2010/11/08/do-you-know-resque[Learning Resque] 192 | * http://railscasts.com/episodes/271-resque[Railscast on Resque] 193 | * https://devcenter.heroku.com/articles/queuing-ruby-resque[Queuing in Ruby with Redis and Resque - Heroku Blog] 194 | * https://github.com/nesquena/backburner[Simple and reliable beanstalkd job queue for ruby] 195 | * https://github.com/iron-io/delayed_job_ironmq[IronMQ backend for delayed_job] 196 | * https://github.com/mperham/sidekiq[Sidekiq] - Simple, efficient message processing for Ruby, based on Celluloid actor model 197 | * http://railscasts.com/episodes/366-sidekiq[Railscast on Sidekiq] 198 | 199 | ===== Messaging 200 | 201 | * http://rubyamqp.info/articles/getting_started/[Ampq/RabbitMQ] 202 | * http://www.iron.io/[IronMQ is the Message Queue for the Cloud], see http://www.iron.io/mq[comparison chart] 203 | * http://rubysource.com/an-introduction-to-celluloid-part-ii/ 204 | 205 | ===== Celluloid 206 | 207 | * http://www.unlimitednovelty.com/2011/05/introducing-celluloid-concurrent-object.html["Introducing Celluloid: a concurrent object framework for Ruby" - Blog post from May 11. 2011] 208 | * https://groups.google.com/forum/?fromgroups#!forum/celluloid-ruby[Celluloid Google Group] 209 | * http://www.confreaks.com/videos/1302-rubyconf2012-the-celluloid-ecosystem[RubyConf presentation on Celluloid by Tony Arcieri] 210 | * http://rubysource.com/an-introduction-to-celluloid-part-i[An Introduction to Celluloid - Part II] and http://rubysource.com/an-introduction-to-celluloid-part-ii/[An Introduction to Celluloid - Part II] 211 | * http://railscasts.com/episodes/367-celluloid[Railscast (pro) on Celluloid] - good examples 212 | 213 | === DDD and Rails 214 | 215 | * http://victorsavkin.com/post/41016739721/building-rich-domain-models-in-rails-separating[Entity Data Repository] - Blog post describing hybrid ActiveRecord/DAO approach to building rich domain models in Rails, implemented in https://github.com/nulogy/edr[EDR library]. Implements restricted version of http://martinfowler.com/eaaCatalog/dataMapper.html[DataMapper pattern]. Datamapper 2 will be implementing the same pattern, but is not production-ready yet (see above) 216 | * http://iain.nl/domain-driven-design-building-blocks-in-ruby[DDD in Ruby article] - recommends using to_s for UI concerns and structs for value objects, both of which seem problematic to me. 217 | * https://github.com/cavalle/banksimplistic[Interesting implementation of CQRS in Rails with Redis] 218 | * http://blog.carbonfive.com/2012/01/10/does-my-rails-app-need-a-service-layer/[Does My Rails App Need a Service Layer?] - blog post from Jan 2012 by Jared Carroll 219 | * http://confreaks.com/videos/977-goruco2012-hexagonal-rails[Hexagonal Rails] - Video of Matt Wynne's Goruco 2012 presentation 220 | * https://www.agileplannerapp.com/blog/building-agile-planner/refactoring-with-hexagonal-rails[Refactoring with Hexagonal Rails] - blog post showing how to set up pub/sub eventing for use within Rails (inspired by Matt Wynne's approach of passing controller object into domain object, so domain object can run a success/failure callback method on the controller) 221 | * https://github.com/krisleech/wisper[Wisper] - Ruby library for decoupling and managing the dependencies of your domain models]. See also this http://shcatula.wordpress.com/2013/06/02/whisper-ruby/[blog post on Wisper] and this https://gist.github.com/krisleech/5326823[business case Gist]. 222 | * https://github.com/krisleech/wisper-async[Wisper-Async] - Extends Wisper with async broadcasting of events. Each listener is transparently turned in to a Celluloid Actor. 223 | 224 | == Contributing 225 | 226 | This is a learning experiment, pull requests are welcome! Bonus points for feature branches. 227 | 228 | To get started, see https://github.com/paulrayner/ddd_sample_app_ruby/issues?state=open[milestones and issues]. Use the https://github.com/SzymonPobiega/DDDSample.Net[vanilla .NET port version] as the basis for any work. 229 | 230 | Progress and learning will be shared after the DDD Exchange on June 14 through posts on http://thepaulrayner.com[Paul Rayner's blog]. 231 | 232 | 233 | == Copyright 234 | 235 | Copyright (C) 2013 Paul Rayner. See link:LICENSE[LICENSE] for details. 236 | 237 | 238 | -------------------------------------------------------------------------------- /spec/domain/cargo/delivery_spec.rb_dan: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'delivery' 3 | require 'handling_activity' 4 | 5 | # reopen Delivery to avoid all the method calls from initialization and allow stubbing 6 | class Delivery 7 | def initialize(route_specification='x', itinerary=nil, last_handled_event=nil) 8 | raise InitializationError unless route_specification 9 | end 10 | end 11 | 12 | # class HandlingActivity < Struct.new(:param_one, :param_two); end 13 | 14 | describe Delivery do 15 | context "initialize()" do 16 | it "should raise an error if route_specification is nil" do 17 | expect { 18 | Delivery.new(nil, 'something', 'something') 19 | }.to raise_error(Delivery::InitializationError) 20 | end 21 | 22 | it "should not raise an error if last_handled_event is nil" do 23 | expect { 24 | Delivery.new('something', 'something', nil) 25 | }.to_not raise_error 26 | end 27 | end # context initialize() 28 | 29 | # =========== derived_from =========== 30 | context "derived_from()" do 31 | # skipped because there isn't an implementation (and may not be one) 32 | end # context derived_from() 33 | 34 | # =========== calculate_last_known_location =========== 35 | context "calculate_last_known_location()" do 36 | before { @delivery = Delivery.new } # (@fake_route, @fake_itinerary, @fake_event) } 37 | 38 | it "should return nil if last_handled_event is nil" do 39 | @delivery.calculate_last_known_location(nil).should be_nil 40 | end 41 | 42 | it "should return the last handled event location" do 43 | fake_last_handled_event = double(:location => 'a_location') 44 | @delivery.calculate_last_known_location(fake_last_handled_event).should == 'a_location' 45 | end 46 | end # context calculate_last_known_location() 47 | 48 | # =========== calculate_unloaded_at_destination =========== 49 | context "calculate_unloaded_at_destination()" do 50 | before { @delivery = Delivery.new } 51 | 52 | it "should return false if last_handled_event is nil" do 53 | @delivery.calculate_unloaded_at_destination(nil, 'something').should be_false 54 | end 55 | 56 | it "should return true if event_type is Unload AND location is the destination" do 57 | fake_last_handled_event = double('last_handled_event', location:'a_location', event_type:'Unload') 58 | fake_route_specification = double('route_specification', destination:'a_location') 59 | @delivery.calculate_unloaded_at_destination(fake_last_handled_event, fake_route_specification).should be_true 60 | end 61 | 62 | it "should return false if event_type is not Unload" do 63 | fake_last_handled_event = double('last_handled_event', event_type:'something') 64 | fake_route_specification = double('route_specification') 65 | @delivery.calculate_unloaded_at_destination(fake_last_handled_event, fake_route_specification).should be_false 66 | end 67 | 68 | it "should return false if location doesn't equal destination" do 69 | fake_last_handled_event = double('last_handled_event', location:'a_location', event_type:'Unload') 70 | fake_route_specification = double('route_specification', destination:'another_location') 71 | @delivery.calculate_unloaded_at_destination(fake_last_handled_event, fake_route_specification).should be_false 72 | end 73 | end # context calculate_unloaded_at_destination() 74 | 75 | # =========== calculate_misdirection_status =========== 76 | context "calculate_misdirection_status()" do 77 | before { @delivery = Delivery.new } 78 | 79 | it "should return false if last_handled_event is nil" do 80 | @delivery.calculate_misdirection_status('something', nil).should be_false 81 | end 82 | 83 | it "should return false if itinerary is nil" do 84 | @delivery.calculate_misdirection_status(nil,'something').should be_false 85 | end 86 | 87 | it "should return true if is_expected() returns false" do 88 | fake_last_handled_event = double('last_handled_event') 89 | fake_itinerary = double('itinerary', is_expected:false) 90 | @delivery.calculate_misdirection_status(fake_last_handled_event, fake_itinerary).should be_true 91 | end 92 | 93 | it "should return false if is_expected() returns true" do 94 | fake_last_handled_event = double('last_handled_event') 95 | fake_itinerary = double('itinerary', is_expected:true) 96 | @delivery.calculate_misdirection_status(fake_last_handled_event, fake_itinerary).should be_false 97 | end 98 | end # context calculate_misdirection_status() 99 | 100 | # =========== on_track? =========== 101 | context "on_track?()" do 102 | before { @delivery = Delivery.new } 103 | 104 | it "should return true if routing_status is Routed and misdirected is false" do 105 | @delivery.instance_variable_set(:@routing_status, 'Routed') 106 | @delivery.stub(:is_misdirected).and_return(false) 107 | @delivery.on_track?.should be_true 108 | end 109 | 110 | it "should return false if routing_status is not Routed" do 111 | @delivery.instance_variable_set(:@routing_status, 'something') 112 | @delivery.on_track?.should be_false 113 | end 114 | 115 | it "should return false if is_misdirected is not false" do 116 | @delivery.instance_variable_set(:@routing_status, 'Routed') 117 | @delivery.stub(:is_misdirected).and_return(true) 118 | @delivery.on_track?.should be_false 119 | end 120 | end # context on_track?() 121 | 122 | # =========== calculate_routing_status =========== 123 | context "calculate_routing_status()" do 124 | before { @delivery = Delivery.new } 125 | 126 | it "should return nil if itinerary is nil" do 127 | @delivery.calculate_routing_status(nil, 'something').should be_nil 128 | end 129 | 130 | it "should return Routed if route specification is satisfied by the itinerary" do 131 | fake_route_specification = double('route_specification', is_satisfied_by:true) 132 | @delivery.calculate_routing_status('something', fake_route_specification).should == 'Routed' 133 | end 134 | 135 | it "should return Misrouted if route specification is not satisfied by the itinerary" do 136 | fake_route_specification = double('route_specification', is_satisfied_by:false) 137 | @delivery.calculate_routing_status('something', fake_route_specification).should == 'Misrouted' 138 | end 139 | end # context calculate_routing_status() 140 | 141 | # =========== calculate_transport_status =========== 142 | context "calculate_transport_status()" do 143 | before { @delivery = Delivery.new } 144 | 145 | it "should return 'Not Received' if last_handled_event is nil" do 146 | @delivery.calculate_transport_status(nil).should == 'Not Received' 147 | end 148 | 149 | {'Load' => 'Onboard Carrier', 'Unload' => 'In Port', 'Receive' => 'In Port', 'Claim' => 'Claimed', 150 | 'Something' => 'Unknown', nil => 'Unknown'}.each do |event_type, event_display| 151 | it "should show #{event_display} when last_handled_event is of type #{event_type}" do 152 | fake_last_handled_event = double('last_handled_event', event_type:event_type) 153 | fake_last_handled_event.should_receive(:event_type) 154 | @delivery.calculate_transport_status(fake_last_handled_event).should == event_display 155 | end 156 | end 157 | end # context calculate_transport_status() 158 | 159 | # =========== calculate_eta =========== 160 | context "calculate_eta()" do 161 | before { @delivery = Delivery.new } 162 | 163 | it "should return nil if not on track" do 164 | @delivery.stub(:on_track?).and_return(false) 165 | @delivery.calculate_eta('something').should be_nil 166 | end 167 | 168 | it "should return the final_arrival_date on itinerary if on track" do 169 | @delivery.stub(:on_track?).and_return(true) 170 | fake_itinerary = double('itinerary', final_arrival_date:'today') 171 | @delivery.calculate_eta(fake_itinerary).should == 'today' 172 | end 173 | end # context calculate_eta() 174 | 175 | # =========== calculate_next_expected_activity =========== 176 | context "calculate_next_expected_activity()" do 177 | before do 178 | @delivery = Delivery.new 179 | @delivery.stub(:on_track?).and_return(true) 180 | end 181 | 182 | it "should return nil if not on track" do 183 | @delivery.stub(:on_track?).and_return(false) 184 | @delivery.calculate_next_expected_activity('something', 'something', 'something').should be_nil 185 | end 186 | 187 | # this could be split into a context and three checks: class, param one, and param two 188 | it "should return a 'Receive' HandlingActivity if last_handled_event is nil" do 189 | fake_route_specification = double('route_specification', origin:'an_origin') 190 | return_value = @delivery.calculate_next_expected_activity(nil, fake_route_specification, 'something') 191 | return_value.should be_a_kind_of(HandlingActivity) 192 | return_value.handling_event_type.should == 'Receive' 193 | return_value.location.should == 'an_origin' 194 | end 195 | 196 | it "should return a 'Load' HandlingActivity if last_handled_event is 'Receive'" do 197 | fake_last_handled_event = double('last_handled_event', event_type:'Receive') 198 | fake_itinerary = double('itinerary') 199 | fake_itinerary.stub_chain(:legs,:first,:load_location).and_return('a_location') 200 | return_value = @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', fake_itinerary) 201 | return_value.should be_a_kind_of(HandlingActivity) 202 | return_value.handling_event_type.should == 'Load' 203 | return_value.location.should == 'a_location' 204 | end 205 | 206 | it "should return an 'Unload' HandlingActivity if last_handled_event is 'Load' and last_leg_index is not nil" do 207 | fake_last_handled_event = double('last_handled_event', event_type:'Load', location:'a_location') 208 | fake_legs = [double(load_location:'a_location', unload_location:'an_unload_location')] 209 | fake_itinerary = double('itinerary', legs:fake_legs) 210 | return_value = @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', fake_itinerary) 211 | return_value.should be_a_kind_of(HandlingActivity) 212 | return_value.handling_event_type.should == 'Unload' 213 | return_value.location.should == 'an_unload_location' 214 | end 215 | 216 | it "should return nil if last_handled_event is 'Load' and last_leg_index is nil" do 217 | fake_last_handled_event = double('last_handled_event', event_type:'Load', location:'an_unknown_location') 218 | fake_legs = [double(load_location:'a_location', unload_location:'an_unload_location')] 219 | fake_itinerary = double('itinerary', legs:fake_legs) 220 | @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', fake_itinerary).should be_nil 221 | end 222 | 223 | it "should return nil if last_handled_event is 'Unload' and there is no leg that matches the last_handled_event" do 224 | fake_last_handled_event = double('last_handled_event', event_type:'Unload', location:'an_unknown_location') 225 | fake_legs = 4.times.collect { |c| double("leg#{c}", load_location:"load_at_#{c}", unload_location:"unload_at_#{c}") } 226 | fake_itinerary = double('itinerary', legs:fake_legs) 227 | @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', fake_itinerary).should be_nil 228 | end 229 | 230 | it "should return a 'Claim' HandlingActivity if last_handled_event is 'Unload' and locations match and there is no next_leg" do 231 | fake_last_handled_event = double('last_handled_event', event_type:'Unload', location:'unload_at_2') 232 | fake_legs = 3.times.collect { |c| double("leg#{c}", load_location:"load_at_#{c}", unload_location:"unload_at_#{c}") } 233 | fake_legs << nil # set next_leg to be nil 234 | fake_itinerary = double('itinerary', legs:fake_legs) 235 | return_value = @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', fake_itinerary) 236 | return_value.should be_a_kind_of(HandlingActivity) 237 | return_value.handling_event_type.should == 'Claim' 238 | return_value.location.should == 'unload_at_1' 239 | end 240 | 241 | it "should return a 'Load' HandlingActivity if last_handled_event is 'Unload' and locations match and there is a next_leg" do 242 | fake_last_handled_event = double('last_handled_event', event_type:'Unload', location:'unload_at_2') 243 | fake_legs = 4.times.collect { |c| double("leg#{c}", load_location:"load_at_#{c}", unload_location:"unload_at_#{c}") } 244 | fake_itinerary = double('itinerary', legs:fake_legs) 245 | return_value = @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', fake_itinerary) 246 | return_value.should be_a_kind_of(HandlingActivity) 247 | return_value.handling_event_type.should == 'Load' 248 | return_value.location.should == 'unload_at_2' 249 | end 250 | 251 | it "should return nil if last_handled_event is 'Claim'" do 252 | fake_last_handled_event = double('last_handled_event', event_type:'Claim') 253 | @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', 'something').should be_nil 254 | end 255 | 256 | it "should return nil if last_handled_event is anything else" do 257 | fake_last_handled_event = double('last_handled_event', event_type:'something') 258 | @delivery.calculate_next_expected_activity(fake_last_handled_event, 'something', 'something').should be_nil 259 | end 260 | end # context calculate_next_expected_activity() 261 | 262 | # =========== comparison =========== 263 | context "comparison ( == )" do 264 | before do 265 | @fake_delivery_one = Delivery.new 266 | @fake_delivery_two = Delivery.new 267 | @comparators = %w[transport_status last_known_location is_misdirected eta is_unloaded_at_destination routing_status calculated_at last_handled_event next_expected_activity] 268 | end 269 | 270 | it "should make sure they are different objects" do 271 | @fake_delivery_one.object_id != @fake_delivery_two.object_id 272 | end 273 | 274 | it "should return true if one delivery is the same as another delivery" do 275 | @comparators.each do |called_method| 276 | return_value = "value_#{Random.rand(10)}" 277 | @fake_delivery_one.stub(called_method.to_sym).and_return(return_value) 278 | @fake_delivery_two.stub(called_method.to_sym).and_return(return_value) 279 | end 280 | (@fake_delivery_one == @fake_delivery_two).should be_true 281 | end 282 | 283 | context "checking each attribute/function" do 284 | before do 285 | @comparators.each do |called_method| 286 | return_value = "value_#{Random.rand(10)}" 287 | @fake_delivery_one.stub(called_method.to_sym).and_return(return_value) 288 | @fake_delivery_two.stub(called_method.to_sym).and_return(return_value) 289 | end 290 | end 291 | 292 | it "should be false" do 293 | @fake_delivery_one.stub(:eta).and_return('foo') 294 | (@fake_delivery_one == @fake_delivery_two).should be_false 295 | end 296 | 297 | # not sure why I can't just use @comparators here, but it doesn't work. 298 | %w[transport_status last_known_location is_misdirected eta is_unloaded_at_destination routing_status calculated_at last_handled_event next_expected_activity].each do |called_method| 299 | it "should return false if #{called_method} is different" do 300 | @fake_delivery_one.stub(called_method.to_sym).and_return('different') 301 | (@fake_delivery_one == @fake_delivery_two).should be_false 302 | end 303 | end # comparators 304 | end # context 305 | end # context comparison 306 | 307 | 308 | end 309 | -------------------------------------------------------------------------------- /app/assets/javascripts/bootstrap-datetimepicker.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * ========================================================= 4 | * bootstrap-datetimepicker.js 5 | * http://www.eyecon.ro/bootstrap-datepicker 6 | * ========================================================= 7 | * Copyright 2012 Stefan Petre 8 | * 9 | * Contributions: 10 | * - Andrew Rowls 11 | * - Thiago de Arruda 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | * ========================================================= 25 | */ 26 | (function($){var smartPhone=window.orientation!=undefined;var DateTimePicker=function(element,options){this.id=dpgId++;this.init(element,options)};var dateToDate=function(dt){if(typeof dt==="string"){return new Date(dt)}return dt};DateTimePicker.prototype={constructor:DateTimePicker,init:function(element,options){var icon;if(!(options.pickTime||options.pickDate))throw new Error("Must choose at least one picker");this.options=options;this.$element=$(element);this.language=options.language in dates?options.language:"en";this.pickDate=options.pickDate;this.pickTime=options.pickTime;this.isInput=this.$element.is("input");this.component=false;if(this.$element.find(".input-append")||this.$element.find(".input-prepend"))this.component=this.$element.find(".add-on");this.format=options.format;if(!this.format){if(this.isInput)this.format=this.$element.data("format");else this.format=this.$element.find("input").data("format");if(!this.format)this.format="MM/dd/yyyy"}this._compileFormat();if(this.component){icon=this.component.find("i")}if(this.pickTime){if(icon&&icon.length)this.timeIcon=icon.data("time-icon");if(!this.timeIcon)this.timeIcon="icon-time";icon.addClass(this.timeIcon)}if(this.pickDate){if(icon&&icon.length)this.dateIcon=icon.data("date-icon");if(!this.dateIcon)this.dateIcon="icon-calendar";icon.removeClass(this.timeIcon);icon.addClass(this.dateIcon)}this.widget=$(getTemplate(this.timeIcon,options.pickDate,options.pickTime,options.pick12HourFormat,options.pickSeconds,options.collapse)).appendTo("body");this.minViewMode=options.minViewMode||this.$element.data("date-minviewmode")||0;if(typeof this.minViewMode==="string"){switch(this.minViewMode){case"months":this.minViewMode=1;break;case"years":this.minViewMode=2;break;default:this.minViewMode=0;break}}this.viewMode=options.viewMode||this.$element.data("date-viewmode")||0;if(typeof this.viewMode==="string"){switch(this.viewMode){case"months":this.viewMode=1;break;case"years":this.viewMode=2;break;default:this.viewMode=0;break}}this.startViewMode=this.viewMode;this.weekStart=options.weekStart||this.$element.data("date-weekstart")||0;this.weekEnd=this.weekStart===0?6:this.weekStart-1;this.setStartDate(options.startDate||this.$element.data("date-startdate"));this.setEndDate(options.endDate||this.$element.data("date-enddate"));this.fillDow();this.fillMonths();this.fillHours();this.fillMinutes();this.fillSeconds();this.update();this.showMode();this._attachDatePickerEvents()},show:function(e){this.widget.show();this.height=this.component?this.component.outerHeight():this.$element.outerHeight();this.place();this.$element.trigger({type:"show",date:this._date});this._attachDatePickerGlobalEvents();if(e){e.stopPropagation();e.preventDefault()}},disable:function(){this.$element.find("input").prop("disabled",true);this._detachDatePickerEvents()},enable:function(){this.$element.find("input").prop("disabled",false);this._attachDatePickerEvents()},hide:function(){var collapse=this.widget.find(".collapse");for(var i=0;i");while(dowCnt'+dates[this.language].daysMin[dowCnt++%7]+"")}this.widget.find(".datepicker-days thead").append(html)},fillMonths:function(){var html="";var i=0;while(i<12){html+=''+dates[this.language].monthsShort[i++]+""}this.widget.find(".datepicker-months td").append(html)},fillDate:function(){var year=this.viewDate.getUTCFullYear();var month=this.viewDate.getUTCMonth();var currentDate=UTCDate(this._date.getUTCFullYear(),this._date.getUTCMonth(),this._date.getUTCDate(),0,0,0,0);var startYear=typeof this.startDate==="object"?this.startDate.getUTCFullYear():-Infinity;var startMonth=typeof this.startDate==="object"?this.startDate.getUTCMonth():-1;var endYear=typeof this.endDate==="object"?this.endDate.getUTCFullYear():Infinity;var endMonth=typeof this.endDate==="object"?this.endDate.getUTCMonth():12;this.widget.find(".datepicker-days").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-months").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-years").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-days th:eq(1)").text(dates[this.language].months[month]+" "+year);var prevMonth=UTCDate(year,month-1,28,0,0,0,0);var day=DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(),prevMonth.getUTCMonth());prevMonth.setUTCDate(day);prevMonth.setUTCDate(day-(prevMonth.getUTCDay()-this.weekStart+7)%7);if(year==startYear&&month<=startMonth||year=endMonth||year>endYear){this.widget.find(".datepicker-days th:eq(2)").addClass("disabled")}var nextMonth=new Date(prevMonth.valueOf());nextMonth.setUTCDate(nextMonth.getUTCDate()+42);nextMonth=nextMonth.valueOf();var html=[];var row;var clsName;while(prevMonth.valueOf()");html.push(row)}clsName="";if(prevMonth.getUTCFullYear()year||prevMonth.getUTCFullYear()==year&&prevMonth.getUTCMonth()>month){clsName+=" new"}if(prevMonth.valueOf()===currentDate.valueOf()){clsName+=" active"}if(prevMonth.valueOf()+864e5<=this.startDate){clsName+=" disabled"}if(prevMonth.valueOf()>this.endDate){clsName+=" disabled"}row.append(''+prevMonth.getUTCDate()+"");prevMonth.setUTCDate(prevMonth.getUTCDate()+1)}this.widget.find(".datepicker-days tbody").empty().append(html);var currentYear=this._date.getUTCFullYear();var months=this.widget.find(".datepicker-months").find("th:eq(1)").text(year).end().find("span").removeClass("active");if(currentYear===year){months.eq(this._date.getUTCMonth()).addClass("active")}if(currentYear-1endYear){this.widget.find(".datepicker-months th:eq(2)").addClass("disabled")}for(var i=0;i<12;i++){if(year==startYear&&startMonth>i||yearendYear){$(months[i]).addClass("disabled")}}html="";year=parseInt(year/10,10)*10;var yearCont=this.widget.find(".datepicker-years").find("th:eq(1)").text(year+"-"+(year+9)).end().find("td");this.widget.find(".datepicker-years").find("th").removeClass("disabled");if(startYear>year){this.widget.find(".datepicker-years").find("th:eq(0)").addClass("disabled")}if(endYearendYear?" disabled":"")+'">'+year+"";year+=1}yearCont.html(html)},fillHours:function(){var table=this.widget.find(".timepicker .timepicker-hours table");table.parent().hide();var html="";if(this.options.pick12HourFormat){var current=1;for(var i=0;i<3;i+=1){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current++}html+=""}}else{var current=0;for(var i=0;i<6;i+=1){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current++}html+=""}}table.html(html)},fillMinutes:function(){var table=this.widget.find(".timepicker .timepicker-minutes table");table.parent().hide();var html="";var current=0;for(var i=0;i<5;i++){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current+=3}html+=""}table.html(html)},fillSeconds:function(){var table=this.widget.find(".timepicker .timepicker-seconds table");table.parent().hide();var html="";var current=0;for(var i=0;i<5;i++){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current+=3}html+=""}table.html(html)},fillTime:function(){if(!this._date)return;var timeComponents=this.widget.find(".timepicker span[data-time-component]");var table=timeComponents.closest("table");var is12HourFormat=this.options.pick12HourFormat;var hour=this._date.getUTCHours();var period="AM";if(is12HourFormat){if(hour>=12)period="PM";if(hour===0)hour=12;else if(hour!=12)hour=hour%12;this.widget.find(".timepicker [data-action=togglePeriod]").text(period)}hour=padLeft(hour.toString(),2,"0");var minute=padLeft(this._date.getUTCMinutes().toString(),2,"0");var second=padLeft(this._date.getUTCSeconds().toString(),2,"0");timeComponents.filter("[data-time-component=hours]").text(hour);timeComponents.filter("[data-time-component=minutes]").text(minute);timeComponents.filter("[data-time-component=seconds]").text(second)},click:function(e){e.stopPropagation();e.preventDefault();this._unset=false;var target=$(e.target).closest("span, td, th");if(target.length===1){if(!target.is(".disabled")){switch(target[0].nodeName.toLowerCase()){case"th":switch(target[0].className){case"switch":this.showMode(1);break;case"prev":case"next":var vd=this.viewDate;var navFnc=DPGlobal.modes[this.viewMode].navFnc;var step=DPGlobal.modes[this.viewMode].navStep;if(target[0].className==="prev")step=step*-1;vd["set"+navFnc](vd["get"+navFnc]()+step);this.fillDate();this.set();break}break;case"span":if(target.is(".month")){var month=target.parent().find("span").index(target);this.viewDate.setUTCMonth(month)}else{var year=parseInt(target.text(),10)||0;this.viewDate.setUTCFullYear(year)}if(this.viewMode!==0){this._date=UTCDate(this.viewDate.getUTCFullYear(),this.viewDate.getUTCMonth(),this.viewDate.getUTCDate(),this._date.getUTCHours(),this._date.getUTCMinutes(),this._date.getUTCSeconds(),this._date.getUTCMilliseconds());this.notifyChange()}this.showMode(-1);this.fillDate();this.set();break;case"td":if(target.is(".day")){var day=parseInt(target.text(),10)||1;var month=this.viewDate.getUTCMonth();var year=this.viewDate.getUTCFullYear();if(target.is(".old")){if(month===0){month=11;year-=1}else{month-=1}}else if(target.is(".new")){if(month==11){month=0;year+=1}else{month+=1}}this._date=UTCDate(year,month,day,this._date.getUTCHours(),this._date.getUTCMinutes(),this._date.getUTCSeconds(),this._date.getUTCMilliseconds());this.viewDate=UTCDate(year,month,Math.min(28,day),0,0,0,0);this.fillDate();this.set();this.notifyChange()}break}}}},actions:{incrementHours:function(e){this._date.setUTCHours(this._date.getUTCHours()+1)},incrementMinutes:function(e){this._date.setUTCMinutes(this._date.getUTCMinutes()+1)},incrementSeconds:function(e){this._date.setUTCSeconds(this._date.getUTCSeconds()+1)},decrementHours:function(e){this._date.setUTCHours(this._date.getUTCHours()-1)},decrementMinutes:function(e){this._date.setUTCMinutes(this._date.getUTCMinutes()-1)},decrementSeconds:function(e){this._date.setUTCSeconds(this._date.getUTCSeconds()-1)},togglePeriod:function(e){var hour=this._date.getUTCHours();if(hour>=12)hour-=12;else hour+=12;this._date.setUTCHours(hour)},showPicker:function(){this.widget.find(".timepicker > div:not(.timepicker-picker)").hide();this.widget.find(".timepicker .timepicker-picker").show()},showHours:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-hours").show()},showMinutes:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-seconds").show()},selectHour:function(e){var tgt=$(e.target);var value=parseInt(tgt.text(),10);if(this.options.pick12HourFormat){var current=this._date.getUTCHours();if(current>=12){if(value!=12)value=(value+12)%24}else{if(value===12)value=0;else value=value%12}}this._date.setUTCHours(value);this.actions.showPicker.call(this)},selectMinute:function(e){var tgt=$(e.target);var value=parseInt(tgt.text(),10);this._date.setUTCMinutes(value);this.actions.showPicker.call(this)},selectSecond:function(e){var tgt=$(e.target);var value=parseInt(tgt.text(),10);this._date.setUTCSeconds(value);this.actions.showPicker.call(this)}},doAction:function(e){e.stopPropagation();e.preventDefault();if(!this._date)this._date=UTCDate(1970,0,0,0,0,0,0);var action=$(e.currentTarget).data("action");var rv=this.actions[action].apply(this,arguments);this.set();this.fillTime();this.notifyChange();return rv},stopEvent:function(e){e.stopPropagation();e.preventDefault()},keydown:function(e){var self=this,k=e.which,input=$(e.target);if(k==8||k==46){setTimeout(function(){self._resetMaskPos(input)})}},keypress:function(e){var k=e.which;if(k==8||k==46){return}var input=$(e.target);var c=String.fromCharCode(k);var val=input.val()||"";val+=c;var mask=this._mask[this._maskPos];if(!mask){return false}if(mask.end!=val.length){return}if(!mask.pattern.test(val.slice(mask.start))){val=val.slice(0,val.length-1);while((mask=this._mask[this._maskPos])&&mask.character){val+=mask.character;this._maskPos++}val+=c;if(mask.end!=val.length){input.val(val);return false}else{if(!mask.pattern.test(val.slice(mask.start))){input.val(val.slice(0,mask.start));return false}else{input.val(val);this._maskPos++;return false}}}else{this._maskPos++}},change:function(e){var input=$(e.target);var val=input.val();if(this._formatPattern.test(val)){this.update();this.setValue(this._date.getTime());this.notifyChange();this.set()}else if(val&&val.trim()){this.setValue(this._date.getTime());if(this._date)this.set();else input.val("")}else{if(this._date){this.setValue(null);this.notifyChange();this._unset=true}}this._resetMaskPos(input)},showMode:function(dir){if(dir){this.viewMode=Math.max(this.minViewMode,Math.min(2,this.viewMode+dir))}this.widget.find(".datepicker > div").hide().filter(".datepicker-"+DPGlobal.modes[this.viewMode].clsName).show()},destroy:function(){this._detachDatePickerEvents();this._detachDatePickerGlobalEvents();this.widget.remove();this.$element.removeData("datetimepicker");this.component.removeData("datetimepicker")},formatDate:function(d){return this.format.replace(formatReplacer,function(match){var methodName,property,rv,len=match.length;if(match==="ms")len=1;property=dateFormatComponents[match].property;if(property==="Hours12"){rv=d.getUTCHours();if(rv===0)rv=12;else if(rv!==12)rv=rv%12}else if(property==="Period12"){if(d.getUTCHours()>=12)return"PM";else return"AM"}else{methodName="get"+property;rv=d[methodName]()}if(methodName==="getUTCMonth")rv=rv+1;if(methodName==="getUTCYear")rv=rv+1900-2e3;return padLeft(rv.toString(),len,"0")})},parseDate:function(str){var match,i,property,methodName,value,parsed={};if(!(match=this._formatPattern.exec(str)))return null;for(i=1;ival.length){this._maskPos=i;break}else if(this._mask[i].end===val.length){this._maskPos=i+1;break}}},_finishParsingDate:function(parsed){var year,month,date,hours,minutes,seconds,milliseconds;year=parsed.UTCFullYear;if(parsed.UTCYear)year=2e3+parsed.UTCYear;if(!year)year=1970;if(parsed.UTCMonth)month=parsed.UTCMonth-1;else month=0;date=parsed.UTCDate||1;hours=parsed.UTCHours||0;minutes=parsed.UTCMinutes||0;seconds=parsed.UTCSeconds||0;milliseconds=parsed.UTCMilliseconds||0;if(parsed.Hours12){hours=parsed.Hours12}if(parsed.Period12){if(/pm/i.test(parsed.Period12)){if(hours!=12)hours=(hours+12)%24}else{hours=hours%12}}return UTCDate(year,month,date,hours,minutes,seconds,milliseconds)},_compileFormat:function(){var match,component,components=[],mask=[],str=this.format,propertiesByIndex={},i=0,pos=0;while(match=formatComponent.exec(str)){component=match[0];if(component in dateFormatComponents){i++;propertiesByIndex[i]=dateFormatComponents[component].property;components.push("\\s*"+dateFormatComponents[component].getPattern(this)+"\\s*");mask.push({pattern:new RegExp(dateFormatComponents[component].getPattern(this)),property:dateFormatComponents[component].property,start:pos,end:pos+=component.length})}else{components.push(escapeRegExp(component));mask.push({pattern:new RegExp(escapeRegExp(component)),character:component,start:pos,end:++pos})}str=str.slice(component.length)}this._mask=mask;this._maskPos=0;this._formatPattern=new RegExp("^\\s*"+components.join("")+"\\s*$");this._propertiesByIndex=propertiesByIndex},_attachDatePickerEvents:function(){var self=this;this.widget.on("click",".datepicker *",$.proxy(this.click,this));this.widget.on("click","[data-action]",$.proxy(this.doAction,this));this.widget.on("mousedown",$.proxy(this.stopEvent,this));if(this.pickDate&&this.pickTime){this.widget.on("click.togglePicker",".accordion-toggle",function(e){e.stopPropagation();var $this=$(this);var $parent=$this.closest("ul");var expanded=$parent.find(".collapse.in");var closed=$parent.find(".collapse:not(.in)");if(expanded&&expanded.length){var collapseData=expanded.data("collapse");if(collapseData&&collapseData.transitioning)return;expanded.collapse("hide");closed.collapse("show");$this.find("i").toggleClass(self.timeIcon+" "+self.dateIcon);self.$element.find(".add-on i").toggleClass(self.timeIcon+" "+self.dateIcon)}})}if(this.isInput){this.$element.on({focus:$.proxy(this.show,this),change:$.proxy(this.change,this)});if(this.options.maskInput){this.$element.on({keydown:$.proxy(this.keydown,this),keypress:$.proxy(this.keypress,this)})}}else{this.$element.on({change:$.proxy(this.change,this)},"input");if(this.options.maskInput){this.$element.on({keydown:$.proxy(this.keydown,this),keypress:$.proxy(this.keypress,this)},"input")}if(this.component){this.component.on("click",$.proxy(this.show,this))}else{this.$element.on("click",$.proxy(this.show,this))}}},_attachDatePickerGlobalEvents:function(){$(window).on("resize.datetimepicker"+this.id,$.proxy(this.place,this));if(!this.isInput){$(document).on("mousedown.datetimepicker"+this.id,$.proxy(this.hide,this))}},_detachDatePickerEvents:function(){this.widget.off("click",".datepicker *",this.click);this.widget.off("click","[data-action]");this.widget.off("mousedown",this.stopEvent);if(this.pickDate&&this.pickTime){this.widget.off("click.togglePicker")}if(this.isInput){this.$element.off({focus:this.show,change:this.change});if(this.options.maskInput){this.$element.off({keydown:this.keydown,keypress:this.keypress})}}else{this.$element.off({change:this.change},"input");if(this.options.maskInput){this.$element.off({keydown:this.keydown,keypress:this.keypress},"input")}if(this.component){this.component.off("click",this.show)}else{this.$element.off("click",this.show)}}},_detachDatePickerGlobalEvents:function(){$(window).off("resize.datetimepicker"+this.id);if(!this.isInput){$(document).off("mousedown.datetimepicker"+this.id)}},_isInFixed:function(){if(this.$element){var parents=this.$element.parents();var inFixed=false;for(var i=0;i'+"
    "+""+'
    '+DPGlobal.template+"
    "+""+'
  • '+""+'
    '+TPGlobal.getTemplate(is12Hours,showSeconds)+"
    "+""+"
"+""}else if(pickTime){return'"}else{return'"}}function UTCDate(){return new Date(Date.UTC.apply(Date,arguments))}var DPGlobal={modes:[{clsName:"days",navFnc:"UTCMonth",navStep:1},{clsName:"months",navFnc:"UTCFullYear",navStep:1},{clsName:"years",navFnc:"UTCFullYear",navStep:10}],isLeapYear:function(year){return year%4===0&&year%100!==0||year%400===0},getDaysInMonth:function(year,month){return[31,DPGlobal.isLeapYear(year)?29:28,31,30,31,30,31,31,30,31,30,31][month]},headTemplate:""+""+'‹'+''+'›'+""+"",contTemplate:''};DPGlobal.template='
'+''+DPGlobal.headTemplate+""+"
"+"
"+'
'+''+DPGlobal.headTemplate+DPGlobal.contTemplate+"
"+"
"+'
'+''+DPGlobal.headTemplate+DPGlobal.contTemplate+"
"+"
";var TPGlobal={hourTemplate:'',minuteTemplate:'',secondTemplate:''};TPGlobal.getTemplate=function(is12Hours,showSeconds){return'
'+'"+""+''+''+''+(showSeconds?''+'':"")+(is12Hours?'':"")+""+""+" "+''+" "+(showSeconds?''+"":"")+(is12Hours?''+"":"")+""+""+''+''+''+(showSeconds?''+'':"")+(is12Hours?'':"")+""+"
"+TPGlobal.hourTemplate+":"+TPGlobal.minuteTemplate+":"+TPGlobal.secondTemplate+""+''+"
"+"
"+'
'+''+"
"+"
"+'
'+''+"
"+"
"+(showSeconds?'
'+''+"
"+"
":"")}})(window.jQuery); --------------------------------------------------------------------------------