diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..18d7ad1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +src/tmp +src/tmp/* +src/public/uploads +src/public/uploads/* +src/log +src/log/* +.env + diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..ea374d7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# Build on top of latest ruby-slim container +FROM ruby:slim + +MAINTAINER Team Charlie + +# Set up proxy settings +ARG http_proxy +ARG https_proxy +ENV http_proxy=$http_proxy +ENV https_proxy=$https_proxy + +# Set up proxy for the debian package manager +RUN echo 'Acquire::http::Proxy "'$http_proxy'";' > /etc/apt/apt.conf + +####################### +# Install dependencies +####################### +ENV LANG C.UTF-8 + +RUN apt-get update -qy +RUN apt-get upgrade -y +RUN apt-get update -qy +RUN apt-get install -y build-essential + +# for postgres +RUN apt-get install -y libpq-dev + +# for a JS runtime +RUN apt-get install -y nodejs + +# For ffi-1.9.18 gem +RUN apt-get install -y curl + +# For image conversion +RUN apt-get install -y imagemagick + +# Create the server direcotory in the container +RUN mkdir -p /lacr-search +WORKDIR /lacr-search + +# Add source files in the container +ADD src/Gemfile /lacr-search/Gemfile +ADD src/Gemfile.lock /lacr-search/Gemfile.lock + +# Install requered gems +RUN bundle install + + +# Disable the proxy settings. +# Otherwise ruby will try to connect to the other containers +# via this proxy. +RUN echo '' > /etc/apt/apt.conf +ENV http_proxy='' +ENV https_proxy='' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..5bd37dc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '2' + +services: + xmldb: + image: basex/basexhttp:latest + restart: unless-stopped + ports: + - 1984 + - 8984 + db: + image: postgres:latest + restart: unless-stopped + expose: + - 5433 + - 5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + es: + image: elasticsearch:latest + restart: unless-stopped + expose: + - 9200 + + web: + restart: unless-stopped + build: + context: . + dockerfile: Dockerfile + args: + - http_proxy + - https_proxy + + command: bundle exec rails s -p 80 -b '0.0.0.0' + volumes: + - ./src:/lacr-search + environment: + BASEX_ADMIN: ${BASEX_ADMIN} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + + ports: + - "80:80" + depends_on: + - es + - db + - xmldb diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..d862a82 --- /dev/null +++ b/setup.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Description: Shell-script to start rails server + +# To romove created containers execute "docker-compose down" + +read -r -p "Install new packages or gems? (Needed only for initial setup) [y/N] " response +if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]] +then + docker-compose build +fi + +#read -r -p "Open bash in the Ruby container? (Used for testing) [y/N] " response +#if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]] +#then +# docker-compose start +# docker exec -it lacrsearch_web_1 bash +# docker-compose stop +# exit +#fi + +read -r -p "Reset database? (Needed only for initial setup) [y/N] " response +if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]] +then + docker-compose run web bash -c "rails db:drop && rails db:create && rails db:migrate && rails db:seed" +fi +echo '' +echo 'Starting containers ....' +docker-compose up diff --git a/src/Gemfile b/src/Gemfile new file mode 100755 index 0000000..aea4d83 --- /dev/null +++ b/src/Gemfile @@ -0,0 +1,65 @@ +source 'https://rubygems.org' + +git_source(:github) do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://github.com/#{repo_name}.git" +end + +gem 'rspec' +gem 'lograge' +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails' +# Use PostgreSQL as the database for Active Record +gem 'pg' +# Use Puma as the app server +gem 'puma' +# Use SCSS for stylesheets +gem 'sass-rails' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier' +# See https://github.com/rails/execjs#readme for more supported runtimes +gem 'therubyracer', platforms: :ruby +gem 'coffee-rails' +gem 'jquery-rails' # Use jquery as the JavaScript library +gem 'jquery-ui-rails' # Add JavaScript for UI + +# Additional gems +# Ruby Gem of the Bootstrap +gem 'less-rails' # Javascript runtime +gem 'twitter-bootstrap-rails' +gem "font-awesome-rails" + +gem 'carrierwave' # File upload +gem 'groupdate' # Simple way to group by: day, week, etc. +gem 'rubyzip' # Zip files for download +gem 'mini_magick' # Image resizing +gem 'prawn' # PDF file generator +gem 'will_paginate-bootstrap' # Pagination library + +# API for Elasticsearch +gem 'searchkick' +gem 'oj' # Significantly increase performance with faster JSON generation. +gem 'typhoeus' # Significantly increase performance with persistent HTTP connections + +#User Authentication +gem 'devise' +gem 'devise-bootstrap-views' + +group :development do + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'web-console', '>= 3.3.0' + gem 'listen', '~> 3.0.5' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' + gem 'byebug' +end + +group :test do + gem 'cucumber-rails', :require => false + # database_cleaner is not required, but highly recommended + gem 'database_cleaner' + # for testing JavaScript, require older version selenium and firefox version 47.0.1 for more info check: + # https://github.com/teamcapybara/capybara#drivers + gem 'selenium-webdriver', '~> 2.53.4' +end \ No newline at end of file diff --git a/src/Gemfile.lock b/src/Gemfile.lock new file mode 100755 index 0000000..5ab8cb4 --- /dev/null +++ b/src/Gemfile.lock @@ -0,0 +1,343 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.2.2) + actionpack (= 5.2.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.2) + actionpack (= 5.2.2) + actionview (= 5.2.2) + activejob (= 5.2.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.2.2) + actionview (= 5.2.2) + activesupport (= 5.2.2) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.2.2) + activesupport (= 5.2.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.2.2) + activesupport (= 5.2.2) + globalid (>= 0.3.6) + activemodel (5.2.2) + activesupport (= 5.2.2) + activerecord (5.2.2) + activemodel (= 5.2.2) + activesupport (= 5.2.2) + arel (>= 9.0) + activestorage (5.2.2) + actionpack (= 5.2.2) + activerecord (= 5.2.2) + marcel (~> 0.3.1) + activesupport (5.2.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + arel (9.0.0) + backports (3.11.4) + bcrypt (3.1.12) + bindex (0.5.0) + builder (3.2.3) + byebug (10.0.2) + capybara (3.12.0) + addressable + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (~> 1.2) + xpath (~> 3.2) + carrierwave (1.2.3) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) + mime-types (>= 1.16) + childprocess (0.9.0) + ffi (~> 1.0, >= 1.0.11) + coffee-rails (4.2.2) + coffee-script (>= 2.2.0) + railties (>= 4.0.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + commonjs (0.2.7) + concurrent-ruby (1.1.3) + crass (1.0.4) + cucumber (3.1.2) + builder (>= 2.1.2) + cucumber-core (~> 3.2.0) + cucumber-expressions (~> 6.0.1) + cucumber-wire (~> 0.0.1) + diff-lcs (~> 1.3) + gherkin (~> 5.1.0) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.2) + cucumber-core (3.2.1) + backports (>= 3.8.0) + cucumber-tag_expressions (~> 1.1.0) + gherkin (~> 5.0) + cucumber-expressions (6.0.1) + cucumber-rails (1.6.0) + capybara (>= 1.1.2, < 4) + cucumber (>= 3.0.2, < 4) + mime-types (>= 1.17, < 4) + nokogiri (~> 1.8) + railties (>= 4, < 6) + cucumber-tag_expressions (1.1.1) + cucumber-wire (0.0.1) + database_cleaner (1.7.0) + devise (4.5.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0, < 6.0) + responders + warden (~> 1.2.3) + devise-bootstrap-views (1.1.0) + diff-lcs (1.3) + elasticsearch (6.1.0) + elasticsearch-api (= 6.1.0) + elasticsearch-transport (= 6.1.0) + elasticsearch-api (6.1.0) + multi_json + elasticsearch-transport (6.1.0) + faraday + multi_json + erubi (1.7.1) + ethon (0.11.0) + ffi (>= 1.3.0) + execjs (2.7.0) + faraday (0.15.4) + multipart-post (>= 1.2, < 3) + ffi (1.9.25) + font-awesome-rails (4.7.0.4) + railties (>= 3.2, < 6.0) + gherkin (5.1.0) + globalid (0.4.1) + activesupport (>= 4.2.0) + groupdate (4.1.0) + activesupport (>= 4.2) + hashie (3.6.0) + i18n (1.1.1) + concurrent-ruby (~> 1.0) + jquery-rails (4.3.3) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + jquery-ui-rails (6.0.1) + railties (>= 3.2.16) + less (2.6.0) + commonjs (~> 0.2.7) + less-rails (2.8.0) + actionpack (>= 4.0) + less (~> 2.6.0) + sprockets (> 2, < 4) + tilt + libv8 (3.16.14.19) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + lograge (0.10.0) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) + request_store (~> 1.0) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (0.3.3) + mimemagic (~> 0.3.2) + method_source (0.9.2) + mime-types (3.2.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2018.0812) + mimemagic (0.3.2) + mini_magick (4.9.2) + mini_mime (1.0.1) + mini_portile2 (2.3.0) + minitest (5.11.3) + multi_json (1.13.1) + multi_test (0.1.2) + multipart-post (2.0.0) + nio4r (2.3.1) + nokogiri (1.8.5) + mini_portile2 (~> 2.3.0) + oj (3.7.4) + orm_adapter (0.5.0) + pdf-core (0.7.0) + pg (1.1.3) + prawn (2.2.2) + pdf-core (~> 0.7.0) + ttfunk (~> 1.5) + public_suffix (3.0.3) + puma (3.12.0) + rack (2.0.6) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.2.2) + actioncable (= 5.2.2) + actionmailer (= 5.2.2) + actionpack (= 5.2.2) + actionview (= 5.2.2) + activejob (= 5.2.2) + activemodel (= 5.2.2) + activerecord (= 5.2.2) + activestorage (= 5.2.2) + activesupport (= 5.2.2) + bundler (>= 1.3.0) + railties (= 5.2.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.2.2) + actionpack (= 5.2.2) + activesupport (= 5.2.2) + method_source + rake (>= 0.8.7) + thor (>= 0.19.0, < 2.0) + rake (12.3.2) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + ref (2.0.0) + regexp_parser (1.3.0) + request_store (1.4.1) + rack (>= 1.4) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rubyzip (1.2.2) + sass (3.7.2) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sass-rails (5.0.7) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + searchkick (3.1.2) + activemodel (>= 4.2) + elasticsearch (>= 5) + hashie + selenium-webdriver (2.53.4) + childprocess (~> 0.5) + rubyzip (~> 1.0) + websocket (~> 1.0) + spring (2.0.2) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) + ref + thor (0.20.3) + thread_safe (0.3.6) + tilt (2.0.9) + ttfunk (1.5.1) + twitter-bootstrap-rails (4.0.0) + actionpack (~> 5.0, >= 5.0.1) + execjs (~> 2.7) + less-rails (~> 2.8, >= 2.8.0) + railties (~> 5.0, >= 5.0.1) + typhoeus (1.3.1) + ethon (>= 0.9.0) + tzinfo (1.2.5) + thread_safe (~> 0.1) + uglifier (4.1.20) + execjs (>= 0.3.0, < 3) + warden (1.2.8) + rack (>= 2.0.6) + web-console (3.7.0) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + websocket (1.2.8) + websocket-driver (0.7.0) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) + will_paginate (3.1.6) + will_paginate-bootstrap (1.0.1) + will_paginate (>= 3.0.3) + xpath (3.2.0) + nokogiri (~> 1.8) + +PLATFORMS + ruby + +DEPENDENCIES + byebug + carrierwave + coffee-rails + cucumber-rails + database_cleaner + devise + devise-bootstrap-views + font-awesome-rails + groupdate + jquery-rails + jquery-ui-rails + less-rails + listen (~> 3.0.5) + lograge + mini_magick + oj + pg + prawn + puma + rails + rspec + rubyzip + sass-rails + searchkick + selenium-webdriver (~> 2.53.4) + spring + spring-watcher-listen (~> 2.0.0) + therubyracer + twitter-bootstrap-rails + typhoeus + uglifier + web-console (>= 3.3.0) + will_paginate-bootstrap + +BUNDLED WITH + 1.17.1 diff --git a/src/README.md b/src/README.md new file mode 100755 index 0000000..c15edce --- /dev/null +++ b/src/README.md @@ -0,0 +1,29 @@ +# CS3028 Team Charlie + +LACR Search is Ruby on Rails web application that provides a convenient way to search within a corpus of transcribed documents stored in XML format. + +A primary goal for LACR Search is to contribute to the [Aberdeen Burgh Records Project](https://www.abdn.ac.uk/riiss/about/aberdeen-burgh-records-project-97.php) with user-friendly interface for easy navigation between pages, volumes as well as to enable search queries. + +Plain text search, suggestions, autocomplete, spelling variants are provided utilising the utilising the search engine [Elasticsearch](https://www.elastic.co/products/elasticsearch). + +LACR Search has been further extended with the ability for further analysis of the XML documents by providing support for [XQuery](https://www.w3schools.com/xml/xquery_intro.asp) expressions utilising [BaseX](http://basex.org/products/). + +## Getting Started +LACR Search is composed of several Docker containers providing independentce and isolation from the Host OS. + +1.Install [Docker](https://docs.docker.com/engine/installation/) +- [Debian](https://docs.docker.com/v1.12/engine/installation/linux/debian/) +- [Ubuntu](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-16-04#step-1-—-installing-docker) +- [Linux Mint](http://linuxbsdos.com/2016/12/13/how-to-install-docker-and-run-docker-containers-on-linux-mint-1818-1/) +- [Arch linux](https://wiki.archlinux.org/index.php/Docker#Installation) +- [Fedora](https://fedoraproject.org/wiki/Docker) + +2.Install [docker-compose](https://docs.docker.com/compose/install/) +```sh +pip install docker-compose +``` +3.Navigate to the project directory `lacr-search/` and execute the shell script `setup.sh` +```sh +./setup.sh +``` +4.Follow the instructions diff --git a/src/Rakefile b/src/Rakefile new file mode 100755 index 0000000..e85f913 --- /dev/null +++ b/src/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/src/app/assets/config/manifest.js b/src/app/assets/config/manifest.js new file mode 100755 index 0000000..b16e53d --- /dev/null +++ b/src/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/src/app/assets/images/.keep b/src/app/assets/images/.keep new file mode 100755 index 0000000..e69de29 diff --git a/src/app/assets/images/lacr-round.png b/src/app/assets/images/lacr-round.png new file mode 100755 index 0000000..7b5d38b Binary files /dev/null and b/src/app/assets/images/lacr-round.png differ diff --git a/src/app/assets/images/magnifying_glass_icon.png b/src/app/assets/images/magnifying_glass_icon.png new file mode 100755 index 0000000..ecac94a Binary files /dev/null and b/src/app/assets/images/magnifying_glass_icon.png differ diff --git a/src/app/assets/images/scroll-down.png b/src/app/assets/images/scroll-down.png new file mode 100755 index 0000000..d2aa1c5 Binary files /dev/null and b/src/app/assets/images/scroll-down.png differ diff --git a/src/app/assets/images/scroll-down@2x.png b/src/app/assets/images/scroll-down@2x.png new file mode 100755 index 0000000..3080d12 Binary files /dev/null and b/src/app/assets/images/scroll-down@2x.png differ diff --git a/src/app/assets/javascripts/application.js b/src/app/assets/javascripts/application.js new file mode 100755 index 0000000..4b2ef81 --- /dev/null +++ b/src/app/assets/javascripts/application.js @@ -0,0 +1,23 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery-ui +//= require jquery_ujs +//= require twitter/bootstrap +//= require jquery.noty.packaged.min.js +//= require jquery.fullPage.min.js +//= require scrolloverflow.min.js +//= require jquery.fullpage.extensions.min.js +//= require js.cookie.js +//= require ISO_639_2.min.js +//= require init.js diff --git a/src/app/assets/javascripts/bootstrap.js b/src/app/assets/javascripts/bootstrap.js new file mode 100755 index 0000000..1c4a1f7 --- /dev/null +++ b/src/app/assets/javascripts/bootstrap.js @@ -0,0 +1,4 @@ +jQuery(function() { + $("a[rel~=popover], .has-popover").popover(); + $("a[rel~=tooltip], .has-tooltip").tooltip(); +}); diff --git a/src/app/assets/javascripts/cable.js b/src/app/assets/javascripts/cable.js new file mode 100755 index 0000000..71ee1e6 --- /dev/null +++ b/src/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the rails generate channel command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/src/app/assets/javascripts/channels/.keep b/src/app/assets/javascripts/channels/.keep new file mode 100755 index 0000000..e69de29 diff --git a/src/app/assets/javascripts/documents.js b/src/app/assets/javascripts/documents.js new file mode 100755 index 0000000..302f241 --- /dev/null +++ b/src/app/assets/javascripts/documents.js @@ -0,0 +1,57 @@ +//= require jquery.zoom.min.js +//= require prettify.js +//= require xml2html.js +//= require ISO_639_2.min.js + +var jqxhr; + +// Load partial content +var load_document = function (p, v){ + $('#doc-title').html("Volume: "+v+" Page: "+p); + $("#transcription-image").load("/doc/page?p="+p+"&v="+v, function(responseTxt, statusTxt, xhr){ + if(statusTxt == "success"){ + // Transform language codes + $(".pr-language").each(function() { + try { + $(this).html(ISO_639_2[$(this).html()].native[0]); + } catch (e) {} + }); + + // Image zoom on hover + $('#doc-image').zoom({ + url: $('#doc-image img').data('largeImage') + }); + + // Initialise prettify + try {PR.prettyPrint(); } catch (e) {console.log(e);} + + // Event listener for add-to-list of selected entries + init_selected_checkboxes(); + + // If there is an image + if($('#doc-image').length){ + // Enable scroll for transcriptions + $('#doc-transcriptions').height("600px"); + $('#doc-transcriptions').css("overflow", "auto"); + } + } + }); + $('div.active').removeClass("active"); + $('#vol-'+v+'-page-'+p).addClass("active"); + +}; + +$(document).ready(function() { + + //Load the list of volumes and pages using ajax + jqxhr = $.getJSON( "/ajax/doc/list") + .done(function(data) { + $.each( data, function( i, e ) { + if($('#vol-'+e.volume).length === 0){ + $(''; + volumes[v] = ''+ + ''; + } + volumes[v] += ''+ + ' Page '+p+ + ''; + }); + for(var i in volumes){ + PageHTML += '
' + volumes[i] + "
"; + } + $( "#volume" ).html( VolumeHTML ); + $( "#page" ).html( PageHTML ); + + // Display the page after it has been build + show_browser(); + + // Collpse other volumes on open + $("[data-collapse-group='volume']").click(function () { + var $this = $(this); + $("[data-collapse-group='volume']").removeClass('active'); + $(this).addClass('active'); + $("[data-collapse-group='volume']:not([data-target='" + $this.data("target") + "'])").each(function () { + $($(this).data("target")).removeClass("in").addClass('collapse'); + }); + }); + + // Update badge value when checkbox has been changed + $(":checkbox").change(function(){ + var $chkbox_vol = $(this).attr('data-vol'); + var $chkbox_page = $(this).attr('data-page'); + var $vol_badge = $('#badge-'+$chkbox_vol); + + // If select all + if ($chkbox_page == 'all') { + var $chk_boxes = $('input[data-page!="all"][data-vol="'+$chkbox_vol+'"]'); + $chk_boxes.prop('checked', $(this).is(":checked")); + if ($(this).is(":checked")) { + // Indicate with badge on the volume + $vol_badge.html($chk_boxes.length); + // Add checked page to list + $chk_boxes.each(function(){ + $selected[$(this).attr('name')] = {'volume': $chkbox_vol, 'page':$(this).attr('data-page')}; + }); + } else { + // Remove chacked page from list + $chk_boxes.each(function(){ + delete $selected[$(this).attr('name')]; + }); + // Do not show 0 badge + $vol_badge.html(''); + } + } + else { + + var $chkbox_name = $(this).attr('name'); + var badge_value = $vol_badge.html(); + + // Convert "badge_value" from str to int + if (badge_value) total_chked = parseInt(badge_value); else total_chked = 0; + + // If Checked + if ($(this).is(":checked")) { + // Indicate with badge on the volume + $vol_badge.html(total_chked+1); + // Add checked page to list + $selected[$chkbox_name] = {'volume': $chkbox_vol, 'page':$chkbox_page}; + } else { + // Remove chacked page from list + delete $selected[$chkbox_name]; + // Do not show 0 badge + if (total_chked <= 1) $vol_badge.html(''); else $vol_badge.html(total_chked-1); + } + } + + // Disable buttons when nothing has been $selected + $('.doc-tools').attr("disabled", Object.keys($selected).length === 0); + }); + } + });} + + //Load the list of volumes and pages using ajax + ajax_loader(); + + $('#doc_download').click(function(){download_zip();}); + $('#doc_download_img').click(function(){download_zip(img="jpeg");}); + $('#doc_download_img_orig').click(function(){download_zip(img="tiff");}); + + + $('#doc_delete').click(function() { + selected_len = Object.keys($selected).length; + if (selected_len > 0) { + var n; + noty({ + text: '

'+selected_len+' page(s) will be removed.
Do you want to continue?

', + layout: 'center', + buttons: [ + {addClass: 'btn btn-danger', text: 'Yes', onClick: function($noty) { + $noty.close(); + + $.ajax({ + type: 'POST', + data: {'selected': $selected}, + url: 'ajax/doc/destroy', + cache:false, + beforeSend: function(){ + $.noty.closeAll(); + n = noty({text: '

Removing documents...

', + layout: 'center', + type: 'information'}); + } + }).success(function(response) { + n.setType(response.type); + n.setTimeout(3000); + if (response.type == 'success') { + n.setText('

'+response.msg+'

'); + ajax_loader(); + } + else { + n.setText('

'+response.msg+'

'); + } + }) + .fail(function() { + $.noty.closeAll(); + n = noty({timeout: 5000, text: '

Connection failure...

', layout: 'center', type: 'error'}); + }); + } + }, + {addClass: 'btn btn-default', text: 'Cancel', onClick: function($noty) {$noty.close();}} + ] // buttons: + }); // noty({ + } // if ($selected.length > 0) { + }); // $('#doc_delete').click(function() { +}); // $(document).ready(function() diff --git a/src/app/assets/javascripts/documents_selected.js b/src/app/assets/javascripts/documents_selected.js new file mode 100755 index 0000000..b0bec38 --- /dev/null +++ b/src/app/assets/javascripts/documents_selected.js @@ -0,0 +1,34 @@ +//= require prettify.js +//= require xml2html.js +//= require ISO_639_2.min.js + +$(document).ready(function() { + // Deselect all + $('#deselect-all').click(function() { + noty({ + text: '

Would you like to deselect all entries ?

', + layout: 'center', + buttons: [ + { addClass: 'btn btn-default', text: 'Yes', onClick: function($noty) { + $noty.close(); + Cookies.remove('selected_entries'); + window.location.href="/doc"; + } + }, + { addClass: 'btn btn-default', text: 'Cancel', onClick: function($noty) { $noty.close(); }} + ] + }); + }); + + // Transform language codes + $(".pr-language").each(function() { + try{ $(this).html(ISO_639_2[$(this).html()].native[0]); } + catch (e){} + }); + + // Initialise prettyPrint + PR.prettyPrint(); + + /// Event listener for add-to-list of selected entries + init_selected_checkboxes(); +}); diff --git a/src/app/assets/javascripts/home.js b/src/app/assets/javascripts/home.js new file mode 100755 index 0000000..f7bb367 --- /dev/null +++ b/src/app/assets/javascripts/home.js @@ -0,0 +1,117 @@ +$(document).ready(function() { + var dateFormat = 'yy-mm-dd'; + var minDate = '1398-01-01'; + var maxDate = '1511-12-31'; + // Initialise date fields for Advanced Search + $( "#date_from" ).datepicker({ + showOtherMonths: true, + selectOtherMonths: true, + dateFormat: 'yy-mm-dd', + minDate: $.datepicker.parseDate( dateFormat, minDate ), + maxDate: $.datepicker.parseDate( dateFormat, maxDate ), + defaultDate: $.datepicker.parseDate( dateFormat, minDate ) + }).on( "change", function() { + $( "#date_to" ).datepicker( "option", "minDate", getDate( this ) ); + }); + + $( "#date_to" ).datepicker({ + showOtherMonths: true, + selectOtherMonths: true, + dateFormat: dateFormat, + minDate: $.datepicker.parseDate( dateFormat, minDate ), + maxDate: $.datepicker.parseDate( dateFormat, maxDate ), + defaultDate: $.datepicker.parseDate( dateFormat, maxDate ) + }).on( "change", function() { + $( "#date_from" ).datepicker( "option", "maxDate", getDate( this ) ); + }); + + function getDate( element ) { + var date; + try { + date = $.datepicker.parseDate( dateFormat, element.value ); + } catch( error ) { + date = null; + } + return date; + } + + window.onresize = function() { + responsiveScreen(); + }; + + var responsiveScreen = function() { + screenCheck = $(window).height() > 630 && $(window).width() > 1024; + $.fn.fullpage.setFitToSection(screenCheck); + $.fn.fullpage.setAutoScrolling(screenCheck); + }; + + // Initialise FullPageJS + $('#fullpage').fullpage({ + anchors:['homepage', 'advsearch', 'about'], + navigation: true, + paddingTop: '60px', + onLeave: function(index, nextIndex, direction){ + // Hide datepicer after leaving Advanced Search section + if(index == 2){ + $( ".datepicker" ).datepicker('hide'); + } + }, + // Focus earch input field + afterLoad: function(anchorLink, index){ + if(index == 1 || index == 2){$('.simple-search').eq(index-1).focus();} + } + }); + + // Initialise spelling variants slider + $('#slider-spellVar').slider({ range: "max", + min: 0, max: 4, value: 1, + slide: function( event, ui ) { + $( "#spellVar" ).val( ui.value ); + } + }); + $( "#spellVar" ).val( $( "#slider-spellVar" ).slider( "value" ) ); + + // Toggle spelling variants on Regular expressions selected + $('input[name="sm"]').on('click', toggleMisspellings); + toggleMisspellings(); + +}); + +var toggleMisspellings = function () { + $disabled = $('input:checked[name="sm"]').val() == 5; + $("#slider-spellVar").slider( "option", "disabled", $disabled); + $('#spellVar').prop('disabled', $disabled); +}; + +$('#adv-search-nav').click(function(){ + $.fn.fullpage.moveTo('advsearch'); +}); + +$('#about-nav').click(function(){ + $.fn.fullpage.moveTo('about'); +}); + +$('#home-nav').click(function(){ + $.fn.fullpage.moveTo('homepages'); +}); + +$('.fp-controlArrow-down').click(function(){ + $.fn.fullpage.moveSectionDown(); +}); + + +function submitForm(){ + var name=$('.vol'); + var str=""; + try { + for(i=0;i<(name.length);i++){ + if(name[i].checked){ + str+=name[i].value+","; + } + } + if(str.length>0){str=str.substring(0,str.length-1);} // remove the last comma + $('input[name="v"]').attr('value', str); + + } catch (e) {} + $('#adv-search').submit(); +} diff --git a/src/app/assets/javascripts/init.js b/src/app/assets/javascripts/init.js new file mode 100755 index 0000000..33d56c6 --- /dev/null +++ b/src/app/assets/javascripts/init.js @@ -0,0 +1,95 @@ +// Store list of selected entries +s_list = Cookies.get('selected_entries'); +var selected_list = s_list !== undefined ? s_list.split(',') : []; + +var init_selected_checkboxes = function (){ + // Event listener for add-to-list of selected entries + $('.add-to-list').click(function(){ + // Store the entry ID + var entryID = $(this).attr('data-entry'); + + if($(this).is(":checked")) { + selected_list.push(entryID); + Cookies.set('selected_entries', selected_list.toString()); + $("#documents-btn").hide(); + $("#documents-selected-btn").show(); + // Show tooltip if this is the first selected entry + if (selected_list.length == 1) { + $('#doc_caret').tooltip('show'); + // Set 5 sec timeout. + setTimeout(function () { $('#doc_caret').tooltip('hide'); }, 5000); + } + + } else { + // Remove all records with this entryID + selected_list = jQuery.grep(selected_list, function( a ) { return a !== entryID ;}); + Cookies.set('selected_entries', selected_list.toString()); + // If on select page remove hide the entry + if (window.location.pathname == "/doc/selected") { $('#'+entryID).fadeOut(); } + // Remove the cookie if no selected documents are left + if (selected_list.length === 0) { + $("#documents-selected-btn").hide(); + $("#documents-btn").show(); + Cookies.remove('selected_entries'); + // Redirect to documents + if (window.location.pathname == "/doc/selected") { window.location.pathname="/doc"; } + } + } + }); + + // Set to checked add-to-list if it is already in the list + $('.add-to-list').each(function () {$(this).prop('checked', selected_list.indexOf($(this).attr("data-entry")) >= 0);}); +}; + +/* Adding a parameter to the URL + * key - name of parameter + * value - value of parameter + * remove - parameter to be removed from url + */ +function insertParam(key, value, remove) { + remove = (remove !== 'undefined') ? remove : ''; + key = encodeURI(key); value = encodeURI(value); remove = encodeURI(remove); + var kvp = document.location.search.substr(1).split('&'); + var i=kvp.length, found=false; + while(i--) + { + x = kvp[i].split('='); + if (x[0]==remove) {kvp.splice(i, 1);} + else if (x[0]==key && !found) + { + x[1] = value; + kvp[i] = x.join('='); + found=true; + } + } + if(!found) {kvp[kvp.length] = [key,value].join('=');} + //this will reload the page, it's likely better to store this until finished + document.location.search = kvp.join('&'); + } + +$(document).ready(function() { + + $('#documents-selected-btn').hover(function() { + $('#doc_caret').tooltip('hide'); + }); + // Enable autocomplete + $('.simple-search').autocomplete({minLength: 2,source: '/ajax/search/autocomplete'}); + $('#entry').autocomplete({minLength: 2, source: '/ajax/search/autocomplete-entry'}); + + $('#adv-search').submit(function () { + // Ignore empty values + if($('#content').val() === ''){$('#content').attr('value', '*');} + + $(this).find('[name]').each(function(){ + if($(this).val() === ''){ + $(this).filter(function (input) { + return !input.value; + }) + .prop('name', ''); + } + }); + + // Ignore submit button + $(this).find('[name="commit"]').filter(function (input) { return !input.value;}).prop('name', ''); + }); +}); diff --git a/src/app/assets/javascripts/search.js b/src/app/assets/javascripts/search.js new file mode 100755 index 0000000..a75932d --- /dev/null +++ b/src/app/assets/javascripts/search.js @@ -0,0 +1,74 @@ +//= require highlightRegex.min.js + +var chartData = []; +var loadChart = function (chartAPI) { + chartAPI = (chartAPI !== 'undefined') ? chartAPI : ""; + + if (chartData.length === 0) + { + if (chartAPI !== '') + { + $.getJSON( chartAPI ) + .done(function( data ) { + if (data.length > 1) { + for(i=0; i< data.length; i++) + { + var row = data[i]; + newRow = row.slice(0, 3); + newRow.push(new Date(row[3])); + newRow.push(new Date(row[4])); + chartData[i] = newRow; + } + $('#toggleChart-1').show(); + } + }); + } + } + else if ($('#chart-1').css('visibility') === 'hidden') { + google.charts.load('current', {'packages':['timeline']}); + google.charts.setOnLoadCallback(drawChart); + var drawChart = function () { + var container = document.getElementById('chart-1'); + var chart = new google.visualization.Timeline(container); + var dataTable = new google.visualization.DataTable(); + + dataTable.addColumn({type: "string", id: "Name"}); + dataTable.addColumn({type: "string", id: 'dummy bar label' }); + dataTable.addColumn({type: "string", role: 'tooltip', 'p': {'html': true} }); + dataTable.addColumn({type: "date", id: "Start"}); + dataTable.addColumn({type: "date", id: "End"}); + dataTable.addRows(chartData); + + var options = { + timeline: { colorByRowLabel: true }, + tooltip: {isHtml: true}, + }; + + chart.draw(dataTable, options); + + $('#chart-1').css('visibility', 'visible'); + $('#chart-1').css('height', 'auto'); + $(window).trigger('resize'); + }; drawChart(); + } + else { + $('#chart-1').fadeToggle(); + $(window).trigger('resize'); + } +}; + +toggle_search_tools_when_regex = function(){ + $disabled = $("select[name='sm']").val() == 5; + $("select[name='m']").prop("disabled", $disabled); +}; + +$(function() { + // Search method is Regex + $('select').on('change', function() { + toggle_search_tools_when_regex(); + }); + toggle_search_tools_when_regex(); + if($("select[name='sm']").val() == 5){ + $('.list-group-item').highlightRegex(new RegExp($("input.simple-search").val(), "ig")); + } +}); diff --git a/src/app/assets/javascripts/xml2html.js b/src/app/assets/javascripts/xml2html.js new file mode 100755 index 0000000..b13fe1e --- /dev/null +++ b/src/app/assets/javascripts/xml2html.js @@ -0,0 +1,6 @@ +// Javascript where to toggle the xml tags +function toggle_xml(e){ + $(e).toggleClass('btn-success'); + $(e).parents('.panel-body').children('.transcription').toggle("slow"); + $(window).trigger('resize'); // Fix for FullPageJS +} diff --git a/src/app/assets/stylesheets/application.css b/src/app/assets/stylesheets/application.css new file mode 100755 index 0000000..2c920c7 --- /dev/null +++ b/src/app/assets/stylesheets/application.css @@ -0,0 +1,255 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require jquery-ui + *= require bootstrap_and_overrides + *= require font-awesome + *= require documents.scss + *= require home.scss + *= require search.scss + *= require jquery.fullPage.css + *= require_self + */ + +/*Leave space for the top nav menu and footer*/ +@font-face { font-family: "minion-pro-1"; font-style: normal; font-weight: 600; src: url("https://use.typekit.net/af/5754c8/0000000000000000000151d7/27/l?subset_id=2&fvd=n6&v=3") format("woff2"), url("https://use.typekit.net/af/5754c8/0000000000000000000151d7/27/d?subset_id=2&fvd=n6&v=3") format("woff"), url("https://use.typekit.net/af/5754c8/0000000000000000000151d7/27/a?subset_id=2&fvd=n6&v=3") format("opentype"); } +@font-face { font-family: "museo-slab-1"; font-style: normal; font-weight: 500; src: url("https://use.typekit.net/af/13f056/000000000000000000012043/27/l?subset_id=2&fvd=n5&v=3") format("woff2"), url("https://use.typekit.net/af/13f056/000000000000000000012043/27/d?subset_id=2&fvd=n5&v=3") format("woff"), url("https://use.typekit.net/af/13f056/000000000000000000012043/27/a?subset_id=2&fvd=n5&v=3") format("opentype"); } +body{ + /* Leave space for top nav bar */ + padding-top: 70px; +} + +a { + cursor: pointer; +} + +/*Sticky the foother to bottom of the page*/ +/*footer p{ + margin: 0; + padding: 0; +} + +.footer { + width:100%; + min-height:30px; + background: #f2f2f2; + border-top: 1px solid #e4e4e4; + color:#222; + font-size:12px; +}*/ + + /*The style for the menu*/ + .navbar-default { + background-color: #eaeaea; + border-color: #eaeaea; + } + .navbar-default .navbar-brand { + color: #666666; + } + .navbar-default .navbar-brand:hover, + .navbar-default .navbar-brand:focus { + color: #3276ac; + } + .navbar-default .navbar-text { + color: #666666; + } + .navbar-default .navbar-nav > li > a { + color: #666666; + } + .navbar-default .navbar-nav > li > a:hover, + .navbar-default .navbar-nav > li > a:focus { + color: #3276ac; + } + .navbar-default .navbar-nav > li > .dropdown-menu { + background-color: #eaeaea; + } + .navbar-default .navbar-nav > li > .dropdown-menu > li > a { + color: #666666; + } + .navbar-default .navbar-nav > li > .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav > li > .dropdown-menu > li > a:focus { + color: #3276ac; + background-color: #eaeaea; + } + .navbar-default .navbar-nav > li > .dropdown-menu > li > .divider { + background-color: #eaeaea; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #3276ac; + background-color: #eaeaea; + } + .navbar-default .navbar-nav > .active > a, + .navbar-default .navbar-nav > .active > a:hover, + .navbar-default .navbar-nav > .active > a:focus { + color: #3276ac; + background-color: #eaeaea; + } + .navbar-default .navbar-nav > .open > a, + .navbar-default .navbar-nav > .open > a:hover, + .navbar-default .navbar-nav > .open > a:focus { + color: #3276ac; + background-color: #eaeaea; + } + .navbar-default .navbar-toggle { + border-color: #eaeaea; + } + .navbar-default .navbar-toggle:hover, + .navbar-default .navbar-toggle:focus { + background-color: #eaeaea; + } + .navbar-default .navbar-toggle .icon-bar { + background-color: #666666; + } + .navbar-default .navbar-link { + color: #666666; + } + .navbar-default .navbar-link:hover { + color: #3276ac; + } + + /*Align search methods on one line*/ + #search-method .radio-inline { + display: inline-block; + } + @media (max-width: 767px) { + + /*Align search methods on multiple lines*/ + #search-method .radio-inline label{ + font-weight: normal; + } + #search-method .radio-inline { + margin-left: 0; + display: block; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #666666; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #3276ac; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #eaeaea; + background-color: #eaeaea; + } + } + + /*The list of links on the left side of the page */ + .nav > div > div > a { + display: block; + padding: 3px 15px; + font-size: 12px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + } + + .nav-list > div > li > a { + padding: 3px 15px; + } + + .nav-list > div > .active > a, + .nav-list > div > .active > a:hover, + .nav-list > div > .active > a:focus { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; + } + + .sidebar-nav .nav { + list-style: none; + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + float: left; + min-width: 140px; + max-width: 140px; + max-height: 500px; + overflow-y: auto; + padding:0; + margin: 0; + font-size: 14px; + text-align: left; + background-color: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + } + + +/*Small box for Upload files*/ + .center-box-sm{ + display: inline-block; + width: 400px; + text-align: left; + } + +.main-container{ + margin-top: 40px; + margin-bottom: 40px; +} + +.vcenter { + display: table-cell; + vertical-align: middle; + float: none; +} + +#navbar-collapse-search .navbar-form,{ + /* Fix overflow on small screen */ + margin: 0; + padding: 0; +} +.navbar-brand{ + text-align: center; +} + +#noty_topRight_layout_container{ + /* Fix notification overflow with the nav menu */ + margin-top: 50px !important; +} + +#doc_nav::before { + /* Remove white-space above nav-list on doc/show */ + content: none; +} +#documents-selected-btn { + /* Fix dropdown-menu for the selected documets button */ + padding-top: 8px; + padding-bottom: 10px; +} + +.ui-widget{ + /* Fix overlap when using autocomplete */ + z-index: 99999; +} + +.list-group-item{ + /* Concise the pages in Document Browse */ + padding: 1px; +} + +.tr-text{ + font-family: "museo-slab-1","museo-slab-2",serif; +} + +.navbar-fixed-top .navbar-collapse { + padding-right: 2%; +} diff --git a/src/app/assets/stylesheets/bootstrap_and_overrides.css b/src/app/assets/stylesheets/bootstrap_and_overrides.css new file mode 100755 index 0000000..3c09492 --- /dev/null +++ b/src/app/assets/stylesheets/bootstrap_and_overrides.css @@ -0,0 +1,29 @@ +/* + =require twitter-bootstrap-static/bootstrap + + Static version of css will use Glyphicons sprites by default + =require twitter-bootstrap-static/sprites +*/ +/* Collapse mav-bar-search on small screen */ +@media (max-width: 1045px) { + .navbar-toggle-search { + display: block; + } + #navbar-collapse-search { + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); + } + #navbar-collapse-search.collapse { + display: none!important; + } + #navbar-collapse-search { + float: none; + } + + #navbar-collapse-search.collapse.in { + display: block!important; + } + #navbar-collapse-search.collapsing { + overflow: hidden!important; + } +} diff --git a/src/app/assets/stylesheets/documents.scss b/src/app/assets/stylesheets/documents.scss new file mode 100755 index 0000000..7ce8e65 --- /dev/null +++ b/src/app/assets/stylesheets/documents.scss @@ -0,0 +1,50 @@ +/* Place all the styles related to the Documents controller here. +* They will automatically be included in application.css. +* You can use Sass (SCSS) here: http://sass-lang.com/ + +*= require annotation.css +*/ + + // Styles for image zoom jquery plugin + .zoom { + display:inline-block; + position: relative; + } + + /* magnifying glass icon */ + .zoom:after { + content:''; + display:block; + width:33px; + height:33px; + position:absolute; + top:0; + right:0; + background:url('magnifying_glass_icon.png'); + } + .zoom img { + display: block; + } + .zoom img::selection { background-color: transparent; } + +.doc-tools { + display: none; +} +#doc-browse { + display: none; +} +#doc_view { + display: none; +} + +// Fix for documents browser collapse (Overwrite .list-group-item) +.collapse{ + display: none; +} +.collapse .in{ + display: block; +} +// Change the cursor for list items +.list-group-item{ + cursor: pointer; +} diff --git a/src/app/assets/stylesheets/home.scss b/src/app/assets/stylesheets/home.scss new file mode 100755 index 0000000..916487f --- /dev/null +++ b/src/app/assets/stylesheets/home.scss @@ -0,0 +1,240 @@ +// Place all the styles related to the Home controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ + + + .headline { + padding: 120px 0; + } + + .headline h1 { + background: #fff; + background: rgba(255,255,255,0.9); + } + + .headline h2 { + background: #fff; + background: rgba(255,255,255,0.9); + } + + .featurette-divider { + margin: 80px 0; + } + + .featurette { + overflow: hidden; + } + + .featurette-image.pull-left { + margin-right: 40px; + } + + .featurette-image.pull-right { + margin-left: 40px; + } + + + @media(max-width:1200px) { + + .featurette-divider { + margin: 50px 0; + } + + .featurette-image.pull-left { + margin-right: 20px; + } + + .featurette-image.pull-right { + margin-left: 20px; + } + } + + @media(max-width:991px) { + .featurette-divider { + margin: 40px 0; + } + + .featurette-image { + max-width: 50%; + } + + .featurette-image.pull-left { + margin-right: 10px; + } + + .featurette-image.pull-right { + margin-left: 10px; + } + } + + @media(max-width:768px) { + .container { + margin: 0 15px; + } + + .featurette-divider { + margin: 40px 0; + } + + } + + @media(max-width:668px) { + + .featurette-divider { + margin: 30px 0; + } + } + + @media(max-width:640px) { + .headline { + padding: 75px 0 25px 0; + } + } + + @media(max-width:375px) { + .featurette-divider { + margin: 10px 0; + } + + .featurette-image { + max-width: 100%; + } + + .featurette-image.pull-left { + margin-right: 0; + margin-bottom: 10px; + } + + .featurette-image.pull-right { + margin-bottom: 10px; + margin-left: 0; + } + } + + #index-home-image{ + width: 90%; + height: 90%; + margin-top: 5px; + } + + textarea:focus, input:focus, .uneditable-input:focus { + border-color: rgba(119, 119, 119, 0.8) !important; + box-shadow: 0 1px 1px rgba(119, 119, 119, 0.075) inset, 0 0 8px rgba(119, 119, 119, 0.6) !important; + outline: 0 none !important; + } + + #h1-home-light-text{ + color: #ffffff; + font-family: Baskerville, "Baskerville Old Face", "Goudy Old Style", Garamond, "Times New Roman", serif; + text-rendering: optimizeLegibility; + } + + #p-home-light-text{ + color: rgba(255, 255, 255, 0.8); + text-rendering: optimizeLegibility; + } + + .h2-home{ + text-align: center; + font-family: Baskerville, "Baskerville Old Face", "Goudy Old Style", Garamond, "Times New Roman", serif; + } + + #home-top-container{ + background-color: #888888; + width: 100%; + // margin-top: -20px; + // margin-bottom: -20px; + + } + + .jumbotron{ + background-color: #1E1E1E; + padding-left: 0px ; + padding-right: 0px; + box-shadow: 7px 7px 5px #222; + } + + .fp-controlArrow-down { + -webkit-user-select: none; /* webkit (safari, chrome) browsers */ + -moz-user-select: none; /* mozilla browsers */ + -khtml-user-select: none; /* webkit (konqueror) browsers */ + -ms-user-select: none; /* IE10+ */ + z-index: 4; + cursor: pointer; + // Horiszontal center + position: absolute; + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + // Stick to the bottom of the page + margin-bottom: auto; + bottom: 0; + } + + .fp-controlArrow-down .icon{ + display: inline-block; + width: 20px; + height: 38px; + position: relative; + background: url("scroll-down.png") center center no-repeat; + background-size: 20px 38px; + } + @media all and (-webkit-min-device-pixel-ratio: 1.5), all and (-o-min-device-pixel-ratio: 3 / 2), all and (min--moz-device-pixel-ratio: 1.5), all and (min-device-pixel-ratio: 1.5) { + .fp-controlArrow-down .icon { + background: url("scroll-down@2x.png") center center no-repeat; + background-size: 20px 38px; + } + } + @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + .fp-controlArrow-down .icon { + background: url("scroll-down@2x.png") center center no-repeat; + background-size: 20px 38px; + } + } + @-webkit-keyframes scrollAnimation { + 0% { top: 6px; } + 50% { top: 18px; } + 100% { top: 6px; } + } + @keyframes scrollAnimation { + 0% { top: 6px; } + 50% { top: 18px; } + 100% { top: 6px; } + } + .fp-controlArrow-down .icon:after { + content: ''; + display: block; + width: 2px; + height: 6px; + position: absolute; + top: 6px; + left: 9px; + background: #444; + -webkit-animation: scrollAnimation 1s linear infinite; + animation: scrollAnimation 1s linear infinite; + } +.fp-controlArrow-down .text-helper{ + display: block; + font-size: 13px; + color: #444; +} + +@media screen and (max-width: 1199px) { + #index-home-image { + // Fix FullPageJS resize + max-width: 300px; + } +} +@media screen and (max-width: 991px) { + #index-home-image { + // Fix FullPageJS resize + max-width: 200px; + } +} +@media screen and (max-width: 767px) { + #index-home-image { + // Fix FullPageJS resize + max-width: 150px; + } +} diff --git a/src/app/assets/stylesheets/search.scss b/src/app/assets/stylesheets/search.scss new file mode 100755 index 0000000..720ca4c --- /dev/null +++ b/src/app/assets/stylesheets/search.scss @@ -0,0 +1,5 @@ +// *= require annotation.css + +.highlight, mark{ + background-color: #ffff4d; +} diff --git a/src/app/assets/stylesheets/xquery.scss b/src/app/assets/stylesheets/xquery.scss new file mode 100755 index 0000000..1725293 --- /dev/null +++ b/src/app/assets/stylesheets/xquery.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the xquery controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/src/app/channels/application_cable/channel.rb b/src/app/channels/application_cable/channel.rb new file mode 100755 index 0000000..d672697 --- /dev/null +++ b/src/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/src/app/channels/application_cable/connection.rb b/src/app/channels/application_cable/connection.rb new file mode 100755 index 0000000..0ff5442 --- /dev/null +++ b/src/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/src/app/controllers/application_controller.rb b/src/app/controllers/application_controller.rb new file mode 100755 index 0000000..3053435 --- /dev/null +++ b/src/app/controllers/application_controller.rb @@ -0,0 +1,14 @@ +class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + + private + def mobile? + request.user_agent =~ /Mobile|webOS/ + end + + def page_not_found + raise ActionController::RoutingError.new('Not Found') + end + + helper_method :mobile? +end diff --git a/src/app/controllers/concerns/.keep b/src/app/controllers/concerns/.keep new file mode 100755 index 0000000..e69de29 diff --git a/src/app/controllers/documents_controller.rb b/src/app/controllers/documents_controller.rb new file mode 100755 index 0000000..7345d24 --- /dev/null +++ b/src/app/controllers/documents_controller.rb @@ -0,0 +1,244 @@ +require "#{Rails.root}/lib/BaseXClient" + +class DocumentsController < ApplicationController + + def index; end + + def selected + selected_entries = cookies[:selected_entries] + if selected_entries + @documents = Search.where({entry: selected_entries.split(',')}) + if @documents.length == 0 + cookies.delete :selected_entries + redirect_to doc_path, :alert => "No selected paragraphs!" + end + else + cookies.delete :selected_entries + redirect_to doc_path, :alert => "No selected paragraphs!" + end + end + + def list + @documents = Search.select(:page, :volume).distinct.order(volume: :asc, page: :asc).group(:volume, :page) + respond_to do |format| + format.html { redirect_to doc_path } + format.json { render json: @documents } + format.js { render :layout => false } + end + end + + def new + if not user_signed_in? or not current_user.admin? + redirect_to new_user_session_path, :alert => "Not logged in or Insufficient rights!" + end + end + + def show + if params.has_key?(:p) and params.has_key?(:v) \ + and params[:v].to_i > 0 and params[:v].to_i < 1000000 \ + and params[:p].to_i > 0 and params[:p].to_i < 1000000 + # Store the volume and page from the input + @volume, @page = params[:v].to_i, params[:p].to_i + + if params[:searchID] + @searchID = 'searchID' + end + else + redirect_to doc_path, notice: "The document has not been found." + end + end + + def page_simplified + if params.has_key?(:p) and params.has_key?(:v) \ + and params[:v].to_i > 0 and params[:v].to_i < 1000000 \ + and params[:p].to_i > 0 and params[:p].to_i < 1000000 + # Store the volume and page from the input + @volume, @page = params[:v].to_i, params[:p].to_i + # Select Documents + @documents = Search.order(:paragraph).where('volume' => @volume).rewhere('page' => @page) + if @documents.length > 0 + # Select image + respond_to do |format| + format.html { render :partial => "documents/page_simplified" } + end + else + render status: 500 + end + else + render status: 500 + end + end + + def page + if params.has_key?(:p) and params.has_key?(:v) \ + and params[:v].to_i > 0 and params[:v].to_i < 1000000 \ + and params[:p].to_i > 0 and params[:p].to_i < 1000000 + # Store the volume and page from the input + @volume, @page = params[:v].to_i, params[:p].to_i + # Select Documents + @documents = Search.order(:paragraph).where('volume' => @volume).rewhere('page' => @page) + if @documents.length > 0 + # Select image + page_image = PageImage.find_by_volume_and_page(@volume, @page) + if page_image # Has been uploaded + # Simple Fix of the file extension after image convert + @document_image_normal = page_image.image.normal.url.split('.')[0...-1].join + '.jpeg' + @document_image_large = page_image.image.large.url.split('.')[0...-1].join + '.jpeg' + end + respond_to do |format| + format.html { render :partial => "documents/page" } + end + + else + render status: 500 + end + else + render status: 500 + end + end + + def upload + if user_signed_in? and current_user.admin? + @succesfully_uploaded = {xml:[], image:[]} + @unsuccesfully_uploaded = {xml:[], image:[]} + if params.has_key?(:transcription_xml) + xml_files = xml_upload_params + xml_files_content = [] + # Save all uploaded xml files, call method ... + xml_files['xml'].each do |file| + # Get filename + filename = file.original_filename + # Check namespace + nokogiri_obj = Nokogiri::XML(File.open(file.path)) + if ( nokogiri_obj.collect_namespaces.values.include? TranscriptionXml::HISTEI_NS ) + # Show message when overwrites + output_message = TranscriptionXml.exists?(filename: filename) ? "Overwritten #{filename}" : filename + # Create new or find existing record + t = TranscriptionXml.find_or_create_by(filename: filename) + # Store the information about the uploaded file + t.xml = file + # If the save was successful + if t.save! + @succesfully_uploaded[:xml].push(output_message) + # Store file content and filename for import to BaseX + xml_files_content.push([filename, nokogiri_obj.to_xml.gsub('xml:lang="sc"', 'xml:lang="sco"').gsub('xml:lang="la"', 'xml:lang="lat"').gsub('xml:lang="nl"', 'xml:lang="nld"')]) + # Proccess the XML file + t.histei_split_to_paragraphs + else + @unsuccesfully_uploaded[:xml].push("Unexpected error on saving: #{filename}") + end + else + @unsuccesfully_uploaded[:xml].push("HisTEI namespace not found: #{filename}") + end + end + + # Upload xml contnet to BaseX + begin # Catch connection error + session = BaseXClient::Session.new(ENV['BASEX_URL'], 1984, "createOnly", ENV['BASEX_CREATEONLY']) + session.execute('open xmldb') # Open XML database + xml_files_content.each do |file_name, file_content| + begin # Catch document creation error + session.replace(file_name, file_content) + rescue Exception => e + logger.error(e) + end + end + session.close + rescue Exception => e + logger.error(e) + end + #End of uploading to BaseX + + # Generate new index for Elasticsearch + Search.reindex() + end + + if params.has_key?(:page_image) + image_files = image_upload_params + # Save all uploaded image files, call method ... + image_files['image'].each do |file| + t = PageImage.new + t.image = file + t.parse_filename_to_volume_page file.original_filename + if t.save! + @succesfully_uploaded[:image].push(file.original_filename) + else + @unsuccesfully_uploaded[:image].push(file.original_filename) + end + end + end + else + redirect_to new_user_session_path, :alert => "Not logged in or Insufficient rights!" + end + end + + def destroy + if user_signed_in? and current_user.admin? + selected = params['selected'] + # If there is at least one selected page + if selected + selected.each do |s| + entry = selected[s] + if entry.key?("volume") and entry.key?("page") + vol, page = entry['volume'], entry['page'] + tr_xml = Search.where('volume' => vol).rewhere('page' => page) + if tr_xml # If the db record was found + tr_xml.each do |tr| + # Remove content from BaseX + begin # Catch connection error + session = BaseXClient::Session.new(ENV['BASEX_URL'], 1984, "createOnly", ENV['BASEX_CREATEONLY']) + session.execute('open xmldb') # Open XML database + + # XQuery delete node query + # Create instance the BaseX Client in Query Mode + query = session.query("\ + declare namespace ns = \"http://www.tei-c.org/ns/1.0\"; \ + delete node //ns:div[@xml:id=\"#{tr.entry}\"] \ + ") + query.execute + query.close + session.close + rescue Exception => e + logger.error(e) + end + # End of BaseX remove + + tr.tr_paragraph.destroy + tr.destroy + end # tr_xml.each + end # if xml + end # if entry.key? + end # selected.each + respond_to do |format| + format.json { render json: {'type': 'success', 'msg': "Selected documents have been removed."} } + format.js { render :layout => false } + end + else + respond_to do |format| + format.json { render json: {'type': 'warning', 'msg': "Error: No selected documents!"} } + format.js { render :layout => false } + end + end # if selected + else + redirect_to new_user_session_path, :alert => "Not logged in or Insufficient rights!" + end # if user_signed_in? + + end + + private # all methods that follow will be made private: not accessible for outside objects + def xml_upload_params + if user_signed_in? and current_user.admin? + params.require(:transcription_xml).permit xml: [] + else + redirect_to new_user_session_path, :alert => "Not logged in or Insufficient rights!" + end + end + + def image_upload_params + if user_signed_in? and current_user.admin? + params.require(:page_image).permit image: [] + else + redirect_to new_user_session_path, :alert => "Not logged in or Insufficient rights!" + end + end +end diff --git a/src/app/controllers/download_controller.rb b/src/app/controllers/download_controller.rb new file mode 100755 index 0000000..db4437e --- /dev/null +++ b/src/app/controllers/download_controller.rb @@ -0,0 +1,114 @@ +require 'zip' + +class DownloadController < ApplicationController + def index + selected = params['selected'] + add_images = params['img'] + # If there are selected pages + if selected + images_paths = Set.new + xml_paths = Set.new + # For each selected page + selected.each do |s| + entry = selected[s] + # Continue if volume and page are spacified + if entry.key?("volume") and entry.key?("page") + vol, page = entry['volume'], entry['page'] + + if add_images == "jpeg" or add_images == "tiff" + img = PageImage.find_by_volume_and_page(vol, page) + if img + if add_images == "tiff" and user_signed_in? and current_user.admin? + images_paths.add([img.image_identifier, img.image.path]) + else + images_paths.add([ + img.image_identifier.split('.')[0...-1].join + '.jpeg', + img.image.large.path.split('.')[0...-1].join + '.jpeg' + ]) + end + end + end + + xml = Search.where('volume' => vol).rewhere('page' => page) + if xml + xml_file = xml[0].transcription_xml + xml_paths.add([xml_file.xml_identifier, xml_file.xml.path]) + end + + end # if entry.key?("volume") and entry.key?("page") + end # selected.each do |s| + + # Catch exceptions during archiving files + begin + + filename = 'archive.zip' + temp_file = Tempfile.new(filename) + Zip::File.open(temp_file.path, Zip::File::CREATE) do |zip_file| + if (xml_paths) + zip_file.mkdir('Transcriptions') + xml_paths.each do |filename, path| + zip_file.add("Transcriptions/#{filename}", path) + end # xml_paths.each + end # if (xml_paths) + if add_images + if (images_paths) + zip_file.mkdir('Images') + images_paths.each do |filename, path| + zip_file.add("Images/#{filename}", path) + end # images_paths.each do |filename, path| + end # if (images_paths) + end # if add_images + end # Zip::File.open + + # Sore the filepath in the session + key_length = 6 + key = rand(36**key_length).to_s(36) + session[:tmp_file] = {key => temp_file.path} + + respond_to do |format| + format.json { render json: {'type': 'success', 'msg': "Archive created.", 'url': download_archive_path + "?key=#{key}" }} + format.js { render :layout => false } + end # respond_to + + rescue + respond_to do |format| + format.json { render json: {'type': 'warning', 'msg': "Error: Files archiving failure."} } + format.js { render :layout => false } + end # respond_to + end # begin rescue + else + respond_to do |format| + format.json { render json: {'type': 'warning', 'msg': "Download error: No selected documents!"} } + format.js { render :layout => false } + end # respond + end # if selected + end # index + + def archive + if params.has_key? "key" + filepath = session[:tmp_file][params[:key]] + if filepath + begin + zip_data = File.read(filepath) + send_data(zip_data, :type => 'application/zip', :filename => 'archive.zip') + return + end # begin ensure + end # if key + end # params.has_key? "key" + render file: "#{Rails.root}/public/404.html" , status: :not_found + end # archive + + def selected_gen_pdf + selected_entries = cookies[:selected_entries] + if selected_entries + documents = Search.where({entry: selected_entries.split(',')}) + if documents.length > 0 + send_data TrParagraph.new().print_data(documents), filename:'Selected_entries.pdf', type: "application/pdf", disposition: :attachment + else + page_not_found + end + else + page_not_found + end + end +end diff --git a/src/app/controllers/home_controller.rb b/src/app/controllers/home_controller.rb new file mode 100755 index 0000000..b6606e8 --- /dev/null +++ b/src/app/controllers/home_controller.rb @@ -0,0 +1,5 @@ +class HomeController < ApplicationController + def index + @volumes = Search.select(:volume).distinct.order(volume: :asc).group(:volume) + end +end diff --git a/src/app/controllers/search_controller.rb b/src/app/controllers/search_controller.rb new file mode 100755 index 0000000..6a47ed1 --- /dev/null +++ b/src/app/controllers/search_controller.rb @@ -0,0 +1,237 @@ +class SearchController < ApplicationController + + def chart_wordstart_date + if params[:term] + # Strip white-space at the beginning and the end. + query = params[:term].strip.gsub(/[^0-9a-z]/i, '') + + # Do not use autocomplete for phrases; The search method is not appropriate + # if query.length < 20 and !query.include? ' ' + render json: ( + Search.search(query, { + fields: ['content'], # Autocomplete for words in content + match: :word_start, # Use word_start method + highlight: {tag: "" , + fields: {content: {fragment_size: 0}}}, # Highlight only single word + # limit: 10, # Limit the number of results + load: false, # Do not query the database (PostgreSQL) + misspellings: { + edit_distance: 1, # Limit misspelled distance to 1 + below: 4, # Do not use misspellings if there are more than 4 results + transpositions: false # Show more accurate results + } + } + # Get only the highlighted word + # Remove non-aplhanumeric characters, such as white-space + # Return only unique words + ).pluck(:highlighted_content, :date, :entry) + .delete_if{|content, date, entry| not content or not date or not entry} + .collect{ + |content, date, entry| [ + content.gsub(/[^0-9a-z]/i, ''), + '', + "ID: #{entry}
Date: #{date}", + date, + date + ] + } + ) + return # Finish here to avoid render {} + # end + end + render json: {} # Render this if one of the 2 if statements fails + end + + # Simple Search + def search + redirect_to doc_path if Search.count.zero? # Fix search on empty DB + + # Use strong params + permited = simple_search_params + + # Parse Spelling variants and Results per page + get_search_tools_params(permited) + + # Send the query to Elasticsearch + if permited[:sm].to_i == 5 + @searchMethod = 5 # Regexp + + @documents = Search.search '*', + page: permited[:page], per_page: @results_per_page, # Pagination + where: {content:{"regexp":".*" + @query + ".*"}}.merge(get_adv_search_params(permited)), + match: get_serch_method(permited), # Parse search method parameter + order: get_order_by(permited), # Parse order_by parameter + load: false # Do not retrieve data from PostgreSQL + + else + @documents = Search.search @query, + misspellings: {edit_distance: @misspellings,transpositions: false}, + page: permited[:page], per_page: @results_per_page, # Pagination + where: get_adv_search_params(permited), # Parse adv search parameters + match: get_serch_method(permited), # Parse search method parameter + order: get_order_by(permited), # Parse order_by parameter + highlight: {tag: ""}, # Set html tag for highlight + fields: ['content'], # Search for the query only within content + suggest: true, # Enable suggestions + load: false # Do not retrieve data from PostgreSQL + end + end + + def autocomplete + # Strip white-space at the beginning and the end. + query = params[:term].strip.gsub(/[^0-9a-z]/i, '') + + # Do not use autocomplete for phrases; The search method is not appropriate + if query.length < 20 and !query.include? ' ' + render json: (Search.search(query, { + fields: ['content'], # Autocomplete for words in content + match: :word_start, # Use word_start method + highlight: {tag: "" , + fields: {content: {fragment_size: 0}}}, # Highlight only single word + limit: 10, # Limit the number of results + load: false, # Do not query the database (PostgreSQL) + misspellings: { + edit_distance: 1, # Limit misspelled distance to 1 + below: 4, # Do not use misspellings if there are more than 4 results + transpositions: false # Show more accurate results + } + } + # Get only the highlighted word + # Remove non-aplhanumeric characters, such as white-space + # Return only unique words + ).map {|x| x.highlighted_content.gsub(/[^0-9a-z]/i, '')}).uniq + else + render json: {} + end + end + + def autocomplete_entry + render json: Search.search(params[:term].strip, { + fields: ['entry'], + match: :word_start, + limit: 5, + load: false, + misspellings: false + }).map(&:entry) + end + + # Define functions for simplicity + private + + def simple_search_params + params.permit(:q, :r, :m, :o, :sm, :entry, :date_from, :date_to, :v, :pg, :pr, :lang, :page) + end + + def get_search_tools_params(permited) + # Get text from the user input. In case of empty search -> use '*' + @query = permited[:q].present? ? permited[:q].strip : '*' + + # Get the number of results per page; Default value -> 5 + @results_per_page = 5 + results_per_page = permited[:r].to_i + if results_per_page >= 5 and results_per_page <= 50 + @results_per_page = results_per_page + end + + # Get the misspelling distance; Default value -> 2 + @misspellings = 2 + misspellings = permited[:m].to_i + if misspellings >= 0 and misspellings <= 5 + @misspellings = misspellings + end + end + + def get_order_by(permited) + # Get the orderBy mode + # 0 -> Most relevant first + # 1 -> Volume/Page in ascending order + # 2 -> Volume/Page in descending order + # 3 -> Chronological orther + @orderBy = permited[:o].to_i + order_by = {} + if @orderBy == 0 + order_by['_score'] = :desc # most relevant first - default + elsif @orderBy == 1 + order_by['volume'] = :asc # volume ascending order + order_by['page'] = :asc # page ascending order + elsif @orderBy == 2 + order_by['volume'] = :desc # volume descending order + order_by['page'] = :desc # page descending order + elsif @orderBy == 3 + order_by['date'] = :asc + end + return order_by + end + + def get_serch_method(permited) + # Get the search method value + # 0 -> Analyzed (Default) + # 1 -> Phrase + # 2 -> word_start + # 3 -> word_middle + # 4 -> word_end + @searchMethod = permited[:sm].to_i + + case @searchMethod + when 1 then search_method = :phrase + when 2 then search_method = :word_start + when 3 then search_method = :word_middle + when 4 then search_method = :word_end + else search_method = :analyzed + end + + return search_method + end + + def get_adv_search_params(permited) + where_query = {} + if permited[:entry] # Filter by Entry ID + where_query['entry'] = Regexp.new "#{permited[:entry]}.*" + end + date_range = {} + if permited[:date_from] # Filter by lower date bound + begin + date_str = permited[:date_from] # Get date + # Fix incorrect date format + case date_str.split('-').length + when 3 then date_range[:gte] = date_str.to_date + when 2 then date_range[:gte] = "#{date_str}-1".to_date + when 1 then date_range[:gte] = "#{date_str}-1-1".to_date + else flash[:notice] = "Incorrect \"Date from\" format" + end + rescue + flash[:notice] = "Incorrect \"Date from\" format" + end + end + if permited[:date_to] # Filter by upper date bound + begin + date_str = permited[:date_to] # Get date + # Fix incorrect date format + case date_str.split('-').length + when 3 then date_range[:lte] = date_str.to_date + when 2 then date_range[:lte] = "#{date_str}-28".to_date + when 1 then date_range[:lte] = "#{date_str}-12-31".to_date + else flash[:notice] = "Incorrect \"Date to\" format" + end + rescue + flash[:notice] = "Incorrect \"Date to\" format" + end + end + # Append to where_query + where_query['date'] = date_range + + if permited[:lang] # Filter by language + where_query['lang'] = permited[:lang] + end + if permited[:v] # Filter by voume + where_query['volume'] = permited[:v].split(/,| /).map { |s| s.to_i } + end + if permited[:pg] # Filter by page + where_query['page'] = permited[:pg].split(/,| /).map { |s| s.to_i } + end + if permited[:pr] # Filter by paragraph + where_query['paragraph'] = permited[:pr].split(/,| /).map { |s| s.to_i } + end + return where_query + end +end diff --git a/src/app/controllers/xquery_controller.rb b/src/app/controllers/xquery_controller.rb new file mode 100755 index 0000000..087cbfd --- /dev/null +++ b/src/app/controllers/xquery_controller.rb @@ -0,0 +1,33 @@ +require "#{Rails.root}/lib/BaseXClient" + +class XqueryController < ApplicationController + + def index + end + + def show + begin + # create session + session = BaseXClient::Session.new(ENV['BASEX_URL'], 1984, "readOnly", ENV['BASEX_READONLY']) + # session.create_readOnly() + # Open DB or create if does not exist + session.execute("open xmldb") + # Get user query + input = params[:search] + # XQuery declaration of the namespace + declarate_ns = 'declare namespace ns = "http://www.tei-c.org/ns/1.0";' + # Create instance the BaseX Client in Query Mode + query = session.query(declarate_ns + input) + # Store the result + @query_result = query.execute + # Count the number of results + @number_of_results = session.query("#{declarate_ns}count(#{input})").execute.to_i + # close session + query.close() + session.close + rescue Exception => e + logger.error(e) + @query_result = "--- Sorry, this query cannot be executed ---\n"+e.to_s + end + end +end diff --git a/src/app/helpers/application_helper.rb b/src/app/helpers/application_helper.rb new file mode 100755 index 0000000..de6be79 --- /dev/null +++ b/src/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/src/app/helpers/documents_helper.rb b/src/app/helpers/documents_helper.rb new file mode 100755 index 0000000..242b4fc --- /dev/null +++ b/src/app/helpers/documents_helper.rb @@ -0,0 +1,2 @@ +module DocumentsHelper +end diff --git a/src/app/helpers/home_helper.rb b/src/app/helpers/home_helper.rb new file mode 100755 index 0000000..23de56a --- /dev/null +++ b/src/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/src/app/helpers/search_helper.rb b/src/app/helpers/search_helper.rb new file mode 100755 index 0000000..b3ce20a --- /dev/null +++ b/src/app/helpers/search_helper.rb @@ -0,0 +1,2 @@ +module SearchHelper +end diff --git a/src/app/helpers/xquery_helper.rb b/src/app/helpers/xquery_helper.rb new file mode 100755 index 0000000..ae40204 --- /dev/null +++ b/src/app/helpers/xquery_helper.rb @@ -0,0 +1,2 @@ +module XqueryHelper +end diff --git a/src/app/jobs/application_job.rb b/src/app/jobs/application_job.rb new file mode 100755 index 0000000..a009ace --- /dev/null +++ b/src/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/src/app/mailers/application_mailer.rb b/src/app/mailers/application_mailer.rb new file mode 100755 index 0000000..286b223 --- /dev/null +++ b/src/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/src/app/models/application_record.rb b/src/app/models/application_record.rb new file mode 100755 index 0000000..10a4cba --- /dev/null +++ b/src/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/src/app/models/concerns/.keep b/src/app/models/concerns/.keep new file mode 100755 index 0000000..e69de29 diff --git a/src/app/models/page_image.rb b/src/app/models/page_image.rb new file mode 100755 index 0000000..a3c3106 --- /dev/null +++ b/src/app/models/page_image.rb @@ -0,0 +1,15 @@ +class PageImage < ApplicationRecord + validates :image, :volume, :page, presence: true + validates :volume, :page, :numericality => { :greater_than_or_equal_to => 0 } + # Mount the file uploader + mount_uploader :image, ImageUploader + + def parse_filename_to_volume_page(filename) + # Split the file name into array of valid integers + arr = filename.split(/-|_/).map{|x| x.to_i}.delete_if{|i| i<=0} + + # Set the volume number to be the first valid integer + # and the page number to be the last valid integer + self.volume, self.page = arr[0], arr[-1] + end +end \ No newline at end of file diff --git a/src/app/models/search.rb b/src/app/models/search.rb new file mode 100755 index 0000000..2b5c3d3 --- /dev/null +++ b/src/app/models/search.rb @@ -0,0 +1,13 @@ +class Search < ApplicationRecord + + has_one :tr_paragraph + belongs_to :transcription_xml + searchkick searchable: [:content, :entry], + suggest: [:content], + word_start: [:content, :entry], + word_middle: [:content], + word_end: [:content], + highlight: [:content], + settings: {index: {max_result_window: 100000}} + +end diff --git a/src/app/models/tr_paragraph.rb b/src/app/models/tr_paragraph.rb new file mode 100755 index 0000000..c60b187 --- /dev/null +++ b/src/app/models/tr_paragraph.rb @@ -0,0 +1,17 @@ +class TrParagraph < ApplicationRecord + def print_data(documents) + pdf = Prawn::Document.new + documents.each do |document| + # Get date and language + date = document.date_incorrect ? document.date_incorrect : document.date + lang = document.lang == "lat" ? "Latin" : document.lang == "sco" ? "Scots" : document.lang == "nld" ? "Dutch" : document.lang + # Format the pdf content + pdf.pad(10){ pdf.text "ID: #{document.entry} Date: #{date} Language: #{lang}" } + pdf.pad(10){ pdf.text document.content} + pdf.stroke_horizontal_rule + pdf.move_down 30 + end + # Create the file + pdf.render + end +end diff --git a/src/app/models/transcription_xml.rb b/src/app/models/transcription_xml.rb new file mode 100755 index 0000000..64b89a2 --- /dev/null +++ b/src/app/models/transcription_xml.rb @@ -0,0 +1,267 @@ +class TranscriptionXml < ApplicationRecord + # Use unique filenames and overwrite on upload + validates_uniqueness_of :filename + validates :filename, :xml, presence: true + +#----------------------------------- +# Definition of functions +#----------------------------------- + + HISTEI_NS = 'http://www.tei-c.org/ns/1.0' + # Mount the file uploader + mount_uploader :xml, XmlUploader + + # Extract the volume, page and paragraph from the Entry ID + def splitEntryID(id) + return id.to_s.split(/-|_/).map{|x| x.to_i}.delete_if{|i| i==0} + end + + # Recursive function to convert the XML format to valid HTML5 + def xml_to_html(tag) + tag.children().each do |c| + # Rename the attributes + c.keys.each do |k| + c["data-#{k}"] = c.delete(k) + end + # Rename the tag and replace lb with br + c['class'] = "xml-tag #{c.name.gsub(':', '-')}" + # To avoid invalid void tags: Use "br" if "lb", otherwise "span" + c.name = c.name == 'lb' ? "br" : "span" + # Use recursion + xml_to_html(c) + end + end + +#----------------------------------- +# Parsing the paragraphs +#----------------------------------- + + def histei_split_to_paragraphs + doc = Nokogiri::XML (File.open(xml.current_path)) + + # Remove Processing Instructions. Tags of type + doc.xpath('//processing-instruction()').remove + + #----------------------------------- + # Create empty page for each not-problemtatic page break + #----------------------------------- + + # Get all page break tags which are not inside of div entry + page_breaks = doc.xpath("//xmlns:pb[@n][not(ancestor::xmlns:div[@xml:id])]", 'xmlns' => HISTEI_NS) + # Volume = the volume of any div in the document (Assuming it is always the same) + volume = splitEntryID(doc.xpath('//xmlns:div[@xml:id]/@xml:id', 'xmlns' => HISTEI_NS)[0])[0] + + # Fix for empty pages + page_breaks.each do |pb| + begin + # The attribute @n is assumed to be the page number + page = pb.xpath('@n').to_s.to_i + # If the page in the DB in not yet created + if not Search.exists?(page: page, volume: volume, paragraph: 1) + # Create a page with message "Page is empty." + pr = TrParagraph.new + pr.content_xml = "Page is empty." + pr.content_html = "Page is empty." + pr.save + s = Search.new + s.volume = volume + s.page = page + s.paragraph = 1 + s.tr_paragraph = pr + s.transcription_xml = self + s.save + end + rescue Exception => e + logger.error(e) + end + end + + #----------------------------------- + # Insert the content of each paragraph + #----------------------------------- + + # Extract all "div" tags with atribute "xml:id" + entries = doc.xpath("//xmlns:div[@xml:id]", 'xmlns' => HISTEI_NS) + # Parse each entry as follows + entries.each do |entry| + + d = entry.xpath("ancestor::xmlns:div[1]//xmlns:date/@when", 'xmlns' => HISTEI_NS) + if d.count > 1 + date_str = '' + for entry_date in d + entry_date_str = entry_date.to_s + if date_str.length < entry_date_str.length + date_str = entry_date_str + end + end + else + date_str = d.to_s + end + + # Go to the closest parent "div" of the entry and find a child "date" + # and extract the 'when' argument + date_from = entry.xpath("ancestor::xmlns:div[1]//xmlns:date/@from", 'xmlns' => HISTEI_NS).to_s + date_to = entry.xpath("ancestor::xmlns:div[1]//xmlns:date/@to", 'xmlns' => HISTEI_NS).to_s + if date_str.split('-').length == 3 + entry_date_incorrect = nil + entry_date = date_str.to_date + elsif date_str.split('-').length == 2 + entry_date_incorrect = date_str + entry_date = "#{date_str}-1".to_date # If the day is missing set ot 1-st + elsif date_str.split('-').length == 1 + entry_date_incorrect = date_str + entry_date = "#{date_str}-1-1".to_date # If the day and month are missing set ot 1-st Jan. + else + entry_date_incorrect = date_from.length != 0 ? "#{date_from} : #{date_to}": 'N/A' + entry_date = nil # The date is missing + end + + # Convert the 'entry' and 'date' Nokogiri objects to Ruby Hashes + entry_id = entry.xpath("@xml:id").to_s + entry_lang = entry.xpath("@xml:lang").to_s + case entry_lang # Fix language standad + when 'sc' + entry_lang = 'sco' + when 'la' + entry_lang = 'lat' + when 'nl' + entry_lang = 'nld' + end + entry_xml = entry.to_xml.gsub('xml:lang="sc"', 'xml:lang="sco"').gsub('xml:lang="la"', 'xml:lang="lat"').gsub('xml:lang="nl"', 'xml:lang="nld"') + entry_text =(Nokogiri::XML(entry_xml.gsub('', "\n"))).xpath('normalize-space()') + xml_to_html(entry) + entry_html = entry.to_xml + + # Split entryID + volume, page, paragraph = splitEntryID(entry) + # Overwrite if exists + if Search.exists?(page: page, volume: volume, paragraph: paragraph) + s = Search.find_by(page: page, volume: volume, paragraph: paragraph) + # Get existing paragraph + pr = s.tr_paragraph + else + # Create new search record + s = Search.new + # Create TrParagraph record + pr = TrParagraph.new + end + + # Save the new content + pr.content_xml = entry_xml + pr.content_html = entry_html + pr.save + + # Create Search record + s.entry = entry_id + s.volume = volume + s.page = page + s.paragraph = paragraph + s.tr_paragraph = pr + s.transcription_xml = self + s.lang = entry_lang + s.date = entry_date + s.date_incorrect = entry_date_incorrect + # Replace line-break tag with \n and normalize whitespace + s.content = entry_text + s.save + end + + doc = Nokogiri::XML (File.open(xml.current_path)) + doc.xpath('//processing-instruction()').remove + entries_with_page_break = doc.xpath('//xmlns:div[@xml:id]//xmlns:pb[@n]/ancestor::xmlns:div[@xml:id]', 'xmlns' => HISTEI_NS) + + #----------------------------------- + # Fix problematic page breaks + # -> Page breaks inside entries + #----------------------------------- + + # Get all entries with page break inside + + entries_with_page_break.each do |entry| + begin + + # Split the string of content by the page break + xmlContentFirstPart, xmlContentSecondPart = entry.to_s.split(/<.*pb.*\/>/) + xmlContentFirstPart = xmlContentFirstPart.gsub('xml:lang="sc"', 'xml:lang="sco"').gsub('xml:lang="la"', 'xml:lang="lat"').gsub('xml:lang="nl"', 'xml:lang="nld"') + xmlContentSecondPart = xmlContentSecondPart.gsub('xml:lang="sc"', 'xml:lang="sco"').gsub('xml:lang="la"', 'xml:lang="lat"').gsub('xml:lang="nl"', 'xml:lang="nld"') + + htmlContentFirstPart = Nokogiri::XML("

#{xmlContentFirstPart}

") + htmlContentSecondPart = Nokogiri::XML("

#{xmlContentSecondPart}

") + + # Get the id of the problematic entry + oldEntryId = entry.xpath('@xml:id').to_s + + # Split the id into page, paragraph, volume + volume, page, paragraph = splitEntryID(oldEntryId) + newPage = page + 1 + + # + # Insert the extracted content in the new paragraph + # + + # if the new paragraph is not created + # This is false when: + # -> There is inconcistency the first paragraph of the page + # has started in the previous entry + # -> The document is overwritten + # + if Search.exists?(page: newPage, volume: volume, paragraph: 1) + # Get existing paragraph + s = Search.find_by(page: newPage, volume: volume, paragraph: 1) + # Get paragraph record + pr = s.tr_paragraph + # Store the updated content for the paragraph record + pr.content_xml = xmlContentSecondPart + pr.content_html = htmlContentSecondPart.to_xml+""+ pr.content_html + pr.save + + else + + # Create new search record + s = Search.new + + # Create TrParagraph record + pr = TrParagraph.new + pr.content_xml = xmlContentSecondPart + pr.content_html = htmlContentSecondPart.to_xml + pr.save + + end + + textContentFirstPart =(Nokogiri::XML("

"+xmlContentFirstPart.gsub('', "\n")+"

")).xpath('normalize-space()') + textContentSecondPart =(Nokogiri::XML("

"+xmlContentSecondPart.gsub('', "\n")+"

")).xpath('normalize-space()') + + # Find the original record + previousEntry = Search.find_by(volume: volume,page: page,paragraph: paragraph) + + # Create Search record + s.tr_paragraph = pr + s.entry = previousEntry.entry + s.volume = volume + s.page = newPage + s.paragraph = 1 + s.transcription_xml = self + s.lang = previousEntry.lang + s.date = previousEntry.date + s.date_incorrect = previousEntry.date_incorrect + # Replace line-break tag with \n and normalize whitespace + s.content = "#{textContentSecondPart}\n#{s.content}" + s.save + + previousEntry.content = textContentFirstPart + # Get paragraph record + prPreviusEntry = previousEntry.tr_paragraph + # Remove duplicated content from entry which contains the page break + prPreviusEntry.content_xml = xmlContentFirstPart + prPreviusEntry.content_html = htmlContentFirstPart.to_xml + prPreviusEntry.save + # Save the change + previousEntry.tr_paragraph = prPreviusEntry + previousEntry.save + rescue Exception => e + logger.error(e) + end + end + + end +end diff --git a/src/app/models/user.rb b/src/app/models/user.rb new file mode 100755 index 0000000..b2091f9 --- /dev/null +++ b/src/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :trackable, :validatable +end diff --git a/src/app/uploaders/image_uploader.rb b/src/app/uploaders/image_uploader.rb new file mode 100755 index 0000000..47321d3 --- /dev/null +++ b/src/app/uploaders/image_uploader.rb @@ -0,0 +1,58 @@ +class ImageUploader < CarrierWave::Uploader::Base + include CarrierWave::MiniMagick + storage :file + + CarrierWave.configure do |config| + config.ignore_processing_errors = true + end + + # Fix By default, CarrierWave copies an uploaded file twice, + # first copying the file into the cache, then copying the file into the store.# + # For large files, this can be prohibitively time consuming. + def move_to_cache + true + end + + def move_to_store + true + end + + # Generate Web version in jpeg format + version :large do + process :efficient_conversion => [2048,2048] + def filename + super.chomp(File.extname(super)) + '.jpeg' if original_filename.present? + end + end + + version :normal do + process :efficient_conversion => [800,800] + def filename + super.chomp(File.extname(super)) + '.jpeg' if original_filename.present? + end + end + + def store_dir + "uploads/image/" + end + + def efficient_conversion(width, height) + manipulate! do |img| + img.format("JPEG") do |c| + c.fuzz "3%" + c.trim + c.resize "#{width}x#{height}>" + c.resize "#{width}x#{height}<" + end + img + end + end + + def extension_whitelist + %w(tiff tif) + end + + def content_type_whitelist + ['image/tiff', 'image/tiff-fx'] + end +end diff --git a/src/app/uploaders/xml_uploader.rb b/src/app/uploaders/xml_uploader.rb new file mode 100755 index 0000000..a5d0878 --- /dev/null +++ b/src/app/uploaders/xml_uploader.rb @@ -0,0 +1,15 @@ +class XmlUploader < CarrierWave::Uploader::Base + storage :file + + def store_dir + "uploads/xml/" + end + + def extension_whitelist + %w(xml) + end + + def content_type_whitelist + ['application/xml', 'text/xml'] + end +end diff --git a/src/app/views/devise/confirmations/new.html.erb b/src/app/views/devise/confirmations/new.html.erb new file mode 100755 index 0000000..bff85ce --- /dev/null +++ b/src/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,20 @@ +<%= bootstrap_devise_error_messages! %> +
+
+
+

<%= t('.resend_confirmation_instructions', default: 'Resend confirmation instructions') %>

+
+
+ <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, role: 'form' }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
+ + <%= f.submit t('.resend_confirmation_instructions', default: 'Resend confirmation instructions'), class: 'btn btn-primary' %> + <% end %> +
+
+ + <%= render 'devise/shared/links' %> +
diff --git a/src/app/views/devise/mailer/confirmation_instructions.html.erb b/src/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100755 index 0000000..ae24ad6 --- /dev/null +++ b/src/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,6 @@ +

