wikidata_position_history 1.9.0 → 2.2.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: 6a2bc5a49d34a368363b7e92ec9e1b0d08b141d96babd15eccf2913d99e9b7b0
4
- data.tar.gz: 87594145f811917672fd4772722e086907d508479d73b43bfb21a0aac24e5d27
3
+ metadata.gz: 8f9b65df015932c64b9995789de132f401400b8641fe6da6d34529ee08a897f7
4
+ data.tar.gz: 8eb66f80771e638a76e477db20e48c20cf10aa69843e20660c49a995e7040126
5
5
  SHA512:
6
- metadata.gz: 6eb361e602c3a34c97bd8b584b4029135d9bb8e14c1f99b2ed513ce049f8df780c9b09ac31941c31a7ebea544d0a6cc0065cb2210bc4af4448de66cfb40800c0
7
- data.tar.gz: 1b07fb333e1f13d25985420ec6700f2b0dc81dc98a5c4a762713bcf8426c532b48e28f6d80646d7c5ea85692f63e921748a603708e4d9e7286f254bc8c665e21
6
+ metadata.gz: 84464bf1eb62fad5dfe9287baa7988ed1e27024b3dce9ceefd1b2a74a79b7ea4fe472151c9b04bc10ed8bba846694553e929ce6f0bd316cff86aee4f09806ec4
7
+ data.tar.gz: 1b8813db12384488d4719a6858fa31b3dd69b9692b2df9c62c4d861a7c46ab533715fc374bd5b7fd2441b07108257cd782369175318e89afdbb7f5cdea106945
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ # [2.2.0] 2020-09-18
4
+
5
+ ## Enhancements
6
+
7
+ * Every position came into existence at _some_ point, and so should have
8
+ a P571 inception date. If that's missing, a warning will now be
9
+ displayed.
10
+
11
+ ## Improvements
12
+
13
+ * When a position has more than one successor or prdecessor, those will
14
+ now be displayed as a proper Mediawiki list, rather than one long line
15
+ of text. As these are in a table cell, this should stop those growing
16
+ unnecessarily wide, and should generally make everything look a little
17
+ nicer.
18
+
19
+ # [2.1.0] 2020-09-16
20
+
21
+ ## Enhancements
22
+
23
+ * When displaying a list of members for a constituency, also include
24
+ a 'parliamentary group' (P4100) column, and if there's a
25
+ 'parliamentary term' (P2937) qualifier, use it for the ordinal.
26
+
27
+ # [2.0.0] 2020-09-16
28
+
29
+ ## Interface change
30
+
31
+ * `Report#wikitext_with_header` has been removed. This was undocumented,
32
+ and only used internally, so should not be a breaking change, but if
33
+ anything *was* using it, that will now break loudly (but, usefully,
34
+ should also break very early.)
35
+
36
+ ## Enhancements
37
+
38
+ * A {{PositionHolderHistory}} template can now also be added to items
39
+ representing single-member constituencies, to see the history of
40
+ representatives for that seat.
41
+
42
+ # [1.11.0] 2020-09-14
43
+
44
+ ## Enhancements
45
+
46
+ * Wikidata now has a new {{QB}} template to link to _both_ an Item and
47
+ its Talk page. As the PositionHolderHistory template for a position
48
+ often lives on its Talk page, any link to a position item now uses
49
+ this template, for easier of access to that. (Requested by Tagishsimon:
50
+ https://twitter.com/Tagishsimon/status/1304363879322079233)
51
+
52
+ * The change to how warnings templates work from the last release (i.e.
53
+ using on-wiki templates, so they can be improved or translated, or used
54
+ to look for problems via backlinks) definitely seems to have been a good
55
+ idea, so now the warnings for having an unexpected number of inception
56
+ or abolition dates have also been adjusted to work the same way.
57
+
58
+ # [1.10.0] 2020-09-13
59
+
60
+ ## Enhancements
61
+
62
+ * If some other position has a "replaces" (P1365) or "replaced by"
63
+ (P1366) pointing to _this_ position, but this position doesn't have
64
+ the reciprocal inverse claims back to that one, include them as a
65
+ successor/predecessor, but warn that it’s only an indirect connection.
66
+
67
+ * The warnings in the above case now use an on-wiki template for their
68
+ text. This means they can be translated into other languages, and also
69
+ means that backlinks to these templates, via WhatLinksHere, can act as
70
+ a TODO list. The other warnings will be migrated to this approach Real
71
+ Soon Now™.
72
+
3
73
  # [1.9.0] 2020-09-11
4
74
 
5
75
  ## Enhancements
@@ -8,4 +8,4 @@ if ARGV.size != 1
8
8
  e.g. #{$PROGRAM_NAME} Q14211'"
9
9
  end
10
10
 
11
- puts WikidataPositionHistory::Report.new(ARGV.first).wikitext_with_header
11
+ puts WikidataPositionHistory::Report.new(ARGV.first).wikitext
@@ -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,18 @@ module QueryService
45
49
  "{{Q|#{id}}}" if id
46
50
  end
47
51
 
52
+ def qblink
53
+ "{{QB|#{id}}}" if id
54
+ end
55
+
56
+ def qlink_i
57
+ "''#{qlink}''" if qlink
58
+ end
59
+
60
+ def qblink_i
61
+ "''#{qblink}''" if qblink
62
+ end
63
+
48
64
  private
49
65
 
50
66
  attr_reader :url
@@ -21,16 +21,28 @@ module WikidataPositionHistory
21
21
  SPARQL
22
22
  end
23
23
  end
24
- end
25
24
 
26
- # Represents a single row returned from the Position query
27
- class BioRow
28
- def initialize(row)
29
- @row = row
25
+ # Biographical data for Members for a Constituency
26
+ class ConstituencyBioQuery < ItemQuery
27
+ def raw_sparql
28
+ <<~SPARQL
29
+ # constituency-biodata
30
+
31
+ SELECT DISTINCT ?item ?image
32
+ WHERE {
33
+ ?item wdt:P31 wd:Q5 ; p:P39/pq:P768 wd:%s .
34
+ OPTIONAL { ?item wdt:P18 ?image }
35
+ }
36
+ ORDER BY ?item
37
+ SPARQL
38
+ end
30
39
  end
40
+ end
31
41
 
42
+ # Represents a single row returned from the Position query
43
+ class BioRow < SPARQL::QueryRow
32
44
  def person
33
- QueryService::WikidataItem.new(row.dig(:item, :value))
45
+ item_from(:item)
34
46
  end
35
47
 
36
48
  def image_title
@@ -47,10 +59,8 @@ module WikidataPositionHistory
47
59
 
48
60
  private
49
61
 
50
- attr_reader :row
51
-
52
62
  def image_url
53
- row.dig(:image, :value)
63
+ raw(:image)
54
64
  end
55
65
  end
56
66
  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
@@ -34,5 +34,34 @@ module WikidataPositionHistory
34
34
  @json ||= QueryService::Query.new(sparql).results
35
35
  end
36
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
37
66
  end
38
67
  end
@@ -25,35 +25,60 @@ module WikidataPositionHistory
25
25
  SPARQL
26
26
  end
27
27
  end
28
- end
29
28
 
30
- # Represents a single row returned from the Mandates query
31
- class MandateRow
32
- def initialize(row)
33
- @row = row
29
+ # SPARQL for fetching all mandates of a single-member district
30
+ class ConstituencyMandatesQuery < ItemQuery
31
+ def raw_sparql
32
+ <<~SPARQL
33
+ # constituency-mandates
34
+
35
+ SELECT DISTINCT ?ordinal ?item ?start_date ?start_precision ?end_date ?end_precision ?party ?prev ?next ?term
36
+ WHERE {
37
+ ?item wdt:P31 wd:Q5 ; p:P39 ?posn .
38
+ ?posn pq:P768 wd:%s .
39
+ FILTER NOT EXISTS { ?posn wikibase:rank wikibase:DeprecatedRank }
40
+
41
+ OPTIONAL { ?posn pqv:P580 [ wikibase:timeValue ?start_date; wikibase:timePrecision ?start_precision ] }
42
+ OPTIONAL { ?posn pqv:P582 [ wikibase:timeValue ?end_date; wikibase:timePrecision ?end_precision ] }
43
+ OPTIONAL { ?posn pq:P1365 ?prev }
44
+ OPTIONAL { ?posn pq:P1366 ?next }
45
+ OPTIONAL { ?posn pq:P4100 ?party }
46
+ OPTIONAL {
47
+ ?posn pq:P2937 ?term .
48
+ OPTIONAL { ?term p:P31/pq:P1545 ?ordinal }
49
+ }
50
+ }
51
+ ORDER BY DESC(?start_date) ?item
52
+ SPARQL
53
+ end
34
54
  end
55
+ end
35
56
 
57
+ # Represents a single row returned from the Mandates query
58
+ class MandateRow < SPARQL::QueryRow
36
59
  def ordinal
37
- row.dig(:ordinal, :value)
60
+ raw(:ordinal)
38
61
  end
39
62
 
40
63
  def officeholder
41
- QueryService::WikidataItem.new(row.dig(:item, :value))
64
+ item_from(:item)
42
65
  end
43
66
 
44
- # TODO: rename or remove. 'item' is meaningless/ambiguous
45
- def item
46
- officeholder.qlink
67
+ def party
68
+ item_from(:party)
47
69
  end
48
70
 
71
+ # TODO: switch to item_from
49
72
  def prev
50
73
  QueryService::WikidataItem.new(row.dig(:prev, :value)).qlink
51
74
  end
52
75
 
76
+ # TODO: switch to item_from
53
77
  def next
54
78
  QueryService::WikidataItem.new(row.dig(:next, :value)).qlink
55
79
  end
56
80
 
81
+ # TODO: switch to item_from
57
82
  def nature
58
83
  QueryService::WikidataItem.new(row.dig(:nature, :value)).id
59
84
  end
@@ -63,35 +88,11 @@ module WikidataPositionHistory
63
88
  end
64
89
 
65
90
  def start_date
66
- return if start_date_raw.empty?
67
-
68
- QueryService::WikidataDate.new(start_date_raw, start_date_precision)
91
+ date_from(:start_date, :start_precision)
69
92
  end
70
93
 
71
94
  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)
