cortex-reaver 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. data/LICENSE +25 -0
  2. data/README +48 -0
  3. data/bin/console +11 -0
  4. data/bin/cortex_reaver +110 -0
  5. data/bin/import.rb +78 -0
  6. data/lib/cortex_reaver.rb +114 -0
  7. data/lib/cortex_reaver/config.rb +21 -0
  8. data/lib/cortex_reaver/controller/comment.rb +113 -0
  9. data/lib/cortex_reaver/controller/config.rb +14 -0
  10. data/lib/cortex_reaver/controller/journal.rb +40 -0
  11. data/lib/cortex_reaver/controller/main.rb +56 -0
  12. data/lib/cortex_reaver/controller/page.rb +34 -0
  13. data/lib/cortex_reaver/controller/photograph.rb +45 -0
  14. data/lib/cortex_reaver/controller/project.rb +40 -0
  15. data/lib/cortex_reaver/controller/tag.rb +67 -0
  16. data/lib/cortex_reaver/controller/user.rb +71 -0
  17. data/lib/cortex_reaver/helper/activity.rb +9 -0
  18. data/lib/cortex_reaver/helper/attachments.rb +139 -0
  19. data/lib/cortex_reaver/helper/auth.rb +45 -0
  20. data/lib/cortex_reaver/helper/canonical.rb +55 -0
  21. data/lib/cortex_reaver/helper/crud.rb +317 -0
  22. data/lib/cortex_reaver/helper/date.rb +29 -0
  23. data/lib/cortex_reaver/helper/error.rb +14 -0
  24. data/lib/cortex_reaver/helper/feeds.rb +88 -0
  25. data/lib/cortex_reaver/helper/form.rb +114 -0
  26. data/lib/cortex_reaver/helper/navigation.rb +142 -0
  27. data/lib/cortex_reaver/helper/photographs.rb +25 -0
  28. data/lib/cortex_reaver/helper/tags.rb +47 -0
  29. data/lib/cortex_reaver/helper/workflow.rb +27 -0
  30. data/lib/cortex_reaver/migrations/001_users.rb +24 -0
  31. data/lib/cortex_reaver/migrations/002_pages.rb +29 -0
  32. data/lib/cortex_reaver/migrations/003_journals.rb +26 -0
  33. data/lib/cortex_reaver/migrations/004_photographs.rb +24 -0
  34. data/lib/cortex_reaver/migrations/005_projects.rb +27 -0
  35. data/lib/cortex_reaver/migrations/006_tags.rb +64 -0
  36. data/lib/cortex_reaver/migrations/007_comments.rb +40 -0
  37. data/lib/cortex_reaver/migrations/008_config.rb +23 -0
  38. data/lib/cortex_reaver/model/comment.rb +109 -0
  39. data/lib/cortex_reaver/model/journal.rb +53 -0
  40. data/lib/cortex_reaver/model/page.rb +87 -0
  41. data/lib/cortex_reaver/model/photograph.rb +133 -0
  42. data/lib/cortex_reaver/model/project.rb +49 -0
  43. data/lib/cortex_reaver/model/tag.rb +72 -0
  44. data/lib/cortex_reaver/model/user.rb +147 -0
  45. data/lib/cortex_reaver/public/css/admin.css +45 -0
  46. data/lib/cortex_reaver/public/css/custom.css +0 -0
  47. data/lib/cortex_reaver/public/css/form.css +51 -0
  48. data/lib/cortex_reaver/public/css/main.css +325 -0
  49. data/lib/cortex_reaver/public/css/photo.css +113 -0
  50. data/lib/cortex_reaver/public/css/ramaze_error.css +90 -0
  51. data/lib/cortex_reaver/public/css/text.css +25 -0
  52. data/lib/cortex_reaver/public/dispatch.fcgi +11 -0
  53. data/lib/cortex_reaver/public/images/CortexReaver.gif +0 -0
  54. data/lib/cortex_reaver/public/images/atom-xml-icon.png +0 -0
  55. data/lib/cortex_reaver/public/images/body.png +0 -0
  56. data/lib/cortex_reaver/public/images/border_bottom.png +0 -0
  57. data/lib/cortex_reaver/public/images/border_bottom_left.png +0 -0
  58. data/lib/cortex_reaver/public/images/border_bottom_right.png +0 -0
  59. data/lib/cortex_reaver/public/images/border_left.png +0 -0
  60. data/lib/cortex_reaver/public/images/border_right.png +0 -0
  61. data/lib/cortex_reaver/public/images/border_top.png +0 -0
  62. data/lib/cortex_reaver/public/images/border_top_left.png +0 -0
  63. data/lib/cortex_reaver/public/images/border_top_right.png +0 -0
  64. data/lib/cortex_reaver/public/images/comment.gif +0 -0
  65. data/lib/cortex_reaver/public/images/dark_trans.png +0 -0
  66. data/lib/cortex_reaver/public/images/delete.gif +0 -0
  67. data/lib/cortex_reaver/public/images/edit.gif +0 -0
  68. data/lib/cortex_reaver/public/images/header.png +0 -0
  69. data/lib/cortex_reaver/public/images/header.xcf +0 -0
  70. data/lib/cortex_reaver/public/images/header_background.png +0 -0
  71. data/lib/cortex_reaver/public/images/parent.gif +0 -0
  72. data/lib/cortex_reaver/public/images/rss-xml-icon.png +0 -0
  73. data/lib/cortex_reaver/public/images/sections.png +0 -0
  74. data/lib/cortex_reaver/public/images/sections_highlight.png +0 -0
  75. data/lib/cortex_reaver/public/js/admin.js +36 -0
  76. data/lib/cortex_reaver/public/js/cookie.js +27 -0
  77. data/lib/cortex_reaver/public/js/jquery.js +32 -0
  78. data/lib/cortex_reaver/public/js/photo.js +33 -0
  79. data/lib/cortex_reaver/snippets/array.rb +7 -0
  80. data/lib/cortex_reaver/snippets/ramaze/dispatcher/file.rb +37 -0
  81. data/lib/cortex_reaver/support/attachments.rb +235 -0
  82. data/lib/cortex_reaver/support/cached_rendering.rb +79 -0
  83. data/lib/cortex_reaver/support/canonical.rb +107 -0
  84. data/lib/cortex_reaver/support/comments.rb +69 -0
  85. data/lib/cortex_reaver/support/pagination.rb +38 -0
  86. data/lib/cortex_reaver/support/renderer.rb +196 -0
  87. data/lib/cortex_reaver/support/sequenceable.rb +248 -0
  88. data/lib/cortex_reaver/support/tags.rb +108 -0
  89. data/lib/cortex_reaver/support/timestamps.rb +33 -0
  90. data/lib/cortex_reaver/version.rb +8 -0
  91. data/lib/cortex_reaver/view/adminbox.rhtml +56 -0
  92. data/lib/cortex_reaver/view/blank_layout.rhtml +46 -0
  93. data/lib/cortex_reaver/view/comments/comment.rhtml +34 -0
  94. data/lib/cortex_reaver/view/comments/form.rhtml +25 -0
  95. data/lib/cortex_reaver/view/comments/list.rhtml +5 -0
  96. data/lib/cortex_reaver/view/comments/post_form.rhtml +36 -0
  97. data/lib/cortex_reaver/view/config/form.rhtml +10 -0
  98. data/lib/cortex_reaver/view/error.rhtml +72 -0
  99. data/lib/cortex_reaver/view/journals/form.rhtml +12 -0
  100. data/lib/cortex_reaver/view/journals/journal.rhtml +39 -0
  101. data/lib/cortex_reaver/view/journals/list.rhtml +33 -0
  102. data/lib/cortex_reaver/view/journals/short.rhtml +3 -0
  103. data/lib/cortex_reaver/view/journals/show.rhtml +5 -0
  104. data/lib/cortex_reaver/view/pages/form.rhtml +12 -0
  105. data/lib/cortex_reaver/view/pages/list.rhtml +26 -0
  106. data/lib/cortex_reaver/view/pages/show.rhtml +12 -0
  107. data/lib/cortex_reaver/view/photographs/atom_fragment.rhtml +82 -0
  108. data/lib/cortex_reaver/view/photographs/form.rhtml +19 -0
  109. data/lib/cortex_reaver/view/photographs/grid.rhtml +36 -0
  110. data/lib/cortex_reaver/view/photographs/list.rhtml +9 -0
  111. data/lib/cortex_reaver/view/photographs/short.rhtml +3 -0
  112. data/lib/cortex_reaver/view/photographs/show.rhtml +114 -0
  113. data/lib/cortex_reaver/view/photographs/sidebar.rhtml +7 -0
  114. data/lib/cortex_reaver/view/projects/form.rhtml +13 -0
  115. data/lib/cortex_reaver/view/projects/list.rhtml +27 -0
  116. data/lib/cortex_reaver/view/projects/show.rhtml +38 -0
  117. data/lib/cortex_reaver/view/tags/form.rhtml +9 -0
  118. data/lib/cortex_reaver/view/tags/list.rhtml +28 -0
  119. data/lib/cortex_reaver/view/tags/show.rhtml +51 -0
  120. data/lib/cortex_reaver/view/text_layout.rhtml +78 -0
  121. data/lib/cortex_reaver/view/users/form.rhtml +16 -0
  122. data/lib/cortex_reaver/view/users/list.rhtml +38 -0
  123. data/lib/cortex_reaver/view/users/login.rhtml +13 -0
  124. data/lib/cortex_reaver/view/users/register.rhtml +13 -0
  125. data/lib/cortex_reaver/view/users/show.rhtml +37 -0
  126. data/lib/cortex_reaver/view/users/user.rhtml +35 -0
  127. data/lib/proto/cortex_reaver.yaml +28 -0
  128. metadata +285 -0
