radiant-reader-extension 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/.gitignore +2 -0
  2. data/README.md +89 -0
  3. data/Rakefile +140 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/admin/messages_controller.rb +20 -0
  6. data/app/controllers/admin/reader_settings_controller.rb +92 -0
  7. data/app/controllers/admin/readers_controller.rb +28 -0
  8. data/app/controllers/password_resets_controller.rb +64 -0
  9. data/app/controllers/reader_action_controller.rb +84 -0
  10. data/app/controllers/reader_activations_controller.rb +60 -0
  11. data/app/controllers/reader_sessions_controller.rb +56 -0
  12. data/app/controllers/readers_controller.rb +131 -0
  13. data/app/helpers/admin/reader_settings_helper.rb +36 -0
  14. data/app/models/message.rb +108 -0
  15. data/app/models/message_function.rb +37 -0
  16. data/app/models/message_reader.rb +13 -0
  17. data/app/models/reader.rb +146 -0
  18. data/app/models/reader_notifier.rb +34 -0
  19. data/app/models/reader_session.rb +3 -0
  20. data/app/views/admin/messages/_form.html.haml +29 -0
  21. data/app/views/admin/messages/_help.html.haml +41 -0
  22. data/app/views/admin/messages/_message_description.html.haml +3 -0
  23. data/app/views/admin/messages/edit.html.haml +16 -0
  24. data/app/views/admin/messages/new.html.haml +16 -0
  25. data/app/views/admin/reader_settings/_setting.html.haml +24 -0
  26. data/app/views/admin/reader_settings/edit.html.haml +10 -0
  27. data/app/views/admin/reader_settings/index.html.haml +35 -0
  28. data/app/views/admin/reader_settings/show.html.haml +1 -0
  29. data/app/views/admin/readers/_avatar.html.haml +3 -0
  30. data/app/views/admin/readers/_form.html.haml +50 -0
  31. data/app/views/admin/readers/_list_head.html.haml +9 -0
  32. data/app/views/admin/readers/_listed.html.haml +22 -0
  33. data/app/views/admin/readers/_password_fields.html.haml +18 -0
  34. data/app/views/admin/readers/edit.html.haml +8 -0
  35. data/app/views/admin/readers/index.html.haml +17 -0
  36. data/app/views/admin/readers/new.html.haml +7 -0
  37. data/app/views/admin/readers/remove.html.haml +18 -0
  38. data/app/views/admin/sites/_choose_reader_layout.html.haml +7 -0
  39. data/app/views/password_resets/create.html.haml +13 -0
  40. data/app/views/password_resets/edit.html.haml +71 -0
  41. data/app/views/password_resets/new.html.haml +31 -0
  42. data/app/views/reader_activations/_activation_required.html.haml +34 -0
  43. data/app/views/reader_activations/_on_activation.html.haml +4 -0
  44. data/app/views/reader_activations/show.html.haml +41 -0
  45. data/app/views/reader_notifier/message.html.haml +1 -0
  46. data/app/views/reader_sessions/_login_form.html.haml +59 -0
  47. data/app/views/reader_sessions/new.html.haml +38 -0
  48. data/app/views/readers/_contributions.html.haml +2 -0
  49. data/app/views/readers/_controls.html.haml +25 -0
  50. data/app/views/readers/_extra_controls.html.haml +0 -0
  51. data/app/views/readers/_flasher.html.haml +6 -0
  52. data/app/views/readers/_form.html.haml +73 -0
  53. data/app/views/readers/create.html.haml +28 -0
  54. data/app/views/readers/edit.html.haml +47 -0
  55. data/app/views/readers/index.html.haml +16 -0
  56. data/app/views/readers/login.html.haml +15 -0
  57. data/app/views/readers/new.html.haml +41 -0
  58. data/app/views/readers/permission_denied.html.haml +23 -0
  59. data/app/views/readers/show.html.haml +35 -0
  60. data/app/views/wrappers/_field_errors.html.haml +5 -0
  61. data/config/routes.rb +22 -0
  62. data/config/settings.rb +9 -0
  63. data/db/migrate/001_create_readers.rb +31 -0
  64. data/db/migrate/002_extend_sites.rb +17 -0
  65. data/db/migrate/003_reader_honorifics.rb +12 -0
  66. data/db/migrate/004_user_readers.rb +11 -0
  67. data/db/migrate/005_last_login.rb +15 -0
  68. data/db/migrate/007_adapt_for_authlogic.rb +27 -0
  69. data/db/migrate/20090921125653_reader_messages.rb +27 -0
  70. data/db/migrate/20090924164413_functional_messages.rb +9 -0
  71. data/db/migrate/20090925081225_standard_messages.rb +106 -0
  72. data/db/migrate/20091006102438_message_visibility.rb +9 -0
  73. data/db/migrate/20091010083503_registration_config.rb +10 -0
  74. data/db/migrate/20091019124021_message_functions.rb +9 -0
  75. data/db/migrate/20091020133533_forenames.rb +9 -0
  76. data/db/migrate/20091020135152_contacts.rb +23 -0
  77. data/db/migrate/20091111090819_ensure_functional_messages_visible.rb +9 -0
  78. data/db/migrate/20091119092936_messages_have_layout.rb +9 -0
  79. data/db/migrate/20100922152338_lock_versions.rb +9 -0
  80. data/db/migrate/20100927095703_default_settings.rb +14 -0
  81. data/db/migrate/20101004074945_unlock_version.rb +9 -0
  82. data/lib/config_extensions.rb +5 -0
  83. data/lib/controller_extensions.rb +77 -0
  84. data/lib/reader_admin_ui.rb +64 -0
  85. data/lib/reader_helper.rb +36 -0
  86. data/lib/reader_site.rb +10 -0
  87. data/lib/reader_tags.rb +297 -0
  88. data/lib/rfc822.rb +29 -0
  89. data/lib/tasks/reader_extension_tasks.rake +28 -0
  90. data/pkg/radiant-reader-extension-0.9.0.gem +0 -0
  91. data/public/images/admin/chk_off.png +0 -0
  92. data/public/images/admin/chk_on.png +0 -0
  93. data/public/images/admin/new-message.png +0 -0
  94. data/public/images/admin/new-reader.png +0 -0
  95. data/public/javascripts/admin/messages.js +13 -0
  96. data/public/stylesheets/sass/admin/reader.sass +95 -0
  97. data/radiant-reader-extension.gemspec +184 -0
  98. data/reader_extension.rb +55 -0
  99. data/spec/controllers/admin/messages_controller_spec.rb +38 -0
  100. data/spec/controllers/admin/readers_controller_spec.rb +14 -0
  101. data/spec/controllers/password_resets_controller_spec.rb +140 -0
  102. data/spec/controllers/reader_activations_controller_spec.rb +45 -0
  103. data/spec/controllers/readers_controller_spec.rb +193 -0
  104. data/spec/datasets/messages_dataset.rb +49 -0
  105. data/spec/datasets/reader_layouts_dataset.rb +26 -0
  106. data/spec/datasets/reader_sites_dataset.rb +10 -0
  107. data/spec/datasets/readers_dataset.rb +51 -0
  108. data/spec/lib/reader_admin_ui_spec.rb +35 -0
  109. data/spec/lib/reader_site_spec.rb +18 -0
  110. data/spec/matchers/reader_login_system_matcher.rb +35 -0
  111. data/spec/models/message_spec.rb +109 -0
  112. data/spec/models/reader_notifier_spec.rb +34 -0
  113. data/spec/models/reader_spec.rb +155 -0
  114. data/spec/spec.opts +5 -0
  115. data/spec/spec_helper.rb +48 -0
  116. metadata +267 -0
