├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── annotot.gemspec ├── annotot.png ├── app ├── assets │ ├── config │ │ └── annotot_manifest.js │ ├── images │ │ └── annotot │ │ │ └── .keep │ └── javascripts │ │ └── annotot │ │ ├── annotot_endpoint.js │ │ └── application.js ├── controllers │ └── annotot │ │ ├── annotations_controller.rb │ │ └── application_controller.rb ├── helpers │ └── annotot │ │ ├── annotations_helper.rb │ │ └── application_helper.rb ├── jobs │ └── annotot │ │ └── application_job.rb ├── mailers │ └── annotot │ │ └── application_mailer.rb ├── models │ └── annotot │ │ ├── annotation.rb │ │ └── application_record.rb └── views │ ├── annotot │ └── annotations │ │ ├── index.json.erb │ │ ├── lists.json.jbuilder │ │ ├── pages.json.jbuilder │ │ └── show.json.erb │ └── layouts │ └── annotot │ └── application.html.erb ├── bin └── rails ├── config └── routes.rb ├── db └── migrate │ └── 20180215183225_create_annotot_annotations.rb ├── lib ├── annotot.rb ├── annotot │ ├── engine.rb │ └── version.rb ├── generators │ └── annotot │ │ └── install_generator.rb └── tasks │ └── annotot_tasks.rake └── spec ├── controllers └── annotations_controller_spec.rb ├── factories └── annotations.rb ├── models └── annotation_spec.rb ├── spec_helper.rb └── test_app_templates └── lib └── generators └── test_app_generator.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby: [2.7, '3.0'] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby }} 21 | - uses: actions/cache@v2 22 | with: 23 | path: vendor/bundle 24 | key: ${{ runner.os }}-gems-202103-${{ hashFiles('**/Gemfile.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-gems-202103 27 | - name: Bundle install 28 | run: bundle config path vendor/bundle 29 | - name: Install dependencies 30 | run: bin/setup 31 | - name: Run tests 32 | run: bundle exec rake 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | Gemfile.lock 9 | 10 | .internal_test_app 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in annotot.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | # BEGIN ENGINE_CART BLOCK 16 | # engine_cart: 1.2.0 17 | # engine_cart stanza: 0.10.0 18 | # the below comes from engine_cart, a gem used to test this Rails engine gem in the context of a Rails app. 19 | file = File.expand_path('Gemfile', ENV['ENGINE_CART_DESTINATION'] || ENV['RAILS_ROOT'] || File.expand_path('.internal_test_app', File.dirname(__FILE__))) 20 | if File.exist?(file) 21 | begin 22 | eval_gemfile file 23 | rescue Bundler::GemfileError => e 24 | Bundler.ui.warn '[EngineCart] Skipping Rails application dependencies:' 25 | Bundler.ui.warn e.message 26 | end 27 | else 28 | Bundler.ui.warn "[EngineCart] Unable to find test application dependencies in #{file}, using placeholder dependencies" 29 | 30 | if ENV['RAILS_VERSION'] 31 | if ENV['RAILS_VERSION'] == 'edge' 32 | gem 'rails', github: 'rails/rails' 33 | ENV['ENGINE_CART_RAILS_OPTIONS'] = '--edge --skip-turbolinks' 34 | else 35 | gem 'rails', ENV['RAILS_VERSION'] 36 | end 37 | end 38 | 39 | case ENV['RAILS_VERSION'] 40 | when /^5.[12]/ 41 | gem 'sass-rails', '~> 5.1' 42 | when /^4.2/ 43 | gem 'responders', '~> 2.0' 44 | gem 'sass-rails', '>= 5.0' 45 | gem 'coffee-rails', '~> 4.1.0' 46 | when /^4.[01]/ 47 | gem 'sass-rails', '< 5.0' 48 | end 49 | end 50 | # END ENGINE_CART BLOCK 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | © 2018 The Board of Trustees of the Leland Stanford Junior University. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Annotot 2 | [](https://travis-ci.org/mejackreed/annotot) 3 | 4 | Need to persist annotations quick and easily? Annotot, the original mini annotation API is for you. Don't annotate, annotot instead. 5 | 6 | h/t [@eefahy](https://github.com/eefahy) for the inspiration and name 7 | 8 |  9 | 10 | This logo, "Annotot", is a derivative of "[Sweet Potato Tator Tots](https://www.flickr.com/photos/flyfarther79/6270223411/)" by [Lisa Brettschneider](https://www.flickr.com/photos/flyfarther79/) used under [CC BY-NC 2.0](https://creativecommons.org/licenses/by-nc/2.0/). "Annotot" is also licensed under [CC BY-NC 2.0](https://creativecommons.org/licenses/by-nc/2.0/). 11 | 12 | ## Usage 13 | 14 | Annotot provides a simple RESTful endpoint for persisting annotations. Just configure your client to work with its endpoint. 15 | 16 | For Mirador integration, you can use the provided `AnnototEndpoint`: 17 | 18 | ```javascript 19 | // Within Mirador creation options 20 | ... 21 | annotationEndpoint: { 22 | name: 'Annotot', 23 | module: 'AnnototEndpoint', 24 | options: { 25 | endpoint: 'https://www.annotot.biz/annotot/annotations' 26 | } 27 | }, 28 | ... 29 | ``` 30 | 31 | If you want to configure Annotot to receive annotations from external sources make sure that you enable CORs in the Rails application. This can be done using [rack-cors](https://github.com/cyu/rack-cors). 32 | 33 | ## API 34 | 35 | Annotot by default mounts itself at `/`. Though [this can be changed](http://guides.rubyonrails.org/engines.html#mounting-the-engine). All API endpoints are relative to its mount location. 36 | 37 | | Path / Url | HTTP Verb | Path | Controller#Action | 38 | | --- | --- | --- | --- | 39 | | [annotations_path](#annotationspath) | GET | /annotations(.:format) | annotot/annotations#index {:format=>:json} | 40 | | | POST | /annotations(.:format) | annotot/annotations#create {:format=>:json} | 41 | | [lists_annotations_path](#listsannotationspath) | GET | /annotations/lists(.:format) | annotot/annotations#lists {:format=>:json} | 42 | | [annotation_path](#annotationpath) | PATCH | /annotations/:id(.:format) | annotot/annotations#update {:format=>:json} | 43 | | | PUT | /annotations/:id(.:format) | annotot/annotations#update {:format=>:json} 44 | | | DELETE | /annotations/:id(.:format) | annotot/annotations#destroy {:format=>:json} 45 | 46 | --- 47 | 48 | ### annotations_path 49 | `GET` - Return annotations for a given canvas 50 | 51 | Parameters: 52 | 53 | | Name | Required? | Description | 54 | | --- | --- | --- | 55 | | uri | yes | Canvas uri for which to return annotations 56 | 57 | `POST` - Create a new annotation 58 | 59 | Parameters: 60 | 61 | | Name | Required? | Description | 62 | | --- | --- | --- | 63 | | annotation | yes | object containing creation parameters 64 | | annotation.uuid | no | uuid for annotation 65 | | annotation.data | no | annotation body data as string 66 | | annotation.canvas | no | canvas to place the annotation on 67 | 68 | --- 69 | 70 | ### lists_annotations_path 71 | `GET` - Return an AnnotationList of annotations for a given canvas 72 | 73 | Parameters: 74 | 75 | | Name | Required? | Description | 76 | | --- | --- | --- | 77 | | uri | yes | Canvas uri for which to return annotations 78 | 79 | --- 80 | 81 | ### annotation_path 82 | `PATCH`, `PUT` - Update an annotation 83 | 84 | Parameters: 85 | 86 | | Name | Required? | Description | 87 | | --- | --- | --- | 88 | | id | yes | Canvas uri or Rails ActiveRecord id for annotation to update 89 | | annotation | yes | object containing creation parameters 90 | | annotation.uuid | no | uuid for annotation 91 | | annotation.data | no | annotation body data as string 92 | | annotation.canvas | no | canvas to place the annotation on 93 | 94 | `DELETE` - Delete an annotation 95 | 96 | Parameters: 97 | 98 | | Name | Required? | Description | 99 | | --- | --- | --- | 100 | | id | yes | Canvas uri or Rails ActiveRecord id for annotation to delete 101 | 102 | 103 | ## Installation 104 | Add this line to your application's Gemfile: 105 | 106 | ```ruby 107 | gem 'annotot' 108 | ``` 109 | 110 | And then execute: 111 | ```bash 112 | $ bundle install 113 | ``` 114 | 115 | Install the gem: 116 | ```bash 117 | $ rails g annotot:install 118 | ``` 119 | 120 | If you are serving both Mirador and Annotot in the same application, the integration will be easier. You can just require the `annotot_endpoint.js` file. 121 | 122 | ```javascript 123 | //= require annotot/annotot_endpoint 124 | ``` 125 | 126 | ## License 127 | The gem is available as open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0). 128 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | Bundler::GemHelper.install_tasks 7 | 8 | require 'engine_cart/rake_task' 9 | require 'rspec/core/rake_task' 10 | RSpec::Core::RakeTask.new(:spec) 11 | 12 | task ci: ['engine_cart:generate'] do 13 | ENV['environment'] = 'test' 14 | Rake::Task['spec'].invoke 15 | end 16 | 17 | task default: [:ci] 18 | -------------------------------------------------------------------------------- /annotot.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require 'annotot/version' 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = 'annotot' 9 | s.version = Annotot::VERSION 10 | s.authors = ['Jack Reed'] 11 | s.email = ['phillipjreed@gmail.com'] 12 | s.homepage = 'https://github.com/mejackreed/annotot' 13 | s.summary = 'Annotot. Open annotations in Rails.' 14 | s.description = 'Annotot. Open annotations in Rails.' 15 | s.license = 'Apache-2.0' 16 | 17 | s.files = Dir['{app,config,db,lib}/**/*', 'LICENSE', 'Rakefile', 'README.md'] 18 | 19 | s.add_dependency 'rails', '>= 5.1', '< 8' 20 | 21 | s.add_development_dependency 'engine_cart' 22 | s.add_development_dependency 'factory_bot_rails' 23 | s.add_development_dependency 'rails-controller-testing' 24 | s.add_development_dependency 'rspec-rails' 25 | s.add_development_dependency 'sqlite3' 26 | end 27 | -------------------------------------------------------------------------------- /annotot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PenguinParadigm/annotot/2c0c097da5a6bb74ccdfd6325b5b8ec7498b622e/annotot.png -------------------------------------------------------------------------------- /app/assets/config/annotot_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../javascripts/annotot .js 2 | //= link_directory ../stylesheets/annotot .css 3 | -------------------------------------------------------------------------------- /app/assets/images/annotot/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PenguinParadigm/annotot/2c0c097da5a6bb74ccdfd6325b5b8ec7498b622e/app/assets/images/annotot/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/annotot/annotot_endpoint.js: -------------------------------------------------------------------------------- 1 | /* 2 | * annotationsList - current list of OA Annotations 3 | * dfd - Deferred Object 4 | * init() 5 | * search(options, successCallback, errorCallback) 6 | * create(oaAnnotation, successCallback, errorCallback) 7 | * update(oaAnnotation, successCallback, errorCallback) 8 | * deleteAnnotation(annotationID, successCallback, errorCallback) 9 | * 10 | * getAnnotationInOA(endpointAnnotation) 11 | * getAnnotationInEndpoint(oaAnnotation) 12 | */ 13 | (function($){ 14 | 15 | $.AnnototEndpoint = function(options) { 16 | 17 | jQuery.extend(this, { 18 | dfd: null, 19 | annotationsList: [], // OA list for Mirador use 20 | windowID: null, 21 | eventEmitter: null 22 | }, options); 23 | 24 | this.init(); 25 | }; 26 | 27 | $.AnnototEndpoint.prototype = { 28 | init: function() {}, 29 | 30 | // Search endpoint for all annotations with a given URI in options 31 | search: function(options, successCallback, errorCallback) { 32 | var _this = this; 33 | _this.annotationsList = []; 34 | options.format = 'json'; 35 | 36 | jQuery.ajax({ 37 | url: _this.endpoint, 38 | type: 'GET', 39 | dataType: 'json', 40 | headers: { }, 41 | data: options, // options.uri contains the Canvas URI 42 | contentType: "application/json; charset=utf-8", 43 | success: function(data) { 44 | if (typeof successCallback === "function") { 45 | successCallback(data); 46 | } else { 47 | jQuery.each(data, function(index, value) { 48 | value.endpoint = _this; 49 | _this.annotationsList.push(_this.getAnnotationInOA(value)); 50 | }); 51 | _this.dfd.resolve(true); 52 | } 53 | }, 54 | error: function(data) { 55 | if (typeof errorCallback === "function") { 56 | errorCallback(data); 57 | } else { 58 | console.log("The request for annotations has caused an error for endpoint: " + options.uri); 59 | } 60 | } 61 | }); 62 | }, 63 | 64 | // Delete an annotation by endpoint identifier 65 | deleteAnnotation: function(annotationID, successCallback, errorCallback) { 66 | var _this = this; 67 | jQuery.ajax({ 68 | url: _this.endpoint + '/' + encodeURIComponent(annotationID), 69 | type: 'DELETE', 70 | dataType: 'json', 71 | headers: { 72 | // 'X-CSRF-Token': _this.csrfToken() 73 | }, 74 | contentType: "application/json; charset=utf-8", 75 | success: function(data) { 76 | if (typeof successCallback === "function") { 77 | successCallback(); 78 | } 79 | }, 80 | error: function(a, b) { 81 | if (typeof errorCallback === "function") { 82 | errorCallback(); 83 | } 84 | } 85 | }); 86 | }, 87 | 88 | // Update an annotation given the OA version 89 | update: function(oaAnnotation, successCallback, errorCallback) { 90 | var _this = this; 91 | var annotation = _this.getAnnotationInEndpoint(oaAnnotation); 92 | 93 | var annotationID = annotation.annotation.uuid; 94 | 95 | jQuery.ajax({ 96 | url: _this.endpoint + '/' + encodeURIComponent(annotationID), 97 | type: 'PATCH', 98 | dataType: 'json', 99 | headers: { 100 | 'X-CSRF-Token': _this.csrfToken() 101 | }, 102 | data: JSON.stringify(annotation), 103 | contentType: "application/json; charset=utf-8", 104 | success: function(data) { 105 | if (typeof successCallback === "function") { 106 | data.endpoint = _this; 107 | successCallback(data); 108 | } 109 | }, 110 | error: function(a, b, c) { 111 | if (typeof errorCallback === "function") { 112 | errorCallback(); 113 | } 114 | } 115 | }); 116 | }, 117 | 118 | // Takes OA Annotation, gets Endpoint Annotation, and saves 119 | // if successful, MUST return the OA rendering of the annotation 120 | create: function(oaAnnotation, successCallback, errorCallback) { 121 | var _this = this; 122 | var annotation = _this.getAnnotationInEndpoint(oaAnnotation); 123 | jQuery.ajax({ 124 | url: _this.endpoint, 125 | type: 'POST', 126 | headers: { 127 | // 'X-CSRF-Token': _this.csrfToken() 128 | }, 129 | data: JSON.stringify(annotation), 130 | contentType: "application/json; charset=utf-8", 131 | success: function(data) { 132 | data.endpoint = _this; 133 | if (typeof successCallback === "function") { 134 | successCallback(_this.getAnnotationInOA(data)); 135 | } 136 | }, 137 | error: function() { 138 | if (typeof errorCallback === "function") { 139 | errorCallback(); 140 | } 141 | } 142 | }); 143 | }, 144 | 145 | set: function(prop, value, options) { 146 | if (options) { 147 | this[options.parent][prop] = value; 148 | } else { 149 | this[prop] = value; 150 | } 151 | }, 152 | 153 | //Convert Endpoint annotation to OA 154 | getAnnotationInOA: function(annotation) { 155 | return annotation; 156 | }, 157 | 158 | // Converts OA Annotation to endpoint format 159 | getAnnotationInEndpoint: function(oaAnnotation) { 160 | // Generate a new uuid if one is not present 161 | // condition needs to be set to check for "falsy" values 162 | // !oaAnnotation['@id'] should match null and undefined 163 | if (!oaAnnotation['@id']) { 164 | oaAnnotation["@id"] = Mirador.genUUID(); 165 | } 166 | 167 | canvas = oaAnnotation.on[0].full; 168 | var newAnno = jQuery.extend({}, oaAnnotation); 169 | delete newAnno.endpoint; 170 | return { 171 | annotation: { 172 | uuid: oaAnnotation["@id"], 173 | data: JSON.stringify(newAnno), 174 | canvas: canvas 175 | } 176 | }; 177 | }, 178 | 179 | userAuthorize: function(action, annotation) { 180 | return true; // allow all 181 | }, 182 | 183 | csrfToken: function() { 184 | // We need to monkey patch this since $ !== jQuery in Mirador context 185 | return jQuery('meta[name=csrf-token]').attr('content'); 186 | }, 187 | 188 | }; 189 | 190 | }(Mirador)); 191 | -------------------------------------------------------------------------------- /app/assets/javascripts/annotot/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /app/controllers/annotot/annotations_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "annotot/application_controller" 2 | 3 | module Annotot 4 | class AnnotationsController < ApplicationController 5 | before_action :load_annotations, except: %i[update destroy show create] 6 | before_action :set_annotation, only: %i[update destroy show] 7 | before_action :build_annotation, only: :create 8 | 9 | # GET /annotations 10 | def index; end 11 | 12 | # GET /annotations 13 | def show; end 14 | 15 | # Get /annotations/lists 16 | def lists; end 17 | 18 | # Get /annotations/pages 19 | def pages; end 20 | 21 | # POST /annotations 22 | def create 23 | @annotation.assign_attributes(annotation_params) 24 | 25 | if @annotation.save 26 | render json: @annotation.data 27 | else 28 | render status: :bad_request, json: { 29 | message: 'An annotation could not be created' 30 | } 31 | end 32 | end 33 | 34 | # PATCH/PUT /annotations/1 35 | def update 36 | if @annotation.update(annotation_params) 37 | render status: :ok, json: @annotation.data 38 | else 39 | render status: :bad_request, json: { 40 | message: 'Annotation could not be updated.', 41 | status: :bad_request 42 | } 43 | end 44 | end 45 | 46 | # DELETE /annotations/1 47 | def destroy 48 | @annotation.destroy 49 | render status: :ok, json: { 50 | message: 'Annotation was successfully destroyed.' 51 | } 52 | end 53 | 54 | private 55 | 56 | def load_annotations 57 | @annotations = Annotation.where(canvas: annotation_search_params) 58 | end 59 | 60 | def set_annotation 61 | @annotation = Annotot::Annotation.retrieve_by_id_or_uuid( 62 | CGI.unescape(params[:id]) 63 | ) 64 | raise ActiveRecord::RecordNotFound unless @annotation.present? 65 | end 66 | 67 | def build_annotation 68 | @annotation = Annotation.new 69 | end 70 | 71 | def annotation_params 72 | params.require(:annotation).permit(:uuid, :data, :canvas) 73 | end 74 | 75 | def annotation_search_params 76 | CGI.unescape(params.require(:uri)) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/controllers/annotot/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery with: :exception, unless: -> { request.format.json? } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/annotot/annotations_helper.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | module AnnotationsHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/annotot/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | module ApplicationHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/jobs/annotot/application_job.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | class ApplicationJob < ActiveJob::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/annotot/application_mailer.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: 'from@example.com' 4 | layout 'mailer' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/annotot/annotation.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | class Annotation < ApplicationRecord 3 | validates :uuid, presence: true, uniqueness: true 4 | validates :canvas, presence: true 5 | 6 | serialize :data, JSON 7 | 8 | ## 9 | # Used to differentiate between a numeric id and a uuid. Rails will trim a 10 | # uuid with leading numbers eg '123a'.to_i => 123 11 | # @param [String] identifier 12 | def self.retrieve_by_id_or_uuid(identifier) 13 | if identifier =~ /\A\d+\z/ 14 | find_by(id: identifier) 15 | else 16 | find_by(uuid: identifier) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/annotot/application_record.rb: -------------------------------------------------------------------------------- 1 | module Annotot 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/annotot/annotations/index.json.erb: -------------------------------------------------------------------------------- 1 | <%= @annotations.map{ |a| JSON.parse(a.data )}.to_json.html_safe %> 2 | -------------------------------------------------------------------------------- /app/views/annotot/annotations/lists.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! '@context', 'http://iiif.io/api/presentation/2/context.json' 2 | json.set! '@id', request.original_url 3 | json.set! '@type', 'sc:AnnotationList' 4 | json.resources(@annotations.map { |a| JSON.parse(a.data) }) 5 | -------------------------------------------------------------------------------- /app/views/annotot/annotations/pages.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! '@context', 'http://iiif.io/api/presentation/3/context.json' 2 | json.set! 'id', request.original_url 3 | json.set! 'type', 'AnnotationPage' 4 | json.items(@annotations.map { |a| JSON.parse(a.data) }) 5 | -------------------------------------------------------------------------------- /app/views/annotot/annotations/show.json.erb: -------------------------------------------------------------------------------- 1 | <%= JSON.parse(@annotation.data).to_json.html_safe %> 2 | -------------------------------------------------------------------------------- /app/views/layouts/annotot/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |