wikidata_position_history 1.5.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cc474575654a299ceef536c692402288d072d7caf3d9bd62de17d9027bcd287
4
- data.tar.gz: 2e6324cc0e272b5b747d65eab1aaa32f1c621a6e672487312f616afd9449d374
3
+ metadata.gz: d45377b5ba871d76f191e7dafe516f308c4f1b17f166b19d0658ee15fff98745
4
+ data.tar.gz: ede75c366b3cf99a417ab402f5151880a477b3aa1fc10ff0d4261fb7b7fc0d44
5
5
  SHA512:
6
- metadata.gz: 709a0916f16fd2a890df7ae3e2873968a23f0640945cf756785eb59bf8d7589b578fb2bd51e6263b36de729e20ea2a200cfcd9731ee6957a2e539169cacb0cbf
7
- data.tar.gz: 3dbe5d81f0fe77f6487ce0109b91205d3b4361e13c70b48f93464ea4a323d06f3c07b70ea263e4817592cedf1b632e10b10be832dd03a92114f59505b53dae5a
6
+ metadata.gz: 141397d38a5a771e8b2cb159bcd8db327d50f157ba93539b09bb08ff23a64a60e6aa4bdac2bf1a45bdd65dc2f8d343e1a4737a89fc939eafe7ed25e7473562ff
7
+ data.tar.gz: 0a3cb133222f05d0ff46dd7810a62a72f3b19ca1f7431bf265752867b916b8ef176f04e11b65a50d2b6ed8d04ba20dc3ecf6fe73cd5fb24caf98737f401b685b
@@ -1,12 +1,99 @@
1
1
  # Changelog
2
2
 
3
+ # [1.10.0] 2020-09-13
4
+
5
+ ## Enhancements
6
+
7
+ * If some other position has a "replaces" (P1365) or "replaced by"
8
+ (P1366) pointing to _this_ position, but this position doesn't have
9
+ the reciprocal inverse claims back to that one, include them as a
10
+ successor/predecessor, but warn that it’s only an indirect connection.
11
+
12
+ * The warnings in the above case now use an on-wiki template for their
13
+ text. This means they can be translated into other languages, and also
14
+ means that backlinks to these templates, via WhatLinksHere, can act as
15
+ a TODO list. The other warnings will be migrated to this approach Real
16
+ Soon Now™.
17
+
18
+ # [1.9.0] 2020-09-11
19
+
20
+ ## Enhancements
21
+
22
+ * If a position has any successor or predecessor offices (in "replaces"
23
+ (P1365) or "replaced by" (P1366)) the report will now display those.
24
+
25
+ # [1.8.0] 2020-09-09
26
+
27
+ ## Enhancements
28
+
29
+ * If the very latest person we know of having held this position also
30
+ has a 'replaced by' qualifier, that’s a sign that the successor should
31
+ really also have a suitable P39, and actually appear here too. So we
32
+ want to display a warning in such cases. Likewise if the earliest person
33
+ we know if also has a 'replaces'
34
+
35
+ * This report is meant to be used with positions that are held by only a
36
+ single person at a time. Using it to produce a report of everyone who
37
+ has been, say, a Member of the UK Parliament, is the sort of thing that
38
+ will cause all manner of havoc, as it will try to display tens of
39
+ thousands of people, all of whom have overlaps with other members etc.
40
+ So we now sanity-check first of all that the position isn't legislative,
41
+ and produce a nice "Don’t do that!" message in such cases.
42
+
43
+ * It seems that the recent ability to handle dates that are only know at
44
+ decade-level precision isn’t actually enough, as we have some that we
45
+ only know at century-level precision! (For example, that the position of
46
+ Lord Chancellor of Ireland was created some time in the 12th Century.)
47
+ Such dates will now appear in a nicer format.
48
+
49
+ * Sometimes updating the table looks like something has changed, but
50
+ really the only difference is that a few of people who have no dates
51
+ are shuffled around a bit in the list. This is because we previously
52
+ only sorted by date order, so people with no dates were effectively in a
53
+ random order. Now we sort those people by ID too, which should minimise
54
+ the number of times an update will appear in your watchlist, only to
55
+ discover nothing significant actually happened.
56
+
57
+ # [1.7.0] 2020-09-08
58
+
59
+ ## Enchancements
60
+
61
+ * Yesterday’s future, when we said we’d do something a little better
62
+ with positions that have multiple inception or abolition dates, has
63
+ arrived. Now we display all of them (with a warning), rather than just
64
+ picking one semi-randomly.
65
+
66
+ ## Improvements
67
+
68
+ * A query like https://w.wiki/bVz is taking about 6 seconds to run.
69
+ Changing that to https://w.wiki/bW3 drops that to about half a second.
70
+ If you were to guess that the first has now been replaced by the
71
+ second, you’d be entirely correct.
72
+
73
+ # [1.6.0] 2020-09-07
74
+
75
+ ## Enhancements
76
+
77
+ * If a position has an inception date and/or abolition date, those will
78
+ now also be displayed. (If a position has more than one of either of
79
+ those — which really shouldn’t happen, but sometimes does — then the
80
+ behaviour may not be particularly sensible. Later evolutions of this
81
+ feature will hopefully handle that better.)
82
+
83
+ ## Fixes
84
+
85
+ * Previously, any warnings would be displayed at the bottom of the page,
86
+ which was fine if this table was the only thing on the page, but would
87
+ be slightly odd if there was other discussion after it. Now the
88
+ footnotes are explicitly displayed immediately after the table.
89
+
3
90
  # [1.5.0] 2020-09-06
