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>
{{/if}}
<h3>
{{#if location}}{{fa-icon "map-maker"}}{{location}}{{/if}}
{{#if location}}{{fa-icon "map-marker"}}{{location}}{{/if}}
{{#if websiteName}}
{{fa-icon "globe"}}
{{#if linkWebsite}}

View file

@ -3,7 +3,7 @@ require_dependency 'email/message_builder'
class RejectionMailer < ActionMailer::Base
include Email::BuildEmailHelper
DISALLOWED_TEMPLATE_ARGS = [:to, :from, :site_name, :base_url,
DISALLOWED_TEMPLATE_ARGS = [:to, :from, :base_url,
:user_preferences_url,
:include_respond_instructions, :html_override,
:add_unsubscribe_link, :respond_instructions,

View file

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

View file

@ -36,9 +36,10 @@
<%=form_tag({}, method: :put) do %>
<p>
<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>
</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>
<%=submit_tag( @user.has_password? ? t('password_reset.update') : t('password_reset.save'), class: 'btn')%>
</p>
@ -48,5 +49,11 @@
</div>
<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>

View file

@ -375,7 +375,7 @@ en:
dismiss_notifications: "Mark all as Read"
dismiss_notifications_tooltip: "Mark all unread notifications as read"
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"
external_links_in_new_tab: "Open all external links in a new tab"
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."
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_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"
@ -1443,7 +1443,7 @@ en:
%{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.

View file

@ -50,6 +50,9 @@ class NewPostManager
def perform
# We never queue private messages
return perform_create_post if @args[:archetype] == Archetype.private_message
# Perform handlers until one returns a result
handled = NewPostManager.handlers.any? do |handler|
result = handler.call(self)

View file

@ -217,9 +217,6 @@ class Plugin::Instance
DiscoursePluginRegistry.register_glob(root_path, 'hbs')
end
# Automatically include all rake tasks
Rake.add_rakelib "#{File.dirname(path)}/lib/tasks"
self.instance_eval File.read(path), path
if auto_assets = generate_automatic_assets!
assets.concat auto_assets.map{|a| [a]}
@ -227,10 +224,18 @@ class Plugin::Instance
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 << 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"
if Dir.exists?(public_data)
target = Rails.root.to_s + "/public/plugins/"

View file

@ -12,9 +12,9 @@ export default Em.Component.extend({
}.property("poll.options.@each.{html,votes}"),
average: function() {
const total_votes = this.get("poll.total_votes");
return total_votes == 0 ? 0 : round(this.get("totalScore") / total_votes, -2);
}.property("totalScore", "poll.total_votes"),
const voters = this.get("poll.voters");
return voters === 0 ? 0 : round(this.get("totalScore") / voters, -2);
}.property("totalScore", "poll.voters"),
averageRating: function() {
return I18n.t("poll.average_rating", { average: this.get("average") });

View file

@ -3,11 +3,11 @@ export default Em.Component.extend({
classNames: ["results"],
options: function() {
const totalVotes = this.get("poll.total_votes"),
const voters = this.get("poll.voters"),
backgroundColor = this.get("poll.background");
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 + "%"];
if (backgroundColor) { styles.push("background: " + backgroundColor); }
@ -20,6 +20,6 @@ export default Em.Component.extend({
});
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"),
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
// - poll is closed
// - topic is archived/closed
// - user wants to see the results
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"),
poll: function() {
@ -37,10 +34,6 @@ export default Em.Controller.extend({
return _.map(this.get("poll.options").filterBy("selected"), o => o.get("id"));
}.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() {
let min = parseInt(this.get("poll.min"), 10);
if (isNaN(min) || min < 1) { min = 1; }
@ -54,6 +47,20 @@ export default Em.Controller.extend({
return max;
}.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() {
const options = this.get("poll.options.length"),
min = this.get("min"),

View file

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

View file

@ -1,36 +1,53 @@
<div class="poll-container">
{{#if showingResults}}
{{#if isNumber}}
{{poll-results-number poll=poll}}
{{else}}
{{poll-results-standard poll=poll}}
<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}}
{{else}}
<ul>
{{#each option in poll.options}}
{{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}}
{{/each}}
</ul>
{{/if}}
</div>
<div class="poll-container">
{{#if showingResults}}
{{#if isNumber}}
{{poll-results-number poll=poll}}
{{else}}
{{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>
<p>{{totalVotesText}}</p>
{{#if isMultiple}}
<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"}}
<div class="poll-buttons">
{{#if isMultiple}}
{{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
{{/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);
}
// that's our poll!
var result = ["div", attributes].concat(contents);
// // add some information when type is "multiple"
// 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") {
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
result.push(["a", { "class": "button cast-votes", "title": I18n.t("poll.cast-votes.title") }, I18n.t("poll.cast-votes.label")]);
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
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
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;
}
});
Discourse.Markdown.whiteListTag("div", "class", "poll");
Discourse.Markdown.whiteListTag("div", "class", /^poll-(info|container|buttons)/);
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("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 {
display: table;
border: 1px solid $border-color;
width: 500px;
ul, ol {
margin: 0;
padding: 0;
list-style: none;
display: inline-block;
max-width: 90%;
width: 100%;
}
li, .option {
@ -15,31 +26,29 @@ div.poll {
}
li[data-poll-option-id] {
color: $secondary;
background: $primary;
position: relative;
color: $option-foreground;
background: $option-background;
box-shadow: 0 6px $option-shadow;
padding: 0 .8em;
margin-bottom: .7em;
border-radius: .25rem;
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);
border-radius: 4px;
&:hover {
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.25),
inset 0 0 0 100px rgba(0,0,0,.1),
0 .2em 0 0 rgba(0,0,0,.2);
top: 2px;
box-shadow: 0 4px $option-shadow;
}
&:active {
-webkit-transform: translate(0,2px);
transform: translate(0,2px);
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);
top: 6px;
box-shadow: 0 0 $option-shadow;
}
&[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 {
margin: 0;
display: table-cell;
vertical-align: middle;
padding: 10px;
width: 330px;
span {
font-size: 1.125em;
line-height: 2
font-size: 2em;
}
}
.poll-buttons {
border-top: 1px solid $border-color;
padding: 10px;
.toggle-status {
float: right;
}
}
@ -80,16 +123,16 @@ div.poll {
font-size: 1.7em;
text-align: right;
vertical-align: middle;
color: #9E9E9E;
color: $text-color;
}
.bar-back {
background: rgb(219,219,219);
background: $border-color;
}
.bar {
height: 10px;
background: $primary;
background: $option-foreground;
}
}
@ -98,7 +141,7 @@ div.poll {
li {
display: inline-block;
margin-right: .7em;
margin: 0 12px 15px 5px;
}
}

View file

@ -8,19 +8,23 @@
en:
js:
poll:
voters:
zero: "voters"
one: "voter"
other: "voters"
total_votes:
zero: "No votes yet. Want to be the first?"
one: "There's only 1 vote."
other: "There are %{count} total votes."
zero: "total votes"
one: "total vote"
other: "total votes"
average_rating: "Average rating: <strong>%{average}</strong>."
multiple:
help:
at_least_min_options: "You may choose at least %{count} options."
up_to_max_options: "You may choose up to %{count} options."
x_options: "You may choose %{count} options."
between_min_and_max_options: "You may choose between %{min} and %{max} options."
at_least_min_options: "You may choose at least <strong>%{count}</strong> options."
up_to_max_options: "You may choose up to <strong>%{count}</strong> options."
x_options: "You may choose <strong>%{count}</strong> options."
between_min_and_max_options: "You may choose between <strong>%{min}</strong> and <strong>%{max}</strong> options."
cast-votes:
title: "Cast your votes"

View file

@ -8,7 +8,7 @@
es:
js:
poll:
total_votes:
voters:
zero: "Sin votos todavía. ¿Quieres ser el primero?"
one: "Sólo hay 1 voto."
other: "Hay %{count} votos en total."

View file

@ -8,7 +8,7 @@
fr:
js:
poll:
total_votes:
voters:
zero: "Pas encore de votes. Soyez le premier !"
one: "Il n'y a qu'un vote."
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] || []
# 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|
option["votes"] -= 1 if vote.include?(option["id"])
@ -116,7 +116,7 @@ after_initialize do
# extract polls
parsed.css("div.poll").each do |p|
poll = { "options" => [], "total_votes" => 0 }
poll = { "options" => [], "voters" => 0 }
# extract attributes
p.attributes.values.each do |attribute|
@ -208,9 +208,10 @@ after_initialize do
# only care when raw has changed!
return unless self.raw_changed?
extracted_polls = DiscoursePoll::Poll::extract(self.raw, self.topic_id)
polls = {}
extracted_polls = DiscoursePoll::Poll::extract(self.raw, self.topic_id)
extracted_polls.each do |poll|
# polls should have a unique name
if polls.has_key?(poll["name"])
@ -288,7 +289,7 @@ after_initialize do
next unless previous_polls.has_key?(poll_name)
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
polls[poll_name]["options"][o]["votes"] = previous_polls[poll_name]["options"][o]["votes"]
end

View file

@ -17,7 +17,7 @@ describe ::DiscoursePoll::PollsController do
expect(response).to be_success
json = ::JSON.parse(response.body)
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"])
end
@ -36,7 +36,7 @@ describe ::DiscoursePoll::PollsController do
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] }
expect(response).to be_success
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"][1]["votes"]).to eq(1)
end

View file

@ -17,6 +17,27 @@ describe NewPostManager do
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
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)
end
end
end
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(: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(
:user_email, has_entries(type: :signup_after_approval)
)
user.approve(admin)
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
before do
user.approve(admin)