mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
Merge branch 'master' of github.com:discourse/discourse
This commit is contained in:
commit
8bea398cc1
24 changed files with 328 additions and 124 deletions
|
@ -67,7 +67,7 @@
|
||||||
<h3>{{title}}</h3>
|
<h3>{{title}}</h3>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<h3>
|
<h3>
|
||||||
{{#if location}}{{fa-icon "map-maker"}}{{location}}{{/if}}
|
{{#if location}}{{fa-icon "map-marker"}}{{location}}{{/if}}
|
||||||
{{#if websiteName}}
|
{{#if websiteName}}
|
||||||
{{fa-icon "globe"}}
|
{{fa-icon "globe"}}
|
||||||
{{#if linkWebsite}}
|
{{#if linkWebsite}}
|
||||||
|
|
|
@ -3,7 +3,7 @@ require_dependency 'email/message_builder'
|
||||||
class RejectionMailer < ActionMailer::Base
|
class RejectionMailer < ActionMailer::Base
|
||||||
include Email::BuildEmailHelper
|
include Email::BuildEmailHelper
|
||||||
|
|
||||||
DISALLOWED_TEMPLATE_ARGS = [:to, :from, :site_name, :base_url,
|
DISALLOWED_TEMPLATE_ARGS = [:to, :from, :base_url,
|
||||||
:user_preferences_url,
|
:user_preferences_url,
|
||||||
:include_respond_instructions, :html_override,
|
:include_respond_instructions, :html_override,
|
||||||
:add_unsubscribe_link, :respond_instructions,
|
:add_unsubscribe_link, :respond_instructions,
|
||||||
|
|
|
@ -850,12 +850,14 @@ class User < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_approval_email
|
def send_approval_email
|
||||||
|
if SiteSetting.must_approve_users
|
||||||
Jobs.enqueue(:user_email,
|
Jobs.enqueue(:user_email,
|
||||||
type: :signup_after_approval,
|
type: :signup_after_approval,
|
||||||
user_id: id,
|
user_id: id,
|
||||||
email_token: email_tokens.first.token
|
email_token: email_tokens.first.token
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_default_email_digest
|
def set_default_email_digest
|
||||||
if has_attribute?(:email_digests) && self.email_digests.nil?
|
if has_attribute?(:email_digests) && self.email_digests.nil?
|
||||||
|
@ -887,7 +889,7 @@ class User < ActiveRecord::Base
|
||||||
to_destroy.each do |u|
|
to_destroy.each do |u|
|
||||||
begin
|
begin
|
||||||
destroyer.destroy(u, context: I18n.t(:purge_reason))
|
destroyer.destroy(u, context: I18n.t(:purge_reason))
|
||||||
rescue Discourse::InvalidAccess
|
rescue Discourse::InvalidAccess, UserDestroyer::PostsExistError
|
||||||
# if for some reason the user can't be deleted, continue on to the next one
|
# if for some reason the user can't be deleted, continue on to the next one
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,9 +36,10 @@
|
||||||
<%=form_tag({}, method: :put) do %>
|
<%=form_tag({}, method: :put) do %>
|
||||||
<p>
|
<p>
|
||||||
<span style="display: none;"><input name="username" type="text" value="<%= @user.username %>"></span>
|
<span style="display: none;"><input name="username" type="text" value="<%= @user.username %>"></span>
|
||||||
<input id="user_password" name="password" size="30" type="password" maxlength="<%= User.max_password_length %>">
|
<input id="user_password" name="password" size="30" type="password" maxlength="<%= User.max_password_length %>" onkeypress="capsLock(event)">
|
||||||
<label><%= t('js.user.password.instructions', count: SiteSetting.min_password_length) %></label>
|
<label><%= t('js.user.password.instructions', count: SiteSetting.min_password_length) %></label>
|
||||||
</p>
|
</p>
|
||||||
|
<div id="capsLockWarning" class="caps-lock-warning" style="visibility:hidden"><i class="fa fa-exclamation-triangle"></i> <%= t 'js.login.caps_lock_warning' %></div>
|
||||||
<p>
|
<p>
|
||||||
<%=submit_tag( @user.has_password? ? t('password_reset.update') : t('password_reset.save'), class: 'btn')%>
|
<%=submit_tag( @user.has_password? ? t('password_reset.update') : t('password_reset.save'), class: 'btn')%>
|
||||||
</p>
|
</p>
|
||||||
|
@ -48,5 +49,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.getElementById('user_password').focus()
|
document.getElementById('user_password').focus();
|
||||||
|
|
||||||
|
function capsLock(e) {
|
||||||
|
kc = e.keyCode?e.keyCode:e.which;
|
||||||
|
sk = e.shiftKey?e.shiftKey:((kc == 16)?true:false);
|
||||||
|
(((kc >= 65 && kc <= 90) && !sk)||((kc >= 97 && kc <= 122) && sk)) ? document.getElementById('capsLockWarning').style.visibility = 'visible' : document.getElementById('capsLockWarning').style.visibility = 'hidden';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -375,7 +375,7 @@ en:
|
||||||
dismiss_notifications: "Mark all as Read"
|
dismiss_notifications: "Mark all as Read"
|
||||||
dismiss_notifications_tooltip: "Mark all unread notifications as read"
|
dismiss_notifications_tooltip: "Mark all unread notifications as read"
|
||||||
disable_jump_reply: "Don't jump to my post after I reply"
|
disable_jump_reply: "Don't jump to my post after I reply"
|
||||||
dynamic_favicon: "Show incoming message notifications on favicon (experimental)"
|
dynamic_favicon: "Show new / updated topic count on browser icon"
|
||||||
edit_history_public: "Let other users view my post revisions"
|
edit_history_public: "Let other users view my post revisions"
|
||||||
external_links_in_new_tab: "Open all external links in a new tab"
|
external_links_in_new_tab: "Open all external links in a new tab"
|
||||||
enable_quoting: "Enable quote reply for highlighted text"
|
enable_quoting: "Enable quote reply for highlighted text"
|
||||||
|
|
|
@ -823,7 +823,7 @@ en:
|
||||||
|
|
||||||
traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak."
|
traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak."
|
||||||
post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)."
|
post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)."
|
||||||
must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site."
|
must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!"
|
||||||
ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics"
|
ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics"
|
||||||
ga_domain_name: "Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics"
|
ga_domain_name: "Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics"
|
||||||
ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics"
|
ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics"
|
||||||
|
@ -1443,7 +1443,7 @@ en:
|
||||||
|
|
||||||
%{flag_reason}
|
%{flag_reason}
|
||||||
|
|
||||||
Multiple community members flagged this post before it was hidden, so please consider how you might revise your post to reflect their feedback. **You can edit your post after %{edit_delay} minutes, and it will be automatically unhidden.** This will increase your trust level.
|
Multiple community members flagged this post before it was hidden, so please consider how you might revise your post to reflect their feedback. **You can edit your post after %{edit_delay} minutes, and it will be automatically unhidden.**
|
||||||
|
|
||||||
However, if the post is hidden by the community a second time, it will remain hidden until handled by staff – and there may be further action, including the possible suspension of your account.
|
However, if the post is hidden by the community a second time, it will remain hidden until handled by staff – and there may be further action, including the possible suspension of your account.
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,9 @@ class NewPostManager
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
|
||||||
|
# We never queue private messages
|
||||||
|
return perform_create_post if @args[:archetype] == Archetype.private_message
|
||||||
|
|
||||||
# Perform handlers until one returns a result
|
# Perform handlers until one returns a result
|
||||||
handled = NewPostManager.handlers.any? do |handler|
|
handled = NewPostManager.handlers.any? do |handler|
|
||||||
result = handler.call(self)
|
result = handler.call(self)
|
||||||
|
|
|
@ -217,9 +217,6 @@ class Plugin::Instance
|
||||||
DiscoursePluginRegistry.register_glob(root_path, 'hbs')
|
DiscoursePluginRegistry.register_glob(root_path, 'hbs')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Automatically include all rake tasks
|
|
||||||
Rake.add_rakelib "#{File.dirname(path)}/lib/tasks"
|
|
||||||
|
|
||||||
self.instance_eval File.read(path), path
|
self.instance_eval File.read(path), path
|
||||||
if auto_assets = generate_automatic_assets!
|
if auto_assets = generate_automatic_assets!
|
||||||
assets.concat auto_assets.map{|a| [a]}
|
assets.concat auto_assets.map{|a| [a]}
|
||||||
|
@ -227,10 +224,18 @@ class Plugin::Instance
|
||||||
|
|
||||||
register_assets! unless assets.blank?
|
register_assets! unless assets.blank?
|
||||||
|
|
||||||
# TODO possibly amend this to a rails engine
|
# TODO: possibly amend this to a rails engine
|
||||||
|
|
||||||
|
# Automatically include assets
|
||||||
Rails.configuration.assets.paths << auto_generated_path
|
Rails.configuration.assets.paths << auto_generated_path
|
||||||
Rails.configuration.assets.paths << File.dirname(path) + "/assets"
|
Rails.configuration.assets.paths << File.dirname(path) + "/assets"
|
||||||
|
|
||||||
|
# Automatically include rake tasks
|
||||||
|
Rake.add_rakelib(File.dirname(path) + "/lib/tasks")
|
||||||
|
|
||||||
|
# Automatically include migrations
|
||||||
|
Rails.configuration.paths["db/migrate"] << File.dirname(path) + "/db/migrate"
|
||||||
|
|
||||||
public_data = File.dirname(path) + "/public"
|
public_data = File.dirname(path) + "/public"
|
||||||
if Dir.exists?(public_data)
|
if Dir.exists?(public_data)
|
||||||
target = Rails.root.to_s + "/public/plugins/"
|
target = Rails.root.to_s + "/public/plugins/"
|
||||||
|
|
|
@ -12,9 +12,9 @@ export default Em.Component.extend({
|
||||||
}.property("poll.options.@each.{html,votes}"),
|
}.property("poll.options.@each.{html,votes}"),
|
||||||
|
|
||||||
average: function() {
|
average: function() {
|
||||||
const total_votes = this.get("poll.total_votes");
|
const voters = this.get("poll.voters");
|
||||||
return total_votes == 0 ? 0 : round(this.get("totalScore") / total_votes, -2);
|
return voters === 0 ? 0 : round(this.get("totalScore") / voters, -2);
|
||||||
}.property("totalScore", "poll.total_votes"),
|
}.property("totalScore", "poll.voters"),
|
||||||
|
|
||||||
averageRating: function() {
|
averageRating: function() {
|
||||||
return I18n.t("poll.average_rating", { average: this.get("average") });
|
return I18n.t("poll.average_rating", { average: this.get("average") });
|
||||||
|
|
|
@ -3,11 +3,11 @@ export default Em.Component.extend({
|
||||||
classNames: ["results"],
|
classNames: ["results"],
|
||||||
|
|
||||||
options: function() {
|
options: function() {
|
||||||
const totalVotes = this.get("poll.total_votes"),
|
const voters = this.get("poll.voters"),
|
||||||
backgroundColor = this.get("poll.background");
|
backgroundColor = this.get("poll.background");
|
||||||
|
|
||||||
this.get("poll.options").forEach(option => {
|
this.get("poll.options").forEach(option => {
|
||||||
const percentage = totalVotes == 0 ? 0 : Math.floor(100 * option.get("votes") / totalVotes),
|
const percentage = voters === 0 ? 0 : Math.floor(100 * option.get("votes") / voters),
|
||||||
styles = ["width: " + percentage + "%"];
|
styles = ["width: " + percentage + "%"];
|
||||||
|
|
||||||
if (backgroundColor) { styles.push("background: " + backgroundColor); }
|
if (backgroundColor) { styles.push("background: " + backgroundColor); }
|
||||||
|
@ -20,6 +20,6 @@ export default Em.Component.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.get("poll.options");
|
return this.get("poll.options");
|
||||||
}.property("poll.total_votes", "poll.options.[]")
|
}.property("poll.voters", "poll.options.[]")
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,16 +4,13 @@ export default Em.Controller.extend({
|
||||||
isRandom : Em.computed.equal("poll.order", "random"),
|
isRandom : Em.computed.equal("poll.order", "random"),
|
||||||
isClosed: Em.computed.equal("poll.status", "closed"),
|
isClosed: Em.computed.equal("poll.status", "closed"),
|
||||||
|
|
||||||
// immediately shows the results when the user has already voted
|
|
||||||
showResults: Em.computed.gt("vote.length", 0),
|
|
||||||
|
|
||||||
// shows the results when
|
// shows the results when
|
||||||
// - poll is closed
|
// - poll is closed
|
||||||
// - topic is archived/closed
|
// - topic is archived/closed
|
||||||
// - user wants to see the results
|
// - user wants to see the results
|
||||||
showingResults: Em.computed.or("isClosed", "post.topic.closed", "post.topic.archived", "showResults"),
|
showingResults: Em.computed.or("isClosed", "post.topic.closed", "post.topic.archived", "showResults"),
|
||||||
|
|
||||||
showResultsDisabled: Em.computed.equal("poll.total_votes", 0),
|
showResultsDisabled: Em.computed.equal("poll.voters", 0),
|
||||||
hideResultsDisabled: Em.computed.alias("isClosed"),
|
hideResultsDisabled: Em.computed.alias("isClosed"),
|
||||||
|
|
||||||
poll: function() {
|
poll: function() {
|
||||||
|
@ -37,10 +34,6 @@ export default Em.Controller.extend({
|
||||||
return _.map(this.get("poll.options").filterBy("selected"), o => o.get("id"));
|
return _.map(this.get("poll.options").filterBy("selected"), o => o.get("id"));
|
||||||
}.property("poll.options.@each.selected"),
|
}.property("poll.options.@each.selected"),
|
||||||
|
|
||||||
totalVotesText: function() {
|
|
||||||
return I18n.t("poll.total_votes", { count: this.get("poll.total_votes") });
|
|
||||||
}.property("poll.total_votes"),
|
|
||||||
|
|
||||||
min: function() {
|
min: function() {
|
||||||
let min = parseInt(this.get("poll.min"), 10);
|
let min = parseInt(this.get("poll.min"), 10);
|
||||||
if (isNaN(min) || min < 1) { min = 1; }
|
if (isNaN(min) || min < 1) { min = 1; }
|
||||||
|
@ -54,6 +47,20 @@ export default Em.Controller.extend({
|
||||||
return max;
|
return max;
|
||||||
}.property("poll.max", "poll.options.length"),
|
}.property("poll.max", "poll.options.length"),
|
||||||
|
|
||||||
|
votersText: function() {
|
||||||
|
return I18n.t("poll.voters", { count: this.get("poll.voters") });
|
||||||
|
}.property("poll.voters"),
|
||||||
|
|
||||||
|
totalVotes: function() {
|
||||||
|
return _.reduce(this.get("poll.options"), function(total, o) {
|
||||||
|
return total + parseInt(o.get("votes"), 10);
|
||||||
|
}, 0);
|
||||||
|
}.property("poll.options.@each.votes"),
|
||||||
|
|
||||||
|
totalVotesText: function() {
|
||||||
|
return I18n.t("poll.total_votes", { count: this.get("totalVotes") });
|
||||||
|
}.property("totalVotes"),
|
||||||
|
|
||||||
multipleHelpText: function() {
|
multipleHelpText: function() {
|
||||||
const options = this.get("poll.options.length"),
|
const options = this.get("poll.options.length"),
|
||||||
min = this.get("min"),
|
min = this.get("min"),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<tbody>
|
|
||||||
{{#each option in options}}
|
{{#each option in options}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="option">{{{option.html}}}</td>
|
<td class="option">{{{option.html}}}</td>
|
||||||
|
@ -10,4 +9,3 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
<div>
|
||||||
|
<div class="poll-info">
|
||||||
|
<p>
|
||||||
|
<span class="info-number">{{poll.voters}}</span>
|
||||||
|
<span class="info-text">{{votersText}}</span>
|
||||||
|
</p>
|
||||||
|
{{#if isMultiple}}
|
||||||
|
{{#if showingResults}}
|
||||||
|
<p>
|
||||||
|
<span class="info-number">{{totalVotes}}</span>
|
||||||
|
<span class="info-text">{{totalVotesText}}</span>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p>{{{multipleHelpText}}}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
<div class="poll-container">
|
<div class="poll-container">
|
||||||
{{#if showingResults}}
|
{{#if showingResults}}
|
||||||
{{#if isNumber}}
|
{{#if isNumber}}
|
||||||
|
@ -13,11 +30,10 @@
|
||||||
</ul>
|
</ul>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>{{totalVotesText}}</p>
|
<div class="poll-buttons">
|
||||||
|
|
||||||
{{#if isMultiple}}
|
{{#if isMultiple}}
|
||||||
<p>{{multipleHelpText}}</p>
|
|
||||||
{{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
|
{{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -34,3 +50,4 @@
|
||||||
{{d-button class="toggle-status btn-danger" title="poll.close.title" label="poll.close.label" icon="lock" action="toggleStatus"}}
|
{{d-button class="toggle-status btn-danger" title="poll.close.title" label="poll.close.label" icon="lock" action="toggleStatus"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
|
@ -97,13 +97,25 @@
|
||||||
contents[0][o].splice(1, 0, attr);
|
contents[0][o].splice(1, 0, attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// that's our poll!
|
// // add some information when type is "multiple"
|
||||||
var result = ["div", attributes].concat(contents);
|
// if (attributes[DATA_PREFIX + "type"] === "multiple") {
|
||||||
|
|
||||||
// add a small paragraph displaying the total number of votes
|
|
||||||
result.push(["p", I18n.t("poll.total_votes", { count: 0 })]);
|
|
||||||
|
|
||||||
// add some information when type is "multiple"
|
// }
|
||||||
|
|
||||||
|
var result = ["div", attributes],
|
||||||
|
poll = ["div"];
|
||||||
|
|
||||||
|
// 1 - POLL INFO
|
||||||
|
var info = ["div", { "class": "poll-info" }];
|
||||||
|
|
||||||
|
// # of voters
|
||||||
|
info.push(["p",
|
||||||
|
["span", { "class": "info-number" }, "0"],
|
||||||
|
["span", { "class": "info-text"}, I18n.t("poll.voters", { count: 0 })]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// multiple help text
|
||||||
if (attributes[DATA_PREFIX + "type"] === "multiple") {
|
if (attributes[DATA_PREFIX + "type"] === "multiple") {
|
||||||
var optionCount = contents[0].length - 1;
|
var optionCount = contents[0].length - 1;
|
||||||
|
|
||||||
|
@ -130,22 +142,40 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (help) { result.push(["p", help]); }
|
if (help) { info.push(["p", help]); }
|
||||||
|
}
|
||||||
|
|
||||||
|
poll.push(info);
|
||||||
|
|
||||||
|
// 2 - POLL CONTAINER
|
||||||
|
var container = ["div", { "class": "poll-container" }].concat(contents);
|
||||||
|
poll.push(container);
|
||||||
|
|
||||||
|
// 3 - BUTTONS
|
||||||
|
var buttons = ["div", { "class": "poll-buttons" }];
|
||||||
|
|
||||||
// add "cast-votes" button
|
// add "cast-votes" button
|
||||||
result.push(["a", { "class": "button cast-votes", "title": I18n.t("poll.cast-votes.title") }, I18n.t("poll.cast-votes.label")]);
|
if (attributes[DATA_PREFIX + "type"] === "multiple") {
|
||||||
|
buttons.push(["a", { "class": "button cast-votes", "title": I18n.t("poll.cast-votes.title") }, I18n.t("poll.cast-votes.label")]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add "toggle-results" button
|
// add "toggle-results" button
|
||||||
result.push(["a", { "class": "button toggle-results", "title": I18n.t("poll.show-results.title") }, I18n.t("poll.show-results.label")]);
|
buttons.push(["a", { "class": "button toggle-results", "title": I18n.t("poll.show-results.title") }, I18n.t("poll.show-results.label")]);
|
||||||
|
|
||||||
|
// 4 - MIX IT ALL UP
|
||||||
|
result.push(poll);
|
||||||
|
result.push(buttons)
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag("div", "class", "poll");
|
Discourse.Markdown.whiteListTag("div", "class", "poll");
|
||||||
|
Discourse.Markdown.whiteListTag("div", "class", /^poll-(info|container|buttons)/);
|
||||||
Discourse.Markdown.whiteListTag("div", "data-*");
|
Discourse.Markdown.whiteListTag("div", "data-*");
|
||||||
|
|
||||||
|
Discourse.Markdown.whiteListTag("span", "class", /^info-(number|text)/);
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag("a", "class", /^button (cast-votes|toggle-results)/);
|
Discourse.Markdown.whiteListTag("a", "class", /^button (cast-votes|toggle-results)/);
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag("li", "data-*");
|
Discourse.Markdown.whiteListTag("li", "data-*");
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
|
$border-color: rgb(219,219,219);
|
||||||
|
$text-color: #9E9E9E;
|
||||||
|
|
||||||
|
$option-foreground: $primary;
|
||||||
|
$option-background: dark-light-diff($primary, $secondary, 90%, -65%);
|
||||||
|
$option-shadow: dark-light-diff($option-background, $primary, 10%, -10%);
|
||||||
|
|
||||||
div.poll {
|
div.poll {
|
||||||
|
|
||||||
|
display: table;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
ul, ol {
|
ul, ol {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 90%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
li, .option {
|
li, .option {
|
||||||
|
@ -15,31 +26,29 @@ div.poll {
|
||||||
}
|
}
|
||||||
|
|
||||||
li[data-poll-option-id] {
|
li[data-poll-option-id] {
|
||||||
color: $secondary;
|
position: relative;
|
||||||
background: $primary;
|
color: $option-foreground;
|
||||||
|
background: $option-background;
|
||||||
|
box-shadow: 0 6px $option-shadow;
|
||||||
padding: 0 .8em;
|
padding: 0 .8em;
|
||||||
margin-bottom: .7em;
|
margin-bottom: .7em;
|
||||||
border-radius: .25rem;
|
border-radius: 4px;
|
||||||
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.2),
|
|
||||||
inset 0 0 0 100px rgba(0,0,0,0),
|
|
||||||
0 .2em 0 0 rgba(0,0,0,.2);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.25),
|
top: 2px;
|
||||||
inset 0 0 0 100px rgba(0,0,0,.1),
|
box-shadow: 0 4px $option-shadow;
|
||||||
0 .2em 0 0 rgba(0,0,0,.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
-webkit-transform: translate(0,2px);
|
top: 6px;
|
||||||
transform: translate(0,2px);
|
box-shadow: 0 0 $option-shadow;
|
||||||
box-shadow: inset 0 -.1em 0 0 rgba(0,0,0,.25),
|
|
||||||
inset 0 0 0 100px rgba(0,0,0,.1),
|
|
||||||
0 .1em 0 0 rgba(0,0,0,.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-poll-selected="selected"] {
|
&[data-poll-selected="selected"] {
|
||||||
background: green !important;
|
background: $success !important;
|
||||||
|
box-shadow: 0 6px darken($success, 10%);
|
||||||
|
&:hover { box-shadow: 0 4px darken($success, 10%); }
|
||||||
|
&:active { box-shadow: 0 0 darken($success, 10%); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,11 +67,45 @@ div.poll {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-info {
|
||||||
|
color: $text-color;
|
||||||
|
width: 150px;
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-right: 1px solid $border-color;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-number {
|
||||||
|
font-size: 3.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.poll-container {
|
.poll-container {
|
||||||
margin: 0;
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 10px;
|
||||||
|
width: 330px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 1.125em;
|
font-size: 2em;
|
||||||
line-height: 2
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-buttons {
|
||||||
|
border-top: 1px solid $border-color;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.toggle-status {
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,16 +123,16 @@ div.poll {
|
||||||
font-size: 1.7em;
|
font-size: 1.7em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
color: #9E9E9E;
|
color: $text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-back {
|
.bar-back {
|
||||||
background: rgb(219,219,219);
|
background: $border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background: $primary;
|
background: $option-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -98,7 +141,7 @@ div.poll {
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: .7em;
|
margin: 0 12px 15px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,23 @@
|
||||||
en:
|
en:
|
||||||
js:
|
js:
|
||||||
poll:
|
poll:
|
||||||
|
voters:
|
||||||
|
zero: "voters"
|
||||||
|
one: "voter"
|
||||||
|
other: "voters"
|
||||||
total_votes:
|
total_votes:
|
||||||
zero: "No votes yet. Want to be the first?"
|
zero: "total votes"
|
||||||
one: "There's only 1 vote."
|
one: "total vote"
|
||||||
other: "There are %{count} total votes."
|
other: "total votes"
|
||||||
|
|
||||||
average_rating: "Average rating: <strong>%{average}</strong>."
|
average_rating: "Average rating: <strong>%{average}</strong>."
|
||||||
|
|
||||||
multiple:
|
multiple:
|
||||||
help:
|
help:
|
||||||
at_least_min_options: "You may choose at least %{count} options."
|
at_least_min_options: "You may choose at least <strong>%{count}</strong> options."
|
||||||
up_to_max_options: "You may choose up to %{count} options."
|
up_to_max_options: "You may choose up to <strong>%{count}</strong> options."
|
||||||
x_options: "You may choose %{count} options."
|
x_options: "You may choose <strong>%{count}</strong> options."
|
||||||
between_min_and_max_options: "You may choose between %{min} and %{max} options."
|
between_min_and_max_options: "You may choose between <strong>%{min}</strong> and <strong>%{max}</strong> options."
|
||||||
|
|
||||||
cast-votes:
|
cast-votes:
|
||||||
title: "Cast your votes"
|
title: "Cast your votes"
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
es:
|
es:
|
||||||
js:
|
js:
|
||||||
poll:
|
poll:
|
||||||
total_votes:
|
voters:
|
||||||
zero: "Sin votos todavía. ¿Quieres ser el primero?"
|
zero: "Sin votos todavía. ¿Quieres ser el primero?"
|
||||||
one: "Sólo hay 1 voto."
|
one: "Sólo hay 1 voto."
|
||||||
other: "Hay %{count} votos en total."
|
other: "Hay %{count} votos en total."
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
fr:
|
fr:
|
||||||
js:
|
js:
|
||||||
poll:
|
poll:
|
||||||
total_votes:
|
voters:
|
||||||
zero: "Pas encore de votes. Soyez le premier !"
|
zero: "Pas encore de votes. Soyez le premier !"
|
||||||
one: "Il n'y a qu'un vote."
|
one: "Il n'y a qu'un vote."
|
||||||
other: "Il y a au total %{count} votes."
|
other: "Il y a au total %{count} votes."
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
class RenameTotalVotesToVoters < ActiveRecord::Migration
|
||||||
|
|
||||||
|
def up
|
||||||
|
PostCustomField.where(name: "polls").find_each do |pcf|
|
||||||
|
polls = ::JSON.parse(pcf.value)
|
||||||
|
polls.each_value do |poll|
|
||||||
|
next if poll.has_key?("voters")
|
||||||
|
poll["voters"] = poll["total_votes"]
|
||||||
|
poll.delete("total_votes")
|
||||||
|
end
|
||||||
|
pcf.value = polls.to_json
|
||||||
|
pcf.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
PostCustomField.where(name: "polls").find_each do |pcf|
|
||||||
|
polls = ::JSON.parse(pcf.value)
|
||||||
|
polls.each_value do |poll|
|
||||||
|
next if poll.has_key?("total_votes")
|
||||||
|
poll["total_votes"] = poll["voters"]
|
||||||
|
poll.delete("voters")
|
||||||
|
end
|
||||||
|
pcf.value = polls.to_json
|
||||||
|
pcf.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -57,7 +57,7 @@ after_initialize do
|
||||||
vote = votes[poll_name] || []
|
vote = votes[poll_name] || []
|
||||||
|
|
||||||
# increment counters only when the user hasn't casted a vote yet
|
# increment counters only when the user hasn't casted a vote yet
|
||||||
poll["total_votes"] += 1 if vote.size == 0
|
poll["voters"] += 1 if vote.size == 0
|
||||||
|
|
||||||
poll["options"].each do |option|
|
poll["options"].each do |option|
|
||||||
option["votes"] -= 1 if vote.include?(option["id"])
|
option["votes"] -= 1 if vote.include?(option["id"])
|
||||||
|
@ -116,7 +116,7 @@ after_initialize do
|
||||||
|
|
||||||
# extract polls
|
# extract polls
|
||||||
parsed.css("div.poll").each do |p|
|
parsed.css("div.poll").each do |p|
|
||||||
poll = { "options" => [], "total_votes" => 0 }
|
poll = { "options" => [], "voters" => 0 }
|
||||||
|
|
||||||
# extract attributes
|
# extract attributes
|
||||||
p.attributes.values.each do |attribute|
|
p.attributes.values.each do |attribute|
|
||||||
|
@ -208,9 +208,10 @@ after_initialize do
|
||||||
# only care when raw has changed!
|
# only care when raw has changed!
|
||||||
return unless self.raw_changed?
|
return unless self.raw_changed?
|
||||||
|
|
||||||
extracted_polls = DiscoursePoll::Poll::extract(self.raw, self.topic_id)
|
|
||||||
polls = {}
|
polls = {}
|
||||||
|
|
||||||
|
extracted_polls = DiscoursePoll::Poll::extract(self.raw, self.topic_id)
|
||||||
|
|
||||||
extracted_polls.each do |poll|
|
extracted_polls.each do |poll|
|
||||||
# polls should have a unique name
|
# polls should have a unique name
|
||||||
if polls.has_key?(poll["name"])
|
if polls.has_key?(poll["name"])
|
||||||
|
@ -288,7 +289,7 @@ after_initialize do
|
||||||
next unless previous_polls.has_key?(poll_name)
|
next unless previous_polls.has_key?(poll_name)
|
||||||
next unless polls[poll_name]["options"].size == previous_polls[poll_name]["options"].size
|
next unless polls[poll_name]["options"].size == previous_polls[poll_name]["options"].size
|
||||||
|
|
||||||
polls[poll_name]["total_votes"] = previous_polls[poll_name]["total_votes"]
|
polls[poll_name]["voters"] = previous_polls[poll_name]["voters"]
|
||||||
for o in 0...polls[poll_name]["options"].size
|
for o in 0...polls[poll_name]["options"].size
|
||||||
polls[poll_name]["options"][o]["votes"] = previous_polls[poll_name]["options"][o]["votes"]
|
polls[poll_name]["options"][o]["votes"] = previous_polls[poll_name]["options"][o]["votes"]
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe ::DiscoursePoll::PollsController do
|
||||||
expect(response).to be_success
|
expect(response).to be_success
|
||||||
json = ::JSON.parse(response.body)
|
json = ::JSON.parse(response.body)
|
||||||
expect(json["poll"]["name"]).to eq("poll")
|
expect(json["poll"]["name"]).to eq("poll")
|
||||||
expect(json["poll"]["total_votes"]).to eq(1)
|
expect(json["poll"]["voters"]).to eq(1)
|
||||||
expect(json["vote"]).to eq(["5c24fc1df56d764b550ceae1b9319125"])
|
expect(json["vote"]).to eq(["5c24fc1df56d764b550ceae1b9319125"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ describe ::DiscoursePoll::PollsController do
|
||||||
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] }
|
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] }
|
||||||
expect(response).to be_success
|
expect(response).to be_success
|
||||||
json = ::JSON.parse(response.body)
|
json = ::JSON.parse(response.body)
|
||||||
expect(json["poll"]["total_votes"]).to eq(1)
|
expect(json["poll"]["voters"]).to eq(1)
|
||||||
expect(json["poll"]["options"][0]["votes"]).to eq(0)
|
expect(json["poll"]["options"][0]["votes"]).to eq(0)
|
||||||
expect(json["poll"]["options"][1]["votes"]).to eq(1)
|
expect(json["poll"]["options"][1]["votes"]).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,27 @@ describe NewPostManager do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "default action" do
|
||||||
|
let(:other_user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
it "doesn't enqueue private messages" do
|
||||||
|
manager = NewPostManager.new(topic.user,
|
||||||
|
raw: 'this is a new post',
|
||||||
|
title: 'this is a new title',
|
||||||
|
archetype: Archetype.private_message,
|
||||||
|
target_usernames: other_user.username)
|
||||||
|
|
||||||
|
SiteSetting.approve_unless_trust_level = 4
|
||||||
|
result = manager.perform
|
||||||
|
|
||||||
|
expect(result.action).to eq(:create_post)
|
||||||
|
expect(result).to be_success
|
||||||
|
expect(result.post).to be_present
|
||||||
|
expect(result.post.topic.private_message?).to eq(true)
|
||||||
|
expect(result.post).to be_a(Post)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "default handler" do
|
context "default handler" do
|
||||||
let(:manager) { NewPostManager.new(topic.user, raw: 'this is new post content', topic_id: topic.id) }
|
let(:manager) { NewPostManager.new(topic.user, raw: 'this is new post content', topic_id: topic.id) }
|
||||||
|
|
||||||
|
@ -54,6 +75,7 @@ describe NewPostManager do
|
||||||
expect(result.action).to eq(:enqueued)
|
expect(result.action).to eq(:enqueued)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "extensibility priority" do
|
context "extensibility priority" do
|
||||||
|
|
29
spec/mailers/rejection_mailer_spec.rb
Normal file
29
spec/mailers/rejection_mailer_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
describe RejectionMailer do
|
||||||
|
|
||||||
|
describe "send_rejection" do
|
||||||
|
|
||||||
|
context 'sends rejection email' do
|
||||||
|
let (:user) { Fabricate(:user) }
|
||||||
|
let (:template_args) { {former_title: "Mail Subject", destination: user.email, site_name: SiteSetting.title} }
|
||||||
|
let (:reject_mail) { RejectionMailer.send_rejection("email_reject_topic_not_found", user.email, template_args) }
|
||||||
|
|
||||||
|
it 'renders the senders email' do
|
||||||
|
expect(reject_mail.to).to eql([user.email])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the subject' do
|
||||||
|
expect(reject_mail.subject).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders site title in subject' do
|
||||||
|
expect(reject_mail.subject).to match(SiteSetting.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the body' do
|
||||||
|
expect(reject_mail.body).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -45,13 +45,20 @@ describe User do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:admin) { Fabricate(:admin) }
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
it "enqueues a 'signup after approval' email" do
|
it "enqueues a 'signup after approval' email if must_approve_users is true" do
|
||||||
|
SiteSetting.stubs(:must_approve_users).returns(true)
|
||||||
Jobs.expects(:enqueue).with(
|
Jobs.expects(:enqueue).with(
|
||||||
:user_email, has_entries(type: :signup_after_approval)
|
:user_email, has_entries(type: :signup_after_approval)
|
||||||
)
|
)
|
||||||
user.approve(admin)
|
user.approve(admin)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't enqueue a 'signup after approval' email if must_approve_users is false" do
|
||||||
|
SiteSetting.stubs(:must_approve_users).returns(false)
|
||||||
|
Jobs.expects(:enqueue).never
|
||||||
|
user.approve(admin)
|
||||||
|
end
|
||||||
|
|
||||||
context 'after approval' do
|
context 'after approval' do
|
||||||
before do
|
before do
|
||||||
user.approve(admin)
|
user.approve(admin)
|
||||||
|
|
Loading…
Reference in a new issue