4
91
 
5
92
  ## Enhancements
6
93
 
7
94
  * When showing the results for a position from long long ago (such as
8
95
  the High Kings of Ireland), display the dates as "862 – 879" not as
9
- "862879"
96
+ "08620879"
10
97
  * If we only know that someone took (or left) office sometime in a given
11
98
  decade (i.e. at date precision 8), display that as (say) "1930s"
12
99
 
@@ -37,6 +37,10 @@ module QueryService
37
37
  @url = url
38
38
  end
39
39
 
40
+ def eql?(other)
41
+ id == other.id
42
+ end
43
+
40
44
  def id
41
45
  url.split('/').last unless url.to_s.empty?
42
46
  end
@@ -45,6 +49,10 @@ module QueryService
45
49
  "{{Q|#{id}}}" if id
46
50
  end
47
51
 
52
+ def qlink_i
53
+ "''{{Q|#{id}}}''" if id
54
+ end
55
+
48
56
  private
49
57
 
50
58
  attr_reader :url
@@ -54,7 +62,7 @@ module QueryService
54
62
  class WikidataDate
55
63
  include Comparable
56
64
 
57
- DATELEN = { '11' => 10, '10' => 7, '9' => 4, '8' => 4 }.freeze
65
+ DATELEN = { '11' => 10, '10' => 7, '9' => 4, '8' => 4, '7' => 2 }.freeze
58
66
 
59
67
  def initialize(str, precision)
60
68
  @str = str
@@ -105,6 +113,7 @@ module QueryService
105
113
  end
106
114
 
107
115
  def precisioned_string
116
+ return "#{truncated_string}. century" if precision == '7'
108
117
  return "#{truncated_string}s" if precision == '8'
109
118
 
110
119
  truncated_string
@@ -7,7 +7,7 @@ module WikidataPositionHistory
7
7
  # This is distinct from the mandate query itself to avoid complex
8
8
  # GROUP BY scenarios where people have multiple values for
9
9
  # biographical properties.
10
- class BioData < ItemQuery
10
+ class BioQuery < ItemQuery
11
11
  def raw_sparql
12
12
  <<~SPARQL
13
13
  # holder-biodata
@@ -24,13 +24,9 @@ module WikidataPositionHistory
24
24
  end
25
25
 
26
26
  # Represents a single row returned from the Position query
27
- class BioData
28
- def initialize(row)
29
- @row = row
30
- end
31
-
27
+ class BioRow < SPARQL::QueryRow
32
28
  def person
33
- QueryService::WikidataItem.new(row.dig(:item, :value))
29
+ item_from(:item)
34
30
  end
35
31
 
36
32
  def image_title
@@ -47,10 +43,8 @@ module WikidataPositionHistory
47
43
 
48
44
  private
49
45
 
50
- attr_reader :row
51
-
52
46
  def image_url
53
- row.dig(:image, :value)
47
+ raw(:image)
54
48
  end
55
49
  end
56
50
  end
@@ -4,7 +4,7 @@ require 'erb'
4
4
 
5
5
  module WikidataPositionHistory
6
6
  module SPARQL
7
- # Turn raw SPARQL into result objects
7
+ # Abstract class to turn raw SPARQL into result objects
8
8
  class ItemQuery
9
9
  def initialize(itemid)
10
10
  @itemid = itemid
