Merge pull request #935 from Retrospring/feature/long-questions

Add option to allow long questions
This commit is contained in:
Karina Kwiatek 2023-01-11 22:25:08 +01:00 committed by GitHub
commit 4692753485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 113 additions and 37 deletions

View File

@ -6,7 +6,7 @@ class Settings::ProfileController < ApplicationController
def edit; end
def update
profile_attributes = params.require(:profile).permit(:display_name, :motivation_header, :website, :location, :description, :anon_display_name)
profile_attributes = params.require(:profile).permit(:display_name, :motivation_header, :website, :location, :description, :anon_display_name, :allow_long_questions)
if current_user.profile.update(profile_attributes)
flash[:success] = t(".success")

View File

@ -0,0 +1,30 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['input', 'warning'];
declare readonly inputTarget: HTMLInputElement;
declare readonly warningTarget: HTMLElement;
static values = {
warn: Number
};
declare readonly warnValue: number;
connect(): void {
this.inputTarget.addEventListener('input', this.update.bind(this));
}
update(): void {
if (this.inputTarget.value.length > this.warnValue) {
if (this.warningTarget.classList.contains('d-none')) {
this.warningTarget.classList.remove('d-none');
}
} else {
if (!this.warningTarget.classList.contains('d-none')) {
this.warningTarget.classList.add('d-none');
}
}
}
}

View File

@ -24,10 +24,6 @@ export function questionboxAllHandler(event: Event): void {
document.querySelector<HTMLInputElement>('textarea[name=qb-all-question]').value = '';
const modal = Modal.getInstance(document.querySelector('#modal-ask-followers'));
modal.hide();
// FIXME: also solve this using a Stimulus controller
const characterCount = document.querySelector<HTMLElement>('#modal-ask-followers [data-character-count-max-value]').dataset.characterCountMaxValue;
document.querySelector<HTMLElement>('#modal-ask-followers [data-character-count-target="counter"]').innerHTML = characterCount;
}
showNotification(data.message, data.success);

View File

@ -30,8 +30,10 @@ export function questionboxUserHandler(event: Event): void {
document.querySelector<HTMLInputElement>('textarea[name=qb-question]').value = '';
// FIXME: also solve this using a Stimulus controller
const characterCount = document.querySelector<HTMLElement>('#question-box[data-character-count-max-value]').dataset.characterCountMaxValue;
document.querySelector<HTMLElement>('#question-box [data-character-count-target="counter"]').innerHTML = characterCount;
const questionBox = document.getElementById('question-box');
if ('characterCountMaxValue' in questionBox.dataset) {
questionBox.querySelector<HTMLElement>('[data-character-count-target="counter"]').innerHTML = questionBox.dataset.characterCountMaxValue;
}
if (promote) {
const questionbox = document.querySelector('#question-box');

View File

@ -2,6 +2,7 @@ import { Application } from "@hotwired/stimulus";
import AnnouncementController from "retrospring/controllers/announcement_controller";
import AutofocusController from "retrospring/controllers/autofocus_controller";
import CharacterCountController from "retrospring/controllers/character_count_controller";
import CharacterCountWarningController from "retrospring/controllers/character_count_warning_controller";
import FormatPopupController from "retrospring/controllers/format_popup_controller";
/**
@ -16,5 +17,6 @@ export default function (): void {
window['Stimulus'].register('announcement', AnnouncementController);
window['Stimulus'].register('autofocus', AutofocusController);
window['Stimulus'].register('character-count', CharacterCountController);
window['Stimulus'].register('character-count-warning', CharacterCountWarningController);
window['Stimulus'].register('format-popup', FormatPopupController);
}

View File

@ -26,4 +26,6 @@ class Profile < ApplicationRecord
def safe_name
display_name.presence || user.screen_name
end
def question_length_limit = allow_long_questions ? nil : Question::SHORT_QUESTION_MAX_LENGTH
end

View File

@ -5,7 +5,9 @@ class Question < ApplicationRecord
has_many :answers, dependent: :destroy
has_many :inboxes, dependent: :destroy
validates :content, length: { minimum: 1, maximum: 512 }
validates :content, length: { minimum: 1 }
SHORT_QUESTION_MAX_LENGTH = 512
before_destroy do
rep = Report.where(target_id: self.id, type: 'Reports::Question')
@ -30,4 +32,6 @@ class Question < ApplicationRecord
def generated? = %w[justask retrospring_exporter].include?(author_identifier)
def anonymous? = author_is_anonymous && author_identifier.present?
def long? = content.length > SHORT_QUESTION_MAX_LENGTH
end

View File

@ -22,7 +22,7 @@
%strong= t(".status.require_user_html", sign_in: link_to(t("voc.login"), new_user_session_path), sign_up: link_to(t("voc.register"), new_user_registration_path))
- else
- if user_signed_in? || user.privacy_allow_anonymous_questions?
#question-box{ data: { controller: "character-count", "character-count-max-value": 512 }}
#question-box{ data: user.profile.allow_long_questions ? {} : { controller: "character-count", "character-count-max-value": user.profile.question_length_limit }}
%textarea.form-control{ name: "qb-question", placeholder: t(".placeholder"), data: { "character-count-target": "input" } }
.row{ style: "padding-top: 5px;" }
.col-6
@ -36,7 +36,7 @@
%input{ name: "qb-anonymous", type: :hidden, value: false }/
.col-6
%p.pull-right
%span.text-muted.me-1{ data: { "character-count-target": "counter" } } 512
%span.text-muted.me-1{ class: user.profile.allow_long_questions ? "d-none" : "", data: { "character-count-target": "counter" } }= Question::SHORT_QUESTION_MAX_LENGTH
%input{ name: "qb-to", type: "hidden", value: user.id }/
%button.btn.btn-primary{ name: "qb-ask",
type: :button,

View File

@ -1,14 +1,14 @@
.modal.fade#modal-ask-followers{ aria: { hidden: true, labelledby: "modal-ask-followers-label" }, role: :dialog, tabindex: -1 }
.modal-dialog
.modal-content{ data: { controller: "character-count", "character-count-max-value": 512 }}
.modal-content{ data: { controller: "character-count-warning", "character-count-warning-warn-value": Question::SHORT_QUESTION_MAX_LENGTH }}
.modal-header
%h5.modal-title#modal-ask-followers-label= t(".title")
%button.btn-close{ data: { bs_dismiss: :modal }, type: :button }
%span.visually-hidden= t("voc.close")
.modal-body
.form-group.has-feedback
%textarea.form-control{ name: "qb-all-question", placeholder: t(".placeholder"), data: { "character-count-target": "input" } }
%p.text-end.text-muted.form-control-feedback{ data: { "character-count-target": "counter" } } 512
%textarea.form-control{ name: "qb-all-question", placeholder: t(".placeholder"), data: { "character-count-warning-target": "input" } }
.alert.alert-warning.mt-3.d-none{ data: { "character-count-warning-target": "warning" } }= t('.long_question_warning')
.modal-footer
%button.btn.btn-default{ type: :button, data: { bs_dismiss: :modal } }= t("voc.cancel")
%button.btn.btn-primary{ name: "qb-all-ask", type: :button, data: { "character-count-target": "action", loading_text: t(".loading") } }= t(".action")
%button.btn.btn-primary{ name: "qb-all-ask", type: :button, data: { loading_text: t(".loading") } }= t(".action")

View File

@ -49,6 +49,8 @@
= f.text_area :description
= f.check_box :allow_long_questions
= f.primary
- provide(:title, generate_title(t(".title")))

View File

@ -30,6 +30,7 @@ class QuestionWorker
return true if follower.banned?
return true if muted?(follower, question)
return true if user.muting?(question.user)
return true if question.long? && !follower.profile.allow_long_questions
false
end

View File

@ -254,6 +254,7 @@ en:
placeholder: "Type your question here…"
action: "Ask"
loading: "Asking…"
long_question_warning: "This question will only be sent to those who allow long questions in their profile settings."
comment_smiles:
title: "People who smiled this comment"
none: "No one has smiled this comment yet."

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAllowLongQuestionsToProfiles < ActiveRecord::Migration[6.1]
def change
add_column :profiles, :allow_long_questions, :boolean, default: false
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_12_27_065923) do
ActiveRecord::Schema.define(version: 2023_01_08_114333) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -134,6 +134,7 @@ ActiveRecord::Schema.define(version: 2022_12_27_065923) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "anon_display_name"
t.boolean "allow_long_questions", default: false
t.index ["user_id"], name: "index_profiles_on_user_id"
end

View File

@ -28,6 +28,9 @@ module Errors
class InvalidBanDuration < BadRequest
end
class QuestionTooLong < BadRequest
end
class Forbidden < Base
def status
403

View File

@ -63,6 +63,7 @@ module UseCase
def check_user
raise Errors::NotAuthorized if target_user.privacy_require_user && !source_user_id
raise Errors::QuestionTooLong if content.length > ::Question::SHORT_QUESTION_MAX_LENGTH && !target_user.profile.allow_long_questions
end
def create_question

View File

@ -27,11 +27,12 @@ describe UseCase::DataExport::User, :data_export do
sign_in_count: 10,
smiled_count: 28,
profile: {
display_name: "Fizzy Raccoon",
description: "A small raccoon",
location: "Binland",
motivation_header: "",
website: "https://retrospring.net"
display_name: "Fizzy Raccoon",
description: "A small raccoon",
location: "Binland",
motivation_header: "",
website: "https://retrospring.net",
allow_long_questions: true
}
}
end
@ -87,14 +88,15 @@ describe UseCase::DataExport::User, :data_export do
privacy_noindex: false
},
profile: {
display_name: "Fizzy Raccoon",
description: "A small raccoon",
location: "Binland",
website: "https://retrospring.net",
motivation_header: "",
created_at: user.profile.created_at.as_json,
updated_at: user.profile.updated_at.as_json,
anon_display_name: nil
display_name: "Fizzy Raccoon",
description: "A small raccoon",
location: "Binland",
website: "https://retrospring.net",
motivation_header: "",
created_at: user.profile.created_at.as_json,
updated_at: user.profile.updated_at.as_json,
anon_display_name: nil,
allow_long_questions: true
},
roles: {
administrator: false,

View File

@ -57,11 +57,22 @@ describe UseCase::Question::Create do
end
end
context "content is too long" do
context "content is over 512 characters long" do
let(:content) { "a" * 513 }
it "raises an error" do
expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
context "recipient does not allow long questions" do
it "raises an error" do
expect { subject }.to raise_error(Errors::QuestionTooLong)
end
end
context "recipient allows long questions" do
before do
target_user.profile.allow_long_questions = true
target_user.profile.save
end
it_behaves_like "creates the question"
end
end
end

View File

@ -16,11 +16,6 @@ RSpec.describe Question, :type => :model do
expect(@question.content).to match 'Is this a question?'
end
it 'does not save questions longer than 512 characters' do
@question.content = 'X' * 513
expect{@question.save!}.to raise_error(ActiveRecord::RecordInvalid)
end
it 'has many answers' do
5.times { |i| Answer.create(content: "This is an answer. #{i}", user: FactoryBot.create(:user), question: @question) }
expect(@question.answer_count).to match 5

View File

@ -6,7 +6,8 @@ describe QuestionWorker do
describe "#perform" do
let(:user) { FactoryBot.create(:user) }
let(:user_id) { user.id }
let(:question) { FactoryBot.create(:question, user:) }
let(:content) { Faker::Lorem.sentence }
let(:question) { FactoryBot.create(:question, content:, user:) }
let(:question_id) { question.id }
before do
@ -92,5 +93,20 @@ describe QuestionWorker do
)
end
end
context "long question" do
let(:content) { "x" * 1000 }
it "sends to recipients who allow long questions" do
user.followers.first.profile.update(allow_long_questions: true)
expect { subject }
.to(
change { Inbox.where(user_id: user.followers.ids, question_id:, new: true).count }
.from(0)
.to(1)
)
end
end
end
end