radiant-reader-extension 2.0.0.rc4 → 3.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/README.md +9 -5
  2. data/app/controllers/{readers_controller.rb → accounts_controller.rb} +31 -30
  3. data/app/controllers/groups_controller.rb +35 -0
  4. data/app/controllers/messages_controller.rb +1 -0
  5. data/app/controllers/password_resets_controller.rb +6 -6
  6. data/app/controllers/reader_action_controller.rb +8 -0
  7. data/app/controllers/reader_activations_controller.rb +3 -2
  8. data/app/controllers/reader_sessions_controller.rb +6 -2
  9. data/app/helpers/reader_helper.rb +61 -17
  10. data/app/models/group.rb +33 -5
  11. data/app/models/message.rb +7 -4
  12. data/app/models/message_reader.rb +4 -0
  13. data/app/models/reader.rb +71 -3
  14. data/app/models/reader_page.rb +56 -0
  15. data/app/views/{readers → accounts}/_contributions.html.haml +0 -0
  16. data/app/views/{readers → accounts}/_controls.html.haml +1 -2
  17. data/app/views/accounts/_description.html.haml +2 -0
  18. data/app/views/{readers → accounts}/_extra_controls.html.haml +0 -0
  19. data/app/views/{readers → accounts}/_flasher.html.haml +0 -0
  20. data/app/views/{readers → accounts}/_form.html.haml +10 -15
  21. data/app/views/accounts/_gravatar.html.haml +3 -0
  22. data/app/views/accounts/_groups.html.haml +9 -0
  23. data/app/views/accounts/_links.html.haml +12 -0
  24. data/app/views/accounts/_list.html.haml +17 -0
  25. data/app/views/{readers → accounts}/_memberships.html.haml +0 -0
  26. data/app/views/accounts/_profile.html.haml +29 -0
  27. data/app/views/accounts/_profile_form.html.haml +86 -0
  28. data/app/views/accounts/_reader.html.haml +10 -0
  29. data/app/views/accounts/dashboard.html.haml +28 -0
  30. data/app/views/{readers → accounts}/edit.html.haml +12 -10
  31. data/app/views/accounts/edit_profile.html.haml +34 -0
  32. data/app/views/accounts/index.html.haml +23 -0
  33. data/app/views/{readers → accounts}/login.html.haml +0 -0
  34. data/app/views/{readers → accounts}/new.html.haml +0 -0
  35. data/app/views/{readers → accounts}/permission_denied.html.haml +0 -0
  36. data/app/views/{readers → accounts}/show.html.haml +9 -12
  37. data/app/views/dashboard/_description.html.haml +3 -0
  38. data/app/views/dashboard/_directory.html.haml +3 -0
  39. data/app/views/dashboard/_groups.html.haml +8 -0
  40. data/app/views/dashboard/_messages.html.haml +11 -0
  41. data/app/views/dashboard/_profile.html.haml +3 -0
  42. data/app/views/dashboard/_welcome.html.haml +5 -0
  43. data/app/views/groups/_all.html.haml +10 -0
  44. data/app/views/groups/index.html.haml +21 -0
  45. data/app/views/groups/show.html.haml +31 -0
  46. data/app/views/messages/show.html.haml +12 -3
  47. data/app/views/reader_sessions/new.html.haml +1 -1
  48. data/app/views/shared/_standard_reader_parts.html.haml +9 -3
  49. data/config/initializers/formats.rb +2 -0
  50. data/config/initializers/radiant_config.rb +3 -0
  51. data/config/locales/en.yml +90 -25
  52. data/config/routes.rb +8 -6
  53. data/db/migrate/20110707101339_group_slugs.rb +9 -0
  54. data/db/migrate/20110711150605_snail_addresses.rb +21 -0
  55. data/db/migrate/20110712081159_directory_permissions.rb +11 -0
  56. data/db/migrate/20110712141134_name_parts.rb +9 -0
  57. data/db/migrate/20110728112254_current_login_at.rb +9 -0
  58. data/lib/grouped_model.rb +16 -12
  59. data/lib/grouped_page.rb +1 -0
  60. data/lib/radiant-reader-extension.rb +1 -1
  61. data/lib/reader_admin_ui.rb +32 -1
  62. data/lib/reader_tags.rb +119 -52
  63. data/lib/site_controller_extensions.rb +2 -2
  64. data/public/stylesheets/sass/reader.sass +49 -0
  65. data/radiant-reader-extension.gemspec +4 -1
  66. data/reader_extension.rb +9 -9
  67. data/spec/controllers/{readers_controller_spec.rb → accounts_controller_spec.rb} +9 -5
  68. data/spec/controllers/groups_controller_spec.rb +17 -0
  69. data/spec/controllers/reader_activations_controller_spec.rb +3 -3
  70. data/spec/datasets/readers_dataset.rb +3 -0
  71. data/spec/lib/reader_tags_spec.rb +55 -0
  72. data/spec/matchers/reader_login_system_matcher.rb +2 -2
  73. data/spec/models/group_spec.rb +6 -0
  74. data/spec/models/reader_page_spec.rb +106 -0
  75. metadata +109 -26
  76. data/app/views/readers/index.html.haml +0 -38