@@ -23,12 +23,45 @@ module WikidataPositionHistory
23
23
  attr_reader :itemid
24
24
 
25
25
  def sparql
26
- raw_sparql % itemid
26
+ raw_sparql % sparql_args
27
+ end
28
+
29
+ def sparql_args
30
+ itemid
27
31
  end
28
32
 
29
33
  def json
30
34
  @json ||= QueryService::Query.new(sparql).results
31
35
  end
32
36
  end
37
+
38
+ # Abstract class to represents a single row returned from a query
39
+ class QueryRow
40
+ def initialize(row)
41
+ @row = row
42
+ end
43
+
44
+ protected
45
+
46
+ attr_reader :row
47
+
48
+ def item_from(key)
49
+ value = raw(key)
50
+ return if value.to_s.empty?
51
+
52
+ QueryService::WikidataItem.new(value)
53
+ end
54
+
55
+ def date_from(key, precision_key)
56
+ trunc = raw(key).to_s[0..9]
57
+ return if trunc.empty?
58
+
59
+ QueryService::WikidataDate.new(trunc, raw(precision_key))
60
+ end
61
+
62
+ def raw(key)
63
+ row.dig(key, :value)
64
+ end
65
+ end
33
66
  end
34
67
  end
@@ -3,7 +3,7 @@
3
3
  module WikidataPositionHistory
4
4
  module SPARQL
5
5
  # SPARQL for fetching all officeholdings of a position
6
- class Mandates < ItemQuery
6
+ class MandatesQuery < ItemQuery
7
7
  def raw_sparql
8
8
  <<~SPARQL
9
9
  # position-mandates
@@ -21,24 +21,20 @@ module WikidataPositionHistory
21
21
  OPTIONAL { ?posn pq:P1545 ?ordinal }
22
22
  OPTIONAL { ?posn pq:P5102 ?nature }
23
23
  }
24
- ORDER BY DESC(?start_date)
24
+ ORDER BY DESC(?start_date) ?item
25
25
  SPARQL
26
26
  end
27
27
  end
28
28
  end
29
29
 
30
30
  # Represents a single row returned from the Mandates query
31
- class Mandate
32
- def initialize(row)
33
- @row = row
34
- end
35
-
31
+ class MandateRow < SPARQL::QueryRow
36
32
  def ordinal
37
- row.dig(:ordinal, :value)
33
+ raw(:ordinal)
38
34
  end
39
35
 
40
36
  def officeholder
41
- QueryService::WikidataItem.new(row.dig(:item, :value))
37
+ item_from(:item)
42
38
  end
43
39
 
44
40
  # TODO: rename or remove. 'item' is meaningless/ambiguous
@@ -46,14 +42,17 @@ module WikidataPositionHistory
46
42
  officeholder.qlink
47
43
  end
48
44
 
45
+ # TODO: switch to item_from
49
46
  def prev
50
47
  QueryService::WikidataItem.new(row.dig(:prev, :value)).qlink
51
48
  end
52
49
 
50
+ # TODO: switch to item_from
53
51
  def next
54
52
  QueryService::WikidataItem.new(row.dig(:next, :value)).qlink
55
53
  end
56
54
 
55
+ # TODO: switch to item_from
57
56
  def nature
58
57
  QueryService::WikidataItem.new(row.dig(:nature, :value)).id
59
58
  end
@@ -63,35 +62,11 @@ module WikidataPositionHistory
63
62
  end
64
63
 
65
64
  def start_date
66
- return if start_date_raw.empty?
67
-
68
- QueryService::WikidataDate.new(start_date_raw, start_date_precision)
65
+ date_from(:start_date, :start_precision)
69
66
  end
70
67
 
71
68
  def end_date
72
- return if end_date_raw.empty?
73
-
74
- QueryService::WikidataDate.new(end_date_raw, end_date_precision)
75
- end
76
-
77
- def start_date_raw
78
- row.dig(:start_date, :value).to_s[0..9]
79
- end
80
-
81
- def end_date_raw
82
- row.dig(:end_date, :value).to_s[0..9]
83
- end
84
-
85
- def start_date_precision
86
- row.dig(:start_precision, :value)
69
+ date_from(:end_date, :end_precision)
87
70
  end
88
-
89
- def end_date_precision
90
- row.dig(:end_precision, :value)
91
- end
92
-
93
- private
94
-
95
- attr_reader :row
96
71
  end
97
72
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WikidataPositionHistory
4
+ module SPARQL
5
+ # SPARQL for fetching metadata about a position
6
+ class PositionQuery < ItemQuery
7
+ def raw_sparql
8
+ <<~SPARQL
9
+ # position-metadata
10
+
11
+ SELECT DISTINCT ?item ?inception ?inception_precision ?abolition ?abolition_precision
12
+ ?replaces ?replacedBy ?derivedReplaces ?derivedReplacedBy
13
+ ?isPosition ?isLegislator
14
+ WHERE {
15
+ VALUES ?item { wd:%s }
16
+ BIND(EXISTS { wd:%s wdt:P279+ wd:Q4164871 } as ?isPosition)
17
+ BIND(EXISTS { wd:%s wdt:P279+ wd:Q4175034 } as ?isLegislator)
18
+ OPTIONAL { ?item p:P571 [ a wikibase:BestRank ;
19
+ psv:P571 [ wikibase:timeValue ?inception; wikibase:timePrecision ?inception_precision ]
20
+ ] }
21
+ OPTIONAL { ?item p:P576 [ a wikibase:BestRank ;
22
+ psv:P576 [ wikibase:timeValue ?abolition; wikibase:timePrecision ?abolition_precision ]
23
+ ] }
24
+ OPTIONAL { ?item wdt:P1365 ?replaces }
25
+ OPTIONAL { ?item wdt:P1366 ?replacedBy }
26
+ OPTIONAL { ?derivedReplaces wdt:P1366 ?item }
27
+ OPTIONAL { ?derivedReplacedBy wdt:P1365 ?item }
28
+ }
29
+ SPARQL
30
+ end
31
+
32
+ def sparql_args
33
+ [itemid] * 3
34
+ end
35
+ end
36
+ end
37
+
38
+ # Represents a single row returned from the Position query
39
+ class PositionRow < SPARQL::QueryRow
40
+ def item
41
+ item_from(:item)
42
+ end
43
+
44
+ def inception_date
45
+ date_from(:inception, :inception_precision)
46
+ end
47
+
48
+ def abolition_date
49
+ date_from(:abolition, :abolition_precision)
50
+ end
51
+
52
+ def replaces
53
+ item_from(:replaces)
54
+ end
55
+
56
+ def replaced_by
57
+ item_from(:replacedBy)
58
+ end
59
+
60
+ def derived_replaces
61
+ item_from(:derivedReplaces)
62
+ end
63
+
64
+ def derived_replaced_by
65
+ item_from(:derivedReplacedBy)
66
+ end
67
+
68
+ def position?
69
+ raw(:isPosition) == 'true'
70
+ end
71
+
72
+ def legislator?
73
+ raw(:isLegislator) == 'true'
74
+ end
75
+ end
76
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  require 'query_service'
4
4
  require 'sparql/item_query'
5
- require 'sparql/position_data'
6
- require 'sparql/bio_data'
7
- require 'sparql/mandates'
5
+ require 'sparql/position_query'
6
+ require 'sparql/bio_query'
7
+ require 'sparql/mandates_query'
8
8
  require 'wikidata_position_history/checks'
9
9
  require 'wikidata_position_history/template'
10
+ require 'wikidata_position_history/output_row'
10
11
  require 'wikidata_position_history/report'
11
12
  require 'wikidata_position_history/version'
12
13
 
@@ -25,11 +25,11 @@ module WikidataPositionHistory
25
25
  current.prev
26
26
  end
27
27
 
28
- def latest_holder?
28
+ def later_holder?
29
29
  !!later
30
30
  end
31
31
 
32
- def earliest_holder?
32
+ def earlier_holder?
33
33
  !!earlier
34
34
  end
35
35
  end
@@ -92,7 +92,7 @@ module WikidataPositionHistory
92
92
  # Does the 'replaces' match the previous item in the list?
93
93
  class WrongPredecessor < Check
94
94
  def problem?
95
- earliest_holder? && !!predecessor && (earlier.item != predecessor)
95
+ earlier_holder? && !!predecessor && (earlier.item != predecessor)
96
96
  end
97
97
 
98
98
  def headline
@@ -104,10 +104,25 @@ module WikidataPositionHistory
104
104
  end
105
105
  end
106
106
 
107
+ # Is there a 'replaces' but no previous item in the list?
108
+ class MissingPredecessor < Check
109
+ def problem?
110
+ predecessor && !earlier_holder?
111
+ end
112
+
113
+ def headline
114
+ 'Inconsistent predecessor'
115
+ end
116
+
117
+ def possible_explanation
118
+ "#{current.item} has a {{P|1365}} of #{predecessor}, but does not follow anyone here"
119
+ end
120
+ end
121
+
107
122
  # Does the 'replaced by' match the next item in the list?
