Commit 0c4f7f17 authored by Michael Rose's avatar Michael Rose

shipsearch: Initial working prototype

 - Ships are saved to saved search, all works, presuming the shipstring
   is populated.
 - Converted search.js.coffee to plain ES6 with a mechanical converter.
   Pending rewrite.
parent a7f94409
This diff is collapsed.
window.init = ->
title = $("[name='search[title]']")
author = $("[name='search[author]']")
summary = $("[name='search[summary]']")
saved = $("[name='saved_search_id']")
category_required = $("[name='search[category_required][]']")
category_optional = $("[name='search[category_optional][]']")
category_optional_exclude = $("[name='search[category_optional_exclude]']")
character_required = $("[name='search[character_required][]']")
character_optional = $("[name='search[character_optional][]']")
character_optional_exclude = $("[name='search[character_optional_exclude]']")
tags_include_ids = $("[name='search[tags_include_ids][]']")
tags_exclude_ids = $("[name='search[tags_exclude_ids][]']")
fandoms = $("[name='search[fandoms][]']")
crossovers = $("[name='search[crossovers][]']")
include_source_fandoms = $("[name='search[include_source_fandoms]']")
status = $("[name='search[status]']")
language = $("[name='search[language]']")
rating = $("[name='search[rating][]']")
wordcount_lower = $("[name='search[wordcount_lower]']")
wordcount_upper = $("[name='search[wordcount_upper]']")
chapters_lower = $("[name='search[chapters_lower]']")
chapters_upper = $("[name='search[chapters_upper]']")
published_after = $("[name='search[published_after]']")
published_before = $("[name='search[published_before]']")
updated_after = $("[name='search[updated_after]']")
updated_before = $("[name='search[updated_before]']")
sort_by = $("[name='search[sort_by]']")
order_by = $("[name='search[order_by]']")
selectors = {title,author,summary,saved,
category_required,category_optional,category_optional_exclude,
character_required,character_optional,character_optional_exclude,
fandoms,crossovers,include_source_fandoms
status,language,rating,
tags_include_ids, tags_exclude_ids,
wordcount_lower, wordcount_upper,
chapters_lower, chapters_upper,
published_after, published_before, updated_after, updated_before
sort_by,order_by
}
saved.chosen({allow_single_deselect: true}).off('change').on('change', (e,p) -> navigate(e,p))
category_required.chosen({max_selected_options: 2})
category_optional.chosen()
character_required.chosen({max_selected_options: 4})
character_optional.chosen()
tags_include_ids.chosen()
tags_exclude_ids.chosen()
fandoms.chosen().off('change').on('change', (e,p) -> updateFandom(e,p))
crossovers.chosen({allow_single_deselect: true}).off('change').on('change', (e,p) -> updateCrossover(e,p))
status.chosen({allow_single_deselect: true})
language.chosen({allow_single_deselect: true})
rating.chosen()
sort_by.chosen()
order_by.chosen()
$('#clear_fandoms').click ->
fandoms.val([])
fandoms.trigger('chosen:updated')
$('#clear_crossovers').click ->
crossovers.val([])
crossovers.trigger('chosen:updated')
$('#clear_character_required').click ->
character_required.val([])
character_required.trigger('chosen:updated')
$('#clear_character_optional').click ->
character_optional.val([])
character_optional.trigger('chosen:updated')
$('#advanced_search :checkbox').click ->
updateCheckboxLabel($(this))
updateCheckboxLabel = (elem) ->
label_elem = $("label[for='"+elem.attr("name")+"']")
label = label_elem.data("type")
if (elem.is(':checked'))
label = "Excluded " + label
else
label = "Included " + label
label_elem.text(label)
navigate = (e,p) ->
opt = $("option:selected", $("[name='saved_search_id']"))[0]
if(opt.value != null && opt.value != "")
$.getJSON('/user/saved_searches/'+opt.value, (data) ->
s = data.search
title.val(s.title)
author.val(s.author)
summary.val(s.summary)
fandoms.val(s.fandoms)
fandoms.trigger('chosen:updated')
crossovers.val(s.crossovers)
crossovers.trigger('chosen:updated')
include_source_fandoms.prop('checked', s.include_source_fandoms)
updateFandom().done(->
# wait for crossovers change before adding chars
for prop in ['category', 'character']
for type in ['required', 'optional']
propname = prop + "_" + type
selectors[propname].val(s[propname])
selectors[propname].trigger('chosen:updated')
toggleprop = prop + "_optional_exclude"
selectors[toggleprop].prop('checked', s[toggleprop])
updateCheckboxLabel(selectors[toggleprop])
)
for prop in ['wordcount_lower', 'wordcount_upper', 'chapters_lower', 'chapters_upper',
'published_after', 'published_before', 'updated_after', 'updated_before',
'tags_include_ids', 'tags_exclude_ids',
'rating', 'language', 'status', 'sort_by', 'order_by']
selectors[prop].val(s[prop])
selectors[prop].trigger('chosen:updated')
)
updateCrossover = (e,p) ->
fandom = $("option:selected", $("[name='search[fandoms][]']"));
crossover = $("option:selected", $("[name='search[crossovers][]']"));
params = (fandom.map (c) -> "fandom[]="+$(this).val()).toArray()
.concat((crossover.map (c) -> "fandom[]="+$(this).val()).toArray())
if params.length > 0
$.getJSON("/characters?"+params.join('&'), (data) ->
select = $("[name='search[character_required][]']")
select2 = $("[name='search[character_optional][]']")
existing = select.val()
existing2 = select2.val()
options = select.prop('options')
options2 = select2.prop('options')
$('option', select).remove()
$('option', select2).remove()
$.each(data, (k,v) ->
options[options.length] = new Option(v.name,v.id)
options2[options.length] = new Option(v.name,v.id)
)
select.val(existing)
select2.val(existing2)
select.trigger('chosen:updated');
select2.trigger('chosen:updated');
)
else
$.Deferred().resolveWith({})
updateFandom = (e,p,c) ->
updateCrossover(e,p).done(->
fandom = $("option:selected", $("[name='search[fandoms][]']"));
crossover = $("option:selected", $("[name='search[crossovers][]']"));
fandoms_selected = (fandom.map (c) -> $(this).val()).toArray()
crossovers_selected = (crossover.map (c) -> $(this).val()).toArray()
params = (fandom.map (c) -> "fandom[]="+$(this).val()).toArray()
.concat((crossover.map (c) -> "fandom[]="+$(this).val()).toArray())
if params.length > 0
$.getJSON("/crossovers?"+params.join('&'), (data) ->
select = $("[name='search[crossovers][]']")
options = select.prop('options')
$('option', select).remove()
options[options.length] = new Option('', '')
$.each(data, (k,v) ->
options[options.length] = new Option(v.name,v.id)
)
select.val(crossovers_selected)
select.trigger('chosen:updated')
)
)
$ ->
$('.report-broken').click (e) ->
e.stopImmediatePropagation();
story_id = $(this).data('story-id')
$.post('/search/report_broken', {'story_id': story_id})
.done((data) -> alert('Reported link as broken! Thank you. It may take some time to deindex this story.'))
$('.read-it-later').click (e) ->
e.stopImmediatePropagation();
selector = $(this)
story_id = selector.data('story-id')
state = selector.data('state')
method = 'POST'
if state == 'in-queue'
method = 'DELETE'
$.ajax({
url: '/user/queue',
method: method
data: {'story_id': story_id}
})
.done((data) ->
if method == 'POST'
selector.data('state', 'in-queue')
selector.html('<i class="fa fa-minus"></i> Remove from queue')
else
selector.data('state', 'out-of-queue')
selector.html('<i class="fa fa-plus"></i> Add to queue')
)
.fail((response) ->
if response.status != 409
alert('Failed to add/remove story to queue: ' + response.statusText)
)
$('a.story-link').on 'mousedown', (e) ->
e.stopImmediatePropagation();
search = $('#search').data('search');
story_id = $(this).data('story-id')
searchParams = new URLSearchParams(location.search);
page = if searchParams && searchParams.has('page') then searchParams.get('page') else 1
$.post('/search/click', {
'story_id': story_id,
'search': search,
'page': page
})
......@@ -32,6 +32,7 @@ class Search < ActiveRecord::Base
@@fields = Set.new(%w(fandoms crossovers include_source_fandoms title author summary
category_required category_optional category_optional_exclude
character_required character_optional character_optional_exclude
relationships
tags_include tags_exclude
language status rating
wordcount_lower wordcount_upper chapters_lower chapters_upper
......@@ -53,6 +54,7 @@ class Search < ActiveRecord::Base
self.character_required ||= []
self.character_optional ||= []
self.character_optional_exclude ||= false
self.relationships ||= []
self.tags_include_ids ||= []
self.tags_exclude_ids ||= []
self.language ||= 'english'
......@@ -77,6 +79,17 @@ class Search < ActiveRecord::Base
rating
end
end
# Fix relationships
if self.relationships.is_a? Hash
self.relationships = self.relationships.values.collect do |ship|
if ship['characters'] && !ship['characters'].empty?
ship
else
nil
end
end.compact
end
end
def execute(page, per_page)
......
......@@ -184,6 +184,16 @@ class SearchQuery
clauses += search.tags_exclude.map{ |t| term 'meta.tags' => t.storage_string }
end
unless search.relationships.empty?
search.relationships.each do |ship|
if ship.key?('exclude') && ship['characters']
shipstring = ship['characters'].map(&:to_i).sort.join('_')
clauses << (term 'shipstrings' => shipstring)
end
end
end
clauses
end
......@@ -254,6 +264,20 @@ class SearchQuery
end
end
unless search.relationships.empty?
subclauses = []
search.relationships.each do |ship|
unless ship.key?('exclude')
if ship['characters']
shipstring = ship['characters'].map(&:to_i).sort.join('_')
subclauses << (term 'shipstrings' => shipstring)
end
end
end
clauses << or_clause(subclauses)
end
clauses << (terms 'meta.rated' => search.rating)
if search.wordcount_lower
......
<div class="form-group col-lg-12 shiprow" data-ship-index="<%= index -%>">
<div class="input-group">
<span class="input-group-addon">
<%= check_box_tag "search[relationships][#{index}][exclude]", true, ship.key?('exclude') %> Exclude
</span>
<select multiple
class="form-control character_list"
data-placeholder="Choose up to 4 characters"
name="search[relationships][<%= index -%>][characters][]"
id="search[relationships][<%= index -%>][characters][]">
<option disabled></option>
<%= options_from_collection_for_select(@characters, :id, :name, ship['characters'] || []) %>
</select>
<span class="input-group-btn">
<button id="remove_relationships[<%= index -%>]" class="btn btn-default btn-sm" type="button" <% if index == 0 %>disabled="disabled"<% end %>>Remove</button>
</span>
</div>
</div>
\ No newline at end of file
......@@ -19,7 +19,12 @@
{include_blank: 'Create new search', selected: @saved_search.try(:hash_id)},
:class => 'form-control'
- @search_params.each do |k,v|
- if v.kind_of? Array
- if k == 'relationships'
- v.each do |k1, v1|
= hidden_field_tag "save_search[relationships][#{k1}][exclude]", true if v1.key?('exclude')
- v1['characters'].each do |c|
= hidden_field_tag "save_search[relationships][#{k1}][characters][]", c
- elsif v.kind_of? Array
- v.each do |v1|
= hidden_field_tag "save_search[#{k}][]", v1
- else
......
......@@ -137,7 +137,7 @@
<div class="form-group col-lg-6">
<label for="search[character_required][]">Required Characters (max: 4)</label>
<select multiple
class="form-control"
class="form-control character_list"
data-placeholder="Choose up to 4 required characters"
name="search[character_required][]"
id="search[character_required][]">
......@@ -157,7 +157,7 @@
<%= check_box_tag 'search[character_optional_exclude]', 'true', @search[:character_optional_exclude] %> Exclude
</span>
<select multiple
class="form-control"
class="form-control character_list"
data-placeholder="Choose some characters"
name="search[character_optional][]"
id="search[character_optional][]">
......@@ -172,6 +172,44 @@
</div>
</div>
<h3><i class="fa fa-heart text-muted"></i> Relationships</h3>
<div id="shippingcontainer" class="row">
<div id="shiprow-template" class="hidden form-group col-lg-12">
<div class="input-group">
<span class="input-group-addon">
<%= check_box_tag 'search[relationships][x][exclude]', 'true' %> Exclude
</span>
<select multiple
class="form-control"
data-placeholder="Choose up to 4 characters"
name="search[relationships][x][characters][]"
id="search[relationships][x][characters][]">
<option disabled></option>
<%= options_from_collection_for_select(@characters, :id, :name) %>
</select>
<span class="input-group-btn">
<button id="remove_relationships[x]" class="btn btn-default btn-sm" type="button" disabled="disabled">Remove</button>
</span>
</div>
</div>
<% if @search[:relationships].empty? %>
<%= render 'relationship_row', ship: { characters: [] }, index: 0 %>
<% else %>
<% @search[:relationships].each_with_index do |s, idx| %>
<%= render 'relationship_row', ship: s, index: idx %>
<% end %>
<% end %>
</div>
<div class="row col-lg-12">
<button id="add_relationships" class="btn btn-default btn-sm" type="button">Add Criteria</button>
</div>
<span class="help-block">
Each ship will be matched on an OR basis. That is, if you select <code>[Character A, Character B]</code>
and <code>[Character A, Character C]</code>, this will match stories with <code>[A, B] OR [A, C]</code>.
Excluded ships will be completely excluded.
</span>
<h3><i class="fa fa-tag text-muted"></i> Tags</h3>
<div class="row">
<div class="form-group col-lg-6">
......
class AddRelationshipsToSearch < ActiveRecord::Migration[5.1]
def change
change_table :searches do |t|
t.jsonb :relationships
end
end
end
......@@ -20,7 +20,6 @@ module Scryer
tags << label(:slash, 'danger')
end
# Ya'll are worth every query <3
story.relationships.each { |group|
genders = group.inject(Hash.new(0)) { |h, v| h[v.isfemale] += 1; h }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment