Add tag functionality as provider for other plugins

This commit is contained in:
Alexander Meindl 2017-08-05 16:13:34 +02:00
parent 1f41f55097
commit 80c0d5c821
8 changed files with 261 additions and 0 deletions

View File

@ -0,0 +1,121 @@
module AdditionalsTagHelper
# Renders list of tags
# Clouds are rendered as block <tt>div</tt> with internal <tt>span</t> per tag.
# Lists are rendered as unordered lists <tt>ul</tt>. Lists are ordered by
# <tt>tag.count</tt> descending.
# === Parameters
# * <i>tags</i> = Array of Tag instances
# * <i>options</i> = (optional) Options (override system settings)
# * show_count - Boolean. Whenever show tag counts
# * style - list, cloud
def render_additionals_tags_list(tags, options = {})
return if tags.blank?
content = ''
style = options.delete(:style)
# prevent ActsAsTaggableOn::TagsHelper from calling `all`
# otherwise we will need sort tags after `tag_cloud`
tags = tags.all if tags.respond_to?(:all)
if style == :list
list_el = 'ul'
item_el = 'li'
else
list_el = 'div'
item_el = 'span'
tags = cloudify(tags)
end
content = content.html_safe
tag_cloud tags, (1..8).to_a do |tag, weight|
content << ' '.html_safe + content_tag(item_el,
render_additionals_tag_link(tag, options),
class: "tag-pass-#{weight}",
style: (style == :simple_cloud ? 'font-size: 1em;' : '')) + ' '.html_safe
end
content_tag(list_el, content, class: 'tags', style: (style == :simple_cloud ? 'text-align: left;' : ''))
end
def render_additionals_tag_link_line(tags)
s = []
tags.each do |tag|
s << render_additionals_tag_link(tag)
end
safe_join(s, ', ')
end
# Returns tag link
# === Parameters
# * <i>tag</i> = Instance of Tag
# * <i>options</i> = (optional) Options (override system settings)
# * show_count - Boolean. Whenever show tag counts
def render_additionals_tag_link(tag, options = {})
filters = [[:tags, '=', tag.name]]
content = if options[:use_search]
link_to(tag, controller: 'search', action: 'index', id: @project, q: tag.name, wiki_pages: true, issues: true)
else
additionals_link_to_filter(tag.name, filters, project_id: @project)
end
if options[:show_count]
content << content_tag('span', " (#{tag.count})", class: 'tag-count')
end
style = { class: 'tag-label-color', style: "background-color: #{tag_color(tag)}" }
content_tag('span', content, style)
end
def additionals_link_to_filter(title, filters, options = {})
options.merge! additionals_link_to_filter_options(filters)
link_to title, options
end
private
def tag_cloud(tags, classes)
return [] if tags.empty?
max_count = tags.sort_by(&:count).last.count.to_f
tags.each do |tag|
index = ((tag.count / max_count) * (classes.size - 1))
yield tag, classes[index.nan? ? 0 : index.round]
end
end
def tag_color(tag)
tag_name = tag.respond_to?(:name) ? tag.name : tag
"##{Digest::MD5.hexdigest(tag_name)[0..5]}"
end
def cloudify(tags)
new_tags = []
trigger = true
tags.each do |tag|
new_tags.send((trigger ? 'push' : 'unshift'), tag)
trigger = !trigger
end
new_tags
end
def additionals_link_to_filter_options(filters)
options = {
controller: controller_name,
action: 'index',
set_filter: 1,
fields: [],
values: {},
operators: {}
}
filters.each do |f|
name, operator, value = f
options[:fields].push(name)
options[:operators][name] = operator
options[:values][name] = [value]
end
options
end
end

View File