95
+ date_from(:end_date, :end_precision)
87
96
  end
88
-
89
- def end_date_precision
90
- row.dig(:end_precision, :value)
91
- end
92
-
93
- private
94
-
95
- attr_reader :row
96
97
  end
97
98
  end
@@ -9,12 +9,15 @@ module WikidataPositionHistory
9
9
  # position-metadata
10
10
 
11
11
  SELECT DISTINCT ?item ?inception ?inception_precision ?abolition ?abolition_precision
12
- ?replaces ?replacedBy
12
+ ?replaces ?replacedBy ?derivedReplaces ?derivedReplacedBy
13
13
  ?isPosition ?isLegislator
14
+ ?isConstituency ?representative_count ?legislature
14
15
  WHERE {
15
16
  VALUES ?item { wd:%s }
16
17
  BIND(EXISTS { wd:%s wdt:P279+ wd:Q4164871 } as ?isPosition)
17
18
  BIND(EXISTS { wd:%s wdt:P279+ wd:Q4175034 } as ?isLegislator)
19
+ BIND(EXISTS { wd:%s wdt:P31/wdt:P279+ wd:Q192611 } as ?isConstituency)
20
+
18
21
  OPTIONAL { ?item p:P571 [ a wikibase:BestRank ;
19
22
  psv:P571 [ wikibase:timeValue ?inception; wikibase:timePrecision ?inception_precision ]
20
23
  ] }
@@ -23,84 +26,70 @@ module WikidataPositionHistory
23
26
  ] }
24
27
  OPTIONAL { ?item wdt:P1365 ?replaces }
25
28
  OPTIONAL { ?item wdt:P1366 ?replacedBy }
29
+ OPTIONAL { ?derivedReplaces wdt:P1366 ?item }
30
+ OPTIONAL { ?derivedReplacedBy wdt:P1365 ?item }
31
+
32
+ OPTIONAL { # if constituency
33
+ ?item p:P1410 [ a wikibase:BestRank ; ps:P1410 ?representative_count ; pq:P194 ?legislature ]
34
+ }
26
35
  }
27
36
  SPARQL
28
37
  end
29
38
 
30
39
  def sparql_args
31
- [itemid] * 3
40
+ [itemid] * 4
32
41
  end
33
42
  end
34
43
  end
35
44
 
36
45
  # Represents a single row returned from the Position query
37
- class PositionRow
38
- def initialize(row)
39
- @row = row
40
- end
41
-
46
+ class PositionRow < SPARQL::QueryRow
42
47
  def item
43
- QueryService::WikidataItem.new(row.dig(:item, :value))
48
+ item_from(:item)
44
49
  end
45
50
 
46
51
  def inception_date
47
- return if inception_date_raw.empty?
48
-
49
- QueryService::WikidataDate.new(inception_date_raw, inception_date_precision)
52
+ date_from(:inception, :inception_precision)
50
53
  end
51
54
 
52
55
  def abolition_date
53
- return if abolition_date_raw.empty?
54
-
55
- QueryService::WikidataDate.new(abolition_date_raw, abolition_date_precision)
56
+ date_from(:abolition, :abolition_precision)
56
57
  end
57
58
 
58
59
  def replaces
59
- return if replaces_raw.to_s.empty?
60
-
61
- QueryService::WikidataItem.new(replaces_raw)
60
+ item_from(:replaces)
62
61
  end
63
62
 
64
63
  def replaced_by
65
- return if replaced_by_raw.to_s.empty?
66
-
67
- QueryService::WikidataItem.new(replaced_by_raw)
64
+ item_from(:replacedBy)
68
65
  end
69
66
 
70
- def position?
71
- row.dig(:isPosition, :value) == 'true'
72
- end
73
-
74
- def legislator?
75
- row.dig(:isLegislator, :value) == 'true'
67
+ def derived_replaces
68
+ item_from(:derivedReplaces)
76
69
  end
