diff --git a/app/assets/javascripts/feedback.js b/app/assets/javascripts/feedback.js
new file mode 100644
index 000000000..7d9ab3772
--- /dev/null
+++ b/app/assets/javascripts/feedback.js
@@ -0,0 +1,74 @@
+$(document).on("turbolinks:load", () => {
+ if (!shouldRegisterFeedback()) {
+ return;
+ }
+ registerToasts();
+ registerSubmitButtonHandler();
+ registerFeedbackBodyValidator();
+});
+
+var SUBMIT_FEEDBACK_ID = "#submit-feedback";
+
+var TOAST_OPTIONS = {
+ animation: true,
+ autohide: true,
+ delay: 6000, // autohide after ... milliseconds
+};
+
+function shouldRegisterFeedback() {
+ return $(SUBMIT_FEEDBACK_ID).length > 0;
+}
+
+function registerToasts() {
+ const toastElements = document.querySelectorAll(".toast");
+ [...toastElements].map((toast) => {
+ new bootstrap.Toast(toast, TOAST_OPTIONS);
+ });
+}
+
+function registerSubmitButtonHandler() {
+ // Invoke the hidden submit button inside the actual Rails form
+ $("#submit-feedback-form-btn-outside").click(() => {
+ submitFeedback();
+ });
+
+ // Submit form by pressing Ctrl + Enter
+ document.addEventListener("keydown", (event) => {
+ const isModalOpen = $(SUBMIT_FEEDBACK_ID).is(":visible");
+ if (isModalOpen && event.ctrlKey && event.key == "Enter") {
+ submitFeedback();
+ }
+ });
+}
+
+function registerFeedbackBodyValidator() {
+ const feedbackBody = document.getElementById("feedback_feedback");
+ feedbackBody.addEventListener("input", () => {
+ validateFeedback();
+ });
+}
+
+function validateFeedback() {
+ const feedbackBody = document.getElementById("feedback_feedback");
+ const validityState = feedbackBody.validity;
+ if (validityState.tooShort) {
+ const tooShortMessage = feedbackBody.dataset.tooShortMessage;
+ feedbackBody.setCustomValidity(tooShortMessage);
+ }
+ else if (validityState.valueMissing) {
+ const valueMissingMessage = feedbackBody.dataset.valueMissingMessage;
+ feedbackBody.setCustomValidity(valueMissingMessage);
+ }
+ else {
+ // render input valid, so that form will submit
+ feedbackBody.setCustomValidity("");
+ }
+
+ feedbackBody.reportValidity();
+}
+
+function submitFeedback() {
+ const submitButton = $("#submit-feedback-form-btn");
+ validateFeedback();
+ submitButton.click();
+}
diff --git a/app/assets/javascripts/lectures.coffee b/app/assets/javascripts/lectures.coffee
index 15dfacca0..5e47d4fea 100644
--- a/app/assets/javascripts/lectures.coffee
+++ b/app/assets/javascripts/lectures.coffee
@@ -214,6 +214,7 @@ $(document).on 'turbolinks:load', ->
$('#secondnav').show()
$('#lecturesDropdown').appendTo($('#secondnav'))
$('#notificationDropdown').appendTo($('#secondnav'))
+ $('#feedback-btn').appendTo($('#secondnav'))
$('#searchField').appendTo($('#secondnav'))
$('#second-admin-nav').show()
$('#adminDetails').appendTo($('#second-admin-nav'))
@@ -236,6 +237,7 @@ $(document).on 'turbolinks:load', ->
$('#secondnav').hide()
$('#lecturesDropdown').appendTo($('#firstnav'))
$('#notificationDropdown').appendTo($('#firstnav'))
+ $('#feedback-btn').appendTo($('#firstnav'))
$('#searchField').appendTo($('#firstnav'))
$('#second-admin-nav').hide()
$('#teachableDrop').appendTo($('#first-admin-nav'))
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 35f8c0d78..4fed1ddea 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -302,4 +302,8 @@ a {
&:hover {
text-decoration: underline;
}
+}
+
+.toast {
+ background-color: white;
}
\ No newline at end of file
diff --git a/app/assets/stylesheets/feedback.scss b/app/assets/stylesheets/feedback.scss
new file mode 100644
index 000000000..7f14ad18c
--- /dev/null
+++ b/app/assets/stylesheets/feedback.scss
@@ -0,0 +1,11 @@
+#feedback-btn {
+ color: white;
+
+ &:focus {
+ box-shadow: none;
+ }
+
+ &:hover {
+ color: #ffc107;
+ }
+}
\ No newline at end of file
diff --git a/app/controllers/erdbeere_controller.rb b/app/controllers/erdbeere_controller.rb
index 23c78601e..f47ec1daa 100644
--- a/app/controllers/erdbeere_controller.rb
+++ b/app/controllers/erdbeere_controller.rb
@@ -8,7 +8,7 @@ def current_ability
end
def show_example
- response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/examples/#{params[:id]}")
+ response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/examples/#{params[:id]}")
@content = if response.status == 200
JSON.parse(response.body)["embedded_html"]
else
@@ -17,7 +17,7 @@ def show_example
end
def show_property
- response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/properties/#{params[:id]}")
+ response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/properties/#{params[:id]}")
@content = if response.status == 200
JSON.parse(response.body)["embedded_html"]
@@ -28,7 +28,7 @@ def show_property
def show_structure
params[:id]
- response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) + "/structures/#{params[:id]}")
+ response = Faraday.get(ENV.fetch("ERDBEERE_API") + "/structures/#{params[:id]}")
@content = if response.status == 200
JSON.parse(response.body)["embedded_html"]
else
@@ -51,7 +51,7 @@ def cancel_edit_tags
def display_info
@id = params[:id]
@sort = params[:sort]
- response = Faraday.get(ENV.fetch("ERDBEERE_API", nil) +
+ response = Faraday.get(ENV.fetch("ERDBEERE_API") +
"/#{@sort.downcase.pluralize}/#{@id}/view_info")
@content = JSON.parse(response.body)
if response.status != 200
@@ -87,7 +87,7 @@ def update_tags
end
def fill_realizations_select
- response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures/")
+ response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/structures/")
@tag = Tag.find_by(id: params[:id])
hash = JSON.parse(response.body)
@structures = hash["data"].map do |d|
@@ -102,7 +102,7 @@ def fill_realizations_select
end
def find_example
- response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/find?#{find_params.to_query}")
+ response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/find?#{find_params.to_query}")
@content = if response.status == 200
JSON.parse(response.body)["embedded_html"]
else
diff --git a/app/controllers/feedbacks_controller.rb b/app/controllers/feedbacks_controller.rb
new file mode 100644
index 000000000..9ae31a957
--- /dev/null
+++ b/app/controllers/feedbacks_controller.rb
@@ -0,0 +1,21 @@
+class FeedbacksController < ApplicationController
+ authorize_resource except: [:create]
+
+ def create
+ feedback = Feedback.new(feedback_params)
+ feedback.user_id = current_user.id
+ @feedback_success = feedback.save
+
+ if @feedback_success
+ FeedbackMailer.with(feedback: feedback).new_user_feedback_email.deliver_later
+ end
+
+ respond_to(&:js)
+ end
+
+ private
+
+ def feedback_params
+ params.require(:feedback).permit(:title, :feedback, :can_contact)
+ end
+end
diff --git a/app/controllers/lectures_controller.rb b/app/controllers/lectures_controller.rb
index d108fbbc0..a7196ea79 100644
--- a/app/controllers/lectures_controller.rb
+++ b/app/controllers/lectures_controller.rb
@@ -217,7 +217,7 @@ def edit_structures
def search_examples
if @lecture.structure_ids.any?
- response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/search")
+ response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/search")
@form = JSON.parse(response.body)["embedded_html"]
# rubocop:disable Style/StringConcatenation
@form.gsub!("token_placeholder",
@@ -402,7 +402,7 @@ def eager_load_stuff
def set_erdbeere_data
@structure_ids = @lecture.structure_ids
- response = Faraday.get("#{ENV.fetch("ERDBEERE_API", nil)}/structures")
+ response = Faraday.get("#{ENV.fetch("ERDBEERE_API")}/structures")
response_hash = if response.status == 200
JSON.parse(response.body)
else
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index bc9152b78..939f2d56b 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -9,12 +9,12 @@ def verify_captcha
return true unless ENV["USE_CAPTCHA_SERVICE"]
begin
- uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL", nil))
+ uri = URI.parse(ENV.fetch("CAPTCHA_VERIFY_URL"))
data = { message: params["frc-captcha-solution"],
- application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN", nil) }
+ application_token: ENV.fetch("CAPTCHA_APPLICATION_TOKEN") }
header = { "Content-Type": "text/json" }
http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true if ENV["CAPTCHA_VERIFY_URL"].include?("https")
+ http.use_ssl = true if ENV.fetch("CAPTCHA_VERIFY_URL").include?("https")
request = Net::HTTP::Post.new(uri.request_uri, header)
request.body = data.to_json
@@ -70,9 +70,9 @@ def after_sign_up_path_for(_resource)
private
def check_registration_limit
- timeframe = ((ENV["MAMPF_REGISTRATION_TIMEFRAME"] || 15).to_i.minutes.ago..)
+ timeframe = (ENV.fetch("MAMPF_REGISTRATION_TIMEFRAME", 15).to_i.minutes.ago..)
num_new_registrations = User.where(confirmed_at: nil, created_at: timeframe).count
- max_registrations = (ENV["MAMPF_MAX_REGISTRATION_PER_TIMEFRAME"] || 40).to_i
+ max_registrations = ENV.fetch("MAMPF_MAX_REGISTRATION_PER_TIMEFRAME", 40).to_i
return if num_new_registrations <= max_registrations
# Current number of new registrations is too high
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 50f6b333b..f267191e3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -16,7 +16,7 @@ def current_lecture
def host
if Rails.env.production?
# rubocop:disable Style/StringConcatenation
- ENV.fetch("MEDIA_SERVER", nil) + "/" + ENV.fetch("INSTANCE_NAME", nil)
+ ENV.fetch("MEDIA_SERVER") + "/" + ENV.fetch("INSTANCE_NAME")
# rubocop:enable Style/StringConcatenation
else
""
@@ -29,7 +29,7 @@ def host
# the actual media server.
# This is used for the download buttons for videos and manuscripts.
def download_host
- Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION", nil) : ""
+ Rails.env.production? ? ENV.fetch("DOWNLOAD_LOCATION") : ""
end
# Returns the full title on a per-page basis.
diff --git a/app/mailers/exception_handler/exception_mailer.rb b/app/mailers/exception_handler/exception_mailer.rb
index 84db08e96..f56a25431 100644
--- a/app/mailers/exception_handler/exception_mailer.rb
+++ b/app/mailers/exception_handler/exception_mailer.rb
@@ -5,7 +5,7 @@ class ExceptionMailer < ApplicationMailer
# Defaults
default subject: I18n.t("exception.exception",
- host: ENV.fetch("URL_HOST", nil))
+ host: ENV.fetch("URL_HOST"))
default from: ExceptionHandler.config.email
default template_path: "exception_handler/mailers"
# => http://stackoverflow.com/a/18579046/1143732
diff --git a/app/mailers/feedback_mailer.rb b/app/mailers/feedback_mailer.rb
new file mode 100644
index 000000000..152b1378b
--- /dev/null
+++ b/app/mailers/feedback_mailer.rb
@@ -0,0 +1,15 @@
+class FeedbackMailer < ApplicationMailer
+ default from: DefaultSetting::FEEDBACK_EMAIL
+ layout false
+
+ # Mail to the MaMpf developers including the new feedback of a user.
+ def new_user_feedback_email
+ @feedback = params[:feedback]
+ reply_to_mail = @feedback.can_contact ? @feedback.user.email : ""
+ subject = "Feedback: #{@feedback.title}"
+ mail(to: DefaultSetting::FEEDBACK_EMAIL,
+ subject: subject,
+ content_type: "text/plain",
+ reply_to: reply_to_mail)
+ end
+end
diff --git a/app/models/feedback.rb b/app/models/feedback.rb
new file mode 100644
index 000000000..2cc0a0c6b
--- /dev/null
+++ b/app/models/feedback.rb
@@ -0,0 +1,10 @@
+# Feedback from users regarding MaMpf itself.
+class Feedback < ApplicationRecord
+ belongs_to :user
+
+ BODY_MIN_LENGTH = 10
+ BODY_MAX_LENGTH = 10_000
+ validates :feedback, length: { minimum: BODY_MIN_LENGTH,
+ maximum: BODY_MAX_LENGTH },
+ allow_blank: false
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 40feb6f60..4b612f20e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -78,6 +78,9 @@ class User < ApplicationRecord
# a user has a watchlist with watchlist_entries
has_many :watchlists, dependent: :destroy
+
+ has_many :feedbacks, dependent: :destroy
+
include ScreenshotUploader[:image]
# if a homepage is given it should at leat be a valid address
diff --git a/app/models/user_cleaner.rb b/app/models/user_cleaner.rb
index 877760420..8a5763e53 100644
--- a/app/models/user_cleaner.rb
+++ b/app/models/user_cleaner.rb
@@ -3,9 +3,9 @@ class UserCleaner
attr_accessor :imap, :email_dict, :hash_dict
def login
- @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER", nil), port: 993, ssl: true)
- @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME", nil),
- ENV.fetch("PROJECT_EMAIL_PASSWORD", nil))
+ @imap = Net::IMAP.new(ENV.fetch("IMAPSERVER"), port: 993, ssl: true)
+ @imap.authenticate("LOGIN", ENV.fetch("PROJECT_EMAIL_USERNAME"),
+ ENV.fetch("PROJECT_EMAIL_PASSWORD"))
end
def logout
@@ -15,7 +15,7 @@ def logout
def search_emails_and_hashes
@email_dict = {}
@hash_dict = {}
- @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil))
+ @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX"))
# Mails containing multiple email addresses (Subject: "Undelivered Mail Returned to Sender")
@imap.search(["SUBJECT",
"Undelivered Mail Returned to Sender"]).each do |message_id|
@@ -89,11 +89,11 @@ def send_hashes
end
def delete_ghosts
- @hash_dict.each do |mail, hash|
- u = User.find_by(email: mail, ghost_hash: hash)
- move_mail(@email_dict[mail]) if u.present? && @email_dict.present?
- u.destroy! if u&.generic?
- end
+ # @hash_dict.each do |mail, hash|
+ # u = User.find_by(email: mail, ghost_hash: hash)
+ # move_mail(@email_dict[mail]) if u.present? && @email_dict.present?
+ # u.destroy! if u&.generic?
+ # end
end
def move_mail(message_ids, attempt = 0)
@@ -103,7 +103,7 @@ def move_mail(message_ids, attempt = 0)
return if attempt > 3
begin
- @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX", nil))
+ @imap.examine(ENV.fetch("PROJECT_EMAIL_MAILBOX"))
@imap.move(message_ids, "Other Users/mampf/handled_bounces")
rescue Net::IMAP::BadResponseError
move_mail(message_ids, attempt + 1)
@@ -111,14 +111,15 @@ def move_mail(message_ids, attempt = 0)
end
def clean!
- login
- search_emails_and_hashes
- return if @email_dict.blank?
-
- send_hashes
- sleep(10)
- search_emails_and_hashes
- delete_ghosts
- logout
+ # TODO: Implement new user cleaner logic
+ # login
+ # search_emails_and_hashes
+ # return if @email_dict.blank?
+
+ # send_hashes
+ # sleep(10)
+ # search_emails_and_hashes
+ # delete_ghosts
+ # logout
end
end
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb
index 2cb17d595..2e47ea0f0 100644
--- a/app/views/devise/registrations/new.html.erb
+++ b/app/views/devise/registrations/new.html.erb
@@ -33,13 +33,13 @@
target: :_blank)), class: "d-inline", for: "dsgvo-consent" %>
<%= f.hidden_field :locale, value: I18n.locale %>
- <% if ENV['USE_CAPTCHA_SERVICE']%>
+ <% if ENV["USE_CAPTCHA_SERVICE"]%>
-
+
<%end %>
- <%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'register-user', disabled:ENV['USE_CAPTCHA_SERVICE'] %>
+ <%= f.submit t('.sign_up'), class: 'btn btn-primary', id: 'register-user', disabled:ENV.fetch("USE_CAPTCHA_SERVICE") %>
<% end %>
diff --git a/app/views/exception_handler/mailers/new_exception.html.erb b/app/views/exception_handler/mailers/new_exception.html.erb
index 93b3b9b89..0ebfbbe16 100644
--- a/app/views/exception_handler/mailers/new_exception.html.erb
+++ b/app/views/exception_handler/mailers/new_exception.html.erb
@@ -1,5 +1,5 @@
<%= t('exception.exception_report',
- host: ENV['URL_HOST']) %>
+ host: ENV.fetch("URL_HOST")) %>
<%= @exception.response %> (<%= @exception.status %>)
diff --git a/app/views/feedback_mailer/new_user_feedback_email.text.erb b/app/views/feedback_mailer/new_user_feedback_email.text.erb
new file mode 100644
index 000000000..9b580a4a4
--- /dev/null
+++ b/app/views/feedback_mailer/new_user_feedback_email.text.erb
@@ -0,0 +1,13 @@
+# Title (optional)
+<%= @feedback.title %>
+
+# Feedback
+<%= @feedback.feedback %>
+
+
+-----
+<% if @feedback.can_contact %>
+Reply to this mail to contact the user.
+<% else %>
+User did not give permission to contact them regarding their feedback, so we cannot reply to this mail.
+<% end %>
diff --git a/app/views/feedbacks/_feedback.html.erb b/app/views/feedbacks/_feedback.html.erb
new file mode 100644
index 000000000..b98f5c8bc
--- /dev/null
+++ b/app/views/feedbacks/_feedback.html.erb
@@ -0,0 +1,44 @@
+<%= stylesheet_link_tag 'feedback' %>
+
+<%# Main Modal %>
+
+
+
+ <%= render partial: 'feedbacks/feedback_form' %>
+
+
+
+
+<%# Messages toasts %>
+
+ <%# Error %>
+
+
+
+
+
+ <%= t('feedback.error') %>
+
+
+
+ <%# Success %>
+
+
+
+
+
+ <%= t('feedback.success') %>
+
+
+
+
diff --git a/app/views/feedbacks/_feedback_button.html.erb b/app/views/feedbacks/_feedback_button.html.erb
new file mode 100644
index 000000000..c3d9c02fa
--- /dev/null
+++ b/app/views/feedbacks/_feedback_button.html.erb
@@ -0,0 +1,8 @@
+<%= stylesheet_link_tag 'feedback' %>
+
+<%# Feedback button %>
+
diff --git a/app/views/feedbacks/_feedback_form.html.erb b/app/views/feedbacks/_feedback_form.html.erb
new file mode 100644
index 000000000..2e0833e3e
--- /dev/null
+++ b/app/views/feedbacks/_feedback_form.html.erb
@@ -0,0 +1,63 @@
+<%= javascript_include_tag :feedback %>
+
+
+
+
+ <%= t('feedback.description_html',
+ github_mampf: link_to('GitHub', 'https://github.com/MaMpf-HD/mampf/issues', {target: '_blank'}),
+ feedback_mail: mail_to(DefaultSetting::FEEDBACK_EMAIL, t('basics.mail_noun'))) %>
+
+ <%= form_with model: Feedback.new, remote: true do |f| %>
+
+
+ <%= f.text_field :title, class: 'form-control', placeholder: '_' %>
+ <%= f.label :title, t('feedback.title') %>
+
+
+
+
+ <%= f.text_area :feedback,
+ class: 'form-control',
+ placeholder: '_',
+ style: 'height: 200px',
+ required: '',
+ minlength: Feedback::BODY_MIN_LENGTH,
+ maxlength: Feedback::BODY_MAX_LENGTH,
+ 'data-too-short-message': t('feedback.body_too_short_error',
+ min_length: Feedback::BODY_MIN_LENGTH),
+ 'data-value-missing-message': t('feedback.body_missing_error') %>
+ <%= f.label :feedback, t('feedback.comment') %>
+
+
+
+
+ <%= f.check_box :can_contact,
+ checked: 'checked',
+ class: 'form-check-input' %>
+ <%= f.label :can_contact,
+ t('feedback.mail_checkbox', user_mail: @current_user.email),
+ class: 'form-check-label' %>
+
+
+
+
+ <%= f.submit 'Submit', id: 'submit-feedback-form-btn', style: 'display: none;' %>
+ <% end %>
+
+
+
diff --git a/app/views/feedbacks/create.js.erb b/app/views/feedbacks/create.js.erb
new file mode 100644
index 000000000..1538b24bf
--- /dev/null
+++ b/app/views/feedbacks/create.js.erb
@@ -0,0 +1,7 @@
+<% if @feedback_success %>
+$("#submit-feedback").modal("hide");
+$("#submit-feedback").find("form").trigger("reset"); // clear form
+$("#toast-successfully-sent").toast("show");
+<% else %>
+$("#toast-could-not-send").toast("show");
+<% end %>
diff --git a/app/views/shared/_dropdown_notifications.html.erb b/app/views/shared/_dropdown_notifications.html.erb
index 2117400a9..5405d3bc0 100644
--- a/app/views/shared/_dropdown_notifications.html.erb
+++ b/app/views/shared/_dropdown_notifications.html.erb
@@ -1,7 +1,7 @@
<% relevant_notifications = current_user.notifications
.sort_by(&:created_at).reverse %>
<% if relevant_notifications.present? %>
-
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/shared/_footer.html.erb b/app/views/shared/_footer.html.erb
index efb373378..501a4b168 100644
--- a/app/views/shared/_footer.html.erb
+++ b/app/views/shared/_footer.html.erb
@@ -46,7 +46,7 @@
bugs: link_to(t('here'),
'https://github.com/MaMpf-HD/mampf/issues',
target: :_blank),
- feedback: mail_to(DefaultSetting::PROJECT_EMAIL, nil),
+ feedback: mail_to(DefaultSetting::FEEDBACK_EMAIL, nil),
background_photo_credit: link_to('Craig S. Kaplan',
'https://github.com/isohedral/hatviz',
target: :_blank)) %>
diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb
index 18cc82648..73e98ebf1 100644
--- a/app/views/shared/_navbar.html.erb
+++ b/app/views/shared/_navbar.html.erb
@@ -1,5 +1,8 @@
<% I18n.with_locale(current_user.try(:locale)) do %>
<%= stylesheet_link_tag 'navbar' %>
+
+<%= render partial: 'feedbacks/feedback' %>
+