From 1467ec9364d767f741b968807b3ca4373f20cdf8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 12 May 2026 18:07:35 +0930 Subject: [PATCH] Add Wikidata climate attributes and integration to gardens (#4627) * Add Wikidata integration for garden climate data - Add location_wikidata_id, lowest_temp_c, and highest_temp_c to gardens. - Implement WikidataService for fetching IDs and temperature properties. - Map P6591 to highest_temp_c and P7422 to lowest_temp_c with unit conversion. - Automatically populate Wikidata info on garden location change. - Add manual "Fetch Wikidata info" button and opt-in prompt to garden show page. - Update gardens_controller to permit new attributes and handle manual fetch. - Update db/schema.rb manually to include new columns and migration version. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Fix migration * Improve display --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- app/controllers/gardens_controller.rb | 13 +++- app/models/garden.rb | 14 ++++ app/services/wikidata_service.rb | 74 +++++++++++++++++++ app/views/gardens/show.html.haml | 25 +++++++ config/routes.rb | 1 + ...35341_add_wikidata_and_temps_to_gardens.rb | 7 ++ db/schema.rb | 3 + 7 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 app/services/wikidata_service.rb create mode 100644 db/migrate/20250511035341_add_wikidata_and_temps_to_gardens.rb diff --git a/app/controllers/gardens_controller.rb b/app/controllers/gardens_controller.rb index d28fab854..43acc9893 100644 --- a/app/controllers/gardens_controller.rb +++ b/app/controllers/gardens_controller.rb @@ -57,12 +57,23 @@ class GardensController < DataController redirect_to(member_gardens_path(@garden.owner)) end + def fetch_wikidata + if @garden.populate_wikidata_info + @garden.save + flash[:notice] = "Wikidata information updated." + else + flash[:alert] = "Could not find Wikidata information for this location." + end + redirect_to @garden + end + private def garden_params params.require(:garden).permit( :name, :slug, :description, :active, - :location, :latitude, :longitude, :area, :area_unit, :garden_type_id + :location, :latitude, :longitude, :area, :area_unit, :garden_type_id, + :location_wikidata_id, :lowest_temp_c, :highest_temp_c ) end end diff --git a/app/models/garden.rb b/app/models/garden.rb index ac55b5ffa..33242a782 100644 --- a/app/models/garden.rb +++ b/app/models/garden.rb @@ -21,6 +21,7 @@ class Garden < ApplicationRecord after_validation :cleanup_area after_validation :geocode after_validation :empty_unwanted_geocodes + after_validation :populate_wikidata_info, if: :will_save_change_to_location? after_save :mark_inactive_garden_plantings_as_finished scope :active, -> { where(active: true) } @@ -92,6 +93,19 @@ class Garden < ApplicationRecord end end + def populate_wikidata_info + return false if location.blank? + + wd_id = WikidataService.find_wikidata_id(location) + return false if wd_id.blank? + + self.location_wikidata_id = wd_id + temps = WikidataService.fetch_temps(wd_id) + self.highest_temp_c = temps[:highest_temp_c] + self.lowest_temp_c = temps[:lowest_temp_c] + true + end + protected def strip_blanks diff --git a/app/services/wikidata_service.rb b/app/services/wikidata_service.rb new file mode 100644 index 000000000..0fcaa6bbf --- /dev/null +++ b/app/services/wikidata_service.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'net/http' +require 'json' + +class WikidataService + CELSIUS_UNIT_ID = 'http://www.wikidata.org/entity/Q25267' + FAHRENHEIT_UNIT_ID = 'http://www.wikidata.org/entity/Q42289' + + def self.find_wikidata_id(location_name) + return nil if location_name.blank? + + uri = URI("https://www.wikidata.org/w/api.php?action=wbsearchentities&search=#{URI.encode_www_form_component(location_name)}&language=en&format=json") + req = Net::HTTP::Get.new(uri) + req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)" + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(req) + end + + data = JSON.parse(response.body) + data.dig('search', 0, 'id') + rescue StandardError => e + Rails.logger.error "WikidataService.find_wikidata_id error: #{e.message}" + nil + end + + def self.fetch_temps(wikidata_id) + return {} if wikidata_id.blank? + + uri = URI("https://www.wikidata.org/w/api.php?action=wbgetentities&ids=#{wikidata_id}&props=claims&format=json") + req = Net::HTTP::Get.new(uri) + req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)" + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(req) + end + + data = JSON.parse(response.body) + claims = data.dig('entities', wikidata_id, 'claims') || {} + + highest_temp = extract_temp(claims['P6591']) + lowest_temp = extract_temp(claims['P7422']) + + { + highest_temp_c: highest_temp, + lowest_temp_c: lowest_temp + } + rescue StandardError => e + Rails.logger.error "WikidataService.fetch_temps error: #{e.message}" + {} + end + + def self.extract_temp(claim_data) + return nil if claim_data.blank? + + # We take the first value + main_snak = claim_data.first&.dig('mainsnak') + return nil unless main_snak&.dig('datavalue', 'type') == 'quantity' + + quantity_data = main_snak.dig('datavalue', 'value') + amount = quantity_data['amount'].to_f + unit = quantity_data['unit'] + + case unit + when CELSIUS_UNIT_ID + amount + when FAHRENHEIT_UNIT_ID + (amount - 32) * 5.0 / 9.0 + else + nil + end + end +end diff --git a/app/views/gardens/show.html.haml b/app/views/gardens/show.html.haml index 02a9f66d9..75c0d4dfa 100644 --- a/app/views/gardens/show.html.haml +++ b/app/views/gardens/show.html.haml @@ -124,6 +124,31 @@ %strong Garden type: = @garden.garden_type.name + - if @garden.location_wikidata_id.present? + %hr + %p + %small + Data about this location from + = link_to "wikidata", "https://www.wikidata.org/wiki/#{@garden.location_wikidata_id}", target: '_blank', rel: 'noopener noreferrer' + + %p + %strong Highest temperature: + - if @garden.highest_temp_c.present? + = "#{ @garden.highest_temp_c.round(1) }°C" + - else + Not known + %p + %strong Lowest temperature: + - if @garden.lowest_temp_c.present? + = "#{ @garden.lowest_temp_c.round(1) }°C" + - else + Not known + + - elsif can?(:edit, @garden) && @garden.location.present? + .alert.alert-info + %p Wikidata information is missing for this location. + = button_to "Fetch Wikidata info", fetch_wikidata_garden_path(@garden), method: :post, class: 'btn btn-info btn-sm' + .card .card-header %h4 #{@garden.owner}'s gardens diff --git a/config/routes.rb b/config/routes.rb index ee7a7a966..361db5310 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,7 @@ Rails.application.routes.draw do resources :gardens, concerns: :has_photos, param: :slug do get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' } + post 'fetch_wikidata' => 'gardens#fetch_wikidata', on: :member resources :garden_collaborators end diff --git a/db/migrate/20250511035341_add_wikidata_and_temps_to_gardens.rb b/db/migrate/20250511035341_add_wikidata_and_temps_to_gardens.rb new file mode 100644 index 000000000..e0ada6432 --- /dev/null +++ b/db/migrate/20250511035341_add_wikidata_and_temps_to_gardens.rb @@ -0,0 +1,7 @@ +class AddWikidataAndTempsToGardens < ActiveRecord::Migration[7.2] + def change + add_column :gardens, :location_wikidata_id, :string + add_column :gardens, :lowest_temp_c, :float + add_column :gardens, :highest_temp_c, :float + end +end diff --git a/db/schema.rb b/db/schema.rb index 41c702c77..1c13cf6b9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -631,6 +631,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do t.decimal "area" t.string "area_unit" t.integer "garden_type_id" + t.string "location_wikidata_id" + t.float "lowest_temp_c" + t.float "highest_temp_c" t.index ["garden_type_id"], name: "index_gardens_on_garden_type_id" t.index ["owner_id"], name: "index_gardens_on_owner_id" t.index ["slug"], name: "index_gardens_on_slug", unique: true