<%= t('.greeting', recipient: @resource.email, default: "Welcome #{@resource.email}!") %>

+ +

<%= t('.instruction', default: 'You can confirm your account email through the link below:') %>

+ +

<%= link_to t('.action', default: 'Confirm my account'), + confirmation_url(@resource, confirmation_token: @token, locale: I18n.locale) %>

diff --git a/src/app/views/devise/mailer/reset_password_instructions.html.erb b/src/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100755 index 0000000..f757e10 --- /dev/null +++ b/src/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

<%= t('.greeting', recipient: @resource.email, default: "Hello #{@resource.email}!") %>

+ +

<%= t('.instruction', default: 'Someone has requested a link to change your password, and you can do this through the link below.') %>

+ +

<%= link_to t('.action', default: 'Change my password'), edit_password_url(@resource, reset_password_token: @token, locale: I18n.locale) %>

+ +

<%= t('.instruction_2', default: "If you didn't request this, please ignore this email.") %>

+

<%= t('.instruction_3', default: "Your password won't change until you access the link above and create a new one.") %>

diff --git a/src/app/views/devise/mailer/unlock_instructions.html.erb b/src/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100755 index 0000000..98e723d --- /dev/null +++ b/src/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

<%= t('.greeting', recipient: @resource.email, default: "Hello #{@resource.email}!") %>

