.
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-08-19 14:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rapids', '0028_auto_20210816_1306'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rapidimage',
|
||||||
|
name='image_annotations',
|
||||||
|
field=models.TextField(blank=True, help_text='Stores a JSON representation of annotations to be applied by cornerstonetools'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -378,6 +378,11 @@ class RapidImage(models.Model):
|
|||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
image = models.FileField(upload_to=image_directory_path)
|
image = models.FileField(upload_to=image_directory_path)
|
||||||
|
|
||||||
|
image_annotations = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
|
||||||
|
)
|
||||||
|
|
||||||
feedback_image = models.BooleanField(default=False)
|
feedback_image = models.BooleanField(default=False)
|
||||||
|
|
||||||
def image_tag(self):
|
def image_tag(self):
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>,
|
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>,
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button id="add-to-exam" data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}" data-exam_list_url="{% url 'rapid-exam-list' %}" data-type="rapid" data-csrf="{{ csrf_token}}" data-qid="{{question.pk}}">Add to exam</button>
|
<button id="add-to-exam" data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||||
|
data-exam_list_url="{% url 'rapid-exam-list' %}" data-type="rapid" data-csrf="{{ csrf_token}}"
|
||||||
|
data-qid="{{question.pk}}">Add to exam</button>
|
||||||
<span id="exam-options"></span>
|
<span id="exam-options"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
|
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
|
||||||
@@ -39,14 +41,14 @@
|
|||||||
<p><b>Checked by:</b> {% for verified in question.verified.all %} <a
|
<p><b>Checked by:</b> {% for verified in question.verified.all %} <a
|
||||||
href="{% url 'rapids:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
href="{% url 'rapids:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
||||||
<p><b>Scrapped:</b> {{ question.scrapped }} <a href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a>
|
<p><b>Scrapped:</b> {{ question.scrapped }} <a href="{% url 'rapids:rapid_scrap' pk=question.pk %}">(toggle)</a>
|
||||||
<p class="pre-whitespace">
|
<p class="pre-whitespace">
|
||||||
Answers (score): {% for answer in question.answers.all %}
|
Answers (score): {% for answer in question.answers.all %}
|
||||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid" {%
|
||||||
{% endif %}>
|
endif %}>
|
||||||
{{ answer }} ({{answer.status}}),
|
{{ answer }} ({{answer.status}}),
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
@@ -89,28 +91,65 @@
|
|||||||
</summary>
|
</summary>
|
||||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_urls }}" data-annotations=''>
|
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_urls }}" data-annotations=''>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="save-annotations">Save Annotations</button>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
// send request to change the is_private state on customSwitches toggle
|
||||||
|
$("#save-annotations").click(function () {
|
||||||
|
json = getJsonToolStateNoId();
|
||||||
|
|
||||||
|
console.log(json);
|
||||||
|
return
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'rapids:question_save_annotation' pk=question.pk %}",
|
||||||
|
data: {
|
||||||
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
|
annotation: json,
|
||||||
|
},
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
error: function (e) {
|
||||||
|
toastr.warning(`Error saving annotations`)
|
||||||
|
console.log(e);
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
|
.done(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
// show some message according to the response.
|
||||||
|
// For eg. A message box showing that the status has been changed
|
||||||
|
if (data.status == "success") {
|
||||||
|
toastr.info('Annotations saved')
|
||||||
|
} else {
|
||||||
|
toastr.warning('Error saving annotations')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
console.log('[Done]');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
$("#add-to-exam").click(function (evt) {
|
$("#add-to-exam").click(function (evt) {
|
||||||
|
|
||||||
var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) {
|
var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
$("#exam-options").empty();
|
$("#exam-options").empty();
|
||||||
data.forEach((obj, n) => {
|
data.forEach((obj, n) => {
|
||||||
$("#exam-options").append($(document.createElement('button')).prop({
|
$("#exam-options").append($(document.createElement('button')).prop({
|
||||||
type: 'button',
|
type: 'button',
|
||||||
innerHTML: obj.name,
|
innerHTML: obj.name,
|
||||||
class: '',
|
class: '',
|
||||||
|
|
||||||
}).data("exam-id", obj.id).click((evt) => {
|
}).data("exam-id", obj.id).click((evt) => {
|
||||||
addToExam(evt)
|
addToExam(evt)
|
||||||
}))
|
}))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
.done(function () {
|
.done(function () {
|
||||||
//alert( "second success" );
|
//alert( "second success" );
|
||||||
})
|
})
|
||||||
@@ -139,16 +178,16 @@
|
|||||||
let type = exam_button.dataset.type;
|
let type = exam_button.dataset.type;
|
||||||
let csrf = exam_button.dataset.csrf;
|
let csrf = exam_button.dataset.csrf;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: post_url,
|
url: post_url,
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: csrf,
|
csrfmiddlewaretoken: csrf,
|
||||||
add_exam_questions: JSON.stringify(ids),
|
add_exam_questions: JSON.stringify(ids),
|
||||||
type: type,
|
type: type,
|
||||||
exam_id: $(evt.target).data("exam-id"),
|
exam_id: $(evt.target).data("exam-id"),
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
@@ -170,18 +209,41 @@
|
|||||||
/* beautify ignore:end */
|
/* beautify ignore:end */
|
||||||
|
|
||||||
$("#toggle-normal-button").click(function () {
|
$("#toggle-normal-button").click(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'rapid-detail' question.id %}",
|
||||||
|
type: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
"X-CSRFToken": "{{ csrf_token }}"
|
||||||
|
},
|
||||||
|
timeout: 3000,
|
||||||
|
data: {
|
||||||
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
|
normal: n,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.done(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
toastr.info('Answer updated')
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
alert('Error updating this model instance.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".toggle-laterality-button").each((n, el) => {
|
||||||
|
$(el).click(function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{% url 'rapid-detail' question.id %}",
|
url: "{% url 'rapid-detail' question.id %}",
|
||||||
type: 'PATCH',
|
type: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
"X-CSRFToken": "{{ csrf_token }}"
|
"X-CSRFToken": "{{ csrf_token }}"
|
||||||
},
|
},
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
normal: n,
|
laterality: el.dataset.lat,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
toastr.info('Answer updated')
|
toastr.info('Answer updated')
|
||||||
@@ -190,130 +252,125 @@
|
|||||||
alert('Error updating this model instance.');
|
alert('Error updating this model instance.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".toggle-laterality-button").each((n, el) => {
|
|
||||||
$(el).click(function () {
|
|
||||||
$.ajax({
|
|
||||||
url: "{% url 'rapid-detail' question.id %}",
|
|
||||||
type: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
"X-CSRFToken": "{{ csrf_token }}"
|
|
||||||
},
|
|
||||||
timeout: 3000,
|
|
||||||
data: {
|
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
|
||||||
laterality: el.dataset.lat,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.done(function (data) {
|
|
||||||
console.log(data);
|
|
||||||
toastr.info('Answer updated')
|
|
||||||
})
|
|
||||||
.fail(function () {
|
|
||||||
alert('Error updating this model instance.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".suggested_answers li").each((n, el) => {
|
|
||||||
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
|
||||||
$.ajax({
|
|
||||||
url: "{% url 'answer_submit' %}",
|
|
||||||
data: {
|
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
|
||||||
//active: this.checked // true if checked else false
|
|
||||||
question_type: "rapid",
|
|
||||||
qid: "{{question.pk}}",
|
|
||||||
status: 2,
|
|
||||||
answer: el.dataset.string,
|
|
||||||
},
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
})
|
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
|
||||||
.done(function (data) {
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
toastr.info('Answer saved')
|
|
||||||
$(el).find(".correct").remove()
|
|
||||||
}
|
|
||||||
// show some message according to the response.
|
|
||||||
// For eg. A message box showing that the status has been changed
|
|
||||||
})
|
|
||||||
.always(function () {
|
|
||||||
console.log('[Done]');
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// send request to change the is_private state on customSwitches toggle
|
|
||||||
$(".proposed-answer").each((n, el) => {
|
|
||||||
|
|
||||||
// Add button to confirm answer is correct
|
|
||||||
$(el).append($("<span class='confirm'>[Add Correct]</span>").on("click", function () {
|
|
||||||
$.ajax({
|
|
||||||
url: "{% url 'answer_suggestion_confirm' %}",
|
|
||||||
data: {
|
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
|
||||||
//active: this.checked // true if checked else false
|
|
||||||
question_type: "rapid",
|
|
||||||
aid: el.dataset.aid,
|
|
||||||
status: 2,
|
|
||||||
},
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
})
|
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
|
||||||
.done(function (data) {
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
toastr.info('Answer saved')
|
|
||||||
$(el).find(".confirm").remove()
|
|
||||||
$(el).removeClass("proposed-answer")
|
|
||||||
}
|
|
||||||
// show some message according to the response.
|
|
||||||
// For eg. A message box showing that the status has been changed
|
|
||||||
})
|
|
||||||
.always(function () {
|
|
||||||
console.log('[Done]');
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Add button to confirm answer is incorrect
|
|
||||||
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
|
||||||
$.ajax({
|
|
||||||
url: "{% url 'answer_suggestion_confirm' %}",
|
|
||||||
data: {
|
|
||||||
csrfmiddlewaretoken: "{{ csrf_token }}",
|
|
||||||
//active: this.checked // true if checked else false
|
|
||||||
question_type: "rapid",
|
|
||||||
aid: el.dataset.aid,
|
|
||||||
status: 0,
|
|
||||||
},
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
})
|
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
|
||||||
.done(function (data) {
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
toastr.info('Answer saved')
|
|
||||||
$(el).find(".confirm").remove()
|
|
||||||
$(el).removeClass("proposed-answer")
|
|
||||||
}
|
|
||||||
// show some message according to the response.
|
|
||||||
// For eg. A message box showing that the status has been changed
|
|
||||||
})
|
|
||||||
.always(function () {
|
|
||||||
console.log('[Done]');
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".suggested_answers li").each((n, el) => {
|
||||||
|
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'answer_submit' %}",
|
||||||
|
data: {
|
||||||
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
|
//active: this.checked // true if checked else false
|
||||||
|
question_type: "rapid",
|
||||||
|
qid: "{{question.pk}}",
|
||||||
|
status: 2,
|
||||||
|
answer: el.dataset.string,
|
||||||
|
},
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
})
|
||||||
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
|
.done(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toastr.info('Answer saved')
|
||||||
|
$(el).find(".correct").remove()
|
||||||
|
}
|
||||||
|
// show some message according to the response.
|
||||||
|
// For eg. A message box showing that the status has been changed
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
console.log('[Done]');
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// send request to change the is_private state on customSwitches toggle
|
||||||
|
$(".proposed-answer").each((n, el) => {
|
||||||
|
|
||||||
|
// Add button to confirm answer is correct
|
||||||
|
$(el).append($("<span class='confirm'>[Add Correct]</span>").on("click", function () {
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'answer_suggestion_confirm' %}",
|
||||||
|
data: {
|
||||||
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
|
//active: this.checked // true if checked else false
|
||||||
|
question_type: "rapid",
|
||||||
|
aid: el.dataset.aid,
|
||||||
|
status: 2,
|
||||||
|
},
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
})
|
||||||
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
|
.done(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toastr.info('Answer saved')
|
||||||
|
$(el).find(".confirm").remove()
|
||||||
|
$(el).removeClass("proposed-answer")
|
||||||
|
}
|
||||||
|
// show some message according to the response.
|
||||||
|
// For eg. A message box showing that the status has been changed
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
console.log('[Done]');
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Add button to confirm answer is incorrect
|
||||||
|
$(el).append($("<span class='confirm'>[Incorrect]</span>").on("click", function () {
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'answer_suggestion_confirm' %}",
|
||||||
|
data: {
|
||||||
|
csrfmiddlewaretoken: "{{ csrf_token }}",
|
||||||
|
//active: this.checked // true if checked else false
|
||||||
|
question_type: "rapid",
|
||||||
|
aid: el.dataset.aid,
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
})
|
||||||
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
|
.done(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toastr.info('Answer saved')
|
||||||
|
$(el).find(".confirm").remove()
|
||||||
|
$(el).removeClass("proposed-answer")
|
||||||
|
}
|
||||||
|
// show some message according to the response.
|
||||||
|
// For eg. A message box showing that the status has been changed
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
console.log('[Done]');
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getJsonToolStateNoId() {
|
||||||
|
const el = document.getElementById("dicom-image");
|
||||||
|
|
||||||
|
const c = cornerstone.getEnabledElement(el);
|
||||||
|
|
||||||
|
const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
|
const image_id = c.image.imageId;
|
||||||
|
|
||||||
|
const state_with_id = toolStateManager.saveToolState()
|
||||||
|
|
||||||
|
const json_tool_state = JSON.stringify(state_with_id[image_id]);
|
||||||
|
|
||||||
|
return json_tool_state;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.toggle-button {
|
.toggle-button {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"question/<int:pk>/", views.question_detail, name="question_detail"
|
"question/<int:pk>/", views.question_detail, name="question_detail"
|
||||||
), # path("question/<int:pk>/json", views.question_json, name="question_json"),
|
), # path("question/<int:pk>/json", views.question_json, name="question_json"),
|
||||||
|
path("question/<int:pk>/save_annotation", views.question_save_annotation, name="question_save_annotation"),
|
||||||
path("question/<int:pk>/split", views.rapid_split, name="rapid_split"),
|
path("question/<int:pk>/split", views.rapid_split, name="rapid_split"),
|
||||||
path("question/<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"),
|
path("question/<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"),
|
||||||
# path("verified/", views.verified, name="verified"),
|
# path("verified/", views.verified, name="verified"),
|
||||||
|
|||||||
+17
-1
@@ -1149,4 +1149,20 @@ def question_review(request, pk):
|
|||||||
|
|
||||||
question_json = question.get_question_json(based=False)
|
question_json = question.get_question_json(based=False)
|
||||||
return JsonResponse(question_json)
|
return JsonResponse(question_json)
|
||||||
return JsonResponse({"status": "error"})
|
return JsonResponse({"status": "error"})
|
||||||
|
|
||||||
|
@user_is_author_or_rapid_checker
|
||||||
|
def question_save_annotation(request, pk):
|
||||||
|
if request.is_ajax() and request.method == "POST":
|
||||||
|
question = get_object_or_404(Rapid, pk=pk)
|
||||||
|
|
||||||
|
question.image_annotations = request.POST.get("annotation")
|
||||||
|
print(question.image_annotations)
|
||||||
|
|
||||||
|
question.save()
|
||||||
|
data = {"status": "success"}
|
||||||
|
return JsonResponse(data, status=200)
|
||||||
|
else:
|
||||||
|
data = {"status": "error"}
|
||||||
|
return JsonResponse(data, status=400)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user