108
123
  class WrongSuccessor < Check
109
124
  def problem?
110
- latest_holder? && !!successor && (later.item != successor)
125
+ later_holder? && !!successor && (later.item != successor)
111
126
  end
112
127
 
113
128
  def headline
@@ -119,6 +134,21 @@ module WikidataPositionHistory
119
134
  end
120
135
  end
121
136
 
137
+ # Is there a 'replaced by' but no next item in the list?
138
+ class MissingSuccessor < Check
139
+ def problem?
140
+ successor && !later_holder?
141
+ end
142
+
143
+ def headline
144
+ 'Inconsistent successor'
145
+ end
146
+
147
+ def possible_explanation
148
+ "#{current.item} has a {{P|1366}} of #{successor}, but is not followed by anyone here"
149
+ end
150
+ end
151
+
122
152
  # Does the end date overlap with the successor's start date?
123
153
  class Overlap < Check
124
154
  def problem?
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WikidataPositionHistory
4
+ # simplified version of a WikidataPositionHistory::Check
5
+ Warning = Struct.new(:headline, :explanation)
6
+
7
+ class OutputRow
8
+ # Date for a single mandate row, to be passed to the report template
9
+ class Mandate
10
+ CHECKS = [Check::MissingFields, Check::Overlap,
11
+ Check::WrongPredecessor, Check::MissingPredecessor,
12
+ Check::WrongSuccessor, Check::MissingSuccessor].freeze
13
+
14
+ def initialize(later, current, earlier)
15
+ @later = later
16
+ @current = current
17
+ @earlier = earlier
18
+ end
19
+
20
+ def ordinal_string
21
+ ordinal = current.ordinal or return ''
22
+ "#{ordinal}."
23
+ end
24
+
25
+ def person
26
+ current.item
27
+ end
28
+
29
+ def dates
30
+ dates = [current.start_date, current.end_date]
31
+ return '' if dates.compact.empty?
32
+
33
+ dates.join(' – ')
34
+ end
35
+
36
+ def acting?
37
+ current.acting?
38
+ end
39
+
40
+ def warnings
41
+ CHECKS.map { |klass| klass.new(later, current, earlier) }.select(&:problem?)
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :later, :current, :earlier
47
+ end
48
+
49
+ # Base class for the Inception/Abolition date rows
50
+ class PositionDate
51
+ def initialize(metadata)
52
+ @metadata = metadata
53
+ end
54
+
55
+ def date
56
+ return if dates.empty?
57
+
58
+ dates.join(' / ')
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :metadata
64
+
65
+ def qlink
66
+ metadata.position.qlink
67
+ end
68
+ end
69
+
70
+ # Data for the Inception date of the position
71
+ class Inception < PositionDate
72
+ def warnings
73
+ count = dates.count
74
+ return [] if count == 1
75
+
76
+ return [Warning.new('Missing field', "#{qlink} is missing {{P|571}}")] if count.zero?
77
+
78
+ [Warning.new('Multiple values', "#{qlink} has more than one {{P|571}}")]
79
+ end
80
+
81
+ private
82
+
83
+ def dates
84
+ metadata.inception_dates
85
+ end
86
+ end
87
+
88
+ # Data for the Abolition date of the position
89
+ class Abolition < PositionDate
90
+ def warnings
91
+ return [] unless dates.count > 1
92
+
93
+ [Warning.new('Multiple values', "#{qlink} has more than one {{P|576}}")]
94
+ end
95
+
96
+ private
97
+
98
+ def dates
99
+ metadata.abolition_dates
100
+ end
101
+ end
102
+
103
+ # Data for related position: e.g. Successor/Predecessor
104
+ class RelatedPosition
105
+ def initialize(metadata)
106
+ @metadata = metadata
107
+ end
108
+
109
+ def position
110
+ return if implied_list.empty?
111
+
112
+ (implied_list.direct.map(&:qlink) + implied_list.indirect_only.map(&:qlink_i)).join(', ')
113
+ end
114
+
115
+ def warnings
116
+ implied_list.indirect_only.map do |from|
117
+ Warning.new('Indirect only', "{{PositionHolderHistory/#{indirect_warning_template}|from=#{from.id}|to=#{metadata.position.id}}}")
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ attr_reader :metadata
124
+
125
+ def indirect_warning_template
126
+ format('warning_indirect_%s', self.class.name.split('::').last.downcase)
127
+ end
128
+ end
129
+
130
+ # Data for the position that comes after this one
131
+ class Successor < RelatedPosition
132
+ def implied_list
133
+ metadata.replaced_by_combined
134
+ end
135
+ end
136
+
137
+ # Data for the position that came before this one
138
+ class Predecessor < RelatedPosition
139
+ def implied_list
140
+ metadata.replaces_combined
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,43 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WikidataPositionHistory
4
- # Date for a single mandate row, to be passed to the report template
5
- class MandateData
6
- CHECKS = [Check::MissingFields, Check::WrongPredecessor, Check::WrongSuccessor, Check::Overlap].freeze
4
+ # A list made up of both direct and indirect claims, where we
5
+ # can tell which came from which, when required
6
+ class ImpliedList
7
+ def initialize(direct, indirect)
8
+ @direct = direct
9
+ @indirect = indirect
10
+ end
7
11
 