+ +

<%= t('.message', default: 'Your account has been locked due to an excessive amount of unsuccessful sign in attempts.') %>

+ +

<%= t('.instruction', default: 'Click the link below to unlock your account:') %>

+ +

<%= link_to t('.action', default: 'Unlock my account'), unlock_url(@resource, unlock_token: @resource.unlock_token, locale: I18n.locale) %>

diff --git a/src/app/views/devise/passwords/edit.html.erb b/src/app/views/devise/passwords/edit.html.erb new file mode 100755 index 0000000..e1a2fd5 --- /dev/null +++ b/src/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,26 @@ +<%= bootstrap_devise_error_messages! %> +
+
+
+

<%= t('.change_your_password', default: 'Change your password') %>

+
+
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, role: 'form' }) do |f| %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, t('.new_password', default: 'New password') %> + <%= f.password_field :password, autofocus: true, class: 'form-control' %> +
+ +
+ <%= f.label :password_confirmation, t('.confirm_new_password', default: 'Confirm new password') %> + <%= f.password_field :password_confirmation, class: 'form-control' %> +
+ + <%= f.submit t('.change_my_password', default: 'Change my password'), class: 'btn btn-primary' %> + <% end %> +
+
+ <%= render 'devise/shared/links' %> +
diff --git a/src/app/views/devise/passwords/new.html.erb b/src/app/views/devise/passwords/new.html.erb new file mode 100755 index 0000000..06f7b84 --- /dev/null +++ b/src/app/views/devise/passwords/new.html.erb @@ -0,0 +1,19 @@ +<%= bootstrap_devise_error_messages! %> +
+
+
+

