# frozen_string_literal: true require 'intranet/abstract_responder' require 'intranet/core/haml_wrapper' require 'intranet/core/locales' require 'json' require_relative 'version' module Intranet module Pictures # The responder for the Pictures monitor module of the Intranet. class Responder < AbstractResponder # rubocop:disable Metrics/ClassLength include Core::HamlWrapper # 'inherits' from methods of HamlWrapper # Returns the name of the module. # @return [String] The name of the module. def self.module_name NAME end # Returns the version of the module, according to semantic versionning. # @return [String] The version of the module. def self.module_version VERSION end # Returns the homepage URL of the module. # @return [String] The homepage URL of the module. def self.module_homepage HOMEPAGE_URL end # Initializes a new Pictures responder instance. # @param provider [#title,#list_pictures,#group_thumbnail,#group_brief,#picture] # The pictures provider. # @see Intranet::Pictures::JsonDbProvider The specification of the provider, and in particular # the minimal mandatory elements that must be returned by the operations. # @param recents [Array] # The description of the recent pictures to be displayed on the module home page. Pictures # will first be sorted according to +sort_by+ and +sort_order+, then grouped by +group_by+, # keeping only the first +limit+ elements, or all if +limit+ is zero. All keys except # +group_by+ may be omitted. # @param home_groups [Array] # The description of the pictures groups to be displayed on the module home page, after the # +recents+. Pictures will first be sorted according to +sort_by+ and +sort_order+, then # grouped by +group_by+. The obtained groups will be displayed with their thumbnail, # (optional) brief text and a link to display images of that group sorted according to # +browse_sort_by+ and +browse_sort_order+ and grouped by +browse_group_by+. All keys except # +group_by+ and +browse_group_by+ may be omitted. # @param in_menu [Boolean] Whether the module instance should be displayed in the main # navigation menu or not. def initialize(provider, recents = [], home_groups = [], in_menu = true) @provider = provider @recents = recents @in_menu = in_menu @home_groups = home_groups end # Specifies if the responder instance should be displayed in the main navigation menu or not. # @return [Boolean] True if the responder instance should be added to the main navigation # menu, False otherwise. def in_menu? @in_menu end # Specifies the absolute path to the resources directory for that module. # @return [String] The absolute path to the resources directory for the module. def resources_dir File.absolute_path(File.join('..', 'resources'), __dir__) end # Generates the HTML content associated to the given +path+ and +query+. # @param path [String] The requested URI, relative to that module root URI. # @param query [Hash] The URI variable/value pairs, if any. # @return [Integer, String, String] The HTTP return code, the MIME type and the answer body. # # Relevant queries are as follows: # # * If +path+ is +/browse.html+, key/value pairs may be used to restrict the displayed images # to those satisfying all the pairs. Moreover, +sort_by+ (followed by a key), +sort_order+ # (followed by either +asc+ or +desc+) and +group_by+ may be used to alter the way images # are displayed. # * If +path+ is +/api/group_thumbnail+ or +/api/group_brief+, query must contain only a # single key/value pair designating the pictures group. # * If +path+ is +/api/pictures+, key/value pairs may be used to restrict the displayed images # to those satisfying all the pairs. Moreover, +sort_by+ (followed by a key), and # +sort_order+ (followed by either +asc+ or +desc+) may be used to alter the order in which # images are returned. # * If +path+ is +api/picture+, key/value pairs must be used to select a single image from the # gallery. # # When +path+ is +/api/pictures+, the selected pictures are returned in JSON format (REST API) # with the following structure: # [ # { "id": "...", "height": 480, "width": 640, "title": "...", "datetime": "...", ... }, # { ... } # ] def generate_page(path, query) case path when %r{^/index\.html$} then serve_home when %r{^/browse\.html$} then serve_browse(query) when %r{^/api/} then serve_api(path.gsub(%r{^/api}, ''), query) when %r{^/i18n\.js$} then serve_i18n_js else super(path, query) end end # Returns the title of the Pictures module, as displayed on the web page. # @return [String] The title of the Pictures module web page. def title @provider.title end private # Extract a selector from the given +query+. # @return [Hash] The picture selector. def selector(query) query.except('group_by', 'sort_by', 'sort_order') end # Extract the grouping criteria from the given +query+, ie. the key that will be used to group # the selected pictures. # @return [String] The key to use to group pictures. # @raise KeyError If no grouping criteria is specified. def group_by(query) query.fetch('group_by') end # Extract the sorting criteria from the given +query+, ie. the key that will be used to sort # the selected pictures. # @return [String] The key to use to sort pictures, or nil if no sorting is specified. def sort_by(query) query.fetch('sort_by') rescue KeyError nil end # Extract the sorting order from the given +query+. # @return [Boolean] True if the pictures should be sorted in ascending order, False otherwise. # @raise KeyError If the query requests an invalid sort order. def sort_order(query) return false if query['sort_order'] == 'desc' return true if query['sort_order'].nil? || query['sort_order'] == 'asc' raise KeyError # incorrect value for 'sort_order' end # Provides the list of required stylesheets. # @return [Array] The list of required stylesheets. def stylesheets ['design/style.css', 'design/photoswipe/photoswipe.css', 'design/photoswipe/photoswipe-dynamic-caption-plugin.css'] end # Provides the list of required script files. # @return [Array>] The list of required scripts. def scripts [{ src: 'design/jpictures.js', type: 'module' }] end ########################################################################## ### Servicing of the HTML "display-able" content ### ########################################################################## def collect_groups(selector, group_by, sort_by, sort_order) # Select all pictures, sort them & eventually keep only group names (first occurrence only) @provider.list_pictures(selector, sort_by, sort_order).map do |picture| picture.fetch(group_by) end.uniq end def hash_to_query(hash) hash.map { |k, v| [k, v].join('=') }.join('&') end def recent_groups @recents.map do |recent| groups = collect_groups({}, group_by(recent), sort_by(recent), sort_order(recent)) see_more_url = '' if recent['limit'].to_i.positive? groups = groups.first(recent['limit'].to_i) see_more_url = "browse.html?#{hash_to_query(recent.except('limit'))}" end { group_key: group_by(recent), groups: groups, see_more_url: see_more_url } end end def all_groups @home_groups.map do |sec| groups = collect_groups({}, group_by(sec), sort_by(sec), sort_order(sec)) url_prefix = "browse.html?group_by=#{sec['browse_group_by']}" url_prefix += "&sort_by=#{sec['browse_sort_by']}" if sec['browse_sort_by'] url_prefix += "&sort_order=#{sec['browse_sort_order']}" if sec['browse_sort_order'] { group_key: group_by(sec), groups: groups, url_prefix: url_prefix } end end def make_nav(query = {}) h = { I18n.t('nav.home') => '/index.html', I18n.t('pictures.menu') => nil, title => nil } unless query['group_by'].nil? h[title] = 'index.html' extra_key = I18n.t("pictures.nav.#{group_by(query)}") filters = selector(query).values extra_key += " (#{filters.join(', ')})" unless filters.empty? h.store(extra_key, nil) end h end def gallery_url(key, value, filters = {}) filters.store(key, value) filters.store('sort_by', 'datetime') hash_to_query(filters) end def serve_home content = to_markup('pictures_home', nav: make_nav) [206, 'text/html', { content: content, title: title, stylesheets: stylesheets, scripts: scripts }] rescue KeyError [404, '', ''] end def serve_browse(query) groups = collect_groups(selector(query), group_by(query), sort_by(query), sort_order(query)) content = to_markup('pictures_browse', nav: make_nav(query), group_key: group_by(query), filters: selector(query), groups: groups) [206, 'text/html', { content: content, title: title, stylesheets: stylesheets, scripts: scripts }] rescue KeyError [404, '', ''] end def serve_i18n_js [200, 'text/javascript', "export default {\n" \ " viewer_close: '#{I18n.t('pictures.viewer.close')}',\n" \ " viewer_zoom: '#{I18n.t('pictures.viewer.zoom')}',\n" \ " viewer_previous: '#{I18n.t('pictures.viewer.previous')}',\n" \ " viewer_next: '#{I18n.t('pictures.viewer.next')}' };"] end ########################################################################## ### Servicing of the REST API (raw JSON data & pictures) ### ########################################################################## def api_group_thumbnail(query) raise KeyError unless query.size == 1 key, value = query.first pic = @provider.group_thumbnail(key, value) if pic.nil? pic = ['image/svg+xml', File.read(File.join(resources_dir, 'www', 'group_thumbnail.svg'))] end pic end def api_group_brief(query) raise KeyError unless query.size == 1 key, value = query.first @provider.group_brief(key, value) end def api_list_pictures(query) @provider.list_pictures(selector(query), sort_by(query), sort_order(query)) end def serve_api(path, query) case path when %r{^/group_thumbnail$} then [200, api_group_thumbnail(query)].flatten when %r{^/group_brief$} then [200, 'application/json', api_group_brief(query).to_json] when %r{^/pictures$} then [200, 'application/json', api_list_pictures(query).to_json] when %r{^/picture$} then [200, @provider.picture(query)].flatten else [404, '', ''] end rescue KeyError [404, '', ''] end end end end