From 078a74ace5ca83c9899129b8bb7c62948920aeb9 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Tue, 25 Nov 2025 12:46:55 +0100 Subject: [PATCH 01/12] Add OAI-PMH Ingestors --- lib/ingestors/ingestor_factory.rb | 4 +- lib/ingestors/oai_pmh_bioschemas_ingestor.rb | 157 +++++++++++++++++++ lib/ingestors/oai_pmh_ingestor.rb | 104 ++++++++++++ 3 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 lib/ingestors/oai_pmh_bioschemas_ingestor.rb create mode 100644 lib/ingestors/oai_pmh_ingestor.rb diff --git a/lib/ingestors/ingestor_factory.rb b/lib/ingestors/ingestor_factory.rb index 67e818d02..913deacb6 100644 --- a/lib/ingestors/ingestor_factory.rb +++ b/lib/ingestors/ingestor_factory.rb @@ -10,6 +10,8 @@ def self.ingestors Ingestors::MaterialCsvIngestor, Ingestors::TessEventIngestor, Ingestors::ZenodoIngestor, + Ingestors::OaiPmhIngestor, + Ingestors::OaiPmhBioschemasIngestor ] + taxila_ingestors + llm_ingestors end @@ -35,7 +37,7 @@ def self.taxila_ingestors Ingestors::Taxila::OsciIngestor, Ingestors::Taxila::DccIngestor, Ingestors::Taxila::SenseIngestor, - Ingestors::Taxila::VuMaterialIngestor, + Ingestors::Taxila::VuMaterialIngestor ] end diff --git a/lib/ingestors/oai_pmh_bioschemas_ingestor.rb b/lib/ingestors/oai_pmh_bioschemas_ingestor.rb new file mode 100644 index 000000000..925b982f6 --- /dev/null +++ b/lib/ingestors/oai_pmh_bioschemas_ingestor.rb @@ -0,0 +1,157 @@ +require 'open-uri' +require 'tess_rdf_extractors' + +module Ingestors + class OaiPmhBioschemasIngestor < Ingestor + DUMMY_URL = 'https://example.com' + + attr_reader :verbose + + def self.config + { + key: 'oai_pmh_bioschemas', + title: 'OAI-PMH (Bioschemas RDF)', + user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0', + mail: Rails.configuration.tess['contact_email'] + } + end + + def read(source_url) + provider_events = [] + provider_materials = [] + totals = Hash.new(0) + + client = OAI::Client.new source_url, headers: { 'From' => config[:mail] } + client.list_records(metadata_prefix: 'rdf').full.each do |record| + metadata_tag = Nokogiri::XML(record.metadata.to_s) + bioschemas_xml = metadata_tag.at_xpath('metadata/rdf:RDF', 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')&.to_s + output = read_content(bioschemas_xml) + next unless output + + provider_events += output[:resources][:events] + provider_materials += output[:resources][:materials] + output[:totals].each do |key, value| + totals[key] += value + end + end + + if totals.keys.any? + bioschemas_summary = "Bioschemas summary:\n" + totals.each do |type, count| + bioschemas_summary << "\n - #{type}: #{count}" + end + @messages << bioschemas_summary + end + + deduplicate(provider_events).each do |event_params| + add_event(event_params) + end + + deduplicate(provider_materials).each do |material_params| + add_material(material_params) + end + end + + def read_content(content) + output = { + resources: { + events: [], + materials: [] + }, + totals: Hash.new(0) + } + + return output unless content + + begin + events = Tess::Rdf::EventExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + courses = Tess::Rdf::CourseExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + course_instances = Tess::Rdf::CourseInstanceExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + learning_resources = Tess::Rdf::LearningResourceExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + output[:totals]['Events'] += events.count + output[:totals]['Courses'] += courses.count + output[:totals]['CourseInstances'] += course_instances.count + output[:totals]['LearningResources'] += learning_resources.count + + deduplicate(events + courses + course_instances).each do |event| + output[:resources][:events] << event + end + + deduplicate(learning_resources).each do |material| + output[:resources][:materials] << material + end + rescue StandardError => e + Rails.logger.error("#{e.class}: #{e.message}") + Rails.logger.error(e.backtrace.join("\n")) if e.backtrace&.any? + error = 'An error' + comment = nil + if e.is_a?(RDF::ReaderError) + error = 'A parsing error' + comment = 'Please check your page contains valid RDF/XML.' + end + message = "#{error} occurred while reading the source." + message << " #{comment}" if comment + @messages << message + end + + output + end + + # ---- This is copied unchanged from bioschemas_ingestor.rb and needs to be refactored. ---- + + # If duplicate resources have been extracted, prefer ones with the most metadata. + def deduplicate(resources) + return [] unless resources.any? + + puts "De-duplicating #{resources.count} resources" if verbose + hash = {} + scores = {} + resources.each do |resource| + resource_url = resource[:url] + puts " Considering: #{resource_url}" if verbose + if hash[resource_url] + score = metadata_score(resource) + # Replace the resource if this resource has a higher metadata score + puts " Duplicate! Comparing #{score} vs. #{scores[resource_url]}" if verbose + if score > scores[resource_url] + puts ' Replacing resource' if verbose + hash[resource_url] = resource + scores[resource_url] = score + end + else + puts ' Not present, adding' if verbose + hash[resource_url] = resource + scores[resource_url] = metadata_score(resource) + end + end + + puts "#{hash.values.count} resources after de-duplication" if verbose + + hash.values + end + + # Score based on number of metadata fields available + def metadata_score(resource) + score = 0 + resource.each_value do |value| + score += 1 unless value.nil? || value == {} || value == [] || (value.is_a?(String) && value.strip == '') + end + + score + end + + def convert_params(params) + params[:description] = convert_description(params[:description]) if params.key?(:description) + + params + end + end +end diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb new file mode 100644 index 000000000..c8edab5a9 --- /dev/null +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -0,0 +1,104 @@ +require 'open-uri' +require 'tess_rdf_extractors' +require 'oai' +require 'nokogiri' + +module Ingestors + class OaiPmhIngestor < Ingestor + DUMMY_URL = 'https://example.com' + + attr_reader :verbose + + def self.config + { + key: 'oai_pmh', + title: 'OAI-PMH', + user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0', + mail: Rails.configuration.tess['contact_email'] + } + end + + def ns + { + 'dc' => 'http://purl.org/dc/elements/1.1/', + 'oai_dc' => 'http://www.openarchives.org/OAI/2.0/oai_dc/' + } + end + + def read(source_url) + client = OAI::Client.new source_url, headers: { 'From' => config[:mail] } + count = 0 + client.list_records.full.each do |record| + read_dublin_core(record.metadata.to_s) + count += 1 + end + @messages << "found #{count} records" + end + + def read_dublin_core(xml_string) + doc = Nokogiri::XML(xml_string) + + types = doc.xpath('//dc:type', ns).map(&:text) + if types.include?('http://purl.org/dc/dcmitype/Event') + read_dublin_core_event(doc) + else + read_dublin_core_material(doc) + end + end + + def read_dublin_core_material(xml_doc) + material = OpenStruct.new + material.title = xml_doc.at_xpath('//dc:title', ns)&.text + material.description = convert_description(xml_doc.at_xpath('//dc:description', ns)&.text) + material.authors = xml_doc.xpath('//dc:creator', ns).map(&:text) + material.contributors = xml_doc.xpath('//dc:contributor', ns).map(&:text) + material.licence = xml_doc.at_xpath('//dc:rights', ns)&.text + + dates = xml_doc.xpath('//dc:date', ns).map(&:text) + parsed_dates = dates.map do |d| + Date.parse(d) + rescue StandardError + nil + end.compact + material.date_created = parsed_dates.first + material.date_modified = parsed_dates.last if parsed_dates.size > 1 + + identifiers = xml_doc.xpath('//dc:identifier', ns).map(&:text) + doi = identifiers.find { |id| id.start_with?('10.') || id.include?('doi.org') } + if doi + doi = doi&.sub(%r{https?://doi\.org/}, '') + material.doi = "https://doi.org/#{doi}" + end + material.url = identifiers.find { |id| id.start_with?('http://', 'https://') } + + material.keywords = xml_doc.xpath('//dc:subject', ns).map(&:text) + material.resource_type = xml_doc.xpath('//dc:type', ns).map(&:text) + material.contact = xml_doc.at_xpath('//dc:publisher', ns)&.text + + add_material material + end + + def read_dublin_core_event(_xml_doc) + event = OpenStruct.new + + event.title = doc.at_xpath('//dc:title', ns)&.text + event.description = convert_description(doc.at_xpath('//dc:description', ns)&.text) + event.url = doc.xpath('//dc:identifier', ns).map(&:text).find { |id| id.start_with?('http://', 'https://') } + event.contact = doc.at_xpath('//dc:publisher', ns)&.text + event.organizer = doc.at_xpath('//dc:creator', ns)&.text + event.keywords = doc.xpath('//dc:subject', ns).map(&:text) + event.event_types = types + + dates = doc.xpath('//dc:date', ns).map(&:text) + parsed_dates = dates.map do |d| + Date.parse(d) + rescue StandardError + nil + end.compact + event.start = parsed_dates.first + event.end = parsed_dates.last + + add_event event + end + end +end From 664b1aec9053fca61fd77d88e7700fc9e6af764a Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Thu, 4 Dec 2025 10:10:35 +0100 Subject: [PATCH 02/12] Fix html syntax error when parsing bioschemas from TeSS --- app/views/layouts/application.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cba95b0c3..7367b9154 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,7 +3,7 @@ <%= render 'layouts/head' %> - <%= 'header-notice-present' if TeSS::Config.header_notice&.strip.present? %>> + <%= render partial: 'layouts/header' %>
From b78eb326d91295a01a0f6137fc0debc2a128b239 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Thu, 22 Jan 2026 12:22:17 +0100 Subject: [PATCH 03/12] Merge OAI-PMH ingestors and autodetect Bioschemas support --- lib/ingestors/ingestor_factory.rb | 3 +- lib/ingestors/oai_pmh_bioschemas_ingestor.rb | 157 ------------------ lib/ingestors/oai_pmh_ingestor.rb | 159 ++++++++++++++++++- 3 files changed, 155 insertions(+), 164 deletions(-) delete mode 100644 lib/ingestors/oai_pmh_bioschemas_ingestor.rb diff --git a/lib/ingestors/ingestor_factory.rb b/lib/ingestors/ingestor_factory.rb index 913deacb6..34e5f7290 100644 --- a/lib/ingestors/ingestor_factory.rb +++ b/lib/ingestors/ingestor_factory.rb @@ -10,8 +10,7 @@ def self.ingestors Ingestors::MaterialCsvIngestor, Ingestors::TessEventIngestor, Ingestors::ZenodoIngestor, - Ingestors::OaiPmhIngestor, - Ingestors::OaiPmhBioschemasIngestor + Ingestors::OaiPmhIngestor ] + taxila_ingestors + llm_ingestors end diff --git a/lib/ingestors/oai_pmh_bioschemas_ingestor.rb b/lib/ingestors/oai_pmh_bioschemas_ingestor.rb deleted file mode 100644 index 925b982f6..000000000 --- a/lib/ingestors/oai_pmh_bioschemas_ingestor.rb +++ /dev/null @@ -1,157 +0,0 @@ -require 'open-uri' -require 'tess_rdf_extractors' - -module Ingestors - class OaiPmhBioschemasIngestor < Ingestor - DUMMY_URL = 'https://example.com' - - attr_reader :verbose - - def self.config - { - key: 'oai_pmh_bioschemas', - title: 'OAI-PMH (Bioschemas RDF)', - user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0', - mail: Rails.configuration.tess['contact_email'] - } - end - - def read(source_url) - provider_events = [] - provider_materials = [] - totals = Hash.new(0) - - client = OAI::Client.new source_url, headers: { 'From' => config[:mail] } - client.list_records(metadata_prefix: 'rdf').full.each do |record| - metadata_tag = Nokogiri::XML(record.metadata.to_s) - bioschemas_xml = metadata_tag.at_xpath('metadata/rdf:RDF', 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')&.to_s - output = read_content(bioschemas_xml) - next unless output - - provider_events += output[:resources][:events] - provider_materials += output[:resources][:materials] - output[:totals].each do |key, value| - totals[key] += value - end - end - - if totals.keys.any? - bioschemas_summary = "Bioschemas summary:\n" - totals.each do |type, count| - bioschemas_summary << "\n - #{type}: #{count}" - end - @messages << bioschemas_summary - end - - deduplicate(provider_events).each do |event_params| - add_event(event_params) - end - - deduplicate(provider_materials).each do |material_params| - add_material(material_params) - end - end - - def read_content(content) - output = { - resources: { - events: [], - materials: [] - }, - totals: Hash.new(0) - } - - return output unless content - - begin - events = Tess::Rdf::EventExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) - end - courses = Tess::Rdf::CourseExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) - end - course_instances = Tess::Rdf::CourseInstanceExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) - end - learning_resources = Tess::Rdf::LearningResourceExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) - end - output[:totals]['Events'] += events.count - output[:totals]['Courses'] += courses.count - output[:totals]['CourseInstances'] += course_instances.count - output[:totals]['LearningResources'] += learning_resources.count - - deduplicate(events + courses + course_instances).each do |event| - output[:resources][:events] << event - end - - deduplicate(learning_resources).each do |material| - output[:resources][:materials] << material - end - rescue StandardError => e - Rails.logger.error("#{e.class}: #{e.message}") - Rails.logger.error(e.backtrace.join("\n")) if e.backtrace&.any? - error = 'An error' - comment = nil - if e.is_a?(RDF::ReaderError) - error = 'A parsing error' - comment = 'Please check your page contains valid RDF/XML.' - end - message = "#{error} occurred while reading the source." - message << " #{comment}" if comment - @messages << message - end - - output - end - - # ---- This is copied unchanged from bioschemas_ingestor.rb and needs to be refactored. ---- - - # If duplicate resources have been extracted, prefer ones with the most metadata. - def deduplicate(resources) - return [] unless resources.any? - - puts "De-duplicating #{resources.count} resources" if verbose - hash = {} - scores = {} - resources.each do |resource| - resource_url = resource[:url] - puts " Considering: #{resource_url}" if verbose - if hash[resource_url] - score = metadata_score(resource) - # Replace the resource if this resource has a higher metadata score - puts " Duplicate! Comparing #{score} vs. #{scores[resource_url]}" if verbose - if score > scores[resource_url] - puts ' Replacing resource' if verbose - hash[resource_url] = resource - scores[resource_url] = score - end - else - puts ' Not present, adding' if verbose - hash[resource_url] = resource - scores[resource_url] = metadata_score(resource) - end - end - - puts "#{hash.values.count} resources after de-duplication" if verbose - - hash.values - end - - # Score based on number of metadata fields available - def metadata_score(resource) - score = 0 - resource.each_value do |value| - score += 1 unless value.nil? || value == {} || value == [] || (value.is_a?(String) && value.strip == '') - end - - score - end - - def convert_params(params) - params[:description] = convert_description(params[:description]) if params.key?(:description) - - params - end - end -end diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index c8edab5a9..8522f948b 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -1,7 +1,5 @@ require 'open-uri' require 'tess_rdf_extractors' -require 'oai' -require 'nokogiri' module Ingestors class OaiPmhIngestor < Ingestor @@ -18,6 +16,17 @@ def self.config } end + def read(source_url) + client = OAI::Client.new source_url, headers: { 'From' => config[:mail] } + found_bioschemas = begin + read_oai_rdf(client) + rescue OAI::ArgumentException + false + end + + read_oai_default(client) unless found_bioschemas + end + def ns { 'dc' => 'http://purl.org/dc/elements/1.1/', @@ -25,8 +34,7 @@ def ns } end - def read(source_url) - client = OAI::Client.new source_url, headers: { 'From' => config[:mail] } + def read_oai_default(client) count = 0 client.list_records.full.each do |record| read_dublin_core(record.metadata.to_s) @@ -52,7 +60,9 @@ def read_dublin_core_material(xml_doc) material.description = convert_description(xml_doc.at_xpath('//dc:description', ns)&.text) material.authors = xml_doc.xpath('//dc:creator', ns).map(&:text) material.contributors = xml_doc.xpath('//dc:contributor', ns).map(&:text) - material.licence = xml_doc.at_xpath('//dc:rights', ns)&.text + + rights = xml_doc.xpath('//dc:rights', ns).map { |n| n.text&.strip }.reject(&:empty?) + material.licence = rights.find { |r| r.start_with?('http://', 'https://') } || rights.first || 'notspecified' dates = xml_doc.xpath('//dc:date', ns).map(&:text) parsed_dates = dates.map do |d| @@ -100,5 +110,144 @@ def read_dublin_core_event(_xml_doc) add_event event end + + def read_oai_rdf(client) + provider_events = [] + provider_materials = [] + totals = Hash.new(0) + + client.list_records(metadata_prefix: 'rdf').full.each do |record| + metadata_tag = Nokogiri::XML(record.metadata.to_s) + bioschemas_xml = metadata_tag.at_xpath('metadata/rdf:RDF', 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')&.to_s + output = parse_bioschemas(bioschemas_xml) + next unless output + + provider_events += output[:resources][:events] + provider_materials += output[:resources][:materials] + output[:totals].each do |key, value| + totals[key] += value + end + end + + if totals.keys.any? + bioschemas_summary = "Bioschemas summary:\n" + totals.each do |type, count| + bioschemas_summary << "\n - #{type}: #{count}" + end + @messages << bioschemas_summary + end + + deduplicate(provider_events).each do |event_params| + add_event(event_params) + end + + deduplicate(provider_materials).each do |material_params| + add_material(material_params) + end + + provider_events.any? || provider_materials.any? + end + + def parse_bioschemas(content) + output = { + resources: { + events: [], + materials: [] + }, + totals: Hash.new(0) + } + + return output unless content + + begin + events = Tess::Rdf::EventExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + courses = Tess::Rdf::CourseExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + course_instances = Tess::Rdf::CourseInstanceExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + learning_resources = Tess::Rdf::LearningResourceExtractor.new(content, :rdfxml).extract do |p| + convert_params(p) + end + output[:totals]['Events'] += events.count + output[:totals]['Courses'] += courses.count + output[:totals]['CourseInstances'] += course_instances.count + output[:totals]['LearningResources'] += learning_resources.count + + deduplicate(events + courses + course_instances).each do |event| + output[:resources][:events] << event + end + + deduplicate(learning_resources).each do |material| + output[:resources][:materials] << material + end + rescue StandardError => e + Rails.logger.error("#{e.class}: #{e.message}") + Rails.logger.error(e.backtrace.join("\n")) if e.backtrace&.any? + error = 'An error' + comment = nil + if e.is_a?(RDF::ReaderError) + error = 'A parsing error' + comment = 'Please check your page contains valid RDF/XML.' + end + message = "#{error} occurred while reading the source." + message << " #{comment}" if comment + @messages << message + end + + output + end + + # ---- This is copied unchanged from bioschemas_ingestor.rb and needs to be refactored. ---- + + # If duplicate resources have been extracted, prefer ones with the most metadata. + def deduplicate(resources) + return [] unless resources.any? + + puts "De-duplicating #{resources.count} resources" if verbose + hash = {} + scores = {} + resources.each do |resource| + resource_url = resource[:url] + puts " Considering: #{resource_url}" if verbose + if hash[resource_url] + score = metadata_score(resource) + # Replace the resource if this resource has a higher metadata score + puts " Duplicate! Comparing #{score} vs. #{scores[resource_url]}" if verbose + if score > scores[resource_url] + puts ' Replacing resource' if verbose + hash[resource_url] = resource + scores[resource_url] = score + end + else + puts ' Not present, adding' if verbose + hash[resource_url] = resource + scores[resource_url] = metadata_score(resource) + end + end + + puts "#{hash.values.count} resources after de-duplication" if verbose + + hash.values + end + + # Score based on number of metadata fields available + def metadata_score(resource) + score = 0 + resource.each_value do |value| + score += 1 unless value.nil? || value == {} || value == [] || (value.is_a?(String) && value.strip == '') + end + + score + end + + def convert_params(params) + params[:description] = convert_description(params[:description]) if params.key?(:description) + + params + end end end From c260fa0345909e517f4a6961290e36765e3cfa77 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Thu, 29 Jan 2026 14:48:00 +0100 Subject: [PATCH 04/12] run migrations --- db/schema.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/db/schema.rb b/db/schema.rb index b3a246afe..ec25ad43f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -464,6 +464,16 @@ t.string "title" end + create_table "source_filters", force: :cascade do |t| + t.bigint "source_id", null: false + t.string "filter_mode" + t.string "filter_by" + t.string "filter_value" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["source_id"], name: "index_source_filters_on_source_id" + end + create_table "sources", force: :cascade do |t| t.bigint "content_provider_id" t.bigint "user_id" @@ -669,6 +679,7 @@ add_foreign_key "materials", "users" add_foreign_key "node_links", "nodes" add_foreign_key "nodes", "users" + add_foreign_key "source_filters", "sources" add_foreign_key "sources", "content_providers" add_foreign_key "sources", "spaces" add_foreign_key "sources", "users" From 928dd19c9b0fc3bc3304976a66b7078221c39953 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Tue, 3 Feb 2026 13:16:07 +0100 Subject: [PATCH 05/12] Improve handling of urls in OAI-PMH ingestor --- lib/ingestors/oai_pmh_ingestor.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index 8522f948b..5353eb17c 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -47,7 +47,10 @@ def read_dublin_core(xml_string) doc = Nokogiri::XML(xml_string) types = doc.xpath('//dc:type', ns).map(&:text) - if types.include?('http://purl.org/dc/dcmitype/Event') + # this event detection heuristic captures in particular + # - http://purl.org/dc/dcmitype/Event (the standard way of typing an event in dublin core) + # - https://schema.org/Event + if types.any? { |t| t.downcase.include? 'event' } read_dublin_core_event(doc) else read_dublin_core_material(doc) @@ -74,7 +77,7 @@ def read_dublin_core_material(xml_doc) material.date_modified = parsed_dates.last if parsed_dates.size > 1 identifiers = xml_doc.xpath('//dc:identifier', ns).map(&:text) - doi = identifiers.find { |id| id.start_with?('10.') || id.include?('doi.org') } + doi = identifiers.find { |id| id.start_with?('10.') || id.start_with?('https://doi.org/') || id.start_with?('http://doi.org/') } if doi doi = doi&.sub(%r{https?://doi\.org/}, '') material.doi = "https://doi.org/#{doi}" From 2c0c6b7e60ca0f7b5c71d5859072d64f9336e2cb Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Tue, 3 Feb 2026 15:04:19 +0100 Subject: [PATCH 06/12] improve code style of oai pmh ingestor --- lib/ingestors/oai_pmh_ingestor.rb | 35 ++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index 5353eb17c..12df9abea 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -3,8 +3,6 @@ module Ingestors class OaiPmhIngestor < Ingestor - DUMMY_URL = 'https://example.com' - attr_reader :verbose def self.config @@ -24,7 +22,7 @@ def read(source_url) false end - read_oai_default(client) unless found_bioschemas + read_oai_dublin_core(client) unless found_bioschemas end def ns @@ -34,29 +32,27 @@ def ns } end - def read_oai_default(client) + def read_oai_dublin_core(client) count = 0 client.list_records.full.each do |record| - read_dublin_core(record.metadata.to_s) + xml_string = record.metadata.to_s + doc = Nokogiri::XML(xml_string) + + types = doc.xpath('//dc:type', ns).map(&:text) + # this event detection heuristic captures in particular + # - http://purl.org/dc/dcmitype/Event (the standard way of typing an event in dublin core) + # - https://schema.org/Event + if types.any? { |t| t.downcase.include? 'event' } + read_dublin_core_event(doc) + else + read_dublin_core_material(doc) + end + count += 1 end @messages << "found #{count} records" end - def read_dublin_core(xml_string) - doc = Nokogiri::XML(xml_string) - - types = doc.xpath('//dc:type', ns).map(&:text) - # this event detection heuristic captures in particular - # - http://purl.org/dc/dcmitype/Event (the standard way of typing an event in dublin core) - # - https://schema.org/Event - if types.any? { |t| t.downcase.include? 'event' } - read_dublin_core_event(doc) - else - read_dublin_core_material(doc) - end - end - def read_dublin_core_material(xml_doc) material = OpenStruct.new material.title = xml_doc.at_xpath('//dc:title', ns)&.text @@ -205,6 +201,7 @@ def parse_bioschemas(content) end # ---- This is copied unchanged from bioschemas_ingestor.rb and needs to be refactored. ---- + # note that also attr_reader :verbose is probably related to this # If duplicate resources have been extracted, prefer ones with the most metadata. def deduplicate(resources) From 4d26ecd383ad938022cc5473b8aacf823c60deb0 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Tue, 3 Feb 2026 16:31:37 +0100 Subject: [PATCH 07/12] remove duplicated code in OAI-PMH ingestor --- lib/ingestors/oai_pmh_ingestor.rb | 75 +++++++------------------------ 1 file changed, 15 insertions(+), 60 deletions(-) diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index 12df9abea..063f2bc8c 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -3,8 +3,6 @@ module Ingestors class OaiPmhIngestor < Ingestor - attr_reader :verbose - def self.config { key: 'oai_pmh', @@ -14,6 +12,13 @@ def self.config } end + def initialize + super + + # to use some helper functions that are instance level methods of BioschemasIngestor + @bioschemas_manager = BioschemasIngestor.new + end + def read(source_url) client = OAI::Client.new source_url, headers: { 'From' => config[:mail] } found_bioschemas = begin @@ -136,11 +141,11 @@ def read_oai_rdf(client) @messages << bioschemas_summary end - deduplicate(provider_events).each do |event_params| + @bioschemas_manager.deduplicate(provider_events).each do |event_params| add_event(event_params) end - deduplicate(provider_materials).each do |material_params| + @bioschemas_manager.deduplicate(provider_materials).each do |material_params| add_material(material_params) end @@ -160,27 +165,27 @@ def parse_bioschemas(content) begin events = Tess::Rdf::EventExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) + @bioschemas_manager.convert_params(p) end courses = Tess::Rdf::CourseExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) + @bioschemas_manager.convert_params(p) end course_instances = Tess::Rdf::CourseInstanceExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) + @bioschemas_manager.convert_params(p) end learning_resources = Tess::Rdf::LearningResourceExtractor.new(content, :rdfxml).extract do |p| - convert_params(p) + @bioschemas_manager.convert_params(p) end output[:totals]['Events'] += events.count output[:totals]['Courses'] += courses.count output[:totals]['CourseInstances'] += course_instances.count output[:totals]['LearningResources'] += learning_resources.count - deduplicate(events + courses + course_instances).each do |event| + @bioschemas_manager.deduplicate(events + courses + course_instances).each do |event| output[:resources][:events] << event end - deduplicate(learning_resources).each do |material| + @bioschemas_manager.deduplicate(learning_resources).each do |material| output[:resources][:materials] << material end rescue StandardError => e @@ -199,55 +204,5 @@ def parse_bioschemas(content) output end - - # ---- This is copied unchanged from bioschemas_ingestor.rb and needs to be refactored. ---- - # note that also attr_reader :verbose is probably related to this - - # If duplicate resources have been extracted, prefer ones with the most metadata. - def deduplicate(resources) - return [] unless resources.any? - - puts "De-duplicating #{resources.count} resources" if verbose - hash = {} - scores = {} - resources.each do |resource| - resource_url = resource[:url] - puts " Considering: #{resource_url}" if verbose - if hash[resource_url] - score = metadata_score(resource) - # Replace the resource if this resource has a higher metadata score - puts " Duplicate! Comparing #{score} vs. #{scores[resource_url]}" if verbose - if score > scores[resource_url] - puts ' Replacing resource' if verbose - hash[resource_url] = resource - scores[resource_url] = score - end - else - puts ' Not present, adding' if verbose - hash[resource_url] = resource - scores[resource_url] = metadata_score(resource) - end - end - - puts "#{hash.values.count} resources after de-duplication" if verbose - - hash.values - end - - # Score based on number of metadata fields available - def metadata_score(resource) - score = 0 - resource.each_value do |value| - score += 1 unless value.nil? || value == {} || value == [] || (value.is_a?(String) && value.strip == '') - end - - score - end - - def convert_params(params) - params[:description] = convert_description(params[:description]) if params.key?(:description) - - params - end end end From 0080133e62988f5835640881a21845cffd7714fa Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Tue, 3 Feb 2026 17:04:14 +0100 Subject: [PATCH 08/12] Update dublin core iOAI-PMH import --- lib/ingestors/oai_pmh_ingestor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index 063f2bc8c..3721fd295 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -101,7 +101,7 @@ def read_dublin_core_event(_xml_doc) event.contact = doc.at_xpath('//dc:publisher', ns)&.text event.organizer = doc.at_xpath('//dc:creator', ns)&.text event.keywords = doc.xpath('//dc:subject', ns).map(&:text) - event.event_types = types + event.event_types = doc.xpath('//dc:type', ns).map(&:text) dates = doc.xpath('//dc:date', ns).map(&:text) parsed_dates = dates.map do |d| From cbf2699abd9dcd50128812156eed3140b562cf40 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Wed, 4 Feb 2026 18:21:23 +0100 Subject: [PATCH 09/12] Implement tests for oai-pmh ingestor --- lib/ingestors/oai_pmh_ingestor.rb | 21 ++-- test/unit/ingestors/oai_pmh_test.rb | 187 ++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 test/unit/ingestors/oai_pmh_test.rb diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index 3721fd295..e61e715b3 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -60,7 +60,8 @@ def read_oai_dublin_core(client) def read_dublin_core_material(xml_doc) material = OpenStruct.new - material.title = xml_doc.at_xpath('//dc:title', ns)&.text + material.title = xml_doc.at_xpath('//dc:title', ns)&.text + puts xml_doc.at_xpath('//dc:description', ns)&.text material.description = convert_description(xml_doc.at_xpath('//dc:description', ns)&.text) material.authors = xml_doc.xpath('//dc:creator', ns).map(&:text) material.contributors = xml_doc.xpath('//dc:contributor', ns).map(&:text) @@ -92,18 +93,18 @@ def read_dublin_core_material(xml_doc) add_material material end - def read_dublin_core_event(_xml_doc) + def read_dublin_core_event(xml_doc) event = OpenStruct.new - event.title = doc.at_xpath('//dc:title', ns)&.text - event.description = convert_description(doc.at_xpath('//dc:description', ns)&.text) - event.url = doc.xpath('//dc:identifier', ns).map(&:text).find { |id| id.start_with?('http://', 'https://') } - event.contact = doc.at_xpath('//dc:publisher', ns)&.text - event.organizer = doc.at_xpath('//dc:creator', ns)&.text - event.keywords = doc.xpath('//dc:subject', ns).map(&:text) - event.event_types = doc.xpath('//dc:type', ns).map(&:text) + event.title = xml_doc.at_xpath('//dc:title', ns)&.text + event.description = convert_description(xml_doc.at_xpath('//dc:description', ns)&.text) + event.url = xml_doc.xpath('//dc:identifier', ns).map(&:text).find { |id| id.start_with?('http://', 'https://') } + event.contact = xml_doc.at_xpath('//dc:publisher', ns)&.text + event.organizer = xml_doc.at_xpath('//dc:creator', ns)&.text + event.keywords = xml_doc.xpath('//dc:subject', ns).map(&:text) + event.event_types = xml_doc.xpath('//dc:type', ns).map(&:text) - dates = doc.xpath('//dc:date', ns).map(&:text) + dates = xml_doc.xpath('//dc:date', ns).map(&:text) parsed_dates = dates.map do |d| Date.parse(d) rescue StandardError diff --git a/test/unit/ingestors/oai_pmh_test.rb b/test/unit/ingestors/oai_pmh_test.rb new file mode 100644 index 000000000..65c22e049 --- /dev/null +++ b/test/unit/ingestors/oai_pmh_test.rb @@ -0,0 +1,187 @@ +require 'test_helper' + +class FakeClient + def initialize(rdf_strings, dc_strings) + @rdf_response = Minitest::Mock.new + rdf_response = rdf_strings.map do |s| + inner_mock = Minitest::Mock.new + outer_mock = Minitest::Mock.new + inner_mock.expect(:metadata, outer_mock, []) + outer_mock.expect(:to_s, s, []) + inner_mock + end + dc_response = dc_strings.map do |s| + inner_mock = Minitest::Mock.new + outer_mock = Minitest::Mock.new + inner_mock.expect(:metadata, outer_mock, []) + outer_mock.expect(:to_s, s, []) + inner_mock + end + @rdf_response.expect(:full, rdf_response, []) + @dc_response = Minitest::Mock.new + @dc_response.expect(:full, dc_response, []) + end + + def list_records(metadata_prefix: nil) + if metadata_prefix == 'rdf' + @rdf_response + else + @dc_response + end + end +end + +class OaiPmhTest < ActiveSupport::TestCase + setup do + @ingestor = Ingestors::OaiPmhIngestor.new + @user = users(:regular_user) + @content_provider = content_providers(:another_portal_provider) + end + + test 'should read empty oai pmh endpoint' do + OAI::Client.stub(:new, FakeClient.new([], [])) do + @ingestor.read('https://example.org') + end + assert_equal @ingestor.materials, [] + assert_equal @ingestor.events, [] + end + + test 'should read dublin core material' do + record = <<~METADATA + + + dc_title + dc_description <b>bold_text</b> + A, Alice + B, Bob + + public access + https://opensource.org/licenses/MIT + 2023-06-26 + 2026-06-26 + https://rodare.hzdr.de/record/2513 + 10.14278/rodare.2269 + kA + kB + kC + + + METADATA + + OAI::Client.stub(:new, FakeClient.new([], [record])) do + @ingestor.read('https://example.org') + end + result = @ingestor.materials.first + + assert_equal 'dc_title', result.title + assert_equal 'dc\\_description **bold\\_text**', result.description + assert_equal ['A, Alice', 'B, Bob'], result.authors + assert_equal 'https://opensource.org/licenses/MIT', result.licence + assert_equal Date.parse('2023-06-26'), result.date_created + assert_equal Date.parse('2026-06-26'), result.date_modified + assert_equal 'https://doi.org/10.14278/rodare.2269', result.doi + assert_equal 'https://rodare.hzdr.de/record/2513', result.url + assert_equal %w[kA kB kC], result.keywords + end + + test 'should read dublin core event' do + record = <<~METADATA + + + http://purl.org/dc/dcmitype/Event + dc_title + dc_description <b>bold_text</b> + https://example.org/dc_url + A, Alice + B, Bob + kA + kB + kC + 2026-01-01 + 2026-01-02 + + + METADATA + + OAI::Client.stub(:new, FakeClient.new([], [record])) do + @ingestor.read('https://example.org') + end + result = @ingestor.events.first + + assert_equal 'dc_title', result.title + assert_equal 'dc\\_description **bold\\_text**', result.description + assert_equal 'https://example.org/dc_url', result.url + assert_equal 'A, Alice', result.organizer + assert_equal %w[kA kB kC], result.keywords + assert_equal Date.parse('2026-01-01'), result.start + assert_equal Date.parse('2026-01-02'), result.end + end + + test 'should read multiple dublin core events and materials' do + event1 = <<~METADATA + + + http://purl.org/dc/dcmitype/Event + title1 + + + METADATA + + event2 = <<~METADATA + + + http://purl.org/dc/dcmitype/Event + title2 + + + METADATA + + material1 = <<~METADATA + + + title3 + + + METADATA + + material2 = <<~METADATA + + + title4 + + + METADATA + + OAI::Client.stub(:new, FakeClient.new([], [material1, material2, event1, event2])) do + @ingestor.read('https://example.org') + end + + assert_equal %w[title1 title2], @ingestor.events.map(&:title) + assert_equal %w[title3 title4], @ingestor.materials.map(&:title) + end + + test 'should read bioschemas' do + # TODO: add event and maybe course and course instance + material = <<~METADATA + + + + + + + bioschemas title + + + + METADATA + + OAI::Client.stub(:new, FakeClient.new([material, material], [])) do + @ingestor.read('https://example.org') + end + + assert_equal 1, @ingestor.materials.length + result = @ingestor.materials.first + assert_equal 'bioschemas title', result.title + assert_equal 'https://example.org/bioschemas/material', result.url + end +end From 8581f0301d1ad843eecafe8adb368e1b187aa0a1 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Wed, 4 Feb 2026 18:43:07 +0100 Subject: [PATCH 10/12] Add bioschemas event oai-pmh ingestor test --- lib/ingestors/oai_pmh_ingestor.rb | 1 - test/unit/ingestors/oai_pmh_test.rb | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/ingestors/oai_pmh_ingestor.rb b/lib/ingestors/oai_pmh_ingestor.rb index e61e715b3..20e9998fc 100644 --- a/lib/ingestors/oai_pmh_ingestor.rb +++ b/lib/ingestors/oai_pmh_ingestor.rb @@ -61,7 +61,6 @@ def read_oai_dublin_core(client) def read_dublin_core_material(xml_doc) material = OpenStruct.new material.title = xml_doc.at_xpath('//dc:title', ns)&.text - puts xml_doc.at_xpath('//dc:description', ns)&.text material.description = convert_description(xml_doc.at_xpath('//dc:description', ns)&.text) material.authors = xml_doc.xpath('//dc:creator', ns).map(&:text) material.contributors = xml_doc.xpath('//dc:contributor', ns).map(&:text) diff --git a/test/unit/ingestors/oai_pmh_test.rb b/test/unit/ingestors/oai_pmh_test.rb index 65c22e049..45c22bea8 100644 --- a/test/unit/ingestors/oai_pmh_test.rb +++ b/test/unit/ingestors/oai_pmh_test.rb @@ -161,7 +161,6 @@ class OaiPmhTest < ActiveSupport::TestCase end test 'should read bioschemas' do - # TODO: add event and maybe course and course instance material = <<~METADATA @@ -171,11 +170,21 @@ class OaiPmhTest < ActiveSupport::TestCase bioschemas title + METADATA - OAI::Client.stub(:new, FakeClient.new([material, material], [])) do + event = <<~METADATA + + + bioschemas title2 + + + + METADATA + + OAI::Client.stub(:new, FakeClient.new([material, material, event], [])) do @ingestor.read('https://example.org') end @@ -183,5 +192,11 @@ class OaiPmhTest < ActiveSupport::TestCase result = @ingestor.materials.first assert_equal 'bioschemas title', result.title assert_equal 'https://example.org/bioschemas/material', result.url + assert_equal 'https://opensource.org/licenses/MIT', result.licence + + assert_equal 1, @ingestor.events.length + result = @ingestor.events.first + assert_equal 'bioschemas title2', result.title + assert_equal 'https://example.org/bioschemas/event', result.url end end From 9b2c24fe21c31ea831af74ec53a1c2c17aa8867e Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Thu, 5 Feb 2026 16:30:40 +0100 Subject: [PATCH 11/12] Undo accidental db/schema.rb changes --- db/schema.rb | 1351 +++++++++++++++++++++++++------------------------- 1 file changed, 670 insertions(+), 681 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index ec25ad43f..eb7e8e069 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,686 +10,675 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_12_22_142740) do +ActiveRecord::Schema[7.2].define(version: 20_251_222_142_740) do # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - create_table "activities", force: :cascade do |t| - t.integer "trackable_id" - t.string "trackable_type" - t.integer "owner_id" - t.string "owner_type" - t.string "key" - t.text "parameters" - t.integer "recipient_id" - t.string "recipient_type" - t.datetime "created_at" - t.datetime "updated_at" - t.index ["key"], name: "index_activities_on_key" - t.index ["owner_id", "owner_type"], name: "index_activities_on_owner_id_and_owner_type" - t.index ["recipient_id", "recipient_type"], name: "index_activities_on_recipient_id_and_recipient_type" - t.index ["trackable_id", "trackable_type"], name: "index_activities_on_trackable_id_and_trackable_type" - end - - create_table "ahoy_events", force: :cascade do |t| - t.bigint "visit_id" - t.bigint "user_id" - t.string "name" - t.jsonb "properties" - t.datetime "time" - t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time" - t.index ["properties"], name: "index_ahoy_events_on_properties", opclass: :jsonb_path_ops, using: :gin - t.index ["user_id"], name: "index_ahoy_events_on_user_id" - t.index ["visit_id"], name: "index_ahoy_events_on_visit_id" - end - - create_table "ahoy_visits", force: :cascade do |t| - t.string "visit_token" - t.string "visitor_token" - t.bigint "user_id" - t.string "ip" - t.text "user_agent" - t.text "referrer" - t.string "referring_domain" - t.text "landing_page" - t.string "browser" - t.string "os" - t.string "device_type" - t.string "country" - t.string "region" - t.string "city" - t.float "latitude" - t.float "longitude" - t.string "utm_source" - t.string "utm_medium" - t.string "utm_term" - t.string "utm_content" - t.string "utm_campaign" - t.string "app_version" - t.string "os_version" - t.string "platform" - t.datetime "started_at" - t.index ["user_id"], name: "index_ahoy_visits_on_user_id" - t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true - end - - create_table "autocomplete_suggestions", force: :cascade do |t| - t.string "field" - t.string "value" - t.index ["field", "value"], name: "index_autocomplete_suggestions_on_field_and_value", unique: true - end - - create_table "bans", force: :cascade do |t| - t.integer "user_id" - t.integer "banner_id" - t.boolean "shadow" - t.text "reason" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["banner_id"], name: "index_bans_on_banner_id" - t.index ["user_id"], name: "index_bans_on_user_id" - end - - create_table "collaborations", force: :cascade do |t| - t.integer "user_id" - t.integer "resource_id" - t.string "resource_type" - t.index ["resource_type", "resource_id"], name: "index_collaborations_on_resource_type_and_resource_id" - t.index ["user_id"], name: "index_collaborations_on_user_id" - end - - create_table "collection_items", force: :cascade do |t| - t.bigint "collection_id" - t.string "resource_type" - t.bigint "resource_id" - t.text "comment" - t.integer "order" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["collection_id"], name: "index_collection_items_on_collection_id" - t.index ["resource_type", "resource_id"], name: "index_collection_items_on_resource" - end - - create_table "collections", force: :cascade do |t| - t.string "title" - t.text "description" - t.text "image_url" - t.boolean "public", default: true - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "user_id" - t.string "slug" - t.string "keywords", default: [], array: true - t.string "image_file_name" - t.string "image_content_type" - t.bigint "image_file_size" - t.datetime "image_updated_at" - t.bigint "space_id" - t.index ["slug"], name: "index_collections_on_slug", unique: true - t.index ["space_id"], name: "index_collections_on_space_id" - t.index ["user_id"], name: "index_collections_on_user_id" - end - - create_table "content_providers", force: :cascade do |t| - t.text "title" - t.text "url" - t.text "image_url" - t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "slug" - t.string "keywords", default: [], array: true - t.integer "user_id" - t.integer "node_id" - t.string "content_provider_type", default: "Organisation" - t.string "image_file_name" - t.string "image_content_type" - t.bigint "image_file_size" - t.datetime "image_updated_at" - t.string "contact" - t.string "content_curation_email" - t.index ["node_id"], name: "index_content_providers_on_node_id" - t.index ["slug"], name: "index_content_providers_on_slug", unique: true - t.index ["user_id"], name: "index_content_providers_on_user_id" - end - - create_table "content_providers_users", id: false, force: :cascade do |t| - t.bigint "content_provider_id" - t.bigint "user_id" - t.index ["content_provider_id", "user_id"], name: "provider_user_unique", unique: true - t.index ["content_provider_id"], name: "index_content_providers_users_on_content_provider_id" - t.index ["user_id"], name: "index_content_providers_users_on_user_id" - end - - create_table "edit_suggestions", force: :cascade do |t| - t.text "name" - t.text "text" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "suggestible_id" - t.string "suggestible_type" - t.json "data_fields", default: {} - t.index ["suggestible_id", "suggestible_type"], name: "index_edit_suggestions_on_suggestible_id_and_suggestible_type" - end - - create_table "event_materials", force: :cascade do |t| - t.integer "event_id" - t.integer "material_id" - t.index ["event_id"], name: "index_event_materials_on_event_id" - t.index ["material_id"], name: "index_event_materials_on_material_id" - end - - create_table "events", force: :cascade do |t| - t.string "external_id" - t.string "title" - t.string "subtitle" - t.string "url" - t.string "organizer" - t.text "description" - t.datetime "start" - t.datetime "end" - t.string "sponsors", default: [], array: true - t.text "venue" - t.string "city" - t.string "county" - t.string "country" - t.string "postcode" - t.decimal "latitude", precision: 10, scale: 6 - t.decimal "longitude", precision: 10, scale: 6 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "source", default: "tess" - t.string "slug" - t.integer "content_provider_id" - t.integer "user_id" - t.integer "presence", default: 0 - t.decimal "cost_value" - t.date "last_scraped" - t.boolean "scraper_record", default: false - t.string "keywords", default: [], array: true - t.string "event_types", default: [], array: true - t.string "target_audience", default: [], array: true - t.integer "capacity" - t.string "eligibility", default: [], array: true - t.text "contact" - t.string "host_institutions", default: [], array: true - t.string "timezone" - t.string "funding" - t.integer "attendee_count" - t.integer "applicant_count" - t.integer "trainer_count" - t.string "feedback" - t.text "notes" - t.integer "nominatim_count", default: 0 - t.string "duration" - t.text "recognition" - t.text "learning_objectives" - t.text "prerequisites" - t.text "tech_requirements" - t.string "cost_basis" - t.string "cost_currency" - t.string "fields", default: [], array: true - t.boolean "visible", default: true - t.string "language" - t.string "open_science", default: [], array: true - t.bigint "space_id" - t.index ["presence"], name: "index_events_on_presence" - t.index ["slug"], name: "index_events_on_slug", unique: true - t.index ["space_id"], name: "index_events_on_space_id" - t.index ["user_id"], name: "index_events_on_user_id" - end - - create_table "external_resources", force: :cascade do |t| - t.integer "source_id" - t.text "url" - t.string "title" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "source_type" - t.index ["source_id", "source_type"], name: "index_external_resources_on_source_id_and_source_type" - end - - create_table "field_locks", force: :cascade do |t| - t.integer "resource_id" - t.string "resource_type" - t.string "field" - t.index ["resource_type", "resource_id"], name: "index_field_locks_on_resource_type_and_resource_id" - end - - create_table "friendly_id_slugs", force: :cascade do |t| - t.string "slug", null: false - t.integer "sluggable_id", null: false - t.string "sluggable_type", limit: 50 - t.string "scope" - t.datetime "created_at" - t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true - t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type" - t.index ["sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_id" - t.index ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type" - end - - create_table "learning_path_topic_items", force: :cascade do |t| - t.bigint "topic_id" - t.string "resource_type" - t.bigint "resource_id" - t.text "comment" - t.integer "order" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["resource_type", "resource_id"], name: "index_learning_path_topic_items_on_resource" - t.index ["topic_id"], name: "index_learning_path_topic_items_on_topic_id" - end - - create_table "learning_path_topic_links", force: :cascade do |t| - t.bigint "learning_path_id" - t.bigint "topic_id" - t.integer "order" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["learning_path_id"], name: "index_learning_path_topic_links_on_learning_path_id" - t.index ["topic_id"], name: "index_learning_path_topic_links_on_topic_id" - end - - create_table "learning_path_topics", force: :cascade do |t| - t.string "title" - t.text "description" - t.integer "user_id" - t.string "keywords", default: [], array: true - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "difficulty_level", default: "notspecified" - t.bigint "space_id" - t.index ["space_id"], name: "index_learning_path_topics_on_space_id" - end - - create_table "learning_paths", force: :cascade do |t| - t.text "title" - t.text "description" - t.string "doi" - t.string "target_audience", default: [], array: true - t.string "authors", default: [], array: true - t.string "contributors", default: [], array: true - t.string "licence", default: "notspecified" - t.string "difficulty_level", default: "notspecified" - t.string "slug" - t.bigint "user_id" - t.bigint "content_provider_id" - t.string "keywords", default: [], array: true - t.text "prerequisites" - t.text "learning_objectives" - t.string "status" - t.string "learning_path_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "public", default: true - t.bigint "space_id" - t.index ["content_provider_id"], name: "index_learning_paths_on_content_provider_id" - t.index ["slug"], name: "index_learning_paths_on_slug", unique: true - t.index ["space_id"], name: "index_learning_paths_on_space_id" - t.index ["user_id"], name: "index_learning_paths_on_user_id" - end - - create_table "link_monitors", force: :cascade do |t| - t.string "url" - t.integer "code" - t.datetime "failed_at" - t.datetime "last_failed_at" - t.integer "fail_count" - t.integer "lcheck_id" - t.string "lcheck_type" - t.index ["lcheck_type", "lcheck_id"], name: "index_link_monitors_on_lcheck_type_and_lcheck_id" - end - - create_table "llm_interactions", force: :cascade do |t| - t.bigint "event_id" - t.datetime "created_at" - t.datetime "updated_at" - t.string "scrape_or_process" - t.string "model" - t.string "prompt" - t.string "input" - t.string "output" - t.boolean "needs_processing", default: false - t.index ["event_id"], name: "index_llm_interactions_on_event_id" - end - - create_table "materials", force: :cascade do |t| - t.text "title" - t.string "url" - t.string "doi" - t.date "remote_updated_date" - t.date "remote_created_date" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "description" - t.string "target_audience", default: [], array: true - t.string "authors", default: [], array: true - t.string "contributors", default: [], array: true - t.string "licence", default: "notspecified" - t.string "difficulty_level", default: "notspecified" - t.integer "content_provider_id" - t.string "slug" - t.integer "user_id" - t.date "last_scraped" - t.boolean "scraper_record", default: false - t.string "resource_type", default: [], array: true - t.string "keywords", default: [], array: true - t.string "other_types" - t.date "date_created" - t.date "date_modified" - t.date "date_published" - t.text "prerequisites" - t.string "version" - t.string "status" - t.text "syllabus" - t.string "subsets", default: [], array: true - t.text "contact" - t.text "learning_objectives" - t.string "fields", default: [], array: true - t.boolean "visible", default: true - t.bigint "space_id" - t.index ["content_provider_id"], name: "index_materials_on_content_provider_id" - t.index ["slug"], name: "index_materials_on_slug", unique: true - t.index ["space_id"], name: "index_materials_on_space_id" - t.index ["user_id"], name: "index_materials_on_user_id" - end - - create_table "node_links", force: :cascade do |t| - t.integer "node_id" - t.integer "resource_id" - t.string "resource_type" - t.index ["node_id"], name: "index_node_links_on_node_id" - t.index ["resource_type", "resource_id"], name: "index_node_links_on_resource_type_and_resource_id" - end - - create_table "nodes", force: :cascade do |t| - t.string "name" - t.string "member_status" - t.string "country_code" - t.string "home_page" - t.string "twitter" - t.string "carousel_images", array: true - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "slug" - t.integer "user_id" - t.text "image_url" - t.text "description" - t.index ["slug"], name: "index_nodes_on_slug", unique: true - t.index ["user_id"], name: "index_nodes_on_user_id" - end - - create_table "ontology_term_links", force: :cascade do |t| - t.integer "resource_id" - t.string "resource_type" - t.string "term_uri" - t.string "field" - t.index ["field"], name: "index_ontology_term_links_on_field" - t.index ["resource_type", "resource_id"], name: "index_ontology_term_links_on_resource_type_and_resource_id" - t.index ["term_uri"], name: "index_ontology_term_links_on_term_uri" - end - - create_table "profiles", force: :cascade do |t| - t.text "firstname" - t.text "surname" - t.text "image_url" - t.text "email" - t.text "website" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "user_id" - t.string "slug" - t.boolean "public", default: false - t.text "description" - t.text "location" - t.string "orcid" - t.string "experience" - t.string "expertise_academic", default: [], array: true - t.string "expertise_technical", default: [], array: true - t.string "interest", default: [], array: true - t.string "activity", default: [], array: true - t.string "language", default: [], array: true - t.string "social_media", default: [], array: true - t.string "type", default: "Profile" - t.string "fields", default: [], array: true - t.boolean "orcid_authenticated", default: false - t.index ["orcid"], name: "index_profiles_on_orcid" - t.index ["slug"], name: "index_profiles_on_slug", unique: true - end - - create_table "roles", force: :cascade do |t| - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "title" - end - - create_table "source_filters", force: :cascade do |t| - t.bigint "source_id", null: false - t.string "filter_mode" - t.string "filter_by" - t.string "filter_value" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["source_id"], name: "index_source_filters_on_source_id" - end - - create_table "sources", force: :cascade do |t| - t.bigint "content_provider_id" - t.bigint "user_id" - t.datetime "created_at" - t.datetime "finished_at" - t.string "url" - t.string "method" - t.integer "records_read" - t.integer "records_written" - t.integer "resources_added" - t.integer "resources_updated" - t.integer "resources_rejected" - t.text "log" - t.boolean "enabled" - t.string "token" - t.integer "approval_status" - t.datetime "updated_at" - t.string "default_language" - t.bigint "space_id" - t.index ["content_provider_id"], name: "index_sources_on_content_provider_id" - t.index ["space_id"], name: "index_sources_on_space_id" - t.index ["user_id"], name: "index_sources_on_user_id" - end - - create_table "space_roles", force: :cascade do |t| - t.string "key" - t.bigint "user_id" - t.bigint "space_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["space_id"], name: "index_space_roles_on_space_id" - t.index ["user_id"], name: "index_space_roles_on_user_id" - end - - create_table "spaces", force: :cascade do |t| - t.string "title" - t.text "description" - t.string "host" - t.string "theme" - t.string "image_file_name" - t.string "image_content_type" - t.bigint "image_file_size" - t.datetime "image_updated_at" - t.bigint "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "image_url" - t.string "disabled_features", default: [], array: true - t.index ["host"], name: "index_spaces_on_host", unique: true - t.index ["user_id"], name: "index_spaces_on_user_id" - end - - create_table "staff_members", force: :cascade do |t| - t.string "name" - t.string "role" - t.string "email" - t.text "image_url" - t.integer "node_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "image_file_name" - t.string "image_content_type" - t.bigint "image_file_size" - t.datetime "image_updated_at" - t.index ["node_id"], name: "index_staff_members_on_node_id" - end - - create_table "stars", force: :cascade do |t| - t.integer "user_id" - t.integer "resource_id" - t.string "resource_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["resource_type", "resource_id"], name: "index_stars_on_resource_type_and_resource_id" - t.index ["user_id"], name: "index_stars_on_user_id" - end - - create_table "subscriptions", force: :cascade do |t| - t.integer "user_id" - t.datetime "last_sent_at" - t.text "query" - t.json "facets" - t.integer "frequency" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "subscribable_type" - t.datetime "last_checked_at" - t.index ["user_id"], name: "index_subscriptions_on_user_id" - end - - create_table "users", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "username" - t.integer "role_id" - t.string "authentication_token" - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" - t.inet "current_sign_in_ip" - t.inet "last_sign_in_ip" - t.string "confirmation_token" - t.datetime "confirmed_at" - t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.integer "failed_attempts", default: 0, null: false - t.string "unlock_token" - t.datetime "locked_at" - t.string "slug" - t.string "provider" - t.string "uid" - t.string "identity_url" - t.string "invitation_token" - t.datetime "invitation_created_at" - t.datetime "invitation_sent_at" - t.datetime "invitation_accepted_at" - t.integer "invitation_limit" - t.string "invited_by_type" - t.bigint "invited_by_id" - t.integer "invitations_count", default: 0 - t.text "image_url" - t.string "image_file_name" - t.string "image_content_type" - t.bigint "image_file_size" - t.datetime "image_updated_at" - t.boolean "check_broken_scrapers", default: false - t.index ["authentication_token"], name: "index_users_on_authentication_token" - t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true - t.index ["email"], name: "index_users_on_email", unique: true - t.index ["identity_url"], name: "index_users_on_identity_url", unique: true - t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true - t.index ["invited_by_id"], name: "index_users_on_invited_by_id" - t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by" - t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true - t.index ["role_id"], name: "index_users_on_role_id" - t.index ["slug"], name: "index_users_on_slug", unique: true - t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true - t.index ["username"], name: "index_users_on_username", unique: true - end - - create_table "widget_logs", force: :cascade do |t| - t.string "widget_name" - t.string "action" - t.integer "resource_id" - t.string "resource_type" - t.text "data" - t.json "params" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "referrer" - t.index ["resource_type", "resource_id"], name: "index_widget_logs_on_resource_type_and_resource_id" - end - - create_table "workflows", force: :cascade do |t| - t.string "title" - t.string "description" - t.integer "user_id" - t.json "workflow_content" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "slug" - t.string "target_audience", default: [], array: true - t.string "keywords", default: [], array: true - t.string "authors", default: [], array: true - t.string "contributors", default: [], array: true - t.string "licence", default: "notspecified" - t.string "difficulty_level", default: "notspecified" - t.string "doi" - t.date "remote_created_date" - t.date "remote_updated_date" - t.boolean "hide_child_nodes", default: false - t.boolean "public", default: true - t.bigint "space_id" - t.index ["slug"], name: "index_workflows_on_slug", unique: true - t.index ["space_id"], name: "index_workflows_on_space_id" - t.index ["user_id"], name: "index_workflows_on_user_id" - end - - add_foreign_key "bans", "users" - add_foreign_key "bans", "users", column: "banner_id" - add_foreign_key "collaborations", "users" - add_foreign_key "collections", "spaces" - add_foreign_key "collections", "users" - add_foreign_key "content_providers", "nodes" - add_foreign_key "content_providers", "users" - add_foreign_key "event_materials", "events" - add_foreign_key "event_materials", "materials" - add_foreign_key "events", "spaces" - add_foreign_key "events", "users" - add_foreign_key "learning_path_topic_links", "learning_paths" - add_foreign_key "learning_path_topics", "spaces" - add_foreign_key "learning_paths", "content_providers" - add_foreign_key "learning_paths", "spaces" - add_foreign_key "learning_paths", "users" - add_foreign_key "llm_interactions", "events" - add_foreign_key "materials", "content_providers" - add_foreign_key "materials", "spaces" - add_foreign_key "materials", "users" - add_foreign_key "node_links", "nodes" - add_foreign_key "nodes", "users" - add_foreign_key "source_filters", "sources" - add_foreign_key "sources", "content_providers" - add_foreign_key "sources", "spaces" - add_foreign_key "sources", "users" - add_foreign_key "space_roles", "spaces" - add_foreign_key "space_roles", "users" - add_foreign_key "spaces", "users" - add_foreign_key "staff_members", "nodes" - add_foreign_key "stars", "users" - add_foreign_key "subscriptions", "users" - add_foreign_key "users", "roles" - add_foreign_key "workflows", "spaces" - add_foreign_key "workflows", "users" + enable_extension 'plpgsql' + + create_table 'activities', force: :cascade do |t| + t.integer 'trackable_id' + t.string 'trackable_type' + t.integer 'owner_id' + t.string 'owner_type' + t.string 'key' + t.text 'parameters' + t.integer 'recipient_id' + t.string 'recipient_type' + t.datetime 'created_at' + t.datetime 'updated_at' + t.index ['key'], name: 'index_activities_on_key' + t.index %w[owner_id owner_type], name: 'index_activities_on_owner_id_and_owner_type' + t.index %w[recipient_id recipient_type], name: 'index_activities_on_recipient_id_and_recipient_type' + t.index %w[trackable_id trackable_type], name: 'index_activities_on_trackable_id_and_trackable_type' + end + + create_table 'ahoy_events', force: :cascade do |t| + t.bigint 'visit_id' + t.bigint 'user_id' + t.string 'name' + t.jsonb 'properties' + t.datetime 'time' + t.index %w[name time], name: 'index_ahoy_events_on_name_and_time' + t.index ['properties'], name: 'index_ahoy_events_on_properties', opclass: :jsonb_path_ops, using: :gin + t.index ['user_id'], name: 'index_ahoy_events_on_user_id' + t.index ['visit_id'], name: 'index_ahoy_events_on_visit_id' + end + + create_table 'ahoy_visits', force: :cascade do |t| + t.string 'visit_token' + t.string 'visitor_token' + t.bigint 'user_id' + t.string 'ip' + t.text 'user_agent' + t.text 'referrer' + t.string 'referring_domain' + t.text 'landing_page' + t.string 'browser' + t.string 'os' + t.string 'device_type' + t.string 'country' + t.string 'region' + t.string 'city' + t.float 'latitude' + t.float 'longitude' + t.string 'utm_source' + t.string 'utm_medium' + t.string 'utm_term' + t.string 'utm_content' + t.string 'utm_campaign' + t.string 'app_version' + t.string 'os_version' + t.string 'platform' + t.datetime 'started_at' + t.index ['user_id'], name: 'index_ahoy_visits_on_user_id' + t.index ['visit_token'], name: 'index_ahoy_visits_on_visit_token', unique: true + end + + create_table 'autocomplete_suggestions', force: :cascade do |t| + t.string 'field' + t.string 'value' + t.index %w[field value], name: 'index_autocomplete_suggestions_on_field_and_value', unique: true + end + + create_table 'bans', force: :cascade do |t| + t.integer 'user_id' + t.integer 'banner_id' + t.boolean 'shadow' + t.text 'reason' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['banner_id'], name: 'index_bans_on_banner_id' + t.index ['user_id'], name: 'index_bans_on_user_id' + end + + create_table 'collaborations', force: :cascade do |t| + t.integer 'user_id' + t.integer 'resource_id' + t.string 'resource_type' + t.index %w[resource_type resource_id], name: 'index_collaborations_on_resource_type_and_resource_id' + t.index ['user_id'], name: 'index_collaborations_on_user_id' + end + + create_table 'collection_items', force: :cascade do |t| + t.bigint 'collection_id' + t.string 'resource_type' + t.bigint 'resource_id' + t.text 'comment' + t.integer 'order' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['collection_id'], name: 'index_collection_items_on_collection_id' + t.index %w[resource_type resource_id], name: 'index_collection_items_on_resource' + end + + create_table 'collections', force: :cascade do |t| + t.string 'title' + t.text 'description' + t.text 'image_url' + t.boolean 'public', default: true + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'user_id' + t.string 'slug' + t.string 'keywords', default: [], array: true + t.string 'image_file_name' + t.string 'image_content_type' + t.bigint 'image_file_size' + t.datetime 'image_updated_at' + t.bigint 'space_id' + t.index ['slug'], name: 'index_collections_on_slug', unique: true + t.index ['space_id'], name: 'index_collections_on_space_id' + t.index ['user_id'], name: 'index_collections_on_user_id' + end + + create_table 'content_providers', force: :cascade do |t| + t.text 'title' + t.text 'url' + t.text 'image_url' + t.text 'description' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'slug' + t.string 'keywords', default: [], array: true + t.integer 'user_id' + t.integer 'node_id' + t.string 'content_provider_type', default: 'Organisation' + t.string 'image_file_name' + t.string 'image_content_type' + t.bigint 'image_file_size' + t.datetime 'image_updated_at' + t.string 'contact' + t.string 'content_curation_email' + t.index ['node_id'], name: 'index_content_providers_on_node_id' + t.index ['slug'], name: 'index_content_providers_on_slug', unique: true + t.index ['user_id'], name: 'index_content_providers_on_user_id' + end + + create_table 'content_providers_users', id: false, force: :cascade do |t| + t.bigint 'content_provider_id' + t.bigint 'user_id' + t.index %w[content_provider_id user_id], name: 'provider_user_unique', unique: true + t.index ['content_provider_id'], name: 'index_content_providers_users_on_content_provider_id' + t.index ['user_id'], name: 'index_content_providers_users_on_user_id' + end + + create_table 'edit_suggestions', force: :cascade do |t| + t.text 'name' + t.text 'text' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'suggestible_id' + t.string 'suggestible_type' + t.json 'data_fields', default: {} + t.index %w[suggestible_id suggestible_type], name: 'index_edit_suggestions_on_suggestible_id_and_suggestible_type' + end + + create_table 'event_materials', force: :cascade do |t| + t.integer 'event_id' + t.integer 'material_id' + t.index ['event_id'], name: 'index_event_materials_on_event_id' + t.index ['material_id'], name: 'index_event_materials_on_material_id' + end + + create_table 'events', force: :cascade do |t| + t.string 'external_id' + t.string 'title' + t.string 'subtitle' + t.string 'url' + t.string 'organizer' + t.text 'description' + t.datetime 'start' + t.datetime 'end' + t.string 'sponsors', default: [], array: true + t.text 'venue' + t.string 'city' + t.string 'county' + t.string 'country' + t.string 'postcode' + t.decimal 'latitude', precision: 10, scale: 6 + t.decimal 'longitude', precision: 10, scale: 6 + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'source', default: 'tess' + t.string 'slug' + t.integer 'content_provider_id' + t.integer 'user_id' + t.integer 'presence', default: 0 + t.decimal 'cost_value' + t.date 'last_scraped' + t.boolean 'scraper_record', default: false + t.string 'keywords', default: [], array: true + t.string 'event_types', default: [], array: true + t.string 'target_audience', default: [], array: true + t.integer 'capacity' + t.string 'eligibility', default: [], array: true + t.text 'contact' + t.string 'host_institutions', default: [], array: true + t.string 'timezone' + t.string 'funding' + t.integer 'attendee_count' + t.integer 'applicant_count' + t.integer 'trainer_count' + t.string 'feedback' + t.text 'notes' + t.integer 'nominatim_count', default: 0 + t.string 'duration' + t.text 'recognition' + t.text 'learning_objectives' + t.text 'prerequisites' + t.text 'tech_requirements' + t.string 'cost_basis' + t.string 'cost_currency' + t.string 'fields', default: [], array: true + t.boolean 'visible', default: true + t.string 'language' + t.string 'open_science', default: [], array: true + t.bigint 'space_id' + t.index ['presence'], name: 'index_events_on_presence' + t.index ['slug'], name: 'index_events_on_slug', unique: true + t.index ['space_id'], name: 'index_events_on_space_id' + t.index ['user_id'], name: 'index_events_on_user_id' + end + + create_table 'external_resources', force: :cascade do |t| + t.integer 'source_id' + t.text 'url' + t.string 'title' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'source_type' + t.index %w[source_id source_type], name: 'index_external_resources_on_source_id_and_source_type' + end + + create_table 'field_locks', force: :cascade do |t| + t.integer 'resource_id' + t.string 'resource_type' + t.string 'field' + t.index %w[resource_type resource_id], name: 'index_field_locks_on_resource_type_and_resource_id' + end + + create_table 'friendly_id_slugs', force: :cascade do |t| + t.string 'slug', null: false + t.integer 'sluggable_id', null: false + t.string 'sluggable_type', limit: 50 + t.string 'scope' + t.datetime 'created_at' + t.index %w[slug sluggable_type scope], name: 'index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope', unique: true + t.index %w[slug sluggable_type], name: 'index_friendly_id_slugs_on_slug_and_sluggable_type' + t.index ['sluggable_id'], name: 'index_friendly_id_slugs_on_sluggable_id' + t.index ['sluggable_type'], name: 'index_friendly_id_slugs_on_sluggable_type' + end + + create_table 'learning_path_topic_items', force: :cascade do |t| + t.bigint 'topic_id' + t.string 'resource_type' + t.bigint 'resource_id' + t.text 'comment' + t.integer 'order' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index %w[resource_type resource_id], name: 'index_learning_path_topic_items_on_resource' + t.index ['topic_id'], name: 'index_learning_path_topic_items_on_topic_id' + end + + create_table 'learning_path_topic_links', force: :cascade do |t| + t.bigint 'learning_path_id' + t.bigint 'topic_id' + t.integer 'order' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['learning_path_id'], name: 'index_learning_path_topic_links_on_learning_path_id' + t.index ['topic_id'], name: 'index_learning_path_topic_links_on_topic_id' + end + + create_table 'learning_path_topics', force: :cascade do |t| + t.string 'title' + t.text 'description' + t.integer 'user_id' + t.string 'keywords', default: [], array: true + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'difficulty_level', default: 'notspecified' + t.bigint 'space_id' + t.index ['space_id'], name: 'index_learning_path_topics_on_space_id' + end + + create_table 'learning_paths', force: :cascade do |t| + t.text 'title' + t.text 'description' + t.string 'doi' + t.string 'target_audience', default: [], array: true + t.string 'authors', default: [], array: true + t.string 'contributors', default: [], array: true + t.string 'licence', default: 'notspecified' + t.string 'difficulty_level', default: 'notspecified' + t.string 'slug' + t.bigint 'user_id' + t.bigint 'content_provider_id' + t.string 'keywords', default: [], array: true + t.text 'prerequisites' + t.text 'learning_objectives' + t.string 'status' + t.string 'learning_path_type' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.boolean 'public', default: true + t.bigint 'space_id' + t.index ['content_provider_id'], name: 'index_learning_paths_on_content_provider_id' + t.index ['slug'], name: 'index_learning_paths_on_slug', unique: true + t.index ['space_id'], name: 'index_learning_paths_on_space_id' + t.index ['user_id'], name: 'index_learning_paths_on_user_id' + end + + create_table 'link_monitors', force: :cascade do |t| + t.string 'url' + t.integer 'code' + t.datetime 'failed_at' + t.datetime 'last_failed_at' + t.integer 'fail_count' + t.integer 'lcheck_id' + t.string 'lcheck_type' + t.index %w[lcheck_type lcheck_id], name: 'index_link_monitors_on_lcheck_type_and_lcheck_id' + end + + create_table 'llm_interactions', force: :cascade do |t| + t.bigint 'event_id' + t.datetime 'created_at' + t.datetime 'updated_at' + t.string 'scrape_or_process' + t.string 'model' + t.string 'prompt' + t.string 'input' + t.string 'output' + t.boolean 'needs_processing', default: false + t.index ['event_id'], name: 'index_llm_interactions_on_event_id' + end + + create_table 'materials', force: :cascade do |t| + t.text 'title' + t.string 'url' + t.string 'doi' + t.date 'remote_updated_date' + t.date 'remote_created_date' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'description' + t.string 'target_audience', default: [], array: true + t.string 'authors', default: [], array: true + t.string 'contributors', default: [], array: true + t.string 'licence', default: 'notspecified' + t.string 'difficulty_level', default: 'notspecified' + t.integer 'content_provider_id' + t.string 'slug' + t.integer 'user_id' + t.date 'last_scraped' + t.boolean 'scraper_record', default: false + t.string 'resource_type', default: [], array: true + t.string 'keywords', default: [], array: true + t.string 'other_types' + t.date 'date_created' + t.date 'date_modified' + t.date 'date_published' + t.text 'prerequisites' + t.string 'version' + t.string 'status' + t.text 'syllabus' + t.string 'subsets', default: [], array: true + t.text 'contact' + t.text 'learning_objectives' + t.string 'fields', default: [], array: true + t.boolean 'visible', default: true + t.bigint 'space_id' + t.index ['content_provider_id'], name: 'index_materials_on_content_provider_id' + t.index ['slug'], name: 'index_materials_on_slug', unique: true + t.index ['space_id'], name: 'index_materials_on_space_id' + t.index ['user_id'], name: 'index_materials_on_user_id' + end + + create_table 'node_links', force: :cascade do |t| + t.integer 'node_id' + t.integer 'resource_id' + t.string 'resource_type' + t.index ['node_id'], name: 'index_node_links_on_node_id' + t.index %w[resource_type resource_id], name: 'index_node_links_on_resource_type_and_resource_id' + end + + create_table 'nodes', force: :cascade do |t| + t.string 'name' + t.string 'member_status' + t.string 'country_code' + t.string 'home_page' + t.string 'twitter' + t.string 'carousel_images', array: true + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'slug' + t.integer 'user_id' + t.text 'image_url' + t.text 'description' + t.index ['slug'], name: 'index_nodes_on_slug', unique: true + t.index ['user_id'], name: 'index_nodes_on_user_id' + end + + create_table 'ontology_term_links', force: :cascade do |t| + t.integer 'resource_id' + t.string 'resource_type' + t.string 'term_uri' + t.string 'field' + t.index ['field'], name: 'index_ontology_term_links_on_field' + t.index %w[resource_type resource_id], name: 'index_ontology_term_links_on_resource_type_and_resource_id' + t.index ['term_uri'], name: 'index_ontology_term_links_on_term_uri' + end + + create_table 'profiles', force: :cascade do |t| + t.text 'firstname' + t.text 'surname' + t.text 'image_url' + t.text 'email' + t.text 'website' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'user_id' + t.string 'slug' + t.boolean 'public', default: false + t.text 'description' + t.text 'location' + t.string 'orcid' + t.string 'experience' + t.string 'expertise_academic', default: [], array: true + t.string 'expertise_technical', default: [], array: true + t.string 'interest', default: [], array: true + t.string 'activity', default: [], array: true + t.string 'language', default: [], array: true + t.string 'social_media', default: [], array: true + t.string 'type', default: 'Profile' + t.string 'fields', default: [], array: true + t.boolean 'orcid_authenticated', default: false + t.index ['orcid'], name: 'index_profiles_on_orcid' + t.index ['slug'], name: 'index_profiles_on_slug', unique: true + end + + create_table 'roles', force: :cascade do |t| + t.string 'name' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'title' + end + + create_table 'sources', force: :cascade do |t| + t.bigint 'content_provider_id' + t.bigint 'user_id' + t.datetime 'created_at' + t.datetime 'finished_at' + t.string 'url' + t.string 'method' + t.integer 'records_read' + t.integer 'records_written' + t.integer 'resources_added' + t.integer 'resources_updated' + t.integer 'resources_rejected' + t.text 'log' + t.boolean 'enabled' + t.string 'token' + t.integer 'approval_status' + t.datetime 'updated_at' + t.string 'default_language' + t.bigint 'space_id' + t.index ['content_provider_id'], name: 'index_sources_on_content_provider_id' + t.index ['space_id'], name: 'index_sources_on_space_id' + t.index ['user_id'], name: 'index_sources_on_user_id' + end + + create_table 'space_roles', force: :cascade do |t| + t.string 'key' + t.bigint 'user_id' + t.bigint 'space_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['space_id'], name: 'index_space_roles_on_space_id' + t.index ['user_id'], name: 'index_space_roles_on_user_id' + end + + create_table 'spaces', force: :cascade do |t| + t.string 'title' + t.text 'description' + t.string 'host' + t.string 'theme' + t.string 'image_file_name' + t.string 'image_content_type' + t.bigint 'image_file_size' + t.datetime 'image_updated_at' + t.bigint 'user_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'image_url' + t.string 'disabled_features', default: [], array: true + t.index ['host'], name: 'index_spaces_on_host', unique: true + t.index ['user_id'], name: 'index_spaces_on_user_id' + end + + create_table 'staff_members', force: :cascade do |t| + t.string 'name' + t.string 'role' + t.string 'email' + t.text 'image_url' + t.integer 'node_id' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'image_file_name' + t.string 'image_content_type' + t.bigint 'image_file_size' + t.datetime 'image_updated_at' + t.index ['node_id'], name: 'index_staff_members_on_node_id' + end + + create_table 'stars', force: :cascade do |t| + t.integer 'user_id' + t.integer 'resource_id' + t.string 'resource_type' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index %w[resource_type resource_id], name: 'index_stars_on_resource_type_and_resource_id' + t.index ['user_id'], name: 'index_stars_on_user_id' + end + + create_table 'subscriptions', force: :cascade do |t| + t.integer 'user_id' + t.datetime 'last_sent_at' + t.text 'query' + t.json 'facets' + t.integer 'frequency' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'subscribable_type' + t.datetime 'last_checked_at' + t.index ['user_id'], name: 'index_subscriptions_on_user_id' + end + + create_table 'users', force: :cascade do |t| + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'username' + t.integer 'role_id' + t.string 'authentication_token' + t.string 'email', default: '', null: false + t.string 'encrypted_password', default: '', null: false + t.string 'reset_password_token' + t.datetime 'reset_password_sent_at' + t.datetime 'remember_created_at' + t.integer 'sign_in_count', default: 0, null: false + t.datetime 'current_sign_in_at' + t.datetime 'last_sign_in_at' + t.inet 'current_sign_in_ip' + t.inet 'last_sign_in_ip' + t.string 'confirmation_token' + t.datetime 'confirmed_at' + t.datetime 'confirmation_sent_at' + t.string 'unconfirmed_email' + t.integer 'failed_attempts', default: 0, null: false + t.string 'unlock_token' + t.datetime 'locked_at' + t.string 'slug' + t.string 'provider' + t.string 'uid' + t.string 'identity_url' + t.string 'invitation_token' + t.datetime 'invitation_created_at' + t.datetime 'invitation_sent_at' + t.datetime 'invitation_accepted_at' + t.integer 'invitation_limit' + t.string 'invited_by_type' + t.bigint 'invited_by_id' + t.integer 'invitations_count', default: 0 + t.text 'image_url' + t.string 'image_file_name' + t.string 'image_content_type' + t.bigint 'image_file_size' + t.datetime 'image_updated_at' + t.boolean 'check_broken_scrapers', default: false + t.index ['authentication_token'], name: 'index_users_on_authentication_token' + t.index ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true + t.index ['email'], name: 'index_users_on_email', unique: true + t.index ['identity_url'], name: 'index_users_on_identity_url', unique: true + t.index ['invitation_token'], name: 'index_users_on_invitation_token', unique: true + t.index ['invited_by_id'], name: 'index_users_on_invited_by_id' + t.index %w[invited_by_type invited_by_id], name: 'index_users_on_invited_by' + t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true + t.index ['role_id'], name: 'index_users_on_role_id' + t.index ['slug'], name: 'index_users_on_slug', unique: true + t.index ['unlock_token'], name: 'index_users_on_unlock_token', unique: true + t.index ['username'], name: 'index_users_on_username', unique: true + end + + create_table 'widget_logs', force: :cascade do |t| + t.string 'widget_name' + t.string 'action' + t.integer 'resource_id' + t.string 'resource_type' + t.text 'data' + t.json 'params' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.text 'referrer' + t.index %w[resource_type resource_id], name: 'index_widget_logs_on_resource_type_and_resource_id' + end + + create_table 'workflows', force: :cascade do |t| + t.string 'title' + t.string 'description' + t.integer 'user_id' + t.json 'workflow_content' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.string 'slug' + t.string 'target_audience', default: [], array: true + t.string 'keywords', default: [], array: true + t.string 'authors', default: [], array: true + t.string 'contributors', default: [], array: true + t.string 'licence', default: 'notspecified' + t.string 'difficulty_level', default: 'notspecified' + t.string 'doi' + t.date 'remote_created_date' + t.date 'remote_updated_date' + t.boolean 'hide_child_nodes', default: false + t.boolean 'public', default: true + t.bigint 'space_id' + t.index ['slug'], name: 'index_workflows_on_slug', unique: true + t.index ['space_id'], name: 'index_workflows_on_space_id' + t.index ['user_id'], name: 'index_workflows_on_user_id' + end + + add_foreign_key 'bans', 'users' + add_foreign_key 'bans', 'users', column: 'banner_id' + add_foreign_key 'collaborations', 'users' + add_foreign_key 'collections', 'spaces' + add_foreign_key 'collections', 'users' + add_foreign_key 'content_providers', 'nodes' + add_foreign_key 'content_providers', 'users' + add_foreign_key 'event_materials', 'events' + add_foreign_key 'event_materials', 'materials' + add_foreign_key 'events', 'spaces' + add_foreign_key 'events', 'users' + add_foreign_key 'learning_path_topic_links', 'learning_paths' + add_foreign_key 'learning_path_topics', 'spaces' + add_foreign_key 'learning_paths', 'content_providers' + add_foreign_key 'learning_paths', 'spaces' + add_foreign_key 'learning_paths', 'users' + add_foreign_key 'llm_interactions', 'events' + add_foreign_key 'materials', 'content_providers' + add_foreign_key 'materials', 'spaces' + add_foreign_key 'materials', 'users' + add_foreign_key 'node_links', 'nodes' + add_foreign_key 'nodes', 'users' + add_foreign_key 'sources', 'content_providers' + add_foreign_key 'sources', 'spaces' + add_foreign_key 'sources', 'users' + add_foreign_key 'space_roles', 'spaces' + add_foreign_key 'space_roles', 'users' + add_foreign_key 'spaces', 'users' + add_foreign_key 'staff_members', 'nodes' + add_foreign_key 'stars', 'users' + add_foreign_key 'subscriptions', 'users' + add_foreign_key 'users', 'roles' + add_foreign_key 'workflows', 'spaces' + add_foreign_key 'workflows', 'users' end From bc3951f689104e2c4b77f1ea93e07e61278ec999 Mon Sep 17 00:00:00 2001 From: Martin Voigt Date: Thu, 5 Feb 2026 16:33:38 +0100 Subject: [PATCH 12/12] Undo accidental db/schema.rb changes --- db/schema.rb | 1340 +++++++++++++++++++++++++------------------------- 1 file changed, 670 insertions(+), 670 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index eb7e8e069..b3a246afe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,675 +10,675 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 20_251_222_142_740) do +ActiveRecord::Schema[7.2].define(version: 2025_12_22_142740) do # These are extensions that must be enabled in order to support this database - enable_extension 'plpgsql' - - create_table 'activities', force: :cascade do |t| - t.integer 'trackable_id' - t.string 'trackable_type' - t.integer 'owner_id' - t.string 'owner_type' - t.string 'key' - t.text 'parameters' - t.integer 'recipient_id' - t.string 'recipient_type' - t.datetime 'created_at' - t.datetime 'updated_at' - t.index ['key'], name: 'index_activities_on_key' - t.index %w[owner_id owner_type], name: 'index_activities_on_owner_id_and_owner_type' - t.index %w[recipient_id recipient_type], name: 'index_activities_on_recipient_id_and_recipient_type' - t.index %w[trackable_id trackable_type], name: 'index_activities_on_trackable_id_and_trackable_type' - end - - create_table 'ahoy_events', force: :cascade do |t| - t.bigint 'visit_id' - t.bigint 'user_id' - t.string 'name' - t.jsonb 'properties' - t.datetime 'time' - t.index %w[name time], name: 'index_ahoy_events_on_name_and_time' - t.index ['properties'], name: 'index_ahoy_events_on_properties', opclass: :jsonb_path_ops, using: :gin - t.index ['user_id'], name: 'index_ahoy_events_on_user_id' - t.index ['visit_id'], name: 'index_ahoy_events_on_visit_id' - end - - create_table 'ahoy_visits', force: :cascade do |t| - t.string 'visit_token' - t.string 'visitor_token' - t.bigint 'user_id' - t.string 'ip' - t.text 'user_agent' - t.text 'referrer' - t.string 'referring_domain' - t.text 'landing_page' - t.string 'browser' - t.string 'os' - t.string 'device_type' - t.string 'country' - t.string 'region' - t.string 'city' - t.float 'latitude' - t.float 'longitude' - t.string 'utm_source' - t.string 'utm_medium' - t.string 'utm_term' - t.string 'utm_content' - t.string 'utm_campaign' - t.string 'app_version' - t.string 'os_version' - t.string 'platform' - t.datetime 'started_at' - t.index ['user_id'], name: 'index_ahoy_visits_on_user_id' - t.index ['visit_token'], name: 'index_ahoy_visits_on_visit_token', unique: true - end - - create_table 'autocomplete_suggestions', force: :cascade do |t| - t.string 'field' - t.string 'value' - t.index %w[field value], name: 'index_autocomplete_suggestions_on_field_and_value', unique: true - end - - create_table 'bans', force: :cascade do |t| - t.integer 'user_id' - t.integer 'banner_id' - t.boolean 'shadow' - t.text 'reason' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['banner_id'], name: 'index_bans_on_banner_id' - t.index ['user_id'], name: 'index_bans_on_user_id' - end - - create_table 'collaborations', force: :cascade do |t| - t.integer 'user_id' - t.integer 'resource_id' - t.string 'resource_type' - t.index %w[resource_type resource_id], name: 'index_collaborations_on_resource_type_and_resource_id' - t.index ['user_id'], name: 'index_collaborations_on_user_id' - end - - create_table 'collection_items', force: :cascade do |t| - t.bigint 'collection_id' - t.string 'resource_type' - t.bigint 'resource_id' - t.text 'comment' - t.integer 'order' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['collection_id'], name: 'index_collection_items_on_collection_id' - t.index %w[resource_type resource_id], name: 'index_collection_items_on_resource' - end - - create_table 'collections', force: :cascade do |t| - t.string 'title' - t.text 'description' - t.text 'image_url' - t.boolean 'public', default: true - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'user_id' - t.string 'slug' - t.string 'keywords', default: [], array: true - t.string 'image_file_name' - t.string 'image_content_type' - t.bigint 'image_file_size' - t.datetime 'image_updated_at' - t.bigint 'space_id' - t.index ['slug'], name: 'index_collections_on_slug', unique: true - t.index ['space_id'], name: 'index_collections_on_space_id' - t.index ['user_id'], name: 'index_collections_on_user_id' - end - - create_table 'content_providers', force: :cascade do |t| - t.text 'title' - t.text 'url' - t.text 'image_url' - t.text 'description' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'slug' - t.string 'keywords', default: [], array: true - t.integer 'user_id' - t.integer 'node_id' - t.string 'content_provider_type', default: 'Organisation' - t.string 'image_file_name' - t.string 'image_content_type' - t.bigint 'image_file_size' - t.datetime 'image_updated_at' - t.string 'contact' - t.string 'content_curation_email' - t.index ['node_id'], name: 'index_content_providers_on_node_id' - t.index ['slug'], name: 'index_content_providers_on_slug', unique: true - t.index ['user_id'], name: 'index_content_providers_on_user_id' - end - - create_table 'content_providers_users', id: false, force: :cascade do |t| - t.bigint 'content_provider_id' - t.bigint 'user_id' - t.index %w[content_provider_id user_id], name: 'provider_user_unique', unique: true - t.index ['content_provider_id'], name: 'index_content_providers_users_on_content_provider_id' - t.index ['user_id'], name: 'index_content_providers_users_on_user_id' - end - - create_table 'edit_suggestions', force: :cascade do |t| - t.text 'name' - t.text 'text' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'suggestible_id' - t.string 'suggestible_type' - t.json 'data_fields', default: {} - t.index %w[suggestible_id suggestible_type], name: 'index_edit_suggestions_on_suggestible_id_and_suggestible_type' - end - - create_table 'event_materials', force: :cascade do |t| - t.integer 'event_id' - t.integer 'material_id' - t.index ['event_id'], name: 'index_event_materials_on_event_id' - t.index ['material_id'], name: 'index_event_materials_on_material_id' - end - - create_table 'events', force: :cascade do |t| - t.string 'external_id' - t.string 'title' - t.string 'subtitle' - t.string 'url' - t.string 'organizer' - t.text 'description' - t.datetime 'start' - t.datetime 'end' - t.string 'sponsors', default: [], array: true - t.text 'venue' - t.string 'city' - t.string 'county' - t.string 'country' - t.string 'postcode' - t.decimal 'latitude', precision: 10, scale: 6 - t.decimal 'longitude', precision: 10, scale: 6 - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.text 'source', default: 'tess' - t.string 'slug' - t.integer 'content_provider_id' - t.integer 'user_id' - t.integer 'presence', default: 0 - t.decimal 'cost_value' - t.date 'last_scraped' - t.boolean 'scraper_record', default: false - t.string 'keywords', default: [], array: true - t.string 'event_types', default: [], array: true - t.string 'target_audience', default: [], array: true - t.integer 'capacity' - t.string 'eligibility', default: [], array: true - t.text 'contact' - t.string 'host_institutions', default: [], array: true - t.string 'timezone' - t.string 'funding' - t.integer 'attendee_count' - t.integer 'applicant_count' - t.integer 'trainer_count' - t.string 'feedback' - t.text 'notes' - t.integer 'nominatim_count', default: 0 - t.string 'duration' - t.text 'recognition' - t.text 'learning_objectives' - t.text 'prerequisites' - t.text 'tech_requirements' - t.string 'cost_basis' - t.string 'cost_currency' - t.string 'fields', default: [], array: true - t.boolean 'visible', default: true - t.string 'language' - t.string 'open_science', default: [], array: true - t.bigint 'space_id' - t.index ['presence'], name: 'index_events_on_presence' - t.index ['slug'], name: 'index_events_on_slug', unique: true - t.index ['space_id'], name: 'index_events_on_space_id' - t.index ['user_id'], name: 'index_events_on_user_id' - end - - create_table 'external_resources', force: :cascade do |t| - t.integer 'source_id' - t.text 'url' - t.string 'title' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'source_type' - t.index %w[source_id source_type], name: 'index_external_resources_on_source_id_and_source_type' - end - - create_table 'field_locks', force: :cascade do |t| - t.integer 'resource_id' - t.string 'resource_type' - t.string 'field' - t.index %w[resource_type resource_id], name: 'index_field_locks_on_resource_type_and_resource_id' - end - - create_table 'friendly_id_slugs', force: :cascade do |t| - t.string 'slug', null: false - t.integer 'sluggable_id', null: false - t.string 'sluggable_type', limit: 50 - t.string 'scope' - t.datetime 'created_at' - t.index %w[slug sluggable_type scope], name: 'index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope', unique: true - t.index %w[slug sluggable_type], name: 'index_friendly_id_slugs_on_slug_and_sluggable_type' - t.index ['sluggable_id'], name: 'index_friendly_id_slugs_on_sluggable_id' - t.index ['sluggable_type'], name: 'index_friendly_id_slugs_on_sluggable_type' - end - - create_table 'learning_path_topic_items', force: :cascade do |t| - t.bigint 'topic_id' - t.string 'resource_type' - t.bigint 'resource_id' - t.text 'comment' - t.integer 'order' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index %w[resource_type resource_id], name: 'index_learning_path_topic_items_on_resource' - t.index ['topic_id'], name: 'index_learning_path_topic_items_on_topic_id' - end - - create_table 'learning_path_topic_links', force: :cascade do |t| - t.bigint 'learning_path_id' - t.bigint 'topic_id' - t.integer 'order' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['learning_path_id'], name: 'index_learning_path_topic_links_on_learning_path_id' - t.index ['topic_id'], name: 'index_learning_path_topic_links_on_topic_id' - end - - create_table 'learning_path_topics', force: :cascade do |t| - t.string 'title' - t.text 'description' - t.integer 'user_id' - t.string 'keywords', default: [], array: true - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'difficulty_level', default: 'notspecified' - t.bigint 'space_id' - t.index ['space_id'], name: 'index_learning_path_topics_on_space_id' - end - - create_table 'learning_paths', force: :cascade do |t| - t.text 'title' - t.text 'description' - t.string 'doi' - t.string 'target_audience', default: [], array: true - t.string 'authors', default: [], array: true - t.string 'contributors', default: [], array: true - t.string 'licence', default: 'notspecified' - t.string 'difficulty_level', default: 'notspecified' - t.string 'slug' - t.bigint 'user_id' - t.bigint 'content_provider_id' - t.string 'keywords', default: [], array: true - t.text 'prerequisites' - t.text 'learning_objectives' - t.string 'status' - t.string 'learning_path_type' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.boolean 'public', default: true - t.bigint 'space_id' - t.index ['content_provider_id'], name: 'index_learning_paths_on_content_provider_id' - t.index ['slug'], name: 'index_learning_paths_on_slug', unique: true - t.index ['space_id'], name: 'index_learning_paths_on_space_id' - t.index ['user_id'], name: 'index_learning_paths_on_user_id' - end - - create_table 'link_monitors', force: :cascade do |t| - t.string 'url' - t.integer 'code' - t.datetime 'failed_at' - t.datetime 'last_failed_at' - t.integer 'fail_count' - t.integer 'lcheck_id' - t.string 'lcheck_type' - t.index %w[lcheck_type lcheck_id], name: 'index_link_monitors_on_lcheck_type_and_lcheck_id' - end - - create_table 'llm_interactions', force: :cascade do |t| - t.bigint 'event_id' - t.datetime 'created_at' - t.datetime 'updated_at' - t.string 'scrape_or_process' - t.string 'model' - t.string 'prompt' - t.string 'input' - t.string 'output' - t.boolean 'needs_processing', default: false - t.index ['event_id'], name: 'index_llm_interactions_on_event_id' - end - - create_table 'materials', force: :cascade do |t| - t.text 'title' - t.string 'url' - t.string 'doi' - t.date 'remote_updated_date' - t.date 'remote_created_date' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.text 'description' - t.string 'target_audience', default: [], array: true - t.string 'authors', default: [], array: true - t.string 'contributors', default: [], array: true - t.string 'licence', default: 'notspecified' - t.string 'difficulty_level', default: 'notspecified' - t.integer 'content_provider_id' - t.string 'slug' - t.integer 'user_id' - t.date 'last_scraped' - t.boolean 'scraper_record', default: false - t.string 'resource_type', default: [], array: true - t.string 'keywords', default: [], array: true - t.string 'other_types' - t.date 'date_created' - t.date 'date_modified' - t.date 'date_published' - t.text 'prerequisites' - t.string 'version' - t.string 'status' - t.text 'syllabus' - t.string 'subsets', default: [], array: true - t.text 'contact' - t.text 'learning_objectives' - t.string 'fields', default: [], array: true - t.boolean 'visible', default: true - t.bigint 'space_id' - t.index ['content_provider_id'], name: 'index_materials_on_content_provider_id' - t.index ['slug'], name: 'index_materials_on_slug', unique: true - t.index ['space_id'], name: 'index_materials_on_space_id' - t.index ['user_id'], name: 'index_materials_on_user_id' - end - - create_table 'node_links', force: :cascade do |t| - t.integer 'node_id' - t.integer 'resource_id' - t.string 'resource_type' - t.index ['node_id'], name: 'index_node_links_on_node_id' - t.index %w[resource_type resource_id], name: 'index_node_links_on_resource_type_and_resource_id' - end - - create_table 'nodes', force: :cascade do |t| - t.string 'name' - t.string 'member_status' - t.string 'country_code' - t.string 'home_page' - t.string 'twitter' - t.string 'carousel_images', array: true - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'slug' - t.integer 'user_id' - t.text 'image_url' - t.text 'description' - t.index ['slug'], name: 'index_nodes_on_slug', unique: true - t.index ['user_id'], name: 'index_nodes_on_user_id' - end - - create_table 'ontology_term_links', force: :cascade do |t| - t.integer 'resource_id' - t.string 'resource_type' - t.string 'term_uri' - t.string 'field' - t.index ['field'], name: 'index_ontology_term_links_on_field' - t.index %w[resource_type resource_id], name: 'index_ontology_term_links_on_resource_type_and_resource_id' - t.index ['term_uri'], name: 'index_ontology_term_links_on_term_uri' - end - - create_table 'profiles', force: :cascade do |t| - t.text 'firstname' - t.text 'surname' - t.text 'image_url' - t.text 'email' - t.text 'website' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'user_id' - t.string 'slug' - t.boolean 'public', default: false - t.text 'description' - t.text 'location' - t.string 'orcid' - t.string 'experience' - t.string 'expertise_academic', default: [], array: true - t.string 'expertise_technical', default: [], array: true - t.string 'interest', default: [], array: true - t.string 'activity', default: [], array: true - t.string 'language', default: [], array: true - t.string 'social_media', default: [], array: true - t.string 'type', default: 'Profile' - t.string 'fields', default: [], array: true - t.boolean 'orcid_authenticated', default: false - t.index ['orcid'], name: 'index_profiles_on_orcid' - t.index ['slug'], name: 'index_profiles_on_slug', unique: true - end - - create_table 'roles', force: :cascade do |t| - t.string 'name' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'title' - end - - create_table 'sources', force: :cascade do |t| - t.bigint 'content_provider_id' - t.bigint 'user_id' - t.datetime 'created_at' - t.datetime 'finished_at' - t.string 'url' - t.string 'method' - t.integer 'records_read' - t.integer 'records_written' - t.integer 'resources_added' - t.integer 'resources_updated' - t.integer 'resources_rejected' - t.text 'log' - t.boolean 'enabled' - t.string 'token' - t.integer 'approval_status' - t.datetime 'updated_at' - t.string 'default_language' - t.bigint 'space_id' - t.index ['content_provider_id'], name: 'index_sources_on_content_provider_id' - t.index ['space_id'], name: 'index_sources_on_space_id' - t.index ['user_id'], name: 'index_sources_on_user_id' - end - - create_table 'space_roles', force: :cascade do |t| - t.string 'key' - t.bigint 'user_id' - t.bigint 'space_id' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['space_id'], name: 'index_space_roles_on_space_id' - t.index ['user_id'], name: 'index_space_roles_on_user_id' - end - - create_table 'spaces', force: :cascade do |t| - t.string 'title' - t.text 'description' - t.string 'host' - t.string 'theme' - t.string 'image_file_name' - t.string 'image_content_type' - t.bigint 'image_file_size' - t.datetime 'image_updated_at' - t.bigint 'user_id' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.text 'image_url' - t.string 'disabled_features', default: [], array: true - t.index ['host'], name: 'index_spaces_on_host', unique: true - t.index ['user_id'], name: 'index_spaces_on_user_id' - end - - create_table 'staff_members', force: :cascade do |t| - t.string 'name' - t.string 'role' - t.string 'email' - t.text 'image_url' - t.integer 'node_id' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'image_file_name' - t.string 'image_content_type' - t.bigint 'image_file_size' - t.datetime 'image_updated_at' - t.index ['node_id'], name: 'index_staff_members_on_node_id' - end - - create_table 'stars', force: :cascade do |t| - t.integer 'user_id' - t.integer 'resource_id' - t.string 'resource_type' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index %w[resource_type resource_id], name: 'index_stars_on_resource_type_and_resource_id' - t.index ['user_id'], name: 'index_stars_on_user_id' - end - - create_table 'subscriptions', force: :cascade do |t| - t.integer 'user_id' - t.datetime 'last_sent_at' - t.text 'query' - t.json 'facets' - t.integer 'frequency' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'subscribable_type' - t.datetime 'last_checked_at' - t.index ['user_id'], name: 'index_subscriptions_on_user_id' - end - - create_table 'users', force: :cascade do |t| - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'username' - t.integer 'role_id' - t.string 'authentication_token' - t.string 'email', default: '', null: false - t.string 'encrypted_password', default: '', null: false - t.string 'reset_password_token' - t.datetime 'reset_password_sent_at' - t.datetime 'remember_created_at' - t.integer 'sign_in_count', default: 0, null: false - t.datetime 'current_sign_in_at' - t.datetime 'last_sign_in_at' - t.inet 'current_sign_in_ip' - t.inet 'last_sign_in_ip' - t.string 'confirmation_token' - t.datetime 'confirmed_at' - t.datetime 'confirmation_sent_at' - t.string 'unconfirmed_email' - t.integer 'failed_attempts', default: 0, null: false - t.string 'unlock_token' - t.datetime 'locked_at' - t.string 'slug' - t.string 'provider' - t.string 'uid' - t.string 'identity_url' - t.string 'invitation_token' - t.datetime 'invitation_created_at' - t.datetime 'invitation_sent_at' - t.datetime 'invitation_accepted_at' - t.integer 'invitation_limit' - t.string 'invited_by_type' - t.bigint 'invited_by_id' - t.integer 'invitations_count', default: 0 - t.text 'image_url' - t.string 'image_file_name' - t.string 'image_content_type' - t.bigint 'image_file_size' - t.datetime 'image_updated_at' - t.boolean 'check_broken_scrapers', default: false - t.index ['authentication_token'], name: 'index_users_on_authentication_token' - t.index ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true - t.index ['email'], name: 'index_users_on_email', unique: true - t.index ['identity_url'], name: 'index_users_on_identity_url', unique: true - t.index ['invitation_token'], name: 'index_users_on_invitation_token', unique: true - t.index ['invited_by_id'], name: 'index_users_on_invited_by_id' - t.index %w[invited_by_type invited_by_id], name: 'index_users_on_invited_by' - t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true - t.index ['role_id'], name: 'index_users_on_role_id' - t.index ['slug'], name: 'index_users_on_slug', unique: true - t.index ['unlock_token'], name: 'index_users_on_unlock_token', unique: true - t.index ['username'], name: 'index_users_on_username', unique: true - end - - create_table 'widget_logs', force: :cascade do |t| - t.string 'widget_name' - t.string 'action' - t.integer 'resource_id' - t.string 'resource_type' - t.text 'data' - t.json 'params' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.text 'referrer' - t.index %w[resource_type resource_id], name: 'index_widget_logs_on_resource_type_and_resource_id' - end - - create_table 'workflows', force: :cascade do |t| - t.string 'title' - t.string 'description' - t.integer 'user_id' - t.json 'workflow_content' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'slug' - t.string 'target_audience', default: [], array: true - t.string 'keywords', default: [], array: true - t.string 'authors', default: [], array: true - t.string 'contributors', default: [], array: true - t.string 'licence', default: 'notspecified' - t.string 'difficulty_level', default: 'notspecified' - t.string 'doi' - t.date 'remote_created_date' - t.date 'remote_updated_date' - t.boolean 'hide_child_nodes', default: false - t.boolean 'public', default: true - t.bigint 'space_id' - t.index ['slug'], name: 'index_workflows_on_slug', unique: true - t.index ['space_id'], name: 'index_workflows_on_space_id' - t.index ['user_id'], name: 'index_workflows_on_user_id' - end - - add_foreign_key 'bans', 'users' - add_foreign_key 'bans', 'users', column: 'banner_id' - add_foreign_key 'collaborations', 'users' - add_foreign_key 'collections', 'spaces' - add_foreign_key 'collections', 'users' - add_foreign_key 'content_providers', 'nodes' - add_foreign_key 'content_providers', 'users' - add_foreign_key 'event_materials', 'events' - add_foreign_key 'event_materials', 'materials' - add_foreign_key 'events', 'spaces' - add_foreign_key 'events', 'users' - add_foreign_key 'learning_path_topic_links', 'learning_paths' - add_foreign_key 'learning_path_topics', 'spaces' - add_foreign_key 'learning_paths', 'content_providers' - add_foreign_key 'learning_paths', 'spaces' - add_foreign_key 'learning_paths', 'users' - add_foreign_key 'llm_interactions', 'events' - add_foreign_key 'materials', 'content_providers' - add_foreign_key 'materials', 'spaces' - add_foreign_key 'materials', 'users' - add_foreign_key 'node_links', 'nodes' - add_foreign_key 'nodes', 'users' - add_foreign_key 'sources', 'content_providers' - add_foreign_key 'sources', 'spaces' - add_foreign_key 'sources', 'users' - add_foreign_key 'space_roles', 'spaces' - add_foreign_key 'space_roles', 'users' - add_foreign_key 'spaces', 'users' - add_foreign_key 'staff_members', 'nodes' - add_foreign_key 'stars', 'users' - add_foreign_key 'subscriptions', 'users' - add_foreign_key 'users', 'roles' - add_foreign_key 'workflows', 'spaces' - add_foreign_key 'workflows', 'users' + enable_extension "plpgsql" + + create_table "activities", force: :cascade do |t| + t.integer "trackable_id" + t.string "trackable_type" + t.integer "owner_id" + t.string "owner_type" + t.string "key" + t.text "parameters" + t.integer "recipient_id" + t.string "recipient_type" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["key"], name: "index_activities_on_key" + t.index ["owner_id", "owner_type"], name: "index_activities_on_owner_id_and_owner_type" + t.index ["recipient_id", "recipient_type"], name: "index_activities_on_recipient_id_and_recipient_type" + t.index ["trackable_id", "trackable_type"], name: "index_activities_on_trackable_id_and_trackable_type" + end + + create_table "ahoy_events", force: :cascade do |t| + t.bigint "visit_id" + t.bigint "user_id" + t.string "name" + t.jsonb "properties" + t.datetime "time" + t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time" + t.index ["properties"], name: "index_ahoy_events_on_properties", opclass: :jsonb_path_ops, using: :gin + t.index ["user_id"], name: "index_ahoy_events_on_user_id" + t.index ["visit_id"], name: "index_ahoy_events_on_visit_id" + end + + create_table "ahoy_visits", force: :cascade do |t| + t.string "visit_token" + t.string "visitor_token" + t.bigint "user_id" + t.string "ip" + t.text "user_agent" + t.text "referrer" + t.string "referring_domain" + t.text "landing_page" + t.string "browser" + t.string "os" + t.string "device_type" + t.string "country" + t.string "region" + t.string "city" + t.float "latitude" + t.float "longitude" + t.string "utm_source" + t.string "utm_medium" + t.string "utm_term" + t.string "utm_content" + t.string "utm_campaign" + t.string "app_version" + t.string "os_version" + t.string "platform" + t.datetime "started_at" + t.index ["user_id"], name: "index_ahoy_visits_on_user_id" + t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true + end + + create_table "autocomplete_suggestions", force: :cascade do |t| + t.string "field" + t.string "value" + t.index ["field", "value"], name: "index_autocomplete_suggestions_on_field_and_value", unique: true + end + + create_table "bans", force: :cascade do |t| + t.integer "user_id" + t.integer "banner_id" + t.boolean "shadow" + t.text "reason" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["banner_id"], name: "index_bans_on_banner_id" + t.index ["user_id"], name: "index_bans_on_user_id" + end + + create_table "collaborations", force: :cascade do |t| + t.integer "user_id" + t.integer "resource_id" + t.string "resource_type" + t.index ["resource_type", "resource_id"], name: "index_collaborations_on_resource_type_and_resource_id" + t.index ["user_id"], name: "index_collaborations_on_user_id" + end + + create_table "collection_items", force: :cascade do |t| + t.bigint "collection_id" + t.string "resource_type" + t.bigint "resource_id" + t.text "comment" + t.integer "order" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["collection_id"], name: "index_collection_items_on_collection_id" + t.index ["resource_type", "resource_id"], name: "index_collection_items_on_resource" + end + + create_table "collections", force: :cascade do |t| + t.string "title" + t.text "description" + t.text "image_url" + t.boolean "public", default: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "user_id" + t.string "slug" + t.string "keywords", default: [], array: true + t.string "image_file_name" + t.string "image_content_type" + t.bigint "image_file_size" + t.datetime "image_updated_at" + t.bigint "space_id" + t.index ["slug"], name: "index_collections_on_slug", unique: true + t.index ["space_id"], name: "index_collections_on_space_id" + t.index ["user_id"], name: "index_collections_on_user_id" + end + + create_table "content_providers", force: :cascade do |t| + t.text "title" + t.text "url" + t.text "image_url" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "slug" + t.string "keywords", default: [], array: true + t.integer "user_id" + t.integer "node_id" + t.string "content_provider_type", default: "Organisation" + t.string "image_file_name" + t.string "image_content_type" + t.bigint "image_file_size" + t.datetime "image_updated_at" + t.string "contact" + t.string "content_curation_email" + t.index ["node_id"], name: "index_content_providers_on_node_id" + t.index ["slug"], name: "index_content_providers_on_slug", unique: true + t.index ["user_id"], name: "index_content_providers_on_user_id" + end + + create_table "content_providers_users", id: false, force: :cascade do |t| + t.bigint "content_provider_id" + t.bigint "user_id" + t.index ["content_provider_id", "user_id"], name: "provider_user_unique", unique: true + t.index ["content_provider_id"], name: "index_content_providers_users_on_content_provider_id" + t.index ["user_id"], name: "index_content_providers_users_on_user_id" + end + + create_table "edit_suggestions", force: :cascade do |t| + t.text "name" + t.text "text" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "suggestible_id" + t.string "suggestible_type" + t.json "data_fields", default: {} + t.index ["suggestible_id", "suggestible_type"], name: "index_edit_suggestions_on_suggestible_id_and_suggestible_type" + end + + create_table "event_materials", force: :cascade do |t| + t.integer "event_id" + t.integer "material_id" + t.index ["event_id"], name: "index_event_materials_on_event_id" + t.index ["material_id"], name: "index_event_materials_on_material_id" + end + + create_table "events", force: :cascade do |t| + t.string "external_id" + t.string "title" + t.string "subtitle" + t.string "url" + t.string "organizer" + t.text "description" + t.datetime "start" + t.datetime "end" + t.string "sponsors", default: [], array: true + t.text "venue" + t.string "city" + t.string "county" + t.string "country" + t.string "postcode" + t.decimal "latitude", precision: 10, scale: 6 + t.decimal "longitude", precision: 10, scale: 6 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "source", default: "tess" + t.string "slug" + t.integer "content_provider_id" + t.integer "user_id" + t.integer "presence", default: 0 + t.decimal "cost_value" + t.date "last_scraped" + t.boolean "scraper_record", default: false + t.string "keywords", default: [], array: true + t.string "event_types", default: [], array: true + t.string "target_audience", default: [], array: true + t.integer "capacity" + t.string "eligibility", default: [], array: true + t.text "contact" + t.string "host_institutions", default: [], array: true + t.string "timezone" + t.string "funding" + t.integer "attendee_count" + t.integer "applicant_count" + t.integer "trainer_count" + t.string "feedback" + t.text "notes" + t.integer "nominatim_count", default: 0 + t.string "duration" + t.text "recognition" + t.text "learning_objectives" + t.text "prerequisites" + t.text "tech_requirements" + t.string "cost_basis" + t.string "cost_currency" + t.string "fields", default: [], array: true + t.boolean "visible", default: true + t.string "language" + t.string "open_science", default: [], array: true + t.bigint "space_id" + t.index ["presence"], name: "index_events_on_presence" + t.index ["slug"], name: "index_events_on_slug", unique: true + t.index ["space_id"], name: "index_events_on_space_id" + t.index ["user_id"], name: "index_events_on_user_id" + end + + create_table "external_resources", force: :cascade do |t| + t.integer "source_id" + t.text "url" + t.string "title" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "source_type" + t.index ["source_id", "source_type"], name: "index_external_resources_on_source_id_and_source_type" + end + + create_table "field_locks", force: :cascade do |t| + t.integer "resource_id" + t.string "resource_type" + t.string "field" + t.index ["resource_type", "resource_id"], name: "index_field_locks_on_resource_type_and_resource_id" + end + + create_table "friendly_id_slugs", force: :cascade do |t| + t.string "slug", null: false + t.integer "sluggable_id", null: false + t.string "sluggable_type", limit: 50 + t.string "scope" + t.datetime "created_at" + t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true + t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type" + t.index ["sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_id" + t.index ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type" + end + + create_table "learning_path_topic_items", force: :cascade do |t| + t.bigint "topic_id" + t.string "resource_type" + t.bigint "resource_id" + t.text "comment" + t.integer "order" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["resource_type", "resource_id"], name: "index_learning_path_topic_items_on_resource" + t.index ["topic_id"], name: "index_learning_path_topic_items_on_topic_id" + end + + create_table "learning_path_topic_links", force: :cascade do |t| + t.bigint "learning_path_id" + t.bigint "topic_id" + t.integer "order" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["learning_path_id"], name: "index_learning_path_topic_links_on_learning_path_id" + t.index ["topic_id"], name: "index_learning_path_topic_links_on_topic_id" + end + + create_table "learning_path_topics", force: :cascade do |t| + t.string "title" + t.text "description" + t.integer "user_id" + t.string "keywords", default: [], array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "difficulty_level", default: "notspecified" + t.bigint "space_id" + t.index ["space_id"], name: "index_learning_path_topics_on_space_id" + end + + create_table "learning_paths", force: :cascade do |t| + t.text "title" + t.text "description" + t.string "doi" + t.string "target_audience", default: [], array: true + t.string "authors", default: [], array: true + t.string "contributors", default: [], array: true + t.string "licence", default: "notspecified" + t.string "difficulty_level", default: "notspecified" + t.string "slug" + t.bigint "user_id" + t.bigint "content_provider_id" + t.string "keywords", default: [], array: true + t.text "prerequisites" + t.text "learning_objectives" + t.string "status" + t.string "learning_path_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "public", default: true + t.bigint "space_id" + t.index ["content_provider_id"], name: "index_learning_paths_on_content_provider_id" + t.index ["slug"], name: "index_learning_paths_on_slug", unique: true + t.index ["space_id"], name: "index_learning_paths_on_space_id" + t.index ["user_id"], name: "index_learning_paths_on_user_id" + end + + create_table "link_monitors", force: :cascade do |t| + t.string "url" + t.integer "code" + t.datetime "failed_at" + t.datetime "last_failed_at" + t.integer "fail_count" + t.integer "lcheck_id" + t.string "lcheck_type" + t.index ["lcheck_type", "lcheck_id"], name: "index_link_monitors_on_lcheck_type_and_lcheck_id" + end + + create_table "llm_interactions", force: :cascade do |t| + t.bigint "event_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "scrape_or_process" + t.string "model" + t.string "prompt" + t.string "input" + t.string "output" + t.boolean "needs_processing", default: false + t.index ["event_id"], name: "index_llm_interactions_on_event_id" + end + + create_table "materials", force: :cascade do |t| + t.text "title" + t.string "url" + t.string "doi" + t.date "remote_updated_date" + t.date "remote_created_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.string "target_audience", default: [], array: true + t.string "authors", default: [], array: true + t.string "contributors", default: [], array: true + t.string "licence", default: "notspecified" + t.string "difficulty_level", default: "notspecified" + t.integer "content_provider_id" + t.string "slug" + t.integer "user_id" + t.date "last_scraped" + t.boolean "scraper_record", default: false + t.string "resource_type", default: [], array: true + t.string "keywords", default: [], array: true + t.string "other_types" + t.date "date_created" + t.date "date_modified" + t.date "date_published" + t.text "prerequisites" + t.string "version" + t.string "status" + t.text "syllabus" + t.string "subsets", default: [], array: true + t.text "contact" + t.text "learning_objectives" + t.string "fields", default: [], array: true + t.boolean "visible", default: true + t.bigint "space_id" + t.index ["content_provider_id"], name: "index_materials_on_content_provider_id" + t.index ["slug"], name: "index_materials_on_slug", unique: true + t.index ["space_id"], name: "index_materials_on_space_id" + t.index ["user_id"], name: "index_materials_on_user_id" + end + + create_table "node_links", force: :cascade do |t| + t.integer "node_id" + t.integer "resource_id" + t.string "resource_type" + t.index ["node_id"], name: "index_node_links_on_node_id" + t.index ["resource_type", "resource_id"], name: "index_node_links_on_resource_type_and_resource_id" + end + + create_table "nodes", force: :cascade do |t| + t.string "name" + t.string "member_status" + t.string "country_code" + t.string "home_page" + t.string "twitter" + t.string "carousel_images", array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "slug" + t.integer "user_id" + t.text "image_url" + t.text "description" + t.index ["slug"], name: "index_nodes_on_slug", unique: true + t.index ["user_id"], name: "index_nodes_on_user_id" + end + + create_table "ontology_term_links", force: :cascade do |t| + t.integer "resource_id" + t.string "resource_type" + t.string "term_uri" + t.string "field" + t.index ["field"], name: "index_ontology_term_links_on_field" + t.index ["resource_type", "resource_id"], name: "index_ontology_term_links_on_resource_type_and_resource_id" + t.index ["term_uri"], name: "index_ontology_term_links_on_term_uri" + end + + create_table "profiles", force: :cascade do |t| + t.text "firstname" + t.text "surname" + t.text "image_url" + t.text "email" + t.text "website" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "user_id" + t.string "slug" + t.boolean "public", default: false + t.text "description" + t.text "location" + t.string "orcid" + t.string "experience" + t.string "expertise_academic", default: [], array: true + t.string "expertise_technical", default: [], array: true + t.string "interest", default: [], array: true + t.string "activity", default: [], array: true + t.string "language", default: [], array: true + t.string "social_media", default: [], array: true + t.string "type", default: "Profile" + t.string "fields", default: [], array: true + t.boolean "orcid_authenticated", default: false + t.index ["orcid"], name: "index_profiles_on_orcid" + t.index ["slug"], name: "index_profiles_on_slug", unique: true + end + + create_table "roles", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "title" + end + + create_table "sources", force: :cascade do |t| + t.bigint "content_provider_id" + t.bigint "user_id" + t.datetime "created_at" + t.datetime "finished_at" + t.string "url" + t.string "method" + t.integer "records_read" + t.integer "records_written" + t.integer "resources_added" + t.integer "resources_updated" + t.integer "resources_rejected" + t.text "log" + t.boolean "enabled" + t.string "token" + t.integer "approval_status" + t.datetime "updated_at" + t.string "default_language" + t.bigint "space_id" + t.index ["content_provider_id"], name: "index_sources_on_content_provider_id" + t.index ["space_id"], name: "index_sources_on_space_id" + t.index ["user_id"], name: "index_sources_on_user_id" + end + + create_table "space_roles", force: :cascade do |t| + t.string "key" + t.bigint "user_id" + t.bigint "space_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["space_id"], name: "index_space_roles_on_space_id" + t.index ["user_id"], name: "index_space_roles_on_user_id" + end + + create_table "spaces", force: :cascade do |t| + t.string "title" + t.text "description" + t.string "host" + t.string "theme" + t.string "image_file_name" + t.string "image_content_type" + t.bigint "image_file_size" + t.datetime "image_updated_at" + t.bigint "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "image_url" + t.string "disabled_features", default: [], array: true + t.index ["host"], name: "index_spaces_on_host", unique: true + t.index ["user_id"], name: "index_spaces_on_user_id" + end + + create_table "staff_members", force: :cascade do |t| + t.string "name" + t.string "role" + t.string "email" + t.text "image_url" + t.integer "node_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "image_file_name" + t.string "image_content_type" + t.bigint "image_file_size" + t.datetime "image_updated_at" + t.index ["node_id"], name: "index_staff_members_on_node_id" + end + + create_table "stars", force: :cascade do |t| + t.integer "user_id" + t.integer "resource_id" + t.string "resource_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["resource_type", "resource_id"], name: "index_stars_on_resource_type_and_resource_id" + t.index ["user_id"], name: "index_stars_on_user_id" + end + + create_table "subscriptions", force: :cascade do |t| + t.integer "user_id" + t.datetime "last_sent_at" + t.text "query" + t.json "facets" + t.integer "frequency" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "subscribable_type" + t.datetime "last_checked_at" + t.index ["user_id"], name: "index_subscriptions_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "username" + t.integer "role_id" + t.string "authentication_token" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.inet "current_sign_in_ip" + t.inet "last_sign_in_ip" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.integer "failed_attempts", default: 0, null: false + t.string "unlock_token" + t.datetime "locked_at" + t.string "slug" + t.string "provider" + t.string "uid" + t.string "identity_url" + t.string "invitation_token" + t.datetime "invitation_created_at" + t.datetime "invitation_sent_at" + t.datetime "invitation_accepted_at" + t.integer "invitation_limit" + t.string "invited_by_type" + t.bigint "invited_by_id" + t.integer "invitations_count", default: 0 + t.text "image_url" + t.string "image_file_name" + t.string "image_content_type" + t.bigint "image_file_size" + t.datetime "image_updated_at" + t.boolean "check_broken_scrapers", default: false + t.index ["authentication_token"], name: "index_users_on_authentication_token" + t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["identity_url"], name: "index_users_on_identity_url", unique: true + t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true + t.index ["invited_by_id"], name: "index_users_on_invited_by_id" + t.index ["invited_by_type", "invited_by_id"], name: "index_users_on_invited_by" + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + t.index ["role_id"], name: "index_users_on_role_id" + t.index ["slug"], name: "index_users_on_slug", unique: true + t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true + t.index ["username"], name: "index_users_on_username", unique: true + end + + create_table "widget_logs", force: :cascade do |t| + t.string "widget_name" + t.string "action" + t.integer "resource_id" + t.string "resource_type" + t.text "data" + t.json "params" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "referrer" + t.index ["resource_type", "resource_id"], name: "index_widget_logs_on_resource_type_and_resource_id" + end + + create_table "workflows", force: :cascade do |t| + t.string "title" + t.string "description" + t.integer "user_id" + t.json "workflow_content" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "slug" + t.string "target_audience", default: [], array: true + t.string "keywords", default: [], array: true + t.string "authors", default: [], array: true + t.string "contributors", default: [], array: true + t.string "licence", default: "notspecified" + t.string "difficulty_level", default: "notspecified" + t.string "doi" + t.date "remote_created_date" + t.date "remote_updated_date" + t.boolean "hide_child_nodes", default: false + t.boolean "public", default: true + t.bigint "space_id" + t.index ["slug"], name: "index_workflows_on_slug", unique: true + t.index ["space_id"], name: "index_workflows_on_space_id" + t.index ["user_id"], name: "index_workflows_on_user_id" + end + + add_foreign_key "bans", "users" + add_foreign_key "bans", "users", column: "banner_id" + add_foreign_key "collaborations", "users" + add_foreign_key "collections", "spaces" + add_foreign_key "collections", "users" + add_foreign_key "content_providers", "nodes" + add_foreign_key "content_providers", "users" + add_foreign_key "event_materials", "events" + add_foreign_key "event_materials", "materials" + add_foreign_key "events", "spaces" + add_foreign_key "events", "users" + add_foreign_key "learning_path_topic_links", "learning_paths" + add_foreign_key "learning_path_topics", "spaces" + add_foreign_key "learning_paths", "content_providers" + add_foreign_key "learning_paths", "spaces" + add_foreign_key "learning_paths", "users" + add_foreign_key "llm_interactions", "events" + add_foreign_key "materials", "content_providers" + add_foreign_key "materials", "spaces" + add_foreign_key "materials", "users" + add_foreign_key "node_links", "nodes" + add_foreign_key "nodes", "users" + add_foreign_key "sources", "content_providers" + add_foreign_key "sources", "spaces" + add_foreign_key "sources", "users" + add_foreign_key "space_roles", "spaces" + add_foreign_key "space_roles", "users" + add_foreign_key "spaces", "users" + add_foreign_key "staff_members", "nodes" + add_foreign_key "stars", "users" + add_foreign_key "subscriptions", "users" + add_foreign_key "users", "roles" + add_foreign_key "workflows", "spaces" + add_foreign_key "workflows", "users" end