<%= t('.forgot_your_password', default: 'Forgot your password?') %>

+
+
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, role: "form" }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: 'form-control' %> +
+ + <%= f.submit t('.send_me_reset_password_instructions', default: 'Send me reset password instructions'), class: 'btn btn-primary' %> + <% end %> +
+
+ <%= render 'devise/shared/links' %> +
diff --git a/src/app/views/devise/registrations/edit.html.erb b/src/app/views/devise/registrations/edit.html.erb new file mode 100755 index 0000000..e1ac2a5 --- /dev/null +++ b/src/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,32 @@ +<%= bootstrap_devise_error_messages! %> +
+
+
+

<%= t('.title', resource: resource_class.model_name.human , default: "Edit #{resource_name.to_s.humanize}") %>

+
+
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: 'form-control' %> +
+
+ <%= f.label :password %> (<%= t('.leave_blank_if_you_don_t_want_to_change_it', default: "leave blank if you don't want to change it") %>) + <%= f.password_field :password, autocomplete: "off", class: 'form-control' %> +
+
+ <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, class: 'form-control' %> +
+
+ <%= f.label :current_password %> (<%= t('.we_need_your_current_password_to_confirm_your_changes', default: 'we need your current password to confirm your changes') %>) + <%= f.password_field :current_password, class: 'form-control' %> +
+ <%= f.submit t('.update', default: 'Update'), class: 'btn btn-primary' %> + <% end %> +
+
+

