Merge branch 'back-from-fork'
This commit is contained in:
commit
5849ba4a09
|
@ -18,11 +18,14 @@
|
|||
|
||||
vendor/bundle
|
||||
public/assets
|
||||
upstream-patches
|
||||
|
||||
# templates to be created per-site
|
||||
app/views/home/privacy.*
|
||||
app/views/home/about.*
|
||||
app/views/home/chat.*
|
||||
app/views/home/404.*
|
||||
app/views/layouts/_footer.*
|
||||
app/assets/stylesheets/local/*
|
||||
public/favicon.ico
|
||||
public/apple-touch-icon*
|
||||
|
|
10
Gemfile
10
Gemfile
|
@ -1,6 +1,6 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "rails", "4.1.12"
|
||||
gem "rails", "4.2.8"
|
||||
|
||||
gem "unicorn"
|
||||
|
||||
|
@ -21,10 +21,14 @@ gem "dynamic_form"
|
|||
gem "exception_notification"
|
||||
|
||||
gem "bcrypt", "~> 3.1.2"
|
||||
gem "rotp"
|
||||
gem "rqrcode"
|
||||
|
||||
gem "nokogiri", "= 1.6.1"
|
||||
gem "htmlentities"
|
||||
gem "rdiscount"
|
||||
gem "commonmarker", "~> 0.14"
|
||||
|
||||
gem "activerecord-typedstore"
|
||||
|
||||
# for twitter-posting bot
|
||||
gem "oauth"
|
||||
|
@ -33,7 +37,7 @@ gem "oauth"
|
|||
gem "mail"
|
||||
|
||||
group :test, :development do
|
||||
gem "rspec-rails", "~> 2.6"
|
||||
gem "rspec-rails", "~> 3.5", ">= 3.5.2"
|
||||
gem "machinist"
|
||||
gem "sqlite3"
|
||||
gem "faker"
|
||||
|
|
|
@ -116,7 +116,12 @@ var _Lobsters = Class.extend({
|
|||
|
||||
var li = $(voterEl).closest(".story, .comment");
|
||||
var scoreDiv = li.find("div.score").get(0);
|
||||
var score = parseInt(scoreDiv.innerHTML);
|
||||
var score = 0;
|
||||
var showScore = true;
|
||||
if (scoreDiv.innerHTML == "-")
|
||||
showScore = false;
|
||||
else
|
||||
score = parseInt(scoreDiv.innerHTML);
|
||||
var action = "";
|
||||
|
||||
if (li.hasClass("upvoted") && point > 0) {
|
||||
|
@ -150,7 +155,8 @@ var _Lobsters = Class.extend({
|
|||
action = "downvote";
|
||||
}
|
||||
|
||||
scoreDiv.innerHTML = score;
|
||||
if (showScore)
|
||||
scoreDiv.innerHTML = score;
|
||||
|
||||
if (action == "upvote" || action == "unvote") {
|
||||
li.find(".reason").html("");
|
||||
|
@ -174,12 +180,7 @@ var _Lobsters = Class.extend({
|
|||
if ($(form).find('#parent_comment_short_id').length) {
|
||||
$(form).closest('.comment').replaceWith($.parseHTML(data));
|
||||
} else {
|
||||
if ($(form).attr("id").match(/^edit_comment_.+$/)) {
|
||||
$(form).parent(".comment").replaceWith($.parseHTML(data));
|
||||
} else {
|
||||
$(form).closest('.comment').after($.parseHTML(data));
|
||||
$(form).find('textarea').val('');
|
||||
}
|
||||
$(form).parent(".comment").replaceWith($.parseHTML(data));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -338,6 +339,16 @@ $(document).ready(function() {
|
|||
return Lobsters.moderateStory(this);
|
||||
}),
|
||||
|
||||
$(".toggle_dragons").click(function(a) {
|
||||
var c = $(".dragon_threads").first();
|
||||
if (c) {
|
||||
if (c.hasClass("hidden"))
|
||||
c.removeClass("hidden");
|
||||
else
|
||||
c.addClass("hidden");
|
||||
}
|
||||
}),
|
||||
|
||||
$(document).on("click", "a.comment_replier", function() {
|
||||
if (!Lobsters.curUser) {
|
||||
Lobsters.bounceToLogin();
|
||||
|
@ -408,6 +419,34 @@ $(document).ready(function() {
|
|||
});
|
||||
}
|
||||
});
|
||||
$(document).on("click", "a.comment_undeletor", function() {
|
||||
if (confirm("Êtes-vous sûr de vouloir restaurer ce commentaire ?")) {
|
||||
var li = $(this).closest(".comment");
|
||||
$.post("/comments/" + $(li).attr("data-shortid") + "/undelete",
|
||||
function(d) {
|
||||
$(li).replaceWith(d);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", "a.comment_dragon", function() {
|
||||
if (confirm("Êtes-vous sûr de vouloir marquer ce fil de discussion ?")) {
|
||||
var li = $(this).closest(".comment");
|
||||
$.post("/comments/" + $(li).attr("data-shortid") + "/dragon",
|
||||
function(d) {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
$(document).on("click", "a.comment_undragon", function() {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer la marque de ce fil de discussion ?")) {
|
||||
var li = $(this).closest(".comment");
|
||||
$.post("/comments/" + $(li).attr("data-shortid") + "/undragon",
|
||||
function(d) {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", "a.comment_moderator", function() {
|
||||
var reason = prompt("Raison de la modération :");
|
||||
|
@ -421,16 +460,6 @@ $(document).ready(function() {
|
|||
});
|
||||
});
|
||||
|
||||
$(document).on("click", "a.comment_undeletor", function() {
|
||||
if (confirm("Êtes-vous sûr de vouloir dé-supprimer ce commentaire ?")) {
|
||||
var li = $(this).closest(".comment");
|
||||
$.post("/comments/" + $(li).attr("data-shortid") + "/undelete",
|
||||
function(d) {
|
||||
$(li).replaceWith(d);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Lobsters.runSelect2();
|
||||
|
||||
$(document).on("click", "div.markdown_help_toggler .markdown_help_label",
|
||||
|
@ -484,4 +513,11 @@ $(document).ready(function() {
|
|||
$("#story_guidelines").toggle();
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
$('textarea#comment').keydown(function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.keyCode == 13) {
|
||||
$("button.comment-post").click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ body, textarea, input, button {
|
|||
|
||||
body {
|
||||
background-color: #fefefe;
|
||||
line-height: 1.45em;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -47,11 +48,11 @@ div.clear {
|
|||
a.tag {
|
||||
background-color: #fffcd7;
|
||||
border: 1px solid #d5d458;
|
||||
border-radius: 10px;
|
||||
border-radius: 5px;
|
||||
color: #555;
|
||||
font-size: 8pt;
|
||||
margin-left: 0.25em;
|
||||
padding: 0px 0.5em 1px 0.5em;
|
||||
padding: 0px 0.4em 1px 0.4em;
|
||||
text-decoration: none;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
@ -117,17 +118,16 @@ select,
|
|||
textarea {
|
||||
color: #555;
|
||||
background-color: white;
|
||||
line-height: 1.2em;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
textarea {
|
||||
line-height: 1.35em;
|
||||
resize: vertical;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
textarea {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
@ -232,6 +232,12 @@ button:disabled {
|
|||
color: gray;
|
||||
}
|
||||
|
||||
.totp_code::-webkit-inner-spin-button,
|
||||
.totp_code::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
/* outliners */
|
||||
|
||||
|
@ -366,7 +372,6 @@ ol.comments {
|
|||
margin-left: 20px;
|
||||
margin-bottom: 0em;
|
||||
padding-left: 6px;
|
||||
line-height: 1.35em;
|
||||
}
|
||||
ol.comments1 {
|
||||
margin-left: 0;
|
||||
|
@ -449,8 +454,8 @@ li.story {
|
|||
clear: both;
|
||||
}
|
||||
ol.stories li.story div.story_liner {
|
||||
padding-top: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
padding-top: 0.25em;
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
.comment {
|
||||
clear: both;
|
||||
|
@ -476,13 +481,17 @@ li div.details {
|
|||
opacity: 0.7;
|
||||
}
|
||||
.negative_3 {
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.negative_5 {
|
||||
opacity: 0.2;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.comment.highlighted {
|
||||
.comment.bad {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.comment:target {
|
||||
background-color: #fffcd7;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
@ -493,7 +502,7 @@ li .link {
|
|||
}
|
||||
|
||||
li .link a {
|
||||
font-size: 11pt;
|
||||
font-size: 11.5pt;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -555,9 +564,15 @@ li .comment_folder_button:checked ~ ol.comments li {
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* try to force a highlighted comment to stay visible */
|
||||
li .comment_folder_button:checked ~ div.comment div.comment_text,
|
||||
li .comment_folder_button:checked ~ div.comment div.voters {
|
||||
display: block;
|
||||
}
|
||||
|
||||
li .byline {
|
||||
color: #888;
|
||||
font-size: 9pt;
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
li .byline > img.avatar {
|
||||
margin-bottom: -5px;
|
||||
|
@ -601,6 +616,9 @@ span.user_is_author, a.user_is_author,
|
|||
li .byline a.user_is_author {
|
||||
color: #6081bd;
|
||||
}
|
||||
li .byline a.story_has_suggestions {
|
||||
color: #6081bd;
|
||||
}
|
||||
|
||||
li.story.hidden {
|
||||
opacity: 0.25;
|
||||
|
@ -628,7 +646,6 @@ div.story_content {
|
|||
ol.stories.list div.story_content {
|
||||
color: #777;
|
||||
max-height: 2.6em;
|
||||
line-height: 1.3em;
|
||||
margin: 0.25em 40px 0.25em 0;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
|
@ -674,11 +691,10 @@ div.page_link_buttons span {
|
|||
div.story_text {
|
||||
margin-bottom: 1.5em;
|
||||
max-width: 700px;
|
||||
line-height: 1.4em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
div.story_text p {
|
||||
margin: 0.75em 0;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
div.story_text img {
|
||||
max-width: 100%;
|
||||
|
@ -692,7 +708,9 @@ a#story_text_expander {
|
|||
}
|
||||
|
||||
div.comment_text {
|
||||
font-size: 10.5pt;
|
||||
max-width: 700px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
div.comment_text blockquote,
|
||||
|
@ -730,11 +748,21 @@ div.comment_text code {
|
|||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
div.comment_actions a {
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-size: 8.5pt;
|
||||
text-decoration: none;
|
||||
div.dragons {
|
||||
}
|
||||
.dragon_text {
|
||||
background-color: #f8f8f8;
|
||||
color: gray;
|
||||
font-style: italic;
|
||||
margin: 1em;
|
||||
padding: 2em 0 2.5em 0;
|
||||
text-align: center;
|
||||
}
|
||||
.dragon_text a {
|
||||
color: gray;
|
||||
}
|
||||
.dragon_threads.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.pagelink {
|
||||
|
@ -837,6 +865,11 @@ div.comment_form_container form {
|
|||
max-width: 700px;
|
||||
}
|
||||
|
||||
div.comment_form_container textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* trees */
|
||||
|
||||
|
@ -844,7 +877,6 @@ div.comment_form_container form {
|
|||
.tree ul {
|
||||
margin: 0 0 0 0.5em;
|
||||
padding: 0;
|
||||
line-height: 1.5em;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -890,6 +922,12 @@ div.comment_form_container form {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
li.noparent:before,
|
||||
ul.noparent:before {
|
||||
border-top: 0 !important;
|
||||
border-left: 0 !important;
|
||||
}
|
||||
|
||||
ul.user_tree {
|
||||
color: #888;
|
||||
}
|
||||
|
@ -1201,7 +1239,6 @@ div#flasher {
|
|||
div#flasher div.flash-error,
|
||||
div#flasher div.flash-notice,
|
||||
div#flasher div.flash-success {
|
||||
line-height: 1.5em;
|
||||
display: inline-block;
|
||||
padding-top: 25px;
|
||||
margin-left: auto;
|
||||
|
|
|
@ -242,6 +242,5 @@
|
|||
div#footer {
|
||||
text-align: center;
|
||||
float: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@ class ApplicationController < ActionController::Base
|
|||
TAG_FILTER_COOKIE = :tag_filters
|
||||
|
||||
def authenticate_user
|
||||
# eagerly evaluate, in case this triggers an IpSpoofAttackError
|
||||
request.remote_ip
|
||||
|
||||
if session[:u] &&
|
||||
(user = User.where(:session_token => session[:u].to_s).first) &&
|
||||
user.is_active?
|
||||
|
@ -50,13 +53,12 @@ class ApplicationController < ActionController::Base
|
|||
Rails.logger.info " Traffic level: #{@traffic.to_i}"
|
||||
end
|
||||
|
||||
if rand(2000000) == 1
|
||||
@traffic_color = sprintf("%02x%02x%02x",
|
||||
0, 0, [ 255, (@traffic * 7).floor + 50.0 ].min)
|
||||
else
|
||||
@traffic_color = sprintf("%02x%02x%02x",
|
||||
[ 255, (@traffic * 7).floor + 50.0 ].min, 0, 0)
|
||||
intensity = (@traffic * 7).floor + 50.0
|
||||
if (blue = (rand(2000000) == 1)) && @user
|
||||
Rails.logger.info " User #{@user.id} (#{@user.username}) saw blue logo"
|
||||
end
|
||||
color = (blue ? "0000%02x" : "%02x0000")
|
||||
@traffic_color = sprintf(color, intensity > 255 ? 255 : intensity)
|
||||
|
||||
true
|
||||
end
|
||||
|
|
|
@ -128,6 +128,26 @@ class CommentsController < ApplicationController
|
|||
:content_type => "text/html", :locals => { :comment => comment }
|
||||
end
|
||||
|
||||
def dragon
|
||||
if !((comment = find_comment) && @user.is_moderator?)
|
||||
return render :text => "can't find comment", :status => 400
|
||||
end
|
||||
|
||||
comment.become_dragon_for_user(@user)
|
||||
|
||||
render :text => "ok"
|
||||
end
|
||||
|
||||
def undragon
|
||||
if !((comment = find_comment) && @user.is_moderator?)
|
||||
return render :text => "can't find comment", :status => 400
|
||||
end
|
||||
|
||||
comment.remove_dragon_for_user(@user)
|
||||
|
||||
render :text => "ok"
|
||||
end
|
||||
|
||||
def update
|
||||
if !((comment = find_comment) && comment.is_editable_by_user?(@user))
|
||||
return render :text => "can't find comment", :status => 400
|
||||
|
@ -211,7 +231,7 @@ class CommentsController < ApplicationController
|
|||
@comments = Comment.where(
|
||||
:is_deleted => false, :is_moderated => false
|
||||
).order(
|
||||
"created_at DESC"
|
||||
"id DESC"
|
||||
).offset(
|
||||
(@page - 1) * COMMENTS_PER_PAGE
|
||||
).limit(
|
||||
|
@ -266,7 +286,7 @@ class CommentsController < ApplicationController
|
|||
comments = Comment.where(
|
||||
:thread_id => thread_ids
|
||||
).includes(
|
||||
:user, :story
|
||||
:user, :story, :hat, :votes => :user
|
||||
).arrange_for_user(
|
||||
@showing_user
|
||||
)
|
||||
|
@ -274,7 +294,7 @@ class CommentsController < ApplicationController
|
|||
comments_by_thread_id = comments.group_by(&:thread_id)
|
||||
@threads = comments_by_thread_id.values_at(*thread_ids).compact
|
||||
|
||||
if @user && (@showing_user.id == @user.id)
|
||||
if @user
|
||||
@votes = Vote.comment_votes_by_user_for_story_hash(@user.id,
|
||||
comments.map(&:story_id).uniq)
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ class HatsController < ApplicationController
|
|||
@hat_request.comment = params[:hat_request][:comment]
|
||||
|
||||
if @hat_request.save
|
||||
flash[:success] = "Successfully submitted hat request."
|
||||
flash[:success] = t('.submittedhatrequest')
|
||||
return redirect_to "/hats"
|
||||
end
|
||||
|
||||
|
@ -47,7 +47,7 @@ class HatsController < ApplicationController
|
|||
permit(:hat, :link))
|
||||
@hat_request.approve_by_user!(@user)
|
||||
|
||||
flash[:success] = "Successfully approved hat request."
|
||||
flash[:success] = t('.approvedhatrequest')
|
||||
|
||||
return redirect_to "/hats/requests"
|
||||
end
|
||||
|
@ -57,7 +57,7 @@ class HatsController < ApplicationController
|
|||
@hat_request.reject_by_user_for_reason!(@user,
|
||||
params[:hat_request][:rejection_comment])
|
||||
|
||||
flash[:success] = "Successfully rejected hat request."
|
||||
flash[:success] = t('.rejectedhatrequest')
|
||||
|
||||
return redirect_to "/hats/requests"
|
||||
end
|
||||
|
|
|
@ -4,11 +4,24 @@ class HomeController < ApplicationController
|
|||
before_filter { @page = page }
|
||||
before_filter :require_logged_in_user, :only => [ :upvoted ]
|
||||
|
||||
def four_oh_four
|
||||
begin
|
||||
@title = "Resource Not Found"
|
||||
render :action => "404", :status => 404
|
||||
rescue ActionView::MissingTemplate
|
||||
render :text => "<div class=\"box wide\">" <<
|
||||
"<div class=\"legend\">404</div>" <<
|
||||
"Resource not found" <<
|
||||
"</div>", :layout => "application"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def about
|
||||
begin
|
||||
@title = I18n.t 'controllers.home_controller.abouttitle'
|
||||
render :action => "about"
|
||||
rescue
|
||||
rescue ActionView::MissingTemplate
|
||||
render :text => I18n.t('controllers.home_controller.abouttext'), :layout => "application"
|
||||
end
|
||||
end
|
||||
|
@ -17,8 +30,9 @@ class HomeController < ApplicationController
|
|||
begin
|
||||
@title = I18n.t 'controllers.home_controller.chattitle'
|
||||
render :action => "chat"
|
||||
rescue
|
||||
rescue ActionView::MissingTemplate
|
||||
render :text => "<div class=\"box wide\">" <<
|
||||
"<div class=\"legend\">Chat</div>" <<
|
||||
"Keep it on-site" <<
|
||||
"</div>", :layout => "application"
|
||||
end
|
||||
|
@ -28,7 +42,7 @@ class HomeController < ApplicationController
|
|||
begin
|
||||
@title = I18n.t 'controllers.home_controller.privacytitle'
|
||||
render :action => "privacy"
|
||||
rescue
|
||||
rescue ActionView::MissingTemplate
|
||||
render :text => I18n.t('controllers.home_controller.licensetext'), :layout => "application"
|
||||
end
|
||||
end
|
||||
|
@ -239,7 +253,12 @@ private
|
|||
else
|
||||
key = opts.merge(page: page).sort.map{|k,v| "#{k}=#{v.to_param}"
|
||||
}.join(" ")
|
||||
Rails.cache.fetch("stories #{key}", :expires_in => 45, &block)
|
||||
begin
|
||||
Rails.cache.fetch("stories #{key}", :expires_in => 45, &block)
|
||||
rescue Errno::ENOENT => e
|
||||
Rails.logger.error "error fetching stories #{key}: #{e}"
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
class LoginBannedError < StandardError; end
|
||||
class LoginDeletedError < StandardError; end
|
||||
class LoginTOTPFailedError < StandardError; end
|
||||
class LoginFailedError < StandardError; end
|
||||
|
||||
class LoginController < ApplicationController
|
||||
before_filter :authenticate_user
|
||||
|
||||
|
@ -22,53 +27,104 @@ class LoginController < ApplicationController
|
|||
user = User.where(:username => params[:email]).first
|
||||
end
|
||||
|
||||
fail_reason = nil
|
||||
|
||||
begin
|
||||
if !user
|
||||
raise "no user"
|
||||
raise LoginFailedError
|
||||
end
|
||||
|
||||
if !user.try(:authenticate, params[:password].to_s)
|
||||
raise "authentication failed"
|
||||
if !user.authenticate(params[:password].to_s)
|
||||
# if the user has 2fa enabled and the password looks like it has a totp
|
||||
# code attached, separate them
|
||||
if user.has_2fa? &&
|
||||
(m = params[:password].to_s.match(/\A(.+):(\d+)\z/)) &&
|
||||
user.authenticate(m[1])
|
||||
params[:password] = m[1]
|
||||
params[:totp] = m[2]
|
||||
else
|
||||
raise LoginFailedError
|
||||
end
|
||||
end
|
||||
|
||||
if user.is_banned?
|
||||
raise "user is banned"
|
||||
raise LoginBannedError
|
||||
end
|
||||
|
||||
if !user.is_active?
|
||||
user.undelete!
|
||||
flash[:success] = "Your account has been reactivated and your " <<
|
||||
"unmoderated comments have been undeleted."
|
||||
raise LoginDeletedError
|
||||
end
|
||||
|
||||
session[:u] = user.session_token
|
||||
|
||||
if !user.password_digest.to_s.match(/^\$2a\$#{BCrypt::Engine::DEFAULT_COST}\$/)
|
||||
user.password = user.password_confirmation = params[:password].to_s
|
||||
user.save!
|
||||
end
|
||||
|
||||
if (rd = session[:redirect_to]).present?
|
||||
session.delete(:redirect_to)
|
||||
return redirect_to rd
|
||||
elsif params[:referer].present?
|
||||
begin
|
||||
ru = URI.parse(params[:referer])
|
||||
if ru.host == Rails.application.domain
|
||||
return redirect_to ru.to_s
|
||||
if user.has_2fa?
|
||||
if params[:totp].present?
|
||||
if user.authenticate_totp(params[:totp])
|
||||
# ok, fall through
|
||||
else
|
||||
raise LoginTOTPFailedError
|
||||
end
|
||||
else
|
||||
return respond_to do |format|
|
||||
format.html {
|
||||
session[:twofa_u] = user.session_token
|
||||
redirect_to "/login/2fa"
|
||||
}
|
||||
format.json {
|
||||
render :json => { :status => 0,
|
||||
:error => "must supply totp parameter" }
|
||||
}
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "error parsing referer: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
return redirect_to "/"
|
||||
rescue
|
||||
return respond_to do |format|
|
||||
format.html {
|
||||
session[:u] = user.session_token
|
||||
|
||||
if (rd = session[:redirect_to]).present?
|
||||
session.delete(:redirect_to)
|
||||
return redirect_to rd
|
||||
elsif params[:referer].present?
|
||||
begin
|
||||
ru = URI.parse(params[:referer])
|
||||
if ru.host == Rails.application.domain
|
||||
return redirect_to ru.to_s
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "error parsing referer: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to "/"
|
||||
}
|
||||
format.json {
|
||||
render :json => { :status => 1, :username => user.username }
|
||||
}
|
||||
end
|
||||
rescue LoginBannedError
|
||||
fail_reason = I18n.t 'controllers.login_controller.bannedaccount'
|
||||
rescue LoginDeletedError
|
||||
fail_reason = I18n.t 'controllers.login_controller.deletedaccount'
|
||||
rescue LoginTOTPFailedError
|
||||
fail_reason = I18n.t 'controllers.login_controller.totpinvalid'
|
||||
rescue LoginFailedError
|
||||
fail_reason = I18n.t 'controllers.login_controller.flashlogininvalid'
|
||||
end
|
||||
|
||||
flash.now[:error] = I18n.t 'controllers.login_controller.flashlogininvalid'
|
||||
@referer = params[:referer]
|
||||
index
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
flash.now[:error] = fail_reason
|
||||
@referer = params[:referer]
|
||||
index
|
||||
}
|
||||
format.json {
|
||||
render :json => { :status => 0, :error => fail_reason }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def forgot_password
|
||||
|
@ -114,16 +170,48 @@ class LoginController < ApplicationController
|
|||
end
|
||||
|
||||
if @reset_user.save && @reset_user.is_active?
|
||||
session[:u] = @reset_user.session_token
|
||||
return redirect_to "/"
|
||||
if @reset_user.has_2fa?
|
||||
flash[:success] = t('.passwordreset')
|
||||
return redirect_to "/login"
|
||||
else
|
||||
session[:u] = @reset_user.session_token
|
||||
return redirect_to "/"
|
||||
end
|
||||
else
|
||||
flash[:error] = "Could not reset password."
|
||||
flash[:error] = t('.couldnotresetpassword')
|
||||
end
|
||||
end
|
||||
else
|
||||
flash[:error] = "Invalid reset token. It may have already been " <<
|
||||
"used or you may have copied it incorrectly."
|
||||
flash[:error] = t('.invalidresettoken')
|
||||
return redirect_to forgot_password_path
|
||||
end
|
||||
end
|
||||
|
||||
def twofa
|
||||
if tmpu = find_twofa_user
|
||||
Rails.logger.info " Authenticated as user #{tmpu.id} " <<
|
||||
"(#{tmpu.username}), verifying TOTP"
|
||||
else
|
||||
reset_session
|
||||
return redirect_to "/login"
|
||||
end
|
||||
end
|
||||
|
||||
def twofa_verify
|
||||
if (tmpu = find_twofa_user) && tmpu.authenticate_totp(params[:totp_code])
|
||||
session[:u] = tmpu.session_token
|
||||
session.delete(:twofa_u)
|
||||
return redirect_to "/"
|
||||
else
|
||||
flash[:error] = t('.totpcodenotmatch')
|
||||
return redirect_to "/login/2fa"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_twofa_user
|
||||
if session[:twofa_u].present?
|
||||
return User.where(:session_token => session[:twofa_u]).first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,11 @@ class SearchController < ApplicationController
|
|||
end
|
||||
|
||||
if @search.valid?
|
||||
@search.search_for_user!(@user)
|
||||
begin
|
||||
@search.search_for_user!(@user)
|
||||
rescue ThinkingSph::ConnectionError
|
||||
flash[:error] = I18n.t 'controllers.search_controller.flasherrorsearchcontroller'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class SettingsController < ApplicationController
|
||||
before_filter :require_logged_in_user
|
||||
|
||||
TOTP_SESSION_TIMEOUT = (60 * 15)
|
||||
|
||||
def index
|
||||
@title = t('.accountsettings')
|
||||
|
||||
|
@ -19,9 +21,107 @@ class SettingsController < ApplicationController
|
|||
return redirect_to settings_path
|
||||
end
|
||||
|
||||
def pushover
|
||||
def update
|
||||
@edit_user = @user.clone
|
||||
|
||||
if params[:user][:password].empty? ||
|
||||
@user.authenticate(params[:current_password].to_s)
|
||||
if @edit_user.update_attributes(user_params)
|
||||
flash.now[:success] = t('.updatesettingsflash')
|
||||
@user = @edit_user
|
||||
end
|
||||
else
|
||||
flash[:error] = t('.passwordnotcorrect')
|
||||
end
|
||||
|
||||
render :action => "index"
|
||||
end
|
||||
|
||||
def twofa
|
||||
@title = t('.title')
|
||||
end
|
||||
|
||||
def twofa_auth
|
||||
if @user.authenticate(params[:user][:password].to_s)
|
||||
session[:last_authed] = Time.now.to_i
|
||||
session.delete(:totp_secret)
|
||||
|
||||
if @user.has_2fa?
|
||||
@user.disable_2fa!
|
||||
flash[:success] = t('.2fahasbeendisabled')
|
||||
return redirect_to "/settings"
|
||||
else
|
||||
return redirect_to twofa_enroll_url
|
||||
end
|
||||
else
|
||||
flash[:error] = t('.2fapassnotcorrect')
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
end
|
||||
|
||||
def twofa_enroll
|
||||
@title = t('.title')
|
||||
|
||||
if (Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT
|
||||
flash[:error] = t('.enrollmenttimeout')
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
|
||||
if !session[:totp_secret]
|
||||
session[:totp_secret] = ROTP::Base32.random_base32
|
||||
end
|
||||
|
||||
totp = ROTP::TOTP.new(session[:totp_secret],
|
||||
:issuer => Rails.application.name)
|
||||
totp_url = totp.provisioning_uri(@user.email)
|
||||
|
||||
# no option for inline svg, so just strip off leading <?xml> tag
|
||||
qrcode = RQRCode::QRCode.new(totp_url)
|
||||
qr = qrcode.as_svg(:offset => 0, color: "000", :module_size => 5,
|
||||
:shape_rendering => "crispEdges").gsub(/^<\?xml.*>/, "")
|
||||
|
||||
@qr_svg = "<a href=\"#{totp_url}\">#{qr}</a>"
|
||||
end
|
||||
|
||||
def twofa_verify
|
||||
@title = t('.title')
|
||||
|
||||
if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) ||
|
||||
!session[:totp_secret]
|
||||
flash[:error] = t('.enrollmenttimeout')
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
end
|
||||
|
||||
def twofa_update
|
||||
if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) ||
|
||||
!session[:totp_secret]
|
||||
flash[:error] = t('.enrollmenttimeout')
|
||||
return redirect_to twofa_url
|
||||
end
|
||||
|
||||
@user.totp_secret = session[:totp_secret]
|
||||
if @user.authenticate_totp(params[:totp_code])
|
||||
# re-roll, just in case
|
||||
@user.session_token = nil
|
||||
@user.save!
|
||||
|
||||
session[:u] = @user.session_token
|
||||
|
||||
flash[:success] = t('.2fahasbeenenabled')
|
||||
session.delete(:totp_secret)
|
||||
return redirect_to "/settings"
|
||||
else
|
||||
flash[:error] = t('.totpinvalid')
|
||||
return redirect_to twofa_verify_url
|
||||
end
|
||||
end
|
||||
|
||||
# external services
|
||||
|
||||
def pushover_auth
|
||||
if !Pushover.SUBSCRIPTION_CODE
|
||||
flash[:error] = t('.pushovernotconfigured')
|
||||
flash[:error] = "This site is not configured for Pushover"
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
|
@ -36,12 +136,12 @@ class SettingsController < ApplicationController
|
|||
|
||||
def pushover_callback
|
||||
if !session[:pushover_rand].to_s.present?
|
||||
flash[:error] = t('.pushovernorandomtokensession')
|
||||
flash[:error] = "No random token present in session"
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
if !params[:rand].to_s.present?
|
||||
flash[:error] = t('.pushovernorandomtokenurl')
|
||||
flash[:error] = "No random token present in URL"
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
|
@ -54,23 +154,88 @@ class SettingsController < ApplicationController
|
|||
@user.save!
|
||||
|
||||
if @user.pushover_user_key.present?
|
||||
flash[:success] = t('.accountsetuppushover')
|
||||
flash[:success] = "Your account is now setup for Pushover notifications."
|
||||
else
|
||||
flash[:success] = t('.accountnolongersetuppushover')
|
||||
flash[:success] = "Your account is no longer setup for Pushover " <<
|
||||
"notifications."
|
||||
end
|
||||
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
def update
|
||||
@edit_user = @user.clone
|
||||
def github_auth
|
||||
session[:github_state] = SecureRandom.hex
|
||||
return redirect_to Github.oauth_auth_url(session[:github_state])
|
||||
end
|
||||
|
||||
if @edit_user.update_attributes(user_params)
|
||||
flash.now[:success] = t('.updatesettingsflash')
|
||||
@user = @edit_user
|
||||
def github_callback
|
||||
if !session[:github_state].present? || !params[:code].present? ||
|
||||
(params[:state].to_s != session[:github_state].to_s)
|
||||
flash[:error] = "Invalid OAuth state"
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
render :action => "index"
|
||||
session.delete(:github_state)
|
||||
|
||||
tok, username = Github.token_and_user_from_code(params[:code])
|
||||
if tok.present? && username.present?
|
||||
@user.github_oauth_token = tok
|
||||
@user.github_username = username
|
||||
@user.save!
|
||||
flash[:success] = "Your account has been linked to GitHub user " <<
|
||||
"#{username}."
|
||||
else
|
||||
return github_disconnect
|
||||
end
|
||||
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
def github_disconnect
|
||||
@user.github_oauth_token = nil
|
||||
@user.github_username = nil
|
||||
@user.save!
|
||||
flash[:success] = "Your GitHub association has been removed."
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
def twitter_auth
|
||||
session[:twitter_state] = SecureRandom.hex
|
||||
return redirect_to Twitter.oauth_auth_url(session[:twitter_state])
|
||||
end
|
||||
|
||||
def twitter_callback
|
||||
if !session[:twitter_state].present? ||
|
||||
(params[:state].to_s != session[:twitter_state].to_s)
|
||||
flash[:error] = "Invalid OAuth state"
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
session.delete(:twitter_state)
|
||||
|
||||
tok, sec, username = Twitter.token_secret_and_user_from_token_and_verifier(
|
||||
params[:oauth_token], params[:oauth_verifier])
|
||||
if tok.present? && username.present?
|
||||
@user.twitter_oauth_token = tok
|
||||
@user.twitter_oauth_token_secret = sec
|
||||
@user.twitter_username = username
|
||||
@user.save!
|
||||
flash[:success] = "Your account has been linked to Twitter user @" <<
|
||||
"#{username}."
|
||||
else
|
||||
return twitter_disconnect
|
||||
end
|
||||
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
def twitter_disconnect
|
||||
@user.twitter_oauth_token = nil
|
||||
@user.twitter_username = nil
|
||||
@user.twitter_oauth_token_secret = nil
|
||||
@user.save!
|
||||
flash[:success] = "Your Twitter association has been removed."
|
||||
return redirect_to "/settings"
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -81,7 +246,7 @@ private
|
|||
:email_replies, :email_messages, :email_mentions,
|
||||
:pushover_replies, :pushover_messages, :pushover_mentions,
|
||||
:mailing_list_mode, :show_avatars, :show_story_previews,
|
||||
:show_submitted_story_threads
|
||||
:show_submitted_story_threads, :hide_dragons
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -126,25 +126,15 @@ class StoriesController < ApplicationController
|
|||
return redirect_to @story.merged_into_story.comments_path
|
||||
end
|
||||
|
||||
if @story.can_be_seen_by_user?(@user)
|
||||
@title = @story.title
|
||||
else
|
||||
@title = "[Story removed]"
|
||||
if !@story.can_be_seen_by_user?(@user)
|
||||
raise ActionController::RoutingError.new("story gone")
|
||||
end
|
||||
|
||||
@title = @story.title
|
||||
@short_url = @story.short_id_url
|
||||
|
||||
@comments = @story.merged_comments.includes(:user, :story,
|
||||
:hat).arrange_for_user(@user)
|
||||
|
||||
if params[:comment_short_id]
|
||||
@comments.each do |c,x|
|
||||
if c.short_id == params[:comment_short_id]
|
||||
c.highlighted = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
@comments = @story.merged_comments.includes(:user, :story, :hat,
|
||||
:votes => :user).arrange_for_user(@user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
|
@ -160,6 +150,10 @@ class StoriesController < ApplicationController
|
|||
"apple-touch-icon-144.png",
|
||||
}
|
||||
|
||||
if @story.user.twitter_username.present?
|
||||
@meta_tags["twitter:creator"] = "@" + @story.user.twitter_username
|
||||
end
|
||||
|
||||
load_user_votes
|
||||
|
||||
render :action => "show"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class UsersController < ApplicationController
|
||||
before_filter :require_logged_in_moderator, :only => [ :enable_invitation,
|
||||
:disable_invitation,
|
||||
:ban, :unban ]
|
||||
before_filter :require_logged_in_moderator,
|
||||
:only => [ :enable_invitation, :disable_invitation, :ban, :unban ]
|
||||
|
||||
def show
|
||||
@showing_user = User.where(:username => params[:username]).first!
|
||||
|
@ -16,11 +15,17 @@ class UsersController < ApplicationController
|
|||
def tree
|
||||
@title = I18n.t 'controllers.users_controller.usertitle'
|
||||
|
||||
newest_user = User.last.id
|
||||
|
||||
if params[:by].to_s == "karma"
|
||||
@users = User.order("karma DESC, id ASC").to_a
|
||||
@user_count = @users.length
|
||||
@title << " By Karma"
|
||||
render :action => "list"
|
||||
content = Rails.cache.fetch("users_by_karma_#{newest_user}",
|
||||
:expires_in => (60 * 60 * 24)) {
|
||||
@users = User.order("karma DESC, id ASC").to_a
|
||||
@user_count = @users.length
|
||||
@title << " By Karma"
|
||||
render_to_string :action => "list", :layout => nil
|
||||
}
|
||||
render :text => content, :layout => "application"
|
||||
elsif params[:moderators]
|
||||
@users = User.where("is_admin = ? OR is_moderator = ?", true, true).
|
||||
order("id ASC").to_a
|
||||
|
@ -28,10 +33,15 @@ class UsersController < ApplicationController
|
|||
@title = "Moderators and Administrators"
|
||||
render :action => "list"
|
||||
else
|
||||
users = User.order("id DESC").to_a
|
||||
@user_count = users.length
|
||||
@users_by_parent = users.group_by(&:invited_by_user_id)
|
||||
@newest = User.order("id DESC").limit(10)
|
||||
content = Rails.cache.fetch("users_tree_#{newest_user}",
|
||||
:expires_in => (60 * 60 * 24)) {
|
||||
users = User.order("id DESC").to_a
|
||||
@user_count = users.length
|
||||
@users_by_parent = users.group_by(&:invited_by_user_id)
|
||||
@newest = User.order("id DESC").limit(10)
|
||||
render_to_string :action => "tree", :layout => nil
|
||||
}
|
||||
render :text => content, :layout => "application"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
module ApplicationHelper
|
||||
MAX_PAGES = 15
|
||||
|
||||
def avatar_img(user, size)
|
||||
image_tag(user.avatar_url(size), {
|
||||
:srcset => "#{user.avatar_url(size)} 1x, " <<
|
||||
"#{user.avatar_url(size * 2)} 2x",
|
||||
:class => "avatar",
|
||||
:size => "#{size}x#{size}",
|
||||
:alt => "#{user.username} avatar" })
|
||||
end
|
||||
|
||||
def break_long_words(str, len = 30)
|
||||
safe_join(str.split(" ").map{|w|
|
||||
if w.length > len
|
||||
|
|
|
@ -10,7 +10,7 @@ class Comment < ActiveRecord::Base
|
|||
:class_name => "Moderation"
|
||||
belongs_to :hat
|
||||
|
||||
attr_accessor :current_vote, :previewing, :indent_level, :highlighted
|
||||
attr_accessor :current_vote, :previewing, :indent_level
|
||||
|
||||
before_validation :on => :create do
|
||||
self.assign_short_id_and_upvote
|
||||
|
@ -26,9 +26,14 @@ class Comment < ActiveRecord::Base
|
|||
|
||||
DOWNVOTABLE_DAYS = 7
|
||||
|
||||
# the lowest a score can go, which makes it collapsed by default
|
||||
DOWNVOTABLE_MIN_SCORE = -5
|
||||
|
||||
# after this many minutes old, a comment cannot be edited
|
||||
MAX_EDIT_MINS = (60 * 6)
|
||||
|
||||
SCORE_RANGE_TO_HIDE = (-2 .. 4)
|
||||
|
||||
validate do
|
||||
self.comment.to_s.strip == "" &&
|
||||
errors.add(" ", I18n.t( 'models.comment.commentcannotbeblank'))
|
||||
|
@ -44,10 +49,14 @@ class Comment < ActiveRecord::Base
|
|||
|
||||
self.comment.to_s.strip.match(/\Atl;?dr.?$\z/i) &&
|
||||
errors.add(:base, "Wow! A blue car!")
|
||||
|
||||
self.comment.to_s.strip.match(/\Ame too.?\z/i) &&
|
||||
errors.add(:base, I18n.t( 'models.comment.metooerror'))
|
||||
end
|
||||
|
||||
def self.arrange_for_user(user)
|
||||
parents = self.order("confidence DESC").group_by(&:parent_comment_id)
|
||||
parents = self.order("is_dragon ASC, (upvotes - downvotes) < 0 ASC, " <<
|
||||
"confidence DESC").group_by(&:parent_comment_id)
|
||||
|
||||
# top-down list of comments, regardless of indent level
|
||||
ordered = []
|
||||
|
@ -164,6 +173,36 @@ class Comment < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def become_dragon_for_user(user)
|
||||
Comment.record_timestamps = false
|
||||
|
||||
self.is_dragon = true
|
||||
|
||||
m = Moderation.new
|
||||
m.comment_id = self.id
|
||||
m.moderator_user_id = user.id
|
||||
m.action = I18n.t('models.comment.turnedintodragon')
|
||||
m.save
|
||||
|
||||
self.save(:validate => false)
|
||||
Comment.record_timestamps = true
|
||||
end
|
||||
|
||||
def remove_dragon_for_user(user)
|
||||
Comment.record_timestamps = false
|
||||
|
||||
self.is_dragon = false
|
||||
|
||||
m = Moderation.new
|
||||
m.comment_id = self.id
|
||||
m.moderator_user_id = user.id
|
||||
m.action = I18n.t('models.comment.slayeddragon')
|
||||
m.save
|
||||
|
||||
self.save(:validate => false)
|
||||
Comment.record_timestamps = true
|
||||
end
|
||||
|
||||
# http://evanmiller.org/how-not-to-sort-by-average-rating.html
|
||||
# https://github.com/reddit/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
|
||||
def calculated_confidence
|
||||
|
@ -223,7 +262,7 @@ class Comment < ActiveRecord::Base
|
|||
|
||||
if u.email_mentions?
|
||||
begin
|
||||
EmailReply.mention(self, u).deliver
|
||||
EmailReply.mention(self, u).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "error e-mailing #{u.email}: #{e}"
|
||||
end
|
||||
|
@ -247,7 +286,7 @@ class Comment < ActiveRecord::Base
|
|||
u.id != self.user.id
|
||||
if u.email_replies?
|
||||
begin
|
||||
EmailReply.reply(self, u).deliver
|
||||
EmailReply.reply(self, u).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "error e-mailing #{u.email}: #{e}"
|
||||
end
|
||||
|
@ -321,7 +360,7 @@ class Comment < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_downvotable?
|
||||
if self.created_at
|
||||
if self.created_at && self.score > DOWNVOTABLE_MIN_SCORE
|
||||
Time.now - self.created_at <= DOWNVOTABLE_DAYS.days
|
||||
else
|
||||
false
|
||||
|
@ -392,14 +431,24 @@ class Comment < ActiveRecord::Base
|
|||
self.upvotes - self.downvotes
|
||||
end
|
||||
|
||||
def short_id_path
|
||||
self.story.short_id_path + "/c/#{self.short_id}"
|
||||
def score_for_user(u)
|
||||
if self.showing_downvotes_for_user?(u)
|
||||
score
|
||||
else
|
||||
"-"
|
||||
end
|
||||
end
|
||||
|
||||
def short_id_url
|
||||
Rails.application.root_url + "c/#{self.short_id}"
|
||||
end
|
||||
|
||||
def showing_downvotes_for_user?(u)
|
||||
return (u && u.is_moderator?) ||
|
||||
(self.created_at && self.created_at < 36.hours.ago) ||
|
||||
!SCORE_RANGE_TO_HIDE.include?(self.score)
|
||||
end
|
||||
|
||||
def to_param
|
||||
self.short_id
|
||||
end
|
||||
|
@ -409,13 +458,14 @@ class Comment < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def url
|
||||
self.story.comments_url + "/comments/#{self.short_id}#c_#{self.short_id}"
|
||||
self.story.comments_url + "#c_#{self.short_id}"
|
||||
end
|
||||
|
||||
def vote_summary_for_user(u)
|
||||
r_counts = {}
|
||||
r_users = {}
|
||||
self.votes.includes(:user).each do |v|
|
||||
# don't includes(:user) here and assume the caller did this already
|
||||
self.votes.each do |v|
|
||||
r_counts[v.reason.to_s] ||= 0
|
||||
r_counts[v.reason.to_s] += v.vote
|
||||
|
||||
|
|
|
@ -24,6 +24,6 @@ class Invitation < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def send_email
|
||||
InvitationMailer.invitation(self).deliver
|
||||
InvitationMailer.invitation(self).deliver_now
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,8 +26,8 @@ class InvitationRequest < ActiveRecord::Base
|
|||
def markeddown_memo
|
||||
Markdowner.to_html(self.memo)
|
||||
end
|
||||
|
||||
|
||||
def send_email
|
||||
InvitationRequestMailer.invitation_request(self).deliver
|
||||
InvitationRequestMailer.invitation_request(self).deliver_now
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,7 +49,7 @@ class Message < ActiveRecord::Base
|
|||
|
||||
if self.recipient.email_messages?
|
||||
begin
|
||||
EmailMessage.notify(self, self.recipient).deliver
|
||||
EmailMessage.notify(self, self.recipient).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "error e-mailing #{self.recipient.email}: #{e}"
|
||||
end
|
||||
|
|
|
@ -68,8 +68,14 @@ class Search
|
|||
|
||||
if domain.present?
|
||||
self.what = "stories"
|
||||
story_ids = Story.select(:id).where("`url` REGEXP '//([^/]*\.)?" +
|
||||
ActiveRecord::Base.connection.quote_string(domain) + "/'").
|
||||
begin
|
||||
reg = Regexp.new("//([^/]*\.)?#{domain}/")
|
||||
rescue RegexpError
|
||||
return false
|
||||
end
|
||||
|
||||
story_ids = Story.select(:id).where("`url` REGEXP '" +
|
||||
ActiveRecord::Base.connection.quote_string(reg.source) + "'").
|
||||
collect(&:id)
|
||||
|
||||
if story_ids.any?
|
||||
|
@ -96,14 +102,9 @@ class Search
|
|||
query = Riddle.escape(words)
|
||||
|
||||
# go go gadget search
|
||||
self.results = []
|
||||
self.total_results = 0
|
||||
begin
|
||||
self.results = ThinkingSphinx.search query, opts
|
||||
self.total_results = self.results.total_entries
|
||||
rescue => e
|
||||
Rails.logger.info "Error from Sphinx: #{e.inspect}"
|
||||
end
|
||||
self.total_results = -1
|
||||
self.results = ThinkingSphinx.search query, opts
|
||||
self.total_results = self.results.total_entries
|
||||
|
||||
if self.page > self.page_count
|
||||
self.page = self.page_count
|
||||
|
@ -132,5 +133,10 @@ class Search
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
rescue ThinkingSphinx::ConnectionError => e
|
||||
self.results = []
|
||||
self.total_results = -1
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,8 @@ class StoriesPaginator
|
|||
def get
|
||||
with_pagination_info @scope.limit(per_page + 1)
|
||||
.offset((@page - 1) * per_page)
|
||||
.includes(:user, :taggings => :tag)
|
||||
.includes(:user, :suggested_titles, :suggested_taggings,
|
||||
:taggings => :tag)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -33,6 +33,9 @@ class Story < ActiveRecord::Base
|
|||
|
||||
DOWNVOTABLE_DAYS = 14
|
||||
|
||||
# the lowest a score can go
|
||||
DOWNVOTABLE_MIN_SCORE = -5
|
||||
|
||||
# after this many minutes old, a story cannot be edited
|
||||
MAX_EDIT_MINS = (60 * 6)
|
||||
|
||||
|
@ -123,7 +126,11 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.recalculate_all_hotnesses!
|
||||
Story.all.order("id DESC").each do |s|
|
||||
# do the front page first, since find_each can't take an order
|
||||
Story.order("id DESC").limit(100).each do |s|
|
||||
s.recalculate_hotness!
|
||||
end
|
||||
Story.find_each do |s|
|
||||
s.recalculate_hotness!
|
||||
end
|
||||
true
|
||||
|
@ -138,6 +145,10 @@ class Story < ActiveRecord::Base
|
|||
Story.connection.adapter_name.match(/mysql/i) ? "signed" : "integer"
|
||||
end
|
||||
|
||||
def archive_url
|
||||
"https://archive.is/#{CGI.escape(self.url)}"
|
||||
end
|
||||
|
||||
def as_json(options = {})
|
||||
h = [
|
||||
:short_id,
|
||||
|
@ -185,21 +196,35 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def calculated_hotness
|
||||
base = self.tags.map{|t| t.hotness_mod }.sum
|
||||
# take each tag's hotness modifier into effect, and give a slight bump to
|
||||
# stories submitted by the author
|
||||
base = self.tags.map{|t| t.hotness_mod }.sum +
|
||||
(self.user_is_author ? 0.25 : 0.0)
|
||||
|
||||
# give a story's comment votes some weight (unless the hotness mod is
|
||||
# negative), but ignore the story submitter's own comments
|
||||
if base < 0
|
||||
cpoints = 0
|
||||
else
|
||||
cpoints = self.comments.where("user_id <> ?", self.user_id).
|
||||
select(:upvotes, :downvotes).map{|c| c.upvotes + 1 - c.downvotes }.
|
||||
inject(&:+).to_f * 0.5
|
||||
end
|
||||
# give a story's comment votes some weight, ignoring submitter's comments
|
||||
cpoints = self.comments.
|
||||
where("user_id <> ?", self.user_id).
|
||||
select(:upvotes, :downvotes).
|
||||
map{|c|
|
||||
if base < 0
|
||||
# in stories already starting out with a bad hotness mod, only look
|
||||
# at the downvotes to find out if this tire fire needs to be put out
|
||||
c.downvotes * -0.5
|
||||
else
|
||||
c.upvotes + 1 - c.downvotes
|
||||
end
|
||||
}.
|
||||
inject(&:+).to_f * 0.5
|
||||
|
||||
# mix in any stories this one cannibalized
|
||||
cpoints += self.merged_stories.map{|s| s.score }.inject(&:+).to_f
|
||||
|
||||
# if a story has many comments but few votes, it's probably a bad story, so
|
||||
# cap the comment points at the number of upvotes
|
||||
if cpoints > self.upvotes
|
||||
cpoints = self.upvotes
|
||||
end
|
||||
|
||||
# don't immediately kill stories at 0 by bumping up score by one
|
||||
order = Math.log([ (score + 1).abs + cpoints, 1 ].max, 10)
|
||||
if score > 0
|
||||
|
@ -227,7 +252,7 @@ class Story < ActiveRecord::Base
|
|||
return false
|
||||
end
|
||||
|
||||
if self.tags.select{|t| t.privileged? }.any?
|
||||
if self.taggings.select{|t| t.tag && t.tag.privileged? }.any?
|
||||
return false
|
||||
end
|
||||
|
||||
|
@ -305,10 +330,24 @@ class Story < ActiveRecord::Base
|
|||
|
||||
def fetch_story_cache!
|
||||
if self.url.present?
|
||||
self.story_cache = StoryCacher.get_story_text(self.url)
|
||||
self.story_cache = StoryCacher.get_story_text(self)
|
||||
end
|
||||
end
|
||||
|
||||
def fix_bogus_chars
|
||||
# this is needlessly complicated to work around character encoding issues
|
||||
# that arise when doing just self.title.to_s.gsub(160.chr, "")
|
||||
self.title = self.title.to_s.split("").map{|chr|
|
||||
if chr.ord == 160
|
||||
" "
|
||||
else
|
||||
chr
|
||||
end
|
||||
}.join("")
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def generated_markeddown_description
|
||||
Markdowner.to_html(self.description, { :allow_images => true })
|
||||
end
|
||||
|
@ -323,6 +362,10 @@ class Story < ActiveRecord::Base
|
|||
"hotness = '#{self.calculated_hotness}' WHERE id = #{self.id.to_i}")
|
||||
end
|
||||
|
||||
def has_suggestions?
|
||||
self.suggested_taggings.any? || self.suggested_titles.any?
|
||||
end
|
||||
|
||||
def hider_count
|
||||
@hider_count ||= HiddenStory.where(:story_id => self.id).count
|
||||
end
|
||||
|
@ -341,8 +384,7 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_downvotable?
|
||||
return true
|
||||
if self.created_at
|
||||
if self.created_at && self.score >= DOWNVOTABLE_MIN_SCORE
|
||||
Time.now - self.created_at <= DOWNVOTABLE_DAYS.days
|
||||
else
|
||||
false
|
||||
|
@ -467,20 +509,6 @@ class Story < ActiveRecord::Base
|
|||
self.user_id, nil, false)
|
||||
end
|
||||
|
||||
def fix_bogus_chars
|
||||
# this is needlessly complicated to work around character encoding issues
|
||||
# that arise when doing just self.title.to_s.gsub(160.chr, "")
|
||||
self.title = self.title.to_s.split("").map{|chr|
|
||||
if chr.ord == 160
|
||||
" "
|
||||
else
|
||||
chr
|
||||
end
|
||||
}.join("")
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def score
|
||||
upvotes - downvotes
|
||||
end
|
||||
|
@ -788,6 +816,24 @@ class Story < ActiveRecord::Base
|
|||
title = parsed.at_css("title").try(:text).to_s
|
||||
end
|
||||
|
||||
# see if the site name is available, so we can strip it out in case it was
|
||||
# present in the fetched title
|
||||
begin
|
||||
site_name = parsed.at_css("meta[property='og:site_name']").
|
||||
attributes["content"].text
|
||||
|
||||
if site_name.present? && site_name.length < title.length &&
|
||||
title[-(site_name.length), site_name.length] == site_name
|
||||
title = title[0, title.length - site_name.length]
|
||||
|
||||
# remove title/site name separator
|
||||
if title.match(/ [ \-\|\u2013] $/)
|
||||
title = title[0, title.length - 3]
|
||||
end
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@fetched_attributes[:title] = title
|
||||
|
||||
# now get canonical version of url (though some cms software puts incorrect
|
||||
|
|
|
@ -33,13 +33,34 @@ class User < ActiveRecord::Base
|
|||
|
||||
has_secure_password
|
||||
|
||||
typed_store :settings do |s|
|
||||
s.boolean :email_notifications, :default => false
|
||||
s.boolean :email_replies, :default => false
|
||||
s.boolean :pushover_replies, :default => false
|
||||
s.string :pushover_user_key
|
||||
s.boolean :email_messages, :default => false
|
||||
s.boolean :pushover_messages, :default => false
|
||||
s.boolean :email_mentions, :default => false
|
||||
s.boolean :show_avatars, :default => true
|
||||
s.boolean :show_story_previews, :default => false
|
||||
s.boolean :show_submitted_story_threads, :default => false
|
||||
s.boolean :hide_dragons, :default => false
|
||||
s.string :totp_secret
|
||||
s.string :github_oauth_token
|
||||
s.string :github_username
|
||||
s.string :twitter_oauth_token
|
||||
s.string :twitter_oauth_token_secret
|
||||
s.string :twitter_username
|
||||
end
|
||||
|
||||
validates :email, :format => { :with => /\A[^@ ]+@[^@ ]+\.[^@ ]+\Z/ },
|
||||
:uniqueness => { :case_sensitive => false }
|
||||
|
||||
validates :password, :presence => true, :on => :create
|
||||
|
||||
VALID_USERNAME = /[A-Za-z0-9][A-Za-z0-9_-]{0,24}/
|
||||
validates :username,
|
||||
:format => { :with => /\A[A-Za-z0-9][A-Za-z0-9_-]{0,24}\Z/ },
|
||||
:format => { :with => /\A#{VALID_USERNAME}\z/ },
|
||||
:uniqueness => { :case_sensitive => false }
|
||||
|
||||
validates_each :username do |record,attr,value|
|
||||
|
@ -48,15 +69,17 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
scope :active, -> { where(:banned_at => nil, :deleted_at => nil) }
|
||||
|
||||
before_save :check_session_token
|
||||
before_validation :on => :create do
|
||||
self.create_rss_token
|
||||
self.create_mailing_list_token
|
||||
end
|
||||
|
||||
BANNED_USERNAMES = [ "admin", "administrator", "hostmaster", "mailer-daemon",
|
||||
"postmaster", "root", "security", "support", "webmaster", "moderator",
|
||||
"moderators", "help", "contact", "fraud", "guest", "nobody", ]
|
||||
BANNED_USERNAMES = [ "admin", "administrator", "contact", "fraud", "guest",
|
||||
"help", "hostmaster", "mailer-daemon", "moderator", "moderators", "nobody",
|
||||
"postmaster", "root", "security", "support", "sysop", "webmaster" ]
|
||||
|
||||
# days old accounts are considered new for
|
||||
NEW_USER_DAYS = 7
|
||||
|
@ -65,7 +88,7 @@ class User < ActiveRecord::Base
|
|||
MIN_KARMA_TO_SUGGEST = 10
|
||||
|
||||
# minimum karma required to be able to downvote comments
|
||||
MIN_KARMA_TO_DOWNVOTE = 100
|
||||
MIN_KARMA_TO_DOWNVOTE = 50
|
||||
|
||||
# minimum karma required to be able to submit new stories
|
||||
MIN_KARMA_TO_SUBMIT_STORIES = -4
|
||||
|
@ -77,10 +100,8 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.username_regex
|
||||
User.validators_on(:username).select{|v|
|
||||
v.class == ActiveModel::Validations::FormatValidator }.first.
|
||||
options[:with].inspect
|
||||
def self.username_regex_s
|
||||
"/^" + VALID_USERNAME.to_s.gsub(/(\?-mix:|\(|\))/, "") + "$/"
|
||||
end
|
||||
|
||||
def as_json(options = {})
|
||||
|
@ -98,10 +119,25 @@ class User < ActiveRecord::Base
|
|||
attrs.push :about
|
||||
|
||||
h = super(:only => attrs)
|
||||
h[:avatar_url] = avatar_url
|
||||
|
||||
h[:avatar_url] = self.avatar_url
|
||||
|
||||
if self.github_username.present?
|
||||
h[:github_username] = self.github_username
|
||||
end
|
||||
|
||||
if self.twitter_username.present?
|
||||
h[:twitter_username] = self.twitter_username
|
||||
end
|
||||
|
||||
h
|
||||
end
|
||||
|
||||
def authenticate_totp(code)
|
||||
totp = ROTP::TOTP.new(self.totp_secret)
|
||||
totp.verify(code)
|
||||
end
|
||||
|
||||
def avatar_url(size = 100)
|
||||
"https://secure.gravatar.com/avatar/" +
|
||||
Digest::MD5.hexdigest(self.email.strip.downcase) +
|
||||
|
@ -176,7 +212,7 @@ class User < ActiveRecord::Base
|
|||
# user can unvote
|
||||
return true
|
||||
end
|
||||
elsif obj.is_a?(Comment)
|
||||
elsif obj.is_a?(Comment) && obj.is_downvotable?
|
||||
return !self.is_new? && (self.karma >= MIN_KARMA_TO_DOWNVOTE)
|
||||
end
|
||||
|
||||
|
@ -262,6 +298,11 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def disable_2fa!
|
||||
self.totp_secret = nil
|
||||
self.save!
|
||||
end
|
||||
|
||||
def grant_moderatorship_by_user!(user)
|
||||
User.transaction do
|
||||
self.is_moderator = true
|
||||
|
@ -287,7 +328,11 @@ class User < ActiveRecord::Base
|
|||
self.password_reset_token = "#{Time.now.to_i}-#{Utils.random_str(30)}"
|
||||
self.save!
|
||||
|
||||
PasswordReset.password_reset_link(self, ip).deliver
|
||||
PasswordReset.password_reset_link(self, ip).deliver_now
|
||||
end
|
||||
|
||||
def has_2fa?
|
||||
self.totp_secret.present?
|
||||
end
|
||||
|
||||
def is_active?
|
||||
|
@ -303,9 +348,7 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def linkified_about
|
||||
# most users are probably mentioning "@username" to mean a twitter url, not
|
||||
# a link to a profile on this site
|
||||
Markdowner.to_html(self.about, { :disable_profile_links => true })
|
||||
Markdowner.to_html(self.about)
|
||||
end
|
||||
|
||||
def most_common_story_tag
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<input id="comment_folder_<%= comment.short_id %>"
|
||||
class="comment_folder_button" type="checkbox"
|
||||
<%= comment.score <= -1 ? "checked" : "" %>>
|
||||
<div id="comment_<%= comment.short_id %>"
|
||||
<%= comment.score <= Comment::DOWNVOTABLE_MIN_SCORE ? "checked" : "" %>>
|
||||
<div id="c_<%= comment.short_id %>"
|
||||
data-shortid="<%= comment.short_id if comment.persisted? %>"
|
||||
class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
||||
"upvoted" : "downvoted") : "" %>
|
||||
<%= comment.highlighted ? "highlighted" : "" %>
|
||||
<%= comment.score <= 0 ? "negative" : "" %>
|
||||
<%= comment.score <= -1 ? "negative_1" : "" %>">
|
||||
<%= comment.score < Comment::SCORE_RANGE_TO_HIDE.first ? "bad" : "" %>">
|
||||
<% if !comment.is_gone? %>
|
||||
<div class="voters">
|
||||
<% if @user %>
|
||||
|
@ -15,7 +13,7 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
<% else %>
|
||||
<%= link_to "", login_path, :class => "upvoter" %>
|
||||
<% end %>
|
||||
<div class="score"><%= comment.score %></div>
|
||||
<div class="score"><%= comment.score_for_user(@user) %></div>
|
||||
<% if @user && @user.can_downvote?(comment) %>
|
||||
<a class="downvoter"></a>
|
||||
<% else %>
|
||||
|
@ -35,8 +33,8 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
<% end %>
|
||||
|
||||
<% if (@user && @user.show_avatars?) || !@user %>
|
||||
<a href="/u/<%= comment.user.username %>"><img
|
||||
src="<%= comment.user.avatar_url(16) %>" class="avatar"></a>
|
||||
<a href="/u/<%= comment.user.username %>"><%=
|
||||
avatar_img(comment.user, 16) %></a>
|
||||
<% end %>
|
||||
|
||||
<a href="/u/<%= comment.user.username %>"
|
||||
|
@ -61,7 +59,7 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
|
||||
<% if !comment.previewing %>
|
||||
|
|
||||
<a href="<%= comment.short_id_url %>"><%= t('.link') %></a>
|
||||
<a href="<%= comment.url %>"><%= t('.link') %></a>
|
||||
|
||||
<% if comment.is_editable_by_user?(@user) %>
|
||||
|
|
||||
|
@ -80,6 +78,15 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if !comment.parent_comment_id && @user && @user.is_moderator? %>
|
||||
|
|
||||
<% if comment.is_dragon? %>
|
||||
<a class="comment_undragon"><%= t('.undragon') %></a>
|
||||
<% else %>
|
||||
<a class="comment_dragon"><%= t('.dragon') %></a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if @user && !comment.story.is_gone? && !comment.is_gone? %>
|
||||
|
|
||||
<a class="comment_replier" unselectable="on"><%= t('.reply') %></a>
|
||||
|
@ -93,8 +100,8 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
|
||||
<span class="reason">
|
||||
<% if comment.downvotes > 0 &&
|
||||
((comment.score <= 0 && comment.user_id == @user.try(:id)) ||
|
||||
@user.try("is_moderator?")) %>
|
||||
comment.showing_downvotes_for_user?(@user) &&
|
||||
(comment.user_id == @user.try(:id) || @user.try("is_moderator?")) %>
|
||||
| <%= comment.vote_summary_for_user(@user).downcase %>
|
||||
<% elsif comment.current_vote && comment.current_vote[:vote] == -1 %>
|
||||
| -1
|
||||
|
|
|
@ -15,7 +15,7 @@ data-shortid="<%= comment.short_id if comment.persisted? %>">
|
|||
|
||||
<div style="width: 100%;">
|
||||
<%= text_area_tag "comment", comment.comment, :rows => 5,
|
||||
:style => "width: 100%;", :autocomplete => "off", :disabled => !@user,
|
||||
:disabled => !@user,
|
||||
:placeholder => (@user ? "" : t('.mustbelogged'))
|
||||
%>
|
||||
|
||||
|
|
|
@ -8,12 +8,10 @@
|
|||
<td><strong><%= t('.strongtext') %></strong></td>
|
||||
<td><%= raw t('.strongtextdesc') %></td>
|
||||
</tr>
|
||||
<!--
|
||||
<tr>
|
||||
<td><strike>struck-through</strike></td>
|
||||
<td>surround text with <tt>~~two tilde characters~~</tt></td>
|
||||
</tr>
|
||||
-->
|
||||
<tr>
|
||||
<td><tt><%= t('.fixedwidth') %></tt></td>
|
||||
<td><%= raw t('.fixedwidthdesc') %></td>
|
||||
|
|
|
@ -1,60 +1,29 @@
|
|||
<div class="box wide">
|
||||
<div class="legend">
|
||||
Request a Hat
|
||||
<%= t('.title') %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
A hat is a formal, verified, way of posting a comment while speaking for a
|
||||
project, organization, or company. Each user may have multiple hats, one of
|
||||
which may be worn at any time when posting a comment or sending a private
|
||||
message.
|
||||
</p>
|
||||
<p>
|
||||
Hats are intended for core project developers and company employees that are
|
||||
authorized to make comments on behalf of those organizations, and are usually
|
||||
reserved for notable projects and organizations. Hats are not intended to
|
||||
list every contribution a person has made to any software project.
|
||||
</p>
|
||||
<p>
|
||||
Hats will not be authorized for occasional contributors to projects,
|
||||
developers of projects which are not widely known, past employees of
|
||||
companies, or other situations where it cannot be verified that one is
|
||||
authorized to represent that organization. In general, unless one has an
|
||||
e-mail address at a compay domain or have commit access to a project, a hat
|
||||
will not be authorized.
|
||||
</p>
|
||||
<p>
|
||||
To request a hat for your account, provide a short description of the hat
|
||||
(e.g., "OpenBSD Developer"), a public link that will be shown when hovering
|
||||
over the hat that users can see, such as your e-mail address at that project
|
||||
or company, or a link to a company website showing your employment, and
|
||||
private comments that will be seen only by moderators during approval.
|
||||
</p>
|
||||
<p>
|
||||
Once your hat is requested, a moderator will verify your request by e-mailing
|
||||
the address you submitted as the link, or doing some other manual
|
||||
verification of project association.
|
||||
</p>
|
||||
<%= raw(t('.description')) %>
|
||||
|
||||
<%= form_for @hat_request, :url => create_hat_request_path do |f| %>
|
||||
<p>
|
||||
<%= f.label :hat, "Hat:" %>
|
||||
<%= f.label :hat, t('.hat') %>
|
||||
<%= f.text_field :hat, :size => 20,
|
||||
:placeholder => "XYZ Project Member" %>
|
||||
:placeholder => t('.hatplaceholder') %>
|
||||
<br />
|
||||
|
||||
<%= f.label :link, "Link:" %>
|
||||
<%= f.label :link, t('.link') %>
|
||||
<%= f.text_field :link, :size => 50,
|
||||
:placeholder => "user@project.org, or a URL to an employment page" %>
|
||||
:placeholder => t('.linkplaceholder') %>
|
||||
<br />
|
||||
|
||||
<%= f.label :comment, "Comment:" %>
|
||||
<%= f.label :comment, t('.comment') %>
|
||||
<%= f.text_area :comment, :rows => 4,
|
||||
:placeholder => "Will only be shown to moderators during approval" %>
|
||||
:placeholder => t('.commentplaceholder') %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= submit_tag "Request Hat" %>
|
||||
<%= submit_tag t('.requesthatbutton') %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
<div class="box wide">
|
||||
<% if @user %>
|
||||
<div class="legend right">
|
||||
<a href="<%= request_hat_url %>">Request Hat</a>
|
||||
<a href="<%= request_hat_url %>"><%= t('.request') %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="legend">
|
||||
Hats
|
||||
<%= t('.title') %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
A hat is a formal, verified, way of posting a comment while speaking for a
|
||||
project, organization, or company. Each user may have multiple hats, one of
|
||||
which may be selected to be worn when posting a comment or sending a private
|
||||
message.
|
||||
<%= t('.description') %>
|
||||
</p>
|
||||
|
||||
<table class="data" width="100%" cellspacing=0>
|
||||
<tr>
|
||||
<th style="width: 130px;">User</th>
|
||||
<th>Hat</th>
|
||||
<th>Link</th>
|
||||
<th style="width: 130px;"><%= t('.user') %></th>
|
||||
<th><%= t('.hat') %></th>
|
||||
<th><%= t('.link') %></th>
|
||||
</tr>
|
||||
<% bit = 0 %>
|
||||
<% @hat_groups.keys.sort_by{|a| a.downcase }.each do |hg| %>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<div class="box wide">
|
||||
<div class="legend">
|
||||
Requested Hats
|
||||
<%= t('.title') %>
|
||||
</div>
|
||||
|
||||
<% if @hat_requests.count == 0 %>
|
||||
No hat requests.
|
||||
<%= t('.nohatrequests') %>
|
||||
<% else %>
|
||||
<% @hat_requests.each_with_index do |hr,x| %>
|
||||
<% if x > 0 %>
|
||||
|
@ -15,42 +15,42 @@
|
|||
:method => :post do |f| %>
|
||||
<p>
|
||||
<div class="boxline">
|
||||
<%= f.label :user_id, "User:", :class => "required" %>
|
||||
<%= f.label :user_id, t('.user'), :class => "required" %>
|
||||
<a href="/u/<%= hr.user.username %>"><%= hr.user.username %></a>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :hat, "Hat:", :class => "required" %>
|
||||
<%= f.text_field "hat", :size => 25 %>
|
||||
<%= f.label :hat, t('.hat'), :class => "required" %>
|
||||
<%= f.text_field "hat", :size => 75 %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :link, "Link:", :class => "required" %>
|
||||
<%= f.text_field "link", :size => 25 %>
|
||||
<%= f.label :link, t('.link'), :class => "required" %>
|
||||
<%= f.text_field "link", :size => 75 %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :link, "Comment:", :class => "required" %>
|
||||
<%= f.label :link, t('.comment'), :class => "required" %>
|
||||
<div class="d">
|
||||
<%= raw(h(hr.comment.to_s).gsub(/\n/, "<br>")) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="clear: both;">
|
||||
<%= submit_tag "Approve Hat Request" %>
|
||||
<%= submit_tag t('.approve') %>
|
||||
</p>
|
||||
<% end %>
|
||||
<p>
|
||||
or
|
||||
<%= t('.hator') %>
|
||||
</p>
|
||||
<%= form_for hr, :url => reject_hat_request_url(:id => hr),
|
||||
:method => :post do |f| %>
|
||||
<div class="boxline">
|
||||
<%= f.label :link, "Reason:", :class => "required" %>
|
||||
<%= f.label :link, t('.reason'), :class => "required" %>
|
||||
<%= f.text_area :rejection_comment, :rows => 4 %>
|
||||
</div>
|
||||
<p>
|
||||
<%= submit_tag "Reject Hat Request" %>
|
||||
<%= submit_tag t('.reject') %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<br />
|
||||
|
||||
<%= f.label :email, t('.buildinvemail') %>
|
||||
<%= f.text_field :email, :size => 30 %>
|
||||
<%= f.email_field :email, :size => 30 %>
|
||||
<br />
|
||||
|
||||
<%= f.label :memo, t('.buildinvurl') %>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-144.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noarchive,noodp,noydir" />
|
||||
<meta name="referrer" content="always" />
|
||||
<meta name="theme-color" content="#AC130D" />
|
||||
<% if @meta_tags %>
|
||||
|
@ -97,9 +96,6 @@
|
|||
raw("class=\"cur_url\"") : "" %>><%= @user.username %>
|
||||
(<%= @user.karma %>)</a>
|
||||
|
||||
<%= link_to t('.logoutlink'), { :controller => "login", :action => "logout" },
|
||||
:data => { :confirm => t('.confirmlogoutlink') },
|
||||
:method => "post" %>
|
||||
<% else %>
|
||||
<a href="/login"><%= t('.loginlink') %></a>
|
||||
<% end %>
|
||||
|
@ -119,21 +115,27 @@
|
|||
<%= yield %>
|
||||
|
||||
<div id="footer">
|
||||
<a href="/moderations"><%= t('.moderationloglink') %></a>
|
||||
<% if @user && !@user.is_new? &&
|
||||
(iqc = InvitationRequest.verified_count) > 0 %>
|
||||
<a href="/invitations"><%= t('.invitationqueuelink') %>(<%= iqc %>)</a>
|
||||
<% if lookup_context.template_exists?("footer", "layouts", true) %>
|
||||
<%= render :partial => "layouts/footer" %>
|
||||
<% else %>
|
||||
<a href="/moderations">Moderation Log</a>
|
||||
<% if @user && !@user.is_new? &&
|
||||
(iqc = InvitationRequest.verified_count) > 0 %>
|
||||
<a href="/invitations">Invitation Queue(<%= iqc %>)</a>
|
||||
<% end %>
|
||||
<% if @user && @user.is_moderator? &&
|
||||
(hrc = HatRequest.count) > 0 %>
|
||||
<a href="/hats/requests">Hat Requests (<%= hrc %>)</a>
|
||||
<% else %>
|
||||
<a href="/hats"><%= t('.hatslink') %></a>
|
||||
<% end %>
|
||||
<a href="/privacy"><%= t('.privacylink') %></a>
|
||||
<a href="/about"><%= t('.aboutlink') %></a>
|
||||
<a href="https://blog.journalduhacker.net"><%= t('.blog') %></a>
|
||||
<a href="https://twitter.com/journalduhacker"><%= t('.twitter') %></a>
|
||||
<a href="https://framasphere.org/people/2aaaaba0110c0133c7ea2a0000053625"><%= t('.diaspora') %></a>
|
||||
<a href="https://framapiaf.org/@journalduhacker"><%= t('.mastodon') %></a>
|
||||
<% end %>
|
||||
<% if @user && @user.is_moderator? &&
|
||||
(hrc = HatRequest.count) > 0 %>
|
||||
<a href="/hats/requests"><%= t('.hatrequestlink') %> (<%= hrc %>)</a>
|
||||
<% end %>
|
||||
<a href="/privacy"><%= t('.privacylink') %></a>
|
||||
<a href="/about"><%= t('.aboutlink') %></a>
|
||||
<a href="https://blog.journalduhacker.net"><%= t('.blog') %></a>
|
||||
<a href="https://twitter.com/journalduhacker"><%= t('.twitter') %></a>
|
||||
<a href="https://framasphere.org/people/2aaaaba0110c0133c7ea2a0000053625"><%= t('.diaspora') %></a>
|
||||
<a href="https://framapiaf.org/@journalduhacker"><%= t('.mastodon') %></a>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<div class="box wide">
|
||||
<div class="legend">
|
||||
<%= t('.login2fa') %>
|
||||
</div>
|
||||
|
||||
<%= form_tag twofa_login_url do %>
|
||||
<p>
|
||||
<%= t('.logintotpcode') %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= label_tag :totp_code, t('.totpcode') %>
|
||||
<%= number_field_tag :totp_code, "", :size => 10, :autocomplete => "off",
|
||||
:autofocus => true, :class => "totp_code" %>
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= submit_tag t('.loginbutton') %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -73,20 +73,17 @@
|
|||
|
||||
<div class="boxline">
|
||||
<%= f.label :recipient_username, t('.tomsglabel'), :class => "required" %>
|
||||
<%= f.text_field :recipient_username, :size => 20,
|
||||
:autocomplete => "off" %>
|
||||
<%= f.text_field :recipient_username, :size => 20 %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :subject, t('.subject'), :class => "required" %>
|
||||
<%= f.text_field :subject, :style => "width: 500px;",
|
||||
:autocomplete => "off" %>
|
||||
<%= f.text_field :subject, :style => "width: 500px;" %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :body, t('.message'), :class => "required" %>
|
||||
<%= f.text_area :body, :style => "width: 500px;", :rows => 5,
|
||||
:autocomplete => "off" %>
|
||||
<%= f.text_area :body, :style => "width: 500px;", :rows => 5 %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
|
|
|
@ -69,13 +69,11 @@
|
|||
<%= error_messages_for @new_message %>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.text_field :subject, :style => "width: 500px;",
|
||||
:autocomplete => "off" %>
|
||||
<%= f.text_field :subject, :style => "width: 500px;" %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.text_area :body, :style => "width: 500px;", :rows => 5,
|
||||
:autocomplete => "off" %>
|
||||
<%= f.text_area :body, :style => "width: 500px;", :rows => 5 %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
|
||||
<table class="data" width="100%" cellspacing=0>
|
||||
<tr>
|
||||
<th style="min-width: 130px;"><%= t('.datecolumn') %></th>
|
||||
<th style="min-width: 150px;"><%= t('.datecolumn') %></th>
|
||||
<th><%= t('.moderatorcolumn') %></th>
|
||||
<th><%= t('.reasoncolumn') %></th>
|
||||
</tr>
|
||||
<% bit = 0 %>
|
||||
<% @moderations.each do |mod| %>
|
||||
<tr class="row<%= bit %> nobottom">
|
||||
<td><%= l mod.created_at %></td>
|
||||
<td><%= mod.created_at.strftime("%Y-%m-%d %H:%M %z") %></td>
|
||||
<td><% if mod.moderator %>
|
||||
<a href="/messages?to=<%= mod.moderator.try(:username) %>"><%=
|
||||
mod.moderator.try(:username) %></a>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/u/<%= @user.username %>"><%= t('.viewprofile') %></a>
|
||||
|
|
||||
<%= link_to t('.logoutlink'), { :controller => "login", :action => "logout" },
|
||||
:data => { :confirm => t('.confirmlogoutlink') },
|
||||
:method => "post" %>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= t('.accountsettings') %>
|
||||
|
@ -14,10 +18,16 @@
|
|||
<%= f.label :username, t('.username'), :class => "required" %>
|
||||
<%= f.text_field :username, :size => 15 %>
|
||||
<span class="hint">
|
||||
<tt><%= User.username_regex %></tt>
|
||||
<tt><%= User.username_regex_s %></tt>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= label_tag :current_password, t('.currentpassword'),
|
||||
:class => "required" %>
|
||||
<%= password_field_tag :current_password, nil, :size => 40 %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :password, t('.password'), :class => "required" %>
|
||||
<%= f.password_field :password, :size => 40, :autocomplete => "off" %>
|
||||
|
@ -32,7 +42,7 @@
|
|||
|
||||
<div class="boxline">
|
||||
<%= f.label :email, t('.emailaddress'), :class => "required" %>
|
||||
<%= f.text_field :email, :size => 40 %>
|
||||
<%= f.email_field :email, :size => 40 %>
|
||||
<span class="hint">
|
||||
<%= raw(t('.gravatarized')) %>
|
||||
</span>
|
||||
|
@ -63,18 +73,19 @@
|
|||
<br>
|
||||
|
||||
<div class="legend">
|
||||
<%= t('.notificationsettings') %>
|
||||
<%= t('.securitysettings') %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :pushover_user_key,
|
||||
raw(t(".pushover")),
|
||||
:class => "required" %>
|
||||
<%= link_to((f.object.pushover_user_key.present??
|
||||
t('.managepushoversubscription') : t('.subscribewithpushover')),
|
||||
"/settings/pushover", :class => "pushover_button", :method => :post) %>
|
||||
<span class="hint">
|
||||
<%= t('.foroptionalcomment') %>
|
||||
<%= f.label :twofa, t('.twofactorauth'), :class => "required" %>
|
||||
<span>
|
||||
<% if @edit_user.totp_secret.present? %>
|
||||
<span style="color: green; font-weight: bold;">
|
||||
<%= t('.enabled2fa') %>
|
||||
</span> (<a href="/settings/2fa"><%= t('.disable2fa') %></a>)
|
||||
<% else %>
|
||||
<%= t('.disabled2fa') %> (<a href="/settings/2fa"><%= t('.enroll2fa') %></a>)
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -193,6 +204,10 @@
|
|||
<%= f.check_box :show_avatars %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :hide_dragons, t('.hidedragons'), :class => "required" %>
|
||||
<%= f.check_box :hide_dragons %>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<%= f.submit t('.saveallsettings') %>
|
||||
|
@ -201,6 +216,58 @@
|
|||
<br>
|
||||
<br>
|
||||
|
||||
<div class="legend">
|
||||
External Accounts
|
||||
</div>
|
||||
|
||||
<% if Pushover.enabled? %>
|
||||
<div class="boxline">
|
||||
<%= label_tag :pushover_user_key,
|
||||
raw("<a href=\"https://pushover.net/\">Pushover</a>:"),
|
||||
:class => "required" %>
|
||||
<%= link_to((@edit_user.pushover_user_key.present??
|
||||
"Manage Pushover Subscription" : "Subscribe With Pushover"),
|
||||
"/settings/pushover_auth", :class => "pushover_button",
|
||||
:method => :post) %>
|
||||
<span class="hint">
|
||||
For optional comment and message notifications above
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if Github.enabled? %>
|
||||
<div class="boxline">
|
||||
<%= label_tag :github_username, "GitHub:", :class => "required" %>
|
||||
<% if @edit_user.github_username.present? %>
|
||||
Linked to
|
||||
<strong><a href="https://github.com/<%= h(@edit_user.github_username)
|
||||
%>"><%= h(@edit_user.github_username) %></a></strong>
|
||||
(<%= link_to "Disconnect", "/settings/github_disconnect",
|
||||
:method => :post %>)
|
||||
<% else %>
|
||||
<a href="/settings/github_auth">Connect</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if Twitter.enabled? %>
|
||||
<div class="boxline">
|
||||
<%= label_tag :twitter_username, "Twitter:", :class => "required" %>
|
||||
<% if @edit_user.twitter_username.present? %>
|
||||
Linked to
|
||||
<strong><a href="https://twitter.com/<%= h(@edit_user.twitter_username)
|
||||
%>">@<%= h(@edit_user.twitter_username) %></a></strong>
|
||||
(<%= link_to "Disconnect", "/settings/twitter_disconnect",
|
||||
:method => :post %>)
|
||||
<% else %>
|
||||
<a href="/settings/twitter_auth">Connect</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<a name="invite"></a>
|
||||
<div class="legend">
|
||||
<%= t('.inviteuser') %>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/settings"><%= t('.backtosettings') %></a>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= @title %>
|
||||
</div>
|
||||
|
||||
<%= form_for @user, :url => twofa_auth_url, :method => :post do |f| %>
|
||||
<p>
|
||||
<% if @user.has_2fa? %>
|
||||
<%= t('.turnoff') %>
|
||||
<% else %>
|
||||
<%= t('.turnon') %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<div class="boxline">
|
||||
<%= f.label :password, t('.currentpassword'), :class => "required" %>
|
||||
<%= f.password_field :password, :size => 40, :autocomplete => "off" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<% if @user.has_2fa? %>
|
||||
<%= submit_tag t('.disable2fa') %>
|
||||
<% else %>
|
||||
<%= submit_tag t('.continue') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/settings"><%= t('.backtosettings') %></a>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= @title %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= raw(t('.scanqrcode')) %>
|
||||
</p>
|
||||
|
||||
<%= raw @qr_svg %>
|
||||
|
||||
<p>
|
||||
<%= t('.registring') %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= button_to t('.verifyenable'), twofa_verify_url, :method => :get %>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
<div class="box wide">
|
||||
<div class="legend right">
|
||||
<a href="/settings">Back to Settings</a>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<%= @title %>
|
||||
</div>
|
||||
|
||||
<%= form_tag twofa_update_url do %>
|
||||
<p>
|
||||
<%= t('.enablecode') %>
|
||||
</p>
|
||||
|
||||
<div class="boxline">
|
||||
<%= label_tag :totp_code, "TOTP Code:", :class => "required" %>
|
||||
<%= number_field_tag :totp_code, "", :size => 10, :autocomplete => "off",
|
||||
:autofocus => true, :class => "totp_code" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= submit_tag t('.verifyenable') %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -3,8 +3,7 @@
|
|||
Create an Account
|
||||
</div>
|
||||
|
||||
<%= form_for @new_user, { :url => signup_path,
|
||||
:autocomplete => "off" } do |f| %>
|
||||
<%= form_for @new_user, { :url => signup_path } do |f| %>
|
||||
<%= hidden_field_tag "invitation_code", @invitation.code %>
|
||||
|
||||
<p>
|
||||
|
@ -28,7 +27,7 @@
|
|||
<%= f.label :username, "Username:", :class => "required" %>
|
||||
<%= f.text_field :username, :size => 30 %>
|
||||
<span class="hint">
|
||||
<tt><%= User.username_regex %></tt>
|
||||
<tt><%= User.username_regex_s %></tt>
|
||||
</span>
|
||||
<br />
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<div class="boxline">
|
||||
<%= f.label :title, t('.title'), :class => "required" %>
|
||||
<%= f.text_field :title, :maxlength => 100, :autocomplete => "off" %>
|
||||
<%= f.text_field :title, :maxlength => 100 %>
|
||||
</div>
|
||||
|
||||
<% if f.object.id && !defined?(suggesting) %>
|
||||
|
@ -101,8 +101,7 @@
|
|||
<div class="boxline">
|
||||
<%= f.label :description, t('.text'), :class => "required" %>
|
||||
<%= f.text_area :description, :rows => 15,
|
||||
:placeholder => t('.placeholdertext'),
|
||||
:autocomplete => "off" %>
|
||||
:placeholder => t('.placeholdertext') %>
|
||||
</div>
|
||||
|
||||
<div class="boxline actions markdown_help_toggler">
|
||||
|
|
|
@ -26,7 +26,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
|
|||
<% end %>
|
||||
</span>
|
||||
<% if story.markeddown_description.present? %>
|
||||
<a class="description_present" title="<%= t '.additionaltext' %>" href="<%= story.comments_path %>">☶</a>
|
||||
<a class="description_present" title="<%= truncate(story.description,
|
||||
:length => 500) %>" href="<%= story.comments_path %>">☶</a>
|
||||
<% end %>
|
||||
<% if story.can_be_seen_by_user?(@user) %>
|
||||
<span class="tags">
|
||||
|
@ -62,10 +63,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
|
|||
<% end %>
|
||||
<span class="byline">
|
||||
<% if (@user && @user.show_avatars?) || !@user %>
|
||||
<a href="/u/<%= ms.user.username %>"><img
|
||||
src="<%= ms.user.avatar_url(16) %>"
|
||||
srcset="<%= ms.user.avatar_url(16) %> 1x,
|
||||
<%= ms.user.avatar_url(32) %> 2x" class="avatar"></a>
|
||||
<a href="/u/<%= ms.user.username %>"><%=
|
||||
avatar_img(ms.user, 16) %></a>
|
||||
<% end %>
|
||||
<% if story.user_is_author? %>
|
||||
<%= t('.authoredby') %>
|
||||
|
@ -76,6 +75,10 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
|
|||
ms.html_class_for_user %>"><%= ms.user.username %></a>
|
||||
|
||||
<%= distance_of_time_in_words(ms.created_at, Time.now, :strip_about => true) %>
|
||||
<% if ms.is_editable_by_user?(@user) %>
|
||||
|
|
||||
<a href="<%= edit_story_path(ms.short_id) %>"><%= t('.edit') %></a>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
@ -92,10 +95,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
|
|||
|
||||
<div class="byline">
|
||||
<% if (@user && @user.show_avatars?) || !@user %>
|
||||
<a href="/u/<%= story.user.username %>"><img
|
||||
src="<%= story.user.avatar_url(16) %>"
|
||||
srcset="<%= story.user.avatar_url(16) %> 1x,
|
||||
<%= story.user.avatar_url(32) %> 2x" class="avatar"></a>
|
||||
<a href="/u/<%= story.user.username %>"><%=
|
||||
avatar_img(story.user, 16) %></a>
|
||||
<% end %>
|
||||
<% if story.previewing %>
|
||||
<% if story.user_is_author? %>
|
||||
|
@ -119,7 +120,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
|
|||
|
||||
<% if story.is_editable_by_user?(@user) %>
|
||||
|
|
||||
<a href="<%= edit_story_path(story.short_id) %>"><%= t('.edit') %></a>
|
||||
<a href="<%= edit_story_path(story.short_id) %>" class="<%=
|
||||
story.has_suggestions? ? "story_has_suggestions" : "" %>"><%= t('.edit') %></a>
|
||||
|
||||
<% if story.is_gone? && story.is_undeletable_by_user?(@user) %>
|
||||
|
|
||||
|
@ -163,7 +165,7 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
|
|||
<% end %>
|
||||
<% if story.url.present? %>
|
||||
|
|
||||
<a href="https://archive.is/<%= story.url %>" rel="nofollow"
|
||||
<a href="<%= story.archive_url %>" rel="nofollow"
|
||||
target="_new"><%= t('.cached') %></a>
|
||||
<% end %>
|
||||
<% if !story.is_gone? %>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<div class="boxline">
|
||||
<%= f.label :moderation_reason, t('.modreason'),
|
||||
:class => "required" %>
|
||||
<%= f.text_field :moderation_reason, :autocomplete => "off" %>
|
||||
<%= f.text_field :moderation_reason %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -45,9 +45,22 @@
|
|||
<% comments_by_parent = @comments.group_by(&:parent_comment_id) %>
|
||||
<% subtree = comments_by_parent[nil] %>
|
||||
<% ancestors = [] %>
|
||||
<% dragons = false %>
|
||||
|
||||
<% while subtree %>
|
||||
<% if (comment = subtree.shift) %>
|
||||
<% if comment.is_dragon? && !dragons %>
|
||||
<div class="dragons">
|
||||
<div class="dragon_text">
|
||||
<a class="toggle_dragons">
|
||||
<%= raw(t('.toggledragons')) %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dragon_threads <%= @user && @user.hide_dragons? ?
|
||||
"hidden" : "" %>">
|
||||
<% dragons = true %>
|
||||
<% end %>
|
||||
|
||||
<li>
|
||||
<%= render "comments/comment", :comment => comment,
|
||||
:show_story => (comment.story_id != @story.id),
|
||||
|
@ -64,5 +77,10 @@
|
|||
</ol></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if dragons %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</ol>
|
||||
<% end %>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="boxline">
|
||||
<%= label_tag :email, t(:emailaddress), :class => "required" %>
|
||||
<%= text_field_tag :email, "", :size => 30, :autocomplete => "off" %>
|
||||
<%= email_field_tag :email, "", :size => 30, :autocomplete => "off" %>
|
||||
</div>
|
||||
|
||||
<div class="boxline">
|
||||
|
|
|
@ -15,11 +15,10 @@
|
|||
><%= user.username %></a>
|
||||
<% if user.is_admin? %>
|
||||
<%= t('.moderator') %>
|
||||
<% elsif user.is_moderator? %>
|
||||
<%= t('.moderator') %>
|
||||
<% else %>
|
||||
(<%= user.karma %>)
|
||||
<% if user.is_moderator? %>
|
||||
<%= t('.administrator') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
|
||||
<% if @showing_user.is_active? %>
|
||||
<div id="gravatar">
|
||||
<img src="<%= @showing_user.avatar_url(100) %>"
|
||||
srcset="<%= @showing_user.avatar_url(100) %> 1x,
|
||||
<%= @showing_user.avatar_url(200) %> 2x">
|
||||
<%= avatar_img(@showing_user, 100) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
@ -49,7 +47,7 @@
|
|||
<span class="d">
|
||||
<%= distance_of_time_in_words(@showing_user.created_at, Time.now) %>
|
||||
<% if @showing_user.invited_by_user %>
|
||||
<%= t('.byinvitationfrom') %>
|
||||
<%= raw(t('.byinvitationfrom', :user => @showing_user.username)) %>
|
||||
<%= link_to @showing_user.invited_by_user.try(:username),
|
||||
@showing_user.invited_by_user %>
|
||||
<% end %>
|
||||
|
@ -69,16 +67,6 @@
|
|||
<br>
|
||||
<% end %>
|
||||
|
||||
<% if @showing_user.hats.any? %>
|
||||
<label class="required"><%= t('.hats') %></label>
|
||||
<span class="d">
|
||||
<% @showing_user.hats.each do |hat| %>
|
||||
<%= hat.to_html_label %>
|
||||
<% end %>
|
||||
</span>
|
||||
<br>
|
||||
<% end %>
|
||||
|
||||
<% if @showing_user.deleted_at? %>
|
||||
<label class="required"><% t('.left') %></label>
|
||||
<span class="d">
|
||||
|
@ -117,6 +105,38 @@
|
|||
</span>
|
||||
<br>
|
||||
|
||||
<% if @showing_user.hats.any? %>
|
||||
<label class="required"><%= t('.hats') %></label>
|
||||
<div class="d">
|
||||
<% @showing_user.hats.each do |hat| %>
|
||||
<%= hat.to_html_label %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div style="clear: both;"></div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<% if @showing_user.github_username.present? %>
|
||||
<label class="required">GitHub:</label>
|
||||
|
||||
<span class="d">
|
||||
<a href="https://github.com/<%= h(@showing_user.github_username) %>"
|
||||
rel="nofollow">https://github.com/<%= h(@showing_user.github_username)
|
||||
%></a>
|
||||
</span>
|
||||
<br>
|
||||
<% end %>
|
||||
|
||||
<% if @showing_user.twitter_username.present? %>
|
||||
<label class="required">Twitter:</label>
|
||||
|
||||
<span class="d">
|
||||
<a href="https://twitter.com/<%= h(@showing_user.twitter_username) %>"
|
||||
rel="nofollow">@<%= h(@showing_user.twitter_username) %></a>
|
||||
</span>
|
||||
<br>
|
||||
<% end %>
|
||||
|
||||
<% if @showing_user.is_active? %>
|
||||
<label class="required"><%= t('.about') %></label>
|
||||
|
||||
|
|
|
@ -3,21 +3,23 @@
|
|||
<strong><%= @title %> (<%= @user_count %>)</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= t('.newestusers') %>
|
||||
<%= raw @newest.map{|u| "<a href=\"/u/#{u.username}\" class=\"" <<
|
||||
(u.is_new?? "new_user" : "") << "\">#{u.username}</a> " <<
|
||||
"(#{u.karma})" }.join(", ") %>
|
||||
</p>
|
||||
<% if @newest %>
|
||||
<p>
|
||||
<%= t('.newestusers') %>
|
||||
<%= raw @newest.map{|u| "<a href=\"##{u.username}\" class=\"" <<
|
||||
(u.is_new?? "new_user" : "") << "\">#{u.username}</a> " <<
|
||||
"(#{u.karma})" }.join(", ") %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<ul class="tree user_tree">
|
||||
<ul class="tree user_tree noparent">
|
||||
|
||||
<% subtree = @users_by_parent[nil] %>
|
||||
<% ancestors = [] %>
|
||||
|
||||
<% while subtree %>
|
||||
<% if (user = subtree.pop) %>
|
||||
<li>
|
||||
<li class="<%= user.invited_by_user_id ? "" : "noparent" %>">
|
||||
<a href="/u/<%= user.username %>"
|
||||
<% if !user.is_active? %>
|
||||
class="inactive_user"
|
||||
|
@ -27,11 +29,10 @@
|
|||
><%= user.username %></a>
|
||||
<% if user.is_admin? %>
|
||||
<%= t('.administrator') %>
|
||||
<% elsif user.is_moderator? %>
|
||||
<%= t('.moderator') %>
|
||||
<% else %>
|
||||
(<%= user.karma %>)
|
||||
<% if user.is_moderator? %>
|
||||
<%= t('.moderator') %>(moderator)
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if (children = @users_by_parent[user.id]) %>
|
||||
<% # drill down deeper in the tree %>
|
||||
|
|
|
@ -26,7 +26,15 @@ module Lobsters
|
|||
# Raise an exception when using mass assignment with unpermitted attributes
|
||||
config.action_controller.action_on_unpermitted_parameters = :raise
|
||||
|
||||
config.active_record.raise_in_transactional_callbacks = true
|
||||
|
||||
config.cache_store = :file_store, "#{config.root}/tmp/cache/"
|
||||
|
||||
config.exceptions_app = self.routes
|
||||
|
||||
config.after_initialize do
|
||||
require "#{Rails.root}/lib/monkey.rb"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -68,5 +76,3 @@ class << Rails.application
|
|||
true
|
||||
end
|
||||
end
|
||||
|
||||
require "#{Rails.root}/lib/monkey"
|
||||
|
|
|
@ -20,7 +20,7 @@ Lobsters::Application.configure do
|
|||
# config.action_dispatch.rack_cache = true
|
||||
|
||||
# Disable Rails's static asset server (Apache or nginx will already do this).
|
||||
config.serve_static_assets = false
|
||||
config.serve_static_files = false
|
||||
|
||||
# Compress JavaScripts and CSS.
|
||||
config.assets.js_compressor = :uglifier
|
||||
|
@ -76,3 +76,7 @@ Lobsters::Application.configure do
|
|||
# Do not dump schema after migrations.
|
||||
config.active_record.dump_schema_after_migration = false
|
||||
end
|
||||
|
||||
%w{render_template render_partial render_collection}.each do |event|
|
||||
ActiveSupport::Notifications.unsubscribe "#{event}.action_view"
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ Lobsters::Application.configure do
|
|||
config.eager_load = false
|
||||
|
||||
# Configure static asset server for tests with Cache-Control for performance.
|
||||
config.serve_static_assets = true
|
||||
config.serve_static_files = true
|
||||
config.static_cache_control = 'public, max-age=3600'
|
||||
|
||||
# Show full error reports and disable caching.
|
||||
|
@ -35,5 +35,7 @@ Lobsters::Application.configure do
|
|||
config.active_support.deprecation = :stderr
|
||||
|
||||
# Raises error for missing translations
|
||||
# config.action_view.raise_on_missing_translations = true
|
||||
config.action_view.raise_on_missing_translations = true
|
||||
end
|
||||
|
||||
RSpec::Expectations.configuration.on_potential_false_positives = :nothing
|
||||
|
|
|
@ -22,6 +22,9 @@ en:
|
|||
storyidcannotbeblank: "A story ID cannot be blank."
|
||||
deletedcomment: "deleted comment"
|
||||
threadremovedby: "Thread removed by moderator %{modoname} : %{modreason}"
|
||||
turnedintodragon: "turned into a dragon"
|
||||
slayeddragon: "slayed dragon"
|
||||
metooerror: "Please just upvote the parent post instead."
|
||||
moderation:
|
||||
storyeditedby: "Your story has been edited by "
|
||||
usersuggestions: "user suggestions"
|
||||
|
@ -61,11 +64,11 @@ en:
|
|||
messageslink: "Messages"
|
||||
loginlink: "Login"
|
||||
logoutlink: "Logout"
|
||||
confirmlogoutlink: "Are you sure you want to logout?"
|
||||
moderationloglink: "Moderation Log"
|
||||
invitationqueuelink: "Invitation Queue"
|
||||
chatlink: "Chat"
|
||||
hatrequestlink: "Hat Requests"
|
||||
hatslink: "Hats"
|
||||
privacylink: "Privacy"
|
||||
aboutlink: "About"
|
||||
blog: "Blog"
|
||||
|
@ -102,6 +105,8 @@ en:
|
|||
delete: "delete"
|
||||
reply: "reply"
|
||||
about: "on:"
|
||||
dragon: "dragon"
|
||||
undragon: "undragon"
|
||||
email_message:
|
||||
notification.text:
|
||||
replyat: "Reply at "
|
||||
|
@ -120,6 +125,41 @@ en:
|
|||
pretextdesc: "prefix text with at least<tt> 4 spaces</tt>"
|
||||
inlineimage: "(inline image)"
|
||||
inlineimagedesc: "![alt text](http://example.com/image.jpg)</tt> (only allowed in story text)"
|
||||
hats:
|
||||
build_request:
|
||||
title: "Request a Hat"
|
||||
description: "<p>A hat is a formal, verified, way of posting a comment while speaking for a project, organization, or company. Each user may have multiple hats, one of which may be worn at any time when posting a comment or sending a private message.</p><p>Hats are intended for core project developers and company employees that are authorized to make comments on behalf of those organizations, and are usually reserved for notable projects and organizations. Hats are not intended to list every contribution a person has made to any software project.</p><p>Hats will not be authorized for occasional contributors to projects, developers of projects which are not widely known, past employees of companies, or other situations where it cannot be verified that one is authorized to represent that organization. In general, unless one has an e-mail address at a compay domain or have commit access to a project, a hat will not be authorized.</p><p>To request a hat for your account, provide a short description of the hat (e.g., \"OpenBSD Developer\"), a public link that will be shown when hovering over the hat that users can see, such as your e-mail address at that project or company, or a link to a company website showing your employment, and private comments that will be seen only by moderators during approval.</p><p>Once your hat is requested, a moderator will verify your request by e-mailing the address you submitted as the link, or doing some other manual verification of project association.</p>"
|
||||
hat: "Hat:"
|
||||
hatplaceholder: "XYZ Project Member"
|
||||
link: "Link:"
|
||||
linkplaceholder: "user@project.org, or a URL to an employment page"
|
||||
comment: "Comment:"
|
||||
commentplaceholder: "Will only be shown to moderators during approval"
|
||||
requesthatbutton: "Request Hat"
|
||||
create_request:
|
||||
submittedhatrequest: "Successfully submitted hat request."
|
||||
index:
|
||||
title: "Hats"
|
||||
description: "A hat is a formal, verified, way of posting a comment while speaking for a project, organization, or company. Each user may have multiple hats, one of which may be selected to be worn when posting a comment or sending a private message."
|
||||
request: "Request Hat"
|
||||
user: "User"
|
||||
hat: "Hat"
|
||||
link: "Link"
|
||||
reject_request:
|
||||
requestedhatrequest: "Successfully rejected hat request."
|
||||
requests_index:
|
||||
title: "Requested Hats"
|
||||
nohatrequests: "No hat requests."
|
||||
user: "User:"
|
||||
hat: "Hat:"
|
||||
link: "Link:"
|
||||
comment: "Comment:"
|
||||
approve: Approve Hat Request"
|
||||
reason: "Reason:"
|
||||
reject: "Reject Hat Request"
|
||||
hator: "or"
|
||||
approve_request:
|
||||
approvedhatrequest: "Successfully approved hat request."
|
||||
home:
|
||||
index:
|
||||
homerecentsdesc: "<em>The <a href=\"/newest\">newest</a> stories with a random sampling of recently submitted stories that have not yet reached the front page.</em>"
|
||||
|
@ -180,6 +220,16 @@ en:
|
|||
password: "New Password:"
|
||||
again: "(Again):"
|
||||
setpassbutton: "Set New Password"
|
||||
passwordreset: "Your password has been reset."
|
||||
couldnotresetpassword: "Could not reset password."
|
||||
invalidresettoken: "Invalid reset token. It may have already been used or you may have copied it incorrectly."
|
||||
twofa:
|
||||
login2fa: "Login - Two Factor Authentication"
|
||||
logintotpcode: "Enter the current TOTP code from your TOTP application:"
|
||||
loginbutton: "Login"
|
||||
totpcode: "TOTP Code:"
|
||||
twofa_verify:
|
||||
totpcodenotmatch: "Your TOTP code did not match. Please try again."
|
||||
messages:
|
||||
index:
|
||||
viewreceived: "View Received"
|
||||
|
@ -245,10 +295,13 @@ en:
|
|||
deleteaccountflash: "Your account has been deleted."
|
||||
verifypasswordflash: "Your password could not be verified."
|
||||
index:
|
||||
logoutlink: "Logout"
|
||||
confirmlogoutlink: "Are you sure you want to logout?"
|
||||
viewprofile: "View Profile"
|
||||
accountsettings: "Account Settings"
|
||||
username: "Username:"
|
||||
password: "New Password:"
|
||||
currentpassword: "Current Password:"
|
||||
confirmpassword: "Confirm Password:"
|
||||
emailaddress: "E-mail Address:"
|
||||
gravatarized: "<a href=\"http://www.gravatar.com/\" target=\"_blank\">Gravatar</a>'ized"
|
||||
|
@ -263,7 +316,7 @@ en:
|
|||
commentreplynotificationsettings: "Comment Reply Notification Settings"
|
||||
receiveemail: "Receive E-mail:"
|
||||
receivepushover: "Receive Pushover Alert:"
|
||||
requirepushover: "Requires Pushover subscription above"
|
||||
requirepushover: "Requires Pushover subscription below"
|
||||
commentmentionnotificationsettings: "Comment Mention Notification Settings"
|
||||
privatemessagenotificationsettings: "Private Message Notification Settings"
|
||||
submittedstorycommentsettings: "Submitted Story Comment Settings"
|
||||
|
@ -278,6 +331,7 @@ en:
|
|||
miscsettings: "Miscellaneous Settings"
|
||||
storypreview: "Show Story Previews:"
|
||||
useravatars: "Show User Avatars:"
|
||||
hidedragons: "Hide Dragons:"
|
||||
saveallsettings: "Save All Settings"
|
||||
inviteuser: "Invite a New User"
|
||||
cannotsendinvitations: "You cannot send invitations."
|
||||
|
@ -285,6 +339,12 @@ en:
|
|||
deleteaccounttext: "To permanently delete your account, verify your current password below. Your account will be put into a deleted state, your comments will be marked as deleted and no longer readable by any other users, and your private messages will be deleted. Your submitted stories will not be deleted. Your username will remain reserved and will not be available to be used on any other account."
|
||||
verifypassword: "Verify Password:"
|
||||
deleteaccountconfirmation: "Yes, Delete My Account"
|
||||
securitysettings: "Security Settings"
|
||||
twofactorauth: "Two-Factor Auth:"
|
||||
disable2fa: "Disable"
|
||||
enroll2fa: "Enroll"
|
||||
disabled2fa: "Disabled"
|
||||
enabled2fa: "Enabled"
|
||||
pushover:
|
||||
pushovernotconfigured: "This site is not configured for Pushover"
|
||||
pushover_callback:
|
||||
|
@ -292,8 +352,36 @@ en:
|
|||
pushovernorandomtokenurl: "No random token present in URL"
|
||||
accountsetuppushover: "Your account is now setup for Pushover notifications."
|
||||
accountnolongersetuppushover: "Your account is no longer setup for Pushover notifications."
|
||||
twofa:
|
||||
title: "Two-Factor Authentication"
|
||||
backtosettings: "Back to Settings"
|
||||
disable2fa: "Disable Two-Factor Authentication"
|
||||
continue: "Continue"
|
||||
currentpassword: "Current Password:"
|
||||
turnoff: "To turn off two-factor authentication for your account, enter your current password:"
|
||||
turnon: "To begin the two-factor authentication enrollment for your account, enter your current password:"
|
||||
twofa_auth:
|
||||
2fahasbeendisabled: "Two-Factor Authentication has been disabled on your account."
|
||||
2fapassnotcorrect: "Your password was not correct."
|
||||
twofa_enroll:
|
||||
title: "Two-Factor Authentication"
|
||||
backtosettings: "Back to Settings"
|
||||
enrollmenttimeout: "Your enrollment period timed out."
|
||||
scanqrcode: "Scan the QR code below or click on it to open in your <a href=\"https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm\" target=\"_blank\">TOTP</a> application of choice:"
|
||||
registring: "Once you have finished registering with your TOTP application, proceed to the next screen to verify your current TOTP code and actually enable Two-Factor Authentication on your account."
|
||||
verifyenable: "Verify and Enable"
|
||||
twofa_update:
|
||||
enrollmenttimeout: "Your enrollment period timed out."
|
||||
2fahasbeenenabled: "Two-Factor Authentication has been enabled on your account."
|
||||
totpinvalid: "Your TOTP code was invalid, please verify the current code in your TOTP application."
|
||||
twofa_verify:
|
||||
title: "Two-Factor Authentication"
|
||||
enrollmenttimeout: "Your enrollment period timed out."
|
||||
enablecode: "To enable Two-Factor Authentication on your account using your new TOTP secret, enter the six-digit code from your TOTP application:"
|
||||
verifyenable: "Verify and Enable"
|
||||
update:
|
||||
updatesettingsflash: "Successfully updated settings."
|
||||
passwordnotcorrect: "Your current password was not entered correctly."
|
||||
stories:
|
||||
edit:
|
||||
edit: "Edit Story"
|
||||
|
@ -355,6 +443,8 @@ en:
|
|||
preview: "Preview"
|
||||
submit: "Submit a Story"
|
||||
submitbutton: "Submit"
|
||||
show:
|
||||
toggledragons: "— here be dragons —"
|
||||
users:
|
||||
list:
|
||||
administrator: "administrator"
|
||||
|
@ -372,7 +462,7 @@ en:
|
|||
storysubmissions: "with story submissions"
|
||||
disabled: "disabled"
|
||||
joined: "Joined:"
|
||||
byinvitationfrom: "by invitation from"
|
||||
byinvitationfrom: "by <a href=\"/u/%{user}\">invitation</a> from"
|
||||
banneduser: "Banned:"
|
||||
bannedby: "by"
|
||||
hats: "Hats:"
|
||||
|
@ -439,6 +529,9 @@ en:
|
|||
flashsuccessdeleteinvit: "Successfully deleted invitation request from %{name}"
|
||||
login_controller:
|
||||
flashlogininvalid: "Invalid e-mail address and/or password."
|
||||
totpinvalid: "Your TOTP code was invalid."
|
||||
deletedaccount: "Your account has been deleted."
|
||||
bannedaccount: "Your account has been banned."
|
||||
messages_controller:
|
||||
messagestitle: "Messages"
|
||||
messagessenttitle: "Messages Sent"
|
||||
|
@ -449,6 +542,7 @@ en:
|
|||
flashdeletedmessage: "Deleted message."
|
||||
search_controller:
|
||||
searchtitle: "Search"
|
||||
flasherrorsearchcontroller: "Sorry, but the search engine is currently out of order"
|
||||
stories_controller:
|
||||
submitstorytitle: "Submit Story"
|
||||
editstorytitle: "Edit Story"
|
||||
|
|
|
@ -34,6 +34,9 @@ fr:
|
|||
storyidcannotbeblank: "Un ID d'info ne peut pas être vide."
|
||||
deletedcomment: "commentaire supprimé"
|
||||
threadremovedby: "Fil supprimé par le modérateur %{modoname} : %{modreason}"
|
||||
turnedintodragon: "transformé en dragon"
|
||||
slayeddragon: "le dragon a été terrassé"
|
||||
metooerror: "Merci de voter pour le commentaire à la place."
|
||||
moderation:
|
||||
storyeditedby: "Votre info a été éditée par "
|
||||
usersuggestions: "suggestions d'utilisateur"
|
||||
|
@ -78,6 +81,7 @@ fr:
|
|||
invitationqueuelink: "File d'invitation"
|
||||
chatlink: "Chat"
|
||||
hatrequestlink: "Porter le chapeau"
|
||||
hatslink: "Chapeaux"
|
||||
privacylink: "Confidentialité"
|
||||
aboutlink: "À propos"
|
||||
blog: "Blog"
|
||||
|
@ -104,6 +108,8 @@ fr:
|
|||
delete: "supprimer"
|
||||
reply: "répondre"
|
||||
about: "sur :"
|
||||
dragon: "dragon"
|
||||
undragon: "non-dragon"
|
||||
global:
|
||||
markdownhelp:
|
||||
emphasizedtext: "italique"
|
||||
|
@ -120,6 +126,41 @@ fr:
|
|||
pretextdesc: "précéder le texte avec au moins <tt> 4 espaces</tt>"
|
||||
inlineimage: "(image en ligne)"
|
||||
inlineimagedesc: "![texte de substitution](http://example.com/image.jpg)</tt> (autorisé seulement dans les articles)"
|
||||
hats:
|
||||
build_request:
|
||||
title: "Demander un chapeau"
|
||||
description: "<p>Un chapeau est un moyen formel, vérifié de poster un commentaire au nom d'un projet, d'une organisation ou d'une entreprise. Chaque utilisateur peut avoir plusieurs chapeaux, l'un d'eux pouvant être choisi d'être porté au moment de poster un commentaire ou d'envoyer un message privé.</p><p>Les chapeaux sont destinés aux développeurs principaux d'un projet et les employés d'entreprise qui sont autorisés à faire des commentaires au nom de leurs organisations, et sont habituellement réservés pour les projets notables et les organisations. Les chapeaux ne sont pas destinés à lister toutes les contributions d'une personne à des projets logiciels.</p><p>Les chapeaux ne vont pas être autorisés pour les contributeurs occasionnels d'un projet, développeurs d'un projet qui n'est pas largement connu, pas non plus pour les anciens employés d'une entreprise ou autres situations où il n'est pas possible de vérifier que quelqu'un est autorisé à représenter une organisation. En général à moins d'avoir un e-mail avec le nom de domaine de l'entreprise ou du projet ou les droits d'accès aux sources d'un projet, un chapeau ne sera pas autorisé.</p><p>Pour demander un chapeau pour votre compte, merci de fournir une courte description du chapeau (exemple \"développeur OpenBSD\"), un lien public qui sera affiché quand on passe la souris au-dessus du chapeau que les utilisateurs peuvent voir, comme votre adresse e-mail du projet ou de l'entreprise ou un lien vers le site web de l'entreprise puis une description privée qui sera seulement vue par les modérateurs durant le processus d'approbation.</p><p>Une fois qu'un chapeau a été demandé, un modérateur va vérifier votre demande en envoyant un e-mail à l'adresse soumise comme lien et/ou en effectuant d'autres vérifications manuelles de votre association au projet.</p>"
|
||||
hat: "Chapeau:"
|
||||
hatplaceholder: "Membre du projet XYZ"
|
||||
link: "Lien :"
|
||||
linkplaceholder: "membre@projet.org, ou l'URL de la page de l'employeur"
|
||||
comment: "Commentaire :"
|
||||
commentplaceholder: "Sera seulement montré aux modérateurs devant le processus d'approbation"
|
||||
requesthatbutton: "Demander un chapeau"
|
||||
create_request:
|
||||
submittedhatrequest: "Demande de chapeau transmise avec succès."
|
||||
index:
|
||||
title: "Chapeaux"
|
||||
description: "Un chapeau est un moyen formel, vérifié de poster un commentaire au nom d'un projet, d'une organisation ou d'une entreprise. Chaque utilisateur peut avoir plusieurs chapeaux, l'un d'eux pouvant être choisi d'être porté au moment de poster un commentaire ou d'envoyer un message privé."
|
||||
request: "Demander un chapeau"
|
||||
user: "Utilisateur"
|
||||
hat: "Chapeau"
|
||||
link: "Lien"
|
||||
reject_request:
|
||||
rejectedhatrequest: "Demande de chapeau rejetée avec succès."
|
||||
requests_index:
|
||||
title: "Chapeaux demandés"
|
||||
nohatrequests: "Aucune demande de chapeau."
|
||||
user: "Utilisateur :"
|
||||
hat: "Chapeau :"
|
||||
link: "Lien :"
|
||||
comment: "Commentaire :"
|
||||
approve: "Approuver la demande de chapeau"
|
||||
reason: "Raison :"
|
||||
reject: "Rejeter la demande de chapeau"
|
||||
hator: "ou"
|
||||
approve_request:
|
||||
approvedhatrequest: "La demande de chapeau a été approuvée."
|
||||
home:
|
||||
index:
|
||||
homerecentsdesc: "<em>Les <a href=\"/newest\">plus récentes</a> infos avec un panaché aléatoire des infos récentes soumises qui n'ont pas encore atteint la page principale.</em>"
|
||||
|
@ -180,6 +221,16 @@ fr:
|
|||
password: "Mot de passe :"
|
||||
again: "(encore):"
|
||||
setpassbutton: "Changer le mot de passe"
|
||||
passwordreset: "Votre mot de passe a été changé"
|
||||
couldnotresetpassword: "Le mot de passe n'a pas pu être changé."
|
||||
invalidresettoken: "Jeton de changement invalide. Il a pu déjà être utilisé ou mal copié."
|
||||
twofa:
|
||||
login2fa: "Identification par authentification à deux facteurs"
|
||||
logintotpcode: "Entrez le code TOTP affiché par votre application :"
|
||||
loginbutton: "S'identifier"
|
||||
totpcode: "Code TOTP :"
|
||||
twofa_verify:
|
||||
totpcodenotmatch: "Code TOTP incorrect. Merci de ré-essayer."
|
||||
filters:
|
||||
index:
|
||||
filteredtags: "Marques filtrées"
|
||||
|
@ -255,10 +306,13 @@ fr:
|
|||
deleteaccountflash: "Votre compte a été supprimé."
|
||||
verifypasswordflash: "Votre mot de passe n'a pas pu être vérifié."
|
||||
index:
|
||||
logoutlink: "Se déconnecter"
|
||||
confirmlogoutlink: "Êtes-vous sûr de vouloir vous déconnecter?"
|
||||
viewprofile: "Voir le profil"
|
||||
accountsettings: "Paramètres du compte"
|
||||
username: "Utilisateur :"
|
||||
password: "Nouveau mot de passe :"
|
||||
currentpassword: "Mot de passe actuel :"
|
||||
confirmpassword: "Confirmer le mot de passe :"
|
||||
emailaddress: "Adresse e-mail :"
|
||||
gravatarized: "<a href=\"http://www.gravatar.com/\" target=\"_blank\">Gravatar</a>isé"
|
||||
|
@ -273,7 +327,7 @@ fr:
|
|||
commentreplynotificationsettings: "Paramètres de notification de réponse à un commentaire"
|
||||
receiveemail: "Recevoir un e-mail :"
|
||||
receivepushover: "Recevoir une alerte Pushover :"
|
||||
requirepushover: "Requière un abonnement Pushover ci-dessus"
|
||||
requirepushover: "Requière un abonnement Pushover ci-dessous"
|
||||
commentmentionnotificationsettings: "Paramètres de notification de mention d'un commentaire"
|
||||
privatemessagenotificationsettings: "Paramètres de notification de message privé"
|
||||
submittedstorycommentsettings: "Paramètres de commentaires relatifs à vos infos"
|
||||
|
@ -288,6 +342,7 @@ fr:
|
|||
miscsettings: "Paramètres variés"
|
||||
storypreview: "Montrer les aperçus des infos: "
|
||||
useravatars: "Montrer les avatars des utilisateurs :"
|
||||
hidedragons: "Cacher les dragons :"
|
||||
saveallsettings: "Sauver tous les paramètres"
|
||||
inviteuser: "Inviter un nouvel utilisateur"
|
||||
cannotsendinvitations: "Vous ne pouvez pas envoyer d'invitations."
|
||||
|
@ -295,6 +350,12 @@ fr:
|
|||
deleteaccounttext: "Pour supprimer définitivement votre compte, vérifiez votre mot de passe actuel ci-dessous. Votre compte sera passé à l'état supprimé, vos commentaires seront marqués comme supprimés et ne seront plus lisibles par les autres utilisateurs, vos messages privés seront également supprimés. Vos infos proposées ne seront pas supprimées. Votre nom d'utilisateur restera réservé et ne sera pas disponible pour un autre compte."
|
||||
verifypassword: "Vérification du mot de passe :"
|
||||
deleteaccountconfirmation: "Oui, supprimez mon compte"
|
||||
securitysettings: "Paramètres de sécurité"
|
||||
twofactorauth: "Authentification à deux facteurs :"
|
||||
disable2fa: "Désactiver"
|
||||
enroll2fa: "Activer"
|
||||
disabled2fa: "Désactivée"
|
||||
enabled2fa: "Activée"
|
||||
pushover:
|
||||
pushovernotconfigured: "Ce site n'est pas configuré pour le Pushover"
|
||||
pushover_callback:
|
||||
|
@ -302,8 +363,36 @@ fr:
|
|||
pushovernorandomtokenurl: "Pas de jeton alétoire présent dans l'url"
|
||||
accountsetuppushover: "Votre compte est maintenant configuré pour les notifications Pushover."
|
||||
accountnolongersetuppushover: "Votre compte n'est plus configuré pour les notifications Pushover."
|
||||
twofa:
|
||||
title: "Authentification à deux facteurs"
|
||||
backtosettings: "Retour aux paramètres"
|
||||
disable2fa: "Désactiver l'authentification à deux facteurs"
|
||||
continue: "Continuer"
|
||||
currentpassword: "Mot de passe actuel :"
|
||||
turnoff: "Pour désactiver l'authentification à deux facteurs, entrez votre mot de passe actuel :"
|
||||
turnon: "Pour activer l'authentification à deux facteurs pour votre compte, entrez votre mot de passe actuel :"
|
||||
twofa_auth:
|
||||
2fahasbeendisabled: "L'authentification à deux facteurs a été désactivée sur votre compte"
|
||||
2fapassnotcorrect: "Mot de passe incorrect"
|
||||
twofa_enroll:
|
||||
title: "Authentification à deux facteurs"
|
||||
backtosettings: "Retour aux paramètres"
|
||||
enrollmenttimeout: "Le délai d'attente est dépassé"
|
||||
scanqrcode: "Scannez le QR code ci-dessous ou cliquez dessus pour l'ouvrir dans l'application <a href=\"https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm\" target=\"_blank\">TOTP</a> de votre choix :"
|
||||
registring: "Une fois le processus d'enregistrement de vos applications TOTP achevé, passez à l'écran suivant pour vérifier et activer l'authentification à deux facteurs pour votre compte."
|
||||
verifyenable: "Vérifier et Activer"
|
||||
twofa_update:
|
||||
enrollmenttimeout: "Le délai d'attente est dépassé"
|
||||
2fahasbeenenabled: "L'authentification à deux facteurs a été activée sur votre compte"
|
||||
totpinvalid: "Votre code TOTP est invalide, merci de vérifier le code actuellement affiché par votre application TOTP"
|
||||
twofa_verify:
|
||||
title: "Authentification à deux facteurs"
|
||||
enrollmenttimeout: "Le délai d'attente est dépassé"
|
||||
enablecode: "Afin d'activer l'authentification à deux facteurs pour votre compte en utilisant votre nouveau secret TOTP, entrez le code à 6 chiffres de votre application :"
|
||||
verifyenable: "Vérifier et Activer"
|
||||
update:
|
||||
updatesettingsflash: "Paramètres mis à jour avec succès."
|
||||
passwordnotcorrect: "Le mot de passe courant n'a pas été entré correctement."
|
||||
stories:
|
||||
edit:
|
||||
edit: "Éditer l'info"
|
||||
|
@ -365,6 +454,8 @@ fr:
|
|||
preview: "Aperçu"
|
||||
submit: "Soumettre une info"
|
||||
submitbutton: "Soumettre"
|
||||
show:
|
||||
toggledragons: "— voici les dragons —"
|
||||
users:
|
||||
list:
|
||||
administrator: "administrateur"
|
||||
|
@ -382,7 +473,7 @@ fr:
|
|||
storysubmissions: "avec propositions d'infos"
|
||||
disabled: "désactivé"
|
||||
joined: "Inscrit :"
|
||||
byinvitationfrom: "par invitation de"
|
||||
byinvitationfrom: "par <a href=\"/u/%{user}\">invitation</a> de"
|
||||
banneduser: "Banni :"
|
||||
bannedby: "par"
|
||||
hats: "Chapeaux :"
|
||||
|
@ -449,6 +540,10 @@ fr:
|
|||
flashsuccessdeleteinvit: "Demande d'invitation de %{name} supprimée avec succès"
|
||||
login_controller:
|
||||
flashlogininvalid: "Adresse e-mail et/ou mot de passe invalide."
|
||||
flashlogininvalid: "Adresse e-mail et/ou mot de passe invalide."
|
||||
totpinvalid: "Votre code TOTP est invalide."
|
||||
deletedaccount: "Votre compte a été supprimé."
|
||||
bannedaccount: "Votre compte a été banni."
|
||||
messages_controller:
|
||||
messagestitle: "Messages"
|
||||
messagessenttitle: "Messages envoyés"
|
||||
|
@ -459,6 +554,7 @@ fr:
|
|||
flashdeletedmessage: "Message effacé."
|
||||
search_controller:
|
||||
searchtitle: "Rechercher"
|
||||
flasherrorsearchcontroller: "Désolé mais le moteur de recherche est actuellement cassé"
|
||||
stories_controller:
|
||||
submitstorytitle: "Soumettre une info"
|
||||
editstorytitle: "Éditer une info"
|
||||
|
|
|
@ -4,6 +4,8 @@ Lobsters::Application.routes.draw do
|
|||
:protocol => (Rails.application.config.force_ssl ? "https://" : "http://"),
|
||||
:as => "root"
|
||||
|
||||
get "/404" => "home#four_oh_four", :via => :all
|
||||
|
||||
get "/rss" => "home#index", :format => "rss"
|
||||
get "/hottest" => "home#index", :format => "json"
|
||||
|
||||
|
@ -30,8 +32,10 @@ Lobsters::Application.routes.draw do
|
|||
get "/threads/:user" => "comments#threads"
|
||||
|
||||
get "/login" => "login#index"
|
||||
post "/login" => "login#login"
|
||||
post "/login" => "login#login", :format => /html|json/
|
||||
post "/logout" => "login#logout"
|
||||
get "/login/2fa" => "login#twofa"
|
||||
post "/login/2fa_verify" => "login#twofa_verify", :as => "twofa_login"
|
||||
|
||||
get "/signup" => "signup#index"
|
||||
post "/signup" => "signup#signup"
|
||||
|
@ -72,6 +76,9 @@ Lobsters::Application.routes.draw do
|
|||
|
||||
post "delete"
|
||||
post "undelete"
|
||||
|
||||
post "dragon"
|
||||
post "undragon"
|
||||
end
|
||||
end
|
||||
get "/comments/page/:page" => "comments#index"
|
||||
|
@ -84,12 +91,14 @@ Lobsters::Application.routes.draw do
|
|||
post "keep_as_new"
|
||||
end
|
||||
|
||||
get "/s/:id/:title/comments/:comment_short_id" => "stories#show"
|
||||
get "/s/:id/(:title)" => "stories#show", :format => /html|json/
|
||||
|
||||
get "/c/:id" => "comments#redirect_from_short_id"
|
||||
get "/c/:id.json" => "comments#show_short_id", :format => "json"
|
||||
|
||||
# deprecated
|
||||
get "/s/:story_id/:title/comments/:id" => "comments#redirect_from_short_id"
|
||||
|
||||
get "/s/:id/(:title)" => "stories#show", :format => /html|json/
|
||||
|
||||
get "/u" => "users#tree"
|
||||
get "/u/:username" => "users#show", :as => "user", :format => /html|json/
|
||||
|
||||
|
@ -100,10 +109,25 @@ Lobsters::Application.routes.draw do
|
|||
|
||||
get "/settings" => "settings#index"
|
||||
post "/settings" => "settings#update"
|
||||
post "/settings/pushover" => "settings#pushover"
|
||||
get "/settings/pushover_callback" => "settings#pushover_callback"
|
||||
post "/settings/delete_account" => "settings#delete_account",
|
||||
:as => "delete_account"
|
||||
get "/settings/2fa" => "settings#twofa", :as => "twofa"
|
||||
post "/settings/2fa_auth" => "settings#twofa_auth", :as => "twofa_auth"
|
||||
get "/settings/2fa_enroll" => "settings#twofa_enroll",
|
||||
:as => "twofa_enroll"
|
||||
get "/settings/2fa_verify" => "settings#twofa_verify",
|
||||
:as => "twofa_verify"
|
||||
post "/settings/2fa_update" => "settings#twofa_update",
|
||||
:as => "twofa_update"
|
||||
|
||||
post "/settings/pushover_auth" => "settings#pushover_auth"
|
||||
get "/settings/pushover_callback" => "settings#pushover_callback"
|
||||
get "/settings/github_auth" => "settings#github_auth"
|
||||
get "/settings/github_callback" => "settings#github_callback"
|
||||
post "/settings/github_disconnect" => "settings#github_disconnect"
|
||||
get "/settings/twitter_auth" => "settings#twitter_auth"
|
||||
get "/settings/twitter_callback" => "settings#twitter_callback"
|
||||
post "/settings/twitter_disconnect" => "settings#twitter_disconnect"
|
||||
|
||||
get "/filters" => "filters#index"
|
||||
post "/filters" => "filters#update"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
class MoveUserSettings < ActiveRecord::Migration
|
||||
def up
|
||||
add_column :users, :settings, :text
|
||||
|
||||
[
|
||||
:email_notifications,
|
||||
:email_replies,
|
||||
:pushover_replies,
|
||||
:pushover_user_key,
|
||||
:email_messages,
|
||||
:pushover_messages,
|
||||
:email_mentions,
|
||||
:show_avatars,
|
||||
:show_story_previews,
|
||||
:show_submitted_story_threads,
|
||||
].each do |col|
|
||||
rename_column :users, col, "old_#{col}"
|
||||
end
|
||||
|
||||
User.find_each do |u|
|
||||
[
|
||||
:email_notifications,
|
||||
:email_replies,
|
||||
:pushover_replies,
|
||||
:pushover_user_key,
|
||||
:email_messages,
|
||||
:pushover_messages,
|
||||
:email_mentions,
|
||||
:show_avatars,
|
||||
:show_story_previews,
|
||||
:show_submitted_story_threads,
|
||||
].each do |k|
|
||||
u.settings[k] = u.send("old_#{k}")
|
||||
end
|
||||
|
||||
u.save(:validate => false)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :users, :settings
|
||||
|
||||
[
|
||||
:email_notifications,
|
||||
:email_replies,
|
||||
:pushover_replies,
|
||||
:pushover_user_key,
|
||||
:email_messages,
|
||||
:pushover_messages,
|
||||
:email_mentions,
|
||||
:show_avatars,
|
||||
:show_story_previews,
|
||||
:show_submitted_story_threads,
|
||||
].each do |col|
|
||||
rename_column :users, "old#{col}", col
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddDragons < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :comments, :is_dragon, :boolean, :default => false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
class DeleteOldSettings < ActiveRecord::Migration
|
||||
def change
|
||||
[
|
||||
:email_notifications,
|
||||
:email_replies,
|
||||
:pushover_replies,
|
||||
:pushover_user_key,
|
||||
:email_messages,
|
||||
:pushover_messages,
|
||||
:email_mentions,
|
||||
:show_avatars,
|
||||
:show_story_previews,
|
||||
:show_submitted_story_threads,
|
||||
].each do |col|
|
||||
remove_column :users, "old_#{col}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
class AddIndexes < ActiveRecord::Migration
|
||||
def change
|
||||
add_index :votes, [ :comment_id ]
|
||||
add_index :comments, [ :user_id ]
|
||||
add_index :stories, [ :created_at ]
|
||||
end
|
||||
end
|
193
db/schema.rb
193
db/schema.rb
|
@ -11,88 +11,90 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20160704022756) do
|
||||
ActiveRecord::Schema.define(version: 20170413161450) do
|
||||
|
||||
create_table "comments", force: true do |t|
|
||||
create_table "comments", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at"
|
||||
t.string "short_id", limit: 10, default: "", null: false
|
||||
t.integer "story_id", null: false
|
||||
t.integer "user_id", null: false
|
||||
t.integer "parent_comment_id"
|
||||
t.integer "thread_id"
|
||||
t.integer "story_id", limit: 4, null: false
|
||||
t.integer "user_id", limit: 4, null: false
|
||||
t.integer "parent_comment_id", limit: 4
|
||||
t.integer "thread_id", limit: 4
|
||||
t.text "comment", limit: 16777215, null: false
|
||||
t.integer "upvotes", default: 0, null: false
|
||||
t.integer "downvotes", default: 0, null: false
|
||||
t.integer "upvotes", limit: 4, default: 0, null: false
|
||||
t.integer "downvotes", limit: 4, default: 0, null: false
|
||||
t.decimal "confidence", precision: 20, scale: 19, default: 0.0, null: false
|
||||
t.text "markeddown_comment", limit: 16777215
|
||||
t.boolean "is_deleted", default: false
|
||||
t.boolean "is_moderated", default: false
|
||||
t.boolean "is_from_email", default: false
|
||||
t.integer "hat_id"
|
||||
t.integer "hat_id", limit: 4
|
||||
t.boolean "is_dragon", default: false
|
||||
end
|
||||
|
||||
add_index "comments", ["confidence"], name: "confidence_idx", using: :btree
|
||||
add_index "comments", ["short_id"], name: "short_id", unique: true, using: :btree
|
||||
add_index "comments", ["story_id", "short_id"], name: "story_id_short_id", using: :btree
|
||||
add_index "comments", ["thread_id"], name: "thread_id", using: :btree
|
||||
add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree
|
||||
|
||||
create_table "hat_requests", force: true do |t|
|
||||
create_table "hat_requests", force: :cascade do |t|
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "user_id"
|
||||
t.string "hat"
|
||||
t.string "link"
|
||||
t.text "comment"
|
||||
t.integer "user_id", limit: 4
|
||||
t.string "hat", limit: 255
|
||||
t.string "link", limit: 255
|
||||
t.text "comment", limit: 65535
|
||||
end
|
||||
|
||||
create_table "hats", force: true do |t|
|
||||
create_table "hats", force: :cascade do |t|
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "user_id"
|
||||
t.integer "granted_by_user_id"
|
||||
t.string "hat"
|
||||
t.string "link"
|
||||
t.integer "user_id", limit: 4
|
||||
t.integer "granted_by_user_id", limit: 4
|
||||
t.string "hat", limit: 255
|
||||
t.string "link", limit: 255
|
||||
end
|
||||
|
||||
create_table "hidden_stories", force: true do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "story_id"
|
||||
create_table "hidden_stories", force: :cascade do |t|
|
||||
t.integer "user_id", limit: 4
|
||||
t.integer "story_id", limit: 4
|
||||
end
|
||||
|
||||
add_index "hidden_stories", ["user_id", "story_id"], name: "index_hidden_stories_on_user_id_and_story_id", unique: true, using: :btree
|
||||
|
||||
create_table "invitation_requests", force: true do |t|
|
||||
t.string "code"
|
||||
t.boolean "is_verified", default: false
|
||||
t.string "email"
|
||||
t.string "name"
|
||||
t.text "memo"
|
||||
t.string "ip_address"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
create_table "invitation_requests", force: :cascade do |t|
|
||||
t.string "code", limit: 255
|
||||
t.boolean "is_verified", default: false
|
||||
t.string "email", limit: 255
|
||||
t.string "name", limit: 255
|
||||
t.text "memo", limit: 65535
|
||||
t.string "ip_address", limit: 255
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "invitations", force: true do |t|
|
||||
t.integer "user_id"
|
||||
t.string "email"
|
||||
t.string "code"
|
||||
create_table "invitations", force: :cascade do |t|
|
||||
t.integer "user_id", limit: 4
|
||||
t.string "email", limit: 255
|
||||
t.string "code", limit: 255
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "memo", limit: 16777215
|
||||
end
|
||||
|
||||
create_table "keystores", id: false, force: true do |t|
|
||||
create_table "keystores", id: false, force: :cascade do |t|
|
||||
t.string "key", limit: 50, default: "", null: false
|
||||
t.integer "value", limit: 8
|
||||
end
|
||||
|
||||
add_index "keystores", ["key"], name: "key", unique: true, using: :btree
|
||||
|
||||
create_table "messages", force: true do |t|
|
||||
create_table "messages", force: :cascade do |t|
|
||||
t.datetime "created_at"
|
||||
t.integer "author_user_id"
|
||||
t.integer "recipient_user_id"
|
||||
t.integer "author_user_id", limit: 4
|
||||
t.integer "recipient_user_id", limit: 4
|
||||
t.boolean "has_been_read", default: false
|
||||
t.string "subject", limit: 100
|
||||
t.text "body", limit: 16777215
|
||||
|
@ -103,39 +105,40 @@ ActiveRecord::Schema.define(version: 20160704022756) do
|
|||
|
||||
add_index "messages", ["short_id"], name: "random_hash", unique: true, using: :btree
|
||||
|
||||
create_table "moderations", force: true do |t|
|
||||
create_table "moderations", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "moderator_user_id"
|
||||
t.integer "story_id"
|
||||
t.integer "comment_id"
|
||||
t.integer "user_id"
|
||||
t.integer "moderator_user_id", limit: 4
|
||||
t.integer "story_id", limit: 4
|
||||
t.integer "comment_id", limit: 4
|
||||
t.integer "user_id", limit: 4
|
||||
t.text "action", limit: 16777215
|
||||
t.text "reason", limit: 16777215
|
||||
t.boolean "is_from_suggestions", default: false
|
||||
end
|
||||
|
||||
create_table "stories", force: true do |t|
|
||||
create_table "stories", force: :cascade do |t|
|
||||
t.datetime "created_at"
|
||||
t.integer "user_id"
|
||||
t.integer "user_id", limit: 4
|
||||
t.string "url", limit: 250, default: ""
|
||||
t.string "title", limit: 150, default: "", null: false
|
||||
t.text "description", limit: 16777215
|
||||
t.string "short_id", limit: 6, default: "", null: false
|
||||
t.boolean "is_expired", default: false, null: false
|
||||
t.integer "upvotes", default: 0, null: false
|
||||
t.integer "downvotes", default: 0, null: false
|
||||
t.integer "upvotes", limit: 4, default: 0, null: false
|
||||
t.integer "downvotes", limit: 4, default: 0, null: false
|
||||
t.boolean "is_moderated", default: false, null: false
|
||||
t.decimal "hotness", precision: 20, scale: 10, default: 0.0, null: false
|
||||
t.text "markeddown_description", limit: 16777215
|
||||
t.text "story_cache", limit: 16777215
|
||||
t.integer "comments_count", default: 0, null: false
|
||||
t.integer "merged_story_id"
|
||||
t.integer "comments_count", limit: 4, default: 0, null: false
|
||||
t.integer "merged_story_id", limit: 4
|
||||
t.datetime "unavailable_at"
|
||||
t.string "twitter_id", limit: 20
|
||||
t.boolean "user_is_author", default: false
|
||||
end
|
||||
|
||||
add_index "stories", ["created_at"], name: "index_stories_on_created_at", using: :btree
|
||||
add_index "stories", ["hotness"], name: "hotness_idx", using: :btree
|
||||
add_index "stories", ["is_expired", "is_moderated"], name: "is_idxes", using: :btree
|
||||
add_index "stories", ["merged_story_id"], name: "index_stories_on_merged_story_id", using: :btree
|
||||
|
@ -143,35 +146,35 @@ ActiveRecord::Schema.define(version: 20160704022756) do
|
|||
add_index "stories", ["twitter_id"], name: "index_stories_on_twitter_id", using: :btree
|
||||
add_index "stories", ["url"], name: "url", length: {"url"=>191}, using: :btree
|
||||
|
||||
create_table "suggested_taggings", force: true do |t|
|
||||
t.integer "story_id"
|
||||
t.integer "tag_id"
|
||||
t.integer "user_id"
|
||||
create_table "suggested_taggings", force: :cascade do |t|
|
||||
t.integer "story_id", limit: 4
|
||||
t.integer "tag_id", limit: 4
|
||||
t.integer "user_id", limit: 4
|
||||
end
|
||||
|
||||
create_table "suggested_titles", force: true do |t|
|
||||
t.integer "story_id"
|
||||
t.integer "user_id"
|
||||
create_table "suggested_titles", force: :cascade do |t|
|
||||
t.integer "story_id", limit: 4
|
||||
t.integer "user_id", limit: 4
|
||||
t.string "title", limit: 150, null: false
|
||||
end
|
||||
|
||||
create_table "tag_filters", force: true do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "user_id"
|
||||
t.integer "tag_id"
|
||||
create_table "tag_filters", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "user_id", limit: 4
|
||||
t.integer "tag_id", limit: 4
|
||||
end
|
||||
|
||||
add_index "tag_filters", ["user_id", "tag_id"], name: "user_tag_idx", using: :btree
|
||||
|
||||
create_table "taggings", force: true do |t|
|
||||
t.integer "story_id", null: false
|
||||
t.integer "tag_id", null: false
|
||||
create_table "taggings", force: :cascade do |t|
|
||||
t.integer "story_id", limit: 4, null: false
|
||||
t.integer "tag_id", limit: 4, null: false
|
||||
end
|
||||
|
||||
add_index "taggings", ["story_id", "tag_id"], name: "story_id_tag_id", unique: true, using: :btree
|
||||
|
||||
create_table "tags", force: true do |t|
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.string "tag", limit: 25, default: "", null: false
|
||||
t.string "description", limit: 100
|
||||
t.boolean "privileged", default: false
|
||||
|
@ -182,39 +185,30 @@ ActiveRecord::Schema.define(version: 20160704022756) do
|
|||
|
||||
add_index "tags", ["tag"], name: "tag", unique: true, using: :btree
|
||||
|
||||
create_table "users", force: true do |t|
|
||||
t.string "username", limit: 50
|
||||
t.string "email", limit: 100
|
||||
t.string "password_digest", limit: 75
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "username", limit: 50
|
||||
t.string "email", limit: 100
|
||||
t.string "password_digest", limit: 75
|
||||
t.datetime "created_at"
|
||||
t.boolean "email_notifications", default: false
|
||||
t.boolean "is_admin", default: false
|
||||
t.string "password_reset_token", limit: 75
|
||||
t.string "session_token", limit: 75, default: "", null: false
|
||||
t.text "about", limit: 16777215
|
||||
t.integer "invited_by_user_id"
|
||||
t.boolean "email_replies", default: false
|
||||
t.boolean "pushover_replies", default: false
|
||||
t.string "pushover_user_key"
|
||||
t.boolean "email_messages", default: true
|
||||
t.boolean "pushover_messages", default: true
|
||||
t.boolean "is_moderator", default: false
|
||||
t.boolean "email_mentions", default: false
|
||||
t.boolean "pushover_mentions", default: false
|
||||
t.string "rss_token", limit: 75
|
||||
t.string "mailing_list_token", limit: 75
|
||||
t.integer "mailing_list_mode", default: 0
|
||||
t.integer "karma", default: 0, null: false
|
||||
t.boolean "is_admin", default: false
|
||||
t.string "password_reset_token", limit: 75
|
||||
t.string "session_token", limit: 75, default: "", null: false
|
||||
t.text "about", limit: 16777215
|
||||
t.integer "invited_by_user_id", limit: 4
|
||||
t.boolean "is_moderator", default: false
|
||||
t.boolean "pushover_mentions", default: false
|
||||
t.string "rss_token", limit: 75
|
||||
t.string "mailing_list_token", limit: 75
|
||||
t.integer "mailing_list_mode", limit: 4, default: 0
|
||||
t.integer "karma", limit: 4, default: 0, null: false
|
||||
t.datetime "banned_at"
|
||||
t.integer "banned_by_user_id"
|
||||
t.string "banned_reason", limit: 200
|
||||
t.integer "banned_by_user_id", limit: 4
|
||||
t.string "banned_reason", limit: 200
|
||||
t.datetime "deleted_at"
|
||||
t.boolean "show_avatars", default: false
|
||||
t.boolean "show_story_previews", default: false
|
||||
t.boolean "show_submitted_story_threads", default: true
|
||||
t.datetime "disabled_invite_at"
|
||||
t.integer "disabled_invite_by_user_id"
|
||||
t.string "disabled_invite_reason", limit: 200
|
||||
t.integer "disabled_invite_by_user_id", limit: 4
|
||||
t.string "disabled_invite_reason", limit: 200
|
||||
t.text "settings", limit: 65535
|
||||
end
|
||||
|
||||
add_index "users", ["mailing_list_mode"], name: "mailing_list_enabled", using: :btree
|
||||
|
@ -224,14 +218,15 @@ ActiveRecord::Schema.define(version: 20160704022756) do
|
|||
add_index "users", ["session_token"], name: "session_hash", unique: true, using: :btree
|
||||
add_index "users", ["username"], name: "username", unique: true, using: :btree
|
||||
|
||||
create_table "votes", force: true do |t|
|
||||
t.integer "user_id", null: false
|
||||
t.integer "story_id", null: false
|
||||
t.integer "comment_id"
|
||||
create_table "votes", force: :cascade do |t|
|
||||
t.integer "user_id", limit: 4, null: false
|
||||
t.integer "story_id", limit: 4, null: false
|
||||
t.integer "comment_id", limit: 4
|
||||
t.integer "vote", limit: 1, null: false
|
||||
t.string "reason", limit: 1
|
||||
end
|
||||
|
||||
add_index "votes", ["comment_id"], name: "index_votes_on_comment_id", using: :btree
|
||||
add_index "votes", ["user_id", "comment_id"], name: "user_id_comment_id", using: :btree
|
||||
add_index "votes", ["user_id", "story_id"], name: "user_id_story_id", using: :btree
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
class Github
|
||||
cattr_accessor :CLIENT_ID, :CLIENT_SECRET
|
||||
|
||||
# these need to be overridden in config/initializers/production.rb
|
||||
@@CLIENT_ID = nil
|
||||
@@CLIENT_SECRET = nil
|
||||
|
||||
def self.enabled?
|
||||
self.CLIENT_ID.present?
|
||||
end
|
||||
|
||||
def self.oauth_consumer
|
||||
OAuth::Consumer.new(self.CLIENT_ID, self.CLIENT_SECRET,
|
||||
{ :site => "https://api.github.com" })
|
||||
end
|
||||
|
||||
def self.token_and_user_from_code(code)
|
||||
s = Sponge.new
|
||||
res = s.fetch("https://github.com/login/oauth/access_token", :post,
|
||||
{ :client_id => self.CLIENT_ID, :client_secret => self.CLIENT_SECRET,
|
||||
:code => code })
|
||||
ps = CGI.parse(res)
|
||||
tok = ps["access_token"].first
|
||||
|
||||
if tok.present?
|
||||
res = s.fetch("https://api.github.com/user?access_token=#{tok}")
|
||||
js = JSON.parse(res)
|
||||
if js && js["login"].present?
|
||||
return [ tok, js["login"] ]
|
||||
end
|
||||
end
|
||||
|
||||
return [ nil, nil ]
|
||||
end
|
||||
|
||||
def self.oauth_auth_url(state)
|
||||
"https://github.com/login/oauth/authorize?client_id=#{self.CLIENT_ID}&" <<
|
||||
"state=#{state}"
|
||||
end
|
||||
end
|
|
@ -1,55 +1,74 @@
|
|||
class Markdowner
|
||||
# opts[:allow_images] allows <img> tags
|
||||
# opts[:disable_profile_links] disables @username -> /u/username links
|
||||
|
||||
def self.to_html(text, opts = {})
|
||||
if text.blank?
|
||||
return ""
|
||||
end
|
||||
|
||||
args = [ :smart, :autolink, :safelink, :filter_styles, :filter_html,
|
||||
:strict ]
|
||||
if !opts[:allow_images]
|
||||
args.push :no_image
|
||||
end
|
||||
exts = [:tagfilter, :autolink, :strikethrough]
|
||||
root = CommonMarker.render_doc(text.to_s, [:SMART], exts)
|
||||
|
||||
ng = Nokogiri::HTML(RDiscount.new(text.to_s, *args).to_html)
|
||||
walk_text_nodes(root){|n| postprocess_text_node(n) }
|
||||
|
||||
ng = Nokogiri::HTML(root.to_html([:SAFE], exts))
|
||||
|
||||
# change <h1>, <h2>, etc. headings to just bold tags
|
||||
ng.css("h1, h2, h3, h4, h5, h6").each do |h|
|
||||
h.name = "strong"
|
||||
end
|
||||
|
||||
if !opts[:allow_images]
|
||||
ng.css("img").remove
|
||||
end
|
||||
|
||||
# make links have rel=nofollow
|
||||
ng.css("a").each do |h|
|
||||
h[:rel] = "nofollow"
|
||||
h[:rel] = "nofollow" unless (URI.parse(h[:href]).host.nil? rescue false)
|
||||
end
|
||||
|
||||
# XXX: t.replace(tx) unescapes HTML, so disable for now. this probably
|
||||
# needs to split text into separate nodes and then replace the @username
|
||||
# with a proper 'a' node
|
||||
if false
|
||||
unless opts[:disable_profile_links]
|
||||
# make @username link to that user's profile
|
||||
ng.search("//text()").each do |t|
|
||||
if t.parent && t.parent.name.downcase == "a"
|
||||
# don't replace inside <a>s
|
||||
next
|
||||
end
|
||||
|
||||
tx = t.text.gsub(/\B\@([\w\-]+)/) do |u|
|
||||
if User.exists?(:username => u[1 .. -1])
|
||||
"<a href=\"/u/#{u[1 .. -1]}\">#{u}</a>"
|
||||
else
|
||||
u
|
||||
end
|
||||
end
|
||||
|
||||
t.replace(tx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ng.at_css("body").inner_html
|
||||
end
|
||||
|
||||
def self.walk_text_nodes(node, &block)
|
||||
return if node.type == :link
|
||||
return block.call(node) if node.type == :text
|
||||
|
||||
node.each do |child|
|
||||
walk_text_nodes(child, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def self.postprocess_text_node(node)
|
||||
while node
|
||||
return unless node.string_content =~ /\B(@#{User::VALID_USERNAME})/
|
||||
before, user, after = $`, $1, $'
|
||||
|
||||
node.string_content = before
|
||||
|
||||
if User.exists?(:username => user[1..-1])
|
||||
link = CommonMarker::Node.new(:link)
|
||||
link.url = "/u/#{user[1..-1]}"
|
||||
node.insert_after(link)
|
||||
|
||||
link_text = CommonMarker::Node.new(:text)
|
||||
link_text.string_content = user
|
||||
link.append_child(link_text)
|
||||
|
||||
node = link
|
||||
else
|
||||
node.string_content += user
|
||||
end
|
||||
|
||||
if after.length > 0
|
||||
remainder = CommonMarker::Node.new(:text)
|
||||
remainder.string_content = after
|
||||
node.insert_after(remainder)
|
||||
|
||||
node = remainder
|
||||
else
|
||||
node = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,12 @@ class Pushover
|
|||
cattr_accessor :API_TOKEN
|
||||
cattr_accessor :SUBSCRIPTION_CODE
|
||||
|
||||
def self.enabled?
|
||||
self.API_TOKEN.present?
|
||||
end
|
||||
|
||||
def self.push(user, params)
|
||||
if !@@API_TOKEN
|
||||
if !self.enabled?
|
||||
return
|
||||
end
|
||||
|
||||
|
@ -15,7 +19,7 @@ class Pushover
|
|||
|
||||
s = Sponge.new
|
||||
s.fetch("https://api.pushover.net/1/messages.json", :post, {
|
||||
:token => @@API_TOKEN,
|
||||
:token => self.API_TOKEN,
|
||||
:user => user,
|
||||
}.merge(params))
|
||||
rescue => e
|
||||
|
@ -24,7 +28,7 @@ class Pushover
|
|||
end
|
||||
|
||||
def self.subscription_url(params)
|
||||
u = "https://pushover.net/subscribe/#{@@SUBSCRIPTION_CODE}"
|
||||
u = "https://pushover.net/subscribe/#{self.SUBSCRIPTION_CODE}"
|
||||
u << "?success=#{CGI.escape(params[:success])}"
|
||||
u << "&failure=#{CGI.escape(params[:failure])}"
|
||||
u
|
||||
|
|
|
@ -6,18 +6,18 @@ class StoryCacher
|
|||
|
||||
DIFFBOT_API_URL = "http://www.diffbot.com/api/article"
|
||||
|
||||
def self.get_story_text(url)
|
||||
def self.get_story_text(story)
|
||||
if !@@DIFFBOT_API_KEY
|
||||
return
|
||||
end
|
||||
|
||||
# XXX: diffbot tries to read pdfs as text, so disable for now
|
||||
if url.to_s.match(/\.pdf$/i)
|
||||
if story.url.to_s.match(/\.pdf$/i)
|
||||
return nil
|
||||
end
|
||||
|
||||
db_url = "#{DIFFBOT_API_URL}?token=#{@@DIFFBOT_API_KEY}&url=" <<
|
||||
CGI.escape(url)
|
||||
CGI.escape(story.url)
|
||||
|
||||
begin
|
||||
s = Sponge.new
|
||||
|
@ -44,7 +44,7 @@ class StoryCacher
|
|||
begin
|
||||
s = Sponge.new
|
||||
s.timeout = 45
|
||||
s.fetch("https://archive.is/#{db_url}")
|
||||
s.fetch(story.archive_url)
|
||||
rescue => e
|
||||
Rails.logger.error "error caching #{db_url}: #{e.message}"
|
||||
end
|
||||
|
|
|
@ -4,6 +4,9 @@ class Twitter
|
|||
# these need to be overridden in config/initializers/production.rb
|
||||
@@CONSUMER_KEY = nil
|
||||
@@CONSUMER_SECRET = nil
|
||||
|
||||
# these are set for the account used to post updates in
|
||||
# script/post_to_twitter (needs read/write access)
|
||||
@@AUTH_TOKEN = nil
|
||||
@@AUTH_SECRET = nil
|
||||
|
||||
|
@ -12,6 +15,10 @@ class Twitter
|
|||
# https://t.co/eyW1U2HLtP
|
||||
TCO_LEN = 23
|
||||
|
||||
def self.enabled?
|
||||
self.CONSUMER_KEY.present?
|
||||
end
|
||||
|
||||
def self.oauth_consumer
|
||||
OAuth::Consumer.new(self.CONSUMER_KEY, self.CONSUMER_SECRET,
|
||||
{ :site => "https://api.twitter.com" })
|
||||
|
@ -47,4 +54,28 @@ class Twitter
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.token_secret_and_user_from_token_and_verifier(tok, verifier)
|
||||
rt = OAuth::RequestToken.from_hash(self.oauth_consumer,
|
||||
{ :oauth_token => tok })
|
||||
at = rt.get_access_token({ :oauth_verifier => verifier })
|
||||
|
||||
res = at.get("/1.1/account/verify_credentials.json")
|
||||
js = JSON.parse(res.body)
|
||||
|
||||
if !js["screen_name"].present?
|
||||
return nil
|
||||
end
|
||||
|
||||
[ at.token, at.secret, js["screen_name"] ]
|
||||
end
|
||||
|
||||
def self.oauth_request_token(state)
|
||||
self.oauth_consumer.get_request_token(:oauth_callback =>
|
||||
Rails.application.root_url + "settings/twitter_callback?state=#{state}")
|
||||
end
|
||||
|
||||
def self.oauth_auth_url(state)
|
||||
self.oauth_request_token(state).authorize_url
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>404</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>gone fishin'</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1,19 +1,2 @@
|
|||
# block all spiders by default
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
|
||||
# but allow major ones
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
|
||||
User-agent: Slurp
|
||||
Allow: /
|
||||
|
||||
User-Agent: msnbot
|
||||
Disallow:
|
||||
|
||||
User-agent: Baiduspider
|
||||
Disallow: /
|
||||
|
||||
User-agent: *
|
||||
Disallow: /search
|
||||
|
|
|
@ -140,7 +140,7 @@ last_comment_id = (Keystore.value_for(LAST_COMMENT_KEY) ||
|
|||
Comment.where("id > ? AND (is_deleted = ? AND is_moderated = ?)",
|
||||
last_comment_id, false, false).order(:id).each do |c|
|
||||
# allow some time for newer comments to be edited before sending them out
|
||||
if (Time.now - c.created_at) < 2.minutes
|
||||
if (Time.now - (c.updated_at || c.created_at)) < 2.minutes
|
||||
break
|
||||
end
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ require File.expand_path('../../config/boot', __FILE__)
|
|||
require APP_PATH
|
||||
Rails.application.require_environment!
|
||||
|
||||
MIN_STORY_SCORE = 2
|
||||
MIN_STORY_SCORE = 3
|
||||
|
||||
Story.where("is_expired = ? AND #{Story.score_sql} > ? AND " <<
|
||||
Story.where("is_expired = ? AND #{Story.score_sql} >= ? AND " <<
|
||||
"twitter_id IS NULL AND created_at >= ?", false, MIN_STORY_SCORE,
|
||||
Time.now - 1.days).order(:id).each_with_index do |s,x|
|
||||
if s.tags.map(&:tag).include?("meta")
|
||||
|
@ -25,12 +25,18 @@ Time.now - 1.days).order(:id).each_with_index do |s,x|
|
|||
tags += ' #' + tagging.tag.tag
|
||||
end
|
||||
|
||||
tco_status = "\n" +
|
||||
via = ""
|
||||
if s.user.twitter_username.present?
|
||||
via = "\n" + (s.user_is_author? ? "by" : "via") +
|
||||
" @#{s.user.twitter_username}"
|
||||
end
|
||||
|
||||
tco_status = via + "\n" +
|
||||
(s.url.present?? ("X" * Twitter::TCO_LEN) + "\n" : "") +
|
||||
("X" * Twitter::TCO_LEN) +
|
||||
tags
|
||||
|
||||
status = "\n" +
|
||||
status = via + "\n" +
|
||||
(s.url.present?? s.url + "\n" : "") +
|
||||
s.short_id_url +
|
||||
tags
|
||||
|
|
|
@ -6,6 +6,16 @@ describe Markdowner do
|
|||
"<p>hello there <em>italics</em> and <strong>bold</strong>!</p>"
|
||||
end
|
||||
|
||||
it "turns @username into a link if @username exists" do
|
||||
User.make!(:username => "blahblah")
|
||||
|
||||
Markdowner.to_html("hi @blahblah test").should ==
|
||||
"<p>hi <a href=\"/u/blahblah\">@blahblah</a> test</p>"
|
||||
|
||||
Markdowner.to_html("hi @flimflam test").should ==
|
||||
"<p>hi @flimflam test</p>"
|
||||
end
|
||||
|
||||
# bug#209
|
||||
it "keeps punctuation inside of auto-generated links when using brackets" do
|
||||
Markdowner.to_html("hi <http://example.com/a.> test").should ==
|
||||
|
@ -25,4 +35,17 @@ describe Markdowner do
|
|||
"<p>hi <a href=\"http://example.com/@blahblah/\" rel=\"nofollow\">" <<
|
||||
"test</a></p>"
|
||||
end
|
||||
|
||||
it "correctly adds nofollow" do
|
||||
Markdowner.to_html("[ex](http://example.com)").should ==
|
||||
"<p><a href=\"http://example.com\" rel=\"nofollow\">" <<
|
||||
"ex</a></p>"
|
||||
|
||||
Markdowner.to_html("[ex](//example.com)").should ==
|
||||
"<p><a href=\"//example.com\" rel=\"nofollow\">" <<
|
||||
"ex</a></p>"
|
||||
|
||||
Markdowner.to_html("[ex](/u/abc)").should ==
|
||||
"<p><a href=\"/u/abc\">ex</a></p>"
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue