aenea 0.2.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +39 -0
  3. data/Rakefile +24 -0
  4. data/app/assets/config/aenea_manifest.js +0 -0
  5. data/app/helpers/inline_boolean_input.rb +9 -0
  6. data/app/helpers/markdown_input.rb +21 -0
  7. data/app/helpers/non_input_input.rb +7 -0
  8. data/app/models/concerns/acts_as_only_flagged.rb +65 -0
  9. data/app/models/concerns/can_be_inactive.rb +8 -0
  10. data/app/models/concerns/has_external_id.rb +9 -0
  11. data/app/models/concerns/has_obfuscated_id.rb +51 -0
  12. data/app/models/concerns/model.rb +51 -0
  13. data/app/models/concerns/model/belongs_to.rb +38 -0
  14. data/app/models/concerns/model/enum.rb +24 -0
  15. data/app/models/concerns/model/has_many.rb +127 -0
  16. data/app/models/concerns/model/has_one.rb +77 -0
  17. data/app/models/concerns/sluggable.rb +7 -0
  18. data/app/models/countries.rb +278 -0
  19. data/app/models/country_validator.rb +10 -0
  20. data/app/models/database_function_helper.rb +88 -0
  21. data/app/models/google_maps_api.rb +19 -0
  22. data/app/models/google_maps_api/geocode.rb +29 -0
  23. data/app/models/google_maps_api/time_zone.rb +29 -0
  24. data/app/models/json_api.rb +16 -0
  25. data/app/models/phone_number_validator.rb +10 -0
  26. data/app/models/us_state_validator.rb +10 -0
  27. data/app/models/us_states.rb +75 -0
  28. data/app/models/zip_code_validator.rb +12 -0
  29. data/lib/aenea.rb +12 -0
  30. data/lib/aenea/active_admin.rb +23 -0
  31. data/lib/aenea/active_admin/index_as_table.rb +9 -0
  32. data/lib/aenea/active_admin/index_as_table/sortable_columns.rb +29 -0
  33. data/lib/aenea/active_admin/resource_controller.rb +9 -0
  34. data/lib/aenea/active_admin/resource_controller/find_sluggable.rb +21 -0
  35. data/lib/aenea/active_admin/resource_dsl.rb +11 -0
  36. data/lib/aenea/active_admin/resource_dsl/sortable_actions.rb +53 -0
  37. data/lib/aenea/active_admin/resource_dsl/use_parent_actions.rb +79 -0
  38. data/lib/aenea/date.rb +14 -0
  39. data/lib/aenea/engine.rb +20 -0
  40. data/lib/aenea/enum.rb +13 -0
  41. data/lib/aenea/enum/changes.rb +61 -0
  42. data/lib/aenea/enum/ext.rb +68 -0
  43. data/lib/aenea/enum/groups.rb +53 -0
  44. data/lib/aenea/markdown_attr.rb +43 -0
  45. data/lib/aenea/time.rb +9 -0
  46. data/lib/aenea/time_of_day.rb +41 -0
  47. data/lib/aenea/time_window.rb +86 -0
  48. data/lib/aenea/version.rb +3 -0
  49. data/lib/tasks/aenea_tasks.rake +4 -0
  50. metadata +203 -0
