Add "Lock note" feature

This commit is contained in:
Yohann D'ANELLO 2020-08-31 20:15:48 +02:00
parent 0c753c3288
commit 5e65e2d74a
11 changed files with 279 additions and 81 deletions

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="row mt-4"> <div class="row mt-4">
<div class="col-xl-4"> <div class="col-xl-4">
{% block profile_info %} {% block profile_info %}
<div class="card bg-light"> <div class="card bg-light" id="card-infos">
<h4 class="card-header text-center"> <h4 class="card-header text-center">
{% if user_object %} {% if user_object %}
{% trans "Account #" %}{{ user_object.pk }} {% trans "Account #" %}{{ user_object.pk }}
@ -31,6 +31,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a> </a>
{% endif %} {% endif %}
</div> </div>
{% if note.inactivity_reason %}
<div class="alert alert-danger polymorphic-add-choice">
{{ note.get_inactivity_reason_display }}
</div>
{% endif %}
<div class="card-body" id="profile_infos"> <div class="card-body" id="profile_infos">
{% if user_object %} {% if user_object %}
{% include "member/includes/profile_info.html" %} {% include "member/includes/profile_info.html" %}
@ -63,6 +68,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="btn btn-sm btn-primary" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-sm btn-primary" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if can_lock_note %}
<button class="btn btn-sm btn-danger" data-toggle="modal" data-target="#lock-note-modal">
<i class="fas fa-ban"></i> {% trans 'Lock note' %}
</button>
{% elif can_unlock_note %}
<button class="btn btn-sm btn-success" data-toggle="modal" data-target="#unlock-note-modal">
<i class="fas fa-check-circle"></i> {% trans 'Unlock note' %}
</button>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@ -70,16 +84,102 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="col-xl-8"> <div class="col-xl-8">
{% block profile_content %}{% endblock %} {% block profile_content %}{% endblock %}
</div> </div>
{# Popup to confirm the action of locking the note. Managed by a button #}
<div class="modal fade" id="lock-note-modal" tabindex="-1" role="dialog" aria-labelledby="lockNote"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="lockNote">{% trans "Lock note" %}</h5>
<button type="button" class="close btn-modal" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% blocktrans trimmed %}
Are you sure you want to lock this note? This will prevent any transaction that would be performed,
until the note is unlocked.
{% endblocktrans %}
{% if can_force_lock %}
{% blocktrans trimmed %}
If you use the force mode, the user won't be able to unlock the note by itself.
{% endblocktrans %}
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-modal" data-dismiss="modal">{% trans "Close" %}</button>
{% if can_force_lock %}
<button type="button" class="btn btn-danger btn-modal" onclick="lock_note(true, 'forced')">{% trans "Force mode" %}</button>
{% endif %}
<button type="button" class="btn btn-warning btn-modal" onclick="lock_note(true, 'manual')">{% trans "Lock note" %}</button>
</div>
</div>
</div>
</div>
{# Popup to confirm the action of unlocking the note. Managed by a button #}
<div class="modal fade" id="unlock-note-modal" tabindex="-1" role="dialog" aria-labelledby="unlockNote"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unlockNote">{% trans "Unlock note" %}</h5>
<button type="button" class="close btn-modal" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% blocktrans trimmed %}
Are you sure you want to unlock this note? Transactions will be re-enabled.
{% endblocktrans %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-modal" data-dismiss="modal">{% trans "Close" %}</button>
<button type="button" class="btn btn-success btn-modal" onclick="lock_note(false, null)">{% trans "Unlock note" %}</button>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
{% if object %}
<script> <script>
function refreshHistory() { function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); {% if user_object %}
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); $("#history_list").load("{% url 'member:user_detail' pk=user_object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=user_object.pk %} #profile_infos");
{% else %}
$("#history_list").load("{% url 'member:club_detail' pk=club.pk %} #history_list");
$("#profile_infos").load("{% url 'member:club_detail' pk=club.pk %} #profile_infos");
{% endif %}
}
function lock_note(locked, mode) {
$("button.btn-modal").attr("disabled", "disabled");
$.ajax({
url: "/api/note/note/{{ note.pk }}/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
is_active: !locked,
inactivity_reason: mode,
resourcetype: "{% if user_object %}NoteUser{% else %}NoteClub{% endif %}"
}
}).done(function () {
$("#card-infos").load("#card-infos #card-infos", function () {
$(".modal").modal("hide");
$("button.btn-modal").removeAttr("disabled");
});
}).fail(function(xhr, textStatus, error) {
$(".modal").modal("hide");
$("button.btn-modal").removeAttr("disabled");
errMsg(xhr.responseJSON);
});
} }
</script> </script>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -46,12 +46,3 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos");
}
</script>
{% endblock %}

View File

@ -37,12 +37,3 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=user_object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=user_object.pk %} #profile_infos");
}
</script>
{% endblock %}

View File

@ -10,6 +10,7 @@ from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.db import transaction
from django.db.models import Q, F from django.db.models import Q, F
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -151,6 +152,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['user_object'] user = context['user_object']
context["note"] = user.note
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\ Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
.order_by("-created_at")\ .order_by("-created_at")\
@ -164,6 +166,28 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
membership_table = MembershipTable(data=club_list, prefix='membership-') membership_table = MembershipTable(data=club_list, prefix='membership-')
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1)) membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
context['club_list'] = membership_table context['club_list'] = membership_table
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteUser.objects.get(pk=user.note.pk)
modified_note.is_active = True
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_noteuser_is_active",
modified_note)
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
old_note._force_save = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
return context return context
@ -203,7 +227,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
else: else:
qs = qs.none() qs = qs.none()
return qs[:20] return qs
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):

View File

@ -1,7 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import PolymorphicSerializer from rest_polymorphic.serializers import PolymorphicSerializer
from member.api.serializers import MembershipSerializer from member.api.serializers import MembershipSerializer
from member.models import Membership from member.models import Membership
@ -23,7 +25,7 @@ class NoteSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Note model = Note
fields = '__all__' fields = '__all__'
read_only_fields = [f.name for f in model._meta.get_fields()] # Notes are read-only protected read_only_fields = ('balance', 'last_negative', 'created_at', ) # Note balances are read-only protected
class NoteClubSerializer(serializers.ModelSerializer): class NoteClubSerializer(serializers.ModelSerializer):
@ -36,7 +38,7 @@ class NoteClubSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = NoteClub model = NoteClub
fields = '__all__' fields = '__all__'
read_only_fields = ('note', 'club', ) read_only_fields = ('note', 'club', 'balance', 'last_negative', 'created_at', )
def get_name(self, obj): def get_name(self, obj):
return str(obj) return str(obj)
@ -52,7 +54,7 @@ class NoteSpecialSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = NoteSpecial model = NoteSpecial
fields = '__all__' fields = '__all__'
read_only_fields = ('note', ) read_only_fields = ('note', 'balance', 'last_negative', 'created_at', )
def get_name(self, obj): def get_name(self, obj):
return str(obj) return str(obj)
@ -68,7 +70,7 @@ class NoteUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = NoteUser model = NoteUser
fields = '__all__' fields = '__all__'
read_only_fields = ('note', 'user', ) read_only_fields = ('note', 'user', 'balance', 'last_negative', 'created_at', )
def get_name(self, obj): def get_name(self, obj):
return str(obj) return str(obj)
@ -170,13 +172,22 @@ class TransactionSerializer(serializers.ModelSerializer):
REST API Serializer for Transactions. REST API Serializer for Transactions.
The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API. The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API.
""" """
def validate_source(self, value):
if value.is_active:
raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active."))
def validate_destination(self, value):
if value.is_active:
raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active."))
class Meta: class Meta:
model = Transaction model = Transaction
fields = '__all__' fields = '__all__'
class RecurrentTransactionSerializer(serializers.ModelSerializer): class RecurrentTransactionSerializer(TransactionSerializer):
""" """
REST API Serializer for Transactions. REST API Serializer for Transactions.
The djangorestframework plugin will analyse the model `RecurrentTransaction` and parse all fields in the API. The djangorestframework plugin will analyse the model `RecurrentTransaction` and parse all fields in the API.
@ -187,7 +198,7 @@ class RecurrentTransactionSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class MembershipTransactionSerializer(serializers.ModelSerializer): class MembershipTransactionSerializer(TransactionSerializer):
""" """
REST API Serializer for Membership transactions. REST API Serializer for Membership transactions.
The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API. The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API.
@ -198,7 +209,7 @@ class MembershipTransactionSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class SpecialTransactionSerializer(serializers.ModelSerializer): class SpecialTransactionSerializer(TransactionSerializer):
""" """
REST API Serializer for Special transactions. REST API Serializer for Special transactions.
The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API. The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API.

View File

