radiant-reader-extension 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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