app/controllers/tag_controller.rb
class TagController < ApplicationController
respond_to :html, :xml, :json, :ics
before_action :require_user, only: %i(create delete)
include Pagy::Backend
def index
@toggle = params[:sort] || "uses"
@title = I18n.t('tag_controller.tags')
@paginated = true
@order_type = params[:order].blank? || (params[:order] == "desc") ? "desc" : "asc"
@order_type_for_name = params[:order].blank? || (params[:order] == "asc") ? "asc" : "desc"
powertag_clause = params[:powertags] == 'true' ? '' : ['name NOT LIKE ?', '%:%']
if params[:search]
keyword = params[:search]
@tags = Tag.joins(:node_tag, :node)
.select('MAX(term_data.count) count, MAX(term_data.name) name, MAX(term_data.tid) tid, MAX(node.nid) nid, node.status, MAX(community_tags.tid), MAX(community_tags.date)')
.where('node.status = ?', 1)
.where('community_tags.date > ?', (DateTime.now - 1.month).to_i)
.where("name LIKE :keyword", keyword: "%#{keyword}%")
.where(powertag_clause)
.group('term_data.name')
.order(order_string.gsub('count', "MAX(term_data.count)"))
.paginate(page: params[:page], per_page: 24)
elsif @toggle == "uses"
@tags = Tag.joins(:node_tag, :node)
.select('MAX(term_data.count) count, MAX(term_data.name) name, MAX(term_data.tid) tid, MAX(node.nid) nid, node.status, MAX(community_tags.tid), MAX(community_tags.date)')
.where('node.status = ?', 1)
.where('community_tags.date > ?', (DateTime.now - 1.month).to_i)
.where(powertag_clause)
.group(:name)
.order(order_string.gsub('count', "MAX(term_data.count)"))
.paginate(page: params[:page], per_page: 24)
elsif @toggle == "name"
@tags = Tag.joins(:node_tag, :node)
.select('MAX(term_data.count) count, MAX(term_data.name) name, MAX(term_data.tid) tid, MAX(node.nid) nid, node.status, MAX(community_tags.tid), MAX(community_tags.date)')
.where('node.status = ?', 1)
.where('community_tags.date > ?', (DateTime.now - 1.month).to_i)
.where(powertag_clause)
.group(:name)
.order(order_string.gsub('count', "MAX(term_data.count)"))
.paginate(page: params[:page], per_page: 24)
elsif @toggle == "followers"
raw_tags = Tag.joins(:node_tag, :node)
.select('MAX(term_data.count) count, MAX(term_data.name) name, MAX(term_data.tid) tid, MAX(node.nid) nid, node.status, MAX(community_tags.tid), MAX(community_tags.date)')
.where('node.status = ?', 1)
.where('community_tags.date > ?', (DateTime.now - 1.month).to_i)
.where(powertag_clause)
.group(:name)
raw_tags = Tag.sort_according_to_followers(raw_tags, params[:order])
@tags = raw_tags.paginate(page: params[:page], per_page: 24)
else
tags = Tag.joins(:node_tag, :node)
.select('MAX(node.nid), node.status, term_data.*, community_tags.*')
.where('node.status = ?', 1)
.where('community_tags.date > ?', (DateTime.now - 1.month).to_i)
.where(powertag_clause)
.group(:name)
.order(order_string)
followed = []
not_followed = []
tags.each do |tag|
if current_user.following(tag.name) == true
followed.append(tag.tid)
else
not_followed.append(tag.tid)
end
end
ids = followed + not_followed
@tags = Tag.where(tid: ids).sort_by { |p| ids.index(p.tid) }.paginate(page: params[:page], per_page: 24)
end
end
def show
get_wiki
@node = @wiki # expose the wiki node in the @node variable so we get open graph meta tags in the layout
default_type = params[:id]&.match?('question:') ? 'questions' : 'note'
@node_type = params[:node_type] || default_type
@start = Time.parse(params[:start]) if params[:start]
@end = Time.parse(params[:end]) if params[:end]
node_type = if %w(questions note).include?(@node_type)
'note'
elsif @node_type == 'wiki'
'page'
elsif @node_type == 'maps'
'map'
elsif @node_type == 'contributors'
'contributor'
elsif @node_type == 'comments'
'comments'
end
if params[:id][-1..-1] == '*' # wildcard tags
@wildcard = true
@tags = Tag.where('name LIKE (?)', params[:id][0..-2] + '%')
nodes = Node.for_tagname_and_type(params[:id], node_type, wildcard: true)
else
@tags = Tag.where(name: params[:id])
nodes = Node.for_tagname_and_type(params[:id], node_type, question: (@node_type == 'questions'))
end
if @start && @end
nodes = nodes.where(created: @start.to_i..@end.to_i)
else
@pinned_nodes = NodeShared.pinned_nodes(params[:id])
if @pinned_nodes.size.positive? && params[:page].nil? # i.e. first page
nodes = nodes.where.not(nid: @pinned_nodes.collect(&:id))
end
end
order_by = if params[:order] == 'views'
'node.views DESC'
elsif params[:order] == 'likes'
'node.cached_likes DESC'
elsif @node_type == 'wiki' # wiki sorting by timestamp isn't working; https://github.com/publiclab/plots2/issues/7334#issuecomment-696938352
'node.nid DESC'
else # params[:order] == 'last_updated'
'node_revisions.timestamp DESC'
end
@qids = Node.questions.where(status: 1)
.collect(&:nid)
if @qids.empty?
@questions = []
else
nodes = nodes.where('node.nid NOT IN (?)', @qids) if @node_type == 'note'
if @node_type == 'questions'
@questions = nodes.where('node.nid IN (?)', @qids)
@pagy, nodes = pagy(@questions, items: 24)
else
@pagy, nodes = pagy(nodes, items: 24)
end
end
nodes = nodes.order(order_by)
@notes = nodes
@paginated = true
@wikis = nodes if @node_type == 'wiki'
@wikis ||= []
@nodes = nodes if @node_type == 'maps'
@title = params[:id]
@contributor_count = Tag.contributor_count(params[:id]) || 0
@tagnames = [params[:id]]
@tag = Tag.find_by(name: params[:id])
@note_count = Tag.tagged_node_count(params[:id]) || 0
@users = Tag.contributors(@tagnames[0])
@related_tags = Tag.related(@tagnames[0])
fetch_counts
respond_with(nodes) do |format|
format.html { render 'tag/show' }
format.xml { render xml: nodes }
format.json do
json = []
nodes.each do |node|
json << node.as_json(except: %i(path tags))
json.last['path'] = 'https://' + request.host.to_s + node.path
json.last['preview'] = node.body_preview(500)
json.last['image'] = node.main_image.path(:large) if node.main_image
json.last['tags'] = Node.find(node.id).tags.collect(&:name) if node.tags
end
render json: json
end
end
end
def show_for_author
# try for a matching /wiki/_TAGNAME_ or /_TAGNAME_
@wiki = Node.where(path: "/wiki/#{params[:id]}").try(:first) || Node.where(path: "/#{params[:id]}").try(:first)
@wiki = Node.find(@wiki.power_tag('redirect')) if @wiki&.has_power_tag('redirect')
default_type = if params[:id].match?('question:')
'questions'
else
'note'
end
# params[:node_type] - this is an optional param
# if params[:node_type] is nil - use @default_type
@node_type = params[:node_type] || default_type
node_type = 'note' if @node_type == 'questions' || @node_type == 'note'
node_type = 'page' if @node_type == 'wiki'
node_type = 'map' if @node_type == 'maps'
qids = Node.questions.where(status: 1).collect(&:nid)
if params[:id][-1..-1] == '*' # wildcard tags
@wildcard = true
@tags = Tag.where('name LIKE (?)', params[:id][0..-2] + '%')
else
@tags = Tag.where(name: params[:id])
end
@tagname = params[:id]
@user = User.find_by(name: params[:author])
nodes = Tag.tagged_nodes_by_author(@tagname, @user)
.where(status: 1, type: node_type)
@total_posts = nodes.size
nodes = nodes.paginate(page: params[:page], per_page: 24)
@notes ||= []
@notes = nodes.where('node.nid NOT IN (?)', qids) if @node_type == 'note'
@questions = nodes.where('node.nid IN (?)', qids) if @node_type == 'questions'
@wikis = nodes if @node_type == 'wiki'
@nodes = nodes if @node_type == 'maps'
@title = "'" + @tagname.to_s + "' by " + params[:author]
@contributor_count = Tag.contributor_count(params[:id]) || 0
respond_with(nodes) do |format|
format.html { render 'tag/show' }
format.xml { render xml: nodes }
format.json do
json = []
nodes.each do |node|
json << node.as_json(except: %i(path tags))
json.last['path'] = 'https://' + request.host
.to_s + node.path
json.last['preview'] = node.body_preview(500)
json.last['image'] = node.main_image.path(:large) if node.main_image
json.last['tags'] = Node.find(node.id).tags.collect(&:name) if node.tags
end
render json: json
end
end
end
def related
@tags = Tag.related(params[:id])
render partial: 'tag/related', layout: false, locals: { tags: @tags }
end
def widget
num = params[:n] || 4
nids = Tag.find_nodes_by_type(params[:id], 'note', num).collect(&:nid)
@notes = Node.paginate(page: params[:page], per_page: 24)
.where('status = 1 AND nid in (?)', nids)
.order('nid DESC')
render layout: false
end
def blog2
nids = Tag.find_nodes_by_type(params[:id], 'note', nil).collect(&:nid)
@pagy, @notes = pagy(Node.includes(:node_tag).where('node.status = 1 AND node.nid in (?)', nids).order('community_tags.updated_at DESC'), items: 6)
@tags = Tag.where(name: params[:id])
@tagnames = @tags.collect(&:name).uniq! || []
@title = @tagnames.join(',') + ' Blog' if @tagnames
end
def blog
nids = Tag.find_nodes_by_type(params[:id], 'note', nil).collect(&:nid)
@pagy, @notes = pagy(Node.where('status = 1 AND nid in (?)', nids).order('created DESC'), items: 6)
@tags = Tag.where(name: params[:id])
@tagnames = @tags.collect(&:name).uniq! || []
@title = @tagnames.join(',') + ' Blog' if @tagnames
end
def author
render json: User.find_by(name: params[:id]).tag_counts
end
def barnstar
node = Node.find params[:nid]
tagname = 'barnstar:' + params[:star]
if Tag.exists?(tagname, params[:nid])
flash[:error] = I18n.t('tag_controller.tag_already_exists')
elsif !node.add_barnstar(tagname.strip, current_user)
flash[:error] = I18n.t('tag_controller.barnstar_not_created')
else
flash[:notice] = I18n.t('tag_controller.barnstar_awarded', url1: '/wiki/barnstars#' + params[:star].split('-').each(&:capitalize!).join('+') + '+Barnstar', star: params[:star], url2: '/profile/' + node.author.name, awardee: node.author.name).html_safe
# on success add comment
barnstar_info_link = '<a href="//' + request.host.to_s + '/wiki/barnstars">barnstar</a>'
node.add_comment(subject: 'barnstar',
uid: current_user.uid,
body: "@#{current_user.username} awards a #{barnstar_info_link} to #{node.user.name} for their awesome contribution!")
end
redirect_to node.path + '?_=' + Time.now.to_i.to_s
end
def create
params[:name] ||= ''
tagnames = params[:name].split(',')
@output = {
errors: [],
saved: []
}
@tags = [] # not used except in tests for now
nid = params[:nid] || params[:id]
node = Node.find nid
tagnames.each do |tagname|
# this should all be done in the model:
tagname = tagname.strip
if Tag.exists?(tagname, nid)
@output[:errors] << I18n.t('tag_controller.tag_already_exists')
elsif tagname.include?(":") && tagname.split(':').size < 2
if tagname.split(':')[0] == "barnstar" || tagname.split(':')[0] == "with"
@output[:errors] << I18n.t('tag_controller.cant_be_empty')
end
elsif node.can_tag(tagname, current_user) === true || logged_in_as(['admin'])
saved, tag = node.add_tag(tagname.strip, current_user)
if tagname.include?(":") && tagname.split(':').size == 2
if tagname.split(':')[0] == "barnstar"
CommentMailer.notify_barnstar(current_user, node)
barnstar_info_link = '<a href="//' + request.host.to_s + '/wiki/barnstars">barnstar</a>'
node.add_comment(subject: 'barnstar',
uid: current_user.uid,
body: "@#{current_user.username} awards a #{barnstar_info_link} to #{node.user.name} for their awesome contribution!")
elsif tagname.split(':')[0] == "with"
user = User.find_by_username_case_insensitive(tagname.split(':')[1])
CommentMailer.notify_coauthor(user, node)
node.add_comment(subject: 'co-author',
uid: current_user.uid,
body: " @#{current_user.username} has marked @#{tagname.split(':')[1]} as a co-author. ")
end
end
if saved
@tags << tag
@output[:saved] << [tag.name, tag.id, nid]
else
@output[:errors] << I18n.t('tag_controller.error_tags') + tag.errors[:name].first
end
else
@output[:errors] << node.can_tag(tagname, current_user, true)
end
end
respond_with do |format|
format.html do
if request.xhr?
render json: @output
else
flash[:notice] = I18n.t('tag_controller.tags_created_error',
tag_count: @output[:saved].size,
error_count: @output[:errors].size).html_safe
redirect_to node.path
end
end
end
end
# should delete only the term_node/node_tag (instance), not the term_data (class)
def delete
node_tag = NodeTag.where(nid: params[:nid], tid: params[:tid]).first
node = Node.where(nid: params[:nid]).first
# only admins, mods can delete other peoples' tags if the note/wiki contains the locked tag
if (node_tag.uid == current_user.uid && !node.has_tag('locked')) || logged_in_as(['admin', 'moderator']) || (node.uid == current_user.uid && !node.has_tag('locked'))
tag = Tag.joins(:node_tag)
.select('term_data.name')
.where(tid: params[:tid])
.first
if (tag.name.split(':')[0] == "lat") || (tag.name.split(':')[0] == "lon")
node.delete_coord_attribute(tag.name)
end
node_tag.delete
output = {
status: true,
tid: node_tag.tid
}
respond_with do |format|
format.html do
if request.xhr?
render json: output
else
flash[:notice] = I18n.t('tag_controller.tag_deleted')
redirect_to node_tag.node.path
end
end
end
elsif node.has_tag('locked')
flash[:error] = "Only admins can delete tags on locked pages."
redirect_to Node.find_by(nid: params[:nid]).path
else
flash[:error] = I18n.t('tag_controller.must_own_tag_to_delete')
redirect_to Node.find_by(nid: params[:nid]).path
end
end
def suggested
if !params[:id].empty? && params[:id].size > 2
@suggestions = SearchService.new.search_tags(params[:id])
render json: @suggestions.collect { |tag| tag.name }.uniq
else
render json: []
end
end
def rss
@notes = if params[:tagname][-1..-1] == '*'
Node.where(status: 1, type: 'note')
.includes(:revision, :tag)
.references(:term_data, :node_revisions)
.where('term_data.name LIKE (?)', params[:tagname][0..-2] + '%')
.limit(20)
.order('node_revisions.timestamp DESC')
else
Tag.find_nodes_by_type([params[:tagname]], 'note', 20)
end
respond_to do |format|
format.rss do
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
response.headers['Access-Control-Allow-Origin'] = '*'
render layout: false
end
format.ics do
response.headers['Content-Disposition'] = "attachment; filename='public-lab-events.ics'"
response.headers['Content-Type'] = 'text/calendar; charset=utf-8'
render layout: false, template: 'tag/icalendar.ics', filename: 'public-lab-events.ics'
end
end
end
def rss_for_tagged_with_author
@user = User.find_by(name: params[:authorname])
@notes = Tag.tagged_nodes_by_author(params[:tagname], @user)
.where(status: 1)
.limit(20)
respond_to do |format|
format.rss do
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
response.headers['Access-Control-Allow-Origin'] = '*'
render layout: false
end
format.ics do
response.headers['Content-Disposition'] = "attachment; filename='public-lab-events.ics'"
response.headers['Content-Type'] = 'text/calendar; charset=utf-8'
render layout: false, template: 'tag/icalendar.ics', filename: 'public-lab-events.ics'
end
end
end
def location
render template: 'locations/_form'
end
def location_modal
render template: 'locations/_modal', layout: false
end
def gridsEmbed
if %w(nodes wikis activities questions upgrades notes).include?(params[:tagname].split(':').first)
params[:t] = params[:tagname]
params[:tagname] = ""
end
render layout: false
end
def graph
render layout: false
end
def graph_data
render json: params.key?(:limit) ? Tag.graph_data(params[:limit].to_i, params[:type].to_s, params[:weight].to_i) : Tag.graph_data
end
def stats
@start = params[:start] ? Time.parse(params[:start].to_s) : Time.now - 1.year
@end = params[:end] ? Time.parse(params[:end].to_s) : Time.now
tagname = params[:id]
@tag_name = params[:id]
@tags = Tag.where(name: params[:id])
return if @tags.empty?
@tag_notes = @tags.first.contribution_graph_making('note', @start, @end)
@tag_wikis = @tags.first.contribution_graph_making('page', @start, @end)
@tag_questions = @tags.first.quiz_graph(@start, @end)
@tag_comments = @tags.first.comment_graph(@start, @end)
@subscriptions = @tags.first.subscription_graph(@start, @end)
@first_time_poster_content_tally = Rails.cache.fetch("#{params[:id].to_s+@start.to_s+@end.to_s}/first-time-posters-in-period", expires_in: 1.day) do
# count nodes tagged "first-time-poster" in addition to this tag:
ftp_tid = Tag.where(name: 'first-time-poster')&.first&.tid
ftp_nids = NodeTag
.where(tid: ftp_tid)
.joins(:node)
.where('node.created': @start.to_i..@end.to_i)
.collect(&:nid)
tag_nids = NodeTag.where(tid: @tags&.first&.tid)
.collect(&:nid)
(ftp_nids & tag_nids).count # intersection of 2 collections
end
@overall_contributor_tally = Tag.contributors(@tag_name, start: @start, finish: @end).size
@all_subscriptions = TagSelection.graph(@start, @end)
total_questions = Node.published.questions
.where(created: @start.to_i..@end.to_i)
.where(nid: Node.find_by_tag(tagname))
@answers = total_questions.joins(:comments).size.size
@questions = total_questions.size.size
end
def comments
@qids = Node.questions.where(status: 1).collect(&:nid)
@wildcard = true if params[:id][-1..-1] == '*' # wildcard tags
fetch_counts
get_wiki
@title = params[:id]
@contributor_count = Tag.contributor_count(params[:id]) || 0
tids = Tag.where(name: params[:id]).collect(&:tid)
nids = NodeTag.where('tid IN (?)', tids).collect(&:nid)
@pagy, @comments = pagy(Comment.where(nid: nids).order('timestamp DESC'), items: 24)
@node_type = "comments"
render 'show'
end
private
def order_string
if params[:search] || @toggle == "uses"
params[:order].blank? || (params[:order] == "desc") ? "count DESC" : "count ASC"
else
params[:order].blank? || (params[:order] == "desc") ? "name DESC" : "name ASC"
end
end
def get_wiki
if params[:id].is_a? Integer
@wiki = Node.find(params[:id])&.first
elsif params[:id].to_s.match?(":")
@wiki = Node.where(slug: params[:id].match('[^:]*$').to_s).try(:first)
else
@wiki = Node.where(path: "/wiki/#{params[:id]}").try(:first) || Node.where(path: "/#{params[:id]}").try(:first)
@wiki = Node.where(slug: @wiki.power_tag('redirect'))&.first if @wiki&.has_power_tag('redirect') # use a redirected wiki page if it exists
end
end
def fetch_counts
# Enhancement #6306 - Add counts to `by type` dropdown on tag pages
@counts = {}
@counts[:posts] = Node.for_tagname_and_type(params[:id], 'note', wildcard: @wildcard).where('node.nid NOT IN (?)', @qids).size
@counts[:questions] = Node.for_tagname_and_type(params[:id], 'note', question: true, wildcard: @wildcard).where('node.nid IN (?)', @qids).size
@counts[:wiki] = Node.for_tagname_and_type(params[:id], 'page', wildcard: @wildcard).size
params[:counts] = @counts
# end Enhancement #6306 ============================================
@total_posts = case @node_type
when 'note'
@notes.size
when 'questions'
@questions.size
when 'wiki'
@wikis.size
when 'maps'
@nodes.size
end
end
def topic_tree; end
end