@ -18,7 +18,7 @@ from ..models.notes import Note, Alias
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet): class NotePolymorphicViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
@ -36,25 +36,16 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
Parse query and apply filters. Parse query and apply filters.
:return: The filtered set of requested notes :return: The filtered set of requested notes
""" """
queryset = super().get_queryset() queryset = super().get_queryset().distinct()
alias = self.request.query_params.get("alias", ".*") alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter( queryset = queryset.filter(
name__iregex="^" + alias Q(alias__name__iregex="^" + alias)
).union( | Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
queryset.filter( | Q(alias__normalized_name__iregex="^" + alias.lower())
Q(normalized_name__iregex="^" + Alias.normalize(alias)) )
& ~Q(name__iregex="^" + alias)
),
all=True).union(
queryset.filter(
Q(normalized_name__iregex="^" + alias.lower())
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True)
return queryset.distinct() return queryset
class AliasViewSet(ReadProtectedModelViewSet): class AliasViewSet(ReadProtectedModelViewSet):

View File

@ -25,24 +25,20 @@ class Note(PolymorphicModel):
A Note can be searched find throught an :model:`note.Alias` A Note can be searched find throught an :model:`note.Alias`
""" """
balance = models.BigIntegerField( balance = models.BigIntegerField(
verbose_name=_('account balance'), verbose_name=_('account balance'),
help_text=_('in centimes, money credited for this instance'), help_text=_('in centimes, money credited for this instance'),
default=0, default=0,
) )
last_negative = models.DateTimeField( last_negative = models.DateTimeField(
verbose_name=_('last negative date'), verbose_name=_('last negative date'),
help_text=_('last time the balance was negative'), help_text=_('last time the balance was negative'),
null=True, null=True,
blank=True, blank=True,
) )
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this note should be treated as active. '
'Unselect this instead of deleting notes.'),
)
display_image = models.ImageField( display_image = models.ImageField(
verbose_name=_('display image'), verbose_name=_('display image'),
max_length=255, max_length=255,
@ -51,11 +47,31 @@ class Note(PolymorphicModel):
upload_to='pic/', upload_to='pic/',
default='pic/default.png' default='pic/default.png'
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
verbose_name=_('created at'), verbose_name=_('created at'),
default=timezone.now, default=timezone.now,
) )
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this note should be treated as active. '
'Unselect this instead of deleting notes.'),
)
inactivity_reason = models.CharField(
max_length=255,
choices=[
('manual', _("The user blocked his/her note manually, eg. when he/she left the school for holidays. "
"It can be reactivated at any time.")),
('forced', _("The note is blocked by the the BDE and can't be manually reactivated.")),
],
null=True,
default=None,
)
class Meta: class Meta:
verbose_name = _("note") verbose_name = _("note")
verbose_name_plural = _("notes") verbose_name_plural = _("notes")
@ -141,8 +157,12 @@ class NoteUser(Note):
old_note = NoteUser.objects.get(pk=self.pk) old_note = NoteUser.objects.get(pk=self.pk)
if old_note.balance >= 0: if old_note.balance >= 0:
# Passage en négatif # Passage en négatif
super().save(*args, **kwargs)
self.last_negative = timezone.now() self.last_negative = timezone.now()
self._force_save = True
self.save(*args, **kwargs)
self.send_mail_negative_balance() self.send_mail_negative_balance()
else:
super().save(*args, **kwargs) super().save(*args, **kwargs)
def send_mail_negative_balance(self): def send_mail_negative_balance(self):
@ -178,7 +198,10 @@ class NoteClub(Note):
old_note = NoteClub.objects.get(pk=self.pk) old_note = NoteClub.objects.get(pk=self.pk)
if old_note.balance >= 0: if old_note.balance >= 0:
# Passage en négatif # Passage en négatif
super().save(*args, **kwargs)
self.last_negative = timezone.now() self.last_negative = timezone.now()
self._force_save = True
self.save(*args, **kwargs)
self.send_mail_negative_balance() self.send_mail_negative_balance()
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -221,8 +221,6 @@ class Transaction(PolymorphicModel):
diff_source, diff_dest = self.validate() diff_source, diff_dest = self.validate()
if not self.source.is_active or not self.destination.is_active: if not self.source.is_active or not self.destination.is_active:
if 'force_insert' not in kwargs or not kwargs['force_insert']:
if 'force_update' not in kwargs or not kwargs['force_update']:
raise ValidationError(_("The transaction can't be saved since the source note " raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active.")) "or the destination note is not active."))

View File

@ -55,17 +55,19 @@ class HistoryTable(tables.Table):
"id": lambda record: "validate_" + str(record.id), "id": lambda record: "validate_" + str(record.id),
"class": lambda record: "class": lambda record:
str(record.valid).lower() str(record.valid).lower()
+ (' validate' if PermissionBackend.check_perm(get_current_authenticated_user(), + (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
"note.change_transaction_invalidity_reason", .check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
record) else ''), else ''),
"data-toggle": "tooltip", "data-toggle": "tooltip",
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate")) "title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
if PermissionBackend.check_perm(get_current_authenticated_user(), if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) else None, "note.change_transaction_invalidity_reason", record)
and record.source.is_active and record.destination.is_active else None,
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
+ ', "' + str(record.__class__.__name__) + '")' + ', "' + str(record.__class__.__name__) + '")'
if PermissionBackend.check_perm(get_current_authenticated_user(), if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) else None, "note.change_transaction_invalidity_reason", record)
and record.source.is_active and record.destination.is_active else None,
"onmouseover": lambda record: '$("#invalidity_reason_' "onmouseover": lambda record: '$("#invalidity_reason_'
+ str(record.id) + '").show();$("#invalidity_reason_' + str(record.id) + '").show();$("#invalidity_reason_'
+ str(record.id) + '").focus();', + str(record.id) + '").focus();',
@ -108,7 +110,7 @@ class HistoryTable(tables.Table):
val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \ val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
+ "' value='" + (html.escape(record.invalidity_reason) + "' value='" + (html.escape(record.invalidity_reason)
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \ if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
+ "'" + ("" if value and has_perm else " disabled") \ + "'" + ("" if value and record.source.is_active and record.destination.is_active else " disabled") \
+ " placeholder='" + html.escape(_("invalidity reason").capitalize()) + "'" \ + " placeholder='" + html.escape(_("invalidity reason").capitalize()) + "'" \
+ " style='position: absolute; width: 15em; margin-left: -15.5em; margin-top: -2em; display: none;'>" + " style='position: absolute; width: 15em; margin-left: -15.5em; margin-top: -2em; display: none;'>"
return format_html(val) return format_html(val)
@ -135,16 +137,10 @@ class AliasTable(tables.Table):
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs= attrs={'td': {'class': lambda record: 'col-sm-1' + (
{'td': ' d-none' if not PermissionBackend.check_perm(
{'class': get_current_authenticated_user(), "note.delete_alias",
lambda record: 'col-sm-1' record) else '')}}, verbose_name=_("Delete"), )
+ (' d-none' if not PermissionBackend
.check_perm(get_current_authenticated_user(),
"note.delete_alias", record) else '')
}
},
verbose_name=_("Delete"), )
class ButtonTable(tables.Table): class ButtonTable(tables.Table):

View File

@ -2503,6 +2503,70 @@
"description": "Supprimer ses propres invitations non validées à une activité" "description": "Supprimer ses propres invitations non validées à une activité"
} }
}, },
{
"model": "permission.permission",
"pk": 161,
"fields": {
"model": [
"note",
"noteuser"
],
"query": "[\"AND\", {\"user\": [\"user\"]}, [\"OR\", {\"inactivity_reason\": \"manual\"}, {\"inactivity_reason\": null}]]",
"type": "change",
"mask": 1,
"field": "is_active",
"permanent": true,
"description": "(Dé)bloquer sa propre note manuellement"
}
},
{
"model": "permission.permission",
"pk": 162,
"fields": {
"model": [
"note",
"noteuser"
],
"query": "[\"AND\", {\"user\": [\"user\"]}, [\"OR\", {\"inactivity_reason\": \"manual\"}, {\"inactivity_reason\": null}]]",
"type": "change",
"mask": 1,
"field": "inactivity_reason",
"permanent": true,
"description": "(Dé)bloquer sa propre note et indiquer que cela a été fait manuellement"
}
},
{
"model": "permission.permission",
"pk": 163,
"fields": {
"model": [
"note",
"note"
],
"query": "{}",
"type": "change",
"mask": 3,
"field": "is_active",
"permanent": false,
"description": "(Dé)bloquer n'importe quelle note, y compris en mode forcé"
}
},
{
"model": "permission.permission",
"pk": 164,
"fields": {
"model": [
"note",
"note"
],
"query": "{}",
"type": "change",
"mask": 3,
"field": "inactivity_reason",
"permanent": false,
"description": "(Dé)bloquer sa propre note et modifier la raison"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -2525,7 +2589,9 @@
22, 22,
48, 48,
52, 52,
126 126,
161,
162
] ]
} }
}, },
@ -2695,7 +2761,9 @@
146, 146,
147, 147,
150, 150,
151 151,
163,
164
] ]
} }
}, },
@ -2859,7 +2927,12 @@
156, 156,
157, 157,
158, 158,
159 159,
160,
161,
162,
163,
164
] ]
} }
}, },

View File

@ -107,7 +107,7 @@ function displayStyle (note) {
css += " text-warning"; css += " text-warning";
else if (!note.email_confirmed) else if (!note.email_confirmed)
css += " text-white bg-primary"; css += " text-white bg-primary";
else if (note.membership && note.membership.date_end < new Date().toISOString()) else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString()))
css += "text-white bg-info"; css += "text-white bg-info";
return css; return css;
} }