-
Notifications
You must be signed in to change notification settings - Fork 482
/
prompt.rb
executable file
·356 lines (304 loc) · 13 KB
/
prompt.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
class Prompt < ApplicationRecord
include ActiveModel::ForbiddenAttributesProtection
# -1 represents all matching
ALL = -1
# number of checkbox options to keep visible by default in form
OPTIONS_TO_SHOW = 3
# maximum number of options to allow to be shown via checkboxes
MAX_OPTIONS_FOR_CHECKBOXES = 10
# ASSOCIATIONS
belongs_to :collection
belongs_to :pseud
has_one :user, through: :pseud
belongs_to :challenge_signup, touch: true, inverse_of: :prompts
belongs_to :tag_set, dependent: :destroy
accepts_nested_attributes_for :tag_set
has_many :tags, through: :tag_set
belongs_to :optional_tag_set, class_name: "TagSet", dependent: :destroy
accepts_nested_attributes_for :optional_tag_set
has_many :optional_tags, through: :optional_tag_set, source: :tag
has_many :request_claims, class_name: "ChallengeClaim", foreign_key: 'request_prompt_id'
# SCOPES
scope :claimed, -> { joins("INNER JOIN challenge_claims on prompts.id = challenge_claims.request_prompt_id") }
scope :in_collection, lambda {|collection| where(collection_id: collection.id) }
scope :unused, -> { where(used_up: false) }
scope :with_tag, lambda { |tag|
joins("JOIN set_taggings ON set_taggings.tag_set_id = prompts.tag_set_id").
where("set_taggings.tag_id = ?", tag.id)
}
# CALLBACKS
before_destroy :clear_claims
def clear_claims
# remove this prompt reference from any existing assignments
request_claims.each {|claim| claim.destroy}
end
# VALIDATIONS
validates_presence_of :collection_id
validates_presence_of :challenge_signup
before_save :set_pseud
def set_pseud
unless self.pseud
self.pseud = self.challenge_signup.pseud
end
true
end
# based on the prompt restriction
validates_presence_of :url, if: :url_required?
validates_presence_of :description, if: :description_required?
validates_presence_of :title, if: :title_required?
def url_required?
(restriction = get_prompt_restriction) && restriction.url_required
end
def description_required?
(restriction = get_prompt_restriction) && restriction.description_required
end
validates_length_of :description,
maximum: ArchiveConfig.NOTES_MAX,
too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.NOTES_MAX)
def title_required?
(restriction = get_prompt_restriction) && restriction.title_required
end
validates_length_of :title,
maximum: ArchiveConfig.TITLE_MAX,
too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.TITLE_MAX)
validates :url, url_format: {allow_blank: true} # we validate the presence above, conditionally
before_validation :cleanup_url
def cleanup_url
self.url = reformat_url(self.url) if self.url
end
validate :correct_number_of_tags
def correct_number_of_tags
prompt_type = self.class.name
restriction = get_prompt_restriction
if restriction
# make sure tagset has no more/less than the required/allowed number of tags of each type
TagSet::TAG_TYPES.each do |tag_type|
# get the tags of this type the user has specified
taglist = tag_set ? eval("tag_set.#{tag_type}_taglist") : []
tag_count = taglist.count
# check if user has chosen the "Any" option
if self.send("any_#{tag_type}")
if tag_count > 0
errors.add(:base, ts("^You have specified tags for %{tag_type} in your %{prompt_type} but also chose 'Any,' which will override them! Please only choose one or the other.",
tag_type: tag_type, prompt_type: prompt_type))
end
next
end
# otherwise let's make sure they offered the right number of tags
required = eval("restriction.#{tag_type}_num_required")
allowed = eval("restriction.#{tag_type}_num_allowed")
unless tag_count.between?(required, allowed)
taglist_string = taglist.empty? ?
ts("none") :
"(#{tag_count}) -- " + taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT)
if allowed == 0
errors.add(:base, ts("^#{prompt_type}: Your #{prompt_type} cannot include any #{tag_type} tags, but you have included %{taglist}.",
taglist: taglist_string))
elsif required == allowed
errors.add(:base, ts("^#{prompt_type}: Your #{prompt_type} must include exactly %{required} #{tag_type} tags, but you have included #{tag_count} #{tag_type} tags in your current #{prompt_type}.",
required: required))
else
errors.add(:base, ts("^#{prompt_type}: Your #{prompt_type} must include between %{required} and %{allowed} #{tag_type} tags, but you have included #{tag_count} #{tag_type} tags in your current #{prompt_type}.",
required: required, allowed: allowed))
end
end
end
end
end
# make sure that if there is a specified set of allowed tags, the user's choices
# are within that set, or otherwise canonical
validate :allowed_tags
def allowed_tags
restriction = get_prompt_restriction
if restriction && tag_set
TagSet::TAG_TYPES.each do |tag_type|
# if we have a specified set of tags of this type, make sure that all the
# tags in the prompt are in the set.
if TagSet::TAG_TYPES_RESTRICTED_TO_FANDOM.include?(tag_type) && restriction.send("#{tag_type}_restrict_to_fandom")
# skip the check, these will be tested in restricted_tags below
elsif restriction.has_tags?(tag_type)
disallowed_taglist = tag_set.send("#{tag_type}_taglist") - restriction.tags(tag_type)
unless disallowed_taglist.empty?
errors.add(:base, ts("^These %{tag_type} tags in your %{prompt_type} are not allowed in this challenge: %{taglist}",
tag_type: tag_type,
prompt_type: self.class.name.downcase,
taglist: disallowed_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT)))
end
else
noncanonical_taglist = tag_set.send("#{tag_type}_taglist").reject {|t| t.canonical}
unless noncanonical_taglist.empty?
errors.add(:base, ts("^These %{tag_type} tags in your %{prompt_type} are not canonical and cannot be used in this challenge: %{taglist}. To fix this, please ask your challenge moderator to set up a tag set for the challenge. New tags can be added to the tag set manually by the moderator or through open nominations.",
tag_type: tag_type,
prompt_type: self.class.name.downcase,
taglist: noncanonical_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT)))
end
end
end
end
end
# make sure that if any tags are restricted to fandom, the user's choices are
# actually in the fandom they have chosen.
validate :restricted_tags
def restricted_tags
restriction = get_prompt_restriction
if restriction
TagSet::TAG_TYPES_RESTRICTED_TO_FANDOM.each do |tag_type|
if restriction.send("#{tag_type}_restrict_to_fandom")
# tag_type is one of a set set so we know it is safe for constantize
allowed_tags = tag_type.classify.constantize.with_parents(tag_set.fandom_taglist).canonical
disallowed_taglist = tag_set ? eval("tag_set.#{tag_type}_taglist") - allowed_tags : []
# check for tag set associations
disallowed_taglist.reject! {|tag| TagSetAssociation.where(tag_id: tag.id, parent_tag_id: tag_set.fandom_taglist).exists?}
unless disallowed_taglist.empty?
errors.add(:base, ts("^These %{tag_type} tags in your %{prompt_type} are not in the selected fandom(s), %{fandom}: %{taglist} (Your moderator may be able to fix this.)",
prompt_type: self.class.name.downcase,
tag_type: tag_type, fandom: tag_set.fandom_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT),
taglist: disallowed_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT)))
end
end
end
end
end
# INSTANCE METHODS
# make sure we are not blank
def blank?
return false if (url || description)
tagcount = 0
[tag_set, optional_tag_set].each do |set|
if set
tagcount += set.taglist.size + (TagSet::TAG_TYPES.collect {|type| eval("set.#{type}_taglist.size")}.sum)
end
end
return false if tagcount > 0
true # everything empty
end
def can_delete?
if challenge_signup && !challenge_signup.can_delete?(self)
false
else
true
end
end
def unfulfilled_claims
self.request_claims.unfulfilled_in_collection(self.collection)
end
def fulfilled_claims
self.request_claims.fulfilled
end
# We want to have all the matching methods defined on
# TagSet available here, too, without rewriting them,
# so we just pass them through method_missing
def method_missing(method, *args, &block)
super || (tag_set && tag_set.respond_to?(method) ? tag_set.send(method) : super)
end
def respond_to?(method, include_private = false)
super || tag_set.respond_to?(method, include_private)
end
# Computes the "full" tag set (tag_set + optional_tag_set), and stores the
# result as an instance variable for speed. This is used by the matching
# algorithm, which doesn't change any signup/prompt/tagset information, so
# it's okay to cache some information. (And if the info does change
# mid-matching process, it's okay that we're using the tag sets that were
# there when the moderator started the matching process.)
def full_tag_set
if @full_tag_set.nil?
@full_tag_set = optional_tag_set ? tag_set + optional_tag_set : tag_set
end
@full_tag_set
end
# Returns true if there's a match, false otherwise.
# self is the request, other is the offer
def matches?(other, settings = nil)
return nil if challenge_signup.id == other.challenge_signup.id
return nil if settings.nil?
TagSet::TAG_TYPES.each do |type|
# We definitely match in this type if the request or the offer accepts
# "any" for it. No need to check any more info for this type.
next if send("any_#{type}") || other.send("any_#{type}")
required_count = settings.send("num_required_#{type.pluralize}")
match_count = if settings.send("include_optional_#{type.pluralize}")
full_tag_set.match_rank(other.full_tag_set, type)
else
# we don't use optional tags to count towards required
tag_set.match_rank(other.tag_set, type)
end
# if we have to match all and don't, not a match
return false if required_count == ALL && match_count != ALL
# we are a match only if we either match all or at least as many as required
return false if match_count != ALL && match_count < required_count
end
true
end
# Count the number of overlapping tags of all types. Does not use ALL to
# indicate a 100% match, since the goal is to give a bonus to matches where
# both requester and offerer were specific about their desires, and had a lot
# of overlap.
def count_tags_matched(other)
self_tags = full_tag_set.tags.map(&:id)
other_tags = other.full_tag_set.tags.map(&:id)
(self_tags & other_tags).size
end
def accepts_any?(type)
send("any_#{type.downcase}")
end
def get_prompt_restriction
if collection && collection.challenge
collection.challenge.prompt_restriction
else
nil
end
end
def self.reset_positions_in_collection!(collection)
minpos = collection.prompts.minimum(:position) - 1
collection.prompts.by_position.each do |prompt|
prompt.position = prompt.position - minpos
prompt.save
end
end
# tag groups
def tag_groups
self.tag_set ? self.tag_set.tags.group_by { |t| t.type.to_s } : {}
end
# Takes an array of tags and returns a comma-separated list, without the markup
def tag_list(tags)
tags = tags.uniq.compact
if !tags.blank? && tags.respond_to?(:collect)
last_tag = tags.pop
tag_list = tags.collect{|tag| tag.name + ", "}.join
tag_list += last_tag.name
tag_list.html_safe
else
""
end
end
# gets the list of tags for this prompt
def tag_unlinked_list
list = ""
TagSet::TAG_TYPES.each do |type|
eval("@show_request_#{type}_tags = (self.collection.challenge.request_restriction.#{type}_num_allowed > 0)")
if eval("@show_request_#{type}_tags")
if self && self.tag_set && !self.tag_set.with_type(type).empty?
list += " - " + tag_list(self.tag_set.with_type(type))
end
end
end
return list
end
def claim_by(user)
ChallengeClaim.where(request_prompt_id: self.id, claiming_user_id: user.id)
end
# checks if a prompt has been filled in a prompt meme
def unfulfilled?
if self.request_claims.empty? || !self.request_claims.fulfilled.exists?
return true
end
end
# currently only prompt meme prompts can be claimed, and by any number of people
def claimable?
if self.collection.challenge.is_a?(PromptMeme)
true
else
false
end
end
end