# 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 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 # The version of the module, according to semantic versionning. # @return [String] The version of the module. def self.module_version VERSION end # The homepage 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,#group_types,#list_groups,#group_thumbnail,#list_pictures,#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 album to be displayed on the module home page (all # keys except +group_type+ may be omitted). # @param home_groups [Array] # The description of the groups to be displayed on the module home page. All groups of the # +group_type+ will be displayed and link to a page showing all groups of the +browse+ type. # @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+. # === REST API Description: # * Read-only access to pictures listings under */api/pictures* using +GET+ method, response # is in JSON format with the following structure: # [ # { "id": "...", "height": 480, "width": 640, "title": "...", "datetime": "...", ... }, # { ... } # ] # * Read-only access to groups listings under */api/groups/*+group_type+ using +GET+ method, # response is in JSON format with the following structure: # [ # { "id": "...", "title": "...", ... }, # { ... } # ] # @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. def generate_page(path, query) case path when %r{^/index\.html$} then serve_home when %r{^/browse_\w+\.html$} serve_groups(path.gsub(%r{^/browse_(\w+)\.html$}, '\\1'), query) when %r{^/api/} then serve_api(path.gsub(%r{^/api}, ''), query) else super(path, query) end end # 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 # Provides the list of Cascade Style Sheets (CSS) dependencies for this module. # @return [Array] The list of CSS dependencies. def css_dependencies super + ['design/style.css', 'design/photoswipe/photoswipe.css', 'design/photoswipe/default-skin/default-skin.css'] end # Provides the list of Javascript files (JS) dependencies for this module. # @return [Array] The list of JS dependencies. def js_dependencies super + ['design/jpictures.js', 'design/photoswipe/photoswipe.min.js', 'design/photoswipe/photoswipe-ui-default.min.js'] end private # Extract a selector from the given +query+. # @return [Hash] The picture or group selector. def selector(query) query.except('sort_by', 'sort_order') end # Extract the sort criteria from the given +query+. # @return [String] The key to use to sort pictures or groups, or nil if no sort order is # specified in +query+. def sort_by(query) query.fetch('sort_by') rescue KeyError nil end # Extract the sort order from the given +query+. # @return [Boolean] False if the pictures or groups should be sorted in descending order, # True 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 ########################################################################## ### Servicing of the HTML "display-able" content ### ########################################################################## def active_filters(query) selector(query).map do |k, v| { k => @provider.list_groups(k, { k => v }).first.fetch('title') } rescue KeyError { k => v } end.reduce({}, :merge) end def recent_groups @recents.map do |recent| groups = @provider.list_groups(recent[:group_type], {}, recent[:sort_by], recent[:asc]) see_more_url = '' if recent[:limit].to_i.positive? groups = groups.first(recent[:limit]) see_more_url = "browse_#{recent[:group_type]}.html?sort_by=#{recent[:sort_by]}" see_more_url += '&sort_order=desc' unless recent[:asc] end { group_type: recent[:group_type], groups: groups, see_more_url: see_more_url } end end def all_groups @home_groups.map do |section| groups = @provider.list_groups(section[:group_type], {}, section[:sort_by], section[:asc]) url_prefix = "browse_#{section[:browse]}.html?sort_by=#{section[:browse_sort_by]}" url_prefix += '&sort_order=desc' unless section[:browse_asc] { group_type: section[:group_type], groups: groups, url_prefix: url_prefix } end end def make_nav(group_type = nil, query = {}) h = { I18n.t('nav.home') => '/index.html', I18n.t('pictures.menu') => nil, title => nil } unless group_type.nil? h[title] = 'index.html' extra_key = I18n.t("pictures.nav.#{group_type}") filters = active_filters(query).values extra_key += " (#{filters.join(', ')})" unless filters.empty? h.store(extra_key, nil) end h end def gallery_url(group_type, group_id, filters = {}) filters.store(group_type, group_id) filters.store('sort_by', 'datetime') filters.map { |k, v| [k, v].join('=') }.join('&') end def serve_home content = to_markup('pictures_home', nav: make_nav) [206, 'text/html', { content: content, title: title }] rescue KeyError [404, '', ''] end def serve_groups(type, query) groups = @provider.list_groups(type, selector(query), sort_by(query), sort_order(query)) content = to_markup('pictures_browse', nav: make_nav(type, query), group_type: type, filters: selector(query), groups: groups) [206, 'text/html', { content: content, title: title }] rescue KeyError [404, '', ''] end ########################################################################## ### Servicing of the REST API (raw JSON data & pictures) ### ########################################################################## def api_list_groups(path, query) group_type = path.split('/')[2].to_s @provider.list_groups(group_type, selector(query), sort_by(query), sort_order(query)) end def api_group_thumbnail(path, query) group_type = path.split('/')[2].to_s pic = @provider.group_thumbnail(group_type, selector(query)) if pic.nil? pic = ['image/jpeg', File.read(File.join(resources_dir, 'www', 'group_thumbnail.jpg'))] end pic 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{^/groups/} then [200, 'application/json', api_list_groups(path, query).to_json] when %r{^/group/} then [200, api_group_thumbnail(path, query)].flatten 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