open_porch 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +41 -0
- data/Gemfile.lock +130 -0
- data/README.md +170 -0
- data/Rakefile +19 -0
- data/VERSION +1 -0
- data/app/controllers/admin/areas/base_controller.rb +11 -0
- data/app/controllers/admin/areas/issues_controller.rb +67 -0
- data/app/controllers/admin/areas/memberships_controller.rb +7 -0
- data/app/controllers/admin/areas/posts_controller.rb +44 -0
- data/app/controllers/admin/areas_controller.rb +89 -0
- data/app/controllers/admin/base_controller.rb +16 -0
- data/app/controllers/admin/user_activity_controller.rb +29 -0
- data/app/controllers/admin/users_controller.rb +57 -0
- data/app/controllers/application_controller.rb +15 -0
- data/app/controllers/areas/base_controller.rb +25 -0
- data/app/controllers/areas/issues_controller.rb +35 -0
- data/app/controllers/areas/posts_controller.rb +28 -0
- data/app/controllers/areas_controller.rb +30 -0
- data/app/controllers/passwords_controller.rb +42 -0
- data/app/controllers/registrations_controller.rb +42 -0
- data/app/controllers/sessions_controller.rb +33 -0
- data/app/controllers/users_controller.rb +77 -0
- data/app/helpers/open_porch_helper.rb +24 -0
- data/app/mailers/user_mailer.rb +35 -0
- data/app/models/address.rb +57 -0
- data/app/models/area.rb +171 -0
- data/app/models/area_activity.rb +24 -0
- data/app/models/email_message.rb +104 -0
- data/app/models/issue.rb +66 -0
- data/app/models/issue_number.rb +22 -0
- data/app/models/membership.rb +27 -0
- data/app/models/post.rb +64 -0
- data/app/models/session_user.rb +69 -0
- data/app/models/user.rb +140 -0
- data/app/models/user_activity.rb +31 -0
- data/app/models/user_authority_check.rb +14 -0
- data/app/views/admin/areas/_form.html.haml +8 -0
- data/app/views/admin/areas/_nav.html.haml +12 -0
- data/app/views/admin/areas/edit.html.haml +22 -0
- data/app/views/admin/areas/edit_borders.html.haml +44 -0
- data/app/views/admin/areas/index.html.haml +61 -0
- data/app/views/admin/areas/issues/_post.html.haml +15 -0
- data/app/views/admin/areas/issues/add_posts.js.rjs +3 -0
- data/app/views/admin/areas/issues/edit.html.haml +37 -0
- data/app/views/admin/areas/issues/index.html.haml +31 -0
- data/app/views/admin/areas/issues/remove_posts.js.rjs +3 -0
- data/app/views/admin/areas/issues/show.html.haml +13 -0
- data/app/views/admin/areas/memberships/index.html.haml +17 -0
- data/app/views/admin/areas/new.html.haml +6 -0
- data/app/views/admin/areas/new.js.haml +4 -0
- data/app/views/admin/areas/posts/_edit.html.haml +6 -0
- data/app/views/admin/areas/posts/_post_status.html.haml +1 -0
- data/app/views/admin/areas/posts/destroy.js.rjs +1 -0
- data/app/views/admin/areas/posts/edit.js.rjs +1 -0
- data/app/views/admin/areas/posts/show.js.rjs +1 -0
- data/app/views/admin/areas/posts/toggle_reviewed_by.js.rjs +1 -0
- data/app/views/admin/areas/posts/update.js.rjs +5 -0
- data/app/views/admin/areas/show.html.haml +5 -0
- data/app/views/admin/user_activity/show.html.haml +5 -0
- data/app/views/admin/users/_form.html.haml +21 -0
- data/app/views/admin/users/edit.html.haml +5 -0
- data/app/views/admin/users/index.html.haml +31 -0
- data/app/views/admin/users/new.html.haml +5 -0
- data/app/views/areas/issues/index.html.haml +31 -0
- data/app/views/areas/issues/show.html.haml +22 -0
- data/app/views/areas/posts/_post.html.haml +8 -0
- data/app/views/areas/posts/_posts_search_form.html.haml +8 -0
- data/app/views/areas/posts/index.html.haml +27 -0
- data/app/views/areas/posts/new.html.haml +10 -0
- data/app/views/areas/show.html.haml +47 -0
- data/app/views/layouts/_account_nav.html.haml +18 -0
- data/app/views/layouts/_flash_message.html.haml +4 -0
- data/app/views/layouts/_footer.html.haml +9 -0
- data/app/views/layouts/_head.html.haml +16 -0
- data/app/views/layouts/admin/_nav.html.haml +13 -0
- data/app/views/layouts/admin.html.haml +15 -0
- data/app/views/layouts/application.html.haml +14 -0
- data/app/views/layouts/area_editor.html.haml +11 -0
- data/app/views/passwords/edit.html.haml +9 -0
- data/app/views/passwords/new.html.haml +13 -0
- data/app/views/registrations/_address_form.html.haml +7 -0
- data/app/views/registrations/create.html.haml +49 -0
- data/app/views/registrations/index.html.haml +30 -0
- data/app/views/registrations/new.html.haml +17 -0
- data/app/views/sessions/new.html.haml +18 -0
- data/app/views/stylesheets/common.sass +239 -0
- data/app/views/stylesheets/content.sass +193 -0
- data/app/views/stylesheets/reset.sass +46 -0
- data/app/views/stylesheets/structure.sass +11 -0
- data/app/views/stylesheets/typography.sass +57 -0
- data/app/views/user_mailer/email_verification.html.erb +5 -0
- data/app/views/user_mailer/email_verification.text.erb +7 -0
- data/app/views/user_mailer/new_issue.html.erb +32 -0
- data/app/views/user_mailer/new_issue.text.erb +26 -0
- data/app/views/user_mailer/password_reset.html.erb +7 -0
- data/app/views/user_mailer/password_reset.text.erb +6 -0
- data/app/views/users/_form.html.haml +5 -0
- data/app/views/users/edit.html.haml +11 -0
- data/app/views/users/new.html.haml +41 -0
- data/app/views/users/show.html.haml +30 -0
- data/bin/open_porch_engine +135 -0
- data/config/application.rb +43 -0
- data/config/boot.rb +13 -0
- data/config/database_example.yml +59 -0
- data/config/environment.rb +5 -0
- data/config/environments/development.rb +26 -0
- data/config/environments/production.rb +49 -0
- data/config/environments/test.rb +35 -0
- data/config/initializers/backtrace_silencers.rb +7 -0
- data/config/initializers/email_regex.rb +38 -0
- data/config/initializers/geokit_config.rb +61 -0
- data/config/initializers/inflections.rb +20 -0
- data/config/initializers/meta_search.rb +7 -0
- data/config/initializers/mime_types.rb +5 -0
- data/config/initializers/open_porch.rb +41 -0
- data/config/initializers/sass.rb +1 -0
- data/config/initializers/secret_token.rb +7 -0
- data/config/initializers/session_store.rb +8 -0
- data/config/initializers/states_provinces.rb +2 -0
- data/config/initializers/will_paginate.rb +2 -0
- data/config/locales/en.yml +5 -0
- data/config/open_porch_example.yml +23 -0
- data/config/routes.rb +54 -0
- data/config/schedule.rb +9 -0
- data/config.ru +4 -0
- data/db/migrate/01_create_areas.rb +21 -0
- data/db/migrate/02_create_users.rb +28 -0
- data/db/migrate/03_create_memberships.rb +14 -0
- data/db/migrate/04_create_posts.rb +20 -0
- data/db/migrate/05_create_issue_numbers.rb +13 -0
- data/db/migrate/06_create_issues.rb +21 -0
- data/db/migrate/20110204173301_add_published_to_areas.rb +10 -0
- data/db/migrate/20110204194840_create_user_activities.rb +13 -0
- data/db/migrate/20110208163604_add_zip_to_areas.rb +11 -0
- data/db/migrate/20110209175723_add_counters_to_areas.rb +11 -0
- data/db/migrate/20110209182244_remove_subject_from_issues.rb +9 -0
- data/db/migrate/20110209190146_add_reviewer_info_to_posts.rb +9 -0
- data/db/migrate/20110215173144_add_email_validation_key_to_users.rb +13 -0
- data/db/migrate/20110215182716_remove_published_from_areas.rb +10 -0
- data/db/migrate/20110215211012_create_area_activities.rb +19 -0
- data/db/migrate/20110215213802_create_email_messages.rb +19 -0
- data/db/migrate/20110217165018_change_send_mode_to_string.rb +17 -0
- data/db/migrate/20110223160609_denormalize_user_info_in_posts.rb +19 -0
- data/db/seeds.rb +7 -0
- data/doc/README_FOR_APP +2 -0
- data/lib/generators/open_porch_generator.rb +37 -0
- data/lib/open_porch/engine.rb +20 -0
- data/lib/open_porch.rb +3 -0
- data/lib/tasks/.gitkeep +0 -0
- data/lib/tasks/open_porch.rake +10 -0
- data/lib/tasks/postageapp_tasks.rake +78 -0
- data/open_porch.gemspec +335 -0
- data/public/404.html +26 -0
- data/public/422.html +26 -0
- data/public/500.html +26 -0
- data/public/favicon.ico +0 -0
- data/public/images/icons/ajax-loader.gif +0 -0
- data/public/images/icons/calendar.png +0 -0
- data/public/images/icons/post_new.png +0 -0
- data/public/images/icons/post_reviewed.png +0 -0
- data/public/javascripts/application.js +3 -0
- data/public/javascripts/google_maps.js +153 -0
- data/public/javascripts/highcharts.js +162 -0
- data/public/javascripts/highcharts_init.js +30 -0
- data/public/javascripts/issue_edit.js +57 -0
- data/public/javascripts/jquery-ui.min.js +191 -0
- data/public/javascripts/jquery.js +154 -0
- data/public/javascripts/rails.js +137 -0
- data/public/javascripts/region_editor.js +616 -0
- data/public/javascripts/user_activity.js +29 -0
- data/public/robots.txt +5 -0
- data/public/stylesheets/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/public/stylesheets/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/public/stylesheets/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/public/stylesheets/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/public/stylesheets/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/public/stylesheets/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/public/stylesheets/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/public/stylesheets/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/public/stylesheets/images/ui-icons_222222_256x240.png +0 -0
- data/public/stylesheets/images/ui-icons_2e83ff_256x240.png +0 -0
- data/public/stylesheets/images/ui-icons_454545_256x240.png +0 -0
- data/public/stylesheets/images/ui-icons_888888_256x240.png +0 -0
- data/public/stylesheets/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/public/stylesheets/jquery-ui.css +362 -0
- data/script/rails +6 -0
- data/test/dummy/area.rb +5 -0
- data/test/dummy/area_activity.rb +7 -0
- data/test/dummy/issue.rb +4 -0
- data/test/dummy/membership.rb +4 -0
- data/test/dummy/post.rb +12 -0
- data/test/dummy/user.rb +29 -0
- data/test/dummy/user_activity.rb +11 -0
- data/test/functional/admin/areas/issues_controller_test.rb +48 -0
- data/test/functional/admin/areas/memberships_controller_test.rb +45 -0
- data/test/functional/admin/areas/posts_controller_test.rb +54 -0
- data/test/functional/admin/areas_controller_test.rb +134 -0
- data/test/functional/admin/base_controller_test.rb +8 -0
- data/test/functional/admin/user_activity_controller_test.rb +28 -0
- data/test/functional/admin/users_controller_test.rb +144 -0
- data/test/functional/areas/issues_controller_test.rb +33 -0
- data/test/functional/areas/posts_controller_test.rb +50 -0
- data/test/functional/areas_controller_test.rb +12 -0
- data/test/functional/passwords_controller_test.rb +64 -0
- data/test/functional/registrations_controller_test.rb +64 -0
- data/test/functional/sessions_controller_test.rb +120 -0
- data/test/functional/users_controller_test.rb +144 -0
- data/test/performance/browsing_test.rb +9 -0
- data/test/test_helper.rb +92 -0
- data/test/unit/address_test.rb +25 -0
- data/test/unit/area_activity_test.rb +57 -0
- data/test/unit/area_test.rb +92 -0
- data/test/unit/email_message_test.rb +8 -0
- data/test/unit/issue_number_test.rb +25 -0
- data/test/unit/issue_test.rb +154 -0
- data/test/unit/membership_test.rb +20 -0
- data/test/unit/post_test.rb +69 -0
- data/test/unit/session_user_test.rb +65 -0
- data/test/unit/user_activity_test.rb +31 -0
- data/test/unit/user_mailer_test.rb +20 -0
- data/test/unit/user_test.rb +61 -0
- data/vendor/plugins/.gitkeep +0 -0
- metadata +456 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
class EmailMessage < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Relationships ========================================================
|
4
|
+
|
5
|
+
has_many :posts
|
6
|
+
|
7
|
+
# == Class Methods ========================================================
|
8
|
+
|
9
|
+
def self.create_from_pop3
|
10
|
+
last_message_number = EmailMessage.maximum(:number).to_i
|
11
|
+
msg_count = 0
|
12
|
+
|
13
|
+
puts "Connecting to #{OPEN_PORCH_POP3['host']} ..."
|
14
|
+
Net::POP.enable_ssl(OpenSSL::SSL::VERIFY_NONE) if OPEN_PORCH_POP3['enable_ssl']
|
15
|
+
Net::POP3.start(OPEN_PORCH_POP3['host'], OPEN_PORCH_POP3['port'], OPEN_PORCH_POP3['username'], OPEN_PORCH_POP3['password']) do |pop|
|
16
|
+
puts "Found #{pop.n_mails} messages on the server"
|
17
|
+
puts "Last saved message: #{last_message_number}"
|
18
|
+
unless pop.mails.empty?
|
19
|
+
pop.mails.each do |m|
|
20
|
+
next if (m.number <= last_message_number)
|
21
|
+
print "\tSaving message ##{m.number} (#{"%.1f" % (m.length/1024.to_f)} KB) ... "
|
22
|
+
|
23
|
+
begin
|
24
|
+
email_message = EmailMessage.create(
|
25
|
+
:number => m.number,
|
26
|
+
:content => m.pop,
|
27
|
+
:size => m.length,
|
28
|
+
:parsed => false
|
29
|
+
)
|
30
|
+
print 'ok'
|
31
|
+
if email_message.parse
|
32
|
+
msg_count += 1
|
33
|
+
end
|
34
|
+
rescue ActiveRecord::StatementInvalid => e
|
35
|
+
puts
|
36
|
+
puts "========> ERROR saving message ##{m.number}"
|
37
|
+
puts e.message
|
38
|
+
end
|
39
|
+
|
40
|
+
break if msg_count >= 10
|
41
|
+
end
|
42
|
+
puts "\tCopied #{msg_count} new messages from the server."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
puts "Done!"
|
46
|
+
rescue SocketError => e
|
47
|
+
puts "ERROR: Could not connect to #{OPEN_PORCH_POP3['host']} - #{e.message}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# == Instance Methods ========================================================
|
51
|
+
|
52
|
+
def parse
|
53
|
+
return true if self.parsed?
|
54
|
+
|
55
|
+
print "\tParsing message:"
|
56
|
+
mail = Mail.new(self.content)
|
57
|
+
|
58
|
+
# Parse the body of the message
|
59
|
+
if mail.multipart?
|
60
|
+
if mail.text_part
|
61
|
+
body = mail.text_part.body
|
62
|
+
else
|
63
|
+
body = mail.parts[0].body
|
64
|
+
end
|
65
|
+
else
|
66
|
+
body = mail.body
|
67
|
+
end
|
68
|
+
|
69
|
+
# Find the user
|
70
|
+
user = User.where(:email => mail.from).first
|
71
|
+
if user.blank?
|
72
|
+
print "\tUser: Could not find user with email #{mail.from}"
|
73
|
+
puts
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
print "\tUser: #{user.id}"
|
77
|
+
|
78
|
+
# Find the area
|
79
|
+
if mail.to.nil?
|
80
|
+
print "\tArea: recipient is empty. Skipping."
|
81
|
+
puts
|
82
|
+
end
|
83
|
+
slug = Mail::Address.new(mail.to.first).local
|
84
|
+
area = Area.where(:slug => slug).first
|
85
|
+
if area.blank?
|
86
|
+
print "\tArea: Could not find area with slug #{slug}"
|
87
|
+
puts
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
print "\tArea: #{area.name}"
|
91
|
+
|
92
|
+
print "\tCreating post ... "
|
93
|
+
area.posts.create(
|
94
|
+
:user_id => user.id,
|
95
|
+
:email_message_id => self.id,
|
96
|
+
:title => mail.subject,
|
97
|
+
:content => Iconv.conv("UTF-8//TRANSLIT//IGNORE", 'UTF-8', body.to_s + ' ')[0..-2]
|
98
|
+
)
|
99
|
+
|
100
|
+
self.update_attribute(:parsed, true)
|
101
|
+
puts 'ok'
|
102
|
+
return true
|
103
|
+
end
|
104
|
+
end
|
data/app/models/issue.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
class Issue < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Validations ==========================================================
|
4
|
+
|
5
|
+
validates :area_id,
|
6
|
+
:presence => true
|
7
|
+
|
8
|
+
validates :number,
|
9
|
+
:uniqueness => { :scope => :area_id }
|
10
|
+
|
11
|
+
validate :scheduled_time_is_ahead
|
12
|
+
|
13
|
+
# == Relationships ========================================================
|
14
|
+
|
15
|
+
belongs_to :area,
|
16
|
+
:counter_cache => true
|
17
|
+
|
18
|
+
has_many :posts,
|
19
|
+
:dependent => :destroy
|
20
|
+
|
21
|
+
# == Scopes ===============================================================
|
22
|
+
|
23
|
+
scope :sent, where('sent_at IS NOT NULL')
|
24
|
+
|
25
|
+
scope :scheduled_before, lambda { |t|
|
26
|
+
where(['sent_at IS NULL AND scheduled_at <= ?', t.utc])
|
27
|
+
}
|
28
|
+
|
29
|
+
scope :in_month, lambda { |m|
|
30
|
+
where(["to_char(sent_at, 'YYYY-MM') = ?", m])
|
31
|
+
}
|
32
|
+
|
33
|
+
# == Callbacks ============================================================
|
34
|
+
|
35
|
+
before_create :set_issue_number
|
36
|
+
|
37
|
+
# == Instance Methods =====================================================
|
38
|
+
|
39
|
+
def send!
|
40
|
+
UserMailer.new_issue(self).deliver
|
41
|
+
|
42
|
+
self.update_attribute(:sent_at, Time.now.utc)
|
43
|
+
|
44
|
+
# Create a new issue for the area if there are any new posts left
|
45
|
+
if self.area.send_mode?(:batched) && self.area.posts.in_issue(nil).count > 0
|
46
|
+
self.area.issues.create
|
47
|
+
end
|
48
|
+
|
49
|
+
self.area.record_activity_for!(:issues_published)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_params
|
53
|
+
self.number
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
def scheduled_time_is_ahead
|
58
|
+
if self.scheduled_at.present? && (self.scheduled_at < Time.now.utc)
|
59
|
+
errors.add(:scheduled_at, "can't be in the past.")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_issue_number
|
64
|
+
self.number ||= self.area.issue_number.next
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class IssueNumber < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Relationships ========================================================
|
4
|
+
|
5
|
+
belongs_to :area
|
6
|
+
|
7
|
+
# == Instance Methods =====================================================
|
8
|
+
|
9
|
+
def next
|
10
|
+
self.sequence_number = self.connection.select_value("
|
11
|
+
UPDATE #{self.class.table_name}
|
12
|
+
SET sequence_number=sequence_number+1
|
13
|
+
WHERE area_id= #{self.area_id}
|
14
|
+
RETURNING sequence_number"
|
15
|
+
).to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def current
|
19
|
+
self.sequence_number
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Membership < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Validations ==========================================================
|
4
|
+
|
5
|
+
validates :user_id,
|
6
|
+
:uniqueness => { :scope => :area_id }
|
7
|
+
|
8
|
+
# == Relationships ========================================================
|
9
|
+
|
10
|
+
belongs_to :user
|
11
|
+
belongs_to :area,
|
12
|
+
:counter_cache => true
|
13
|
+
|
14
|
+
# == Callbacks ============================================================
|
15
|
+
|
16
|
+
after_create :record_activity_for_new_users
|
17
|
+
after_destroy :record_activity_for_quitters
|
18
|
+
|
19
|
+
protected
|
20
|
+
def record_activity_for_new_users
|
21
|
+
self.area.record_activity_for!(:new_users)
|
22
|
+
end
|
23
|
+
|
24
|
+
def record_activity_for_quitters
|
25
|
+
self.area.record_activity_for!(:quitters)
|
26
|
+
end
|
27
|
+
end
|
data/app/models/post.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
class Post < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Validations ==========================================================
|
4
|
+
|
5
|
+
validates :title, :content, :area_id, :user_id,
|
6
|
+
:presence => true
|
7
|
+
validates :user_first_name, :user_last_name, :user_email, :user_address,
|
8
|
+
:user_city, :user_state,
|
9
|
+
:presence => true
|
10
|
+
|
11
|
+
# == Relationships ========================================================
|
12
|
+
|
13
|
+
belongs_to :area
|
14
|
+
belongs_to :user
|
15
|
+
belongs_to :issue
|
16
|
+
belongs_to :email_message
|
17
|
+
|
18
|
+
# == Callbacks ============================================================
|
19
|
+
|
20
|
+
before_validation :copy_user_info
|
21
|
+
after_create :create_issue, :record_activity_for_new_post
|
22
|
+
|
23
|
+
# == Scope ================================================================
|
24
|
+
|
25
|
+
scope :in_issue, lambda { |issue| where(:issue_id => issue) }
|
26
|
+
|
27
|
+
# == Instance Methods =====================================================
|
28
|
+
|
29
|
+
def reviewed?
|
30
|
+
self.reviewed_by.present?
|
31
|
+
end
|
32
|
+
|
33
|
+
def user_full_name
|
34
|
+
[user_first_name, user_last_name].join(' ')
|
35
|
+
end
|
36
|
+
|
37
|
+
def send_immediatelly!
|
38
|
+
self.issue = self.area.issues.create!
|
39
|
+
self.save
|
40
|
+
self.issue.reload
|
41
|
+
self.issue.send!
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def create_issue
|
46
|
+
if self.area.send_mode?(:immediate)
|
47
|
+
self.send_immediatelly!
|
48
|
+
elsif self.area.send_mode?(:batched) && self.area.current_issue.blank?
|
49
|
+
self.area.issues.create
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def record_activity_for_new_post
|
54
|
+
self.area.record_activity_for!(:new_posts)
|
55
|
+
end
|
56
|
+
|
57
|
+
def copy_user_info
|
58
|
+
if self.user.present?
|
59
|
+
%w(first_name last_name email address city state).each do |field|
|
60
|
+
self.send("user_#{field}=", self.user.send(field))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class SessionUser
|
2
|
+
include ActiveModel::Validations
|
3
|
+
|
4
|
+
# == Attribute ==========================================================
|
5
|
+
|
6
|
+
attr_accessor :email
|
7
|
+
attr_accessor :password
|
8
|
+
attr_accessor :remember_me
|
9
|
+
attr_accessor :user
|
10
|
+
|
11
|
+
# == Validations ==========================================================
|
12
|
+
|
13
|
+
validates :email,
|
14
|
+
:presence => {:message => 'Please enter your email address'},
|
15
|
+
:length => {
|
16
|
+
:within => 6..100,
|
17
|
+
:too_short => "The email address you entered is to short"
|
18
|
+
},
|
19
|
+
:format => {
|
20
|
+
:with => /^([\w.%-+]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i,
|
21
|
+
:message => 'The email address you entered is not valid'
|
22
|
+
}
|
23
|
+
|
24
|
+
validates :password,
|
25
|
+
:length => {
|
26
|
+
:within => 4..40,
|
27
|
+
:too_short => "The password you entered is too short (minimum is 4 characters)"
|
28
|
+
},
|
29
|
+
:presence => {
|
30
|
+
:message => 'Please choose a password'
|
31
|
+
},
|
32
|
+
:confirmation => {:message => "The password you entered does not match with the confirmation"}
|
33
|
+
|
34
|
+
# == Class Methods ========================================================
|
35
|
+
|
36
|
+
def self.create(params={})
|
37
|
+
session_user = self.new(params)
|
38
|
+
session_user.save
|
39
|
+
session_user
|
40
|
+
end
|
41
|
+
|
42
|
+
# == Instance Methods =====================================================
|
43
|
+
|
44
|
+
def initialize(params = {})
|
45
|
+
if params
|
46
|
+
@email = params[:email]
|
47
|
+
@password = params[:password]
|
48
|
+
@remember_me = params[:remember_me]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def save
|
53
|
+
return unless self.valid?
|
54
|
+
if @user = User.authenticate(self.email, self.password)
|
55
|
+
if @user.is_verified?
|
56
|
+
return @user
|
57
|
+
else
|
58
|
+
@user.email_verification_key ||= @user.set_email_verification_key
|
59
|
+
@user.save!
|
60
|
+
self.errors.add(:email, "This email address needs to be verified before you can login. <a href='/resend-email-verification/#{@user.email_verification_key}'>Resend verification</a>".html_safe)
|
61
|
+
return false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_key
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
data/app/models/user.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
class User < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Constants ============================================================
|
4
|
+
|
5
|
+
ROLES = %w{admin regular_user}
|
6
|
+
|
7
|
+
# == Attributes =====================================================
|
8
|
+
|
9
|
+
attr_protected :role
|
10
|
+
|
11
|
+
# == Extensions ===========================================================
|
12
|
+
|
13
|
+
if defined?(Wristband)
|
14
|
+
wristband :roles => ROLES, :has_authorities => true
|
15
|
+
end
|
16
|
+
|
17
|
+
# == Validations ==========================================================
|
18
|
+
|
19
|
+
validates :first_name,
|
20
|
+
:presence => {:message => 'Please enter your first name'}
|
21
|
+
|
22
|
+
validates :last_name,
|
23
|
+
:presence => {:message => 'Please enter your last name'}
|
24
|
+
|
25
|
+
validates :email,
|
26
|
+
:presence => {:message => 'Please enter your email address'},
|
27
|
+
:length => {
|
28
|
+
:within => 6..100,
|
29
|
+
:too_short => "The email address you entered is to short"
|
30
|
+
},
|
31
|
+
:format => {
|
32
|
+
:with => EmailSupport::RFC822::EmailAddress,
|
33
|
+
:message => 'The email address you entered is not valid'
|
34
|
+
},
|
35
|
+
:uniqueness => {:message => 'This email has already been taken'}
|
36
|
+
|
37
|
+
validates :password,
|
38
|
+
:length => {
|
39
|
+
:within => 4..40,
|
40
|
+
:too_short => "The password you entered is too short (minimum is 4 characters)",
|
41
|
+
:if => :password_required?
|
42
|
+
},
|
43
|
+
:presence => {
|
44
|
+
:message => 'Please choose a password',
|
45
|
+
:if => :password_required?
|
46
|
+
},
|
47
|
+
:confirmation => {:message => "The password you entered does not match with the confirmation"}
|
48
|
+
|
49
|
+
validates :role,
|
50
|
+
:inclusion => { :in => ROLES },
|
51
|
+
:presence => true
|
52
|
+
|
53
|
+
validates :address, :city, :state,
|
54
|
+
:presence => {
|
55
|
+
:message => 'Please enter your full address'
|
56
|
+
}
|
57
|
+
|
58
|
+
# == Relationships ========================================================
|
59
|
+
|
60
|
+
has_many :memberships,
|
61
|
+
:dependent => :destroy
|
62
|
+
has_many :areas,
|
63
|
+
:through => :memberships
|
64
|
+
has_many :posts,
|
65
|
+
:dependent => :nullify
|
66
|
+
|
67
|
+
accepts_nested_attributes_for :memberships, :allow_destroy => true
|
68
|
+
|
69
|
+
# == Scopes ===============================================================
|
70
|
+
|
71
|
+
# Search Scope
|
72
|
+
scope :email_or_name_or_address_search,
|
73
|
+
lambda {|str|
|
74
|
+
like_str = "%#{str}%"
|
75
|
+
where("email ILIKE ? OR ((first_name || ' ' || last_name) ILIKE ?) OR ((address || ', ' || city || ', ' || state) ILIKE ?)",
|
76
|
+
like_str, like_str, like_str)
|
77
|
+
}
|
78
|
+
|
79
|
+
if defined?(MetaSearch)
|
80
|
+
search_methods :email_or_name_or_address_search
|
81
|
+
end
|
82
|
+
|
83
|
+
scope :admins, where(:role => 'admin')
|
84
|
+
|
85
|
+
# == Callbacks ============================================================
|
86
|
+
|
87
|
+
before_validation :assign_role, :on => :create
|
88
|
+
|
89
|
+
# == Class Methods ========================================================
|
90
|
+
|
91
|
+
def self.per_page
|
92
|
+
100
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.roles_for_select
|
96
|
+
ROLES.collect{|r| [r.humanize, r]}
|
97
|
+
end
|
98
|
+
|
99
|
+
# == Instance Methods =====================================================
|
100
|
+
|
101
|
+
def address_attributes=(address_attr)
|
102
|
+
if address_attr.is_a?(Address)
|
103
|
+
self.address = address_attr.address
|
104
|
+
self.city = address_attr.city
|
105
|
+
self.state = address_attr.state
|
106
|
+
self.lat = address_attr.lat
|
107
|
+
self.lng = address_attr.lng
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def full_address
|
112
|
+
[self.address, self.city, self.state].join(', ')
|
113
|
+
end
|
114
|
+
|
115
|
+
def full_name
|
116
|
+
[first_name, last_name].join(' ')
|
117
|
+
end
|
118
|
+
|
119
|
+
def member_of?(area)
|
120
|
+
self.areas.include?(area)
|
121
|
+
end
|
122
|
+
|
123
|
+
def set_email_verification_key
|
124
|
+
self.email_verification_key = Wristband::Support.random_salt.gsub(/[^A-Za-z0-9]/,'')
|
125
|
+
end
|
126
|
+
|
127
|
+
def is_verified?
|
128
|
+
!!self.verified_at?
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
def assign_role
|
133
|
+
self.role = 'regular_user'
|
134
|
+
end
|
135
|
+
|
136
|
+
def password_required?
|
137
|
+
self.new_record?
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class UserActivity < ActiveRecord::Base
|
2
|
+
|
3
|
+
# == Constants ============================================================
|
4
|
+
|
5
|
+
EXPIRES = 5 # in seconds
|
6
|
+
|
7
|
+
# == Relationships ========================================================
|
8
|
+
|
9
|
+
validates :name, :url,
|
10
|
+
:presence => true
|
11
|
+
|
12
|
+
# == Scopes ===============================================================
|
13
|
+
|
14
|
+
scope :active_for_page, lambda {|name, url|
|
15
|
+
where(
|
16
|
+
"expires_at >= ? AND name != ? AND url = ?",
|
17
|
+
Time.now, name, url
|
18
|
+
)
|
19
|
+
}
|
20
|
+
|
21
|
+
# == Callbacks ============================================================
|
22
|
+
|
23
|
+
after_create :set_expiration!
|
24
|
+
|
25
|
+
# == Instance Methods =====================================================
|
26
|
+
|
27
|
+
def set_expiration!
|
28
|
+
self.update_attribute(:expires_at, Time.now.advance(:seconds => EXPIRES))
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class UserAuthorityCheck < AuthorityCheck
|
2
|
+
|
3
|
+
def manage_users?
|
4
|
+
unless (@user.is_admin?)
|
5
|
+
fail!("You don't have permission do manage users.")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def manage_areas?
|
10
|
+
unless (@user.is_admin?)
|
11
|
+
fail!("You don't have permission do manage users.")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
%ul.sub_nav
|
2
|
+
%li
|
3
|
+
%h3
|
4
|
+
= @area.name
|
5
|
+
in
|
6
|
+
= @area.location
|
7
|
+
- if @area.current_issue
|
8
|
+
%li= active_link_to 'Current issue', [:edit, :admin, @area, @area.current_issue], :active => { :when => /admin\/areas\/\d*\/issues\/\d*\/edit/ }
|
9
|
+
%li= active_link_to 'Issues', [:admin, @area, :issues], :active => { :when => :self_only }
|
10
|
+
%li= active_link_to 'Memberships', [:admin, @area, :memberships]
|
11
|
+
%li= active_link_to 'Edit borders', [:edit_borders, :admin, @area]
|
12
|
+
%li= active_link_to 'Edit details', [:edit, :admin, @area], :active => { :when => :self_only }
|
@@ -0,0 +1,22 @@
|
|
1
|
+
.columns
|
2
|
+
= render :partial => 'admin/areas/nav'
|
3
|
+
|
4
|
+
.left_column
|
5
|
+
|
6
|
+
= formatted_form_for @area, :url => [:admin, @area] do |f|
|
7
|
+
= render :partial => 'form', :locals => {:f => f}
|
8
|
+
|
9
|
+
|
10
|
+
.right_column
|
11
|
+
|
12
|
+
- if @area.border.present?
|
13
|
+
= content_for :head do
|
14
|
+
= javascript_include_tag "http://maps.google.com/maps/api/js?sensor=false"
|
15
|
+
= javascript_include_tag 'google_maps'
|
16
|
+
|
17
|
+
= content_for :doc_ready do
|
18
|
+
initialize_gmap(document.getElementById("map_canvas"), #{@area.center.x}, #{@area.center.y}, {}, #{@area.bounds.to_json});
|
19
|
+
add_region(#{@area.id}, [#{@area.border_coordinates}], 'selected')
|
20
|
+
|
21
|
+
#map_canvas{:style => 'width:260px; height:400px; float: right'}
|
22
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
.columns
|
2
|
+
= content_for :head do
|
3
|
+
= javascript_include_tag "http://maps.google.com/maps/api/js?libraries=geometry&sensor=false"
|
4
|
+
= javascript_include_tag 'region_editor'
|
5
|
+
|
6
|
+
:javascript
|
7
|
+
$(document).ready(function() {
|
8
|
+
$('#map_canvas').RegionEditor({
|
9
|
+
create_url: '#{admin_areas_path}',
|
10
|
+
update_url: '#{bulk_update_admin_areas_path}',
|
11
|
+
regions: #{@areas.collect(&:to_a).to_json},
|
12
|
+
selected_region_id: #{@selected_area.present? ? @selected_area.id : 'null'},
|
13
|
+
center: {
|
14
|
+
lat: #{@selected_area.present? ? @selected_area.center.x : @default_area.center.x},
|
15
|
+
lng: #{@selected_area.present? ? @selected_area.center.y : @default_area.center.y}
|
16
|
+
},
|
17
|
+
bounds: #{@selected_area.present? ? @selected_area.bounds.to_json : @default_area.bounds.to_json}
|
18
|
+
});
|
19
|
+
});
|
20
|
+
- if @area.present?
|
21
|
+
= render :partial => 'admin/areas/nav'
|
22
|
+
|
23
|
+
#map_canvas
|
24
|
+
.region_details
|
25
|
+
|
26
|
+
%h3 Manage regions:
|
27
|
+
%p
|
28
|
+
%strong 1. Click on a region to select it
|
29
|
+
%br/
|
30
|
+
%strong 2. Drag the markers to change the shape of a region
|
31
|
+
%br/
|
32
|
+
%strong 3. To add a new point to a region:
|
33
|
+
Right click on the map to add a new point to the selected region
|
34
|
+
%br/
|
35
|
+
%strong 4. To delete a point from a region:
|
36
|
+
SHIFT + Click on a marker to delete it
|
37
|
+
|
38
|
+
%h3 Create new regions:
|
39
|
+
%p
|
40
|
+
%strong 1. Click the 'New region' button
|
41
|
+
%br/
|
42
|
+
%strong 2. Click on the map to add points to the region
|
43
|
+
%br/
|
44
|
+
%strong 3. When you're done close the region by clicking on the first marker
|
@@ -0,0 +1,61 @@
|
|
1
|
+
.columns
|
2
|
+
.header
|
3
|
+
%h2 Areas
|
4
|
+
= formatted_form_for @search, :url => [:admin, :areas], :html => {:method => :get, :class => 'admin_search'} do |f|
|
5
|
+
= f.text_field :full_name_search, :label => 'name or location'
|
6
|
+
= f.element('and show activity between') do
|
7
|
+
= text_field_tag :activity_start_date, params[:activity_start_date], :type => :date
|
8
|
+
= text_field_tag :activity_end_date, params[:activity_end_date], :type => :date
|
9
|
+
= f.submit 'Find'
|
10
|
+
%h5= link_to 'Add new area', edit_borders_admin_area_path('new')
|
11
|
+
|
12
|
+
:javascript
|
13
|
+
$(document).ready(function() {
|
14
|
+
draw_chart({
|
15
|
+
days : #{@activities.collect{|d| d.day.strftime('%d<br/>%b')}.to_json},
|
16
|
+
series : [
|
17
|
+
{
|
18
|
+
name: 'New Users',
|
19
|
+
data : #{@activities.collect{|a| a.sum_new_users_count.to_i}.to_json}
|
20
|
+
},
|
21
|
+
{
|
22
|
+
name: 'Quitters',
|
23
|
+
data : #{@activities.collect{|a| a.sum_quitters_count.to_i}.to_json}
|
24
|
+
},
|
25
|
+
{
|
26
|
+
name: 'New Posts',
|
27
|
+
data : #{@activities.collect{|a| a.sum_new_posts_count.to_i}.to_json}
|
28
|
+
},
|
29
|
+
{
|
30
|
+
name: 'Issues Published',
|
31
|
+
data : #{@activities.collect{|a| a.sum_issues_published_count.to_i}.to_json}
|
32
|
+
}
|
33
|
+
]
|
34
|
+
});
|
35
|
+
})
|
36
|
+
#chart-container
|
37
|
+
|
38
|
+
= will_paginate @areas
|
39
|
+
|
40
|
+
%table
|
41
|
+
%tr
|
42
|
+
%th= sort_link @search, :name
|
43
|
+
%th= sort_link @search, :city
|
44
|
+
%th.center= sort_link @search, :memberships_count, 'Users'
|
45
|
+
%th.center= sort_link @search, :households
|
46
|
+
%th.center Send mode
|
47
|
+
%th.center= sort_link @search, :issues_count, 'Issues'
|
48
|
+
%th.center New posts
|
49
|
+
|
50
|
+
- @areas.each do |area|
|
51
|
+
%tr{:class => @new_posts[area.id] && @new_posts[area.id] > 0 ? 'important' : ''}
|
52
|
+
%td= link_to area.name, [:edit_borders, :admin, area]
|
53
|
+
%td= area.location
|
54
|
+
%td.center= link_to area.memberships_count.to_i, [:admin, area, :memberships]
|
55
|
+
%td.center= area.households.to_i
|
56
|
+
%td.center= area.send_mode
|
57
|
+
%td.center= link_to area.issues_count.to_i, [:admin, area, :issues]
|
58
|
+
%td.center= link_to @new_posts[area.id].to_s, [:admin, area]
|
59
|
+
|
60
|
+
= will_paginate @areas
|
61
|
+
|