77
70
 
78
- private
79
-
80
- attr_reader :row
81
-
82
- def replaces_raw
83
- row.dig(:replaces, :value)
71
+ def derived_replaced_by
72
+ item_from(:derivedReplacedBy)
84
73
  end
85
74
 
86
- def replaced_by_raw
87
- row.dig(:replacedBy, :value)
75
+ def position?
76
+ raw(:isPosition) == 'true'
88
77
  end
89
78
 
90
- def inception_date_raw
91
- row.dig(:inception, :value).to_s[0..9]
79
+ def legislator?
80
+ raw(:isLegislator) == 'true'
92
81
  end
93
82
 
94
- def abolition_date_raw
95
- row.dig(:abolition, :value).to_s[0..9]
83
+ def constituency?
84
+ raw(:isConstituency) == 'true'
96
85
  end
97
86
 
98
- def inception_date_precision
99
- row.dig(:inception_precision, :value)
87
+ def legislature
88
+ item_from(:legislature)
100
89
  end
101
90
 
102
- def abolition_date_precision
103
- row.dig(:abolition_precision, :value)
91
+ def representative_count
92
+ raw(:representative_count).to_i
104
93
  end
105
94
  end
106
95
  end
@@ -7,6 +7,7 @@ require 'sparql/bio_query'
7
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
 
@@ -17,11 +17,12 @@ module WikidataPositionHistory
17
17
 
18
18
  attr_reader :later, :current, :earlier
19
19
 
20
- def successor
20
+ # TODO: replace these with objects instead of strings
21
+ def successor_qlink
21
22
  current.next
22
23
  end
23
24
 
24
- def predecessor
25
+ def predecessor_qlink
25
26
  current.prev
26
27
  end
27
28
 
@@ -46,7 +47,7 @@ module WikidataPositionHistory
46
47
  end
47
48
 
48
49
  def possible_explanation
