Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Sam 2015-05-05 09:49:07 +10:00
commit 8bea398cc1
24 changed files with 328 additions and 124 deletions

View file

@ -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}}

View file

@ -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,

View file

@ -850,11 +850,13 @@ class User < ActiveRecord::Base
end end
def send_approval_email def send_approval_email
Jobs.enqueue(:user_email, if SiteSetting.must_approve_users
type: :signup_after_approval, Jobs.enqueue(:user_email,
user_id: id, type: :signup_after_approval,
email_token: email_tokens.first.token user_id: id,
) email_token: email_tokens.first.token
)
end
end end
def set_default_email_digest def set_default_email_digest
@ -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

View file

@ -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>

View file

@ -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"

View file

@ -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.

View file

@ -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)

View file

@ -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/"

View file

@ -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") });

View file

@ -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.[]")
}); });

View file

@ -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"),

View file

@ -1,13 +1,11 @@
<tbody> {{#each option in options}}
{{#each option in options}} <tr>
<tr> <td class="option">{{{option.html}}}</td>
<td class="option">{{{option.html}}}</td> <td class="percentage">{{option.percentage}}%</td>
<td class="percentage">{{option.percentage}}%</td> </tr>
</tr> <tr>
<tr> <td colspan="2" class="bar-back">
<td colspan="2" class="bar-back"> <div class="bar" {{bind-attr style=option.style}}></div>
<div class="bar" {{bind-attr style=option.style}}></div> </td>
</td> </tr>
</tr> {{/each}}
{{/each}}
</tbody>

View file

@ -1,36 +1,53 @@
<div class="poll-container"> <div>
{{#if showingResults}} <div class="poll-info">
{{#if isNumber}} <p>
{{poll-results-number poll=poll}} <span class="info-number">{{poll.voters}}</span>
{{else}} <span class="info-text">{{votersText}}</span>
{{poll-results-standard poll=poll}} </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}} {{/if}}
{{else}} </div>
<ul> <div class="poll-container">
{{#each option in poll.options}} {{#if showingResults}}
{{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}} {{#if isNumber}}
{{/each}} {{poll-results-number poll=poll}}
</ul> {{else}}
{{/if}} {{poll-results-standard poll=poll}}
{{/if}}
{{else}}
<ul>
{{#each option in poll.options}}
{{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}}
{{/each}}
</ul>
{{/if}}
</div>
</div> </div>
<p>{{totalVotesText}}</p> <div class="poll-buttons">
{{#if isMultiple}}
{{#if isMultiple}} {{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
<p>{{multipleHelpText}}</p>
{{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
{{/if}}
{{#if showingResults}}
{{d-button class="toggle-results" title="poll.hide-results.title" label="poll.hide-results.label" icon="eye-slash" disabled=hideResultsDisabled action="toggleResults"}}
{{else}}
{{d-button class="toggle-results" title="poll.show-results.title" label="poll.show-results.label" icon="eye" disabled=showResultsDisabled action="toggleResults"}}
{{/if}}
{{#if canToggleStatus}}
{{#if isClosed}}
{{d-button class="toggle-status" title="poll.open.title" label="poll.open.label" icon="unlock-alt" action="toggleStatus"}}
{{else}}
{{d-button class="toggle-status btn-danger" title="poll.close.title" label="poll.close.label" icon="lock" action="toggleStatus"}}
{{/if}} {{/if}}
{{/if}}
{{#if showingResults}}
{{d-button class="toggle-results" title="poll.hide-results.title" label="poll.hide-results.label" icon="eye-slash" disabled=hideResultsDisabled action="toggleResults"}}
{{else}}
{{d-button class="toggle-results" title="poll.show-results.title" label="poll.show-results.label" icon="eye" disabled=showResultsDisabled action="toggleResults"}}
{{/if}}
{{#if canToggleStatus}}
{{#if isClosed}}
{{d-button class="toggle-status" title="poll.open.title" label="poll.open.label" icon="unlock-alt" action="toggleStatus"}}
{{else}}
{{d-button class="toggle-status btn-danger" title="poll.close.title" label="poll.close.label" icon="lock" action="toggleStatus"}}
{{/if}}
{{/if}}
</div>

View file

@ -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]); }
}
// add "cast-votes" button poll.push(info);
result.push(["a", { "class": "button cast-votes", "title": I18n.t("poll.cast-votes.title") }, I18n.t("poll.cast-votes.label")]);
// 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
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-*");

View file

@ -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;
} }
} }

View file

@ -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"

View file

@ -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."

View file

@ -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."

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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)