<%= t('.unhappy', default: 'Unhappy') %>? <%= link_to t('.cancel_my_account', default: 'Cancel my account'), registration_path(resource_name), data: { confirm: t('.are_you_sure', default: "Are you sure?") }, method: :delete %>.

+ + <%= link_to t('.back', default: 'Back'), :back %> +
diff --git a/src/app/views/devise/registrations/new.html.erb b/src/app/views/devise/registrations/new.html.erb new file mode 100755 index 0000000..a6c0236 --- /dev/null +++ b/src/app/views/devise/registrations/new.html.erb @@ -0,0 +1,26 @@ +<%= bootstrap_devise_error_messages! %> +
+
+
+

<%= t('.sign_up', default: 'Sign up') %>

+
+
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { role: 'form' }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: 'form-control' %> +
+
+ <%= f.label :password %> + <%= f.password_field :password, class: 'form-control' %> +
+
+ <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, class: 'form-control' %> +
+ <%= f.submit t('.sign_up', default: 'Sign up'), class: 'btn btn-primary' %> + <% end %> +
+
+ <%= render 'devise/shared/links' %> +
diff --git a/src/app/views/devise/sessions/new.html.erb b/src/app/views/devise/sessions/new.html.erb new file mode 100755 index 0000000..7b5913c --- /dev/null +++ b/src/app/views/devise/sessions/new.html.erb @@ -0,0 +1,28 @@ +
+
+
+

