├── 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 | Type
6 | Location
7 | Completion Date
8 | Tracking ID
9 |
10 |
11 | <% @handling_events_history.each do |handling_event| %>
12 |
13 | <%= handling_event.event_type %>
14 | <%= handling_event.location.name %>
15 | <%= handling_event.completion_date %>
16 | <%= handling_event.tracking_id %>
17 |
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/app/views/handling_events/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to 'New handling event', new_handling_event_path %>
2 |
3 |
4 |
5 | Type
6 | Location
7 | Completion Date
8 | Tracking ID
9 |
10 |
11 | <% @handling_events_history.each do |handling_event| %>
12 |
13 | <%= handling_event.event_type %>
14 | <%= handling_event.location.name %>
15 | <%= handling_event.completion_date %>
16 | <%= handling_event.tracking_id %>
17 |
18 | <% end %>
19 |
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 | Voyage
17 | From
18 | Load date
19 | To
20 | Unload date
21 |
22 | <% @cargo.itinerary.legs.each do | leg | %>
23 |
24 | <%= leg.voyage %>
25 | <%= leg.load_location.name %>
26 | <%= leg.load_date.strftime("%m/%d/%Y") %>
27 | <%= leg.unload_location.name %>
28 | <%= leg.unload_date.strftime("%m/%d/%Y") %>
29 |
30 | <% end %>
31 |
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 | Tracking ID
8 | Origin
9 | Destination
10 | Routed?
11 |
12 |
13 |
14 |
15 | <% @cargo_documents.each do |cargo_document| -%>
16 |
17 | <%= link_to cargo_document.tracking_id, "bookings/" + cargo_document.tracking_id, :class => 'btn btn-mini btn-success' %>
18 | <%= cargo_document.origin_name %>
19 | <%= cargo_document.destination_name %>
20 | <%= cargo_document.leg_documents.count > 0 ? "Yes" : "No" %>
21 |
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 |
27 |
28 | <% end -%>
29 |
30 |
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 | Type
22 | Location
23 | Completion Date
24 | Tracking ID
25 |
26 |
27 | <% @handling_events_history.each do |handling_event| %>
28 |
29 | <%= handling_event.event_type %>
30 | <%= handling_event.location.name %>
31 | <%= handling_event.completion_date.strftime("%m/%d/%Y") %>
32 | <%= handling_event.tracking_id %>
33 |
34 | <% end %>
35 |
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 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Ruby DDD Sample App
24 |
25 |
26 | <%= link_to "Bookings", bookings_path %>
27 | <%= link_to "Tracking Cargos", tracking_cargos_path %>
28 | <%= link_to "Handling Events", handling_events_path %>
29 |
30 |
31 |
32 |
33 |
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 |
13 | <% @booking.errors.full_messages.each do |message| %>
14 | <%= message %>
15 | <% end %>
16 |
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