aenea 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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