<%= t('.sign_in', default: 'Sign in') %>

+
+
+ <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { role: 'form' }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: 'form-control' %> +
+
+ <%= f.label :password %> + <%= f.password_field :password, autocomplete: 'off', class: 'form-control' %> +
+ <% if devise_mapping.rememberable? %> +
+ <%= f.label :remember_me do %> + <%= f.check_box :remember_me %> <%= t('.remember_me', default: 'Remember me') %> + <% end %> +
+ <% end %> + <%= f.submit t('.sign_in', default: 'Sign in'), class: 'btn btn-primary' %> + <% end %> +
+
+ <%= render 'devise/shared/links' %> +
diff --git a/src/app/views/devise/shared/_links.html.erb b/src/app/views/devise/shared/_links.html.erb new file mode 100755 index 0000000..8e3cd00 --- /dev/null +++ b/src/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<% if controller_name != 'sessions' %> + <%= link_to t('.sign_in', default: 'Sign in'), new_session_path(resource_name) %>
+<% end %> + +<% if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to t('.sign_up', default: 'Sign up'), new_registration_path(resource_name) %>
+<% end %> + +<% if devise_mapping.recoverable? && controller_name != 'passwords' %> + <%= link_to t('.forgot_your_password', default: 'Forgot your password?'), new_password_path(resource_name) %>
+<% end %> + +<% if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to t('.didn_t_receive_confirmation_instructions', default: "Didn't receive confirmation instructions?"), new_confirmation_path(resource_name) %>
+<% end %> + +<% if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to t('.didn_t_receive_unlock_instructions', default: "Didn't receive unlock instructions?"), new_unlock_path(resource_name) %>
+<% end %> + +<% if devise_mapping.omniauthable? %> + <% resource_class.omniauth_providers.each do |provider| %> + <%= link_to t('.sign_in_with_provider', provider: provider.to_s.titleize, default: "Sign in with #{provider.to_s.titleize}"), omniauth_authorize_path(resource_name, provider) %>
+ <% end %> +<% end %> diff --git a/src/app/views/devise/unlocks/new.html.erb b/src/app/views/devise/unlocks/new.html.erb new file mode 100755 index 0000000..792cd2e --- /dev/null +++ b/src/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +<%= bootstrap_devise_error_messages! %> +
+
+