@@ -0,0 +1,37 @@
1
+ class MessageFunction
2
+ attr_accessor :name, :description
3
+
4
+ def initialize(name, description='')
5
+ @name = name
6
+ @description = description
7
+ end
8
+
9
+ def symbol
10
+ @name.to_s.downcase.intern
11
+ end
12
+
13
+ def to_s
14
+ @name
15
+ end
16
+
17
+ def self.[](value)
18
+ return if value.blank?
19
+ @@functions.find { |function| function.symbol == value.to_s.downcase.intern }
20
+ end
21
+
22
+ def self.add(name, description='')
23
+ @@functions.push(MessageFunction.new(name, description)) unless MessageFunction[name]
24
+ end
25
+
26
+ def self.find_all
27
+ @@functions.dup
28
+ end
29
+
30
+ @@functions = [
31
+ MessageFunction.new('welcome', 'Welcome'),
32
+ MessageFunction.new('invitation', 'Invitation' ),
33
+ MessageFunction.new('password_reset', 'Password instructions'),
34
+ MessageFunction.new('activation', 'Activation instructions')
35
+ ]
36
+
37
+ end
@@ -0,0 +1,13 @@
1
+ class MessageReader < ActiveRecord::Base
2
+ belongs_to :message
3
+ belongs_to :reader
4
+
5
+ named_scope :undelivered, {
6
+ :conditions => "sent_at IS NULL OR sent_at > NOW()"
7
+ }
8
+
9
+ named_scope :delivered, {
10
+ :conditions => "sent_at IS NOT NULL and sent_at <= NOW()"
11
+ }
12
+
13
+ end
@@ -0,0 +1,146 @@
1
+ require 'authlogic'
2
+ require 'digest/sha1'
3
+ require 'gravtastic'
4
+
5
+ class Reader < ActiveRecord::Base
6
+ @@user_columns = [:name, :email, :login, :created_at, :password, :notes]
7
+ cattr_accessor :user_columns
8
+ cattr_accessor :current
9
+ default_scope :order => 'name ASC'
10
+
11
+ is_site_scoped if respond_to? :is_site_scoped
12
+
13
+ is_gravtastic :with => :email, :rating => 'PG', :size => 48
14
+ acts_as_authentic do |config|
15
+ config.validations_scope = :site_id if defined? Site
16
+ config.transition_from_restful_authentication = true
17
+ config.validate_email_field = false
18
+ config.validate_login_field = false
19
+ end
20
+
21
+ belongs_to :user
22
+ belongs_to :created_by, :class_name => 'User'
23
+ belongs_to :updated_by, :class_name => 'User'
24
+
25
+ has_many :message_readers
26
+ has_many :messages, :through => :message_readers
27
+
28
+ attr_accessor :current_password # used for authentication on update
29
+
30
+ before_save :set_login
31
+ before_update :update_user
32
+
33
+ validates_presence_of :name, :email, :message => 'is required'
34
+ validates_uniqueness_of :login, :message => "is already in use here"
35
+ validate :email_must_not_be_in_use
36
+
37
+ include RFC822
38
+ validates_format_of :email, :with => RFC822_valid, :message => 'appears not to be an email address'
39
+ validates_length_of :name, :maximum => 100, :allow_nil => true
40
+
41
+ named_scope :any
42
+ named_scope :active, :conditions => "activated_at IS NOT NULL"
43
+ named_scope :inactive, :conditions => "activated_at IS NULL"
44
+
45
+ def forename
46
+ read_attribute(:forename) || name.split(/\s/).first
47
+ end
48
+
49
+ def activate!
50
+ self.activated_at = Time.now.utc
51
+ self.save!
52
+ self.send_welcome_message
53
+ end
54
+
55
+ def activated?
56
+ !inactive?
57
+ end
58
+
59
+ def inactive?
60
+ self.activated_at.nil?
61
+ end
62
+
63
+ def disable_perishable_token_maintenance?
64
+ inactive? && !new_record?
65
+ end
66
+
67
+ [:activation, :invitation, :welcome, :password_reset].each do |function|
68
+ define_method("send_#{function}_message".intern) {
69
+ send_functional_message(function)
70
+ }
71
+ end
72
+
73
+ def send_functional_message(function)
74
+ reset_perishable_token!
75
+ message = Message.functional(function)
76
+ raise StandardError, "No #{function} message could be found" unless message
77
+ message.deliver_to(self)
78
+ end
79
+
80
+ def generate_email_field_name
81
+ generate_password(32)
82
+ end
83
+
84
+ def generate_password(length=12)
85
+ chars = ("a".."z").to_a + ("A".."Z").to_a + ("1".."9").to_a
86
+ Array.new(length, '').collect{chars[rand(chars.size)]}.join
87
+ end
88
+
89
+ def is_user?
90
+ self.user ? true : false
91
+ end
92
+
93
+ def is_admin?
94
+ self.user && self.user.admin? ? true : false
95
+ end
96
+
97
+ def self.find_or_create_for_user(user)
98
+ if user.respond_to?(:site) && site = Page.current_site
99
+ reader = self.find_or_create_by_site_id_and_user_id(site.id, user.id)
100
+ else
101
+ reader = self.find_or_create_by_user_id(user.id)
102
+ end
103
+ if reader.new_record?
104
+ user_columns.each { |att| reader.send("#{att.to_s}=", user.send(att)) }
105
+ reader.crypted_password = user.password
106
+ reader.password_salt = user.salt
107
+ reader.activated_at = reader.created_at
108
+ reader.save(false)
109
+ end
110
+ reader
111
+ end
112
+
113
+ protected
114
+
115
+ def set_login
116
+ self.login = self.email if self.login.blank?
117
+ end
118
+
119
+ private
120
+
121
+ def email_must_not_be_in_use
122
+ reader = Reader.find_by_email(self.email) # the finds will be site-scoped if appropriate
123
+ user = User.find_by_email(self.email)
124
+ if user && user != self.user
125
+ errors.add(:email, "belongs to an author here")
126
+ elsif reader && reader != self
127
+ errors.add(:email, "is already registered here")
128
+ else
129
+ return true
130
+ end
131
+ return false
132
+ end
133
+
134
+ def validate_length_of_password?
135
+ new_record? or not password.to_s.empty?
136
+ end
137
+
138
+ def update_user
139
+ if self.user
140
+ user_columns.each { |att| self.user.send("#{att.to_s}=", send(att)) if send("#{att.to_s}_changed?") }
141
+ self.user.password_confirmation = password_confirmation if password_changed?
142
+ self.user.save! if self.user.changed?
143
+ end
144
+ end
145
+
146
+ end
@@ -0,0 +1,34 @@
1
+ class ReaderNotifier < ActionMailer::Base
2
+
3
+ # this sets a default that will be overridden by the layout association of each message as it is sent out
4
+ radiant_layout "email"
5
+
6
+ def message(reader, message, sender=nil)
7
+ site = reader.site if reader.respond_to?(:site)
8
+ prefix = site ? site.abbreviation : Radiant::Config['site.mail_prefix']
9
+ host = site ? site.base_domain : Radiant::Config['site.url'] || 'www.example.com'
10
+ default_url_options[:host] = host
11
+ sender ||= message.created_by
12
+
13
+ message_layout(message.layout) if message.layout
14
+ content_type("text/html")
15
+ subject (prefix || '') + message.subject
16
+ recipients(reader.email)
17
+ from message.created_by.email
18
+ subject message.subject
19
+ sent_on(Time.now)
20
+
21
+ body({
22
+ :host => host,
23
+ :title => message.subject,
24
+ :message => message.filtered_body,
25
+ :sender => sender,
26
+ :reader => reader,
27
+ :site => site || {
28
+ :name => Radiant::Config['site.name'],
29
+ :url => Radiant::Config['site.url']
30
+ }
31
+ })
32
+ end
33
+
34
+ end
@@ -0,0 +1,3 @@
1
+ class ReaderSession < Authlogic::Session::Base
2
+
3
+ end
@@ -0,0 +1,29 @@
1
+ = render_region :form_top
2
+
3
+ .form-area
4
+ - render_region :form do |formpart|
5
+ - formpart.edit_subject do
6
+ %p.title
7
+ = form.label :subject, "Message Subject"
8
+ = form.text_field :subject, :class => 'textbox', :maxlength => 255
9
+ - if @message.new_record?
10
+ = form.hidden_field :function_id
11
+
12
+ - formpart.edit_body do
13
+ %div.body
14
+ %p
15
+ %span.filter
16
+ = form.label :filter_id, "Filter:"
17
+ = form.select :filter_id, TextFilter.descendants.map { |tf| tf.filter_name }.sort, :id => 'message_filter', :include_blank => true
18
+ = form.label :body, "Message Body"
19
+ = form.text_area :body, :class => 'textarea', :style => 'width: 100%'
20
+
21
+ - render_region :form_bottom do |form_bottom|
22
+ - form_bottom.edit_timestamp do
23
+ = updated_stamp @message
24
+ - form_bottom.edit_buttons do
25
+ %p.buttons
26
+ = save_model_button(@message)
27
+ = save_model_and_continue_editing_button(@message)
28
+ or
29
+ = link_to "cancel", admin_messages_url
@@ -0,0 +1,41 @@
1
+ %p
2
+ You can use tags, snippets and assets in your messages just as you would in a page.
3
+ There are also a few simple mailmerge tags that you can use to place information about the recipient (or yourself) in each message as it is sent out.
4
+ Most are self-explanatory:
5
+ %ul.help
6
+ %li
7
+ %code &lt;r:sender:name /&gt;
8
+ is the name configured in
9
+ %em
10
+ reader.mail_from_name:
11
+ currently
12
+ %strong
13
+ = "'#{Radiant::Config['reader.mail_from_name']}'."
14
+ %li
15
+ %code &lt;r:sender:email /&gt;
16
+ is the address configured in
17
+ %em
18
+ reader.mail_from_address:
19
+ currently
20
+ %strong
21
+ = "'#{Radiant::Config['reader.mail_from_address']}'."
22
+ %li
23
+ %code &lt;r:recipient:name /&gt;
24
+ %li
25
+ %code &lt;r:recipient:email /&gt;
26
+ %li
27
+ %code &lt;r:recipient:login /&gt;
28
+ %li
29
+ %code &lt;r:recipient:password /&gt;
30
+ only works in welcome messages: after first login we forget the unencrypted password
31
+ %li
32
+ %code &lt;r:recipient:url /&gt;
33
+ is the address of the page about this person
34
+ %li
35
+ %code &lt;r:recipient:edit_url /&gt;
36
+ is the address of the preferences form
37
+ %li
38
+ %code &lt;r:recipient:activation_url /&gt;
39
+ is the address that will activate a newly-created account
40
+ %li
41
+ %code &lt;r:recipient:description /&gt;
@@ -0,0 +1,3 @@
1
+ - if @message.function
2
+ = @message.function
3
+ message
@@ -0,0 +1,16 @@
1
+ - include_stylesheet('admin/reader')
2
+
3
+ = render_region :top
4
+ - render_region :main do |main|
5
+
6
+ - main.edit_header do
7
+ %h1
8
+ Edit
9
+ = render :partial => 'message_description'
10
+
11
+ - main.edit_form do
12
+ - form_for :message, @message, :url => admin_message_url(@message), :html => {:id => 'message_form', :method => 'put'} do |f|
13
+ = render :partial => 'form', :object => f
14
+
15
+ - main.edit_footer do
16
+ = render :partial => 'help'
@@ -0,0 +1,16 @@
1
+ - include_stylesheet('admin/reader')
2
+
3
+ = render_region :top
4
+ - render_region :main do |main|
5
+
6
+ - main.edit_header do
7
+ %h1
8
+ Create
9
+ = render :partial => 'message_description'
10
+
11
+ - main.edit_form do
12
+ - form_for :message, @message, :url => admin_messages_url, :html => {:id => 'message_form', :method => 'post'} do |f|
13
+ = render :partial => 'form', :object => f
14
+
15
+ - main.edit_footer do
16
+ = render :partial => 'help'
@@ -0,0 +1,24 @@
1
+ - if setting || key
2
+ - setting ||= Radiant::Config.find_by_key(key)
3
+ - if setting
4
+ - label ||= setting.key
5
+ - notes ||= nil
6
+ - domkey = setting.key.gsub('?', '_')
7
+ - containerid = "set_#{domkey}"
8
+
9
+ - if setting.key.ends_with?("?")
10
+ %p.ruled
11
+ %span{:id => containerid, :class => "checkbox #{setting.value.to_s}"}
12
+ = checkbox_for_setting(setting, label)
13
+
14
+ - else
15
+ %p.ruled
16
+ %label= label
17
+ %span.inplace{:id => containerid}
18
+ = editable_setting(setting)
19
+
20
+ - else
21
+ %p.haserror
22
+ %strong
23
+ = key
24
+ is not defined.
@@ -0,0 +1,10 @@
1
+ - domkey = @setting.key.gsub('?', '_')
2
+ - domid = "set_#{domkey}"
3
+
4
+ - remote_form_for @setting, :url => admin_reader_setting_path(@setting), :update => domid, :loading => "$('#{domid}').addClassName('waiting');", :loaded => "$('#{domid}').removeClassName('waiting');", :html => { :method => :put } do |f|
5
+ - if @setting.key =~ /\.layout$/
6
+ = f.select :value, Layout.all.collect {|l| [ l.name, l.name ] }
7
+ - else
8
+ = f.text_field :value
9
+ = f.submit 'save'
10
+ = link_to_remote 'cancel', {:url => admin_reader_setting_url(@setting.id), :method => 'get', :update => domid, :loading => "$('#{domid}').addClassName('waiting');", :loaded => "$('#{domid}').removeClassName('waiting');"}, {:class => 'cancel'}
@@ -0,0 +1,35 @@
1
+ - body_classes << "reversed"
2
+ - include_stylesheet('admin/reader')
3
+
4
+ #reader_settings.box
5
+ %h3 Reader administration
6
+
7
+ - render_region :settings do |settings|
8
+ - settings.registration do
9
+ = render :partial => 'setting', :locals => {:key => 'reader.allow_registration?', :label => 'Allow visitors to register'}
10
+ = render :partial => 'setting', :locals => {:key => 'reader.require_confirmation?', :label => 'Require email confirmation'}
11
+ = render :partial => 'setting', :locals => {:key => 'reader.layout', :label => "Admin layout", :notes => "The radiant layout used to present reader-administration pages"}
12
+
13
+ - settings.site do
14
+ = render :partial => 'setting', :locals => {:key => 'site.title', :label => "Site title", :notes => "This is the name by which forms and emails refer to your site"}
15
+ = render :partial => 'setting', :locals => {:key => 'site.url', :label => "Site url", :notes => "This is the address we use to build links in emails"}
16
+
17
+ - settings.sender do
18
+ = render :partial => 'setting', :locals => {:key => 'reader.mail_from_name', :label => "Email sender", :notes => "This is the name that automatic emails seem to come from"}
19
+ = render :partial => 'setting', :locals => {:key => 'reader.mail_from_address', :label => "Email address", :notes => "This is the address that automatic emails seem to come from"}
20
+
21
+ #message_settings.box
22
+ %h3 Administrative messages
23
+
24
+ - render_region :messages do |messages|
25
+ - messages.administration do
26
+ - MessageFunction.find_all.each do |func|
27
+ - message = Message.for_function(func).shift
28
+ %p.ruled
29
+ %label= func.description
30
+ - if message
31
+ = link_to message.subject, edit_admin_message_url(message)
32
+ - else
33
+ = link_to image('plus') + " create message", new_admin_message_url(:function => func), :class => 'create'
34
+
35
+
@@ -0,0 +1 @@
1
+ = editable_setting(@setting)
@@ -0,0 +1,3 @@
1
+ #avatar
2
+ = image_tag(gravatar_url(@reader.email, :size=>"96px"), :class=>"avatar", :width=>96, :height=>96, :alt=>"")
3
+ %p.caption Avatar provided by Gravatar.com
@@ -0,0 +1,50 @@
1
+ - form_for [:admin, @reader] do |f|
2
+ = hidden_field "reader", "lock_version"
3
+
4
+ = render :partial => 'avatar' unless @reader.new_record?
5
+
6
+ = render_region :form_top, :locals => {:f => f}
7
+
8
+ - render_region :form, :locals => {:f => f} do |form|
9
+ - form.edit_name do
10
+ %p
11
+ = f.label :name, t('name')
12
+ = f.text_field :name, :class => "textbox activate", :size => 32, :maxlength => 100
13
+
14
+ - form.edit_email do
15
+ %p
16
+ = f.label :email, t('email_address') , :class => "optional"
17
+ = f.text_field "email", :class => 'textbox', :size => 32, :maxlength => 255
18
+
19
+ - form.edit_username do
20
+ %p
21
+ = f.label :login, t('username') , :class => "optional"
22
+ = f.text_field "login", :class => 'textbox', :size => 32, :maxlength => 255
23
+
24
+ - form.edit_readername do
25
+ %p
26
+ = f.label :login, t('readername')
27
+ = f.text_field "login", :class => "textbox", :size => 32, :maxlength => 40
28
+
29
+ - form.edit_password do
30
+ = render "password_fields", :f => f
31
+
32
+ - form.edit_description do
33
+ %p
34
+ = f.label :description, "Self-description", :class => "optional"
35
+ ~ f.text_area "description", :size => "53x8", :class => "textarea"
36
+
37
+ - form.edit_notes do
38
+ %p
39
+ = f.label :notes, t('notes'), :class => "optional"
40
+ ~ f.text_area "notes", :size => "53x4", :class => "textarea"
41
+
42
+ - render_region :form_bottom, :locals => {:f => f} do |form_bottom|
43
+ - form_bottom.edit_buttons do
44
+ %p.buttons
45
+ = save_model_button(@reader)
46
+ = save_model_and_continue_editing_button(@reader)
47
+ = t('or')
48
+ = link_to t('cancel'), admin_readers_path
49
+ - form_bottom.edit_timestamp do
50
+ = updated_stamp @reader