8
- def initialize(later, current, earlier)
9
- @later = later
10
- @current = current
11
- @earlier = earlier
12
+ def empty?
13
+ all.empty?
12
14
  end
13
15
 
14
- def ordinal_string
15
- ordinal = current.ordinal or return ''
16
- "#{ordinal}."
16
+ def all
17
+ direct | indirect
17
18
  end
18
19
 
19
- def person
20
- current.item
20
+ def both
21
+ direct & indirect
21
22
  end
22
23
 
23
- def dates
24
- dates = [current.start_date, current.end_date]
25
- return '' if dates.compact.empty?
24
+ def direct_only
25
+ direct - indirect
26
+ end
27
+
28
+ def indirect_only
29
+ indirect - direct
30
+ end
31
+
32
+ attr_reader :direct, :indirect
33
+ end
34
+
35
+ # Data about the position itself, to be passed to the report template
36
+ class Metadata
37
+ def initialize(rows)
38
+ @rows = rows
39
+ end
40
+
41
+ def position
42
+ rows.map(&:item).first
43
+ end
26
44
 
27
- dates.join(' – ')
45
+ def predecessor
46
+ @predecessor ||= OutputRow::Predecessor.new(self)
28
47
  end
29
48
 
30
- def acting?
31
- current.acting?
49
+ def successor
50
+ @successor ||= OutputRow::Successor.new(self)
32
51
  end
33
52
 
34
- def warnings
35
- CHECKS.map { |klass| klass.new(later, current, earlier) }.select(&:problem?)
53
+ def inception
54
+ @inception ||= OutputRow::Inception.new(self)
55
+ end
56
+
57
+ def abolition
58
+ @abolition ||= OutputRow::Abolition.new(self)
59
+ end
60
+
61
+ def position?
62
+ # this should be the same everywhere
63
+ rows.map(&:position?).first
64
+ end
65
+
66
+ def legislator?
67
+ # this should be the same everywhere
68
+ rows.map(&:legislator?).first
69
+ end
70
+
71
+ def replaces_combined
72
+ @replaces_combined ||= ImpliedList.new(uniq_by_id(:replaces), uniq_by_id(:derived_replaces))
73
+ end
74
+
75
+ def replaced_by_combined
76
+ @replaced_by_combined ||= ImpliedList.new(uniq_by_id(:replaced_by), uniq_by_id(:derived_replaced_by))
77
+ end
78
+
79
+ def inception_dates
80
+ rows.map(&:inception_date).compact.uniq(&:to_s).sort
81
+ end
82
+
83
+ def abolition_dates
84
+ rows.map(&:abolition_date).compact.uniq(&:to_s).sort
36
85
  end
37
86
 
38
87
  private
39
88
 
40
- attr_reader :later, :current, :earlier
89
+ attr_reader :rows
90
+
91
+ def uniq_by_id(method)
92
+ rows.map(&method).compact.uniq(&:id).sort_by(&:id)
93
+ end
41
94
  end
42
95
 
43
96
  # The entire wikitext generated for this report
@@ -50,9 +103,10 @@ module WikidataPositionHistory
50
103
  attr_reader :position_id, :template_class
51
104
 
52
105
  def wikitext
106
+ return legislator_template if metadata.legislator?
53
107
  return no_items_output if mandates.empty?
54
108
 
55
- output
109
+ template_class.new(template_params).output
56
110
  end
57
111
 
58
112
  def header
@@ -60,7 +114,7 @@ module WikidataPositionHistory
60
114
  end