@@ -0,0 +1,19 @@
1
+ module GoogleMapsApi
2
+ include Geocode
3
+ include TimeZone
4
+
5
+ protected
6
+
7
+ def self.send_request(uri_string)
8
+ json = JsonApi.send_request("#{uri_string}&key=#{GOOGLE_MAPS_API_SERVER_KEY}")
9
+
10
+ return nil unless json
11
+
12
+ if json['status'] != 'OK'
13
+ Rails.logger.error "#{uri_string} failed: #{json}"
14
+ return nil
15
+ end
16
+
17
+ json
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module GoogleMapsApi
2
+ module Geocode
3
+ extend ActiveSupport::Concern
4
+
5
+ BASE_URI = 'https://maps.googleapis.com/maps/api/geocode/json'
6
+
7
+ module ClassMethods
8
+ include ActionView::Helpers::JavaScriptHelper
9
+
10
+ def geocode(address_string)
11
+ raise ArgumentError.new('address_string cannot be blank') if address_string.blank?
12
+
13
+ begin
14
+ return nil unless result = send_request("#{BASE_URI}?address=#{escape_javascript address_string}")
15
+ rescue SocketError => e
16
+ if Rails.env.development? || Rails.env.test?
17
+ Rails.logger.warn 'not connected to the Internet... faking geocode API call'
18
+ return 37.792855, -122.403166
19
+ end
20
+
21
+ raise e
22
+ end
23
+
24
+ location_result = result['results'][0]['geometry']['location']
25
+ [ location_result['lat'], location_result['lng'] ]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module GoogleMapsApi
2
+ module TimeZone
3
+ extend ActiveSupport::Concern
4
+
5
+ BASE_URI = 'https://maps.googleapis.com/maps/api/timezone/json'
6
+
7
+ module ClassMethods
8
+ def time_zone_id(latitude, longitude, timestamp = nil)
9
+ raise ArgumentError.new("missing latitude and/or longitude: #{latitude}, #{longitude}") if latitude.blank? || longitude.blank?
10
+
11
+ timestamp ||= Time.now.to_i
12
+ location = "#{latitude},#{longitude}"
13
+
14
+ begin
15
+ return nil unless result = send_request("#{BASE_URI}?location=#{location}&timestamp=#{timestamp}")
16
+ rescue SocketError => e
17
+ if Rails.env.development? || Rails.env.test?
18
+ Rails.logger.warn 'not connected to the Internet... faking time zone API call'
19
+ return 'America/Los_Angeles'
20
+ end
21
+
22
+ raise e
23
+ end
24
+
25
+ result['timeZoneId']
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ module JsonApi
2
+ def self.send_request(uri_string, options = {})
3
+ uri = URI.parse(URI.escape(uri_string))
4
+ Rails.logger.debug uri
5
+
6
+ # handle Ruby 2 OpenSSL cert issue: https://gist.github.com/mislav/5026283
7
+ options[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE if Rails.env.development? || Rails.env.test?
8
+
9
+ response_body = open(uri, options).read
10
+ Rails.logger.info "#{uri} => #{response_body}"
11
+
12
+ json = JSON.parse(response_body)
13
+
14
+ json
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ class PhoneNumberValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ if value.blank?
4
+ record.errors[attribute] << 'is required' unless options[:allow_blank] || options[:allow_nil]
5
+ return
6
+ end
7
+
8
+ record.errors[attribute] << (options[:message] || 'is not a valid phone number') if value.gsub(/[^\d]/, '').length < 10
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class UsStateValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ if value.blank?
4
+ record.errors[attribute] << 'is required' unless options[:allow_blank] || options[:allow_nil]
5
+ return
6
+ end
7
+
8
+ record.errors[attribute] << (options[:message] || 'is not a valid US state') unless UsStates.valid?(value)
9
+ end
10
+ end
@@ -0,0 +1,75 @@
1
+ module UsStates
2
+ SELECT_VALUES = [
3
+ ['Alabama', 'AL'],
4
+ ['Alaska', 'AK'],
5
+ ['American Samoa', 'AS'],
6
+ ['Arizona', 'AZ'],
7
+ ['Arkansas', 'AR'],
8
+ ['California', 'CA'],
9
+ ['Colorado', 'CO'],
10
+ ['Connecticut', 'CT'],
11
+ ['Delaware', 'DE'],
12
+ ['District of Columbia', 'DC'],
13
+ ['Florida', 'FL'],
14
+ ['Georgia', 'GA'],
15
+ ['Guam', 'GU'],
16
+ ['Hawaii', 'HI'],
17
+ ['Idaho', 'ID'],
18
+ ['Illinois', 'IL'],
19
+ ['Indiana', 'IN'],
20
+ ['Iowa', 'IA'],
21
+ ['Kansas', 'KS'],
22
+ ['Kentucky', 'KY'],
23
+ ['Louisiana', 'LA'],
24
+ ['Maine', 'ME'],
25
+ ['Maryland', 'MD'],
26
+ ['Massachusetts', 'MA'],
27
+ ['Michigan', 'MI'],
28
+ ['Minnesota', 'MN'],
29
+ ['Mississippi', 'MS'],
30
+ ['Missouri', 'MO'],
31
+ ['Montana', 'MT'],
32
+ ['Nebraska', 'NE'],
33
+ ['Nevada', 'NV'],
34
+ ['New Hampshire', 'NH'],
35
+ ['New Jersey', 'NJ'],
36
+ ['New Mexico', 'NM'],
37
+ ['New York', 'NY'],
38
+ ['North Carolina', 'NC'],
39
+ ['North Dakota', 'ND'],
40
+ ['Ohio', 'OH'],
41
+ ['Oklahoma', 'OK'],
42
+ ['Oregon', 'OR'],
43
+ ['Pennsylvania', 'PA'],
44
+ ['Puerto Rico', 'PR'],
45
+ ['Rhode Island', 'RI'],
46
+ ['South Carolina', 'SC'],
47
+ ['South Dakota', 'SD'],
48
+ ['Tennessee', 'TN'],
49
+ ['Texas', 'TX'],
50
+ ['Utah', 'UT'],
51
+ ['Vermont', 'VT'],
52
+ ['Virgin Islands', 'VI'],
53
+ ['Virginia', 'VA'],
54
+ ['Washington', 'WA'],
55
+ ['West Virginia', 'WV'],
56
+ ['Wisconsin', 'WI'],
57
+ ['Wyoming', 'WY'],
58
+ ]
59
+
60
+ def self.select_values
61
+ SELECT_VALUES
62
+ end
63
+
64
+ def self.valid?(code)
65
+ (@codes ||= select_values.map(&:last)).include?(code)
66
+ end
67
+
68
+ def self.[](code)
69
+ (@names_by_code ||= Hash[select_values.map(&:reverse)])[code]
70
+ end
71
+
72
+ def self.community_property?(code)
73
+ %w(AZ CA ID LA NV NM PR TX WA WI).include?(code)
74
+ end
75
+ end
@@ -0,0 +1,12 @@
1
+ class ZipCodeValidator < ActiveModel::EachValidator
2
+ FORMAT = /\A\d{5}\z/
3
+
4
+ def validate_each(record, attribute, value)
5
+ if value.blank?
6
+ record.errors[attribute] << 'is required' unless options[:allow_blank] || options[:allow_nil]
7
+ return
8
+ end
9
+
10
+ record.errors[attribute] << (options[:message] || 'must be a 5-digit ZIP code') unless value =~ FORMAT
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require 'aenea/active_admin'
2
+ require 'aenea/date'
3
+ require 'aenea/engine'
4
+ require 'aenea/enum'
5
+ require 'aenea/markdown_attr'
6
+ require 'aenea/time'
7
+ require 'aenea/time_of_day'
8
+ require 'aenea/time_window'
9
+ require 'aenea/version'
10
+
11
+ module Aenea
12
+ end
@@ -0,0 +1,23 @@
1
+ module Aenea
2
+ module ActiveAdmin
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :IndexAsTable
6
+ autoload :ResourceController
7
+ autoload :ResourceDsl
8
+
9
+ cattr_accessor :installed
10
+
11
+ def self.install
12
+ return if installed
13
+
14
+ if defined? ::ActiveAdmin
15
+ ::ActiveAdmin::Views::IndexAsTable.include Aenea::ActiveAdmin::IndexAsTable
16
+ ::ActiveAdmin::ResourceController.include Aenea::ActiveAdmin::ResourceController
17
+ ::ActiveAdmin::ResourceDSL.include Aenea::ActiveAdmin::ResourceDsl
18
+
19
+ self.installed = true
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ require 'aenea/active_admin/index_as_table/sortable_columns'
2
+
3
+ module Aenea
4
+ module ActiveAdmin
5
+ module IndexAsTable
6
+ include SortableColumns
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ module Aenea
2
+ module ActiveAdmin
3
+ module IndexAsTable
4
+ module SortableColumns
5
+ def sortable_columns(options = {})
6
+ unless options[:up_down_only]
7
+ column ''.html_safe do |resource|
8
+ link_to('&#9650;&#9650;'.html_safe, self.send(:"move_to_top_admin_#{resource.class.name.underscore.gsub("/", "_")}_path", resource), :class => 'arrow', title: 'Move to Top') unless resource.first?
9
+ end
10
+ end
11
+
12
+ column ''.html_safe do |resource|
13
+ link_to('&#9650;'.html_safe, self.send(:"move_up_admin_#{resource.class.name.underscore.gsub("/", "_")}_path", resource), :class => 'arrow', title: 'Move Up') unless resource.first?
14
+ end
15
+
16
+ column ''.html_safe do |resource|
17
+ link_to('&#9660;'.html_safe, self.send(:"move_down_admin_#{resource.class.name.underscore.gsub("/", "_")}_path", resource), :class => 'arrow', title: 'Move Down') unless resource.last?
18
+ end
19
+
20
+ unless options[:up_down_only]
21
+ column ''.html_safe do |resource|
22
+ link_to('&#9660;&#9660;'.html_safe, self.send(:"move_to_bottom_admin_#{resource.class.name.underscore.gsub("/", "_")}_path", resource), :class => 'arrow', title: 'Move to Bottom') unless resource.last?
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ require 'aenea/active_admin/resource_controller/find_sluggable'
2
+
3
+ module Aenea
4
+ module ActiveAdmin
5
+ module ResourceController
6
+ include FindSluggable
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Aenea
2
+ module ActiveAdmin
3
+ module ResourceController
4
+ module FindSluggable
5
+ def find_resource
6
+ if resource_class < Sluggable
7
+ if params[:id] =~ /\A\d+\z/
8
+ return scoped_collection.find params[:id]
9
+ else
10
+ return scoped_collection.find_by_url! params[:id]
11
+ end
12
+ end
13
+
14
+ return scoped_collection if scoped_collection.class < ActiveRecord::Base # has_one
15
+
16
+ scoped_collection.find(params[:id])
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ require 'aenea/active_admin/resource_dsl/sortable_actions'
2
+ require 'aenea/active_admin/resource_dsl/use_parent_actions'
3
+
4
+ module Aenea
5
+ module ActiveAdmin
6
+ module ResourceDsl
7
+ include SortableActions
8
+ include UseParentActions
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,53 @@
1
+ module Aenea
2
+ module ActiveAdmin
3
+ module ResourceDsl
4
+ module SortableActions
5
+ def sortable_member_actions(fallback_route)
6
+ member_action :move_to_top do
7
+ if resource.first?
8
+ flash[:warning] = 'Already at top'
9
+ else
10
+ resource.move_to_top
11
+ flash[:notice] = 'Moved to top'
12
+ end
13
+
14
+ redirect_back fallback_location: Rails.application.routes.url_helpers.send(fallback_route)
15
+ end
16
+
17
+ member_action :move_to_bottom do
18
+ if resource.last?
19
+ flash[:warning] = 'Already at bottom'
20
+ else
21
+ resource.move_to_bottom
22
+ flash[:notice] = 'Moved to bottom'
23
+ end
24
+
25
+ redirect_back fallback_location: Rails.application.routes.url_helpers.send(fallback_route)
26
+ end
27
+
28
+ member_action :move_up do
29
+ if resource.first?
30
+ flash[:warning] = 'Already at top'
31
+ else
32
+ resource.move_higher
33
+ flash[:notice] = 'Moved up'
34
+ end
35
+
36
+ redirect_back fallback_location: Rails.application.routes.url_helpers.send(fallback_route)
37
+ end
38
+
39
+ member_action :move_down do
40
+ if resource.last?
41
+ flash[:warning] = 'Already at bottom'
42
+ else
43
+ resource.move_lower
44
+ flash[:notice] = 'Moved down'
45
+ end
46
+
47
+ redirect_back fallback_location: Rails.application.routes.url_helpers.send(fallback_route)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,79 @@
1
+ module Aenea
2
+ module ActiveAdmin
3
+ module ResourceDsl
4
+ module UseParentActions
5
+ def use_parent_index(options = {})
6
+ grandparent_type = options[:grandparent]
7
+
8
+ controller do
9
+ define_method :index do # don't use def so we can access 'options'
10
+ parent_id = params[:"#{parent_type}_id"]
11
+ url = if grandparent_type
12
+ parent = parent_type.to_s.camelize.constantize.find(parent_id)
13
+ grandparent = parent.send grandparent_type
14
+ route = :"admin_#{grandparent_type}_#{parent_type}_path"
15
+ send route, grandparent, parent
16
+ else
17
+ parent_path parent_id
18
+ end
19
+
20
+ redirect_to url
21
+ end
22
+ end
23
+ end
24
+
25
+ def use_parent_action(action, options = {})
26
+ raise 'invalid action' unless %i(create update destroy).include?(action)
27
+
28
+ grandparent_type = options[:grandparent]
29
+ verb = action == :destroy ? 'destroyed' : "#{action}d"
30
+
31
+ controller do
32
+ define_method action do
33
+ super() do |success, failure|
34
+ parent_name = parent_type.to_s rescue
35
+ (options[:parent] or raise 'must specify parent for controller without belongs_to')
36
+
37
+ success.html do
38
+ flash[:notice] = "Succesfully #{verb} #{parent_name.titleize}"
39
+ end
40
+
41
+ failure.html do
42
+ flash[:error] = "Could not #{action} #{parent_name.titleize}: #{resource.errors.full_messages.join(', ')}"
43
+ end
44
+
45
+ parent_id = params[:"#{parent_type}_id"]
46
+ url = if grandparent_type
47
+ parent = parent_type.to_s.camelize.constantize.find(parent_id)
48
+ grandparent = parent.send grandparent_type
49
+ route = :"admin_#{grandparent_type}_#{parent_type}_path"
50
+ send route, grandparent, parent
51
+ else
52
+ parent_path parent_id
53
+ end
54
+
55
+ if options[:redirect_back]
56
+ redirect_back fallback_location: url
57
+ else
58
+ redirect_to url
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def use_parent_create(options = {})
66
+ use_parent_action :create, options
67
+ end
68
+
69
+ def use_parent_update(options = {})
70
+ use_parent_action :update, options
71
+ end
72
+
73
+ def use_parent_destroy(options = {})
74
+ use_parent_action :destroy, options
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end