Merge pull request #94 from AyuntamientoMadrid/vote_comments-25
refactors voting
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
class CommentsController < ApplicationController
|
class CommentsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_debate, :set_parent
|
before_action :set_debate, :set_parent, only: :create
|
||||||
respond_to :html, :js
|
respond_to :html, :js
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -10,6 +10,12 @@ class CommentsController < ApplicationController
|
|||||||
respond_with @comment
|
respond_with @comment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vote
|
||||||
|
@comment = Comment.find(params[:id])
|
||||||
|
@comment.vote_by(voter: current_user, vote: params[:value])
|
||||||
|
respond_with @comment
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def comment_params
|
def comment_params
|
||||||
params.require(:comments).permit(:commentable_type, :commentable_id, :body)
|
params.require(:comments).permit(:commentable_type, :commentable_id, :body)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class DebatesController < ApplicationController
|
class DebatesController < ApplicationController
|
||||||
include RecaptchaHelper
|
include RecaptchaHelper
|
||||||
before_action :set_debate, only: [:show, :edit, :update]
|
before_action :set_debate, only: [:show, :edit, :update, :vote]
|
||||||
before_action :authenticate_user!, except: [:show, :index]
|
before_action :authenticate_user!, except: [:index, :show]
|
||||||
before_action :validate_ownership, only: [:edit, :update]
|
before_action :validate_ownership, only: [:edit, :update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@@ -38,6 +38,10 @@ class DebatesController < ApplicationController
|
|||||||
respond_with @debate
|
respond_with @debate
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vote
|
||||||
|
@debate.vote_by(voter: current_user, vote: params[:value])
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def set_debate
|
def set_debate
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
class VotesController < ApplicationController
|
|
||||||
before_action :set_debate
|
|
||||||
before_action :authenticate_user!
|
|
||||||
respond_to :html, :js
|
|
||||||
|
|
||||||
def create
|
|
||||||
register_vote
|
|
||||||
notice = @debate.vote_registered? ? I18n.t("votes.notice_thanks") : I18n.t("votes.notice_already_registered")
|
|
||||||
respond_with @debate
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_debate
|
|
||||||
@debate = Debate.find(params[:debate_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def register_vote
|
|
||||||
@debate.vote_by voter: current_user, vote: params[:value]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
class Comment < ActiveRecord::Base
|
class Comment < ActiveRecord::Base
|
||||||
acts_as_nested_set scope: [:commentable_id, :commentable_type]
|
acts_as_nested_set scope: [:commentable_id, :commentable_type]
|
||||||
|
acts_as_votable
|
||||||
|
|
||||||
validates :body, presence: true
|
validates :body, presence: true
|
||||||
validates :user, presence: true
|
validates :user, presence: true
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
<%= comment.user.name %> • <%= time_ago_in_words(comment.created_at) %>
|
<%= comment.user.name %> • <%= time_ago_in_words(comment.created_at) %>
|
||||||
</span>
|
</span>
|
||||||
<p><%= comment.body %></p>
|
<p><%= comment.body %></p>
|
||||||
|
|
||||||
|
<span id="<%= dom_id(comment) %>_votes">
|
||||||
|
<%= render 'comments/votes', comment: comment %>
|
||||||
|
</span>
|
||||||
|
|
||||||
<p class="reply"><%= render 'comments/form', parent: comment %></p>
|
<p class="reply"><%= render 'comments/form', parent: comment %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
11
app/views/comments/_votes.html.erb
Normal file
11
app/views/comments/_votes.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<span class="in_favor">
|
||||||
|
<%= link_to "up", vote_comment_path(comment, value: 'yes'),
|
||||||
|
method: "post", remote: true %>
|
||||||
|
<%= comment.get_likes.size %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="against">
|
||||||
|
<%= link_to "down", vote_comment_path(comment, value: 'no'),
|
||||||
|
method: "post", remote: true %>
|
||||||
|
<%= comment.get_dislikes.size %>
|
||||||
|
</span>
|
||||||
1
app/views/comments/vote.js.erb
Normal file
1
app/views/comments/vote.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
$("#<%= dom_id(@comment) %>_votes").html('<%= j render("comments/votes", comment: @comment) %>');
|
||||||
@@ -14,25 +14,11 @@
|
|||||||
<%= render "shared/tags", debate: debate %>
|
<%= render "shared/tags", debate: debate %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-12 medium-3 column">
|
|
||||||
<div class="text-center votes">
|
|
||||||
<%= link_to debate_votes_path(debate, value: 'yes'), class: "like inline-block", title: t('votes.agree'), method: "post" do %>
|
|
||||||
<i class="icon-like"></i>
|
|
||||||
<span><%= percentage('likes', debate) %></span>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<span class="divider"></span>
|
<div id="<%= dom_id(debate) %>_votes" class="small-12 medium-3 column">
|
||||||
|
<%= render 'debates/votes_min', debate: debate %>
|
||||||
<%= link_to debate_votes_path(debate, value: 'no'), class: "unlike inline-block", title: t('votes.disagree'), method: "post" do %>
|
|
||||||
<i class="icon-unlike"></i>
|
|
||||||
<span><%= percentage('dislikes', debate) %></span>
|
|
||||||
<% end %>
|
|
||||||
<br>
|
|
||||||
<span class="total-votes">
|
|
||||||
<%= pluralize(debate.total_votes, t("debates.debate.vote"), t("debates.debate.votes")) %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,22 +16,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row votes">
|
<div class="row votes">
|
||||||
<div class="small-12 column">
|
<div id="<%= dom_id(featured_debate) %>_votes" class="small-12 column">
|
||||||
<%= link_to debate_votes_path(featured_debate, value: "yes"), class: "like", title: t('votes.agree'), method: "post" do %>
|
<%= render 'debates/featured_debate_votes', debate: featured_debate %>
|
||||||
<i class="icon-like"></i>
|
|
||||||
<span><%= percentage('likes', featured_debate) %></span>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<span class="divider"></span>
|
|
||||||
|
|
||||||
<%= link_to debate_votes_path(featured_debate, value: "no"), class: "unlike", title: t('votes.disagree'), method: "post" do %>
|
|
||||||
<i class="icon-unlike"></i>
|
|
||||||
<span><%= percentage('dislikes', featured_debate) %></span>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<span class="total-votes right">
|
|
||||||
<%= pluralize(featured_debate.total_votes, t("debates.debate.vote"), t("debates.debate.votes")) %>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
21
app/views/debates/_featured_debate_votes.html.erb
Normal file
21
app/views/debates/_featured_debate_votes.html.erb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<span id="in_favor">
|
||||||
|
<%= link_to vote_debate_path(debate, value: 'yes', partial: 'featured_debate_votes'),
|
||||||
|
class: "like", title: t('votes.agree'), method: "post", remote: true do %>
|
||||||
|
<i class="icon-like"></i>
|
||||||
|
<span><%= percentage('likes', debate) %></span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="divider"></span>
|
||||||
|
|
||||||
|
<span id="against">
|
||||||
|
<%= link_to vote_debate_path(debate, value: 'no', partial: 'featured_debate_votes'),
|
||||||
|
class: "unlike", title: t('votes.disagree'), method: "post", remote: true do %>
|
||||||
|
<i class="icon-unlike"></i>
|
||||||
|
<span><%= percentage('dislikes', debate) %></span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="total-votes right">
|
||||||
|
<%= pluralize(debate.total_votes, t("debates.debate.vote"), t("debates.debate.votes")) %>
|
||||||
|
</span>
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div id="in_favor" class="inline-block">
|
<div id="in_favor" class="inline-block">
|
||||||
<%= link_to debate_votes_path(@debate, value: 'yes'), class: "like", title: t('votes.agree'), method: "post", remote: true do %>
|
<%= link_to vote_debate_path(@debate, value: 'yes', partial: 'votes'),
|
||||||
|
class: "like", title: t('votes.agree'), method: "post", remote: true do %>
|
||||||
<i class="icon-like"></i>
|
<i class="icon-like"></i>
|
||||||
<span><%= percentage('likes', @debate) %></span>
|
<span><%= percentage('likes', @debate) %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -16,7 +17,8 @@
|
|||||||
<span class="divider"></span>
|
<span class="divider"></span>
|
||||||
|
|
||||||
<div id="against" class="inline-block">
|
<div id="against" class="inline-block">
|
||||||
<%= link_to debate_votes_path(@debate, value: 'no'), class: "unlike", title: t('votes.disagree'), method: "post", remote: true do %>
|
<%= link_to vote_debate_path(@debate, value: 'no', partial: 'votes'),
|
||||||
|
class: "unlike", title: t('votes.disagree'), method: "post", remote: true do %>
|
||||||
<i class="icon-unlike"></i>
|
<i class="icon-unlike"></i>
|
||||||
<span><%= percentage('dislikes', @debate) %></span>
|
<span><%= percentage('dislikes', @debate) %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
22
app/views/debates/_votes_min.html.erb
Normal file
22
app/views/debates/_votes_min.html.erb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="text-center votes">
|
||||||
|
<span id="in_favor">
|
||||||
|
<%= link_to vote_debate_path(debate, value: 'yes', partial: 'votes_min'),
|
||||||
|
class: "like inline-block", title: t('votes.agree'), method: "post", remote: true do %>
|
||||||
|
<i class="icon-like"></i>
|
||||||
|
<span><%= percentage('likes', debate) %></span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<span class="divider"></span>
|
||||||
|
|
||||||
|
<span id="against">
|
||||||
|
<%= link_to vote_debate_path(debate, value: 'no', partial: 'votes_min'),
|
||||||
|
class: "unlike inline-block", title: t('votes.disagree'), method: "post", remote: true do %>
|
||||||
|
<i class="icon-unlike"></i>
|
||||||
|
<span><%= percentage('dislikes', debate) %></span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<span class="total-votes">
|
||||||
|
<%= pluralize(debate.total_votes, t("debates.debate.vote"), t("debates.debate.votes")) %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
<%= @debate.description %>
|
<%= @debate.description %>
|
||||||
<p><%= render 'shared/tags', debate: @debate %></p>
|
<p><%= render 'shared/tags', debate: @debate %></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="votes" class="small-12 medium-3 column votes">
|
<div id="<%= dom_id(@debate) %>_votes" class="votes small-12 medium-3 column">
|
||||||
<%= render 'votes/votes' %>
|
<%= render 'debates/votes' %>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<%= link_to t("debates.show.leave_comment"), "#comments", class: "leave-comment" %>
|
<%= link_to t("debates.show.leave_comment"), "#comments", class: "leave-comment" %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
app/views/debates/vote.js.erb
Normal file
1
app/views/debates/vote.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
$("#<%= dom_id(@debate) %>_votes").html('<%= j render("debates/#{params[:partial]}", debate: @debate) %>');
|
||||||
@@ -1 +0,0 @@
|
|||||||
$("#votes").html("<%= j render('votes') %>");
|
|
||||||
@@ -6,9 +6,18 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
# You can have the root of your site routed with "root"
|
# You can have the root of your site routed with "root"
|
||||||
root 'debates#index'
|
root 'debates#index'
|
||||||
|
|
||||||
resources :debates do
|
resources :debates do
|
||||||
resources :votes, only: :create
|
member do
|
||||||
resources :comments, only: :create
|
post :vote
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :comments, only: :create, shallow: true do
|
||||||
|
member do
|
||||||
|
post :vote
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :account, controller: "account", only: [:show, :update]
|
resource :account, controller: "account", only: [:show, :update]
|
||||||
|
|||||||
@@ -2,74 +2,194 @@ require 'rails_helper'
|
|||||||
|
|
||||||
feature 'Votes' do
|
feature 'Votes' do
|
||||||
|
|
||||||
background do
|
feature 'Debates' do
|
||||||
@manuela = create(:user)
|
|
||||||
@pablo = create(:user)
|
background do
|
||||||
@debate = create(:debate)
|
@manuela = create(:user)
|
||||||
|
@pablo = create(:user)
|
||||||
|
@debate = create(:debate)
|
||||||
|
|
||||||
|
login_as(@manuela)
|
||||||
|
visit debate_path(@debate)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Show' do
|
||||||
|
vote = create(:vote, voter: @manuela, votable: @debate, vote_flag: true)
|
||||||
|
vote = create(:vote, voter: @pablo, votable: @debate, vote_flag: false)
|
||||||
|
|
||||||
|
visit debate_path(@debate)
|
||||||
|
|
||||||
|
expect(page).to have_content "2 votes"
|
||||||
|
|
||||||
|
within('#in_favor') do
|
||||||
|
expect(page).to have_content "50%"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('#against') do
|
||||||
|
expect(page).to have_content "50%"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Create from debate show', :js do
|
||||||
|
find('#in_favor a').click
|
||||||
|
|
||||||
|
within('#in_favor') do
|
||||||
|
expect(page).to have_content "100%"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('#against') do
|
||||||
|
expect(page).to have_content "0%"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content "1 vote"
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Create from debate featured', :js do
|
||||||
|
visit debates_path
|
||||||
|
|
||||||
|
within("#featured-debates") do
|
||||||
|
find('#in_favor a').click
|
||||||
|
|
||||||
|
within('#in_favor') do
|
||||||
|
expect(page).to have_content "100%"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('#against') do
|
||||||
|
expect(page).to have_content "0%"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content "1 vote"
|
||||||
|
end
|
||||||
|
expect(URI.parse(current_url).path).to eq(debates_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Create from debate index', :js do
|
||||||
|
3.times { create(:debate) }
|
||||||
|
visit debates_path
|
||||||
|
|
||||||
|
within("#debates") do
|
||||||
|
expect(page).to have_css(".debate", count: 1)
|
||||||
|
|
||||||
|
find('#in_favor a').click
|
||||||
|
|
||||||
|
within('#in_favor') do
|
||||||
|
expect(page).to have_content "100%"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('#against') do
|
||||||
|
expect(page).to have_content "0%"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content "1 vote"
|
||||||
|
end
|
||||||
|
expect(URI.parse(current_url).path).to eq(debates_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Update', :js do
|
||||||
|
find('#in_favor a').click
|
||||||
|
find('#against a').click
|
||||||
|
|
||||||
|
within('#in_favor') do
|
||||||
|
expect(page).to have_content "0%"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('#against') do
|
||||||
|
expect(page).to have_content "100%"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content "1 vote"
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Trying to vote multiple times', :js do
|
||||||
|
find('#in_favor a').click
|
||||||
|
find('#in_favor a').click
|
||||||
|
|
||||||
|
within('#in_favor') do
|
||||||
|
expect(page).to have_content "100%"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('#against') do
|
||||||
|
expect(page).to have_content "0%"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content "1 vote"
|
||||||
|
end
|
||||||
|
|
||||||
login_as(@manuela)
|
|
||||||
visit debate_path(@debate)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'Show' do
|
|
||||||
vote = create(:vote, voter: @manuela, votable: @debate, vote_flag: true)
|
|
||||||
vote = create(:vote, voter: @pablo, votable: @debate, vote_flag: false)
|
|
||||||
|
|
||||||
visit debate_path(@debate)
|
feature 'Comments' do
|
||||||
|
|
||||||
expect(page).to have_content "2 votes"
|
background do
|
||||||
|
@manuela = create(:user)
|
||||||
|
@pablo = create(:user)
|
||||||
|
@debate = create(:debate)
|
||||||
|
@comment = create(:comment, commentable: @debate)
|
||||||
|
|
||||||
within('#in_favor') do
|
login_as(@manuela)
|
||||||
expect(page).to have_content "50%"
|
visit debate_path(@debate)
|
||||||
end
|
end
|
||||||
|
|
||||||
within('#against') do
|
scenario 'Show' do
|
||||||
expect(page).to have_content "50%"
|
vote = create(:vote, voter: @manuela, votable: @comment, vote_flag: true)
|
||||||
|
vote = create(:vote, voter: @pablo, votable: @comment, vote_flag: false)
|
||||||
|
|
||||||
|
visit debate_path(@debate)
|
||||||
|
|
||||||
|
within("#comment_#{@comment.id}_votes") do
|
||||||
|
within(".in_favor") do
|
||||||
|
expect(page).to have_content "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
within(".against") do
|
||||||
|
expect(page).to have_content "1"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scenario 'Create', :js do
|
||||||
|
within("#comment_#{@comment.id}_votes") do
|
||||||
|
find(".in_favor a").click
|
||||||
|
|
||||||
|
within(".in_favor") do
|
||||||
|
expect(page).to have_content "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
within(".against") do
|
||||||
|
expect(page).to have_content "0"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Update', :js do
|
||||||
|
within("#comment_#{@comment.id}_votes") do
|
||||||
|
find('.in_favor a').click
|
||||||
|
find('.against a').click
|
||||||
|
|
||||||
|
within('.in_favor') do
|
||||||
|
expect(page).to have_content "0"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('.against') do
|
||||||
|
expect(page).to have_content "1"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Trying to vote multiple times', :js do
|
||||||
|
within("#comment_#{@comment.id}_votes") do
|
||||||
|
find('.in_favor a').click
|
||||||
|
find('.in_favor a').click
|
||||||
|
|
||||||
|
within('.in_favor') do
|
||||||
|
expect(page).to have_content "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
within('.against') do
|
||||||
|
expect(page).to have_content "0"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'Create', :js do
|
|
||||||
find('#in_favor a').click
|
|
||||||
|
|
||||||
within('#in_favor') do
|
|
||||||
expect(page).to have_content "100%"
|
|
||||||
end
|
|
||||||
|
|
||||||
within('#against') do
|
|
||||||
expect(page).to have_content "0%"
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_content "1 vote"
|
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'Update', :js do
|
|
||||||
find('#in_favor a').click
|
|
||||||
find('#against a').click
|
|
||||||
|
|
||||||
within('#in_favor') do
|
|
||||||
expect(page).to have_content "0%"
|
|
||||||
end
|
|
||||||
|
|
||||||
within('#against') do
|
|
||||||
expect(page).to have_content "100%"
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_content "1 vote"
|
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'Trying to vote multiple times', :js do
|
|
||||||
find('#in_favor a').click
|
|
||||||
find('#in_favor a').click
|
|
||||||
|
|
||||||
within('#in_favor') do
|
|
||||||
expect(page).to have_content "100%"
|
|
||||||
end
|
|
||||||
|
|
||||||
within('#against') do
|
|
||||||
expect(page).to have_content "0%"
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_content "1 vote"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user