61
115
 
62
116
  def position_dates
63
- dates = [metadata.inception_date, metadata.abolition_date]
117
+ dates = [metadata.inception.date, metadata.abolition.date]
64
118
  return '' if dates.compact.empty?
65
119
 
66
120
  format('(%s)', dates.join(' – '))
@@ -70,16 +124,22 @@ module WikidataPositionHistory
70
124
  [header, wikitext].join("\n")
71
125
  end
72
126
 
127
+ def template_params
128
+ {
129
+ metadata: metadata,
130
+ table_rows: table_rows,
131
+ sparql_url: sparql.wdqs_url,
132
+ }
133
+ end
134
+
73
135
  private
74
136
 
75
137
  def metadata
76
- # TODO: we might get more than one response, if a position has
77
- # multiple dates
78
- @metadata ||= SPARQL::PositionData.new(position_id).results_as(PositionData).first
138
+ @metadata ||= Metadata.new(SPARQL::PositionQuery.new(position_id).results_as(PositionRow))
79
139
  end
80
140
 
81
141
  def biodata
82
- @biodata ||= SPARQL::BioData.new(position_id).results_as(BioData)
142
+ @biodata ||= SPARQL::BioQuery.new(position_id).results_as(BioRow)
83
143
  end
84
144
 
85
145
  def biodata_for(officeholder)
@@ -91,32 +151,25 @@ module WikidataPositionHistory
91
151
  end
92
152
 
93
153
  def sparql
94
- @sparql ||= SPARQL::Mandates.new(position_id)
154
+ @sparql ||= SPARQL::MandatesQuery.new(position_id)
95
155
  end
96
156
 
97
157
  def mandates
98
- @mandates ||= sparql.results_as(Mandate)
158
+ @mandates ||= sparql.results_as(MandateRow)
99
159
  end
100
160
 
101
161
  def no_items_output
102
162
  "\n{{PositionHolderHistory/error_no_holders|id=#{position_id}}}\n"
103
163
  end
104
164
 
105
- def output
106
- template_class.new(template_params).output
107
- end
108
-
109
- def template_params
110
- {
111
- table_rows: table_rows,
112
- sparql_url: sparql.wdqs_url,
113
- }
165
+ def legislator_template
166
+ "\n{{PositionHolderHistory/error_legislator|id=#{position_id}}}\n"
114
167
  end
115
168
 
116
169
  def table_rows
117
170
  padded_mandates.each_cons(3).map do |later, current, earlier|
118
171
  {
119
- mandate: MandateData.new(later, current, earlier),
172
+ mandate: OutputRow::Mandate.new(later, current, earlier),
120
173
  bio: biodata_for(current.officeholder),
121
174
  }
122
175
  end
@@ -16,22 +16,74 @@ module WikidataPositionHistory
16
16
  attr_reader :data
17
17
 
18
18
  def template
19
- @template ||= ERB.new(template_text)
19
+ @template ||= ERB.new(template_text, nil, '-')
20
20
  end
21
21
 
22
22
  def template_text
23
23
  <<~ERB
24
24
  {| class="wikitable" style="text-align: center; border: none;"
25
- <% table_rows.map(&:values).each do |mandate, bio| %>|-
25
+ <% if metadata.abolition.date -%>
26
+ |-
27
+ | colspan="2" style="border: none; background: #fff; font-size: 1.15em; text-align: right;" | '''Position abolished''':
28
+ | style="border: none; background: #fff; text-align: left;" | <%= metadata.abolition.date %>
29
+ | style="border: none; background: #fff; text-align: left;" | \
30
+ <% metadata.abolition.warnings.each do |warning| -%>
31
+ <span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]]&nbsp;<span style="color: #d33; font-weight: bold; vertical-align: middle;"><%= warning.headline %></span>&nbsp;<ref><%= warning.explanation %></ref></span>\
32
+ <% end %>
33
+ <% end -%>
34
+ <% if metadata.successor.position -%>
35
+ |-
36
+ | colspan="2" style="border: none; background: #fff; font-size: 1.15em; text-align: right;" | '''Replaced by''':
37
+ | style=" border: none; background: #fff; text-align: left;" | <%= metadata.successor.position %>
38
+ | style=" border: none; background: #fff; text-align: left;" | \
39
+ <% metadata.successor.warnings.each do |warning| -%>
40
+ <span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]]&nbsp;<span style="color: #d33; font-weight: bold; vertical-align: middle;"><%= warning.headline %></span>&nbsp;<ref><%= warning.explanation %></ref></span>\
41
+ <% end %>
42
+ <% end -%>
43
+ <% if metadata.successor.position || metadata.abolition.date -%>
44
+ |-
45
+ | colspan="3" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
46
+ | colspan="1" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
47
+ <% end -%>
48
+ <% table_rows.map(&:values).each do |mandate, bio| -%>
49
+ |-
26
50
  | style="padding:0.5em 2em" | <%= mandate.ordinal_string %>