@ -0,0 +1,52 @@
class AdditionalsTag
def self.get_available_tags(klass, options = {}, permission = nil)
table_name = klass.table_name
scope = RedmineCrm::Tag.where({})
scope = scope.where("#{Project.table_name}.id = ?", options[:project]) if options[:project]
scope = scope.where(tag_access(permission))
scope = scope.where("LOWER(#{RedmineCrm::Tag.table_name}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
joins = []
joins << "JOIN #{RedmineCrm::Tagging.table_name} " \
"ON #{RedmineCrm::Tagging.table_name}.tag_id = #{RedmineCrm::Tag.table_name}.id "
joins << "JOIN #{table_name} " \
"ON #{table_name}.id = #{RedmineCrm::Tagging.table_name}.taggable_id " \
"AND #{RedmineCrm::Tagging.table_name}.taggable_type = '#{klass}' "
joins << "JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id "
scope = scope.select("#{RedmineCrm::Tag.table_name}.*, " \
"COUNT(DISTINCT #{RedmineCrm::Tagging.table_name}.taggable_id) AS count")
scope = scope.joins(joins.flatten)
scope = scope.group("#{RedmineCrm::Tag.table_name}.id, #{RedmineCrm::Tag.table_name}.name").having('COUNT(*) > 0')
scope = scope.order("#{RedmineCrm::Tag.table_name}.name")
scope
end
def self.tag_access(permission)
cond = ''
projects_allowed = if permission.nil?
Project.visible.pluck(:id)
else
Project.where(Project.allowed_to_condition(User.current, permission)).pluck(:id)
end
unless projects_allowed.empty?
cond << "#{Project.table_name}.id IN (#{projects_allowed.join(',')})"
end
cond
end
def self.remove_unused_tags
unused = RedmineCrm::Tag.find_by_sql(<<-SQL)
SELECT * FROM tags WHERE id NOT IN (
SELECT DISTINCT tag_id FROM taggings
)
SQL
unused.each(&:destroy)
end
def self.sql_for_tags_field(klass, operator, value)
compare = operator.eql?('=') ? 'IN' : 'NOT IN'
ids_list = klass.tagged_with(value).map(&:id).push(0).join(',')
"( #{klass.table_name}.id #{compare} (#{ids_list}) ) "
end
end

View File

@ -0,0 +1,5 @@
<% unless entry.tag_list.empty? %>
<div class="tags attribute"><span class="label"><%= l(:field_tag_list) %>:</span>
<%= safe_join(entry.tag_counts.map { |t| render_additionals_tag_link(t, show_count: false) }, ' ') %>
</div>
<% end %>

View File

@ -0,0 +1,15 @@
<div id="<%= name %>_tag_candidates" class="autocomplete"></div>
<%= javascript_tag do %>
$('<%= id %>').tagit({
tagSource: function(search, showChoices) {
var that = this;
$.ajax({
url: '<%= send("auto_complete_#{name}_tags_url", @project) %>',
data: {q: search.term},
success: function(choices) {
showChoices(that._subtractArray(jQuery.parseJSON(choices), that.assignedTags()));
}
});
},
});
<% end %>

View File

@ -0,0 +1,4 @@
<%= raw @tags.map {|tag|
tag.name
}.to_json
%>

View File

@ -0,0 +1,4 @@
<%= raw @tags.map {|tag|
tag.name
}.to_json
%>

View File

@ -150,3 +150,60 @@
.smiley-failure {
background-image: url(../images/smileys/failure-frame.png);
}
/* TAGs */
ul.tags {
list-style: none;
padding: 0;
}
.tag-label-color {
padding: 2px 4px;
color: #fff;
text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.5);
font-size: 10px !important;
border: 1px solid rgba(0, 0, 0, 0.2);
}
.tag-label-color a::before {
font-family: FontAwesome;
font-size: 1em;
font-weight: normal;
content: "\f02b"; /* fa-tag */
padding-right: 3px;
color: #fff;
}
ul.tags li { margin: 0.25em 0; }
div.tags .tag-pass-1 { font-size: 0.8em; }
div.tags .tag-pass-2 { font-size: 0.9em; }
div.tags .tag-pass-3 { font-size: 1em; }
div.tags .tag-pass-4 { font-size: 1.1em; }
div.tags .tag-pass-5 { font-size: 1.2em; }
div.tags .tag-pass-6 { font-size: 1.3em; }
div.tags .tag-pass-7 { font-size: 1.4em; }
div.tags .tag-pass-8 { font-size: 1.5em; }
.tag-count { font-size: 0.75em; }
.tag-label-color a { color: white; }
.tagit.ui-widget {
font-size: 1em;
margin: 0;
}
ul.tagit li.tagit-choice {
color: #505050;
font-weight: normal;
}
ul.tagit li.tagit-choice:hover,
ul.tagit li.tagit-choice.remove {
border-color: #6d95e0;
}
ul.tagit input[type="text"] {
background: transparent;
}

View File

@ -130,3 +130,6 @@ de:
permission_edit_issue_author: Ticket Autor bearbeiten
permission_change_new_issue_author: Autor bei Ticketerstellung festlegen
label_issue_autowatch_involved: Involvierte Benutzer automatisch als Beobachter hinzufügen
field_tag_list: Tags
field_tags: Tags
label_add_tags: Tags hinzufügen