openopus-core-people 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +38 -0
- data/Rakefile +32 -0
- data/app/assets/config/openopus_core_people_manifest.js +0 -0
- data/app/models/address.rb +168 -0
- data/app/models/credential.rb +15 -0
- data/app/models/email.rb +19 -0
- data/app/models/label.rb +11 -0
- data/app/models/nickname.rb +3 -0
- data/app/models/organization.rb +12 -0
- data/app/models/person.rb +244 -0
- data/app/models/phone.rb +26 -0
- data/app/models/user.rb +32 -0
- data/config/routes.rb +2 -0
- data/db/migrate/00000000000000_create_labels.rb +10 -0
- data/db/migrate/00000000000001_create_nicknames.rb +9 -0
- data/db/migrate/00000000000002_create_addresses.rb +19 -0
- data/db/migrate/00000000000003_create_phones.rb +12 -0
- data/db/migrate/00000000000004_create_emails.rb +12 -0
- data/db/migrate/00000000000005_create_people.rb +15 -0
- data/db/migrate/00000000000006_create_organizations.rb +10 -0
- data/db/migrate/00000000000007_create_users.rb +10 -0
- data/db/migrate/00000000000008_create_credentials.rb +13 -0
- data/db/migrate/00000000000009_create_join_table_organizations_users.rb +8 -0
- data/db/seeds.rb +8 -0
- data/lib/openopus/core/people.rb +9 -0
- data/lib/openopus/core/people/engine.rb +16 -0
- data/lib/openopus/core/people/version.rb +7 -0
- data/lib/tasks/openopus/core/people_tasks.rake +4 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fb6c5d918b4cf8dc73c3c5a22503673dc1ddf9558a8447ef1a505154007753d9
|
4
|
+
data.tar.gz: e749bc852fe3e432d4aae6e3ce33c04135a94fca16cda43c2ccaecc25b3e0942
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5abc7901b229c1850154f628bb6c6efe47bfe3e39ebbf45c0e7d49c4a9286f48c5a1b48af70aaff01fe40ce85d1a148b9f166e83c90fbf371f0eb816aa5f252c
|
7
|
+
data.tar.gz: 9c08ec78f0de97a8e880295e6e01a7db12471371c59c6db043533986d041bef2317fdbee48c7ba6c78a6c1317a6fca432a5641ee75e04d828efdc959ff8c43f9
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Brian J. Fox
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Openopus::Core::People
|
2
|
+
Model the real world of people in your application, making user interaction robust. By way of example, a person can have many email addresses, but this is not usually represented in applications. openopus/core/people creates the database structure, relations, and convenience functions for your application so you don't have to. Just connect your end user model, and away you go.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
Install the gem, run migrations. This will create human structures, from Organization down to people (Person). More documentation and usage examples will be forthcoming.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
opus = Organization.where(name: "Opus Logica, Inc").first_or_create(nicknames_attributes: [{ nickname: "OPUS" }])
|
9
|
+
bfox = User.lookup("bfox@opuslogica.com") ||
|
10
|
+
User.create(person_attributes: { email: "bfox@opuslogica.com", name: "Brian Jhan Fox",
|
11
|
+
address: "901 Olive St., Santa Barbara, CA, 93101",
|
12
|
+
phone: "805.555.8642" },
|
13
|
+
organization: opus,
|
14
|
+
credentials_attributes: [{ password: "idtmp2tv" }])
|
15
|
+
```
|
16
|
+
## Installation
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'openopus-core-people', git: "https://github.com/opuslogica/openopus-core-people"
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
```bash
|
25
|
+
$ bundle install
|
26
|
+
$ bundle exec rake db:migrate
|
27
|
+
```
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
```bash
|
31
|
+
$ gem install openopus-core-people
|
32
|
+
```
|
33
|
+
|
34
|
+
## Contributing
|
35
|
+
Got an idea for an improvement? Think this thing is *almost* good, but still kinda sucks? Create an issue, and we'll get back to you ASAP. Please discuss code contributions with us before hand so that we can agree on the architecture. Otherwise, we might not be able to take your improvements.
|
36
|
+
|
37
|
+
## License
|
38
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Openopus::Core::People'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
File without changes
|
@@ -0,0 +1,168 @@
|
|
1
|
+
class Address < ApplicationRecord
|
2
|
+
belongs_to :label, optional: true
|
3
|
+
belongs_to :addressable, polymorphic: true
|
4
|
+
|
5
|
+
accepts_nested_attributes_for :label
|
6
|
+
|
7
|
+
# require 'carmen'
|
8
|
+
# require 'geocoder'
|
9
|
+
# geocoded_by :oneline, latitude: :lat, longitude: :lon
|
10
|
+
# after_validation :geocode
|
11
|
+
|
12
|
+
def address
|
13
|
+
oneline
|
14
|
+
end
|
15
|
+
|
16
|
+
def save(options={})
|
17
|
+
r = super(options)
|
18
|
+
@already_geocoded = false
|
19
|
+
r
|
20
|
+
end
|
21
|
+
|
22
|
+
def assign_attributes(hash)
|
23
|
+
if hash.has_key?('address')
|
24
|
+
a = hash['address']
|
25
|
+
hash.delete('address')
|
26
|
+
end
|
27
|
+
super(hash)
|
28
|
+
self.address = a if a
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def label=(text_or_label)
|
33
|
+
l = Label.get(text_or_label.to_s) if text_or_label.is_a?(String) or text_or_label.is_a?(Symbol)
|
34
|
+
l ||= text_or_label
|
35
|
+
write_attribute(:label_id, l.id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_lat_lon(info=nil)
|
39
|
+
info ||= Geocoder.search(self.oneline).first rescue nil
|
40
|
+
if info
|
41
|
+
if info.respond_to?(:latitude)
|
42
|
+
self.lat = info.latitude
|
43
|
+
self.lon = info.longitude
|
44
|
+
else
|
45
|
+
self.lat = info.geometry["location"]["lat"] rescue nil
|
46
|
+
self.lon = info.geometry["location"]["lng"] rescue nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def address=(string)
|
52
|
+
return unless not string.blank? and not postal_changed?
|
53
|
+
return unless string != line1
|
54
|
+
return if (string == oneline() || string == oneline({full: true}))
|
55
|
+
|
56
|
+
have_geocoder = defined?(Geocoder)
|
57
|
+
r = Geocoder.search(string) rescue nil
|
58
|
+
@already_geocoded = true
|
59
|
+
|
60
|
+
if not r or r.length == 0
|
61
|
+
self.errors.add(:address, :did_not_geocode, message: "No locations found") if have_geocoder
|
62
|
+
self.line1 = string
|
63
|
+
else
|
64
|
+
res = r[0]
|
65
|
+
self.update_lat_lon(res)
|
66
|
+
if res.respond_to?(:street)
|
67
|
+
self.line1 = (res.house_number ? res.house_number + " " : "") + res.street
|
68
|
+
self.line2 = nil
|
69
|
+
c = Carmen::Country.coded(res.country_code) rescue nil
|
70
|
+
s = c.subregions.named(res.state) if c
|
71
|
+
self.state = s.code.to_s if s
|
72
|
+
else
|
73
|
+
self.line1 = res.street_address.to_s rescue string
|
74
|
+
self.line2 = nil
|
75
|
+
self.state = res.state_code.to_s
|
76
|
+
end
|
77
|
+
self.city = res.city.to_s
|
78
|
+
self.postal = res.postal_code.to_s
|
79
|
+
self.country = res.country_code.to_s.upcase
|
80
|
+
end
|
81
|
+
|
82
|
+
self.errors.add(:address, :too_many_matches, message: "Too many matches") if have_geocoder && r && r.length > 1
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.find_by_address(string)
|
87
|
+
template = self.parse(string)
|
88
|
+
self.find_by(template.attributes.symbolize_keys!.slice(:line1, :line2, :city, :state))
|
89
|
+
end
|
90
|
+
|
91
|
+
def as_json(options={})
|
92
|
+
for_user = options.delete(:for_user)
|
93
|
+
res = super(options)
|
94
|
+
res[:address] = oneline
|
95
|
+
res[:label] = self.label.value if self.label
|
96
|
+
res
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_from_postal
|
100
|
+
return if @already_geocoded or self.postal.blank?
|
101
|
+
r = Geocoder.search(self.postal) rescue nil
|
102
|
+
if not r or r.length == 0
|
103
|
+
return
|
104
|
+
self.errors.add(:postal, :cannot_locate_postal_code, message: "Couldn't locate postal code")
|
105
|
+
else
|
106
|
+
res = r[0]
|
107
|
+
self.update_lat_lon(res)
|
108
|
+
self.city = res.city
|
109
|
+
self.state = res.state_code
|
110
|
+
self.country = res.country_code
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def oneline(options={})
|
115
|
+
if options[:full]
|
116
|
+
return [line1, line2, city, state, postal].compact.join(", ").gsub(/, ,/, ",")
|
117
|
+
else
|
118
|
+
return "#{line1}, #{city}, #{state}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.parse(string)
|
123
|
+
a = self.new
|
124
|
+
a.address = string
|
125
|
+
a
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.new_from_hash(incoming_hash)
|
129
|
+
p = {}
|
130
|
+
incoming_hash.each {|key, val| p[key] = val if not val.blank?}
|
131
|
+
a = self.new
|
132
|
+
a.line1 = p[:line1] || p[:address]
|
133
|
+
a.line2 = p[:line2]
|
134
|
+
a.city = p[:city]
|
135
|
+
a.state = p[:state]
|
136
|
+
a.country = p[:country]
|
137
|
+
a.postal = p[:postal] || p[:zipcode] || p[:zip]
|
138
|
+
a.label = Label.get("Work")
|
139
|
+
a
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.find_or_create_by_example(other)
|
143
|
+
res = self
|
144
|
+
res = res.where(:line1 => other.line1) if other.line1
|
145
|
+
res = res.where(:line2 => other.line2) if other.line2
|
146
|
+
res = res.where(:city => other.city) if other.city
|
147
|
+
res = res.where(:state => other.state) if other.state
|
148
|
+
res = res.where(:country => other.country) if other.country
|
149
|
+
res = res.where(:postal => other.postal) if other.postal
|
150
|
+
|
151
|
+
a = nil
|
152
|
+
addresses = res
|
153
|
+
if addresses != self
|
154
|
+
a = Address.create(other.attributes.symbolize_keys!.except(:id, :created_at, :updated_at)) if addresses == []
|
155
|
+
a ||= addresses[0]
|
156
|
+
end
|
157
|
+
|
158
|
+
a
|
159
|
+
end
|
160
|
+
|
161
|
+
def admin_object_name
|
162
|
+
[line1, city, postal].join(" ").strip rescue ""
|
163
|
+
end
|
164
|
+
|
165
|
+
def person
|
166
|
+
self.people.first rescue nil
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Credential < ApplicationRecord
|
2
|
+
has_secure_password
|
3
|
+
belongs_to :credentialed, polymorphic: true
|
4
|
+
|
5
|
+
# We point to an enail that actually polymorphically belongs to a different thing.
|
6
|
+
# Which is great. So don't declare this to be a polymorphic relationship. It isn't.
|
7
|
+
belongs_to :email
|
8
|
+
before_validation :grab_email_from_credentialed
|
9
|
+
|
10
|
+
# validates :password, length: { minimum: 8 }, on: :create
|
11
|
+
|
12
|
+
def grab_email_from_credentialed
|
13
|
+
self.email ||= self.credentialed.email
|
14
|
+
end
|
15
|
+
end
|
data/app/models/email.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class Email < ApplicationRecord
|
2
|
+
belongs_to :label, optional: true
|
3
|
+
belongs_to :emailable, polymorphic: true
|
4
|
+
accepts_nested_attributes_for :label
|
5
|
+
before_create :default_label
|
6
|
+
|
7
|
+
def self.canonicalize(addr)
|
8
|
+
addr.strip.downcase if not addr.blank?
|
9
|
+
end
|
10
|
+
|
11
|
+
def canonicalize
|
12
|
+
self.address = self.class.canonicalize(self.address)
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_label
|
16
|
+
self.label = Label.get("Work")
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/app/models/label.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class Organization < ApplicationRecord
|
2
|
+
has_many :nicknames, as: :nicknameable
|
3
|
+
accepts_nested_attributes_for :nicknames
|
4
|
+
has_and_belongs_to_many :users
|
5
|
+
accepts_nested_attributes_for :users
|
6
|
+
|
7
|
+
def self.lookup(thing)
|
8
|
+
candidate = self.where(name: thing).first
|
9
|
+
candidate ||= self.includes(:nicknames).joins(:nicknames).find_by("nicknames.nickname" => thing)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
class Person < ApplicationRecord
|
2
|
+
has_many :users
|
3
|
+
has_many :nicknames, as: :nicknameable
|
4
|
+
has_many :addresses, as: :addressable
|
5
|
+
has_many :phones, as: :phoneable
|
6
|
+
has_many :emails, as: :emailable
|
7
|
+
|
8
|
+
accepts_nested_attributes_for :nicknames
|
9
|
+
accepts_nested_attributes_for :addresses
|
10
|
+
accepts_nested_attributes_for :phones
|
11
|
+
accepts_nested_attributes_for :emails
|
12
|
+
|
13
|
+
def self.lookup(name)
|
14
|
+
person = self.find_by(self.name_components(name))
|
15
|
+
person ||= self.includes(:nicknames).joins(:nicknames).find_by("nicknames.nickname" => name)
|
16
|
+
if not person
|
17
|
+
people = self.all.collect do |p|
|
18
|
+
[p.id, [(p.fname[0] || ""), (p.minitial[0] || ""), (p.lname[0] || "")].join("").upcase]
|
19
|
+
end
|
20
|
+
people.each do |parry|
|
21
|
+
if parry[1] == name.upcase
|
22
|
+
person = self.find(parry[0])
|
23
|
+
break
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if not person
|
29
|
+
addr = Email.where(emailable_type: self.name.to_s, address: Email.canonicalize(name.downcase)).first
|
30
|
+
person = addr.person.first if addr
|
31
|
+
end
|
32
|
+
|
33
|
+
person
|
34
|
+
end
|
35
|
+
|
36
|
+
def user
|
37
|
+
self.users.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def user=(u)
|
41
|
+
self.users = [u]
|
42
|
+
end
|
43
|
+
|
44
|
+
def email
|
45
|
+
self.emails.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def email=(address)
|
49
|
+
e = self.emails.where(address: Email.canonicalize(address)).first_or_initialize() if not address.blank?
|
50
|
+
self.emails << e if not emails.include?(e)
|
51
|
+
e
|
52
|
+
end
|
53
|
+
|
54
|
+
def phone
|
55
|
+
p = self.phones.find_by(label: Label.get("Work"))
|
56
|
+
p ||= self.phones.first
|
57
|
+
p
|
58
|
+
end
|
59
|
+
|
60
|
+
def phone=(number)
|
61
|
+
if number.is_a?(Phone)
|
62
|
+
self.phones << number unless self.phones.include?(number)
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
if not number.blank?
|
67
|
+
p = self.phones.first rescue nil
|
68
|
+
p ||= Phone.create(phoneable_type: self.class.name, label: Label.get("Work"))
|
69
|
+
self.phones << p unless self.phones.include?(p)
|
70
|
+
p.number = number
|
71
|
+
p.save
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.is_name_prefix?(text)
|
76
|
+
possibles = %w(mr mrs ms miss dr professor prof)
|
77
|
+
result = possibles.include?(text.gsub(/[.]*/, "").downcase) if text.present?
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.is_name_suffix?(text)
|
81
|
+
possibles = %w(esq phd jr iii ii)
|
82
|
+
result = possibles.include?(text.gsub(/[.]*/, "").downcase) if text.present?
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.name_components(name)
|
86
|
+
res = {}
|
87
|
+
component = ""
|
88
|
+
components = name.gsub(/,/, " ").gsub(/ /, " ").split(" ") rescue [name]
|
89
|
+
num_parts = components.length
|
90
|
+
component = components.shift
|
91
|
+
|
92
|
+
# What kind of thing is this?
|
93
|
+
if is_name_prefix?(component)
|
94
|
+
res[:prefix] = component
|
95
|
+
res[:fname] = components.shift
|
96
|
+
else
|
97
|
+
res[:fname] = component
|
98
|
+
end
|
99
|
+
|
100
|
+
# Next up, middle initial or last name.
|
101
|
+
# If only one word remains, that's the last name
|
102
|
+
if components.length == 1
|
103
|
+
res[:lname] = components.shift
|
104
|
+
elsif components.length > 0
|
105
|
+
# At least 2 words remain. We might have middle names, prefixes, suffixes, etc.
|
106
|
+
components.reverse!
|
107
|
+
component = components.shift
|
108
|
+
if is_name_suffix?(component)
|
109
|
+
res[:suffix] = component
|
110
|
+
res[:lname] = components.shift
|
111
|
+
else
|
112
|
+
res[:lname] = component
|
113
|
+
end
|
114
|
+
res[:minitial] = components.shift
|
115
|
+
end
|
116
|
+
|
117
|
+
res[:minitial] = res[:minitial].gsub(/[.]/, "") if res[:minitial].present?
|
118
|
+
res
|
119
|
+
end
|
120
|
+
|
121
|
+
def name
|
122
|
+
components = []
|
123
|
+
|
124
|
+
if prefix.present?
|
125
|
+
if not prefix.include?(".")
|
126
|
+
components << "#{prefix}."
|
127
|
+
else
|
128
|
+
components << prefix
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
components << self.fname if self.fname.present?
|
133
|
+
|
134
|
+
if minitial.present?
|
135
|
+
if minitial.length < 2
|
136
|
+
components << "#{minitial}."
|
137
|
+
else
|
138
|
+
components << minitial
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
components << self.lname if self.lname.present?
|
143
|
+
components << ", #{suffix}" if self.suffix.present?
|
144
|
+
components.join(" ").strip.gsub(/ ,/, ",")
|
145
|
+
end
|
146
|
+
|
147
|
+
def name=(incoming_name)
|
148
|
+
components = self.class.name_components(incoming_name)
|
149
|
+
self.prefix = components[:prefix]
|
150
|
+
self.fname = components[:fname]
|
151
|
+
self.minitial = components[:minitial]
|
152
|
+
self.lname = components[:lname]
|
153
|
+
self.suffix = components[:suffix]
|
154
|
+
end
|
155
|
+
|
156
|
+
def age
|
157
|
+
((Date.today - self.birthdate).to_i / 365) if self.birthdate.present?
|
158
|
+
end
|
159
|
+
|
160
|
+
def age=(new_age)
|
161
|
+
self.birthdate = (Date.today - (rand(Date.today.month - 1))) - new_age.years
|
162
|
+
self.birthdate
|
163
|
+
end
|
164
|
+
|
165
|
+
def address
|
166
|
+
a = self.addresses.where(label: Label.get("Work")).first
|
167
|
+
a ||= self.addresses.first
|
168
|
+
end
|
169
|
+
|
170
|
+
def address=(new_address)
|
171
|
+
candidate = nil
|
172
|
+
if new_address.is_a?(Address)
|
173
|
+
candidate = new_address
|
174
|
+
elsif %w(Integer Fixnum Bignum).include?(new_address.class.to_s)
|
175
|
+
candidate = Address.find(new_address)
|
176
|
+
else
|
177
|
+
parsed = Address.parse(new_address)
|
178
|
+
candidate = self.addresses.where(line1: parsed.line1).first
|
179
|
+
candidate ||= Address.new
|
180
|
+
parsed.attributes.each {|key, val| candidate.send((key + "=").to_sym, val) unless val == nil }
|
181
|
+
candidate.save
|
182
|
+
end
|
183
|
+
self.addresses << candidate unless self.addresses.include?(candidate)
|
184
|
+
self.save
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.find_or_create_by(hash)
|
188
|
+
options = hash.clone
|
189
|
+
if options.has_key?(:name)
|
190
|
+
options = options.merge(self.name_components(options[:name]))
|
191
|
+
options.delete(:name)
|
192
|
+
end
|
193
|
+
super(options)
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.find_by_nickname(nick)
|
197
|
+
person = self.includes(:nicknames).joins(:nicknames).find_by("nicknames.nickname" => nick)
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.find_by_email(addr)
|
201
|
+
e = Email.where(address: Email.canonicalize(addr), emailable_type: self.name)
|
202
|
+
e.person if e
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.find_by_phone_number(number)
|
206
|
+
p = Phone.where(number: Phone.canonicalize(number), phoneable_type: self.name)
|
207
|
+
p.person if p
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.find_by_address(string)
|
211
|
+
a = Address.find_by_address(string)
|
212
|
+
a.person if a and a.addressable_type == self.name
|
213
|
+
end
|
214
|
+
|
215
|
+
def add_phone(number, label="Home")
|
216
|
+
if self != self.class.find_by_phone_number(number)
|
217
|
+
p = Phone.create(number: number, phoneable_type: self.class.name, label: Label.get(label))
|
218
|
+
phones << p
|
219
|
+
end
|
220
|
+
p = Phone.where(number: Phone.canonicalize(number), phoneable_type: self.class.name)
|
221
|
+
end
|
222
|
+
|
223
|
+
def add_email(address, label="Home")
|
224
|
+
address = Email.canonicalize(address)
|
225
|
+
if self != self.class.find_by_email(address)
|
226
|
+
e = Email.create(address: address, emailable_type: self.class.name, label: Label.get(label))
|
227
|
+
emails << e
|
228
|
+
end
|
229
|
+
e = Email.where(address:address, emailable_type: self.class.name)
|
230
|
+
end
|
231
|
+
|
232
|
+
def add_address(address_line, label="Home")
|
233
|
+
if self != self.class.find_by_address(address_line)
|
234
|
+
a = Address.parse(address_line);
|
235
|
+
a.addressable_type = self.class.name;
|
236
|
+
a.label = Label.get(label)
|
237
|
+
a.save
|
238
|
+
addresses << a
|
239
|
+
end
|
240
|
+
a = Address.find_by_address(address_line)
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
end
|
data/app/models/phone.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Phone < ApplicationRecord
|
2
|
+
belongs_to :label, optional: true
|
3
|
+
belongs_to :phoneable, polymorphic: true
|
4
|
+
before_validation :canonicalize
|
5
|
+
|
6
|
+
def self.canonicalize(digits_and_stuff)
|
7
|
+
canonical = digits_and_stuff
|
8
|
+
canonical.gsub!(" ", "") #remove extra spaces
|
9
|
+
if canonical
|
10
|
+
canonical = canonical[2..100].strip if canonical.starts_with?("+1")
|
11
|
+
if canonical[0] != "+"
|
12
|
+
digits = digits_and_stuff.gsub(/[^0-9]/, "")
|
13
|
+
digits = digits[1..-1] if digits[0] == '1'
|
14
|
+
digits = "805" + digits if digits.length == 7
|
15
|
+
canonical = "(#{digits[0..2]}) #{digits[3..5]}-#{digits[6..10]}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
canonical
|
19
|
+
end
|
20
|
+
|
21
|
+
def canonicalize
|
22
|
+
return if self.number == "76-BAFFLE-76"
|
23
|
+
self.number = self.class.canonicalize(self.number) if self.number
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/app/models/user.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
class User < ApplicationRecord
|
2
|
+
belongs_to :person
|
3
|
+
accepts_nested_attributes_for :person
|
4
|
+
has_many :credentials, as: :credentialed, dependent: :destroy
|
5
|
+
accepts_nested_attributes_for :credentials
|
6
|
+
has_and_belongs_to_many :organizations
|
7
|
+
|
8
|
+
delegate :name, :name=, to: :person
|
9
|
+
delegate :fname, :fname=, to: :person
|
10
|
+
delegate :lname, :lname, to: :person
|
11
|
+
delegate :minitial, :minitial, to: :person
|
12
|
+
delegate :prefix, :prefix, to: :person
|
13
|
+
delegate :suffix, :suffix, to: :person
|
14
|
+
delegate :emails, :emails=, :email, :email=, to: :person
|
15
|
+
delegate :phones, :phones=, :phone, :phone=, to: :person
|
16
|
+
delegate :addresses, :addresses=, :address, :address=, to: :person
|
17
|
+
delegate :birthdate, :birthdate=, to: :person
|
18
|
+
delegate :age, :age=, to: :person
|
19
|
+
|
20
|
+
def self.lookup(item)
|
21
|
+
person = Person.lookup(item)
|
22
|
+
(person.send self.name.downcase.to_sym) if person
|
23
|
+
end
|
24
|
+
|
25
|
+
def organization=(org)
|
26
|
+
self.organizations << org if not self.organizations.include?(org)
|
27
|
+
end
|
28
|
+
|
29
|
+
def organization
|
30
|
+
self.organizations.order(created_at: :desc).first
|
31
|
+
end
|
32
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateAddresses < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :addresses do |t|
|
4
|
+
t.references :label, foreign_key: true
|
5
|
+
t.references :addressable, polymorphic: true
|
6
|
+
t.string :line1
|
7
|
+
t.string :line2
|
8
|
+
t.string :city
|
9
|
+
t.string :state
|
10
|
+
t.string :postal
|
11
|
+
t.string :country
|
12
|
+
t.float :lat
|
13
|
+
t.float :lon
|
14
|
+
t.boolean :confirmed
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreatePhones < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :phones do |t|
|
4
|
+
t.references :label, foreign_key: true
|
5
|
+
t.references :phoneable, polymorphic: true
|
6
|
+
t.string :number
|
7
|
+
t.boolean :confirmed
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateEmails < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :emails do |t|
|
4
|
+
t.references :label, foreign_key: true
|
5
|
+
t.references :emailable, polymorphic: true
|
6
|
+
t.string :address
|
7
|
+
t.boolean :confirmed
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreatePeople < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :people do |t|
|
4
|
+
t.string :prefix, limit: 10
|
5
|
+
t.string :fname
|
6
|
+
t.string :minitial
|
7
|
+
t.string :lname
|
8
|
+
t.string :suffix, limit: 10
|
9
|
+
t.string :birthdate
|
10
|
+
t.string :nationality
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateCredentials < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :credentials do |t|
|
4
|
+
t.references :credentialed, polymorphic: true
|
5
|
+
t.references :email, foreign_key: true
|
6
|
+
t.string :password_digest
|
7
|
+
t.string :provider # Like Google, Facebook, etc.
|
8
|
+
t.string :provider_auth # Whatever their auth token looks like.
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/db/seeds.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
opus = Organization.where(name: "Opus Logica, Inc").first_or_create(nicknames_attributes: [{ nickname: "OPUS" }])
|
3
|
+
bfox = User.lookup("bfox@opuslogica.com") ||
|
4
|
+
User.create(person_attributes: { email: "bfox@opuslogica.com", name: "Brian Jhan Fox",
|
5
|
+
address: "901 Olive St., Santa Barbara, CA, 93101",
|
6
|
+
phone: "805.555.8642" },
|
7
|
+
organization: opus,
|
8
|
+
credentials_attributes: [{ password: "idtmp2tv" }])
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Openopus
|
2
|
+
module Core
|
3
|
+
module People
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace People
|
6
|
+
initializer :append_migrations do |app|
|
7
|
+
unless app.root.to_s.match(root.to_s)
|
8
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
9
|
+
app.config.paths["db/migrate"] << expanded_path
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: openopus-core-people
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian J. Fox
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bcrypt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.13
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.1.13
|
41
|
+
description: A person can have many email addresses, but this is not usually represented
|
42
|
+
in applications. openopus/core/people creates the database structure, relations,
|
43
|
+
and convenience functions for your application so you don't have to. Just connect
|
44
|
+
your end user model, and away you go.
|
45
|
+
email:
|
46
|
+
- bfox@opuslogica.com
|
47
|
+
executables: []
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files: []
|
50
|
+
files:
|
51
|
+
- MIT-LICENSE
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- app/assets/config/openopus_core_people_manifest.js
|
55
|
+
- app/models/address.rb
|
56
|
+
- app/models/credential.rb
|
57
|
+
- app/models/email.rb
|
58
|
+
- app/models/label.rb
|
59
|
+
- app/models/nickname.rb
|
60
|
+
- app/models/organization.rb
|
61
|
+
- app/models/person.rb
|
62
|
+
- app/models/phone.rb
|
63
|
+
- app/models/user.rb
|
64
|
+
- config/routes.rb
|
65
|
+
- db/migrate/00000000000000_create_labels.rb
|
66
|
+
- db/migrate/00000000000001_create_nicknames.rb
|
67
|
+
- db/migrate/00000000000002_create_addresses.rb
|
68
|
+
- db/migrate/00000000000003_create_phones.rb
|
69
|
+
- db/migrate/00000000000004_create_emails.rb
|
70
|
+
- db/migrate/00000000000005_create_people.rb
|
71
|
+
- db/migrate/00000000000006_create_organizations.rb
|
72
|
+
- db/migrate/00000000000007_create_users.rb
|
73
|
+
- db/migrate/00000000000008_create_credentials.rb
|
74
|
+
- db/migrate/00000000000009_create_join_table_organizations_users.rb
|
75
|
+
- db/seeds.rb
|
76
|
+
- lib/openopus/core/people.rb
|
77
|
+
- lib/openopus/core/people/engine.rb
|
78
|
+
- lib/openopus/core/people/version.rb
|
79
|
+
- lib/tasks/openopus/core/people_tasks.rake
|
80
|
+
homepage: https://github.com/opuslogica/openopus-core-people
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
metadata:
|
84
|
+
allowed_push_host: https://rubygems.org
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubygems_version: 3.0.1
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: Model the real world of people in your application, making user interaction
|
104
|
+
robust.
|
105
|
+
test_files: []
|