27
51
  | style="padding:0.5em 2em" | <%= bio.map(&:image_link).first %>
28
52
  | style="padding:0.5em 2em" | <span style="font-size: <%= mandate.acting? ? '1.25em; font-style: italic;' : '1.5em' %>; display: block;"><%= mandate.person %></span> <%= mandate.dates %>
29
- | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | <% mandate.warnings.each do |warning| %><span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]]&nbsp;<span style="color: #d33; font-weight: bold; vertical-align: middle;"><%= warning.headline %></span>&nbsp;<ref><%= warning.explanation %></ref></span><% end %>
30
- <% end %>|}
53
+ | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | \
54
+ <% mandate.warnings.each do |warning| -%>
55
+ <span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]]&nbsp;<span style="color: #d33; font-weight: bold; vertical-align: middle;"><%= warning.headline %></span>&nbsp;<ref><%= warning.explanation %></ref></span>\
56
+ <% end %>
57
+ <% end -%>
58
+ <% if metadata.successor.position || metadata.abolition.date -%>
59
+ |-
60
+ | colspan="3" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
61
+ | colspan="1" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
62
+ <% end -%>
63
+ <% if metadata.inception.date -%>
64
+ |-
65
+ | colspan="2" style="border: none; background: #fff; font-size: 1.15em; text-align: right;" | '''Position created''':
66
+ | style="border: none; background: #fff; text-align: left;" | <%= metadata.inception.date %>
67
+ | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | \
68
+ <% metadata.inception.warnings.each do |warning| -%>
69
+ <span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]]&nbsp;<span style="color: #d33; font-weight: bold; vertical-align: middle;"><%= warning.headline %></span>&nbsp;<ref><%= warning.explanation %></ref></span>\
70
+ <% end %>
71
+ <% end -%>
72
+ <% if metadata.predecessor.position -%>
73
+ |-
74
+ | colspan="2" style=" border: none; background: #fff; font-size: 1.15em; text-align: right;" | '''Replaces''':
75
+ | style="border: none; background: #fff; text-align: left;" | <%= metadata.predecessor.position %>
76
+ | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | \
77
+ <% metadata.predecessor.warnings.each do |warning| -%>
78
+ <span style="display: block">[[File:Pictogram voting comment.svg|15px|link=]]&nbsp;<span style="color: #d33; font-weight: bold; vertical-align: middle;"><%= warning.headline %></span>&nbsp;<ref><%= warning.explanation %></ref></span>\
79
+ <% end %>
80
+ <% end -%>
81
+ |}
31
82
 
32
83
  <div style="margin-bottom:5px; border-bottom:3px solid #2f74d0; font-size:8pt">
33
84
  <div style="float:right">[<%= sparql_url %> WDQS]</div>
34
85
  </div>
86
+ {{reflist}}
35
87
  ERB
36
88
  end
37
89
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WikidataPositionHistory
4
- VERSION = '1.5.0'
4
+ VERSION = '1.10.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wikidata_position_history
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Bowden
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-09-06 00:00:00.000000000 Z
12
+ date: 2020-09-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mediawiki-replaceable-content
@@ -204,12 +204,13 @@ files:
204
204
  - exe/position-history-for-item
205
205
  - exe/update_wikidata_page
206
206
  - lib/query_service.rb
207
- - lib/sparql/bio_data.rb
207
+ - lib/sparql/bio_query.rb
208
208
  - lib/sparql/item_query.rb
209
- - lib/sparql/mandates.rb
210
- - lib/sparql/position_data.rb
209
+ - lib/sparql/mandates_query.rb
210
+ - lib/sparql/position_query.rb
211
211
  - lib/wikidata_position_history.rb
212
212
  - lib/wikidata_position_history/checks.rb
213
+ - lib/wikidata_position_history/output_row.rb
213
214
  - lib/wikidata_position_history/report.rb
214
215
  - lib/wikidata_position_history/template.rb
215
216
  - lib/wikidata_position_history/version.rb
@@ -1,62 +0,0 @@
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