@@ -0,0 +1,248 @@
1
+ module CortexReaver
2
+ module Model
3
+ # Sequences are models that have order. This module provides class
4
+ # and instance methods that support sequential behavior like pagination, finding
5
+ # the next or previoud record, and finding a "window" of results around a
6
+ # specific record.
7
+
8
+ module Sequenceable
9
+
10
+ # Which column table to order this sequence by
11
+ DEFAULT_SEQUENCE_ORDER = :created_on
12
+ DEFAULT_SEQUENCE_REVERSE = false
13
+ DEFAULT_WINDOW_SIZE = 16
14
+
15
+ # Class methods
16
+ def self.included(base)
17
+ base.instance_eval do
18
+ attr_writer :sequence_order, :sequence_reverse, :window_size
19
+
20
+ # Returns the sequence dataset (optionally, restricted to dataset)
21
+ def sequence(dataset = self.dataset)
22
+ if sequence_reverse
23
+ dataset.reverse_order(sequence_order)
24
+ else
25
+ dataset.order(sequence_order)
26
+ end
27
+ end
28
+
29
+ # Returns the first record in the sequence
30
+ def first
31
+ sequence.first
32
+ end
33
+
34
+ # Returns the last record in the sequence
35
+ def last
36
+ sequence.last
37
+ end
38
+
39
+ # Returns the table column to order by
40
+ def sequence_order
41
+ @sequence_order || DEFAULT_SEQUENCE_ORDER
42
+ end
43
+
44
+ # Sets the table column to order by
45
+ def sequence_order=(order)
46
+ @sequence_order = order
47
+ end
48
+
49
+ # Returns whether or not to reverse the order
50
+ def sequence_reverse
51
+ @sequence_reverse || DEFAULT_SEQUENCE_REVERSE
52
+ end
53
+
54
+ # Sets whether to reverse the sequence
55
+ def sequence_reverse=(reverse)
56
+ @sequence_reverse = reverse
57
+ end
58
+
59
+ # Returns an absolute window into this sequence, specified as an offset in
60
+ # window-sized increments from the beginning of the sequence. Hence
61
+ # window(0, 10) returns the first ten records in the sequence, window(1, 10)
62
+ # the next ten, and so on.
63
+ #
64
+ # One can also specify the special page_offsets :first or :last, which
65
+ # return the first and last available windows.
66
+ def window(page_offset, size = self.window_size)
67
+ case page_offset
68
+ when :first
69
+ page_offset = 0
70
+ when :last
71
+ page_offset = window_count(size) - 1
72
+ else
73
+ page_offset = page_offset.to_i
74
+ end
75
+
76
+ # Don't ask for negative pages!
77
+ page_offset = 0 if page_offset < 0
78
+
79
+ # Calculate offset
80
+ offset = page_offset * size
81
+
82
+ # Limit dataset
83
+ sequence.limit size, offset
84
+ end
85
+
86
+ # Returns the number of windows required to span this sequence
87
+ def window_count(size = self.window_size)
88
+ (Float(sequence.count) / size).ceil
89
+ end
90
+
91
+ # Returns the size of a window into this sequence
92
+ def window_size
93
+ @window_size || DEFAULT_WINDOW_SIZE
94
+ end
95
+
96
+ # Sets the size of a window into this sequence
97
+ def window_size=(size)
98
+ @window_size = size
99
+ end
100
+ end
101
+ end
102
+
103
+ # Instance methods
104
+
105
+ # Convenience references to class methods
106
+ def sequence
107
+ self.class.sequence
108
+ end
109
+
110
+ def sequence_order
111
+ self.class.sequence_order
112
+ end
113
+
114
+ def sequence_reverse
115
+ self.class.sequence_reverse
116
+ end
117
+
118
+ # Returns the next record in the sequence. Caches--use next(true) to refresh.
119
+ def next(refresh_cache = false)
120
+ if refresh_cache
121
+ @next = sequence.filter(sequence_order > send(sequence_order)).limit(1).first
122
+ else
123
+ @next ||= sequence.filter(sequence_order > send(sequence_order)).limit(1).first
124
+ end
125
+ end
126
+
127
+ # Returns true if a next record exists. Caches--use next(true) to refresh.
128
+ def next?(refresh_cache = false)
129
+ if refresh_cache
130
+ @next_exists ||= self.next_count > 0 ? true : false
131
+ else
132
+ @next_exists = self.next_count > 0 ? true : false
133
+ end
134
+ end
135
+
136
+ # Returns the number of succeeding records
137
+ def next_count
138
+ sequence.filter(sequence_order > send(sequence_order)).count
139
+ end
140
+
141
+ # Returns the previous record in the sequence. Caches--use previous(true) to
142
+ # refresh.
143
+ def previous(refresh_cache = false)
144
+ if refresh_cache
145
+ @previous = sequence.filter(sequence_order < send(sequence_order)).reverse.limit(1).first
146
+ else
147
+ @previous ||= sequence.filter(sequence_order < send(sequence_order)).reverse.limit(1).first
148
+ end
149
+ end
150
+
151
+ # Returns true if a previous record exists. Caches--use previous?(true) to
152
+ # refresh.
153
+ def previous?(refresh_cache = false)
154
+ if refresh_cache
155
+ @previous_exists = self.previous_count > 0 ? true : false
156
+ else
157
+ @previous_exists ||= self.previous_count > 0 ? true : false
158
+ end
159
+ end
160
+
161
+ # Returns the number of preceding records
162
+ def previous_count
163
+ sequence.filter(sequence_order < send(sequence_order)).count
164
+ end
165
+
166
+ alias :position :previous_count
167
+
168
+ # Returns a collection of records around this record.
169
+ # Mode is one of:
170
+ # 1. :absolute Returns a window containing this record in even multiples from
171
+ # the first record, like a page of a book.
172
+ # 2. :float Returns a centered window of the provided size. If the edge of
173
+ # the sequence is reached, the window shifts to include more
174
+ # elements of the sequence. If no more elements are available
175
+ # (the window is larger than the size of the sequence), the
176
+ # entire sequence is returned. If the size is even, one more
177
+ # record is returned suceeding this record than preceding this
178
+ # record, if possible.
179
+ # 3. :clip Returns a centered window of or smaller than the given size. If
180
+ # the edge of the sequence is reached, the window is clipped.
181
+ def window(mode = :absolute, size = self.class.window_size)
182
+ case mode
183
+ when :absolute
184
+ # Calculate page offset
185
+ offset = (Float(position) / size).floor * size
186
+
187
+ # Find records
188
+ sequence.limit(size, offset)
189
+ when :float
190
+ count = sequence.count # Total records
191
+ if size >= count
192
+ # Window includes all records in the sequence
193
+ return self.class.find(:all)
194
+ else
195
+ # Window is smaller than the sequence
196
+ position = self.position # This record's position
197
+ previous_size = (Float(size - 1) / 2).floor # How many records before
198
+ next_size = (Float(size - 1) / 2).ceil # How many records after
199
+
200
+ # Shift window if necessary
201
+ if (displacement = previous_size - position) > 0
202
+ # The window extends before the start of the sequence
203
+ previous_size -= displacement
204
+ elsif (displacement = next_size - self.next_count) > 0
205
+ # The window extends beyond the end of the sequence
206
+ previous_size += displacement
207
+ end
208
+
209
+ # Calculate window offset
210
+ offset = position - previous_size
211
+
212
+ # Find records
213
+ sequence.limit(size, offset)
214
+ end
215
+ when :clip
216
+ position = self.position # Our position in sequence
217
+ previous_size = (Float(size - 1) / 2).floor # How many records before
218
+
219
+ if (displacement = previous_size - position) > 0
220
+ # The window extends before the beginning of the sequence
221
+ size -= displacement
222
+ offset = 0
223
+ else
224
+ # The window doesn't extend before the beginning of the sequence.
225
+ offset = position - previous_size
226
+ end
227
+
228
+ # Find records.
229
+ sequence.limit(size, offset)
230
+ else
231
+ raise ArgumentError.new('first argument must be one of :absolute, :float, :clip')
232
+ end
233
+ end
234
+
235
+ # Returns a url for this record, viewed in an absolute window. TODO: handle non-default window sizes.
236
+ def absolute_window_url(size = self.class.window_size)
237
+ page = (Float(position) / size).floor
238
+ self.class.url + '/page/' + page.to_s + '#' + self.class.to_s.underscore +
239
+ '_' + send(self.class.canonical_name_attr)
240
+ end
241
+
242
+ # Returns the absolute window index that this record appears in
243
+ def absolute_window_index(size = self.class.window_size)
244
+ (Float(self.position) / size).floor
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,108 @@
1
+ module CortexReaver
2
+ module Model
3
+ module Tags
4
+ # Support for taggable models
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ # If tags have changed, make sure to update those tags' counts.
9
+ after_save(:update_tag_counts) do
10
+ if @added_tags
11
+ @added_tags.each do |tag|
12
+ tag.count += 1
13
+ tag.save
14
+ end
15
+ @removed_tags.each do |tag|
16
+ tag.count -= 1
17
+ tag.save
18
+ end
19
+ end
20
+ end
21
+
22
+ # Remove tags on deletion
23
+ before_destroy(:drop_associated_tags) do
24
+ tags.each do |tag|
25
+ tag.count -= 1
26
+ if tag.count == 0
27
+ tag.destroy
28
+ else
29
+ tag.save
30
+ end
31
+ end
32
+ remove_all_tags
33
+ end
34
+
35
+ # Returns all models with ALL the following tags:
36
+ def self.tagged_with(tags)
37
+ # Find map between this model and tags, e.g. pages_tags
38
+ map = [table_name.to_s, 'tags'].sort.join('_').to_sym
39
+
40
+ # The name in the mapping table which points to us
41
+ own_id = (table_name.to_s.singularize + '_id').to_sym
42
+
43
+ # The tag IDs to search for
44
+ ids = tags.map { |t| t.id }
45
+
46
+ # Now filter this model, finding all ids which appear n times with
47
+ # the same model_id in the mapping table, which has been filtered
48
+ # to contain only rows with one of our interesting tags.
49
+ #
50
+ # Man, don't you wish MySQL had intersect?
51
+ filter(:id =>
52
+ CortexReaver.db[map].filter(:tag_id => ids).group(own_id).having(
53
+ "count(*) = #{ids.size}"
54
+ ).select(own_id)
55
+ )
56
+ end
57
+ end
58
+ end
59
+
60
+ # Set tags from a string, or array of tags. Finds existing tags or
61
+ # creates new ones. Also updates tag counts.
62
+ def tags=(tags)
63
+ if tags.kind_of? String
64
+ tags = tags.squeeze(',').split(',').map do |title|
65
+ title.strip!
66
+ if title.blank?
67
+ # Do nothing
68
+ tag = nil
69
+ else
70
+ # Look up existing tag
71
+ tag = Tag[:title => title]
72
+ # Or create new one
73
+ tag ||= Tag.create(:title => title, :name => Tag.canonicalize(title))
74
+ unless tag
75
+ # Everything failed
76
+ raise RuntimeError.new("couldn't find or create tag for #{title}")
77
+ end
78
+ end
79
+ tag
80
+ end
81
+ end
82
+
83
+ unless tags.respond_to? :each
84
+ raise ArgumentError.new("Needed a string or Enumerable of Tags, got #{tags.class}")
85
+ end
86
+
87
+ # Get rid of empty tags
88
+ tags.reject do |tag|
89
+ tag.nil?
90
+ end
91
+ tags.uniq!
92
+
93
+ # Find which tags to change. Their counts are updated by an after_save
94
+ # callback.
95
+ old_tags = self.tags
96
+ @removed_tags = old_tags - tags
97
+ @added_tags = tags - old_tags
98
+
99
+ # Change own tags
100
+ remove_all_tags
101
+ tags.each do |tag|
102
+ add_tag tag
103
+ end
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,33 @@
1
+ module CortexReaver
2
+ module Model
3
+ # Supports updated_on and created_on behavior
4
+ module Timestamps
5
+ def self.included(base)
6
+ base.class_eval do
7
+ # Create
8
+ before_create(:init_timestamp) do
9
+ unless skip_timestamp_update?
10
+ self.created_on = Time.now
11
+ self.updated_on = Time.now
12
+ end
13
+ end
14
+
15
+ # Update
16
+ before_update(:update_timestamp) do
17
+ unless skip_timestamp_update?
18
+ self.updated_on = Time.now
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def skip_timestamp_update=(boolean)
25
+ @skip_timestamp_update = boolean
26
+ end
27
+
28
+ def skip_timestamp_update?
29
+ @skip_timestamp_update
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ module CortexReaver
2
+ APP_NAME = 'Cortex Reaver'
3
+ APP_VERSION = '0.0.1'
4
+ APP_AUTHOR = 'aphyr'
5
+ APP_EMAIL = 'aphyr@aphyr.com'
6
+ APP_URL = 'http://aphyr.com'
7
+ APP_COPYRIGHT = 'Copyright (c) 2008 aphyr <aphyr@aphyr.com>. All rights reserved.'
8
+ end
@@ -0,0 +1,56 @@
1
+ <div id="adminbox">
2
+ <% if admin? %>
3
+ <table class="actions-table">
4
+ <tr>
5
+ <td class="title">Sections</td>
6
+ <td>
7
+ <ul class="actions">
8
+ <li><a href="/pages">Pages</a></li>
9
+ <li><a href="/projects">Projects</a></li>
10
+ <li><a href="/journals">Journals</a></li>
11
+ <li><a href="/photographs">Photographs</a></li>
12
+ <li><a href="/comments">Comments</a></li>
13
+ <li><a href="/tags">Tags</a></li>
14
+ <li><a href="/users">Users</a></li>
15
+ <li><a href="/config">Config</a></li>
16
+ </ul>
17
+ </td>
18
+ </tr>
19
+ <% if workflows %>
20
+ <tr>
21
+ <td class="title">Actions</td>
22
+ <td>
23
+ <ul class="actions">
24
+ <% workflows.each do |name, href| %>
25
+ <li>
26
+ <a href="<%= href %>"
27
+ <%= name.to_s[/delete|destroy/i] ? "onclick = \"return confirm('Are you sure you want to delete #{@title.gsub(/'"/, '')}?');\"" : '' %>>
28
+ <%= name %>
29
+ </a>
30
+ </li>
31
+ <% end %>
32
+ </ul>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </table>
37
+
38
+ <div id="admin_user_actions">
39
+ <a href="/users/logout">Log out</a>
40
+ </div>
41
+ <% elsif session[:user] %>
42
+ <div id="admin_user_actions">
43
+ <a href="/users/logout">Log out</a>
44
+ </div>
45
+ <% else %>
46
+ <form action="/users/login" method="post">
47
+ <p>
48
+ <label for="admin_login">Login</label> <input type="text" name="login" id="admin_login" />
49
+ <label for="admin_password">Password</label> <input type="password" name="password" id="admin_password" />
50
+ <input type="hidden" id="target_uri" name="target_uri" value="<%= request.request_uri %>" />
51
+ <input type="submit" name="action" value="Log in" />
52
+ </p>
53
+ </form>
54
+ <% end %>
55
+ <div class="clear"></div>
56
+ </div>