[IMP] Deadlines on invitations
[REF] Survey auth refactoring [REF/IMP] Pagination system refactoring + add go back function [FIX] Small glitches [REF] Code refactoring bzr revid: rim@openerp.com-20131212110435-he9h9dkeyb722l1a
This commit is contained in:
@@ -23,16 +23,56 @@ from openerp.addons.web import http
|
||||
from openerp.addons.web.http import request
|
||||
from openerp.addons.website.models import website
|
||||
from openerp import SUPERUSER_ID
|
||||
|
||||
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF
|
||||
from datetime import datetime
|
||||
import werkzeug
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebsiteSurvey(http.Controller):
|
||||
|
||||
## HELPER METHODS ##
|
||||
|
||||
def _check_bad_cases(self, cr, uid, request, survey_obj, survey, user_input_obj, context=None):
|
||||
# In case of bad survey, redirect to surveys list
|
||||
if survey_obj.exists(cr, SUPERUSER_ID, survey.id, context=context) == []:
|
||||
return werkzeug.utils.redirect("/survey/")
|
||||
|
||||
# In case of auth required, block public user
|
||||
if survey.auth_required and uid == request.registry['website'].get_public_user(cr, uid, context):
|
||||
return request.website.render("website.401")
|
||||
|
||||
# In case of non open surveys
|
||||
if survey.state != 'open':
|
||||
return request.website.render("survey.notopen")
|
||||
|
||||
# If enough surveys completed
|
||||
if survey.user_input_limit > 0:
|
||||
completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
|
||||
if completed >= survey.user_input_limit:
|
||||
return request.website.render("survey.notopen")
|
||||
|
||||
# Everything seems to be ok
|
||||
return None
|
||||
|
||||
def _check_deadline(self, cr, uid, user_input, context=None):
|
||||
'''Prevent opening of the survey if the deadline has turned out
|
||||
|
||||
! This will NOT disallow access to users who have already partially filled the survey !'''
|
||||
if user_input.deadline:
|
||||
dt_deadline = datetime.strptime(user_input.deadline, DTF)
|
||||
dt_now = datetime.now()
|
||||
if dt_now > dt_deadline: # survey is not open anymore
|
||||
return request.website.render("survey.notopen")
|
||||
|
||||
return None
|
||||
|
||||
## ROUTES HANDLERS ##
|
||||
|
||||
# Survey list
|
||||
@website.route(['/survey/',
|
||||
'/survey/list/'],
|
||||
@@ -56,23 +96,10 @@ class WebsiteSurvey(http.Controller):
|
||||
survey_obj = request.registry['survey.survey']
|
||||
user_input_obj = request.registry['survey.user_input']
|
||||
|
||||
# In case of bad survey, redirect to surveys list
|
||||
if survey_obj.exists(cr, uid, survey.id, context=context) == []:
|
||||
return werkzeug.utils.redirect("/survey/")
|
||||
|
||||
# In case of auth required, block public user
|
||||
if survey.auth_required and uid == request.registry['website'].get_public_user(request.cr, SUPERUSER_ID, request.context):
|
||||
return request.website.render("website.401")
|
||||
|
||||
# In case of non open surveys
|
||||
if survey.state != 'open':
|
||||
return request.website.render("survey.notopen")
|
||||
|
||||
# If enough surveys completed
|
||||
if survey.user_input_limit > 0:
|
||||
completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
|
||||
if completed >= survey.user_input_limit:
|
||||
return request.website.render("survey.notopen")
|
||||
# Controls if the survey can be displayed
|
||||
errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
|
||||
if errpage:
|
||||
return errpage
|
||||
|
||||
# Manual surveying
|
||||
if not token:
|
||||
@@ -89,73 +116,50 @@ class WebsiteSurvey(http.Controller):
|
||||
else:
|
||||
user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
|
||||
|
||||
# Prevent opening of the survey if the deadline has turned out
|
||||
# ! This will NOT disallow access to users who have already partially filled the survey !
|
||||
# if user_input.deadline > fields.date.now() and user_input.state == 'new':
|
||||
# return request.website.render("survey.notopen")
|
||||
# TODO check if this is ok
|
||||
# Do not open expired survey
|
||||
errpage = self._check_deadline(cr, uid, user_input, context=context)
|
||||
if errpage:
|
||||
return errpage
|
||||
|
||||
# Select the right page
|
||||
|
||||
if user_input.state == 'new': # Intro page
|
||||
data = {'survey': survey, 'page': None, 'token': user_input.token}
|
||||
return request.website.render('survey.survey_init', data)
|
||||
else:
|
||||
return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token))
|
||||
|
||||
|
||||
# Survey displaying
|
||||
@website.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>'],
|
||||
@website.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>',
|
||||
'/survey/fill/<model("survey.survey"):survey>/<string:token>/<string:prev>'],
|
||||
type='http', auth='public', multilang=True)
|
||||
def fill_survey(self, survey, token, **post):
|
||||
def fill_survey(self, survey, token, prev=None, **post):
|
||||
'''Display and validates a survey'''
|
||||
cr, uid, context = request.cr, request.uid, request.context
|
||||
survey_obj = request.registry['survey.survey']
|
||||
user_input_obj = request.registry['survey.user_input']
|
||||
|
||||
# In case of bad survey, redirect to surveys list
|
||||
if survey_obj.exists(cr, uid, survey.id, context=context) == []:
|
||||
return werkzeug.utils.redirect("/survey/")
|
||||
# Controls if the survey can be displayed
|
||||
errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
|
||||
if errpage:
|
||||
return errpage
|
||||
|
||||
# In case of auth required, block public user
|
||||
if survey.auth_required and uid == request.registry['website'].get_public_user(request.cr, SUPERUSER_ID, request.context):
|
||||
return request.website.render("website.401")
|
||||
|
||||
# In case of non open surveys
|
||||
if survey.state != 'open':
|
||||
return request.website.render("survey.notopen")
|
||||
|
||||
# If enough surveys completed
|
||||
if survey.user_input_limit > 0:
|
||||
completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
|
||||
if completed >= survey.user_input_limit:
|
||||
return request.website.render("survey.notopen")
|
||||
|
||||
# Manual surveying
|
||||
if not token:
|
||||
if survey.visible_to_user:
|
||||
user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id})
|
||||
user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
|
||||
else: # An user cannot open hidden surveys without token
|
||||
return request.website.render("website.403")
|
||||
# Load the user_input
|
||||
try:
|
||||
user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0]
|
||||
except IndexError: # Invalid token
|
||||
return request.website.render("website.403")
|
||||
else:
|
||||
try:
|
||||
user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0]
|
||||
except IndexError: # Invalid token
|
||||
return request.website.render("website.403")
|
||||
else:
|
||||
user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
|
||||
user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
|
||||
|
||||
# Prevent opening of the survey if the deadline has turned out
|
||||
# ! This will NOT disallow access to users who have already partially filled the survey !
|
||||
# if user_input.deadline > fields.date.now() and user_input.state == 'new':
|
||||
# return request.website.render("survey.notopen")
|
||||
# TODO check if this is ok
|
||||
# Do not display expired survey (even if some pages have already been
|
||||
# displayed -- There's a time for everything!)
|
||||
errpage = self._check_deadline(cr, uid, user_input, context=context)
|
||||
if errpage:
|
||||
return errpage
|
||||
|
||||
# Select the right page
|
||||
|
||||
if user_input.state == 'new': # First page
|
||||
page, page_nr, last = self.find_next_page(survey, user_input)
|
||||
page, page_nr, last = survey_obj.next_page(cr, uid, user_input, 0, go_back=False, context=context)
|
||||
data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
|
||||
if last:
|
||||
data.update({'last': True})
|
||||
@@ -163,39 +167,33 @@ class WebsiteSurvey(http.Controller):
|
||||
elif user_input.state == 'done': # Display success message
|
||||
return request.website.render('survey.finished', {'survey': survey})
|
||||
elif user_input.state == 'skip':
|
||||
page, page_nr, last = self.find_next_page(survey, user_input)
|
||||
flag = (True if prev and prev == 'prev' else False)
|
||||
page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context)
|
||||
data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
|
||||
if last:
|
||||
data.update({'last': True})
|
||||
return request.website.render('survey.survey', data)
|
||||
else:
|
||||
return request.website.render("website.403")
|
||||
|
||||
# @website.route(['/survey/prefill/<model("survey.survey"):survey>'], type='json', auth='public', multilang=True):
|
||||
# AJAX prefilling of a survey
|
||||
# @website.route(['/survey/prefill/<model("survey.survey"):survey>'],
|
||||
# type='json', auth='public', multilang=True):
|
||||
# def prefill(self, survey, **post):
|
||||
|
||||
# AJAX validation of some questions
|
||||
# @website.route(['/survey/validate/<model("survey.survey"):survey>'],
|
||||
# type='http', auth='public', multilang=True)
|
||||
# def validate(self, survey, **post):
|
||||
# _logger.debug('Incoming data: %s', post)
|
||||
# page_id = int(post['page_id'])
|
||||
# cr, uid, context = request.cr, request.uid, request.context
|
||||
# questions_obj = request.registry['survey.question']
|
||||
# questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
|
||||
# questions = questions_obj.browse(cr, uid, questions_ids, context=context)
|
||||
# errors = {}
|
||||
# for question in questions:
|
||||
# answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
|
||||
# errors.update(self.validate_question(question, post, answer_tag))
|
||||
# ret = {}
|
||||
# if (len(errors) != 0):
|
||||
# ret['errors'] = errors
|
||||
# return json.dumps(ret)
|
||||
|
||||
# AJAX submission of a page
|
||||
@website.route(['/survey/submit/<model("survey.survey"):survey>'],
|
||||
type='http', auth='public', multilang=True)
|
||||
def submit(self, survey, **post):
|
||||
_logger.debug('Incoming data: %s', post)
|
||||
page_id = int(post['page_id'])
|
||||
cr, uid, context = request.cr, request.uid, request.context
|
||||
survey_obj = request.registry['survey.survey']
|
||||
questions_obj = request.registry['survey.question']
|
||||
questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
|
||||
questions = questions_obj.browse(cr, uid, questions_ids, context=context)
|
||||
@@ -213,6 +211,7 @@ class WebsiteSurvey(http.Controller):
|
||||
else:
|
||||
# Store answers into database
|
||||
user_input_obj = request.registry['survey.user_input']
|
||||
|
||||
user_input_line_obj = request.registry['survey.user_input_line']
|
||||
try:
|
||||
user_input_id = user_input_obj.search(cr, uid, [('token', '=', post['token'])], context=context)[0]
|
||||
@@ -222,12 +221,18 @@ class WebsiteSurvey(http.Controller):
|
||||
answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
|
||||
user_input_line_obj.save_lines(cr, uid, user_input_id, question, post, answer_tag, context=context)
|
||||
|
||||
# page, _, _ = self.find_next_page(survey, user_input_obj.browse(cr, uid, [user_input_id], context=context))
|
||||
# if page:
|
||||
# user_input_obj.write(cr, uid, user_input_id, {'state': 'skip'})
|
||||
# else:
|
||||
user_input_obj.write(cr, uid, user_input_id, {'state': 'done'})
|
||||
user_input = user_input_obj.browse(cr, uid, user_input_id, context=context)
|
||||
go_back = post['button_submit'] == 'previous'
|
||||
next_page, _, last = survey_obj.next_page(cr, uid, user_input, page_id, go_back=go_back, context=context)
|
||||
vals = {'last_displayed_page_id': page_id}
|
||||
if next_page is None and not go_back:
|
||||
vals.update({'state': 'done'})
|
||||
else:
|
||||
vals.update({'state': 'skip'})
|
||||
user_input_obj.write(cr, uid, user_input_id, vals, context=context)
|
||||
ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token'])
|
||||
if go_back:
|
||||
ret['redirect'] += '/prev'
|
||||
return json.dumps(ret)
|
||||
|
||||
# Printing routes
|
||||
@@ -256,7 +261,7 @@ class WebsiteSurvey(http.Controller):
|
||||
page_nr = page_nr + 1
|
||||
else:
|
||||
nextpage = page
|
||||
if page_nr == len(survey.page_ids):
|
||||
if page_nr == len(survey.page_ids) - 1:
|
||||
last = True
|
||||
return nextpage, page_nr, last
|
||||
|
||||
|
||||
+79
-29
@@ -150,6 +150,45 @@ class survey_survey(osv.osv):
|
||||
return super(survey_survey, self).copy(cr, uid, ids, vals,
|
||||
context=context)
|
||||
|
||||
def next_page(self, cr, uid, user_input, page_id, go_back=False, context=None):
|
||||
'''The next page to display to the user, knowing that page_id is the id
|
||||
of the last displayed page.
|
||||
|
||||
If page_id == 0, it will always return the first page of the survey.
|
||||
|
||||
If all the pages have been displayed and go_back == False, it will
|
||||
return None
|
||||
|
||||
If go_back == True, it will return the *previous* page instead of the
|
||||
next page.
|
||||
|
||||
.. note::
|
||||
It is assumed here that a careful user will not try to set go_back
|
||||
to True if she knows that the page to display is the first one!
|
||||
(doing this will probably cause a giant worm to eat her house)'''
|
||||
survey = user_input.survey_id
|
||||
pages = list(enumerate(survey.page_ids))
|
||||
|
||||
# First page
|
||||
if page_id == 0:
|
||||
return (pages[0][1], 0, len(pages) == 1)
|
||||
|
||||
current_page_index = pages.index((filter(lambda p: p[1].id == page_id, pages))[0])
|
||||
|
||||
# All the pages have been displayed
|
||||
if current_page_index == len(pages) - 1 and not go_back:
|
||||
return (None, -1, False)
|
||||
# Let's get back, baby!
|
||||
elif go_back and survey.users_can_go_back:
|
||||
return (pages[current_page_index - 1][1], current_page_index - 1, False)
|
||||
else:
|
||||
# This will show the last page
|
||||
if current_page_index == len(pages) - 2:
|
||||
return (pages[current_page_index + 1][1], current_page_index + 1, True)
|
||||
# This will show a regular page
|
||||
else:
|
||||
return (pages[current_page_index + 1][1], current_page_index + 1, False)
|
||||
|
||||
def action_print_survey_questions(self, cr, uid, ids, context=None):
|
||||
''' Generates a printable view of an empty survey '''
|
||||
pass
|
||||
@@ -287,6 +326,9 @@ class survey_page(osv.osv):
|
||||
help="An introductory text to your page", translate=True,
|
||||
oldname="note"),
|
||||
}
|
||||
_defaults = {
|
||||
'sequence': 10
|
||||
}
|
||||
|
||||
# Public methods #
|
||||
|
||||
@@ -408,6 +450,7 @@ class survey_question(osv.osv):
|
||||
}
|
||||
_defaults = {
|
||||
'page_id': lambda s, cr, uid, c: c.get('page_id'),
|
||||
'sequence': 10,
|
||||
'type': 'free_text',
|
||||
'matrix_subtype': 'simple',
|
||||
'column_nb': '12',
|
||||
@@ -661,7 +704,7 @@ class survey_question(osv.osv):
|
||||
if question.constr_mandatory:
|
||||
lines_number = len(question.labels_ids_2)
|
||||
answer_candidates = dict_keys_startswith(post, answer_tag)
|
||||
comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, question.comment_children_ids[0].id)), None)
|
||||
#comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, question.comment_children_ids[0].id)), None)
|
||||
# Number of lines that have been answered
|
||||
if question.matrix_subtype == 'simple':
|
||||
answer_number = len(answer_candidates)
|
||||
@@ -701,6 +744,9 @@ class survey_label(osv.osv):
|
||||
'value': fields.char("Suggested value", translate=True,
|
||||
required=True)
|
||||
}
|
||||
defaults = {
|
||||
'sequence': 10
|
||||
}
|
||||
|
||||
|
||||
class survey_user_input(osv.osv):
|
||||
@@ -714,8 +760,9 @@ class survey_user_input(osv.osv):
|
||||
readonly=1, ondelete='restrict'),
|
||||
'date_create': fields.datetime('Creation Date', required=True,
|
||||
readonly=1),
|
||||
'deadline': fields.date("Deadline",
|
||||
help="Date by which the person can take part to the survey",
|
||||
'deadline': fields.datetime("Deadline",
|
||||
help="Date by which the person can open the survey and submit answers.\
|
||||
Warning: ",
|
||||
oldname="date_deadline"),
|
||||
'type': fields.selection([('manually', 'Manually'), ('link', 'Link')],
|
||||
'Answer Type', required=1, readonly=1,
|
||||
@@ -732,6 +779,9 @@ class survey_user_input(osv.osv):
|
||||
'partner_id': fields.many2one('res.partner', 'Partner', readonly=1),
|
||||
'email': fields.char("E-mail", readonly=1),
|
||||
|
||||
# Displaying data
|
||||
'last_displayed_page_id': fields.many2one('survey.page',
|
||||
'Last displayed page'),
|
||||
# The answers !
|
||||
'user_input_line_ids': fields.one2many('survey.user_input_line',
|
||||
'user_input_id', 'Answers'),
|
||||
@@ -872,7 +922,7 @@ class survey_user_input_line(osv.osv):
|
||||
'survey_id': fields.related('user_input_id', 'survey_id',
|
||||
type="many2one", relation="survey.survey",
|
||||
string='Survey'),
|
||||
'date_create': fields.datetime('Create Date', required=1), # drop
|
||||
'date_create': fields.datetime('Create Date', required=1),
|
||||
'skipped': fields.boolean('Skipped'),
|
||||
'answer_type': fields.selection([('text', 'Text'),
|
||||
('number', 'Number'),
|
||||
@@ -888,7 +938,7 @@ class survey_user_input_line(osv.osv):
|
||||
}
|
||||
_defaults = {
|
||||
'skipped': False,
|
||||
'date_create': fields.datetime.now
|
||||
'date_create': fields.datetime.now()
|
||||
}
|
||||
|
||||
def save_lines(self, cr, uid, user_input_id, question, post, answer_tag,
|
||||
@@ -912,7 +962,7 @@ class survey_user_input_line(osv.osv):
|
||||
'page_id': question.page_id.id,
|
||||
'survey_id': question.survey_id.id,
|
||||
}
|
||||
if answer_tag in post:
|
||||
if answer_tag in post and post[answer_tag].strip() != '':
|
||||
vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
|
||||
else:
|
||||
vals.update({'skipped': True})
|
||||
@@ -933,7 +983,7 @@ class survey_user_input_line(osv.osv):
|
||||
'page_id': question.page_id.id,
|
||||
'survey_id': question.survey_id.id,
|
||||
}
|
||||
if answer_tag in post:
|
||||
if answer_tag in post and post[answer_tag].strip() != '':
|
||||
vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
|
||||
else:
|
||||
vals.update({'skipped': True})
|
||||
@@ -954,10 +1004,10 @@ class survey_user_input_line(osv.osv):
|
||||
'page_id': question.page_id.id,
|
||||
'survey_id': question.survey_id.id,
|
||||
}
|
||||
if answer_tag in post:
|
||||
if answer_tag in post and post[answer_tag].strip() != '':
|
||||
vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
|
||||
else:
|
||||
vals.update({'skipped': True})
|
||||
vals.update({'skipped': True, 'answer_type': None})
|
||||
old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
|
||||
('survey_id', '=', question.survey_id.id),
|
||||
('question_id', '=', question.id)],
|
||||
@@ -989,26 +1039,26 @@ class survey_user_input_line(osv.osv):
|
||||
self.create(cr, uid, vals, context=context)
|
||||
return True
|
||||
|
||||
def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
|
||||
vals = {
|
||||
'user_input_id': user_input_id,
|
||||
'question_id': question.id,
|
||||
'page_id': question.page_id.id,
|
||||
'survey_id': question.survey_id.id,
|
||||
}
|
||||
if answer_tag in post:
|
||||
vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
|
||||
else:
|
||||
vals.update({'skipped': True})
|
||||
old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
|
||||
('survey_id', '=', question.survey_id.id),
|
||||
('question_id', '=', question.id)],
|
||||
context=context)
|
||||
if old_uil:
|
||||
self.write(cr, uid, old_uil[0], vals, context=context)
|
||||
else:
|
||||
self.create(cr, uid, vals, context=context)
|
||||
return True
|
||||
# def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
|
||||
# vals = {
|
||||
# 'user_input_id': user_input_id,
|
||||
# 'question_id': question.id,
|
||||
# 'page_id': question.page_id.id,
|
||||
# 'survey_id': question.survey_id.id,
|
||||
# }
|
||||
# if answer_tag in post:
|
||||
# vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
|
||||
# else:
|
||||
# vals.update({'skipped': True})
|
||||
# old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
|
||||
# ('survey_id', '=', question.survey_id.id),
|
||||
# ('question_id', '=', question.id)],
|
||||
# context=context)
|
||||
# if old_uil:
|
||||
# self.write(cr, uid, old_uil[0], vals, context=context)
|
||||
# else:
|
||||
# self.create(cr, uid, vals, context=context)
|
||||
# return True
|
||||
|
||||
|
||||
def dict_keys_startswith(dictionary, string):
|
||||
|
||||
@@ -115,9 +115,9 @@
|
||||
</div>
|
||||
</t>
|
||||
<div class="text-center mt16 mb16">
|
||||
<button t-if="survey.users_can_go_back and page_nr > 0" type="submit" class="btn btn-default">Previous page</button>
|
||||
<button t-if="not last" type="submit" class="btn btn-primary">Next page</button>
|
||||
<button type="submit" t-if="last" class="btn btn-primary">Submit survey</button>
|
||||
<button t-if="survey.users_can_go_back and page_nr > 0" type="submit" class="btn btn-default" name="button_submit" value="previous">Previous page</button>
|
||||
<button t-if="not last" type="submit" class="btn btn-primary" name="button_submit" value="next">Next page</button>
|
||||
<button t-if="last" type="submit" class="btn btn-primary" name="button_submit" value="finish">Submit survey</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user