<%= t('.resend_unlock_instructions', default: 'Resend unlock instructions') %>

+
+
+ <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, html: { role: "form" } }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: 'form-control' %> +
+ <%= f.submit t('.resend_unlock_instructions', default: 'Resend unlock instructions'), class: 'btn btn-primary'%> + <% end %> +
+
+<%= render 'devise/shared/links' %> diff --git a/src/app/views/documents/_page.html.erb b/src/app/views/documents/_page.html.erb new file mode 100755 index 0000000..1ca2756 --- /dev/null +++ b/src/app/views/documents/_page.html.erb @@ -0,0 +1,50 @@ + +
+ + <% @documents.each do |document| %> +
+
+
+ <% if document.entry %>
+ ID: <%= document.entry %> +
+ <%end%> + + <%if document.date or document.date_incorrect %> +
+ Date: <% if document.date_incorrect %><%= document.date_incorrect %><% else %><%= document.date %><% end %> +
+ <%end%> + + <%if document.lang %> +
+ Language: <%= document.lang %> +
+ <%end%> +
+
+
+ +
<%= document.tr_paragraph.content_html.html_safe %>
+ +
+
+ <% end %> + +
+ + + +<% if @document_image_normal %> +
+ + <%= image_tag(@document_image_normal, class:"img-rounded img-responsive", data: {large_image: @document_image_large}) %> + +
+<% end %> + diff --git a/src/app/views/documents/_page_simplified.html.erb b/src/app/views/documents/_page_simplified.html.erb new file mode 100755 index 0000000..0292f1a --- /dev/null +++ b/src/app/views/documents/_page_simplified.html.erb @@ -0,0 +1,36 @@ +
+ + <% @documents.each do |document| %> +
+
+
+ <% if document.entry %>
+ ID: <%= document.entry %> +
+ <%end%> + + <%if document.date or document.date_incorrect %> +
+ Date: <% if document.date_incorrect %><%= document.date_incorrect %><% else %><%= document.date %><% end %> +
+ <%end%> + + <%if document.lang %> +
+ Language: <%= document.lang %> +
+ <%end%> +
+
+
+ +
<%= document.tr_paragraph.content_html.html_safe %>
+
+ +
+ <% end %> +
diff --git a/src/app/views/documents/index.html.erb b/src/app/views/documents/index.html.erb new file mode 100755 index 0000000..8e91aac --- /dev/null +++ b/src/app/views/documents/index.html.erb @@ -0,0 +1,58 @@ +
+
+
+ + +
+ + <% if user_signed_in? and current_user.admin? %> + + <%end%> +
+
+

