wikidata_position_history 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.reek.yml +14 -0
- data/.rubocop.yml +54 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/Rakefile +24 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/position-history-for-item +11 -0
- data/exe/update_wikidata_page +26 -0
- data/lib/query_service.rb +103 -0
- data/lib/sparql/item_query.rb +28 -0
- data/lib/sparql/mandates.rb +88 -0
- data/lib/sparql/position_data.rb +62 -0
- data/lib/wikidata_position_history.rb +87 -0
- data/lib/wikidata_position_history/checks.rb +157 -0
- data/lib/wikidata_position_history/report.rb +140 -0
- data/lib/wikidata_position_history/version.rb +5 -0
- data/wikidata_position_history.gemspec +38 -0
- metadata +211 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 43e80159fad0253e687e1dc8d52318079bdc2bf5
|
4
|
+
data.tar.gz: 26974422ae9f8584c6907d8531b63eeccee42231
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c1a7fb0eb3c293884452995d44dd7371ace248f48f676c15e64f87e1ed8604eedf0a8df53599186a5c978f8b93cafcd0d0f1282c8592296288d3ef5d7bfbf22f
|
7
|
+
data.tar.gz: 3dc935ea7ad2f67e9c1c8a4ca520a8a129fd05f2b78cb27d22f81e397fff81fcb311f543aa75949d824f5e565c25fdcce18bf478b9a7e3846110f4264036b1d2
|
data/.gitignore
ADDED
data/.reek.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
---
|
2
|
+
exclude_paths:
|
3
|
+
- test
|
4
|
+
|
5
|
+
detectors:
|
6
|
+
DuplicateMethodCall:
|
7
|
+
exclude:
|
8
|
+
- QueryService::WikidataDate#<=>
|
9
|
+
MissingSafeMethod:
|
10
|
+
exclude:
|
11
|
+
- WikidataPositionHistory::PageRewriter
|
12
|
+
UncommunicativeVariableName:
|
13
|
+
exclude:
|
14
|
+
- QueryService::Query#results
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- 'Vagrantfile'
|
4
|
+
- 'vendor/**/*'
|
5
|
+
TargetRubyVersion: 2.4
|
6
|
+
NewCops: enable
|
7
|
+
|
8
|
+
Metrics/AbcSize:
|
9
|
+
Exclude:
|
10
|
+
- 'test/test_helper.rb'
|
11
|
+
|
12
|
+
Metrics/BlockLength:
|
13
|
+
Exclude:
|
14
|
+
- '**/*.gemspec'
|
15
|
+
- 'test/**/*.rb'
|
16
|
+
|
17
|
+
Layout/LineLength:
|
18
|
+
Max: 150
|
19
|
+
Exclude:
|
20
|
+
- 'test/**/*.rb'
|
21
|
+
|
22
|
+
Layout/HashAlignment:
|
23
|
+
EnforcedHashRocketStyle: table
|
24
|
+
EnforcedColonStyle: table
|
25
|
+
|
26
|
+
Lint/AssignmentInCondition:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Naming/ClassAndModuleCamelCase:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
Style/CollectionMethods:
|
33
|
+
Enabled: true
|
34
|
+
|
35
|
+
Style/Documentation:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
Style/FormatStringToken:
|
39
|
+
Enabled: false
|
40
|
+
|
41
|
+
Style/HashSyntax:
|
42
|
+
EnforcedStyle: ruby19_no_mixed_keys
|
43
|
+
|
44
|
+
Style/RescueModifier:
|
45
|
+
Enabled: false
|
46
|
+
|
47
|
+
Style/SymbolArray:
|
48
|
+
Enabled: true
|
49
|
+
|
50
|
+
Style/TrailingCommaInHashLiteral:
|
51
|
+
EnforcedStyleForMultiline: consistent_comma
|
52
|
+
|
53
|
+
Style/TrailingCommaInArrayLiteral:
|
54
|
+
EnforcedStyleForMultiline: no_comma
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
# [1.3.2] - 2020-08-29
|
4
|
+
|
5
|
+
## Fixes
|
6
|
+
|
7
|
+
* Skip all date checks for items with no dates
|
8
|
+
|
9
|
+
# [1.3.1] - 2020-08-28
|
10
|
+
|
11
|
+
## Fixes
|
12
|
+
|
13
|
+
* Skip the dates line if neither date is set
|
14
|
+
|
15
|
+
# [1.3.0] - 2020-08-17
|
16
|
+
|
17
|
+
## Enhancements
|
18
|
+
|
19
|
+
* Display dates at more accurate levels of precision
|
20
|
+
|
21
|
+
# [1.2.0] - 2020-08-14
|
22
|
+
|
23
|
+
## Enhancements
|
24
|
+
|
25
|
+
* Display inception and/or abolition dates for the position itself.
|
26
|
+
|
27
|
+
## Fixes
|
28
|
+
|
29
|
+
* If an officeholder held multiple consecutive terms, do not warn that
|
30
|
+
they do not have 'replaces' or 'replaced by' statements pointing at
|
31
|
+
themselves
|
32
|
+
|
33
|
+
# [1.1.0] - 2020-08-14
|
34
|
+
|
35
|
+
## Enhancements
|
36
|
+
|
37
|
+
* 'Acting' officeholders are visually differtiated in the output, and do
|
38
|
+
not require replaces/replaced-by statements
|
39
|
+
* If no officeholders are found, explicitly say so, rather than
|
40
|
+
displaying a completely empty table
|
41
|
+
* Position ID will be derived from Page name if not supplied
|
42
|
+
|
43
|
+
## Fixes
|
44
|
+
|
45
|
+
* Deprecated P39 (position held) statements are no longer included
|
46
|
+
|
47
|
+
# [1.0.0] - 2017-11-03
|
48
|
+
|
49
|
+
Original release by mySociety
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Tony Bowden, 2017 mySociety
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# WikidataPositionHistory
|
2
|
+
|
3
|
+
Rewrites Mediawiki pages that include a `PositionHolderHistory`
|
4
|
+
template, to show a timeline of people who have held a particular
|
5
|
+
office, along with helpful diagnostic warnings for common errors.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'wikidata_position_history', github: 'tmtmtmtm/wikidata-position-history'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
WikidataPositionHistory::PageRewriter.new(
|
23
|
+
mediawiki_site: 'www.wikidata.org',
|
24
|
+
page_title: 'User:Mhl20/Prime_minister_test'
|
25
|
+
).run!
|
26
|
+
```
|
27
|
+
|
28
|
+
This looks for a Template call in that page of the form:
|
29
|
+
|
30
|
+
```
|
31
|
+
{{PositionHolderHistory|id=Q14211}}
|
32
|
+
```
|
33
|
+
|
34
|
+
If such a template is found, a table is inserted after it listing all
|
35
|
+
people who have held (i.e. have a relevant P39 "position held"
|
36
|
+
statement) position Q14211.
|
37
|
+
|
38
|
+
A sentinel HTML comment is also inserted, so that on subsequent runs
|
39
|
+
only the text between the template and that comment are rewritten.
|
40
|
+
|
41
|
+
## Development
|
42
|
+
|
43
|
+
After checking out the repo, run `bin/setup` to install
|
44
|
+
dependencies. Then, run `rake test` to run the tests. You can
|
45
|
+
also run `bin/console` for an interactive prompt that will allow
|
46
|
+
you to experiment.
|
47
|
+
|
48
|
+
To install this gem onto your local machine, run `bundle exec
|
49
|
+
rake install`. To release a new version, update the version
|
50
|
+
number in `version.rb`, and then run `bundle exec rake release`,
|
51
|
+
which will create a git tag for the version, push git commits
|
52
|
+
and tags, and push the `.gem` file to
|
53
|
+
[rubygems.org](https://rubygems.org).
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
Bug reports and pull requests are welcome on GitHub at
|
58
|
+
https://github.com/tmtmtmtm/wikidata-position-history
|
59
|
+
|
60
|
+
## License
|
61
|
+
|
62
|
+
The gem is available as open source under the terms of the
|
63
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
64
|
+
|
65
|
+
## History
|
66
|
+
|
67
|
+
This was originally developed by Tony Bowden and Mark Longair at
|
68
|
+
mySociety as part of a [Wikimedia Foundation grant-funded
|
69
|
+
project](https://meta.wikimedia.org/wiki/Grants:Project/mySociety/EveryPolitician).
|
70
|
+
|
71
|
+
This version is now maintained independently by Tony Bowden.
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'reek/rake/task'
|
6
|
+
require 'rubocop/rake_task'
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'test'
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.test_files = FileList['test/**/*_test.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Run rubocop'
|
15
|
+
task :rubocop do
|
16
|
+
RuboCop::RakeTask.new
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Run reek'
|
20
|
+
Reek::Rake::Task.new do |t|
|
21
|
+
t.fail_on_error = true
|
22
|
+
end
|
23
|
+
|
24
|
+
task default: %i[test rubocop reek]
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'wikidata_position_history'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'wikidata_position_history'
|
5
|
+
|
6
|
+
if ARGV.size != 1
|
7
|
+
abort "Usage: #{$PROGRAM_NAME} ITEM_ID
|
8
|
+
e.g. #{$PROGRAM_NAME} Q14211'"
|
9
|
+
end
|
10
|
+
|
11
|
+
puts WikidataPositionHistory::Report.new(ARGV.first).wikitext_with_header
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'wikidata_position_history'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
if ARGV.size != 1
|
8
|
+
abort "Usage: #{$PROGRAM_NAME} REFRESH_URL_OR_QUERYSTRING
|
9
|
+
e.g. wikidata_position_history 'mediawiki_site=www.wikidata.org&page_title=User%3AMhl20%2FPrime_minister_test'
|
10
|
+
or wikidata_position_history https://www.wikidata.org/wiki/User:Mhl20/Prime_minister_test"
|
11
|
+
end
|
12
|
+
|
13
|
+
if ARGV.first.start_with?('http')
|
14
|
+
uri = URI.parse ARGV.first
|
15
|
+
options = {
|
16
|
+
mediawiki_site: uri.hostname,
|
17
|
+
page_title: uri.path.gsub('/wiki/', ''), # with 2.5.1 we could use delete_prefix
|
18
|
+
}
|
19
|
+
else
|
20
|
+
options = URI.decode_www_form(ARGV.first).map { |k, v| [k.to_sym, v] }.to_h
|
21
|
+
end
|
22
|
+
|
23
|
+
warn "Running with options: #{options.inspect}" if ENV.key?('DEBUG')
|
24
|
+
|
25
|
+
rewriter = WikidataPositionHistory::PageRewriter.new(**options)
|
26
|
+
rewriter.run!
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'rest-client'
|
5
|
+
|
6
|
+
module QueryService
|
7
|
+
# A SPARQL query against the Wikidata Query Service
|
8
|
+
class Query
|
9
|
+
WIKIDATA_SPARQL_URL = 'https://query.wikidata.org/sparql'
|
10
|
+
|
11
|
+
def initialize(query)
|
12
|
+
@query = query
|
13
|
+
end
|
14
|
+
|
15
|
+
def results
|
16
|
+
json
|
17
|
+
rescue RestClient::Exception => e
|
18
|
+
raise "Wikidata query #{query} failed: #{e.message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :query
|
24
|
+
|
25
|
+
def result
|
26
|
+
@result ||= RestClient.get WIKIDATA_SPARQL_URL, accept: 'application/sparql-results+json', params: { query: query }
|
27
|
+
end
|
28
|
+
|
29
|
+
def json
|
30
|
+
JSON.parse(result, symbolize_names: true)[:results][:bindings]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# different views of a Wikidata item
|
35
|
+
class WikidataItem
|
36
|
+
def initialize(url)
|
37
|
+
@url = url
|
38
|
+
end
|
39
|
+
|
40
|
+
def id
|
41
|
+
url.split('/').last unless url.to_s.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
def qlink
|
45
|
+
"{{Q|#{id}}}" if id
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :url
|
51
|
+
end
|
52
|
+
|
53
|
+
# a Wikidata date of a given precision
|
54
|
+
class WikidataDate
|
55
|
+
include Comparable
|
56
|
+
|
57
|
+
def initialize(str, precision)
|
58
|
+
@str = str
|
59
|
+
@raw_precision = precision.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def <=>(other)
|
63
|
+
return to_s <=> other.to_s if precision == other.precision
|
64
|
+
return year <=> other.year if year != other.year
|
65
|
+
return month <=> other.month if month && other.month
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
return str if precision == '11'
|
70
|
+
return str[0..6] if precision == '10'
|
71
|
+
return str[0..3] if precision == '9'
|
72
|
+
|
73
|
+
warn "Cannot handle precision #{precision} for #{str}"
|
74
|
+
str
|
75
|
+
end
|
76
|
+
|
77
|
+
def empty?
|
78
|
+
str.to_s.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def precision
|
82
|
+
return '11' if raw_precision.empty? # default to YYYY-MM-DD
|
83
|
+
|
84
|
+
raw_precision
|
85
|
+
end
|
86
|
+
|
87
|
+
def year
|
88
|
+
parts[0]
|
89
|
+
end
|
90
|
+
|
91
|
+
def month
|
92
|
+
parts[1]
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
attr_reader :str, :raw_precision
|
98
|
+
|
99
|
+
def parts
|
100
|
+
to_s.split('-')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WikidataPositionHistory
|
4
|
+
module SPARQL
|
5
|
+
# Turn raw SPARQL into result objects
|
6
|
+
class ItemQuery
|
7
|
+
def initialize(itemid)
|
8
|
+
@itemid = itemid
|
9
|
+
end
|
10
|
+
|
11
|
+
def results_as(klass)
|
12
|
+
json.map { |result| klass.new(result) }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :itemid
|
18
|
+
|
19
|
+
def sparql
|
20
|
+
raw_sparql % itemid
|
21
|
+
end
|
22
|
+
|
23
|
+
def json
|
24
|
+
@json ||= QueryService::Query.new(sparql).results
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WikidataPositionHistory
|
4
|
+
module SPARQL
|
5
|
+
# SPARQL for fetching all officeholdings of a position
|
6
|
+
class Mandates < ItemQuery
|
7
|
+
def raw_sparql
|
8
|
+
<<~SPARQL
|
9
|
+
# position-mandates
|
10
|
+
SELECT DISTINCT ?ordinal ?item ?start_date ?start_precision ?end_date ?end_precision ?prev ?next ?nature
|
11
|
+
WHERE {
|
12
|
+
?item wdt:P31 wd:Q5 ; p:P39 ?posn .
|
13
|
+
?posn ps:P39 wd:%s .
|
14
|
+
FILTER NOT EXISTS { ?posn wikibase:rank wikibase:DeprecatedRank }
|
15
|
+
|
16
|
+
OPTIONAL { ?posn pqv:P580 [ wikibase:timeValue ?start_date; wikibase:timePrecision ?start_precision ] }
|
17
|
+
OPTIONAL { ?posn pqv:P582 [ wikibase:timeValue ?end_date; wikibase:timePrecision ?end_precision ] }
|
18
|
+
OPTIONAL { ?posn pq:P1365|pq:P155 ?prev }
|
19
|
+
OPTIONAL { ?posn pq:P1366|pq:P156 ?next }
|
20
|
+
OPTIONAL { ?posn pq:P1545 ?ordinal }
|
21
|
+
OPTIONAL { ?posn pq:P5102 ?nature }
|
22
|
+
OPTIONAL { ?posn pq:P5102 ?nature }
|
23
|
+
}
|
24
|
+
ORDER BY DESC(?start_date)
|
25
|
+
SPARQL
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Represents a single row returned from the Mandates query
|
31
|
+
class Mandate
|
32
|
+
def initialize(row)
|
33
|
+
@row = row
|
34
|
+
end
|
35
|
+
|
36
|
+
def ordinal
|
37
|
+
row.dig(:ordinal, :value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def item
|
41
|
+
QueryService::WikidataItem.new(row.dig(:item, :value)).qlink
|
42
|
+
end
|
43
|
+
|
44
|
+
def prev
|
45
|
+
QueryService::WikidataItem.new(row.dig(:prev, :value)).qlink
|
46
|
+
end
|
47
|
+
|
48
|
+
def next
|
49
|
+
QueryService::WikidataItem.new(row.dig(:next, :value)).qlink
|
50
|
+
end
|
51
|
+
|
52
|
+
def nature
|
53
|
+
QueryService::WikidataItem.new(row.dig(:nature, :value)).id
|
54
|
+
end
|
55
|
+
|
56
|
+
def acting?
|
57
|
+
nature == 'Q4676846'
|
58
|
+
end
|
59
|
+
|
60
|
+
def start_date
|
61
|
+
QueryService::WikidataDate.new(start_date_raw, start_date_precision)
|
62
|
+
end
|
63
|
+
|
64
|
+
def end_date
|
65
|
+
QueryService::WikidataDate.new(end_date_raw, end_date_precision)
|
66
|
+
end
|
67
|
+
|
68
|
+
def start_date_raw
|
69
|
+
row.dig(:start_date, :value).to_s[0..9]
|
70
|
+
end
|
71
|
+
|
72
|
+
def end_date_raw
|
73
|
+
row.dig(:end_date, :value).to_s[0..9]
|
74
|
+
end
|
75
|
+
|
76
|
+
def start_date_precision
|
77
|
+
row.dig(:start_precision, :value)
|
78
|
+
end
|
79
|
+
|
80
|
+
def end_date_precision
|
81
|
+
row.dig(:end_precision, :value)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_reader :row
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WikidataPositionHistory
|
4
|
+
module SPARQL
|
5
|
+
# SPARQL for fetching metadata about a position
|
6
|
+
class PositionData < ItemQuery
|
7
|
+
def raw_sparql
|
8
|
+
<<~SPARQL
|
9
|
+
# position-metadata
|
10
|
+
|
11
|
+
SELECT DISTINCT ?inception ?inception_precision ?abolition ?abolition_precision ?isPosition
|
12
|
+
WHERE {
|
13
|
+
VALUES ?item { wd:%s }
|
14
|
+
BIND(EXISTS { ?item wdt:P279+ wd:Q4164871 } as ?isPosition)
|
15
|
+
OPTIONAL { ?item p:P571/psv:P571 [ wikibase:timeValue ?inception; wikibase:timePrecision ?inception_precision ] }
|
16
|
+
OPTIONAL { ?item p:P576/psv:P576 [ wikibase:timeValue ?abolition; wikibase:timePrecision ?abolition_precision ] }
|
17
|
+
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
|
18
|
+
}
|
19
|
+
SPARQL
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Represents a single row returned from the Position query
|
25
|
+
class PositionData
|
26
|
+
def initialize(row)
|
27
|
+
@row = row
|
28
|
+
end
|
29
|
+
|
30
|
+
def inception_date
|
31
|
+
QueryService::WikidataDate.new(inception_date_raw, inception_date_precision)
|
32
|
+
end
|
33
|
+
|
34
|
+
def abolition_date
|
35
|
+
QueryService::WikidataDate.new(abolition_date_raw, abolition_date_precision)
|
36
|
+
end
|
37
|
+
|
38
|
+
def position?
|
39
|
+
row.dig(:isPosition, :value) == 'true'
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :row
|
45
|
+
|
46
|
+
def inception_date_raw
|
47
|
+
row.dig(:inception, :value).to_s[0..9]
|
48
|
+
end
|
49
|
+
|
50
|
+
def abolition_date_raw
|
51
|
+
row.dig(:abolition, :value).to_s[0..9]
|
52
|
+
end
|
53
|
+
|
54
|
+
def inception_date_precision
|
55
|
+
row.dig(:inception_precision, :value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def abolition_date_precision
|
59
|
+
row.dig(:abolition_precision, :value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'query_service'
|
4
|
+
require 'sparql/item_query'
|
5
|
+
require 'sparql/position_data'
|
6
|
+
require 'sparql/mandates'
|
7
|
+
require 'wikidata_position_history/checks'
|
8
|
+
require 'wikidata_position_history/report'
|
9
|
+
require 'wikidata_position_history/version'
|
10
|
+
|
11
|
+
require 'date'
|
12
|
+
|
13
|
+
require 'mediawiki/client'
|
14
|
+
require 'mediawiki/page'
|
15
|
+
|
16
|
+
module WikidataPositionHistory
|
17
|
+
# Rewrites a Wiki page
|
18
|
+
class PageRewriter
|
19
|
+
WIKI_TEMPLATE_NAME = 'PositionHolderHistory'
|
20
|
+
WIKI_USERNAME = ENV['WIKI_USERNAME']
|
21
|
+
WIKI_PASSWORD = ENV['WIKI_PASSWORD']
|
22
|
+
|
23
|
+
def initialize(mediawiki_site:, page_title:)
|
24
|
+
@mediawiki_site = mediawiki_site
|
25
|
+
@page_title = page_title.tr('_', ' ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def run!
|
29
|
+
section.replace_output(*new_content)
|
30
|
+
end
|
31
|
+
|
32
|
+
def new_content
|
33
|
+
return [NO_ID_ERROR, 'The id parameter was missing'] if position_id.empty?
|
34
|
+
return [MALFORMED_ID_ERROR, 'The id parameter was malformed'] unless position_id =~ /^Q\d+$/
|
35
|
+
|
36
|
+
[WikidataPositionHistory::Report.new(position_id).wikitext, "Successfully updated holders of #{position_id}"]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :mediawiki_site, :page_title
|
42
|
+
|
43
|
+
NO_ID_ERROR = <<~EOERROR
|
44
|
+
'''#{WIKI_TEMPLATE_NAME} Error''': You must pass the <code>id</code>
|
45
|
+
parameter to the <code>#{WIKI_TEMPLATE_NAME}</code> template; e.g.
|
46
|
+
<nowiki>{{#{WIKI_TEMPLATE_NAME}|id=Q14211}}</nowiki>
|
47
|
+
EOERROR
|
48
|
+
|
49
|
+
MALFORMED_ID_ERROR = <<~EOERROR
|
50
|
+
'''#{WIKI_TEMPLATE_NAME} Error''': The <code>id</code> parameter was
|
51
|
+
malformed; it should be Q followed by a number of digits, e.g. as in:
|
52
|
+
|
53
|
+
<nowiki>{{#{WIKI_TEMPLATE_NAME}|id=Q14211}}</nowiki>
|
54
|
+
EOERROR
|
55
|
+
|
56
|
+
def position_id
|
57
|
+
return id_param unless id_param.empty?
|
58
|
+
|
59
|
+
derived_id
|
60
|
+
end
|
61
|
+
|
62
|
+
def id_param
|
63
|
+
section.params[:id].to_s.strip
|
64
|
+
end
|
65
|
+
|
66
|
+
def derived_id
|
67
|
+
page_title.scan(/Q\d+/).last.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def client
|
71
|
+
abort 'You must set the WIKI_USERNAME and WIKI_PASSWORD environment variables' unless WIKI_USERNAME && WIKI_PASSWORD
|
72
|
+
@client ||= MediaWiki::Client.new(
|
73
|
+
site: mediawiki_site,
|
74
|
+
username: ENV['WIKI_USERNAME'],
|
75
|
+
password: ENV['WIKI_PASSWORD']
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def section
|
80
|
+
@section ||= MediaWiki::Page::ReplaceableContent.new(
|
81
|
+
client: client,
|
82
|
+
title: page_title,
|
83
|
+
template: WIKI_TEMPLATE_NAME
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WikidataPositionHistory
|
4
|
+
# Checks if an Officeholder has any warning signs to report on
|
5
|
+
class Check
|
6
|
+
def initialize(later, current, earlier)
|
7
|
+
@later = later
|
8
|
+
@current = current
|
9
|
+
@earlier = earlier
|
10
|
+
end
|
11
|
+
|
12
|
+
def explanation
|
13
|
+
possible_explanation if problem?
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
attr_reader :later, :current, :earlier
|
19
|
+
|
20
|
+
def successor
|
21
|
+
current.next
|
22
|
+
end
|
23
|
+
|
24
|
+
def predecessor
|
25
|
+
current.prev
|
26
|
+
end
|
27
|
+
|
28
|
+
def latest_holder?
|
29
|
+
!!later
|
30
|
+
end
|
31
|
+
|
32
|
+
def earliest_holder?
|
33
|
+
!!earlier
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Check
|
38
|
+
# Does the Officeholder have all the properties we expect?
|
39
|
+
class MissingFields < Check
|
40
|
+
def problem?
|
41
|
+
missing.any?
|
42
|
+
end
|
43
|
+
|
44
|
+
def headline
|
45
|
+
"Missing field#{missing.count > 1 ? 's' : ''}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def possible_explanation
|
49
|
+
"#{current.item} is missing #{missing.map { |field| "{{P|#{field_map[field]}}}" }.join(', ')}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def missing
|
53
|
+
expected.reject { |field| current.send(field) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def field_map
|
57
|
+
{
|
58
|
+
start_date: 580,
|
59
|
+
prev: 1365,
|
60
|
+
end_date: 582,
|
61
|
+
next: 1366,
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def expected
|
66
|
+
field_map.keys.select { |field| send("expect_#{field}?") }
|
67
|
+
end
|
68
|
+
|
69
|
+
def expect_start_date?
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def expect_end_date?
|
74
|
+
later
|
75
|
+
end
|
76
|
+
|
77
|
+
def expect_prev?
|
78
|
+
return unless earlier
|
79
|
+
return if earlier.item == current.item # sucessive terms by same person
|
80
|
+
|
81
|
+
!current.acting?
|
82
|
+
end
|
83
|
+
|
84
|
+
def expect_next?
|
85
|
+
return unless later
|
86
|
+
return if later.item == current.item # sucessive terms by same person
|
87
|
+
|
88
|
+
!current.acting?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Does the 'replaces' match the previous item in the list?
|
93
|
+
class WrongPredecessor < Check
|
94
|
+
def problem?
|
95
|
+
earliest_holder? && !!predecessor && (earlier.item != predecessor)
|
96
|
+
end
|
97
|
+
|
98
|
+
def headline
|
99
|
+
'Inconsistent predecessor'
|
100
|
+
end
|
101
|
+
|
102
|
+
def possible_explanation
|
103
|
+
"#{current.item} has a {{P|1365}} of #{predecessor}, which differs from #{earlier.item}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Does the 'replaced by' match the next item in the list?
|
108
|
+
class WrongSuccessor < Check
|
109
|
+
def problem?
|
110
|
+
latest_holder? && !!successor && (later.item != successor)
|
111
|
+
end
|
112
|
+
|
113
|
+
def headline
|
114
|
+
'Inconsistent successor'
|
115
|
+
end
|
116
|
+
|
117
|
+
def possible_explanation
|
118
|
+
"#{current.item} has a {{P|1366}} of #{successor}, which differs from #{later.item}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Does the end date overlap with the successor's start date?
|
123
|
+
class Overlap < Check
|
124
|
+
def problem?
|
125
|
+
return false unless later
|
126
|
+
|
127
|
+
ends = current.end_date
|
128
|
+
return false if ends.empty?
|
129
|
+
|
130
|
+
ends > later.start_date
|
131
|
+
rescue ArgumentError
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
def headline
|
136
|
+
comparable? ? 'Date overlap' : 'Date precision'
|
137
|
+
end
|
138
|
+
|
139
|
+
def possible_explanation
|
140
|
+
"#{current.item} has a {{P|582}} of #{current.end_date}, which #{overlap_explanation} the {{P|580}} of #{later.start_date} for #{later.item}"
|
141
|
+
end
|
142
|
+
|
143
|
+
protected
|
144
|
+
|
145
|
+
def comparable?
|
146
|
+
# Seems like there must be a better way to do this
|
147
|
+
[current.end_date, later.start_date].sort
|
148
|
+
rescue ArgumentError
|
149
|
+
false
|
150
|
+
end
|
151
|
+
|
152
|
+
def overlap_explanation
|
153
|
+
comparable? ? 'is later than' : 'may overlap with'
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WikidataPositionHistory
|
4
|
+
# A single output row of Wikitext for an officeholding
|
5
|
+
class MandateReport
|
6
|
+
def initialize(later, current, earlier)
|
7
|
+
@later = later
|
8
|
+
@current = current
|
9
|
+
@earlier = earlier
|
10
|
+
end
|
11
|
+
|
12
|
+
def output
|
13
|
+
[row_start, ordinal_cell, member_cell, warnings_cell].join("\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
CHECKS = [Check::MissingFields, Check::WrongPredecessor, Check::WrongSuccessor, Check::Overlap].freeze
|
19
|
+
|
20
|
+
WARNING_LAYOUT = [
|
21
|
+
'<span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]] ',
|
22
|
+
'<span style="color: #d33; font-weight: bold; vertical-align: middle;">%s</span> ',
|
23
|
+
'<ref>%s</ref></span>'
|
24
|
+
].join
|
25
|
+
|
26
|
+
attr_reader :later, :current, :earlier
|
27
|
+
|
28
|
+
def row_start
|
29
|
+
'|-'
|
30
|
+
end
|
31
|
+
|
32
|
+
def ordinal_cell
|
33
|
+
%(| style="padding:0.5em 2em" | #{ordinal_string})
|
34
|
+
end
|
35
|
+
|
36
|
+
def ordinal_string
|
37
|
+
ordinal = current.ordinal or return ''
|
38
|
+
ordinal.concat('.')
|
39
|
+
end
|
40
|
+
|
41
|
+
def member_style
|
42
|
+
return 'font-size: 1.25em; display: block; font-style: italic;' if current.acting?
|
43
|
+
|
44
|
+
'font-size: 1.5em; display: block;'
|
45
|
+
end
|
46
|
+
|
47
|
+
def member_cell
|
48
|
+
format('| style="padding:0.5em 2em" | <span style="%s">%s</span> %s',
|
49
|
+
member_style, membership_person, membership_dates)
|
50
|
+
end
|
51
|
+
|
52
|
+
def warnings_cell
|
53
|
+
format('| style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | %s',
|
54
|
+
combined_warnings)
|
55
|
+
end
|
56
|
+
|
57
|
+
def combined_warnings
|
58
|
+
CHECKS.map do |check_class|
|
59
|
+
check = check_class.new(later, current, earlier)
|
60
|
+
format(WARNING_LAYOUT, check.headline, check.explanation) if check.problem?
|
61
|
+
end.join
|
62
|
+
end
|
63
|
+
|
64
|
+
def membership_person
|
65
|
+
current.item
|
66
|
+
end
|
67
|
+
|
68
|
+
def membership_dates
|
69
|
+
dates = [current.start_date, current.end_date]
|
70
|
+
# compact doesn't work here, even if we add #nil? to WikidataDate
|
71
|
+
return '' if dates.reject(&:empty?).empty?
|
72
|
+
|
73
|
+
dates.join(' – ')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# The entire wikitext generated for this report
|
78
|
+
class Report
|
79
|
+
def initialize(subject_item_id)
|
80
|
+
@subject_item_id = subject_item_id
|
81
|
+
end
|
82
|
+
|
83
|
+
attr_reader :subject_item_id
|
84
|
+
|
85
|
+
def wikitext
|
86
|
+
return no_items_output if mandates.empty?
|
87
|
+
|
88
|
+
[table_header, table_rows, table_footer].compact.join("\n")
|
89
|
+
end
|
90
|
+
|
91
|
+
def header
|
92
|
+
"== {{Q|#{subject_item_id}}} officeholders #{position_dates} =="
|
93
|
+
end
|
94
|
+
|
95
|
+
def position_dates
|
96
|
+
dates = [metadata.inception_date, metadata.abolition_date]
|
97
|
+
return '' if dates.compact.empty?
|
98
|
+
|
99
|
+
format('(%s)', dates.join(' – '))
|
100
|
+
end
|
101
|
+
|
102
|
+
def wikitext_with_header
|
103
|
+
[header, wikitext].join("\n")
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def metadata
|
109
|
+
# TODO: we might get more than one response, if a position has
|
110
|
+
# multiple dates
|
111
|
+
@metadata ||= SPARQL::PositionData.new(subject_item_id).results_as(PositionData).first
|
112
|
+
end
|
113
|
+
|
114
|
+
def padded_mandates
|
115
|
+
[nil, mandates, nil].flatten(1)
|
116
|
+
end
|
117
|
+
|
118
|
+
def mandates
|
119
|
+
@mandates ||= SPARQL::Mandates.new(subject_item_id).results_as(Mandate)
|
120
|
+
end
|
121
|
+
|
122
|
+
def no_items_output
|
123
|
+
"\n{{PositionHolderHistory/error_no_holders|id=#{subject_item_id}}}\n"
|
124
|
+
end
|
125
|
+
|
126
|
+
def table_header
|
127
|
+
'{| class="wikitable" style="text-align: center; border: none;"'
|
128
|
+
end
|
129
|
+
|
130
|
+
def table_footer
|
131
|
+
"|}\n"
|
132
|
+
end
|
133
|
+
|
134
|
+
def table_rows
|
135
|
+
padded_mandates.each_cons(3).map do |later, current, earlier|
|
136
|
+
MandateReport.new(later, current, earlier).output
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'wikidata_position_history/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.required_ruby_version = '>= 2.4.0'
|
9
|
+
spec.name = 'wikidata_position_history'
|
10
|
+
spec.version = WikidataPositionHistory::VERSION
|
11
|
+
spec.authors = ['Tony Bowden', 'Mark Longair']
|
12
|
+
spec.email = ['tony@tmtm.com']
|
13
|
+
|
14
|
+
spec.summary = 'Generates a wikitext history of a holders of a position in Wikidata'
|
15
|
+
spec.homepage = 'https://github.com/everypolitician/wikidata-position-history/'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
f.match(%r{^(test|spec|features)/})
|
22
|
+
end
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_runtime_dependency 'mediawiki-page-replaceable_content', '0.1.3'
|
28
|
+
spec.add_runtime_dependency 'rest-client', '~> 2.0'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'bundler', '~> 2.1'
|
31
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
32
|
+
spec.add_development_dependency 'pry', '~> 0.10'
|
33
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
34
|
+
spec.add_development_dependency 'reek', '~> 6.0'
|
35
|
+
spec.add_development_dependency 'rubocop', '~> 0.89'
|
36
|
+
spec.add_development_dependency 'warning', '~> 1.1'
|
37
|
+
spec.add_development_dependency 'webmock', '~> 3.0.0'
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wikidata_position_history
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.3.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tony Bowden
|
8
|
+
- Mark Longair
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2020-08-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mediawiki-page-replaceable_content
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 0.1.3
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.1.3
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rest-client
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '2.0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '2.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bundler
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '2.1'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '2.1'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: minitest
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '5.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: pry
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0.10'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0.10'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rake
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '13.0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '13.0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: reek
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '6.0'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '6.0'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rubocop
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0.89'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0.89'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: warning
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '1.1'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '1.1'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: webmock
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - "~>"
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 3.0.0
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - "~>"
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: 3.0.0
|
154
|
+
description:
|
155
|
+
email:
|
156
|
+
- tony@tmtm.com
|
157
|
+
executables:
|
158
|
+
- position-history-for-item
|
159
|
+
- update_wikidata_page
|
160
|
+
extensions: []
|
161
|
+
extra_rdoc_files: []
|
162
|
+
files:
|
163
|
+
- ".gitignore"
|
164
|
+
- ".reek.yml"
|
165
|
+
- ".rubocop.yml"
|
166
|
+
- ".travis.yml"
|
167
|
+
- CHANGELOG.md
|
168
|
+
- Gemfile
|
169
|
+
- Gemfile.lock
|
170
|
+
- LICENSE.txt
|
171
|
+
- README.md
|
172
|
+
- Rakefile
|
173
|
+
- bin/console
|
174
|
+
- bin/setup
|
175
|
+
- exe/position-history-for-item
|
176
|
+
- exe/update_wikidata_page
|
177
|
+
- lib/query_service.rb
|
178
|
+
- lib/sparql/item_query.rb
|
179
|
+
- lib/sparql/mandates.rb
|
180
|
+
- lib/sparql/position_data.rb
|
181
|
+
- lib/wikidata_position_history.rb
|
182
|
+
- lib/wikidata_position_history/checks.rb
|
183
|
+
- lib/wikidata_position_history/report.rb
|
184
|
+
- lib/wikidata_position_history/version.rb
|
185
|
+
- wikidata_position_history.gemspec
|
186
|
+
homepage: https://github.com/everypolitician/wikidata-position-history/
|
187
|
+
licenses:
|
188
|
+
- MIT
|
189
|
+
metadata:
|
190
|
+
allowed_push_host: https://rubygems.org
|
191
|
+
post_install_message:
|
192
|
+
rdoc_options: []
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 2.4.0
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.6.14.1
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: Generates a wikitext history of a holders of a position in Wikidata
|
211
|
+
test_files: []
|