data/README.md CHANGED
@@ -2,30 +2,34 @@
2
2
 
3
3
  This is a framework that takes care of all the dull bits of registering, activating, reminding, logging in and editing preferences for your site visitors.
4
4
 
5
- It uses authlogic to handle sessions and provides complete interfaces both for the administrator and the visitor. The admin interface is basic and fits in with radiant. The visitor interface is more friendly (and incidentally includes a trick email field - so-called inverse captcha - that should prevent spam signups).
5
+ It uses authlogic to handle sessions and provides complete interfaces both for the administrator and the visitor. The admin interface is very basic and fits in with radiant. The visitor interface is more friendly (and incidentally includes a trick email field - so-called inverse captcha - that should prevent spam signups).
6
6
 
7
7
  The visitors are referred to as 'readers' here. Readers never see the admin interface, but your site authors and admins are automatically given reader status.
8
8
 
9
- The purpose of this extension is to provide a common core that supports other visitor-facing machinery. See for example our [forum extension](http://github.com/spanner/radiant-forum-extension) for discussions and page/blog comments, [reader groups](http://github.com/spanner/radiant-reader_group-extension) for proper page-access control and [downloads extension](http://github.com/spanner/radiant-downloads-extension) for secure access-controlled file downloads. More will follow and I hope other people will make use of this too.
9
+ The purpose of this extension is to provide a common core that supports other visitor-facing machinery. See for example our [forum extension](http://github.com/spanner/radiant-forum-extension) for discussions and page/blog comments and [downloads extension](http://github.com/spanner/radiant-downloads-extension) for secure access-controlled file downloads. More will follow and I hope other people will make use of this framework.
10
10
 
11
11
  ## Latest
12
12
 
13
13
  This version requires edge radiant, or radiant 1 when it becomes available. We are using a lot of the new configuration and sheets code.
14
14
 
15
+ New ReaderPages provide flexible directory services with configurable access control. The old controller and page parts mechanism is going to be phased out gradually both here and in the forum in favour of more orthodox radiant page-types. We will always need to use the layout-wrapper approach for login and registration forms, though.
16
+
15
17
  Right now we are **not compatible with multi_site or the sites extension**: that's mostly because neither is radiant edge: it will all be sorted out in time for the release of v1, which isn't far away.
16
18
 
19
+ Also:
20
+
17
21
  * public interface internationalized;
18
22
  * Uses the new configuration interface;
19
23
  * Messaging much simplified and now intended to be purely administrative.
20
- * ajaxable status panel returned by `reader_session_url`
24
+ * ajaxable status panel returned by `reader_session_url` (ie. you just have to call /reader_session.js over xmlhttp to get a sensible welcome and control block)
21
25
 
22
26
  ## Status
23
27
 
24
- Compatible with radiant 0.9.2, which isn't out yet. You can use radiant edge to try this out. Expect small changes in support of the new forum and group releases. Multi-site compatibility fixes are likely too.
28
+ Compatible with radiant 1, which isn't out yet. You can use radiant edge to try this out. Expect small changes in support of the new forum and group releases. Multi-site compatibility will follow soon.
25
29
 
26
30
  ## Note on internationalisation and customisation
27
31
 
28
- The locale strings here are generally defined in a functional rather than grammatical way. That is, they have labels like `activation_required_explanation` rather than being assembled out of smaller units. This is partly because for flexibility of translation, but also because it gives you an easy way to change the text on functional pages like reader-preferences and registration forms.
32
+ The locale strings here are generally defined in a functional rather than grammatical way. That is, they have labels like `activation_required_explanation` rather than being assembled out of lexical units. This is partly because for flexibility of translation, but also because it gives you an easy way to change the text on functional pages like reader-preferences and registration forms.
29
33
 
30
34
  ## Requirements
31
35
 
@@ -1,23 +1,40 @@
1
- class ReadersController < ReaderActionController
1
+ class AccountsController < ReaderActionController
2
2
  helper :reader
3
3
 
4
- cattr_accessor :edit_partials, :show_partials, :index_partials
5
-
6
- before_filter :check_registration_allowed, :only => [:new, :create]
7
- before_filter :initialize_partials
8
- before_filter :i_am_me, :only => [:show, :edit]
4
+ before_filter :check_registration_allowed, :only => [:new, :create, :activate]
5
+ before_filter :i_am_me, :only => [:show, :edit, :edit_profile]
9
6
  before_filter :require_reader, :except => [:new, :create, :activate]
10
7
  before_filter :default_to_self, :only => [:show]
11
- before_filter :restrict_to_self, :only => [:edit, :update, :resend_activation]
8
+ before_filter :restrict_to_self, :only => [:edit, :edit_profile, :update, :resend_activation]
12
9
  before_filter :no_removing, :only => [:remove, :destroy]
13
10
  before_filter :ensure_groups_subscribable, :only => [:update, :create]
14
11
 
15
12
  def index
16
- @readers = Reader.active.paginate(pagination_parameters.merge(:per_page => 60))
13
+ @readers = Reader.visible_to(current_reader)
14
+ respond_to do |format|
15
+ format.html {}
16
+ format.csv {
17
+ send_data generate_csv(@readers), :type => 'text/csv; charset=utf-8; header=present', :filename => "everyone.csv"
18
+ }
19
+ format.vcard {
20
+ send_data @readers.map(&:vcard).join("\n"), :filename => "everyone.vcf"
21
+ }
22
+ end
17
23
  end
18
24
 
19
25
  def show
20
26
  @reader = Reader.find(params[:id])
27
+ respond_to do |format|
28
+ format.html
29
+ format.vcard {
30
+ send_data @reader.vcard.to_s, :filename => "#{@reader.filename}.vcf"
31
+ }
32
+ end
33
+ end
34
+
35
+ def dashboard
36
+ # @reader = current_reader
37
+ expires_now
21
38
  end
22
39
 
23
40
  def new
@@ -33,7 +50,11 @@ class ReadersController < ReaderActionController
33
50
  def edit
34
51
  expires_now
35
52
  end
36
-
53
+
54
+ def edit_profile
55
+ expires_now
56
+ end
57
+
37
58
  def create
38
59
  @reader = Reader.new(params[:reader])
39
60
  @reader.clear_password = params[:reader][:password]
@@ -67,7 +88,7 @@ class ReadersController < ReaderActionController
67
88
  @reader.clear_password = params[:reader][:password] if params[:reader][:password]
68
89
  if @reader.save
69
90
  flash[:notice] = t('reader_extension.account_updated')
70
- redirect_to url_for(@reader)
91
+ redirect_to dashboard_url
71
92
  else
72
93
  render :action => 'edit'
73
94
  end
@@ -113,27 +134,7 @@ protected
113
134
  end
114
135
  end
115
136
 
116
- def self.add_edit_partial(path)
117
- @@edit_partials ||= []
118
- edit_partials.push(path)
119
- end
120
-
121
- def self.add_show_partial(path)
122
- @@show_partials ||= []
123
- show_partials.push(path)
124
- end
125
-
126
- def self.add_index_partial(path)
127
- @@index_partials ||= []
128
- index_partials.push(path)
129
- end
130
-
131
137
  private
132
- def initialize_partials
133
- @show_partials = show_partials
134
- @edit_partials = edit_partials
135
- @index_partials = index_partials
136
- end
137
138
 
138
139
  def ensure_groups_subscribable
139
140
  if params[:reader] && params[:reader][:group_ids]
@@ -0,0 +1,35 @@
1
+ class GroupsController < ReaderActionController
2
+ helper :reader
3
+
4
+ before_filter :require_reader
5
+ before_filter :get_group_or_groups
6
+ before_filter :require_group_visibility, :only => [:show]
7
+
8
+ def index
9
+ end
10
+
11
+ def show
12
+ @readers = @group.readers.uniq
13
+ respond_to do |format|
14
+ format.html
15
+ format.csv {
16
+ send_data generate_csv(@readers), :type => 'text/csv; charset=utf-8; header=present', :filename => "#{@group.filename}.csv"
17
+ }
18
+ format.vcard {
19
+ send_data @readers.map(&:vcard).join("\n"), :filename => "#{@group.filename}.vcf"
20
+ }
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def get_group_or_groups
27
+ @groups = Group.visible_to(current_reader)
28
+ @group = @groups.find(params[:id]).first if params[:id]
29
+ end
30
+
31
+ def require_group_visibility
32
+ raise ReaderError::AccessDenied if @group && !@group.visible_to?(current_reader)
33
+ end
34
+
35
+ end
@@ -30,6 +30,7 @@ protected
30
30
 
31
31
  def get_message
32
32
  @message = current_reader.messages.find(params[:id])
33
+ @delivery = @message.delivery_to(current_reader)
33
34
  end
34
35
 
35
36
  end
@@ -13,13 +13,13 @@ class PasswordResetsController < ReaderActionController
13
13
  end
14
14
 
15
15
  def create
16
- @reader = Reader.find_by_email(params[:email])
17
- if @reader
18
- if @reader.activated?
19
- @reader.send_password_reset_message
16
+ @forgetter = Reader.find_by_email(params[:email])
17
+ if @forgetter
18
+ if @forgetter.activated?
19
+ @forgetter.send_password_reset_message
20
20
  render
21
21
  else
22
- @reader.send_activation_message
22
+ @forgetter.send_activation_message
23
23
  redirect_to new_reader_activation_url
24
24
  end
25
25
  else
@@ -42,7 +42,7 @@ class PasswordResetsController < ReaderActionController
42
42
  if @reader.save
43
43
  self.current_reader = @reader
44
44
  flash[:notice] = t('reader_extension.password_updated_notice')
45
- redirect_to url_for(@reader)
45
+ redirect_to dashboard_url
46
46
  else
47
47
  flash[:error] = t('reader_extension.password_mismatch')
48
48
  render :action => :edit
@@ -99,4 +99,12 @@ protected
99
99
  end
100
100
  end
101
101
 
102
+ def generate_csv(readers=[])
103
+ columns = %w{forename surname email phone mobile postal_address}
104
+ table = FasterCSV.generate do |csv|
105
+ csv << columns.map { |f| t("activerecord.attributes.reader.#{f}") }
106
+ readers.each { |r| csv << columns.map{ |f| r.send(f.to_sym) } }
107
+ end
108
+ end
109
+
102
110
  end
@@ -31,11 +31,12 @@ class ReaderActivationsController < ReaderActionController
31
31
  if @reader
32
32
  @reader.activate!
33
33
  self.current_reader = @reader
34
+ redirect_to dashboard_url
34
35
  else
35
36
  @error = t("reader_extension.please_check_message")
37
+ expires_now
38
+ render :action => 'show'
36
39
  end
37
- expires_now
38
- render :action => 'show'
39
40
  end
40
41
 
41
42
  protected
@@ -17,7 +17,7 @@ class ReaderSessionsController < ReaderActionController
17
17
  end
18
18
  }
19
19
  format.js {
20
- render :partial => 'readers/controls', :layout => false
20
+ render :partial => 'accounts/controls', :layout => false
21
21
  }
22
22
  end
23
23
  end
@@ -26,7 +26,7 @@ class ReaderSessionsController < ReaderActionController
26
26
  if current_reader
27
27
  if current_reader.activated?
28
28
  cookies[:error] = t('reader_extension.already_logged_in')
29
- redirect_to reader_url(current_reader)
29
+ redirect_to default_welcome_url(current_reader)
30
30
  else
31
31
  cookies[:error] = t('reader_extension.account_requires_activation')
32
32
  redirect_to reader_activation_url
@@ -72,4 +72,8 @@ class ReaderSessionsController < ReaderActionController
72
72
  redirect_to reader_login_url
73
73
  end
74
74
 
75
+ def default_welcome_url(reader=nil)
76
+ reader.home_url || dashboard_url
77
+ end
78
+
75
79
  end
@@ -1,7 +1,11 @@
1
1
  require 'sanitize'
2
2
  require "sanitize/config/generous"
3
+ require "fastercsv"
3
4
 
4
5
  module ReaderHelper
6
+ include SnailHelpers
7
+ include Admin::RegionsHelper
8
+
5
9
  def standard_gravatar_for(reader=nil, url=nil)
6
10
  size = Radiant::Config['forum.gravatar_size'] || 40
7
11
  url ||= reader_url(reader)
@@ -20,6 +24,26 @@ module ReaderHelper
20
24
  image_tag gravatar_url(reader.email, gravatar_options), img_options
21
25
  end
22
26
  end
27
+
28
+ def link_to_reader(reader)
29
+ if page = ReaderPage.first
30
+ page.url_for(reader)
31
+ else
32
+ reader_url(reader)
33
+ end
34
+ end
35
+
36
+ def link_to_group(group)
37
+ if page = group.homepage
38
+ link_to group.name, page.url
39
+ else
40
+ link_to group.name, group_url(group)
41
+ end
42
+ end
43
+
44
+ def link_to_message(message)
45
+ link_to message.subject, message_url(message)
46
+ end
23
47
 
24
48
  def home_page_link(options={})
25
49
  home_page = Page.find_by_parent_id(nil)
@@ -34,11 +58,14 @@ module ReaderHelper
34
58
  Sanitize.clean(textilize(text), Sanitize::Config::GENEROUS)
35
59
  end
36
60
 
37
- def truncate_words(text='', length=24, omission="...")
61
+ def truncate_words(text='', options={})
38
62
  return '' if text.blank?
63
+ options = {:length => options} unless options.is_a? Hash
64
+ options.reverse_merge!(:length => 30, :omission => '&hellip;')
39
65
  words = text.split
40
- omission = '' unless words.size > length
41
- words[0..(length-1)].join(" ") + omission
66
+ length = options[:length].to_i
67
+ options[:omission] = '' unless words.size > length
68
+ words[0..(length-1)].join(" ") + options[:omission]
42
69
  end
43
70
 
44
71
  def pagination_and_summary_for(list, name='')
@@ -53,20 +80,9 @@ module ReaderHelper
53
80
 
54
81
  def pagination_summary(list, name='')
55
82
  total = list.total_entries
56
- if list.empty?
57
- %{#{t('reader_extension.no')} #{name.pluralize}}
58
- else
59
- name ||= t(list.first.class.to_s.underscore.gsub('_', ' '))
60
- if total == 1
61
- %{#{t('reader_extension.showing')} #{t('reader_extension.one')} #{name}}
62
- elsif list.current_page == 1 && total < list.per_page
63
- %{#{t('reader_extension.all')} #{total} #{name.pluralize}}
64
- else
65
- start = list.offset + 1
66
- finish = ((list.offset + list.per_page) < list.total_entries) ? list.offset + list.per_page : list.total_entries
67
- %{#{start} #{t('reader_extension.to')} #{finish} #{t('reader_extension.of')} #{total} #{name.pluralize}}
68
- end
69
- end
83
+ start = list.offset + 1
84
+ finish = ((list.offset + list.per_page) < list.total_entries) ? list.offset + list.per_page : list.total_entries
85
+ t("reader_extension.showing_of_total", :count => total, :start => start, :finish => finish, :name => name, :names => name.pluralize)
70
86
  end
71
87
 
72
88
  def message_preview(subject, body, reader)
@@ -101,5 +117,33 @@ EOM
101
117
  end
102
118
  options
103
119
  end
120
+
121
+ def friendly_date(datetime)
122
+ I18n.l(datetime, :format => friendly_date_format(datetime)) if datetime
123
+ end
124
+
125
+ def friendly_date_format(datetime)
126
+ if datetime && date = datetime.to_date
127
+ if (date.to_datetime == Date.today)
128
+ :today
129
+ elsif (date.to_datetime == Date.yesterday)
130
+ :yesterday
131
+ elsif (date.to_datetime > 6.days.ago)
132
+ :recently
133
+ elsif (date.year == Date.today.year)
134
+ :this_year
135
+ else
136
+ :standard
137
+ end
138
+ end
139
+ end
140
+
141
+ def country_options_for_select(selected = nil, default_selected = Snail.home_country)
142
+ usps_country_options_for_select(selected, default_selected)
143
+ end
144
+
145
+ def email_link(address)
146
+ mail_to address, nil, :encode => :hex, :replace_at => ' at ', :class => 'mailto'
147
+ end
104
148
 
105
149
  end
data/app/models/group.rb CHANGED
@@ -11,10 +11,11 @@ class Group < ActiveRecord::Base
11
11
  has_many :permissions
12
12
  has_many :pages, :through => :permissions
13
13
  has_many :memberships
14
- has_many :readers, :through => :memberships
14
+ has_many :readers, :through => :memberships, :uniq => true
15
15
 
16
- validates_presence_of :name
17
- validates_uniqueness_of :name
16
+ before_validation :set_slug
17
+ validates_presence_of :name, :slug, :allow_blank => false
18
+ validates_uniqueness_of :name, :slug
18
19
 
19
20
  named_scope :with_home_page, { :conditions => "homepage_id IS NOT NULL", :include => :homepage }
20
21
  named_scope :subscribable, { :conditions => "public = 1" }
@@ -24,6 +25,14 @@ class Group < ActiveRecord::Base
24
25
  { :conditions => ["groups.id IN (#{ids.map{"?"}.join(',')})", *ids] }
25
26
  }
26
27
 
28
+ named_scope :containing, lambda { |reader|
29
+ {
30
+ :joins => "INNER JOIN memberships as mb on mb.group_id = groups.id",
31
+ :conditions => ["mb.reader_id = ?", reader.id],
32
+ :group => column_names.map { |n| 'groups.' + n }.join(',')
33
+ }
34
+ }
35
+
27
36
  named_scope :attached_to, lambda { |objects|
28
37
  conditions = objects.map{|o| "(pp.permitted_type = ? AND pp.permitted_id = ?)" }.join(" OR ")
29
38
  binds = objects.map{|o| [o.class.to_s, o.id]}.flatten
@@ -32,15 +41,30 @@ class Group < ActiveRecord::Base
32
41
  :joins => "INNER JOIN permissions as pp on pp.group_id = groups.id",
33
42
  :conditions => [conditions, *binds],
34
43
  :having => "pcount > 0", # otherwise attached_to([]) returns all groups
35
- :group => column_names.map { |n| self.table_name + '.' + n }.join(','),
44
+ :group => column_names.map { |n| 'groups.' + n }.join(','),
36
45
  :readonly => false
37
46
  }
38
47
  }
39
48
 
49
+ def self.visible_to(reader=nil)
50
+ return all if Radiant.config['readers.public?']
51
+ return scoped({:conditions => "1 = 0"}) unless reader # nasty but chainable
52
+ return containing(reader) if Radiant.config['readers.confine_to_groups?']
53
+ return all
54
+ end
55
+
56
+ def visible_to?(reader=nil)
57
+ self.class.visible_to(reader).include? self
58
+ end
59
+
40
60
  def url
41
61
  homepage.url if homepage
42
62
  end
43
63
 
64
+ def filename
65
+ name.downcase.gsub(/\W/, '_')
66
+ end
67
+
44
68
  def send_welcome_to(reader)
45
69
  if reader.activated? # welcomes are also triggered on activation
46
70
  if message = Message.belonging_to(self).for_function('group_welcome').first # only if a group_welcome message exists *belonging to this group*
@@ -74,7 +98,11 @@ class Group < ActiveRecord::Base
74
98
  define_method("#{classname.downcase.pluralize}") { self.send("#{classname.to_s.downcase}_permissions".intern).map(&:permitted) }
75
99
  end
76
100
 
77
-
101
+ private
102
+
103
+ def set_slug
104
+ self.slug ||= self.name.slugify.to_s
105
+ end
78
106
 
79
107
  end
80
108
 
@@ -50,10 +50,6 @@ class Message < ActiveRecord::Base
50
50
  deliveries.any?
51
51
  end
52
52
 
53
- def delivered_to?(reader)
54
- recipients.include?(reader)
55
- end
56
-
57
53
  def preview(reader=nil)
58
54
  reader ||= possible_readers.first || Reader.for_user(created_by)
59
55
  ReaderNotifier.create_message(reader, self)
@@ -112,4 +108,11 @@ class Message < ActiveRecord::Base
112
108
  MessageReader.find_or_create_by_message_id_and_reader_id(self.id, reader.id).update_attribute(:sent_at, Time.now)
113
109
  end
114
110
 
111
+ def delivered_to?(reader)
112
+ recipients.include?(reader)
113
+ end
114
+
115
+ def delivery_to(reader)
116
+ deliveries.to_reader(reader).first if delivered_to?(reader)
117
+ end
115
118
  end
@@ -9,5 +9,9 @@ class MessageReader < ActiveRecord::Base
9
9
  named_scope :delivered, {
10
10
  :conditions => "sent_at IS NOT NULL and sent_at <= NOW()"
11
11
  }
12
+
13
+ named_scope :to_reader, lambda { |reader| {
14
+ :conditions => ["reader_id = ?", reader.id]
15
+ }}
12
16
 
13
17
  end
data/app/models/reader.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  require 'authlogic'
2
2
  require 'digest/sha1'
3
+ require 'snail'
4
+ require 'vcard'
3
5
 
4
6
  class Reader < ActiveRecord::Base
5
7
  @@user_columns = [:name, :email, :login, :created_at, :password, :notes]
6
8
  cattr_accessor :user_columns
7
9
  cattr_accessor :current
8
- attr_accessor :email_field # used in blocking spam registrations
10
+ attr_accessor :email_field, :newly_activated
9
11
 
10
12
  acts_as_authentic do |config|
11
13
  config.validations_scope = :site_id if defined? Site
@@ -21,7 +23,7 @@ class Reader < ActiveRecord::Base
21
23
  has_many :message_readers
22
24
  has_many :messages, :through => :message_readers
23
25
  has_many :memberships
24
- has_many :groups, :through => :memberships
26
+ has_many :groups, :through => :memberships, :uniq => true
25
27
  accepts_nested_attributes_for :memberships
26
28
 
27
29
  before_update :update_user
@@ -79,13 +81,67 @@ class Reader < ActiveRecord::Base
79
81
  end
80
82
  reader
81
83
  end
84
+
85
+ def self.visible_to(reader=nil)
86
+ return self.all if Radiant.config['readers.public?']
87
+ return self.scoped({:conditions => "1 = 0"}) unless reader # nasty but chainable
88
+ return self.in_groups(reader.groups) if Radiant.config['readers.confine_to_groups?']
89
+ return self.all
90
+ end
91
+
92
+ def visible_to?(reader=nil)
93
+ self.class.visible_to(reader).include? self
94
+ end
82
95
 
96
+ # not very i18nal, this
83
97
  def forename
84
- read_attribute(:forename) || name.split(/\s/).first
98
+ read_attribute(:forename) || name.split(/\s+/).first
85
99
  end
86
100
 
101
+ def surname
102
+ read_attribute(:surname) || name.split(/\s+/).last
103
+ end
104
+
105
+ def postal_address
106
+ Snail.new(
107
+ :name => name,
108
+ :line_1 => post_line1,
109
+ :line_2 => post_line2,
110
+ :city => post_city,
111
+ :region => post_province,
112
+ :postal_code => postcode,
113
+ :country => post_country
114
+ )
115
+ end
116
+
117
+ def vcard
118
+ @vcard ||= Vpim::Vcard::Maker.make2 do |maker|
119
+ maker.add_name do |n|
120
+ n.prefix = honorific || ""
121
+ n.given = forename || ""
122
+ n.family = surname || ""
123
+ end
124
+ maker.add_addr {|a|
125
+ a.location = 'home' # until we do this properly with multiple contact sets
126
+ a.country = post_country || ""
127
+ a.region = post_province || ""
128
+ a.locality = post_city || ""
129
+ a.street = [post_line1, post_line2].compact.join("\n")
130
+ a.postalcode = postcode || ""
131
+ }
132
+ maker.add_tel phone { |t| t.location = 'home' } unless phone.blank?
133
+ maker.add_tel mobile { |t| t.location = 'cell' } unless mobile.blank?
134
+ maker.add_email email { |e| t.location = 'home' }
135
+ end
136
+ end
137
+
138
+ def filename
139
+ name.downcase.gsub(/\W/, '_')
140
+ end
141
+
87
142
  def activate!
88
143
  self.activated_at = Time.now.utc
144
+ self.newly_activated = true
89
145
  self.save!
90
146
  send_welcome_message
91
147
  send_group_welcomes
@@ -94,6 +150,10 @@ class Reader < ActiveRecord::Base
94
150
  def activated?
95
151
  !inactive?
96
152
  end
153
+
154
+ def newly_activated?
155
+ !!newly_activated
156
+ end
97
157
 
98
158
  def inactive?
99
159
  self.activated_at.nil?
@@ -139,6 +199,14 @@ class Reader < ActiveRecord::Base
139
199
  homegroup.homepage
140
200
  end
141
201
  end
202
+
203
+ def home_url
204
+ if homepage = self.find_homepage
205
+ homepage.url
206
+ else
207
+ nil
208
+ end
209
+ end
142
210
 
143
211
  def can_see? (this)
144
212
  permitted_groups = this.permitted_groups
@@ -0,0 +1,56 @@
1
+ class ReaderPage < Page
2
+ include WillPaginate::ViewHelpers
3
+ attr_accessor :reader, :group
4
+
5
+ description %{ Presents readers and groups with configurable access control. }
6
+
7
+ def current_reader
8
+ Reader.current
9
+ end
10
+
11
+ def readers
12
+ if group
13
+ group.readers.visible_to(current_reader)
14
+ else
15
+ Reader.visible_to(current_reader)
16
+ end
17
+ end
18
+
19
+ def groups
20
+ Group.visible_to(current_reader)
21
+ end
22
+
23
+ def cache?
24
+ !!Radiant.config['readers.public?']
25
+ end
26
+
27
+ def visible?
28
+ Radiant.config['readers.public?'] || current_reader
29
+ end
30
+
31
+ def url_for(thing)
32
+ if thing.is_a?(Reader)
33
+ File.join(self.url, thing.id)
34
+ elsif thing.is_a?(Group)
35
+ File.join(self.url, thing.slug)
36
+ end
37
+ end
38
+
39
+ def find_by_url(url, live = true, clean = false)
40
+ url = clean_url(url) if clean
41
+ my_url = self.url
42
+ return false unless url =~ /^#{Regexp.quote(my_url)}(.*)/
43
+ raise ReaderError::AccessDenied unless visible?
44
+
45
+ params = $1.split('/').compact
46
+ self.group = Group.find_by_slug(params.first) if params.first =~ /\w/
47
+ self.reader = Reader.find_by_id(params.last) if params.last !~ /\D/
48
+
49
+ raise ReaderError::AccessDenied if group && !group.visible_to?(current_reader)
50
+ raise ReaderError::AccessDenied if reader && !reader.visible_to?(current_reader)
51
+ raise ActiveRecord::RecordNotFound if reader && group && !reader.is_in?(group)
52
+
53
+ self
54
+ end
55
+
56
+ end
@@ -1,6 +1,5 @@
1
1
  - if !current_reader
2
- = link_to t('reader_extension.navigation.log_in'), reader_login_url
3
- = link_to t('reader_extension.navigation.register'), new_reader_url
2
+ = t('reader_extension.navigation.welcome_please_log_in', :login_url => reader_login_url, :register_url => new_reader_url)
4
3
  - else
5
4
  %strong
6
5
  = link_to t('reader_extension.navigation.greeting', :name => current_reader.name), reader_profile_url
@@ -0,0 +1,2 @@
1
+ - reader ||= @reader
2
+ = clean_html(reader.description)