+
+
+ + <% if user_signed_in? and current_user.admin? %> + + <%end%> +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + +<% content_for :javascript_includes do %> + <%= javascript_include_tag "documents_browse.js" %> +<% end %> diff --git a/src/app/views/documents/new.html.erb b/src/app/views/documents/new.html.erb new file mode 100755 index 0000000..037958c --- /dev/null +++ b/src/app/views/documents/new.html.erb @@ -0,0 +1,26 @@ +
+

Upload new document

+ + <% if !flash[:notice].blank? %> +
+ <%= flash[:notice] %> +
+ <% end %> + +
+ <%= form_tag({action: :upload}, multipart: true) do %> + + <%= fields_for :transcription_xml do |t| %> + <%= t.label :xml, 'XML files' %> + <%= t.file_field :xml, multiple: true, accept: '.xml' %>
+ <% end %> + + <%= fields_for :page_image do |t| %> + <%= t.label :image, 'Image files' %> + <%= t.file_field :image, multiple: true, accept: '.tiff, .tif' %>
+ <% end %> + + <%= submit_tag "Upload", class: "btn btn-primary btn-block" %> + <% end %> +
+
diff --git a/src/app/views/documents/selected.html.erb b/src/app/views/documents/selected.html.erb new file mode 100755 index 0000000..7322b85 --- /dev/null +++ b/src/app/views/documents/selected.html.erb @@ -0,0 +1,15 @@ +
+
+
+ + <%= fa_icon "arrow-circle-o-down", text: "Download PDF"%> +
+
+
+ <%= render "documents/page"%> +
+
+ +<% content_for :javascript_includes do %> + <%= javascript_include_tag "documents_selected.js" %> +<% end %> diff --git a/src/app/views/documents/show.html.erb b/src/app/views/documents/show.html.erb new file mode 100755 index 0000000..baddc81 --- /dev/null +++ b/src/app/views/documents/show.html.erb @@ -0,0 +1,66 @@ +<% begin referrer_link = URI.parse(request.referrer) rescue referrer_link = nil end %> +<% if referrer_link and referrer_link.path == '/search' %> + +<% else %> +
+

+
+<% end %> + + + +
+
+
+ + +<% content_for :javascript_includes do %> + <%= javascript_include_tag "documents.js" %> + <%= javascript_include_tag "jquery.highlight.js" %> + +<% end %> diff --git a/src/app/views/documents/upload.html.erb b/src/app/views/documents/upload.html.erb new file mode 100755 index 0000000..751c0fe --- /dev/null +++ b/src/app/views/documents/upload.html.erb @@ -0,0 +1,74 @@ +
+

Uploaded files

+
+
+ <% if @succesfully_uploaded[:xml].length > 0 %> +
+
+

Succesfully uploaded XML files

+
+
+ <% @succesfully_uploaded[:xml].each do |filename| %> +

<%= filename %>

+ <% end %> +
+
+ <% else %> +
+

No XML files have been uploaded.

+
+ <% end %> +
+ +
+ <% if @succesfully_uploaded[:image].length > 0 %> +
+
+

Succesfully uploaded images

+
+
+ <% @succesfully_uploaded[:image].each do |filename| %> +

<%= filename %>

+ <% end %> +
+
+ <% else %> +
+

No image files have been uploaded.

+
+ <% end %> +
+
+ +
+
+ <% if @unsuccesfully_uploaded[:xml].length > 0 %> +
+
+

Unsuccesfully uploaded XML files

+
+
+ <% @unsuccesfully_uploaded[:xml].each do |filename| %> +

<%= filename %>

+ <% end %> +
+
+ <% end %> +
+ +
+ <% if @unsuccesfully_uploaded[:image].length > 0 %> +
+
+

Unsuccesfully uploaded images

+
+
+ <% @unsuccesfully_uploaded[:image].each do |filename| %> +

<%= filename %>

+ <% end %> +
+
+ <% end %> +
+
+
diff --git a/src/app/views/home/index.html.erb b/src/app/views/home/index.html.erb new file mode 100755 index 0000000..d6d5342 --- /dev/null +++ b/src/app/views/home/index.html.erb @@ -0,0 +1,78 @@ +
+
+
+
+
+
+
+ +
+
+

Aberdeen Registers

+

Aberdeen’s fifteenth-century town council registers illuminate the workings of this Scottish burgh in a way unsurpassed by other Scottish urban records of the era. They also demonstrate the interconnections of people and ideas across northern Europe in the age of the renaissance.

+
+
+
+
+ value="<%= @query%>" <%end%>> +
+ +
+
+
+
+
+
+ +
+
+
+ +
+ <%= render template: "search/advanced_search"%> +
+ +
+
+
+
+

Images of the Original Documents

+

All of the original documents are available to view in stunning high resolution images that will allow for true appreciation of the source material.

+
+
+

Read the entire Corpus

+

The transcribed text of the entire corpus of aberdeen burgh records will be made available giving access to these fascinating and world renowned documents

+
+
+ + +
+
+

Powerful search

+

Our search has the capabilities will make finding exactly what you're looking for easy; Autocomplete, search suggestions and multiple advanced search fields all at lightning fast speeds.

+
+
+

Highly Contextual

+

Relevant Contents of the documents will be displayed around the search term in order to give much greater context. Dates and document ID’s will also be displayed to further enhance this.

+
+
+

Customized Search

+

A plethora of advanced search fields such as setting a start and end date, document by ID and the ability to search for proper names, aim to make search as concise as possible.

+
+
+
+
+ +
+ + +<% content_for :javascript_includes do %> + <%= javascript_include_tag "home.js" %> +<%end%> diff --git a/src/app/views/layouts/application.html.erb b/src/app/views/layouts/application.html.erb new file mode 100755 index 0000000..613c254 --- /dev/null +++ b/src/app/views/layouts/application.html.erb @@ -0,0 +1,22 @@ + + + + LacrDemo + + <%= csrf_meta_tags %> + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= yield :stylesheet_includes %> + <%= javascript_include_tag 'application' %> + + + + + <%= render 'layouts/menu/main' %> + <%= render 'layouts/menu/flash_messages' %> + <%= yield :before_container %> + <%= yield %> + + <%= render 'layouts/menu/footer' %> + <%= yield :javascript_includes %> + + diff --git a/src/app/views/layouts/mailer.html.erb b/src/app/views/layouts/mailer.html.erb new file mode 100755 index 0000000..cbd34d2 --- /dev/null +++ b/src/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/src/app/views/layouts/mailer.text.erb b/src/app/views/layouts/mailer.text.erb new file mode 100755 index 0000000..37f0bdd --- /dev/null +++ b/src/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/src/app/views/layouts/menu/_flash_messages.html.erb b/src/app/views/layouts/menu/_flash_messages.html.erb new file mode 100755 index 0000000..83c4f11 --- /dev/null +++ b/src/app/views/layouts/menu/_flash_messages.html.erb @@ -0,0 +1,22 @@ + + <% if notice %> + + <% end %> + + <% if alert %> + + <% end %> diff --git a/src/app/views/layouts/menu/_footer.html.erb b/src/app/views/layouts/menu/_footer.html.erb new file mode 100755 index 0000000..beb625c --- /dev/null +++ b/src/app/views/layouts/menu/_footer.html.erb @@ -0,0 +1,5 @@ + diff --git a/src/app/views/layouts/menu/_login_items.html.erb b/src/app/views/layouts/menu/_login_items.html.erb new file mode 100755 index 0000000..04d83b9 --- /dev/null +++ b/src/app/views/layouts/menu/_login_items.html.erb @@ -0,0 +1,5 @@ +<% if user_signed_in? %> +
  • <%= link_to('Logout', destroy_user_session_path, :method => :delete) %>
  • +<% else %> +
  • <%= link_to('Login', login_path) %>
  • +<% end %> diff --git a/src/app/views/layouts/menu/_main.html.erb b/src/app/views/layouts/menu/_main.html.erb new file mode 100755 index 0000000..713da72 --- /dev/null +++ b/src/app/views/layouts/menu/_main.html.erb @@ -0,0 +1,81 @@ + diff --git a/src/app/views/layouts/menu/_registration_items.html.erb b/src/app/views/layouts/menu/_registration_items.html.erb new file mode 100755 index 0000000..31a51d2 --- /dev/null +++ b/src/app/views/layouts/menu/_registration_items.html.erb @@ -0,0 +1,5 @@ +<% if user_signed_in? %> +
  • <%= link_to('Edit registration', edit_user_registration_path) %>
  • +<% else %> +
  • <%= link_to('Register', new_user_registration_path) %>
  • +<% end %> diff --git a/src/app/views/search/_search_tools.html.erb b/src/app/views/search/_search_tools.html.erb new file mode 100755 index 0000000..f8a9f8f --- /dev/null +++ b/src/app/views/search/_search_tools.html.erb @@ -0,0 +1,61 @@ +
    +
    +
    + <%= label_tag 'sm', 'Search Method:', style: "font-size: 12px;"%> +
    +
    + +
    +
    + +
    +
    + <%= label_tag 'm', 'Spelling Variants:', style: "font-size: 12px;"%> +
    +
    + +
    +
    + +
    +
    + <%= label_tag 'r', 'Results Per Page:', style: "font-size: 12px;"%> +
    +
    + +
    +
    + +
    +
    + <%= label_tag 'o', 'Order By:', style: "font-size: 12px;"%> +
    +
    + +
    +
    +
    diff --git a/src/app/views/search/advanced_search.html.erb b/src/app/views/search/advanced_search.html.erb new file mode 100755 index 0000000..c779dd5 --- /dev/null +++ b/src/app/views/search/advanced_search.html.erb @@ -0,0 +1,108 @@ +
    +

    Advanced Search

    + <%= form_tag search_path, id: "adv-search", method: :get, onsubmit: "submitForm()" do |f|%> +
    +
    +
    + <%= label_tag 'sm', 'Search Method:'%> +
    +
      +
    • +
    • +
    • +
    • +
    • +
    • +
    +
    +
    +
    + <%= label_tag 'q', 'Content: '%> +
    +
    + <%= search_field_tag('q','', class: "form-control simple-search", placeholder: "Search for words or phrases...") %> +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + <%= label_tag 'v', 'Volume:'%> +
    +
    + + <% @volumes.each do |v| %> + + <% end %> +
    +
    +
    +
    + <%= label_tag 'pg', 'Page:'%> +
    +
    + <%= search_field_tag('pg','', class: "form-control", placeholder: "5, 18, 9") %> +
    +
    +
    +
    + <%= label_tag 'pr', 'Paragraph:'%> +
    +
    + <%= search_field_tag('pr','', class: "form-control", placeholder: "1, 2") %> +
    +
    +
    +
    + <%= label_tag 'content_none', 'Date:'%> +
    +
    +
    +
    + from + <%= search_field_tag('date_from','', placeholder: "yyyy-mm-dd", describedby: "date_from-label", class: "form-control datepicker") %> +
    +
    +
    +
    + to + <%= search_field_tag('date_to','', placeholder: "yyyy-mm-dd ", describedby: "date_to-label", class: "form-control datepicker") %> +
    +
    +
    +
    +
    +
    + <%= label_tag 'entry', 'Entry ID:'%> +
    +
    + <%= search_field_tag('entry','', class: "form-control", placeholder: "ARO-4-0005-03") %> +
    +
    +
    + <%= submit_tag 'Search', class: 'btn btn-primary' %> + <% end %> +
    +
    diff --git a/src/app/views/search/search.html.erb b/src/app/views/search/search.html.erb new file mode 100755 index 0000000..7d27f8b --- /dev/null +++ b/src/app/views/search/search.html.erb @@ -0,0 +1,105 @@ +
    +<%= render '/search/search_tools' %> +<% begin defined? @documents.suggestions %> +
    + <% if @documents.try(:suggestions).length > 0 %> +
    + +
    +
    + <%= @documents.total_count %> <%if @documents.total_count == 1 %>result<% else %>results<%end%> (<%= @documents.took / 1000.0 %> sec) + +
    + <%else%> +
    + <%= @documents.total_count %> <%if @documents.total_count == 1 %>result<% else %>results<%end%> (<%= @documents.took / 1000.0 %> sec) + +
    + <%end%> +
    +<%rescue%> +<%end%> +<% if @documents.length > 0 %> +
    +
    + +
    +
    +
    + <% @documents.each do |document| %> + <% begin highlighted = "&highlight="+Nokogiri::HTML.fragment(document.highlighted_content).css('mark').collect(&:text).uniq.join('+') rescue highlighted = "" end%> + + +

    + Volume: <%= document.volume %>, Page <%= document.page %> + Date: <%= document.date %> +

    + <% if defined? document.highlighted_content %> +

    <%= document.highlighted_content.html_safe %>

    + <%else%> +

    <%= document.content %>

    + <%end%> + ID: <%= document.entry %> +
    + <% end %> +
    +
    +
    + <%= will_paginate @documents, renderer: BootstrapPagination::Rails %> +
    +
    + + <% elsif @searchMethod == 5 # Regular expressions %> +
    +

    This regular expression did not match any paragraphs.

    +
    Examples of regular expressions:
    +
      +
    • The regular expression [a|A]lex... will match any string which has a or A followed by lex and any 3 characters. (E.g. “alexander”, “Alexander” or “Alexandri”)
    • +
    • The regular expression Willelmus.*Jacobi will match any string which starts with Willelmus and ends with Jacobi.
    • +
    +
    + <% else %> +
    +

    Your search - <%= raw truncate(@query, :length => 30) %> - did not match any documents.

    +

    Suggestions:

    +
      +
    • Try different keywords.
    • +
    +
    + <% end %> +
    + +