49
- "#{current.item} is missing #{missing.map { |field| "{{P|#{field_map[field]}}}" }.join(', ')}"
50
+ "#{current.officeholder.qlink} is missing #{missing.map { |field| "{{P|#{field_map[field]}}}" }.join(', ')}"
50
51
  end
51
52
 
52
53
  def missing
@@ -76,14 +77,14 @@ module WikidataPositionHistory
76
77
 
77
78
  def expect_prev?
78
79
  return unless earlier
79
- return if earlier.item == current.item # sucessive terms by same person
80
+ return if earlier.officeholder.id == current.officeholder.id # sucessive terms by same person
80
81
 
81
82
  !current.acting?
82
83
  end
83
84
 
84
85
  def expect_next?
85
86
  return unless later
86
- return if later.item == current.item # sucessive terms by same person
87
+ return if later.officeholder.id == current.officeholder.id # sucessive terms by same person
87
88
 
88
89
  !current.acting?
89
90
  end
@@ -92,7 +93,7 @@ module WikidataPositionHistory
92
93
  # Does the 'replaces' match the previous item in the list?
93
94
  class WrongPredecessor < Check
94
95
  def problem?
95
- earlier_holder? && !!predecessor && (earlier.item != predecessor)
96
+ earlier_holder? && !!predecessor_qlink && (earlier.officeholder.qlink != predecessor_qlink)
96
97
  end
97
98
 
98
99
  def headline
@@ -100,14 +101,14 @@ module WikidataPositionHistory
100
101
  end
101
102
 
102
103
  def possible_explanation
103
- "#{current.item} has a {{P|1365}} of #{predecessor}, but follows #{earlier.item} here"
104
+ "#{current.officeholder.qlink} has a {{P|1365}} of #{predecessor_qlink}, but follows #{earlier.officeholder.qlink} here"
104
105
  end
105
106
  end
106
107
 
107
108
  # Is there a 'replaces' but no previous item in the list?
108
109
  class MissingPredecessor < Check
109
110
  def problem?
110
- predecessor && !earlier_holder?
111
+ predecessor_qlink && !earlier_holder?
111
112
  end
112
113
 
113
114
  def headline
@@ -115,14 +116,14 @@ module WikidataPositionHistory
115
116
  end
116
117
 
117
118
  def possible_explanation
118
- "#{current.item} has a {{P|1365}} of #{predecessor}, but does not follow anyone here"
119
+ "#{current.officeholder.qlink} has a {{P|1365}} of #{predecessor_qlink}, but does not follow anyone here"
119
120
  end
120
121
  end
121
122
 
122
123
  # Does the 'replaced by' match the next item in the list?
123
124
  class WrongSuccessor < Check
124
125
  def problem?
125
- later_holder? && !!successor && (later.item != successor)
126
+ later_holder? && !!successor_qlink && (later.officeholder.qlink != successor_qlink)
126
127
  end
127
128
 
128
129
  def headline
@@ -130,14 +131,14 @@ module WikidataPositionHistory
130
131
  end
131
132
 
132
133
  def possible_explanation
133
- "#{current.item} has a {{P|1366}} of #{successor}, but is followed by #{later.item} here"
134
+ "#{current.officeholder.qlink} has a {{P|1366}} of #{successor_qlink}, but is followed by #{later.officeholder.qlink} here"
134
135
  end
135
136
  end
136
137
 
137
138
  # Is there a 'replaced by' but no next item in the list?
138
139
  class MissingSuccessor < Check
139
140
  def problem?
140
- successor && !later_holder?
141
+ successor_qlink && !later_holder?
141
142
  end
142
143
 
143
144
  def headline
@@ -145,7 +146,7 @@ module WikidataPositionHistory
145
146
  end
146
147
 
147
148
  def possible_explanation
148
- "#{current.item} has a {{P|1366}} of #{successor}, but is not followed by anyone here"
149
+ "#{current.officeholder.qlink} has a {{P|1366}} of #{successor_qlink}, but is not followed by anyone here"
149
150
  end
150
151
  end
151
152
 
@@ -167,7 +168,8 @@ module WikidataPositionHistory
167
168
  end
168
169
 
169
170
  def possible_explanation
170
- "#{current.item} has a {{P|582}} of #{current.end_date}, which #{overlap_explanation} the {{P|580}} of #{later.start_date} for #{later.item}"
171
+ format('%s has a {{P|582}} of %s, which %s the {{P|580}} of %s for %s',
172
+ current.officeholder.qlink, current.end_date, overlap_explanation, later.start_date, later.officeholder.qlink)
171
173
  end
172
174
 
173
175
  protected
@@ -0,0 +1,150 @@
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 party
26
+ current.party
27
+ end
28
+
29
+ def officeholder
30
+ current.officeholder
31
+ end
32
+
33
+ def dates
34
+ dates = [current.start_date, current.end_date]
35
+ return '' if dates.compact.empty?
36
+
37
+ dates.join(' – ')
38
+ end
39
+
40
+ def acting?
41
+ current.acting?
42
+ end
43
+
44
+ def warnings
45
+ CHECKS.map { |klass| klass.new(later, current, earlier) }.select(&:problem?)
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :later, :current, :earlier
51
+ end
52
+
53
+ # Base class for the Inception/Abolition date rows
54
+ class PositionDate
55
+ def initialize(metadata)
56
+ @metadata = metadata
57
+ end
58
+
59
+ def date
60
+ return if dates.empty?
61
+
62
+ dates.join(' / ')
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :metadata
68
+
69
+ def position_id
70
+ metadata.position.id
71
+ end
72
+ end
73
+
74
+ # Data for the Inception date of the position
75
+ class Inception < PositionDate
76
+ def warnings
77
+ count = dates.count
78
+ return [] if count == 1
79
+ return [Warning.new('Missing field', "{{PositionHolderHistory/warning_no_inception_date|item=#{position_id}}}")] if count.zero?
80
+
81
+ [Warning.new('Multiple values', "{{PositionHolderHistory/warning_multiple_inception_dates|item=#{position_id}}}")]
82
+ end
83
+
84
+ private
85
+
86
+ def dates
87
+ metadata.inception_dates
88
+ end
89
+ end
90
+
91
+ # Data for the Abolition date of the position
92
+ class Abolition < PositionDate
93
+ def warnings
94
+ return [] unless dates.count > 1
95
+
96
+ [Warning.new('Multiple values', "{{PositionHolderHistory/warning_multiple_abolition_dates|item=#{position_id}}}")]
97
+ end
98
+
99
+ private
100
+
101
+ def dates
102
+ metadata.abolition_dates
103
+ end
104
+ end
105
+
106
+ # Data for related position: e.g. Successor/Predecessor
107
+ class RelatedPosition
108
+ def initialize(metadata)
109
+ @metadata = metadata
110
+ end
111
+
112
+ def position
113
+ return if implied_list.empty?
114
+
115
+ list = (implied_list.direct.map(&:qblink) + implied_list.indirect_only.map(&:qblink_i))
116
+ return list.first if list.count == 1
117
+
118
+ list.map { |item| "\n* #{item}" }.join
119
+ end
120
+
121
+ def warnings
122
+ implied_list.indirect_only.map do |from|
123
+ Warning.new('Indirect only', "{{PositionHolderHistory/#{indirect_warning_template}|from=#{from.id}|to=#{metadata.position.id}}}")
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ attr_reader :metadata
130
+
131
+ def indirect_warning_template
132
+ format('warning_indirect_%s', self.class.name.split('::').last.downcase)
133
+ end
134
+ end
135
+
136
+ # Data for the position that comes after this one
137
+ class Successor < RelatedPosition
138
+ def implied_list
139
+ metadata.replaced_by_combined
140
+ end
141
+ end
142
+
143
+ # Data for the position that came before this one
144
+ class Predecessor < RelatedPosition
145
+ def implied_list
146
+ metadata.replaces_combined
147
+ end
148
+ end
149
+ end
150
+ end
@@ -1,97 +1,61 @@
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::Overlap,
7
- Check::WrongPredecessor, Check::MissingPredecessor,
8
- Check::WrongSuccessor, Check::MissingSuccessor].freeze
9
-
10
- def initialize(later, current, earlier)
11
- @later = later
12
- @current = current
13
- @earlier = earlier
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
14
10
  end
15
11
 
16
- def ordinal_string
17
- ordinal = current.ordinal or return ''
18
- "#{ordinal}."
12
+ def empty?
13
+ all.empty?
19
14
  end
20
15
 
21
- def person
22
- current.item
16
+ def all
17
+ direct | indirect
23
18
  end
24
19
 
25
- def dates
26
- dates = [current.start_date, current.end_date]
27
- return '' if dates.compact.empty?
28
-
29
- dates.join(' – ')
20
+ def both
21
+ direct & indirect
30
22
  end
31
23
 
32
- def acting?
33
- current.acting?
24
+ def direct_only
25
+ direct - indirect
34
26
  end
35
27
 
36
- def warnings
37
- CHECKS.map { |klass| klass.new(later, current, earlier) }.select(&:problem?)
28
+ def indirect_only
29
+ indirect - direct
38
30
  end
39
31
 
40
- private
41
-
42
- attr_reader :later, :current, :earlier
32
+ attr_reader :direct, :indirect
43
33
  end
44
34
 
45
35
  # Data about the position itself, to be passed to the report template
46
36
  class Metadata
47
- # simplified version of a WikidataPositionHistory::Check
48
- Warning = Struct.new(:headline, :explanation)
49
-
50
37
  def initialize(rows)
51
38
  @rows = rows
52
39
  end
53
40
 
54
- def item
41
+ def position
55
42
  rows.map(&:item).first
56
43
  end
57
44
 
58
- def replaces
59
- return if replaces_list.empty?
60
-
61
- replaces_list.map(&:qlink).join(', ')
62
- end
63
-
64
- def replaced_by
65
- return if replaced_by_list.empty?
66
-
67
- replaced_by_list.map(&:qlink).join(', ')
45
+ def predecessor
46
+ @predecessor ||= OutputRow::Predecessor.new(self)
68
47
  end
69
48
 
70
- def inception_date
71
- return if inception_dates.empty?
72
-
73
- inception_dates.join(' / ')
74
- end
75
-
76
- def inception_warning
77
- count = inception_dates.count
78
-
79
- return if count == 1
80
- return Warning.new('Missing field', "#{item_qlink} is missing {{P|571}}") if count.zero?
81
-
82
- Warning.new('Multiple values', "#{item_qlink} has more than one {{P|571}}")
49
+ def successor
50
+ @successor ||= OutputRow::Successor.new(self)
83
51
  end
84
52
 
85
- def abolition_date
86
- return if abolition_dates.empty?
87
-
88
- abolition_dates.join(' / ')
53
+ def inception
54
+ @inception ||= OutputRow::Inception.new(self)
89
55
  end
90
56
 
91
- def abolition_warning
92
- return unless abolition_dates.count > 1
93
-
94
- Warning.new('Multiple values', "#{item_qlink} has more than one {{P|576}}")
57
+ def abolition
58
+ @abolition ||= OutputRow::Abolition.new(self)
95
59
  end
96
60
 
97
61
  def position?
@@ -104,16 +68,21 @@ module WikidataPositionHistory
104
68
  rows.map(&:legislator?).first
105
69
  end
106
70
 
107
- private
71
+ def constituency?
72
+ # this should be the same everywhere
73
+ rows.map(&:constituency?).first
74
+ end
108
75
 
109
- attr_reader :rows
76
+ def representative_count
77
+ rows.map(&:representative_count).max
78
+ end
110
79
 
111
- def replaces_list
112
- rows.map(&:replaces).compact.uniq(&:id).sort_by(&:id)
80
+ def replaces_combined
81
+ @replaces_combined ||= ImpliedList.new(uniq_by_id(:replaces), uniq_by_id(:derived_replaces))
113
82
  end
114
83
 
115
- def replaced_by_list
116
- rows.map(&:replaced_by).compact.uniq(&:id).sort_by(&:id)
84
+ def replaced_by_combined
85
+ @replaced_by_combined ||= ImpliedList.new(uniq_by_id(:replaced_by), uniq_by_id(:derived_replaced_by))
117
86
  end
118
87
 
119
88
  def inception_dates
@@ -124,8 +93,54 @@ module WikidataPositionHistory
124
93
  rows.map(&:abolition_date).compact.uniq(&:to_s).sort
125
94
  end
126
95
 
127
- def item_qlink
128
- item.qlink
96
+ private
97
+
98
+ attr_reader :rows
99
+
100
+ def uniq_by_id(method)
101
+ rows.map(&method).compact.uniq(&:id).sort_by(&:id)
102
+ end
103
+ end
104
+
105
+ # Construct the correct ReportConfig based on the position metadata
106
+ class ReportConfigFactory
107
+ def self.config(metadata)
108
+ return ReportConfig::Constituency.new if metadata.constituency?
109
+
110
+ ReportConfig::Position.new
111
+ end
112
+
113
+ private
114
+
115
+ attr_reader :metadata
116
+ end
117
+
118
+ # Encapsulates the different configuration for each type of position
119
+ module ReportConfig
120
+ # Configuration for 'default' single-holder position
121
+ class Position
122
+ def mandates_query
123
+ SPARQL::MandatesQuery
124
+ end
125
+
126
+ def biodata_query
127
+ SPARQL::BioQuery
128
+ end
129
+ end
130
+
131
+ # Configuration for representatives of a single-member constituency
132
+ class Constituency
133
+ def mandates_query
134
+ SPARQL::ConstituencyMandatesQuery
135
+ end
136
+
137
+ def biodata_query
138
+ SPARQL::ConstituencyBioQuery
139
+ end
140
+
141
+ def multimember_error_template
142
+ "\n{{PositionHolderHistory/error_multimember}}\n"
143
+ end
129
144
  end
130
145
  end
131
146
 
@@ -140,26 +155,12 @@ module WikidataPositionHistory
140
155
 
141
156
  def wikitext
142
157
  return legislator_template if metadata.legislator?
158
+ return config.multimember_error_template if metadata.constituency? && (metadata.representative_count != 1)
143
159
  return no_items_output if mandates.empty?
144
160
 
145
161
  template_class.new(template_params).output
146
162
  end
147
163
 
148
- def header
149
- "== {{Q|#{position_id}}} officeholders #{position_dates} =="
150
- end
151
-
152
- def position_dates
153
- dates = [metadata.inception_date, metadata.abolition_date]
154
- return '' if dates.compact.empty?
155
-
156
- format('(%s)', dates.join(' – '))
157
- end
158
-
159
- def wikitext_with_header
160
- [header, wikitext].join("\n")
161
- end
162
-
163
164
  def template_params
164
165
  {
165
166
  metadata: metadata,
@@ -175,7 +176,7 @@ module WikidataPositionHistory
175
176
  end
176
177
 
177
178
  def biodata
178
- @biodata ||= SPARQL::BioQuery.new(position_id).results_as(BioRow)
179
+ @biodata ||= biodata_sparql.results_as(BioRow)
179
180
  end
180
181
 
181
182
  def biodata_for(officeholder)
@@ -186,8 +187,16 @@ module WikidataPositionHistory
186
187
  [nil, mandates, nil].flatten(1)
187
188
  end
188
189
 
190
+ def config
191
+ @config ||= ReportConfigFactory.config(metadata)
192
+ end
193
+
189
194
  def sparql
190
- @sparql ||= SPARQL::MandatesQuery.new(position_id)
195
+ @sparql ||= config.mandates_query.new(position_id)
196
+ end
197
+
198
+ def biodata_sparql
199
+ config.biodata_query.new(position_id)
191
200
  end
192
201
 
193
202
  def mandates
@@ -205,7 +214,7 @@ module WikidataPositionHistory
205
214
  def table_rows
206
215
  padded_mandates.each_cons(3).map do |later, current, earlier|
207
216
  {
208
- mandate: MandateData.new(later, current, earlier),
217
+ mandate: OutputRow::Mandate.new(later, current, earlier),
209
218
  bio: biodata_for(current.officeholder),
210
219
  }
211
220
  end
@@ -22,22 +22,25 @@ module WikidataPositionHistory
22
22
  def template_text
23
23
  <<~ERB
24
24
  {| class="wikitable" style="text-align: center; border: none;"
25
- <% if metadata.abolition_date -%>
25
+ <% if metadata.abolition.date -%>
26
26
  |-
27
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 %>
28
+ | style="border: none; background: #fff; text-align: left;" | <%= metadata.abolition.date %>
29
29
  | style="border: none; background: #fff; text-align: left;" | \
30
- <% [metadata.abolition_warning].compact.each do |warning| -%>
30
+ <% metadata.abolition.warnings.each do |warning| -%>
31
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
32
  <% end %>
33
33
  <% end -%>
34
- <% if metadata.replaced_by -%>
34
+ <% if metadata.successor.position -%>
35
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.replaced_by %>
38
- | style=" border: none; background: #fff; text-align: left;" |
36
+ | colspan="2" style="border: none; background: #fff; font-size: 1.15em; vertical-align: baseline; text-align: right;" | '''Replaced by''':
37
+ | style=" border: none; background: #fff; vertical-align: baseline; 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 %>
39
42
  <% end -%>
40
- <% if metadata.replaced_by || metadata.abolition_date -%>
43
+ <% if metadata.successor.position || metadata.abolition.date -%>
41
44
  |-
42
45
  | colspan="3" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
43
46
  | colspan="1" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
@@ -46,31 +49,37 @@ module WikidataPositionHistory
46
49
  |-
47
50
  | style="padding:0.5em 2em" | <%= mandate.ordinal_string %>
48
51
  | style="padding:0.5em 2em" | <%= bio.map(&:image_link).first %>
49
- | 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 %>
52
+ | style="padding:0.5em 2em" | <span style="font-size: <%= mandate.acting? ? '1.25em; font-style: italic;' : '1.5em' %>; display: block;"><%= mandate.officeholder.qlink %></span> <%= mandate.dates %>
53
+ <% if metadata.constituency? -%>
54
+ | style="padding:0.5em 1em" | <% if mandate.party %><%= mandate.party.qlink %><% end %>
55
+ <% end -%>
50
56
  | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | \
51
57
  <% mandate.warnings.each do |warning| -%>
52
58
  <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>\
53
59
  <% end %>
54
60
  <% end -%>
55
- <% if metadata.replaced_by || metadata.abolition_date -%>
61
+ <% if metadata.successor.position || metadata.abolition.date -%>
56
62
  |-
57
63
  | colspan="3" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
58
64
  | colspan="1" style="padding:0.5em; border: none; background: #fff"> |&nbsp;
59
65
  <% end -%>
60
- <% if metadata.inception_date -%>
66
+ <% if metadata.inception.date || metadata.inception.warnings.any? -%>
61
67
  |-
62
68
  | colspan="2" style="border: none; background: #fff; font-size: 1.15em; text-align: right;" | '''Position created''':
63
- | style="border: none; background: #fff; text-align: left;" | <%= metadata.inception_date %>
69
+ | style="border: none; background: #fff; text-align: left;" | <%= metadata.inception.date %>
64
70
  | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | \
65
- <% [metadata.inception_warning].compact.each do |warning| -%>
71
+ <% metadata.inception.warnings.each do |warning| -%>
66
72
  <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>\
67
73
  <% end %>
68
74
  <% end -%>
69
- <% if metadata.replaces -%>
75
+ <% if metadata.predecessor.position -%>
70
76
  |-
71
77
  | colspan="2" style=" border: none; background: #fff; font-size: 1.15em; text-align: right;" | '''Replaces''':
72
- | style="border: none; background: #fff; text-align: left;" | <%= metadata.replaces %>
73
- | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" |
78
+ | style="border: none; background: #fff; text-align: left;" | <%= metadata.predecessor.position %>
79
+ | style="padding:0.5em 2em 0.5em 1em; border: none; background: #fff; text-align: left;" | \
80
+ <% metadata.predecessor.warnings.each do |warning| -%>
81
+ <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>\
82
+ <% end %>
74
83
  <% end -%>
75
84
  |}
76
85
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WikidataPositionHistory
4
- VERSION = '1.9.0'
4
+ VERSION = '2.2.0'
5
5
  end
@@ -36,5 +36,5 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency 'rubocop-performance', '~> 1.8.0'
37
37
  spec.add_development_dependency 'rubocop-rspec', '~> 1.43.2'
38
38
  spec.add_development_dependency 'warning', '~> 1.1'
39
- spec.add_development_dependency 'webmock', '~> 3.8.3'
39
+ spec.add_development_dependency 'webmock', '~> 3.9.1'
40
40
  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.9.0
4
+ version: 2.2.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-11 00:00:00.000000000 Z
12
+ date: 2020-09-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mediawiki-replaceable-content
@@ -171,14 +171,14 @@ dependencies:
171
171
  requirements:
172
172
  - - "~>"
173
173
  - !ruby/object:Gem::Version
174
- version: 3.8.3
174
+ version: 3.9.1
175
175
  type: :development
176
176
  prerelease: false
177
177
  version_requirements: !ruby/object:Gem::Requirement
178
178
  requirements:
179
179
  - - "~>"
180
180
  - !ruby/object:Gem::Version
181
- version: 3.8.3
181
+ version: 3.9.1
182
182
  description:
183
183
  email:
184
184
  - tony@tmtm.com
@@ -210,6 +210,7 @@ files:
210
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