refinerycms-images 2.1.5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/refinery/admin/images_controller.rb +62 -49
  3. data/app/helpers/refinery/admin/images_helper.rb +6 -6
  4. data/app/models/refinery/image.rb +16 -61
  5. data/app/models/refinery/thumbnail_dimensions.rb +97 -0
  6. data/app/views/refinery/admin/images/_actions.html.erb +6 -7
  7. data/app/views/refinery/admin/images/_form.html.erb +30 -12
  8. data/app/views/refinery/admin/images/_grid_view.html.erb +9 -16
  9. data/app/views/refinery/admin/images/_list_view.html.erb +4 -4
  10. data/app/views/refinery/admin/images/_list_view_image.html.erb +24 -15
  11. data/app/views/refinery/admin/images/index.html.erb +3 -3
  12. data/app/views/refinery/admin/images/insert.html.erb +1 -1
  13. data/config/locales/ca.yml +44 -0
  14. data/config/locales/cs.yml +5 -0
  15. data/config/locales/de.yml +5 -0
  16. data/config/locales/en.yml +4 -0
  17. data/config/locales/fr.yml +4 -0
  18. data/config/locales/it.yml +4 -14
  19. data/config/locales/nl.yml +1 -1
  20. data/config/routes.rb +1 -1
  21. data/db/migrate/20140814073957_add_title_and_alt_to_refinery_images.rb +8 -0
  22. data/db/migrate/20150430171341_translate_refinery_images.rb +22 -0
  23. data/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb +2 -5
  24. data/lib/refinery/images.rb +4 -1
  25. data/lib/refinery/images/configuration.rb +4 -6
  26. data/lib/refinery/images/dragonfly.rb +38 -27
  27. data/lib/refinery/images/engine.rb +2 -3
  28. data/license.md +1 -1
  29. data/refinerycms-images.gemspec +6 -3
  30. data/spec/factories/image.rb +6 -2
  31. data/spec/features/refinery/admin/images_spec.rb +49 -136
  32. data/spec/lib/generators/refinery/images/images_generator_spec.rb +1 -1
  33. data/spec/lib/refinery/images/dragonfly_spec.rb +34 -0
  34. data/spec/models/refinery/image_spec.rb +81 -43
  35. data/spec/models/refinery/thumbnail_dimensions_spec.rb +18 -0
  36. data/spec/support/shared contexts/admin_images_tab.rb +17 -0
  37. data/spec/support/shared contexts/many_images.rb +5 -0
  38. data/spec/support/shared contexts/no_images.rb +3 -0
  39. data/spec/support/shared contexts/one_image.rb +3 -0
  40. data/spec/support/shared contexts/visual_editor_add_image.rb +31 -0
  41. data/spec/support/shared examples/image_deleter.rb +31 -0
  42. data/spec/support/shared examples/image_editor.rb +3 -0
  43. data/spec/support/shared examples/image_indexer.rb +118 -0
  44. data/spec/support/shared examples/image_inserter.rb +53 -0
  45. data/spec/support/shared examples/image_previewer.rb +41 -0
  46. data/spec/support/shared examples/image_translator.rb +31 -0
  47. data/spec/support/shared examples/image_uploader.rb +37 -0
  48. data/spec/support/spec_helper.rb +19 -0
  49. data/test.html +126 -0
  50. metadata +64 -9
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Refinery
4
+ describe ThumbnailDimensions, type: :model do
5
+ subject(:dimensions) { described_class.new(geometry, 500, 375) }
6
+
7
+ describe '#scale' do
8
+ context 'when no width is supplied' do
9
+ let(:geometry) { 'x225>' }
10
+
11
+ it 'scales by height factor' do
12
+ expect(dimensions.width).to eq 300
13
+ expect(dimensions.height).to eq 225
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ shared_context 'admin images tab' do
2
+
3
+ let(:open_upload_dialog) { find('a', text: ::I18n.t('create_new_image', scope: 'refinery.admin.images.actions')).trigger(:click) }
4
+ let(:select_upload) {}
5
+ let(:initialize_context) {}
6
+ let(:index_in_frame) {false}
7
+ let(:dialog_frame_id) {'dialog_iframe'}
8
+ let(:initial_path) { refinery.admin_images_path(view: %w(grid list).sample) }
9
+
10
+ let(:index_item_selector) {'#records li'}
11
+ let(:gridview_img_selector) {' > img'}
12
+ let(:gridview_title_selector) {'[tooltip]'}
13
+ let(:gridview_alt_selector) {'[alt]'}
14
+ let(:listview_title_selector) {' > span.title'}
15
+ let(:listview_filename_selector) {' > span.preview'}
16
+
17
+ end
@@ -0,0 +1,5 @@
1
+ shared_context 'many images' do
2
+ let!(:image) { FactoryGirl.create(:image) }
3
+ let!(:alt_image) { FactoryGirl.create(:alternate_image) }
4
+ let!(:another_image) { FactoryGirl.create(:another_image) }
5
+ end
@@ -0,0 +1,3 @@
1
+ shared_context "no existing images" do
2
+ let(:image) { FactoryGirl.create(:image) }
3
+ end
@@ -0,0 +1,3 @@
1
+ shared_context "one image" do
2
+ let!(:image) { FactoryGirl.create(:image) }
3
+ end
@@ -0,0 +1,31 @@
1
+ shared_context 'Visual Editor - add image' do
2
+
3
+ let(:page_for_image) {
4
+ page = Refinery::Page.create :title => "Add Image to me"
5
+ # we need page parts so that there's a visual editor
6
+ Refinery::Pages.default_parts.each_with_index do |default_page_part, index|
7
+ page.parts.create(:title => default_page_part, :slug => default_page_part, :body => nil, :position => index)
8
+ end
9
+ page
10
+ }
11
+
12
+ let(:initialize_context) {click_on('Add Image') }
13
+
14
+ let(:open_upload_dialog) {}
15
+ let(:select_upload) {choose(::I18n.t('new_image', :scope => 'refinery.admin.images.insert')) }
16
+
17
+ let(:initial_path) { refinery.edit_admin_page_path(page_for_image) }
18
+
19
+ # let(:target_image) { find(:xpath, "//img[@title='Beach']")}
20
+ # let(:url){target_image["data-#{size}"]}
21
+
22
+ let(:index_in_frame) {true}
23
+ let(:dialog_frame_id) {'dialog_frame'}
24
+ let(:index_item_selector) {'#existing_image_area_content ul li img'}
25
+ let(:title_attribute_selector) {'[title]'}
26
+ let(:alt_attribute_selector) {'[alt]'}
27
+
28
+ let(:image_title_selector) {|title| '//img[@alt="' << title << '"]'}
29
+ let(:image_size_selector) {|size| '//a[@href="#' << size << '"]'}
30
+
31
+ end
@@ -0,0 +1,31 @@
1
+ shared_examples_for 'deletes an image' do
2
+ before do
3
+ raise "please set let(:initial_path)" if initial_path.blank?
4
+ ensure_on(initial_path)
5
+ end
6
+
7
+ let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min}
8
+ let(:deleting_an_image) {
9
+ -> {
10
+ first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images'))
11
+ }
12
+ }
13
+
14
+ it 'has a delete image link for each image' do
15
+ expect(page).to have_selector('#records.images li a.confirm-delete', count: image_count)
16
+ end
17
+
18
+ it "removes an image" do
19
+ expect(deleting_an_image).to change(Refinery::Image, :count).by(-1)
20
+ end
21
+
22
+ it 'says the image has been removed' do
23
+ image_title = page.has_selector?('#image_grid') ? find("#image_grid li:first").first("img")[:title] :
24
+ find("#image_list li:first").first("span.title").text
25
+
26
+ expect(image_title).to be_present
27
+
28
+ first(".images_list li:first").click_link(::I18n.t('delete', scope: 'refinery.admin.images'))
29
+ expect(page).to have_content(::I18n.t('destroyed', scope: 'refinery.crudify', what: "'#{image_title}'"))
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ shared_examples 'edits an image' do
2
+ pending
3
+ end
@@ -0,0 +1,118 @@
1
+ shared_examples_for 'indexes images' do
2
+
3
+ let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min}
4
+
5
+ before do
6
+ raise "please set let(:initial_path)" if initial_path.blank?
7
+ ensure_on(initial_path)
8
+ initialize_context
9
+ end
10
+
11
+ it 'shows all the images', js: true do
12
+ if index_in_frame
13
+ page.within_frame(dialog_frame_id) do
14
+ expect(page).to have_selector(index_item_selector, count: image_count)
15
+ end
16
+ else
17
+ expect(page).to have_selector(index_item_selector, count: image_count)
18
+ end
19
+ end
20
+
21
+ end # image index
22
+
23
+ shared_examples_for 'shows list and grid views' do
24
+
25
+ let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min}
26
+
27
+ before do
28
+ raise "please set let(:initial_path)" if initial_path.blank?
29
+ ensure_on(initial_path)
30
+ end
31
+
32
+ context "when in grid view" do
33
+
34
+ before { ensure_on(current_path + "?view=grid") }
35
+
36
+ it 'shows the images with thumbnails', js: true do
37
+ expect(page).to have_selector(index_item_selector << gridview_img_selector, count: image_count)
38
+ end
39
+
40
+ it 'makes the title attribute of each image available', js: true do
41
+ expect(page).to have_selector(index_item_selector << gridview_img_selector << gridview_title_selector, count: image_count)
42
+ end
43
+
44
+ it 'makes the alt attribute of each image available', js: true do
45
+ expect(page).to have_selector(index_item_selector << gridview_img_selector << gridview_alt_selector, count: image_count)
46
+ end
47
+
48
+ it 'has an option to switch to list view' do
49
+ expect(page).to have_content(::I18n.t('switch_to', view_name: 'list', scope: 'refinery.admin.images.index.view'))
50
+ end
51
+
52
+ end # grid view
53
+
54
+ context "when in list view" do
55
+
56
+ before do
57
+ ensure_on(current_path + "?view=list")
58
+ end
59
+
60
+ it 'makes the title attribute of each image available', js: true do
61
+ expect(page).to have_selector(index_item_selector << listview_title_selector, count: image_count)
62
+ end
63
+
64
+ it 'makes the filename of each image available' do
65
+ expect(page).to have_selector(index_item_selector << listview_filename_selector, count: image_count)
66
+ end
67
+
68
+ it 'has an option to switch to grid view' do
69
+ ensure_on(current_path + '?view=list')
70
+
71
+ expect(page).to have_content(::I18n.t('switch_to', view_name: 'grid', scope: 'refinery.admin.images.index.view'))
72
+ end
73
+
74
+ end # list view
75
+ end
76
+
77
+ shared_examples_for 'paginates the list of images' do
78
+
79
+ let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min}
80
+
81
+ before do
82
+ raise "please set let(:initial_path)" if initial_path.blank?
83
+ ensure_on(initial_path)
84
+ end
85
+
86
+ describe 'pagination', unless: Refinery::Image.count <= 2 do
87
+ before {
88
+ Refinery::Images.pages_per_admin_index = 2
89
+ Refinery::Images.pages_per_dialog_that_have_size_options = 2 }
90
+
91
+ it 'divides the index into pages' do
92
+ expect(page).to have_selector("div.pagination em.current")
93
+ end
94
+
95
+ context 'when on the first page' do
96
+ it 'shows a link for the next page' do
97
+ ensure_on(current_path + '?from_page=1&page=1')
98
+ expect(page).to have_selector("a.next_page[rel='next']")
99
+ end
100
+
101
+ it 'has disabled the previous page link' do
102
+ expect(page).to have_selector('.previous_page.disabled')
103
+ end
104
+ end
105
+
106
+ context 'when on the last page' do
107
+ it 'shows a link for the previous page' do
108
+ ensure_on(current_path + '?from_page=1&page=2')
109
+ expect(page).to have_selector("a.previous_page[rel='prev start']")
110
+ end
111
+
112
+ it 'has disabled the next link' do
113
+ expect(page).to have_selector('.next_page.disabled')
114
+ end
115
+
116
+ end
117
+ end # pagination
118
+ end # image index
@@ -0,0 +1,53 @@
1
+
2
+ shared_examples 'inserts images' do
3
+ before do
4
+ raise "please set let(:initial_path)" if initial_path.blank?
5
+ ensure_on(initial_path)
6
+ end
7
+
8
+ def select_and_insert_image(title, size='medium')
9
+ page.within_frame(dialog_frame_id) do
10
+ within '#existing_image_area_content' do
11
+ find(:xpath, "//img[@alt=\"#{title}\"]").click
12
+ find(:xpath, "//a[@href=\"##{size}\"]").click
13
+ find(:xpath, '//input[@id="submit_button"]').click
14
+ end
15
+ end
16
+ end
17
+
18
+ describe 'resize the image' do
19
+ pending
20
+ # it 'inserts an image of the selected size', js: true do
21
+ # click_on('Add Image')
22
+ # select_and_insert_image('Beach','large')
23
+ # sleep 10
24
+ # puts page.html
25
+ # page.within_frame(find(:xpath, '//iframe[contains(@id,"WYMeditor")]')) do
26
+ # expect(page).to have_selector('img[title="Beach"][data-rel*="450x450"]')
27
+ # end
28
+ # end
29
+ end
30
+
31
+ context 'when all images are available' do
32
+ pending
33
+ # it 'inserts the image into the page', js: true do
34
+ # click_on('Add Image')
35
+ # select_and_insert_image('Beach')
36
+ # expect(page).to have_image(url)
37
+ # end
38
+ end
39
+
40
+ context "when images are filtered by a search" do
41
+ pending
42
+
43
+ # before do
44
+ # fill_in "search", :with => "Beach"
45
+ # click_button "Search"
46
+ # end
47
+ # it 'inserts the image into the page', js: true do
48
+ # click_on('Add Image')
49
+ # select_and_insert_image('Beach')
50
+ # expect(page).to have_image(url)
51
+ # end
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ def preview_image
2
+ preview_window = window_opened_by do
3
+ image_url
4
+ find(:linkhref, image_url).click
5
+ end
6
+
7
+ page.within_window preview_window do
8
+ expect(page).to have_image(image_url)
9
+ end
10
+ preview_window.close
11
+ end
12
+
13
+ shared_examples 'shows an image preview' do
14
+ before do
15
+ raise "please set let(:initial_path)" if initial_path.blank?
16
+ ensure_on(initial_path)
17
+ end
18
+
19
+ let(:image_url) {first(:xpath, "//a[@class='preview_icon']")[:href]}
20
+
21
+ context "when in list view" do
22
+ before do
23
+ ensure_on(current_path + "?view=list")
24
+ end
25
+
26
+ it 'displays the image in a new window', js: true do
27
+ preview_image()
28
+ end
29
+ end
30
+
31
+
32
+ context "when in grid view" do
33
+ before do
34
+ ensure_on(current_path + "?view=grid")
35
+ end
36
+
37
+ it 'displays the image in a new window', js: true do
38
+ preview_image
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ shared_examples 'translates an image' do
2
+ before do
3
+ allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :fr])
4
+ ensure_on(initial_path)
5
+ end
6
+
7
+ context "when in list view" do
8
+ before do
9
+ ensure_on(current_path + "?view=list")
10
+ end
11
+
12
+ it "can have a second locale added to it" do
13
+ expect(page).to have_content("Beach")
14
+ expect(page).to have_selector("a[href='/refinery/images/#{image.id}/edit']")
15
+
16
+ click_link "Edit this image"
17
+
18
+ within "#switch_locale_picker" do
19
+ click_link "FR"
20
+ end
21
+
22
+ fill_in "Title", :with => "Titre de la première image"
23
+ fill_in "Alt", :with => "Texte alternatif de la première image"
24
+
25
+ click_button "Save"
26
+
27
+ expect(page).to have_content("'Titre de la première image' was successfully updated.")
28
+ expect(Refinery::Image.translation_class.count).to eq(1)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ shared_examples 'uploads images' do
2
+ before do
3
+ raise "please set let(:initial_path)" if initial_path.blank?
4
+ ensure_on(initial_path)
5
+ initialize_context
6
+ end
7
+
8
+ let(:uploading_an_image) {
9
+ ->{
10
+ open_upload_dialog
11
+ page.within_frame(dialog_frame_id) do
12
+ select_upload
13
+ attach_file 'image_image', image_path
14
+ fill_in 'image_image_title', with: 'Image With Dashes'
15
+ fill_in 'image_image_alt', with: "Alt description for image"
16
+ click_button ::I18n.t('save', scope: 'refinery.admin.form_actions')
17
+ end
18
+ }
19
+ }
20
+
21
+ context 'when the image type is acceptable' do
22
+ let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg")}
23
+ it 'the image is uploaded', :js => true do
24
+ expect(uploading_an_image).to change(Refinery::Image, :count).by(1)
25
+ end
26
+ end
27
+
28
+ context 'when the image type is not acceptable' do
29
+ let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf")}
30
+ it 'the image is rejected', :js => true do
31
+ expect(uploading_an_image).to_not change(Refinery::Image, :count)
32
+ page.within_frame(dialog_frame_id) do
33
+ expect(page).to have_content(::I18n.t('incorrect_format', :scope => 'activerecord.errors.models.refinery/image'))
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ module Capybara
2
+ class Session
3
+ def has_image?(src)
4
+ has_xpath?("//img[contains(@src,'#{src}')]")
5
+ end
6
+ end
7
+
8
+ add_selector(:linkhref) do
9
+ xpath {|href| ".//a[@href='#{href}']"}
10
+ end
11
+ end
12
+
13
+ def ensure_on(path)
14
+ visit(path) unless current_path == path
15
+ end
16
+
17
+ RSpec.configure do |c|
18
+ c.alias_it_should_behave_like_to :it_has_behaviour, 'has behaviour:'
19
+ end
@@ -0,0 +1,126 @@
1
+ <!DOCTYPE html>
2
+ <!-- paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ -->
3
+ <!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
4
+ <!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
5
+ <!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
6
+ <!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
7
+ <!--[if (gt IE 9)|!(IE)]><!-->
8
+ <html lang="en" class="no-js">
9
+ <!--<![endif]-->
10
+
11
+ <head>
12
+ <meta charset='utf-8' />
13
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
14
+ <meta refinerycms='3.0.0' />
15
+ <title>Company Name - Refinery</title>
16
+
17
+ <link rel="stylesheet" media="screen" href="/assets/refinery/refinery.css" />
18
+ <link rel="stylesheet" media="screen" href="/assets/wymeditor.css" />
19
+ <link rel="stylesheet" media="screen" href="/assets/wymeditor/skins/refinery/skin.css" />
20
+
21
+ <script type="text/javascript" >
22
+ var refinery = window.refinery || {};
23
+ refinery.current_admin_locale = 'en';
24
+ </script>
25
+ <script src="/assets/admin.js"></script><script src="/assets/refinery/refinery.js"></script><script src="/assets/refinery/application.js"></script>
26
+ <script src="/assets/refi
27
+ nery/wymeditor.js"></script>
28
+ <script src="/assets/wymeditor/lang/en.js"></script>
29
+ <script src="/assets/wymeditor/skins/refinery/skin.js"></script>
30
+
31
+ </head>
32
+
33
+ <body class="index en">
34
+ <div id='site_bar'>
35
+ <div id='site_bar_content' class='clearfix'>
36
+
37
+ <div id='editor_switch'>
38
+ <a data-no-turbolink="true" href="/">Switch to your website</a>
39
+ </div>
40
+
41
+ <a id="site_bar_refinery_cms_logo" target="_blank" href="http://refinerycms.com"> <span>Refinery CMS™</span> </a>
42
+ <div id='site_bar_branding'>
43
+ <span id='site_bar_company_name'> Company Name </span>
44
+
45
+ <a id="logout" href="/refinery/logout">Log out</a>
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <div id='tooltip_container'></div>
51
+ <div id="admin_container" class="clearfix">
52
+ <header id="header">
53
+ <nav id="menu">
54
+ <a id="plugin_refinery_users" href="/refinery/users">Users</a>
55
+ <a class="active" id="plugin_refinery_images" href="/refinery/images">Images</a>
56
+ <a id="plugin_refinery_files" href="/refinery/resources">Files</a>
57
+ <a id="plugin_refinery_pages" href="/refinery/pages">Pages</a>
58
+
59
+ <a id="menu_reorder" title="Reorder menu" class=" reorder_icon" href=""></a>
60
+ <a id="menu_reorder_done" title="i18n: Reorder Done" class=" reorder_done_icon hidden" href=""></a>
61
+
62
+ </nav>
63
+
64
+ </header>
65
+ <div id="page_container">
66
+ <div id="page">
67
+ <div id="content" class="clearfix">
68
+ <div id="flash_container">
69
+ <noscript>
70
+ <div class='flash flash_error'>
71
+ For full functionality of this page it is necessary to enable JavaScript.
72
+ Here are the
73
+ <a target="_blank" href="http://www.enable-javascript.com">instructions for how to enable JavaScript in your web browser</a>
74
+ </div>
75
+ </noscript>
76
+
77
+ </div>
78
+ <section id="records" class="images">
79
+
80
+ <div class="pagination_container">
81
+
82
+ <div class="clearfix pagination_frame frame_center">
83
+ <h3>March 04, 2015</h3>
84
+ <ul class='images_list'>
85
+ <li id="sortable_1" class="clearfix record on">
86
+ <span class="title"> Beach </span>
87
+ <span class='alt'> <a title="Title: Beach Alt text: Beach" class=" info_icon" href="#"></a> </span>
88
+
89
+ <span class="preview">&nbsp;</span>
90
+ <span class="actions"> <a title="View this image <br/><em>Opens in a new window</em>" class=" preview_icon" target="_blank" href="/system/images/W1siZiIsIjIwMTUvMDMvMDQvNmhleWpqcDJvZF9iZWFjaC5q
91
+ cGVnIl1d/beach.jpeg?sha=9fd966feb5fd57e4"></a> <a title="Edit this image" class=" edit_icon" href="/refinery/images/1/edit"></a> <a data-confirm="Are you sure you want to remove &#39;Beach&#39;?" title="Remove this image forever" class=" delete_icon cancel confirm-delete" rel="nofollow" data-method
92
+ ="delete" href="/refinery/images/1"></a> </span>
93
+ </li>
94
+
95
+ </ul>
96
+ </div>
97
+
98
+ </div>
99
+
100
+ </section>
101
+ <section id="actions">
102
+ <ul>
103
+ <li>
104
+ <form method='GET' action='/refinery/images' class='search_form'>
105
+ <input id="search" type="search" name="search" title="Put here a string of at least 3 characters containing what you are looking for." />
106
+
107
+ <input type="submit" value="Search" />
108
+ </form>
109
+
110
+ </li>
111
+ <li>
112
+ <a title="Add new image" class=" add_icon" href="/refinery/images/new?dialog=true">Add new image</a>
113
+ </li>
114
+ <li>
115
+ <a title="Switch to grid view" class=" switch_view_grid_icon" href="/refinery/images?view=grid">Switch to grid view</a>
116
+ </li>
117
+ </ul>
118
+
119
+ </section>
120
+
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </body>
126
+ </html>