[UPDT] BASE: Implement history field settings feature
This commit is contained in:
56
base/templates/base/audit_tag/history_tracking_fields.html
Normal file
56
base/templates/base/audit_tag/history_tracking_fields.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<style>
|
||||||
|
.select2-selection.select2-selection--multiple {
|
||||||
|
min-height: 48px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
div[style="display: flex"] {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
div[style="display: flex"] {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form
|
||||||
|
action="{% url 'history-field-settings' %}"
|
||||||
|
class="settings-label mb-1"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="oh-inner-sidebar-content__header mt-4">
|
||||||
|
<h2 class="oh-inner-sidebar-content__title">
|
||||||
|
{% trans "Employee History Tracking" %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="oh-label__info" for="defatul_expire">
|
||||||
|
<label class="oh-label" for="defatul_expire"
|
||||||
|
>{% trans "Tracking Fields" %}</label
|
||||||
|
>
|
||||||
|
<span class="oh-info mr-2" title="{% trans '' %}"></span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex">
|
||||||
|
<div style="min-width: 273px">
|
||||||
|
{{ history_fields_form.tracking_fields }}
|
||||||
|
</div>
|
||||||
|
{% if perms.payroll.change_payrollsettings %}
|
||||||
|
<button
|
||||||
|
style="display: inline; margin-left: 5px"
|
||||||
|
type="submit"
|
||||||
|
class="oh-btn oh-btn--secondary mr-0 oh-btn--w-100-resp"
|
||||||
|
>
|
||||||
|
{% trans "Save Changes" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oh-inner-sidebar-content__footer"></div>
|
||||||
|
</form>
|
||||||
@@ -21,5 +21,5 @@
|
|||||||
{% if perms.payroll.change_encashmentgeneralsetting %}
|
{% if perms.payroll.change_encashmentgeneralsetting %}
|
||||||
{% include "settings/encashment_settings.html" %}
|
{% include "settings/encashment_settings.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "base/audit_tag/history_tracking_fields.html" %}
|
||||||
{% endblock settings %}
|
{% endblock settings %}
|
||||||
@@ -570,6 +570,7 @@ urlpatterns = [
|
|||||||
path("settings/get-date-format/", views.get_date_format, name="get-date-format"),
|
path("settings/get-date-format/", views.get_date_format, name="get-date-format"),
|
||||||
path("settings/save-time/", views.save_time_format, name="save_time_format"),
|
path("settings/save-time/", views.save_time_format, name="save_time_format"),
|
||||||
path("settings/get-time-format/", views.get_time_format, name="get-time-format"),
|
path("settings/get-time-format/", views.get_time_format, name="get-time-format"),
|
||||||
|
path("history-field-settings",views.history_field_settings,name="history-field-settings"),
|
||||||
path(
|
path(
|
||||||
"settings/attendance-settings-view/",
|
"settings/attendance-settings-view/",
|
||||||
views.validation_condition_view,
|
views.validation_condition_view,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ from attendance.models import AttendanceValidationCondition, GraceTime
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from employee.filters import EmployeeFilter
|
from employee.filters import EmployeeFilter
|
||||||
from employee.forms import ActiontypeForm
|
from employee.forms import ActiontypeForm
|
||||||
from horilla_audit.models import AuditTag
|
from horilla_audit.forms import HistoryTrackingFieldsForm
|
||||||
|
from horilla_audit.models import AuditTag, HistoryTrackingFields
|
||||||
from notifications.models import Notification
|
from notifications.models import Notification
|
||||||
from notifications.base.models import AbstractNotification
|
from notifications.base.models import AbstractNotification
|
||||||
from notifications.signals import notify
|
from notifications.signals import notify
|
||||||
@@ -3750,6 +3751,13 @@ def general_settings(request):
|
|||||||
form = AnnouncementExpireForm(instance=instance)
|
form = AnnouncementExpireForm(instance=instance)
|
||||||
encashment_instance = EncashmentGeneralSettings.objects.first()
|
encashment_instance = EncashmentGeneralSettings.objects.first()
|
||||||
encashment_form = EncashmentGeneralSettingsForm(instance=encashment_instance)
|
encashment_form = EncashmentGeneralSettingsForm(instance=encashment_instance)
|
||||||
|
history_tracking_instance = HistoryTrackingFields.objects.first()
|
||||||
|
history_fields_form_initial = {}
|
||||||
|
if history_tracking_instance:
|
||||||
|
history_fields_form_initial = {
|
||||||
|
"tracking_fields": history_tracking_instance.tracking_fields['tracking_fields']
|
||||||
|
}
|
||||||
|
history_fields_form = HistoryTrackingFieldsForm(initial = history_fields_form_initial)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = AnnouncementExpireForm(request.POST, instance=instance)
|
form = AnnouncementExpireForm(request.POST, instance=instance)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@@ -3762,6 +3770,7 @@ def general_settings(request):
|
|||||||
{
|
{
|
||||||
"form": form,
|
"form": form,
|
||||||
"encashment_form": encashment_form,
|
"encashment_form": encashment_form,
|
||||||
|
"history_fields_form":history_fields_form,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3898,6 +3907,20 @@ def get_time_format(request):
|
|||||||
# Return the date format as JSON response
|
# Return the date format as JSON response
|
||||||
return JsonResponse({"selected_format": time_format})
|
return JsonResponse({"selected_format": time_format})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def history_field_settings(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
fields = request.POST.getlist("tracking_fields")
|
||||||
|
history_object, created = HistoryTrackingFields.objects.get_or_create(
|
||||||
|
pk=1, defaults={"tracking_fields": {"tracking_fields": fields}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
history_object.tracking_fields = {"tracking_fields": fields}
|
||||||
|
history_object.save()
|
||||||
|
|
||||||
|
return redirect(general_settings)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("attendance.view_attendancevalidationcondition")
|
@permission_required("attendance.view_attendancevalidationcondition")
|
||||||
|
|||||||
@@ -434,10 +434,20 @@ class EmployeeWorkInformation(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
verbose_name=_("Company"),
|
verbose_name=_("Company"),
|
||||||
)
|
)
|
||||||
tags = models.ManyToManyField(EmployeeTag, blank=True, verbose_name=_("tags"))
|
tags = models.ManyToManyField(
|
||||||
location = models.CharField(max_length=50, blank=True)
|
EmployeeTag, blank=True, verbose_name=_("Employee tag")
|
||||||
email = models.EmailField(max_length=254, blank=True, null=True)
|
)
|
||||||
mobile = models.CharField(max_length=254, blank=True, null=True)
|
location = models.CharField(
|
||||||
|
max_length=50, blank=True, verbose_name=_("Work Location")
|
||||||
|
)
|
||||||
|
email = models.EmailField(
|
||||||
|
max_length=254, blank=True, null=True, verbose_name=_("Email")
|
||||||
|
)
|
||||||
|
mobile = models.CharField(
|
||||||
|
max_length=254,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
shift_id = models.ForeignKey(
|
shift_id = models.ForeignKey(
|
||||||
EmployeeShift,
|
EmployeeShift,
|
||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
@@ -445,10 +455,16 @@ class EmployeeWorkInformation(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Shift"),
|
verbose_name=_("Shift"),
|
||||||
)
|
)
|
||||||
date_joining = models.DateField(null=True, blank=True)
|
date_joining = models.DateField(
|
||||||
|
null=True, blank=True, verbose_name=_("Joining Date")
|
||||||
|
)
|
||||||
contract_end_date = models.DateField(blank=True, null=True)
|
contract_end_date = models.DateField(blank=True, null=True)
|
||||||
basic_salary = models.IntegerField(null=True, blank=True, default=0)
|
basic_salary = models.IntegerField(
|
||||||
salary_hour = models.IntegerField(null=True, blank=True, default=0)
|
null=True, blank=True, default=0, verbose_name=_("Basic Salary")
|
||||||
|
)
|
||||||
|
salary_hour = models.IntegerField(
|
||||||
|
null=True, blank=True, default=0, verbose_name=_("Salary Per Hour")
|
||||||
|
)
|
||||||
additional_info = models.JSONField(null=True, blank=True)
|
additional_info = models.JSONField(null=True, blank=True)
|
||||||
experience = models.FloatField(null=True, blank=True, default=0)
|
experience = models.FloatField(null=True, blank=True, default=0)
|
||||||
history = HorillaAuditLog(
|
history = HorillaAuditLog(
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ var downloadMessages = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var importSuccess = {
|
var importSuccess = {
|
||||||
ar: "نجح الاستيراد",
|
ar: "نجح الاستيراد", // Arabic
|
||||||
de: "Import erfolgreich",
|
de: "Import erfolgreich", // German
|
||||||
es: "Importado con éxito",
|
es: "Importado con éxito", // Spanish
|
||||||
en: "Imported Successfully!",
|
en: "Imported Successfully!", // English
|
||||||
fr: "Importation réussie",
|
fr: "Importation réussie", // French
|
||||||
};
|
};
|
||||||
|
|
||||||
var uploadSuccess = {
|
var uploadSuccess = {
|
||||||
ar: "تحميل كامل",
|
ar: "تحميل كامل", // Arabic
|
||||||
de: "Upload abgeschlossen",
|
de: "Upload abgeschlossen", // German
|
||||||
es: "Carga completa",
|
es: "Carga completa", // Spanish
|
||||||
en: "Upload Complete!",
|
en: "Upload Complete!", // English
|
||||||
fr: "Téléchargement terminé",
|
fr: "Téléchargement terminé", // French
|
||||||
};
|
};
|
||||||
|
|
||||||
var uploadingMessage = {
|
var uploadingMessage = {
|
||||||
@@ -55,14 +55,28 @@ function getCookie(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentLanguageCode(callback) {
|
function getCurrentLanguageCode(callback) {
|
||||||
|
var languageCode = $("#main-section-data").attr("data-lang");
|
||||||
|
var allowedLanguageCodes = ["ar", "de", "es", "en", "fr"];
|
||||||
|
if (allowedLanguageCodes.includes(languageCode)) {
|
||||||
|
callback(languageCode);
|
||||||
|
} else {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
url: "/employee/get-language-code/",
|
url: "/employee/get-language-code/",
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
var languageCode = response.language_code;
|
var ajaxLanguageCode = response.language_code;
|
||||||
callback(languageCode); // Pass the language code to the callback
|
$("#main-section-data").attr("data-lang", ajaxLanguageCode);
|
||||||
|
callback(
|
||||||
|
allowedLanguageCodes.includes(ajaxLanguageCode)
|
||||||
|
? ajaxLanguageCode
|
||||||
|
: "en"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
callback("en");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the form element
|
// Get the form element
|
||||||
@@ -112,10 +126,9 @@ form.addEventListener("submit", function (event) {
|
|||||||
$("#work-info-import").click(function (e) {
|
$("#work-info-import").click(function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var languageCode = null;
|
var languageCode = null;
|
||||||
languageCode = $("#main-section-data").attr("data-lang");
|
getCurrentLanguageCode(function (code) {
|
||||||
var confirmMessage =
|
languageCode = code;
|
||||||
downloadMessages[languageCode] ||
|
var confirmMessage = downloadMessages[languageCode];
|
||||||
((languageCode = "en"), downloadMessages[languageCode]);
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
text: confirmMessage,
|
text: confirmMessage,
|
||||||
icon: "question",
|
icon: "question",
|
||||||
@@ -137,7 +150,9 @@ $("#work-info-import").click(function (e) {
|
|||||||
$(".progress-bar")
|
$(".progress-bar")
|
||||||
.width(percent + "%")
|
.width(percent + "%")
|
||||||
.attr("aria-valuenow", percent);
|
.attr("aria-valuenow", percent);
|
||||||
$("#progress-text").text("Uploading... " + percent.toFixed(2) + "%");
|
$("#progress-text").text(
|
||||||
|
"Uploading... " + percent.toFixed(2) + "%"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,6 +177,7 @@ $("#work-info-import").click(function (e) {
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).ajaxStart(function () {
|
$(document).ajaxStart(function () {
|
||||||
@@ -173,17 +189,10 @@ $(document).ajaxStop(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function simulateProgress() {
|
function simulateProgress() {
|
||||||
var languageCode = null;
|
getCurrentLanguageCode(function (code) {
|
||||||
languageCode = $("#main-section-data").attr("data-lang");
|
languageCode = code;
|
||||||
var importMessage =
|
var importMessage = importSuccess[languageCode];
|
||||||
importSuccess[languageCode] ||
|
var uploadMessage = uploadSuccess[languageCode];
|
||||||
((languageCode = "en"), importSuccess[languageCode]);
|
|
||||||
var uploadMessage =
|
|
||||||
uploadSuccess[languageCode] ||
|
|
||||||
((languageCode = "en"), uploadSuccess[languageCode]);
|
|
||||||
var uploadingMessage =
|
|
||||||
uploadingMessage[languageCode] ||
|
|
||||||
((languageCode = "en"), uploadingMessage[languageCode]);
|
|
||||||
let progressBar = document.querySelector(".progress-bar");
|
let progressBar = document.querySelector(".progress-bar");
|
||||||
let progressText = document.getElementById("progress-text");
|
let progressText = document.getElementById("progress-text");
|
||||||
|
|
||||||
@@ -210,20 +219,20 @@ function simulateProgress() {
|
|||||||
width++;
|
width++;
|
||||||
progressBar.style.width = width + "%";
|
progressBar.style.width = width + "%";
|
||||||
progressBar.setAttribute("aria-valuenow", width);
|
progressBar.setAttribute("aria-valuenow", width);
|
||||||
progressText.innerText = uploadingMessage + width + "%";
|
progressText.innerText = uploadingMessage[languageCode] + width + "%";
|
||||||
}
|
}
|
||||||
}, 20);
|
}, 20);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("workInfoImportForm")
|
.getElementById("workInfoImportForm")
|
||||||
.addEventListener("submit", function (event) {
|
.addEventListener("submit", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var languageCode = null;
|
getCurrentLanguageCode(function (code) {
|
||||||
languageCode = $("#main-section-data").attr("data-lang");
|
languageCode = code;
|
||||||
var errorMessage =
|
var errorMessage = validationMessage[languageCode];
|
||||||
validationMessage[languageCode] ||
|
|
||||||
((languageCode = "en"), validationMessage[languageCode]);
|
|
||||||
var fileInput = $("#workInfoImportFile").val();
|
var fileInput = $("#workInfoImportFile").val();
|
||||||
var allowedExtensions = /(\.xlsx)$/i;
|
var allowedExtensions = /(\.xlsx)$/i;
|
||||||
|
|
||||||
@@ -248,3 +257,4 @@ document
|
|||||||
simulateProgress();
|
simulateProgress();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||||
<div class="oh-input-group">
|
<div class="oh-input-group">
|
||||||
<label class="oh-label">
|
<label class="oh-label">
|
||||||
<input type="checkbox" id="select-all-fields" /> {% trans "Select
|
<input type="checkbox" id="select-all-fields" /> {% trans "Select All" %}
|
||||||
All" %}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ class HistoryForm(forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_("Updation description"),
|
label=_("Updation description"),
|
||||||
)
|
)
|
||||||
history_highlight = forms.BooleanField(required=False,label=_("Updation highlight"))
|
history_highlight = forms.BooleanField(
|
||||||
|
required=False, label=_("Updation highlight")
|
||||||
|
)
|
||||||
history_tags = forms.ModelMultipleChoiceField(
|
history_tags = forms.ModelMultipleChoiceField(
|
||||||
queryset=AuditTag.objects.all(), required=False,
|
queryset=AuditTag.objects.all(), required=False, label=_("Updation tag")
|
||||||
label = _("Updation tag")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
@@ -50,3 +51,33 @@ class HistoryForm(forms.Form):
|
|||||||
context = {"form": self}
|
context = {"form": self}
|
||||||
table_html = render_to_string("horilla_audit/horilla_audit_log.html", context)
|
table_html = render_to_string("horilla_audit/horilla_audit_log.html", context)
|
||||||
return table_html
|
return table_html
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryTrackingFieldsForm(forms.Form):
|
||||||
|
excluded_fields = [
|
||||||
|
"id",
|
||||||
|
"employee_id",
|
||||||
|
"objects",
|
||||||
|
"mobile",
|
||||||
|
"contract_end_date",
|
||||||
|
"additional_info",
|
||||||
|
"experience",
|
||||||
|
]
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
from employee.models import EmployeeWorkInformation as model
|
||||||
|
super(HistoryTrackingFieldsForm, self).__init__(*args, **kwargs)
|
||||||
|
field_choices = [
|
||||||
|
(field.name, field.verbose_name)
|
||||||
|
for field in model._meta.get_fields()
|
||||||
|
if hasattr(field, "verbose_name") and field.name not in self.excluded_fields
|
||||||
|
]
|
||||||
|
self.fields["tracking_fields"] = forms.MultipleChoiceField(
|
||||||
|
choices=field_choices,
|
||||||
|
required=False,
|
||||||
|
widget=forms.SelectMultiple(
|
||||||
|
attrs={
|
||||||
|
"class": "oh-select oh-select-2 select2-hidden-accessible",
|
||||||
|
"style": "height:270px;",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -6,7 +6,6 @@ This module is used to write methods related to the history
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class Bot:
|
class Bot:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.__str__()
|
self.__str__()
|
||||||
@@ -54,6 +53,21 @@ def get_field_label(model_class, field_name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def filter_history(histories,track_fields):
|
||||||
|
filtered_histories = []
|
||||||
|
for history in histories:
|
||||||
|
changes = history.get("changes", [])
|
||||||
|
filtered_changes = [
|
||||||
|
change
|
||||||
|
for change in changes
|
||||||
|
if change.get("field_name", "") in track_fields
|
||||||
|
]
|
||||||
|
if filtered_changes:
|
||||||
|
history["changes"] = filtered_changes
|
||||||
|
filtered_histories.append(history)
|
||||||
|
histories = filtered_histories
|
||||||
|
return histories
|
||||||
|
|
||||||
def get_diff(instance):
|
def get_diff(instance):
|
||||||
"""
|
"""
|
||||||
This method is used to find the differences in the history
|
This method is used to find the differences in the history
|
||||||
@@ -117,4 +131,10 @@ def get_diff(instance):
|
|||||||
"updated_by": updated_by,
|
"updated_by": updated_by,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
from .models import HistoryTrackingFields
|
||||||
|
history_tracking_instance = HistoryTrackingFields.objects.first()
|
||||||
|
if history_tracking_instance:
|
||||||
|
track_fields = history_tracking_instance.tracking_fields["tracking_fields"]
|
||||||
|
if track_fields:
|
||||||
|
delta_changes = filter_history(delta_changes,track_fields)
|
||||||
return delta_changes
|
return delta_changes
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
models.py
|
models.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -119,6 +120,10 @@ def post_create_horilla_audit_log(sender, instance, *_args, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryTrackingFields(models.Model):
|
||||||
|
tracking_fields = models.JSONField(null=True, blank=True, editable=False)
|
||||||
|
|
||||||
|
|
||||||
# class HistoryComment(models.Model):
|
# class HistoryComment(models.Model):
|
||||||
# """
|
# """
|
||||||
# HistoryComment model
|
# HistoryComment model
|
||||||
|
|||||||
Reference in New Issue
Block a user