periodic_records 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e0429dc9d8d852306fb1ef7f6d97ea8111189fd0
4
+ data.tar.gz: 62588c2e384ae43407036906f73b4378ced4f7d3
5
+ SHA512:
6
+ metadata.gz: 798fe949cd0c014f0961211a1f98641ccbcdaa414821810f317e3c0a2078e6ba3e734ac5649f0e63af89544dccd66703119ba4a16898abd87fb236f009a0aaa4
7
+ data.tar.gz: 52a122f179ab08dc6749f610bc438abe5eeb7caaf3f8134fe70719c05f39b40c7b18add41c9bea33a5681c629a88cfa136ce02a93dcf13b7de801a40dd7076b3
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in periodic_records.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Toms Mikoss
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.
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # PeriodicRecords
2
+
3
+ [![Build Status](https://travis-ci.org/mak-it/periodic_records.svg?branch=master)](https://travis-ci.org/mak-it/periodic_records)
4
+
5
+ Support functions for ActiveRecord models with periodic entries.
6
+
7
+ * Supports periods where the smallest unit is a whole day
8
+ * Adjusts and splits overlapping records
9
+ * Preloads currently active records to avoid N+1 queries
10
+ * Easy querying within history - join returns 0..1 records (no grouping needed)
11
+ `LEFT JOIN ... ON ... AND <date> BETWEEN start_at AND end_at`
12
+
13
+ For example you have employees table and assignments table that stores all the
14
+ employment history.
15
+
16
+ Employees:
17
+
18
+ id | name
19
+ ---|------
20
+ 1 | John
21
+
22
+ Employee assignments:
23
+
24
+ id | employee_id | start_at | end_at | job_title
25
+ ---|-------------|------------|------------|----------
26
+ 1 | 1 | 2014-01-01 | 9999-01-01 | Developer
27
+
28
+ Now John is promoted to "Senior Developer" and you create a new employee
29
+ assignment record and this gem will take care of adjusting and splitting
30
+ overlapping records. In this case it will adjust the `end_at` field for the
31
+ previous assignment.
32
+
33
+ id | employee_id | start_at | end_at | job_title
34
+ ---|-------------|------------|------------|-----------------
35
+ 1 | 1 | 2014-01-01 | 2018-05-04 | Developer
36
+ 2 | 1 | 2018-05-05 | 9999-01-01 | Senior Developer
37
+
38
+
39
+ ## Installation
40
+
41
+ Add this line to your application's Gemfile:
42
+
43
+ ```ruby
44
+ gem 'periodic_records'
45
+ ```
46
+
47
+ And then execute:
48
+
49
+ ```bash
50
+ $ bundle
51
+ ```
52
+
53
+ Or install it yourself as:
54
+
55
+ ```bash
56
+ $ gem install periodic_records
57
+ ```
58
+
59
+ ## Preparation
60
+
61
+ Ensure `start_at` and `end_at` date columns on the model that will have
62
+ periodic versions.
63
+ Include `PeriodicRecords::Model` and define `siblings` method:
64
+
65
+ ```ruby
66
+ class EmployeeAssignment < ActiveRecord::Base
67
+ include PeriodicRecords::Model
68
+
69
+ belongs_to :employee
70
+
71
+ def siblings
72
+ self.class.where(employee_id: employee_id).where.not(id: id)
73
+ end
74
+ end
75
+ ```
76
+
77
+ Include `PeriodicRecords::Associations` in the model that has periodic
78
+ associations, and call `has_periodic`:
79
+
80
+ ```ruby
81
+ class Employee < ActiveRecord::Base
82
+ include PeriodicRecords::Associations
83
+
84
+ has_many :employee_assignments, inverse_of: :employee
85
+ has_periodic :employee_assignments, as: :assignments
86
+ end
87
+ ```
88
+
89
+ ## Usage
90
+
91
+ Look up the currently active record with `model.current_association`:
92
+
93
+ ```ruby
94
+ employee.current_assignment
95
+ ```
96
+
97
+ Look up records for specific date or period
98
+ with `within_date` and `within_interval`:
99
+
100
+ ```ruby
101
+ employee.employee_assignments.within_date(Date.tomorrow)
102
+ ```
103
+
104
+ ```ruby
105
+ employee.employee_assignments.within_interval(Date.current.beginning_of_month...Date.current.end_of_month)
106
+ ```
107
+
108
+ Look up records starting with specific date with `from_date`
109
+
110
+ ```ruby
111
+ employee.employee_assignments.from_date(Date.tomorrow)
112
+ ```
113
+
114
+ Preload currently active records, to avoid N+1 queries on `current_assignment`.
115
+
116
+ ```ruby
117
+ employees = Employee.all
118
+ Employee.preload_current_assignments(employees)
119
+ employees.each do |employee|
120
+ puts employee.current_assignment.to_s
121
+ end
122
+ ```
123
+
124
+ ## Development
125
+
126
+ After checking out the repo, run `bin/setup` to install dependencies.
127
+ Then, run `bin/console` for an interactive prompt that will allow you to
128
+ experiment.
129
+
130
+ To install this gem onto your local machine, run `bundle exec rake install`.
131
+ To release a new version, update the version number in `version.rb`,
132
+ and then run `bundle exec rake release` to create a git tag for the version,
133
+ push git commits and tags, and push the `.gem` file
134
+ to [rubygems.org](https://rubygems.org).
135
+
136
+ ## Contributing
137
+
138
+ 1. Fork it ( https://github.com/mak-it/periodic_records/fork )
139
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
140
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
141
+ 4. Push to the branch (`git push origin my-new-feature`)
142
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+
5
+ require 'active_record'
6
+ require 'active_support/all'
7
+
8
+ require "periodic_records"
9
+
10
+ # You can add fixtures and/or initialization code here to make experimenting
11
+ # with your gem easier. You can also use a different console, if you like.
12
+
13
+ # (If you use this, don't forget to add pry to your Gemfile!)
14
+ # require "pry"
15
+ # Pry.start
16
+
17
+ require "irb"
18
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ require "periodic_records/version"
2
+ require "periodic_records/model"
3
+ require "periodic_records/associations"
4
+
5
+ module PeriodicRecords
6
+ end
@@ -0,0 +1,73 @@
1
+ module PeriodicRecords
2
+ module Associations
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def has_periodic(association, as: nil)
7
+ as ||= association
8
+ define_periodic_preload_method(association, as)
9
+ define_periodic_default_method(association, as)
10
+ define_periodic_current_method(association, as)
11
+ end
12
+
13
+ private
14
+
15
+ def define_periodic_preload_method(association, as)
16
+ method_name = "preload_current_#{as}"
17
+ accessor_name = "current_#{as.to_s.singularize}"
18
+ define_singleton_method method_name do |records, *associations|
19
+ reflection = reflections[association]
20
+ records_hash = {}
21
+ records.each do |record|
22
+ record.send("#{accessor_name}=", nil)
23
+ records_hash[record.id] = record
24
+ end
25
+ states = reflection.klass.current.
26
+ where(reflection.foreign_key => records_hash.keys)
27
+ states.each do |state|
28
+ record = records_hash[state.send(reflection.foreign_key)]
29
+ record.send("#{accessor_name}=", state)
30
+ state.send("#{reflection.inverse_of.name}=", record)
31
+ end
32
+ unless associations.empty?
33
+ ActiveRecord::Associations::Preloader.new.
34
+ preload(states, associations)
35
+ end
36
+ end
37
+ end
38
+
39
+ # def default_assignment
40
+ # @default_assignment ||= employee_assignments.new
41
+ # end
42
+ def define_periodic_default_method(association, as)
43
+ accessor_name = "default_#{as.to_s.singularize}"
44
+ define_method accessor_name do
45
+ instance_variable_get("@#{accessor_name}") ||
46
+ instance_variable_set("@#{accessor_name}", send(association).new)
47
+ end
48
+ end
49
+
50
+ # attr_writer :current_assignment
51
+ # def current_assignment
52
+ # unless defined?(@current_assignment)
53
+ # @current_assignment = \
54
+ # employee_assignments.to_a.find(&:current?) ||
55
+ # default_assignment
56
+ # end
57
+ # @current_assignment
58
+ # end
59
+ def define_periodic_current_method(association, as)
60
+ accessor_name = "current_#{as.to_s.singularize}"
61
+ attr_writer accessor_name
62
+ define_method accessor_name do
63
+ unless instance_variable_defined?("@#{accessor_name}")
64
+ current = send(association).to_a.find(&:current?)
65
+ value = current || send("default_#{as.to_s.singularize}")
66
+ instance_variable_set("@#{accessor_name}", value)
67
+ end
68
+ instance_variable_get("@#{accessor_name}")
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,114 @@
1
+ module PeriodicRecords
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ MIN = Date.new(0001, 1, 1)
6
+ MAX = Date.new(9999, 1, 1)
7
+
8
+ included do
9
+ validates_presence_of :start_at, :end_at
10
+ validate :validate_dates
11
+
12
+ after_initialize :set_default_period, if: :set_default_period_after_initialize?
13
+ after_save :adjust_overlaping_records
14
+ end
15
+
16
+ module ClassMethods
17
+ def within_interval(start_date, end_date)
18
+ t = arel_table
19
+ where(t[:start_at].lteq(end_date)).
20
+ where(t[:end_at].gteq(start_date))
21
+ end
22
+
23
+ def within_date(date)
24
+ within_interval(date, date)
25
+ end
26
+
27
+ def current
28
+ date = Date.current
29
+ within_date(date)
30
+ end
31
+
32
+ def from_date(date)
33
+ t = arel_table
34
+ where(t[:end_at].gteq(date))
35
+ end
36
+ end
37
+
38
+ def current?
39
+ date = Date.current
40
+ within_interval?(date, date)
41
+ end
42
+
43
+ def within_interval?(start_date, end_date)
44
+ start_at && end_at && start_at <= end_date && end_at >= start_date
45
+ end
46
+
47
+ def siblings
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def overlaping_records
52
+ @overlaping_records ||= siblings.within_interval(start_at, end_at)
53
+ end
54
+
55
+ def adjust_overlaping_records
56
+ overlaping_records.each do |overlaping_record|
57
+ if overlaping_record.start_at >= start_at &&
58
+ overlaping_record.end_at <= end_at
59
+ destroy_overlaping_record(overlaping_record)
60
+ elsif overlaping_record.start_at < start_at &&
61
+ overlaping_record.end_at > end_at
62
+ split_overlaping_record(overlaping_record)
63
+ elsif overlaping_record.start_at < start_at
64
+ adjust_overlaping_record_end_at(overlaping_record)
65
+ elsif overlaping_record.end_at > end_at
66
+ adjust_overlaping_record_start_at(overlaping_record)
67
+ end
68
+ end
69
+ end
70
+
71
+ def set_default_period_after_initialize?
72
+ new_record?
73
+ end
74
+
75
+ private
76
+
77
+ def set_default_period
78
+ self.start_at ||= Date.current
79
+ self.end_at ||= MAX
80
+ end
81
+
82
+ def destroy_overlaping_record(overlaping_record)
83
+ overlaping_record.destroy
84
+ end
85
+
86
+ def split_overlaping_record(overlaping_record)
87
+ overlaping_record_end = overlaping_record.dup
88
+ overlaping_record_end.start_at = end_at + 1.day
89
+ overlaping_record_end.end_at = overlaping_record.end_at
90
+
91
+ overlaping_record_start = overlaping_record
92
+ overlaping_record_start.end_at = start_at - 1.day
93
+
94
+ overlaping_record_start.save(validate: false)
95
+ overlaping_record_end.save(validate: false)
96
+ end
97
+
98
+ def adjust_overlaping_record_end_at(overlaping_record)
99
+ overlaping_record.end_at = start_at - 1.day
100
+ overlaping_record.save(validate: false)
101
+ end
102
+
103
+ def adjust_overlaping_record_start_at(overlaping_record)
104
+ overlaping_record.start_at = end_at + 1.day
105
+ overlaping_record.save(validate: false)
106
+ end
107
+
108
+ def validate_dates
109
+ if start_at && end_at && end_at < start_at
110
+ errors.add :end_at, :invalid
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ module PeriodicRecords
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'periodic_records/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "periodic_records"
8
+ spec.version = PeriodicRecords::VERSION
9
+ spec.authors = ["Edgars Beigarts", "Toms Mikoss"]
10
+ spec.email = ["edgars.beigarts@makit.lv", "toms.mikoss@makit.lv"]
11
+
12
+ spec.summary = %q{Support functions for ActiveRecord models with periodic entries}
13
+ spec.description = %q{Support functions for ActiveRecord models with periodic entries}
14
+ spec.homepage = "https://github.com/mak-it/periodic_records"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency "activerecord", [">= 4", "< 5"]
23
+ spec.add_runtime_dependency "activesupport", [">= 4", "< 5"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.9"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.3"
28
+ spec.add_development_dependency "sqlite3"
29
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: periodic_records
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Edgars Beigarts
8
+ - Toms Mikoss
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2015-09-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '5'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '4'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ - !ruby/object:Gem::Dependency
35
+ name: activesupport
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4'
41
+ - - "<"
42
+ - !ruby/object:Gem::Version
43
+ version: '5'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '4'
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.9'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.3'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.3'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sqlite3
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Support functions for ActiveRecord models with periodic entries
111
+ email:
112
+ - edgars.beigarts@makit.lv
113
+ - toms.mikoss@makit.lv
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - bin/console
126
+ - bin/setup
127
+ - lib/periodic_records.rb
128
+ - lib/periodic_records/associations.rb
129
+ - lib/periodic_records/model.rb
130
+ - lib/periodic_records/version.rb
131
+ - periodic_records.gemspec
132
+ homepage: https://github.com/mak-it/periodic_records
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.4.6
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Support functions for ActiveRecord models with periodic entries
156
+ test_files: []