diff --git a/app/assets/images/symbols.svg b/app/assets/images/symbols.svg index 7c12d067bc..23ab1d8b51 100644 --- a/app/assets/images/symbols.svg +++ b/app/assets/images/symbols.svg @@ -303,4 +303,8 @@ + + + + diff --git a/app/assets/javascripts/plugins/jquery.litebox.js b/app/assets/javascripts/plugins/jquery.litebox.js index bf09b11699..79e7e462e7 100644 --- a/app/assets/javascripts/plugins/jquery.litebox.js +++ b/app/assets/javascripts/plugins/jquery.litebox.js @@ -1,266 +1,267 @@ -/*! - * jQuery plugin - * Litebox - modal dialog window - * Copyright (c) 2015 Nick Seryakov https://github.com/kolking - * - * Usage example: - * new LiteBox({ content: '#MyLiteBox' }).open(); - * new LiteBox({ url: '/contact.html', hash: '#contact' }); - */ - -;(function($, window, document, undefined) { - - var LiteBox = function(options) { - this.$win = $(window); - this.$doc = $(document); - this.$body = $(document.body); - this.options = $.extend({}, this.defaults, options); - - if(this.options.hash) { - if(this.options.hash.toString().charAt(0) !== '#') { - this.options.hash = '#' + this.options.hash; - } - this.$win.on('hashchange.LiteBox', $.proxy(this.checkHash, this)); - this.checkHash(); - } - }; - - LiteBox.prototype = { - defaults: { - url: null, // URL for loading content - hash: null, // Hash to show litebox - content: null, // Selector for local content element - cssclass: null, // Custom CSS class for the litebox - noscroll: true, // Disable page scroll when open - disposable: false // Remove from DOM after close - }, - - isopen: false, - $wrapper: null, - $container: null, - $content: null, - - checkHash: function() { - this[this.options.hash.toLowerCase() === window.location.hash.toLowerCase() ? 'open' : 'close'](); - }, - - pageScroll: function(enabled) { - var offset; - if(enabled) { - // Enable page scroll - offset = parseInt($('html').css('top'), 10); - $('html').css({ 'top':'','width':'','position':'','overflow':'','overflow-y':'' }); - $('html,body').scrollTop(-offset).css('overflow', ''); - } else { - // Disable page scroll - if($(document).height() > $(window).height()) { - offset = $(window).scrollTop(); - $('html').css({ - 'top': -offset, - 'width': '100%', - 'position': 'fixed', - 'overflow': 'hidden', - 'overflow-y': 'scroll' - }); - } else { - $('html,body').css('overflow', 'hidden'); - } - } - }, - - open: function() { - if(this.isopen) return; - - // Disable page scroll - if(this.options.noscroll) { - this.pageScroll(false); - } - - if(this.$wrapper) { - // Show litebox if already exists - this.$wrapper.add(this.$container).addClass('visible'); - } else { - // Create elements - this.$wrapper = $('
').addClass('litebox category_modal').addClass(this.options.cssclass); - this.$closebutton = $('').addClass('litebox_close') - .attr('aria-label', 'close').text('×'); - this.$container = $('
').addClass('litebox_container'); - this.$content = $('
').addClass('litebox_content'); - - // Bind close handlers - this.$wrapper.add(this.$closebutton).on('click', $.proxy(this.eventClose, this)); - this.$container.click(function(e) { e.stopPropagation(); }); - - // Insert into DOM and show - this.$container.append(this.$closebutton, this.$content); - this.$wrapper.append(this.$container).appendTo(this.$body); - this.$wrapper.css('opacity'); // Force reflow - this.$wrapper.addClass('visible'); - - if(this.options.url) { - // Load remote content - this.$wrapper.addClass('litebox-busy'); - $.ajax({ - url: this.options.url, - cache: false, - context: this - }).done(function(data) { - this.insert(data); - }).fail(function() { - this.$wrapper.addClass('litebox-error'); - }).always(function() { - this.$wrapper.removeClass('litebox-busy'); - }); - } else if(this.options.content) { - // Get local content - this.insert($(this.options.content).show()); - } else { - console.log('LiteBox: No content has been defined'); - } - } - - // Close with Escape key - this.$doc.on('keyup.LiteBox', $.proxy(this.eventClose, this)); - this.isopen = true; - - return this; - }, - - insert: function(content) { - this.$content.html(content); - this.$container.addClass('visible'); - this.$content.find(':text:not(:hidden,[readonly],[disabled]):first').focus(); - - // Close litebox if button[type=reset] pressed - this.$content.on('click', 'form :reset', $.proxy(this.eventClose, this)); - - // Include submit button value into serialization - this.$content.on('click', 'form :submit', function() { - var button = $(this); - button.after($('').attr({ - type: 'hidden', - name: button.attr('name'), - value: button.val() - })); - }); - - // Submit an inner form via ajax - this.$content.on('submit.LiteBox', 'form', $.proxy(function(e) { - e.preventDefault(); - - var $form = $(e.currentTarget); - var form_method = $form.attr('method'); - var form_action = $form.attr('action'); - var multipart = $form.attr('enctype') === 'multipart/form-data'; - var form_data = multipart ? new FormData($form[0]) : $form.serialize(); - var $submit = $form.find(':submit').prop('disabled', true); - - this.$content.addClass('ajax-busy'); - - $.ajax({ - url: form_action, - method: form_method, - data: form_data, - cache: false, - context: this, - contentType: (multipart ? false : 'application/x-www-form-urlencoded; charset=UTF-8'), - processData: (multipart ? false : true) - }).done(function(data, textStatus, jqXHR) { - var status = jqXHR.status; - var location = jqXHR.getResponseHeader('location'); - if(status === 201) { - if(location) { - var oldPath = window.location.href.split('#')[0]; - var newPath = location.split('#')[0]; - window.location.assign(location); - - // Force page reload if location path not changed - if(newPath === oldPath) { - window.location.reload(); - } - } else { - this.close(); - } - } else { - this.$content.html(data); - } - }).fail(function() { - this.$content.addClass('ajax-error'); - }).always(function() { - $submit.prop('disabled', false); - this.$content.removeClass('ajax-busy'); - }); - }, this)); - }, - - close: function() { - if(!this.isopen || !this.$wrapper) return; - - // Hide litebox and unbind key handler - this.$wrapper.removeClass('visible'); - this.$doc.off('keyup.LiteBox'); - this.isopen = false; - - // Enable page scroll - if(this.options.noscroll) { - this.pageScroll(true); - } - - // Remove litebox from DOM after all CSS transitions - if(this.options.disposable || this.options.url) { - var events = 'transitionend otransitionend oTransitionEnd webkitTransitionEnd msTransitionEnd'; - this.$wrapper.one(events, $.proxy(this.destroy, this)); - } - - // Make sure to reset location hash - if(this.options.hash) { - window.location.replace('#'); - } - }, - - eventClose: function(e) { - if(e.type === 'keyup' && e.keyCode !== 27) return; - - e.preventDefault(); - e.stopPropagation(); - - this.close(); - }, - - destroy: function(e) { - this.$wrapper.remove(); - this.$wrapper = null; - this.$container = null; - this.$content = null; - } - }; - - LiteBox.defaults = LiteBox.prototype.defaults; - - $.fn.litebox = function(eventName) { - eventName = eventName || 'click'; - - return this.each(function() { - if(!this.litebox) { - var $elm = $(this); - var options = $.extend({ url: $elm.attr('href') }, $elm.data('litebox')); - if(options.hash === true) { - options.hash = options.url; - } - this.litebox = new LiteBox(options); - } - - $(this).on(eventName, function(e) { - e.preventDefault(); - if(this.litebox.options.hash) { - document.location.hash = this.litebox.options.hash; - } else { - this.litebox.open(); - } - }); - }); - }; - - window.LiteBox = LiteBox; - -})(jQuery, window, document); +/*! + * jQuery plugin + * Litebox - modal dialog window + * Copyright (c) 2015 Nick Seryakov https://github.com/kolking + * + * Usage example: + * new LiteBox({ content: '#MyLiteBox' }).open(); + * new LiteBox({ url: '/contact.html', hash: '#contact' }); + */ + +;(function($, window, document, undefined) { + + var LiteBox = function(options) { + this.$win = $(window); + this.$doc = $(document); + this.$body = $(document.body); + this.options = $.extend({}, this.defaults, options); + + if(this.options.hash) { + if(this.options.hash.toString().charAt(0) !== '#') { + this.options.hash = '#' + this.options.hash; + } + this.$win.on('hashchange.LiteBox', $.proxy(this.checkHash, this)); + this.checkHash(); + } + }; + + LiteBox.prototype = { + defaults: { + url: null, // URL for loading content + hash: null, // Hash to show litebox + content: null, // Selector for local content element + cssclass: null, // Custom CSS class for the litebox + noscroll: true, // Disable page scroll when open + disposable: false // Remove from DOM after close + }, + + isopen: false, + $wrapper: null, + $container: null, + $content: null, + + checkHash: function() { + this[this.options.hash.toLowerCase() === window.location.hash.toLowerCase() ? 'open' : 'close'](); + }, + + pageScroll: function(enabled) { + var offset; + if(enabled) { + // Enable page scroll + offset = parseInt($('html').css('top'), 10); + $('html').css({ 'top':'','width':'','position':'','overflow':'','overflow-y':'' }); + $('html,body').scrollTop(-offset).css('overflow', ''); + } else { + // Disable page scroll + if($(document).height() > $(window).height()) { + offset = $(window).scrollTop(); + $('html').css({ + 'top': -offset, + 'width': '100%', + 'position': 'fixed', + 'overflow': 'hidden', + 'overflow-y': 'scroll' + }); + } else { + $('html,body').css('overflow', 'hidden'); + } + } + }, + + open: function() { + if(this.isopen) return; + + // Disable page scroll + if(this.options.noscroll) { + this.pageScroll(false); + } + + if(this.$wrapper) { + // Show litebox if already exists + this.$wrapper.add(this.$container).addClass('visible'); + } else { + // Create elements + this.$wrapper = $('
').addClass('litebox category_modal').addClass(this.options.cssclass); + this.$closebutton = $('').addClass('litebox_close') + .attr('aria-label', 'close').text('×'); + this.$container = $('
').addClass('litebox_container'); + this.$content = $('
').addClass('litebox_content'); + + // Bind close handlers + this.$wrapper.add(this.$closebutton).on('click', $.proxy(this.eventClose, this)); + this.$container.click(function(e) { e.stopPropagation(); }); + + // Insert into DOM and show + this.$container.append(this.$closebutton, this.$content); + this.$wrapper.append(this.$container).appendTo(this.$body); + this.$wrapper.css('opacity'); // Force reflow + this.$wrapper.addClass('visible'); + + if(this.options.url) { + // Load remote content + this.$wrapper.addClass('litebox-busy'); + $.ajax({ + url: this.options.url, + cache: false, + context: this + }).done(function(data) { + this.insert(data); + }).fail(function() { + this.$wrapper.addClass('litebox-error'); + }).always(function() { + this.$wrapper.removeClass('litebox-busy'); + }); + } else if(this.options.content) { + // Get local content + this.insert($(this.options.content).show()); + } else { + console.log('LiteBox: No content has been defined'); + } + } + + // Close with Escape key + this.$doc.on('keyup.LiteBox', $.proxy(this.eventClose, this)); + this.isopen = true; + + return this; + }, + + insert: function(content) { + this.$content.html(content); + this.$container.addClass('visible'); + this.$content.find(':text:not(:hidden,[readonly],[disabled]):first').focus(); + + // Close litebox if button[type=reset] pressed + this.$content.on('click', 'form :reset', $.proxy(this.eventClose, this)); + + // Include submit button value into serialization + this.$content.on('click', 'form :submit', function() { + var button = $(this); + button.after($('').attr({ + type: 'hidden', + name: button.attr('name'), + value: button.val() + })); + }); + + // Submit an inner form via ajax + this.$content.on('submit.LiteBox', 'form', $.proxy(function(e) { + e.preventDefault(); + + var $form = $(e.currentTarget); + var form_method = $form.attr('method'); + var form_action = $form.attr('action'); + var multipart = $form.attr('enctype') === 'multipart/form-data'; + var form_data = multipart ? new FormData($form[0]) : $form.serialize(); + var $submit = $form.find(':submit').prop('disabled', true); + + this.$content.addClass('ajax-busy'); + + $.ajax({ + url: form_action, + method: form_method, + data: form_data, + cache: false, + context: this, + contentType: (multipart ? false : 'application/x-www-form-urlencoded; charset=UTF-8'), + processData: (multipart ? false : true) + }).done(function(data, textStatus, jqXHR) { + var status = jqXHR.status; + var location = jqXHR.getResponseHeader('location'); + if(status === 201) { + if(location) { + var oldPath = window.location.href.split('#')[0]; + var newPath = location.split('#')[0]; + window.location.assign(location); + + // Force page reload if location path not changed + if(newPath === oldPath) { + window.location.reload(); + } + } else { + this.close(); + } + } else { + this.$content.html(data); + } + }).fail(function(data) { + this.$content.html(data.responseText); + this.$content.addClass('ajax-error'); + }).always(function() { + $submit.prop('disabled', false); + this.$content.removeClass('ajax-busy'); + }); + }, this)); + }, + + close: function() { + if(!this.isopen || !this.$wrapper) return; + + // Hide litebox and unbind key handler + this.$wrapper.removeClass('visible'); + this.$doc.off('keyup.LiteBox'); + this.isopen = false; + + // Enable page scroll + if(this.options.noscroll) { + this.pageScroll(true); + } + + // Remove litebox from DOM after all CSS transitions + if(this.options.disposable || this.options.url) { + var events = 'transitionend otransitionend oTransitionEnd webkitTransitionEnd msTransitionEnd'; + this.$wrapper.one(events, $.proxy(this.destroy, this)); + } + + // Make sure to reset location hash + if(this.options.hash) { + window.location.replace('#'); + } + }, + + eventClose: function(e) { + if(e.type === 'keyup' && e.keyCode !== 27) return; + + e.preventDefault(); + e.stopPropagation(); + + this.close(); + }, + + destroy: function(e) { + this.$wrapper.remove(); + this.$wrapper = null; + this.$container = null; + this.$content = null; + } + }; + + LiteBox.defaults = LiteBox.prototype.defaults; + + $.fn.litebox = function(eventName) { + eventName = eventName || 'click'; + + return this.each(function() { + if(!this.litebox) { + var $elm = $(this); + var options = $.extend({ url: $elm.attr('href') }, $elm.data('litebox')); + if(options.hash === true) { + options.hash = options.url; + } + this.litebox = new LiteBox(options); + } + + $(this).on(eventName, function(e) { + e.preventDefault(); + if(this.litebox.options.hash) { + document.location.hash = this.litebox.options.hash; + } else { + this.litebox.open(); + } + }); + }); + }; + + window.LiteBox = LiteBox; + +})(jQuery, window, document); diff --git a/app/assets/stylesheets/components/buttons.scss b/app/assets/stylesheets/components/buttons.scss index c15fb2857c..507874b89a 100644 --- a/app/assets/stylesheets/components/buttons.scss +++ b/app/assets/stylesheets/components/buttons.scss @@ -160,4 +160,4 @@ button:empty, .button:empty { position: relative; } } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/sections/collection.scss b/app/assets/stylesheets/sections/collection.scss index e6e8447df1..0d4e0ec014 100644 --- a/app/assets/stylesheets/sections/collection.scss +++ b/app/assets/stylesheets/sections/collection.scss @@ -147,7 +147,7 @@ display: block; overflow: hidden; text-overflow: ellipsis; - + .userpic { font-size: 26px; margin-right: 6px; @@ -306,7 +306,7 @@ .category-article dd p { width: 33%; - display: flex; + display: flex; } .category-article dd p a { @@ -448,7 +448,7 @@ white-space: normal !important; vertical-align: top; } - td:nth-child(1), th { + td:nth-child(1), th { width: 30%; } td:nth-child(2) { @@ -465,7 +465,6 @@ vertical-align: top; } select, .inline-btn, .select2 { - margin-left: 1em; display: inline-block; &.select2-hidden-accessible { @@ -493,6 +492,12 @@ } } +.page-settings { + .sidecol { + padding-top: 0.5em; + } +} + .side-tabs { $textColor: #322; float: left; @@ -571,7 +576,7 @@ padding-right: 1em; } } - + &.expanded { .user-label { display: inline-block; @@ -619,7 +624,7 @@ background-color: $bgColor; border-radius: 16px; padding: 1em; - + text-align: center; color: $bodyFgLight; } @@ -654,12 +659,12 @@ &_format { flex: 1 1 auto; padding-right: 15px; - + h5 { display: flex; align-items: center; margin: 0 0 0.25em 0; - + input { width: 19px; height: 19px; @@ -712,12 +717,12 @@ &_format { flex: 1 1 auto; padding-right: 15px; - + h5 { display: flex; align-items: center; margin: 0 0 0.25em 0; - + input { width: 19px; height: 19px; diff --git a/app/assets/stylesheets/sections/page.scss b/app/assets/stylesheets/sections/page.scss index 3fad04984b..9db707e3bb 100644 --- a/app/assets/stylesheets/sections/page.scss +++ b/app/assets/stylesheets/sections/page.scss @@ -545,6 +545,7 @@ .page-image-preview { text-align: center; + margin-bottom: 50px; img { height: auto; max-width: 100%; diff --git a/app/controllers/page_controller.rb b/app/controllers/page_controller.rb index 270257d5fd..e30a242e95 100644 --- a/app/controllers/page_controller.rb +++ b/app/controllers/page_controller.rb @@ -3,63 +3,20 @@ class PageController < ApplicationController require 'image_helper' include ImageHelper - protect_from_forgery :except => [:set_page_title] - before_action :authorized?, :except => [:alto_xml] + protect_from_forgery except: [:set_page_title] + before_action :authorized?, except: [:alto_xml] # no layout if xhr request - layout Proc.new { |controller| controller.request.xhr? ? false : nil }, :only => [:new, :create] + layout Proc.new { |controller| controller.request.xhr? ? false : nil }, only: [:new, :create] def authorized? if user_signed_in? - if @work - redirect_to dashboard_path unless current_user.like_owner?(@work) - end + redirect_to dashboard_path if @work && !current_user.like_owner?(@work) else redirect_to dashboard_path end end - def alto_xml - # Transkribus ALTO does not include an ID on the String element, but we need one for Annotorious - # we need to read the alto file and iterate over every string element, adding an ID attribute - raw_alto = @page.alto_xml - doc = Nokogiri::XML(raw_alto) - - doc.search('String').each_with_index do |string, i| - string['ID'] = "string_#{i}" - end - - render :plain => doc.to_xml, :layout => false, :content_type => 'text/xml' - end - - def delete - @page.destroy - flash[:notice] = t('.page_deleted') - redirect_to work_pages_tab_path(:work_id => @work.id) - end - - # image functions - def rotate - orientation = params[:orientation].to_i - 0.upto(@page.shrink_factor) do |i| - rotate_file(@page.scaled_image(i), orientation) - end - set_dimensions(@page) - redirect_back fallback_location: @page - end - - - # reordering functions - def reorder_page - if(params[:direction]=='up') - @page.move_higher - else - @page.move_lower - end - redirect_to work_pages_tab_path(:work_id => @work.id) - end - - # new page functions def new @page = Page.new @page.title = @work.suggest_next_page_title @@ -67,82 +24,86 @@ def new end def create - @page = Page.new(page_params) - subaction = params[:subaction] - @work.pages << @page - - if @page.save - flash[:notice] = t('.page_created') + result = Page::Create.call(work: @work, page_params: page_params) - if page_params[:base_image] - process_uploaded_file(@page, page_params[:base_image]) - end + if result.success? + subaction = params[:subaction] + flash[:notice] = t('.page_created') if subaction == 'save_and_new' - ajax_redirect_to({ :controller => 'dashboard', :action => 'startproject', :anchor => 'create-work' }) + ajax_redirect_to({ controller: 'dashboard', action: 'startproject', anchor: 'create-work' }) else - ajax_redirect_to({ :controller => 'work', :action => 'pages_tab', :work_id => @work.id, :anchor => 'create-page' }) + ajax_redirect_to({ controller: 'work', action: 'pages_tab', work_id: @work.id, anchor: 'create-page' }) end else - render :new + @page = result.page + + render :new, status: :unprocessable_entity end end + def edit + # Edit route + end + def update - page = Page.find(params[:id]) - attributes = page_params.to_h.except("base_image") - if page_params[:status].blank? - attributes['status'] = nil - end - page.update_columns(attributes) # bypass page version callbacks - flash[:notice] = t('.page_updated') - page.work.work_statistic.recalculate if page.work.work_statistic - - if params[:page][:base_image] - process_uploaded_file(page, page_params[:base_image]) + result = Page::Update.call(page: @page, page_params: page_params) + + if request.xhr? + render json: { + success: result.success?, + errors: result.errors + } + elsif result.success? + flash[:notice] = t('.page_updated') + redirect_to collection_edit_page_path(@collection.owner, @collection, @work, @page) + else + render :edit, status: :unprocessable_entity end - - redirect_back fallback_location: page end + def destroy + result = Page::Destroy.call(page: @page) - private - - def process_uploaded_file(page, image_file) - # create a new filename - filename = "#{Rails.root}/public/images/working/upload/#{page.id}.jpg" + flash[:notice] = t('.page_deleted') + redirect_to work_pages_tab_path(work_id: result.page.work_id) + end - dirname = File.dirname(filename) - unless Dir.exist? dirname - FileUtils.mkdir_p(dirname) - end + def rotate + result = Page::Rotate.call(page: @page, orientation: params[:orientation].to_i) - FileUtils.mv(image_file.tempfile, filename) - FileUtils.chmod("u=wr,go=r", filename) - page.base_image = filename - page.shrink_factor = 0 - set_dimensions(page) - #reduce_by_one(page) + redirect_back fallback_location: result.page end - def reduce_by_one(page) - page.shrink_factor = page.shrink_factor + 1 - shrink_file(page.scaled_image(0), - page.scaled_image(page.shrink_factor), - page.shrink_factor) - page.save! + def reorder + Page::Reorder.call(page: @page, direction: params[:direction]) + + redirect_to work_pages_tab_path(work_id: @work.id) end - def set_dimensions(page) - image = Magick::ImageList.new(page.base_image) - page.base_width = image.columns - page.base_height = image.rows - image = nil - page.save! + def alto_xml + # Transkribus ALTO does not include an ID on the String element, but we need one for Annotorious + # we need to read the alto file and iterate over every string element, adding an ID attribute + raw_alto = @page.alto_xml + doc = Nokogiri::XML(raw_alto) + + doc.search('String').each_with_index do |string, i| + string['ID'] = "string_#{i}" + end + + render :plain => doc.to_xml, :layout => false, :content_type => 'text/xml' end + private + def page_params - params.require(:page).permit(:page, :title, :base_image, :status, :translation_status) + params.require(:page).permit( + :page, + :title, + :base_image, + :status, + :translation_status + ) end end diff --git a/app/interactors/page/create.rb b/app/interactors/page/create.rb new file mode 100644 index 0000000000..30655bf83a --- /dev/null +++ b/app/interactors/page/create.rb @@ -0,0 +1,23 @@ +class Page::Create + include Page::Lib::Common + include Interactor + + def initialize(work:, page_params:) + @work = work + @page_params = page_params + + super + end + + def call + ActiveRecord::Base.transaction do + @page = Page.new(@page_params) + @work.pages << @page + + context.page = @page + process_uploaded_file(@page_params[:base_image]) if @page_params[:base_image] + end + + context + end +end diff --git a/app/interactors/page/destroy.rb b/app/interactors/page/destroy.rb new file mode 100644 index 0000000000..26c32cf00a --- /dev/null +++ b/app/interactors/page/destroy.rb @@ -0,0 +1,15 @@ +class Page::Destroy + include Interactor + + def initialize(page:) + @page = page + + super + end + + def call + @page.destroy + + context + end +end diff --git a/app/interactors/page/lib/common.rb b/app/interactors/page/lib/common.rb new file mode 100644 index 0000000000..c39a197350 --- /dev/null +++ b/app/interactors/page/lib/common.rb @@ -0,0 +1,32 @@ +module Page::Lib::Common + + def process_uploaded_file(image_file) + unless Page::ACCEPTED_FILE_TYPES.include?(image_file.content_type) + error_msg = I18n.t('errors.unsupported_file_type') + @page.errors.add(:base_image, error_msg) + raise StandardError, error_msg + end + + filename = "#{Rails.root}/public/images/working/upload/#{@page.id}.jpg" + + dirname = File.dirname(filename) + FileUtils.mkdir_p(dirname) unless Dir.exist? dirname + + FileUtils.mv(image_file.tempfile, filename) + FileUtils.chmod('u=wr,go=r', filename) + @page.base_image = filename + @page.shrink_factor = 0 + assign_dimensions + rescue StandardError => e + context.errors = e.message + context.fail! + end + + def assign_dimensions + image = Magick::ImageList.new(@page.base_image) + @page.base_width = image.columns + @page.base_height = image.rows + @page.save! + end + +end diff --git a/app/interactors/page/reorder.rb b/app/interactors/page/reorder.rb new file mode 100644 index 0000000000..643a7bff59 --- /dev/null +++ b/app/interactors/page/reorder.rb @@ -0,0 +1,16 @@ +class Page::Reorder + include Interactor + + def initialize(page:, direction:) + @page = page + @direction = direction + + super + end + + def call + @direction == 'up' ? @page.move_higher : @page.move_lower + + context + end +end diff --git a/app/interactors/page/rotate.rb b/app/interactors/page/rotate.rb new file mode 100644 index 0000000000..a758a36058 --- /dev/null +++ b/app/interactors/page/rotate.rb @@ -0,0 +1,21 @@ +class Page::Rotate + include Page::Lib::Common + include ImageHelper + include Interactor + + def initialize(page:, orientation:) + @page = page + @orientation = orientation + + super + end + + def call + 0.upto(@page.shrink_factor) do |i| + rotate_file(@page.scaled_image(i), @orientation) + end + assign_dimensions + + context + end +end diff --git a/app/interactors/page/update.rb b/app/interactors/page/update.rb new file mode 100644 index 0000000000..5e5cbc2b7e --- /dev/null +++ b/app/interactors/page/update.rb @@ -0,0 +1,26 @@ +class Page::Update + include Page::Lib::Common + include Interactor + + def initialize(page:, page_params:) + @page = page + @page_params = page_params + + super + end + + def call + ActiveRecord::Base.transaction do + attributes = @page_params.to_h.except(:base_image) + attributes['status'] = Page.statuses[:new] if @page_params[:status].blank? + attributes['translation_status'] = Page.translation_statuses[:new] if @page_params[:translation_status].blank? + @page.update_columns(attributes) + + @page.work.work_statistic&.recalculate + + process_uploaded_file(@page_params[:base_image]) if @page_params[:base_image] + end + + context + end +end diff --git a/app/interactors/rake_interactor.rb b/app/interactors/rake_interactor.rb index f6fcfd2e5d..5034c8e67c 100644 --- a/app/interactors/rake_interactor.rb +++ b/app/interactors/rake_interactor.rb @@ -23,8 +23,10 @@ def call Rake::Task[@task_name].reenable Rake::Task[@task_name].invoke(*@args.values) rescue => e + # :nocov: puts "#{e.class}: #{e.message}" puts e.backtrace.join("\n") + # :nocov: end ensure $stdout = old_stdout diff --git a/app/interactors/work/refresh_metadata.rb b/app/interactors/work/refresh_metadata.rb index 82fa514406..ef66ef8914 100644 --- a/app/interactors/work/refresh_metadata.rb +++ b/app/interactors/work/refresh_metadata.rb @@ -18,9 +18,11 @@ def call finalize rescue => e + # :nocov: @errors << "Error: #{e}" context.errors = @errors context.fail! + # :nocov: end end def finalize @@ -37,8 +39,10 @@ def process_batches(works) begin refresh_metadata(work) rescue + # :nocov: @errors << "Failed to refresh metadata for #{work.slug}" context.fail! + # :nocov: end end end end diff --git a/app/models/page.rb b/app/models/page.rb index ecea6dbd58..e99d3f9574 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -89,6 +89,14 @@ class Page < ApplicationRecord serialize :metadata, Hash + ACCEPTED_FILE_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp', + 'image/tiff' + ].freeze + STATUS_VALUES = { new: 'new', blank: 'blank', @@ -99,6 +107,7 @@ class Page < ApplicationRecord translated: 'translated' }.freeze + enum status: STATUS_VALUES, _prefix: :status enum translation_status: STATUS_VALUES, _prefix: :translation_status diff --git a/app/views/collection/edit.html.slim b/app/views/collection/edit.html.slim index 7fc0ecabc0..f664360eb1 100644 --- a/app/views/collection/edit.html.slim +++ b/app/views/collection/edit.html.slim @@ -16,9 +16,9 @@ th h3 =f.label :slug, t('.url') p =t('.url_description') - td + td =f.text_field :slug, value: @collection.slug - tr + tr th =t('.current_url') td b#collection-url =collection_url(@collection.owner.slug,@collection.slug) @@ -26,7 +26,7 @@ =svg_symbol '#icon-copy', class: 'icon' tr th =t('.transcriber_url') - td + td b#transcriber-url =collection_start_transcribing_url(@collection.owner.slug, @collection.slug) a.clipboard-btn (data-clipboard-target='#transcriber-url' aria-label=t('.copy_url') title=t('.copy_url')) =svg_symbol '#icon-copy', class: 'icon' @@ -36,7 +36,7 @@ h3 =f.label :tags, t('.subject_tags') td =f.select(:tags, options_from_collection_for_select(Tag.where(canonical: true), :id, :ai_text, @collection.tags.pluck(:id)), {}, class: 'field-input', 'data-multi-select': true, size: 1, multiple: true) - tr + tr th h3 =t('.collection_image') p =t('.collection_image_description') @@ -52,13 +52,13 @@ th h3 =f.label :intro_block, t('.description') p = t('.description_message') - td =f.text_area :intro_block, rows: 6, value: @collection.intro_block - tr: td - tr - th + td =f.text_area :intro_block, rows: 6, value: @collection.intro_block + tr: td + tr + th h3 =t('.footer') p =t('.footer_description') - td + td .footer-preview ==@collection.owner.footer_block -if current_user == @collection.owner diff --git a/app/views/page/_edit_tabs.html.slim b/app/views/page/_edit_tabs.html.slim new file mode 100644 index 0000000000..350bd2154e --- /dev/null +++ b/app/views/page/_edit_tabs.html.slim @@ -0,0 +1,22 @@ +-tabs = [{\ + icon: '#icon-settings', + name: t('.general'), + selected: 1, + path: "#{main_app.collection_edit_page_path(@collection.owner, @collection, @work, @page)}", +},{\ + icon: '#icon-robot', + name: t('.htr'), + selected: 2, + path: "#", +}] + +.side-tabs + -for tab in tabs + -if tab[:selected] == selected + a.active + =svg_symbol tab[:icon], class: 'icon' + =tab[:name] + -else + =link_to tab[:path] + =svg_symbol tab[:icon], class: 'icon' + =tab[:name] diff --git a/app/views/page/_form.html.slim b/app/views/page/_form.html.slim new file mode 100644 index 0000000000..5bccbf5acf --- /dev/null +++ b/app/views/page/_form.html.slim @@ -0,0 +1,62 @@ +.page-settings.collection-settings-wrapper + p= t('.here_you_can_edit') + .columns + .maincol + =form_for(@page, + url: page_path(@page), + method: :put, + remote: true, + authenticity_token: true, + html: { multipart: true }) do |f| + .validation-wrapper(style="#{@page.errors.any? ? '' : 'display:none;'}") = render 'shared/validation_summary', errors: @page.errors + =f.submit t('.save_changes'), id: 'collection-settings-save', hidden: true + -direction = Rtl.rtl?(@collection.text_language) ? 'rtl' : 'ltr' + table.form.collection-settings lang="#{@collection.text_language}" dir=direction class=direction + tr + th + h3 = f.label :title, t('.page_title') + td.w100 =f.text_field :title, value: @page.title + tr + th + h3 = f.label :status, t('.page_status') + td.w100 + -option_array = Page::MAIN_STATUSES.map {|stat| [t(".page_status_#{stat}"),stat]} + =f.select :status, options_for_select(option_array, @page.status), {}, { class: 'bs ms-0' } + -if @work.supports_translation + tr + th + h3 = f.label :translation_status, t('.page_translation_status') + td + -option_array = Page::TRANSLATION_STATUSES.map {|stat| [t(".page_status_#{stat}"),stat]} + =f.select :translation_status, options_for_select(option_array, @page.translation_status) + tr + th + h3 = f.label :base_image, t('.page_image') + td + .input-file + =f.file_field :base_image, tabindex: '-1', accept: 'image/*' + input(type="text" tabindex="-1" placeholder=t('.click_to_browse_a_file') aria-label=t('.click_to_browse_a_file') readonly) + button(type="button") #{t('.browse')} + .sidecol + h3 = t('.page_position', position: @page.position) + =time_tag @page.created_on + =t('.created', date: l(@page.created_on.localtime)) + + -if @page.base_image.present? + hr + .headline + h2.headline_title =t('.page_image') + .headline_aside + span.fgfaded = t('.dimensions', width: @page.base_width, height: @page.base_height).html_safe + .headline_aside + .button-stack + =link_to rotate_page_index_path(page_id: @page.id, orientation: 90), class: 'button outline', title: t('.rotate_clockwise'), method: :post do + =svg_symbol "#icon-rotate-cw", class: 'icon' + =link_to rotate_page_index_path(page_id: @page.id, orientation: 270), class: 'button outline', title: t('.rotate_counterclockwise'), method: :post do + =svg_symbol "#icon-rotate-ccw", class: 'icon' + + .page-image-preview =image_tag("#{file_to_url(@page.canonical_facsimile_url)}?#{Time.now.to_i}") + + .toolbar + .toolbar_group.w100 + =link_to t('.delete'), page_path(@page), method: :delete, data: { confirm: t('.delete_confirm_message') }, class: 'button big' diff --git a/app/views/page/edit.html.slim b/app/views/page/edit.html.slim index 42dfb11a2c..98c531b475 100644 --- a/app/views/page/edit.html.slim +++ b/app/views/page/edit.html.slim @@ -1,55 +1,5 @@ -=render({ :partial => '/shared/page_tabs', :locals => { :selected => 4, :page_id => @page.id }}) - -.page-settings.columns - .maincol - =form_for :page, url: page_update_path(:id => @page.id), :html => { :multipart => true } do |f| - -direction = Rtl.rtl?(@collection.text_language) ? 'rtl' : 'ltr' - table.form lang="#{@collection.text_language}" dir=direction class=direction - tr.big - th =f.label :title, t('.page_title') - td.w100 =f.text_field :title, value: @page.title - tr - th =f.label :status, t('.page_status') - td - -option_array = Page::MAIN_STATUSES.map {|stat| [t(".page_status_#{stat}"),stat]} - =f.select :status, options_for_select(option_array, @page.status) - -if @work.supports_translation - tr - th =f.label :translation_status, t('.page_translation_status') - td - -option_array = Page::TRANSLATION_STATUSES.map {|stat| [t(".page_status_#{stat}"),stat]} - =f.select :translation_status, options_for_select(option_array, @page.translation_status) - -unless @page.base_image.blank? - tr - th =f.label :base_image, t('.page_image') - td - .input-file - =f.file_field :base_image, tabindex: '-1', accept: 'image/*' - input(type="text" tabindex="-1" placeholder=t('.click_to_browse_a_file') aria-label=t('.click_to_browse_a_file') readonly) - button(type="button") #{t('.browse')} - .toolbar - .toolbar_group.w100 - =link_to({ :action => 'delete', :page_id => @page.id }, data: { :confirm => t('.delete_confirm_message') }, class: 'button') - span =t('.delete_page') - .toolbar_group =f.button t('.save_changes'), class: 'big' - - .sidecol - big = t('.page_position', position: @page.position) - =time_tag @page.created_on - =t('.created', date: l(@page.created_on.localtime)) - p.small.fglight =t('.here_you_can_edit') - --if @page.base_image.present? - hr - .headline - h2.headline_title =t('.page_image') - .headline_aside - span.fgfaded = t('.dimensions', width: @page.base_width, height: @page.base_height).html_safe - .headline_aside - .button-stack - =link_to(page_rotate_path(:page_id => @page.id, :orientation => 90), class: 'button outline', title: t('.rotate_clockwise')) - =svg_symbol "#icon-rotate-cw", class: 'icon' - =link_to(page_rotate_path(:page_id => @page.id, :orientation => 270), class: 'button outline', title: t('.rotate_counterclockwise')) - =svg_symbol "#icon-rotate-ccw", class: 'icon' - - .page-image-preview =image_tag("#{file_to_url(@page.canonical_facsimile_url)}?#{Time.now.to_i}") +=javascript_include_tag "settings" +=render({ partial: '/shared/page_tabs', locals: { selected: 4, page_id: @page.id } }) +.collection-meta-wrapper + =render({ partial: 'edit_tabs', locals: { selected: 1 }}) + =render partial: 'form' diff --git a/app/views/page/new.html.slim b/app/views/page/new.html.slim index 2d0b755d74..d8f0fd5b54 100644 --- a/app/views/page/new.html.slim +++ b/app/views/page/new.html.slim @@ -2,7 +2,7 @@ h1 =t('.add_new_page') p =t('.choose_a_title') - =form_for(@page, { :url => { :action => 'create' }}) do |f| + =form_for @page, url: page_index_path, method: :post do |f| =hidden_field_tag(:work_id, @page.work.id) =validation_summary @page.errors table.form diff --git a/app/views/work/pages_tab.html.slim b/app/views/work/pages_tab.html.slim index b83efdfad4..ebcdb8f081 100644 --- a/app/views/work/pages_tab.html.slim +++ b/app/views/work/pages_tab.html.slim @@ -3,7 +3,7 @@ .work-columns.columns .work-columns_left= t('.pages_tab_description') .work-columns_right - =link_to t('.add_new_page'), page_new_path(work_id: @work.id), :data => { litebox: { hash: 'create-page' }}, class: 'button' + =link_to t('.add_new_page'), new_page_path(work_id: @work.id), data: { litebox: { hash: 'create-page' } }, class: 'button' -if @work.pages.empty? .nodata @@ -24,7 +24,7 @@ td.nowrap.fgfaded -if page.base_image.empty? =svg_symbol '#icon-warning-sign', class: 'icon' - ==t('.upload_page_image', upload: (link_to t('.upload'), { controller: 'page', action: 'edit', page_id: page.id })) + ==t('.upload_page_image', upload: (link_to t('.upload'), collection_edit_page_path(@collection.owner, @collection, @work, page.id))) -else =svg_symbol '#icon-check-sign', class: 'icon a50' ==t('.ready_to_transcribe', transcribe: (link_to t('.transcribe'), { controller: 'transcribe', action: 'display_page', page_id: page.id })) @@ -32,14 +32,14 @@ -if @work.featured_page == page.id b       #{t('.featured_page')}     -else - =link_to t('.make_featured_page'), work_update_featured_page_path(page_id: page.id) + =link_to t('.make_featured_page'), work_update_featured_page_path(page_id: page.id), method: :post | |  - =link_to t('.settings'), page_edit_path(page_id: page.id) + =link_to t('.settings'), collection_edit_page_path(@collection.owner, @collection, @work, page.id) | |  - =link_to t('.delete'), page_delete_path(page_id: page.id), data: { :confirm => t('.confirm_delete_page') } + =link_to t('.delete'), page_path(page), method: :delete, data: { confirm: t('.confirm_delete_page') } td.nowrap .work-page-position - =link_to(page_reorder_page_path(direction: 'up', page_id: page.id), title: t('.move_up'), 'aria-label' => t('.move_up')) + =link_to reorder_page_index_path(direction: 'up', page_id: page.id), method: :post, title: t('.move_up'), aria: { label: t('.move_up') } do =svg_symbol '#icon-arrow-top', class: 'icon', title: 'Move up' - =link_to({ controller: 'page', action: 'reorder_page', direction: 'down', page_id: page.id }, title: t('.move_down'), 'aria-label' => t('.move_down')) + =link_to reorder_page_index_path(direction: 'down', page_id: page.id), method: :post, title: t('.move_down'), aria: { label: t('.move_down') } do =svg_symbol '#icon-arrow-bottom', class: 'icon', title: 'Move down' diff --git a/config/locales/activerecord/activerecord-de.yml b/config/locales/activerecord/activerecord-de.yml index c66a73ecb5..ebcde4efe8 100644 --- a/config/locales/activerecord/activerecord-de.yml +++ b/config/locales/activerecord/activerecord-de.yml @@ -9,6 +9,8 @@ de: document_set: slug: URL title: Titel + page: + base_image: Seitenbild work: slug: URL title: Titel diff --git a/config/locales/activerecord/activerecord-en.yml b/config/locales/activerecord/activerecord-en.yml index 85d2039083..b0a3d0b2ff 100644 --- a/config/locales/activerecord/activerecord-en.yml +++ b/config/locales/activerecord/activerecord-en.yml @@ -9,6 +9,8 @@ en: document_set: slug: URL title: Title + page: + base_image: Page image work: slug: URL title: Title diff --git a/config/locales/activerecord/activerecord-es.yml b/config/locales/activerecord/activerecord-es.yml index e6c185cdc2..a6af5797fc 100644 --- a/config/locales/activerecord/activerecord-es.yml +++ b/config/locales/activerecord/activerecord-es.yml @@ -9,6 +9,8 @@ es: document_set: slug: URL title: Título + page: + base_image: Imagen de la página work: slug: URL title: Título diff --git a/config/locales/activerecord/activerecord-fr.yml b/config/locales/activerecord/activerecord-fr.yml index c22fbcea94..1b1ef76e58 100644 --- a/config/locales/activerecord/activerecord-fr.yml +++ b/config/locales/activerecord/activerecord-fr.yml @@ -9,6 +9,8 @@ fr: document_set: slug: URL title: Titre + page: + base_image: Image de la page work: slug: URL title: Titre diff --git a/config/locales/activerecord/activerecord-pt.yml b/config/locales/activerecord/activerecord-pt.yml index b2ab02bc86..f997ec1727 100644 --- a/config/locales/activerecord/activerecord-pt.yml +++ b/config/locales/activerecord/activerecord-pt.yml @@ -9,6 +9,8 @@ pt: document_set: slug: URL title: Titulo + page: + base_image: Imagem da página work: slug: URL title: Titulo diff --git a/config/locales/collection/collection-de.yml b/config/locales/collection/collection-de.yml index a870347fee..5e66e60ce6 100644 --- a/config/locales/collection/collection-de.yml +++ b/config/locales/collection/collection-de.yml @@ -94,7 +94,6 @@ de: del_description: Markiert Text, der durch Löschen oder Durchstreichen entfernt wurde.
Empfohlen del_label: Streichung description: FromThePage kann Auszeichnungen in Transkripten für TEI-XML oder HTML unterstützen. Wir empfehlen, nicht mehr als sechs Buttons auszuwählen, die im Transkriptionseditor angezeigt werden. Überlegen Sie, ob andere Systeme diese Art von Mark-up verwenden können; wenn nur plain text unterstützt wird, sollten Projekte die Buchdruck-Konventionen anstelle von Mark-up verwenden. - documentation: Dokumentation expan_description: Kennzeichnet erweiterte Abkürzungen mit Originalformen im orig-Attribut (Alternative Form des abbr-Tags). expan_label: Erweiterung fig_description: Kennzeichnet eine Abbildung oder eine horizontale Linie. diff --git a/config/locales/collection/collection-en.yml b/config/locales/collection/collection-en.yml index 7931e71ac5..6aecbcb3c0 100644 --- a/config/locales/collection/collection-en.yml +++ b/config/locales/collection/collection-en.yml @@ -94,7 +94,6 @@ en: del_description: Marks text that has been removed by erasure or strike-through.
Recommended del_label: Deletion description: FromThePage can support mark-up within transcripts in either TEI-XML or HTML forms. We recommend configuring no more than six buttons to appear on the transcription editor. Consider whether other systems will be able to use this kind of mark-up; if only plaintext is supported, projects should use letterpress conventions instead of mark-up. - documentation: Documentation expan_description: Marks expanded abbreviations with original forms in the orig attribute. (Alternate form of abbr tag.) expan_label: Expansion fig_description: Indicates a figure or a horizontal line. diff --git a/config/locales/collection/collection-es.yml b/config/locales/collection/collection-es.yml index 80c207e96e..bc11cd08b3 100644 --- a/config/locales/collection/collection-es.yml +++ b/config/locales/collection/collection-es.yml @@ -94,7 +94,6 @@ es: del_description: Marca el texto que ha sido eliminado por borrado o tachado.
Recomendado del_label: Supresión description: FromThePage puede admitir el marcado dentro de las transcripciones en formularios TEI-XML o HTML. Recomendamos configurar no más de seis botones para que aparezcan en el editor de transcripción. Considere si otros sistemas podrán usar este tipo de margen; si solo se admite texto sin formato, los proyectos deben usar convenciones tipográficas en lugar de marcado. - documentation: Documentación expan_description: Marca abreviaturas expandidas con formas originales en el atributo orig. (Forma alternativa de etiqueta abbr.) expan_label: Expansión fig_description: Indica una figura o una línea horizontal. diff --git a/config/locales/collection/collection-fr.yml b/config/locales/collection/collection-fr.yml index 1999f7ba07..0907451479 100644 --- a/config/locales/collection/collection-fr.yml +++ b/config/locales/collection/collection-fr.yml @@ -94,7 +94,6 @@ fr: del_description: Marque le texte qui a été supprimé par effacement ou barré.
Recommandé del_label: Effacement description: FromThePage peut prendre en charge le balisage dans les transcriptions dans les formulaires TEI-XML ou HTML. Nous vous recommandons de ne pas configurer plus de six boutons à afficher sur l'éditeur de transcription. Demandez-vous si d'autres systèmes pourront utiliser ce type de majoration ; si seul le texte brut est pris en charge, les projets doivent utiliser les conventions typographiques au lieu du balisage. - documentation: Documentation expan_description: Marque les abréviations développées avec des formes originales dans l'attribut orig. (Forme alternative de la balise abbr.) expan_label: Expansion fig_description: Indique un chiffre ou une ligne horizontale. diff --git a/config/locales/collection/collection-pt.yml b/config/locales/collection/collection-pt.yml index 01167d8f29..94f94de4de 100644 --- a/config/locales/collection/collection-pt.yml +++ b/config/locales/collection/collection-pt.yml @@ -94,7 +94,6 @@ pt: del_description: Marca o texto que foi removido por apagamento ou tachado.
Recomendado del_label: Eliminação description: FromThePage pode suportar marcação em transcrições em formulários TEI-XML ou HTML. Recomendamos configurar no máximo seis botões para aparecer no editor de transcrição. Considere se outros sistemas poderão usar esse tipo de marcação; se apenas texto simples for suportado, os projetos devem usar convenções de tipografia em vez de marcação. - documentation: Documentação expan_description: Marca abreviações expandidas com formas originais no atributo orig. (Forma alternativa de tag abbr.) expan_label: Expansão fig_description: Indica uma figura ou uma linha horizontal. diff --git a/config/locales/errors/errors-de.yml b/config/locales/errors/errors-de.yml index ae4c8ee645..0f997a25dc 100644 --- a/config/locales/errors/errors-de.yml +++ b/config/locales/errors/errors-de.yml @@ -1,4 +1,6 @@ --- de: errors: + error: Da ist etwas schiefgelaufen html_syntax_error: ungültige html-syntax + unsupported_file_type: nicht unterstützter Dateityp diff --git a/config/locales/errors/errors-en.yml b/config/locales/errors/errors-en.yml index 8288588128..a7fd1be0cc 100644 --- a/config/locales/errors/errors-en.yml +++ b/config/locales/errors/errors-en.yml @@ -1,4 +1,6 @@ --- en: errors: + error: Something went wrong html_syntax_error: invalid html syntax + unsupported_file_type: unsupported file type diff --git a/config/locales/errors/errors-es.yml b/config/locales/errors/errors-es.yml index 3dcb2cd1f4..1c16c393b2 100644 --- a/config/locales/errors/errors-es.yml +++ b/config/locales/errors/errors-es.yml @@ -1,4 +1,6 @@ --- es: errors: + error: Algo salió mal html_syntax_error: sintaxis html inválida + unsupported_file_type: tipo de archivo no compatible diff --git a/config/locales/errors/errors-fr.yml b/config/locales/errors/errors-fr.yml index d6bf0665af..f4b0332a9d 100644 --- a/config/locales/errors/errors-fr.yml +++ b/config/locales/errors/errors-fr.yml @@ -1,4 +1,6 @@ --- fr: errors: + error: Quelque chose s'est mal passé html_syntax_error: syntaxe html invalide + unsupported_file_type: type de fichier non pris en charge diff --git a/config/locales/errors/errors-pt.yml b/config/locales/errors/errors-pt.yml index 11a64c1a5a..ffd31e83c9 100644 --- a/config/locales/errors/errors-pt.yml +++ b/config/locales/errors/errors-pt.yml @@ -1,4 +1,6 @@ --- pt: errors: + error: Algo deu errado html_syntax_error: sintaxe html inválida + unsupported_file_type: tipo de arquivo não suportado diff --git a/config/locales/page/page-de.yml b/config/locales/page/page-de.yml index e5accae259..634d3a2d17 100644 --- a/config/locales/page/page-de.yml +++ b/config/locales/page/page-de.yml @@ -3,23 +3,28 @@ de: page: create: page_created: Seite erfolgreich erstellt - delete: + destroy: page_deleted: Seite wurde gelöscht edit: + status: 'Status:' + edit_tabs: + general: Allgemein + htr: HTR + form: browse: Durchsuchen click_to_browse_a_file: Klicken Sie hier, um eine Datei zu durchsuchen... created: "%{date} erstellt" + delete: Seite löschen delete_confirm_message: Möchten Sie diese Seite wirklich löschen? Nach dem Löschen können Sie sie nicht wiederherstellen! - delete_page: Seite löschen dimensions: 'Maße: %{width} × %{height}' here_you_can_edit: Hier können Sie den Seitentitel bearbeiten und ein neues Bild hochladen. Wenn Sie die Transkription oder den Übersetzungstext bearbeiten möchten, wechseln Sie bitte oberhalb auf die entsprechende Registerkarte. page_image: Faksimile page_position: 'Seitenposition: %{position}' page_status: Seitenstatus - page_status_: Nicht angefangen page_status_blank: Leere Seite page_status_incomplete: Unvollständig page_status_indexed: Indexiert + page_status_new: Nicht angefangen page_status_review: Überprüfung erforderlich page_status_transcribed: Vollständig page_status_translated: Übersetzt @@ -28,7 +33,6 @@ de: rotate_clockwise: Im Uhrzeigersinn drehen rotate_counterclockwise: Gegen den Uhrzeigersinn drehen save_changes: Änderungen speichern - status: 'Status:' new: add_new_page: Neue Seite hinzufügen browse: Durchsuchen diff --git a/config/locales/page/page-en.yml b/config/locales/page/page-en.yml index eb3da2313e..77206ff486 100644 --- a/config/locales/page/page-en.yml +++ b/config/locales/page/page-en.yml @@ -3,23 +3,28 @@ en: page: create: page_created: Page created successfully - delete: + destroy: page_deleted: Page has been deleted edit: + status: 'Status: ' + edit_tabs: + general: General + htr: HTR + form: browse: Browse click_to_browse_a_file: Click to browse a file... created: Created %{date} + delete: Delete Page delete_confirm_message: Are you sure you want to delete this page? After deleting you won't be able to recover it! - delete_page: Delete Page dimensions: 'Dimensions: %{width}×%{height}' here_you_can_edit: Here you can edit the page title and upload a new image. If you want to edit page transcription or translation text please switch to the appropriate tab above. page_image: Page Image page_position: 'Page Position: %{position}' page_status: Page Status - page_status_: Not Started page_status_blank: Blank Page page_status_incomplete: Incomplete page_status_indexed: Indexed + page_status_new: Not Started page_status_review: Needs Review page_status_transcribed: Complete page_status_translated: Translated @@ -28,7 +33,6 @@ en: rotate_clockwise: Rotate Clockwise rotate_counterclockwise: Rotate Counterclockwise save_changes: Save Changes - status: 'Status: ' new: add_new_page: Add New Page browse: Browse diff --git a/config/locales/page/page-es.yml b/config/locales/page/page-es.yml index bab022bbb2..7a6fabfd5f 100644 --- a/config/locales/page/page-es.yml +++ b/config/locales/page/page-es.yml @@ -3,23 +3,28 @@ es: page: create: page_created: Página creada con suceso - delete: + destroy: page_deleted: La página ha sido eliminada edit: + status: 'Estado:' + edit_tabs: + general: General + htr: HTR + form: browse: Navegar click_to_browse_a_file: Haz clic para consultar un archivo... created: Creado %{date} + delete: Eliminar Página delete_confirm_message: "¿De verdad deseas eliminar esta colección? ¡Después de eliminarla no podrá ser recuperada!" - delete_page: Eliminar Página dimensions: 'Dimensiones: %{width}×%{height}' here_you_can_edit: Aquí puedes editar el título de la página y subir una nueva imagen. Para editar la transcripción de la página o el texto de traducción, cambia a la pestaña correspondiente arriba. page_image: Imagen de la Página page_position: 'Posición de Página: %{position}' page_status: Estado de la página - page_status_: No empezado page_status_blank: Página en blanco page_status_incomplete: Incompleto page_status_indexed: Indexado + page_status_new: No empezado page_status_review: Revisión de necesidades page_status_transcribed: Completo page_status_translated: Traducido @@ -28,7 +33,6 @@ es: rotate_clockwise: Girar en Sentido Horario rotate_counterclockwise: Girar Hacia la Izquierda save_changes: Guardar Cambios - status: 'Estado:' new: add_new_page: Añadir Nueva Página browse: Navegar diff --git a/config/locales/page/page-fr.yml b/config/locales/page/page-fr.yml index 723cd8b594..a593f41ed0 100644 --- a/config/locales/page/page-fr.yml +++ b/config/locales/page/page-fr.yml @@ -3,23 +3,28 @@ fr: page: create: page_created: Page créée avec succès - delete: + destroy: page_deleted: La page a été supprimée edit: + status: 'Statut:' + edit_tabs: + general: Général + htr: HTR + form: browse: Parcourir click_to_browse_a_file: Cliquer pour parcourir un fichier... created: Créée le %{date} + delete: Supprimer la page delete_confirm_message: Voulez-vous vraiment supprimer cette page ? Après la suppression, vous ne pourrez pas le récupérer ! - delete_page: Supprimer la page dimensions: 'Dimensions : %{width}×%{height}' here_you_can_edit: Ici, vous pouvez modifier le titre de la page et téléverser une nouvelle image. Si vous souhaitez modifier la transcription de la page ou le texte de traduction, veuillez passer à l'onglet approprié ci-dessus. page_image: Image de la page page_position: 'Position de la page : %{position}' page_status: Statut de la page - page_status_: Non-trancrite page_status_blank: Page vierge page_status_incomplete: Incomplet page_status_indexed: Indexé + page_status_new: Non-trancrite page_status_review: À besoin d'une révision page_status_transcribed: Complet page_status_translated: Traduit @@ -28,7 +33,6 @@ fr: rotate_clockwise: Tourner dans le sens horaire rotate_counterclockwise: Tourner dans le sens antihoraire save_changes: Sauvegarder les modifications - status: 'Statut:' new: add_new_page: Ajouter une nouvelle page browse: Parcourir diff --git a/config/locales/page/page-pt.yml b/config/locales/page/page-pt.yml index a371a747a2..c40c9fac68 100644 --- a/config/locales/page/page-pt.yml +++ b/config/locales/page/page-pt.yml @@ -3,23 +3,28 @@ pt: page: create: page_created: Esta página foi criada com sucesso - delete: + destroy: page_deleted: Esta página foi eliminada edit: + status: 'Status:' + edit_tabs: + general: Em geral + htr: HTR + form: browse: Navegar click_to_browse_a_file: Clique para navegar num arquivo... created: Criada %{date} + delete: Apagar Página delete_confirm_message: Tem certeza que deseja apagar esta página? Depois de apagar não pode ser recuperada! - delete_page: Apagar Página dimensions: 'Dimensões: %{width}×%{height}' here_you_can_edit: Aqui você pode editar o título da página e subir uma nova imagem. Se quiser editar a transcrição da página ou o texto da tradução, por favor vá para a aba apropriada acima. page_image: Imagem da Página page_position: 'Posição da Página: %{position}' page_status: Status da página - page_status_: não foi iniciado page_status_blank: Página em branco page_status_incomplete: Incompleto page_status_indexed: Indexado + page_status_new: não foi iniciado page_status_review: Precisa de revisão page_status_transcribed: Completo page_status_translated: Traduzido @@ -28,7 +33,6 @@ pt: rotate_clockwise: Girar Sentido Horário rotate_counterclockwise: Girar Sentido Anti-Horário save_changes: Salvar Alterações - status: 'Status:' new: add_new_page: Adicionar Página Nova browse: Navegar diff --git a/config/routes.rb b/config/routes.rb index 72a5392f14..69235cd8c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -132,7 +132,7 @@ scope 'work', as: 'work' do get 'delete', to: 'work#delete' - get 'update_featured_page', to: 'work#update_featured_page' + post 'update_featured_page', to: 'work#update_featured_page' get 'pages_tab', to: 'work#pages_tab' get 'edit', to: 'work#edit' get ':collection_id/:work_id/edit_scribes', to: 'work#edit_scribes', as: 'edit_scribes' @@ -144,14 +144,9 @@ get 'document_sets_select', to: 'work#document_sets_select' end - scope 'page', as: 'page' do - get 'new', to: 'page#new' - get 'delete', to: 'page#delete' - get 'reorder_page', to: 'page#reorder_page' - get 'edit', to: 'page#edit' - get 'rotate', to: 'page#rotate' - post 'update', to: 'page#update' - post 'create', to: 'page#create' + resources :page, except: [:index, :show, :edit], param: :page_id do + post :reorder, on: :collection + post :rotate, on: :collection end scope 'article', as: 'article' do diff --git a/spec/factories/page.rb b/spec/factories/page.rb index 95ae27b6ee..f0a31b41dd 100644 --- a/spec/factories/page.rb +++ b/spec/factories/page.rb @@ -12,6 +12,12 @@ end factory :transcribed_page, :traits => [:transcribed] factory :page_with_links, :traits => [:with_links] + + trait :with_image do + base_image { Rails.root.join('test_data/images/pages/sanskrit.jpg') } + base_width { 1581 } + base_height { 570 } + end end end diff --git a/spec/interactors/page/create_spec.rb b/spec/interactors/page/create_spec.rb new file mode 100644 index 0000000000..ee831cce55 --- /dev/null +++ b/spec/interactors/page/create_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Page::Create do + let(:owner) { User.first } + let(:collection) { create(:collection, owner_user_id: owner.id) } + let(:work) { create(:work, collection: collection) } + + let(:page_params) { { title: 'New page' } } + + let(:result) do + described_class.call(work: work, page_params: page_params) + end + + it 'creates new page' do + expect(result.success?).to be_truthy + expect(result.page).to have_attributes( + title: 'New page', + base_image: '', + work_id: work.id + ) + end + + context 'with valid image' do + let(:file_path) { Rails.root.join('test_data/images/pages/sanskrit.jpg') } + let(:file_type) { 'image/jpeg' } + let(:page_params) do + { + title: 'New page', + base_image: Rack::Test::UploadedFile.new(file_path, file_type) + } + end + + it 'creates new page' do + expect(result.success?).to be_truthy + expect(result.page).to have_attributes( + title: 'New page', + base_image: Rails.root.join("public/images/working/upload/#{result.page.id}.jpg").to_s, + work_id: work.id + ) + end + end + + context 'with invalid image' do + let(:file_path) { Rails.root.join('test_data/transcripts/sanskrit.txt') } + let(:file_type) { 'text/plain' } + let(:page_params) do + { + title: 'New page', + base_image: Rack::Test::UploadedFile.new(file_path, file_type) + } + end + + it 'fails to create new page' do + expect(result).to be_a_failure + + expect(result.page.persisted?).to be_falsey + expect(result.errors).to eq('unsupported file type') + end + end +end diff --git a/spec/interactors/page/destroy_spec.rb b/spec/interactors/page/destroy_spec.rb new file mode 100644 index 0000000000..836b79ceac --- /dev/null +++ b/spec/interactors/page/destroy_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Page::Destroy do + let(:owner) { User.first } + let(:collection) { create(:collection, owner_user_id: owner.id) } + let(:work) { create(:work, collection: collection) } + let!(:page) { create(:page, :with_image, work: work, status: :new) } + + let(:result) do + described_class.call(page: page) + end + + it 'deletes page' do + expect(result.success?).to be_truthy + expect(result.page.destroyed?).to be_truthy + end +end diff --git a/spec/interactors/page/reorder_spec.rb b/spec/interactors/page/reorder_spec.rb new file mode 100644 index 0000000000..9110f4d4db --- /dev/null +++ b/spec/interactors/page/reorder_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Page::Reorder do + let(:owner) { User.first } + let(:collection) { create(:collection, owner_user_id: owner.id) } + let(:work) { create(:work, collection: collection) } + let!(:page_1) { create(:page, :with_image, work: work, position: 1) } + let!(:page_2) { create(:page, :with_image, work: work, position: 2) } + let!(:page_3) { create(:page, :with_image, work: work, position: 3) } + let(:direction) {} + + let(:result) do + described_class.call(page: page_2, direction: direction) + end + + context 'it moves up' do + let(:direction) { 'up' } + + it 'reorders pages' do + expect(result.success?).to be_truthy + + expect(page_2.reload.position).to eq(1) + expect(page_1.reload.position).to eq(2) + expect(page_3.reload.position).to eq(3) + end + end + + context 'it moves down' do + let(:direction) { 'down' } + + it 'reorders pages' do + expect(result.success?).to be_truthy + + expect(page_1.reload.position).to eq(1) + expect(page_3.reload.position).to eq(2) + expect(page_2.reload.position).to eq(3) + end + end +end diff --git a/spec/interactors/page/rotate_spec.rb b/spec/interactors/page/rotate_spec.rb new file mode 100644 index 0000000000..3971504d6d --- /dev/null +++ b/spec/interactors/page/rotate_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Page::Rotate do + let(:owner) { User.first } + let(:collection) { create(:collection, owner_user_id: owner.id) } + let(:work) { create(:work, collection: collection) } + let!(:page) { create(:page, :with_image, work: work, position: 1) } + let(:original_base_width) { 1581 } + let(:original_base_height) { 570 } + let(:orientation) { 0 } + + let(:result) do + described_class.call(page: page, orientation: orientation) + end + + it 'no changes' do + expect(result.success?).to be_truthy + + expect(page.reload).to have_attributes( + base_width: original_base_width, + base_height: original_base_height + ) + end + + context '90 degrees' do + let(:orientation) { 90 } + + it 'rotates image' do + expect(result.success?).to be_truthy + + expect(page.reload).to have_attributes( + base_width: original_base_height, + base_height: original_base_width + ) + end + end + + context '270 degrees' do + let(:orientation) { 270 } + + it 'rotates image' do + expect(result.success?).to be_truthy + + expect(page.reload).to have_attributes( + base_width: original_base_width, + base_height: original_base_height + ) + end + end +end diff --git a/spec/interactors/page/update_spec.rb b/spec/interactors/page/update_spec.rb new file mode 100644 index 0000000000..9b240d7969 --- /dev/null +++ b/spec/interactors/page/update_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Page::Update do + let(:owner) { User.first } + let(:collection) { create(:collection, owner_user_id: owner.id) } + let(:work) { create(:work, collection: collection) } + let(:work_statistic) { create(:work_statistic, work: work) } + let!(:page) { create(:page, :with_image, title: 'Original title', work: work, status: :blank) } + let(:page_params) { { title: 'Updated title', status: :new, translation_status: :new } } + + let(:result) do + described_class.call(page: page, page_params: page_params) + end + + it 'updates page' do + expect(result.success?).to be_truthy + expect(result.page).to have_attributes( + title: 'Updated title', + work_id: work.id, + status: 'new', + translation_status: 'new' + ) + end + + context 'with valid image' do + let(:file_path) { Rails.root.join('test_data/images/pages/sanskrit.jpg') } + let(:file_type) { 'image/jpeg' } + let(:page_params) do + { + title: 'Updated title', + base_image: Rack::Test::UploadedFile.new(file_path, file_type) + } + end + + it 'updates page' do + expect(result.success?).to be_truthy + expect(result.page).to have_attributes( + title: 'Updated title', + base_image: Rails.root.join("public/images/working/upload/#{result.page.id}.jpg").to_s, + work_id: work.id, + status: 'new', + translation_status: 'new' + ) + end + end + + context 'with invalid image' do + let(:file_path) { Rails.root.join('test_data/transcripts/sanskrit.txt') } + let(:file_type) { 'text/plain' } + let(:page_params) do + { + title: 'New page', + base_image: Rack::Test::UploadedFile.new(file_path, file_type) + } + end + + it 'fails to create new page' do + expect(result).to be_a_failure + + expect(result.errors).to eq('unsupported file type') + end + end +end diff --git a/spec/interactors/work/refresh_metadata_spec.rb b/spec/interactors/work/refresh_metadata_spec.rb index 4076072a2d..6d65fbf48f 100644 --- a/spec/interactors/work/refresh_metadata_spec.rb +++ b/spec/interactors/work/refresh_metadata_spec.rb @@ -15,9 +15,10 @@ let(:sc_manifest) { ScManifest.manifest_for_v3_hash(v3_hash) } let(:work) { create(:work, collection: collection, sc_manifest: sc_manifest) } + let(:work_no_manifest) { create(:work, collection: collection) } let(:result) do - described_class.new(work_ids: [work.id]).call + described_class.new(work_ids: [work.id, work_no_manifest.id]).call end context 'when original metadata is blank' do diff --git a/spec/requests/page_controller_spec.rb b/spec/requests/page_controller_spec.rb new file mode 100644 index 0000000000..689e9753c8 --- /dev/null +++ b/spec/requests/page_controller_spec.rb @@ -0,0 +1,253 @@ +require 'spec_helper' + +describe PageController do + let(:owner) { User.first } + let(:collection) { create(:collection, owner_user_id: owner.id) } + let(:work) { create(:work, collection: collection) } + let!(:page) { create(:page, :with_image, work: work, status: :new) } + + describe '#new' do + let(:action_path) { new_page_path(work_id: work.id) } + + let(:subject) { get action_path } + + it 'renders status and template' do + login_as owner + subject + + expect(response).to have_http_status(:ok) + expect(response).to render_template(:new) + end + end + + describe '#create' do + let(:action_path) { page_index_path } + + let(:file_path) { Rails.root.join('test_data/images/pages/sanskrit.jpg') } + let(:file_type) { 'image/jpeg' } + let(:page_params) do + { + title: 'Page title', + base_image: Rack::Test::UploadedFile.new(file_path, file_type) + } + end + let(:subaction) { '' } + let(:params) do + { work_id: work.id, page: page_params, subaction: subaction } + end + + let(:subject) { post action_path, params: params } + + context 'correct params' do + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to( + work_pages_tab_path(work_id: work.id, anchor: 'create-page') + ) + end + + context 'with subaction' do + let(:subaction) { 'save_and_new' } + + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to( + dashboard_startproject_path(anchor: 'create-work') + ) + end + end + end + + context 'incorrect params' do + let(:file_path) { Rails.root.join('test_data/transcripts/sanskrit.txt') } + let(:file_type) { 'text/plain' } + + it 'renders status and template' do + login_as owner + subject + + expect(response).to have_http_status(:unprocessable_entity) + expect(response).to render_template(:new) + end + end + end + + describe '#edit' do + let(:action_path) do + collection_edit_page_path(owner, collection, work, page.id) + end + + let(:subject) { get action_path } + + context 'user not logged in' do + it 'redirects' do + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(dashboard_path) + end + end + + context 'user not owner' do + let(:unique_id) { Time.current.to_i } + let(:user) do + create( + :user, + login: "user_#{unique_id}", + email: "user_#{unique_id}@sample.com" + ) + end + + it 'redirects' do + login_as user + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(dashboard_path) + end + end + + context 'user is owner' do + it 'renders status and template' do + login_as owner + subject + + expect(response).to have_http_status(:ok) + expect(response).to render_template(:edit) + end + end + end + + describe '#update' do + let(:action_path) { page_path(page) } + + let(:file_path) { Rails.root.join('test_data/images/pages/sanskrit.jpg') } + let(:file_type) { 'image/jpeg' } + let(:params) do + { + page: { + title: 'Page title', + base_image: Rack::Test::UploadedFile.new(file_path, file_type) + } + } + end + + let(:subject) { put action_path, params: params } + + context 'correct params' do + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to( + collection_edit_page_path(owner, collection, work, page) + ) + end + + context 'as xhr' do + let(:headers) { { 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest' } } + let(:subject) { put action_path, params: params, headers: headers } + + it 'renders json' do + login_as owner + subject + + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)['success']).to be_truthy + end + end + end + + context 'incorrect params' do + let(:file_path) { Rails.root.join('test_data/transcripts/sanskrit.txt') } + let(:file_type) { 'text/plain' } + + it 'renders status and template' do + login_as owner + subject + + expect(response).to have_http_status(:unprocessable_entity) + expect(response).to render_template(:edit) + end + + context 'as xhr' do + let(:headers) { { 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest' } } + let(:subject) { put action_path, params: params, headers: headers } + + it 'renders json' do + login_as owner + subject + + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)['success']).to be_falsey + expect(JSON.parse(response.body)['errors']).to eq( + 'unsupported file type' + ) + end + end + end + end + + describe '#destroy' do + let(:action_path) { page_path(page) } + + let(:subject) { delete action_path } + + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(work_pages_tab_path(work_id: work.id)) + end + end + + describe '#rotate' do + let(:action_path) { rotate_page_index_path } + let(:params) { { page_id: page.id, orientation: 90 } } + + let(:subject) { post action_path, params: params } + + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(page) + end + + context 'no orientation param' do + let(:params) { { page_id: page.id } } + + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(page) + end + end + end + + describe '#reorder' do + let(:action_path) { reorder_page_index_path } + let(:params) { { page_id: page.id, direction: 'up' } } + + let(:subject) { post action_path, params: params } + + it 'redirects' do + login_as owner + subject + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(work_pages_tab_path(work_id: work.id)) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 579f7c6a6c..7254f1972e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,9 @@ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } +# Require lib files +Dir[Rails.root.join('lib/**/*.rb')].each { |f| require f } + # Checks for pending migrations before tests are run. # If you are not using ActiveRecord, you can remove this line. #ActiveRecord::Migration.maintain_test_schema!