mirror of
https://github.com/ae-utbm/sith.git
synced 2026-06-04 23:29:24 +00:00
add tests
This commit is contained in:
+10
-1
@@ -5,7 +5,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from ordered_model.models import OrderedModel
|
from ordered_model.models import OrderedModel
|
||||||
|
|
||||||
from club.models import Club, ClubRole
|
from club.models import Club, ClubRole, Membership
|
||||||
from core.models import Group, User
|
from core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
@@ -101,6 +101,15 @@ class Election(models.Model):
|
|||||||
results[role.title] = role.results(total_vote)
|
results[role.title] = role.results(total_vote)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def results_applied(self) -> bool:
|
||||||
|
"""Returns True if one or more roles of this election have been applied."""
|
||||||
|
return Membership.objects.filter(
|
||||||
|
role__election_roles__election=self,
|
||||||
|
end_date=None,
|
||||||
|
start_date__gte=self.end_date,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class Role(OrderedModel):
|
class Role(OrderedModel):
|
||||||
"""This class allows to create a new role available for a candidature."""
|
"""This class allows to create a new role available for a candidature."""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
was linked to a club role.
|
was linked to a club role.
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
{% elif already_applied %}
|
{% elif form.election.results_applied %}
|
||||||
<em>
|
<em>
|
||||||
{%- trans trimmed -%}
|
{%- trans trimmed -%}
|
||||||
The results of this election have been applied
|
The results of this election have been applied
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import itertools
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import localdate, now
|
||||||
|
from model_bakery import baker, seq
|
||||||
|
from model_bakery.recipe import Recipe
|
||||||
|
from pytest_django.asserts import assertRedirects
|
||||||
|
|
||||||
|
from club.models import Club, ClubRole, Membership
|
||||||
|
from core.baker_recipes import subscriber_user
|
||||||
|
from core.models import Group, User
|
||||||
|
from election.models import Candidature, Election, ElectionList, Role, Vote
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplyResult(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
# setup is a little bit complicated, but we have to make a whole
|
||||||
|
# election to test result application, including the election,
|
||||||
|
# the lists, the roles, the candidates and the votes.
|
||||||
|
cls.club = baker.make(Club)
|
||||||
|
cls.club_roles = baker.make(
|
||||||
|
ClubRole,
|
||||||
|
club=cls.club,
|
||||||
|
is_presidency=iter([True, False, False]),
|
||||||
|
is_board=True,
|
||||||
|
_quantity=3,
|
||||||
|
_bulk_create=True,
|
||||||
|
)
|
||||||
|
cls.election = baker.make(
|
||||||
|
Election,
|
||||||
|
clubs=[cls.club],
|
||||||
|
edit_groups=[baker.make(Group)],
|
||||||
|
end_date=now() - timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
lists = baker.make(
|
||||||
|
ElectionList, election=cls.election, _quantity=2, _bulk_create=True
|
||||||
|
)
|
||||||
|
role_recipe = Recipe(Role, election=cls.election, title=seq("election role "))
|
||||||
|
roles = [
|
||||||
|
*role_recipe.make(
|
||||||
|
club_role=iter(cls.club_roles), _quantity=len(cls.club_roles)
|
||||||
|
),
|
||||||
|
role_recipe.make(),
|
||||||
|
]
|
||||||
|
roles[1].max_choice = 2
|
||||||
|
roles[1].save()
|
||||||
|
cls.candidatures = baker.make(
|
||||||
|
Candidature,
|
||||||
|
election_list=itertools.chain(
|
||||||
|
itertools.repeat(lists[0], len(roles)),
|
||||||
|
itertools.repeat(lists[1], len(roles)),
|
||||||
|
),
|
||||||
|
role=itertools.cycle(roles),
|
||||||
|
user=iter(
|
||||||
|
baker.make(
|
||||||
|
User, username=seq("user "), _quantity=len(lists) * len(roles)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
_quantity=len(lists) * len(roles),
|
||||||
|
_bulk_create=True,
|
||||||
|
)
|
||||||
|
votes = iter(
|
||||||
|
baker.make(
|
||||||
|
Vote,
|
||||||
|
role=itertools.cycle(roles),
|
||||||
|
_quantity=6 * len(roles),
|
||||||
|
_bulk_create=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
through = []
|
||||||
|
for cand in cls.candidatures:
|
||||||
|
nb_voices = 4 if cand.election_list_id == lists[0].id else 2
|
||||||
|
through.extend(
|
||||||
|
[
|
||||||
|
Vote.candidature.through(candidature=cand, vote=v)
|
||||||
|
for v in itertools.islice(votes, nb_voices)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
Vote.candidature.through.objects.bulk_create(through)
|
||||||
|
cls.election.voters.set(baker.make(User, _quantity=8, _bulk_create=True))
|
||||||
|
cls.url = reverse(
|
||||||
|
"election:apply_result", kwargs={"election_id": cls.election.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_election_result(self):
|
||||||
|
# we have made a complex setup, so testing the results is
|
||||||
|
# useful to be sure we didn't make mistake when generating data
|
||||||
|
assert self.election.results == {
|
||||||
|
"election role 1": {
|
||||||
|
"blank vote": {"percent": 25.0, "vote": 2},
|
||||||
|
"total vote": 8,
|
||||||
|
"user 1": {"percent": 50.0, "vote": 4},
|
||||||
|
"user 5": {"percent": 25.0, "vote": 2},
|
||||||
|
},
|
||||||
|
"election role 2": {
|
||||||
|
"blank vote": {"percent": 62.5, "vote": 10},
|
||||||
|
"total vote": 16,
|
||||||
|
"user 2": {"percent": 25.0, "vote": 4},
|
||||||
|
"user 6": {"percent": 12.5, "vote": 2},
|
||||||
|
},
|
||||||
|
"election role 3": {
|
||||||
|
"blank vote": {"percent": 25.0, "vote": 2},
|
||||||
|
"total vote": 8,
|
||||||
|
"user 3": {"percent": 50.0, "vote": 4},
|
||||||
|
"user 7": {"percent": 25.0, "vote": 2},
|
||||||
|
},
|
||||||
|
"election role 4": {
|
||||||
|
"blank vote": {"percent": 25.0, "vote": 2},
|
||||||
|
"total vote": 8,
|
||||||
|
"user 4": {"percent": 50.0, "vote": 4},
|
||||||
|
"user 8": {"percent": 25.0, "vote": 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_apply_result(self):
|
||||||
|
user = baker.make(
|
||||||
|
User, user_permissions=[Permission.objects.get(codename="add_membership")]
|
||||||
|
)
|
||||||
|
self.client.force_login(user)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
|
inputs = soup.find_all("input", attrs={"type": "checkbox"})
|
||||||
|
assert all("checked" in i.attrs for i in inputs)
|
||||||
|
ids = {int(i.attrs["value"]) for i in inputs}
|
||||||
|
assert ids == {
|
||||||
|
self.candidatures[0].id,
|
||||||
|
self.candidatures[1].id,
|
||||||
|
self.candidatures[2].id,
|
||||||
|
self.candidatures[5].id,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data={"candidates": ids.difference({self.candidatures[5].id})}
|
||||||
|
)
|
||||||
|
assertRedirects(response, self.url)
|
||||||
|
for candidate in self.candidatures[0:3]:
|
||||||
|
assert Membership.objects.filter(
|
||||||
|
start_date=localdate(),
|
||||||
|
end_date=None,
|
||||||
|
user=candidate.user,
|
||||||
|
role=candidate.role.club_role,
|
||||||
|
).exists()
|
||||||
|
assert self.club.members_group.users.contains(candidate.user)
|
||||||
|
assert self.club.board_group.users.contains(candidate.user)
|
||||||
|
# candidatures[5] was unchecked, so it shouldn't receive a club role
|
||||||
|
assert not self.candidatures[5].user.memberships.exists()
|
||||||
|
|
||||||
|
# now that results are applied, it shouldn't be possible to replay the request
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
assert "Les résultats de cette élection ont été appliqués" in response.text
|
||||||
|
response = self.client.post(self.url, data={"candidates": ids})
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
def test_no_result_to_apply(self):
|
||||||
|
self.election.roles.update(club_role=None)
|
||||||
|
user = baker.make(
|
||||||
|
User, user_permissions=[Permission.objects.get(codename="add_membership")]
|
||||||
|
)
|
||||||
|
self.client.force_login(user)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
|
assert not soup.find("input", attrs={"type": "checkbox"})
|
||||||
|
assert "Pas de résultats à appliquer" in response.text
|
||||||
|
|
||||||
|
def test_access_denied(self):
|
||||||
|
user = subscriber_user.make()
|
||||||
|
self.client.force_login(user)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data={"candidates": [self.candidatures[0].id]}
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
def test_election_not_finished(self):
|
||||||
|
user = baker.make(
|
||||||
|
User, user_permissions=[Permission.objects.get(codename="add_membership")]
|
||||||
|
)
|
||||||
|
self.election.end_date = now() + timedelta(minutes=1)
|
||||||
|
self.election.save()
|
||||||
|
self.client.force_login(user)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data={"candidates": [self.candidatures[0].id]}
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from model_bakery import baker
|
||||||
|
from pytest_django.asserts import assertRedirects
|
||||||
|
|
||||||
|
from club.models import Club, ClubRole
|
||||||
|
from core.baker_recipes import subscriber_user
|
||||||
|
from core.models import Group, User
|
||||||
|
from election.models import Election, Role
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestCreateRole(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.club = baker.make(Club)
|
||||||
|
cls.edit_group = baker.make(Group)
|
||||||
|
cls.election = baker.make(
|
||||||
|
Election,
|
||||||
|
clubs=[cls.club],
|
||||||
|
edit_groups=[cls.edit_group],
|
||||||
|
view_groups=[Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID)],
|
||||||
|
end_candidature=now() + timedelta(days=1),
|
||||||
|
)
|
||||||
|
cls.url = reverse(
|
||||||
|
"election:create_role", kwargs={"election_id": cls.election.id}
|
||||||
|
)
|
||||||
|
cls.election_url = reverse(
|
||||||
|
"election:detail", kwargs={"election_id": cls.election.id}
|
||||||
|
)
|
||||||
|
cls.permission = Permission.objects.get(codename="add_role")
|
||||||
|
|
||||||
|
def assert_role_creation_ok(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response = self.client.post(self.url, data={"title": "foo", "max_choice": 1})
|
||||||
|
assertRedirects(response, self.election_url)
|
||||||
|
roles = list(self.election.roles.all())
|
||||||
|
assert len(roles) == 1
|
||||||
|
assert roles[0].title == "foo"
|
||||||
|
|
||||||
|
def assert_role_creation_denied(self):
|
||||||
|
initial_role_count = self.election.roles.count()
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
response = self.client.post(self.url, data={"title": "foo", "max_choice": 1})
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert self.election.roles.count() == initial_role_count
|
||||||
|
|
||||||
|
def test_admin(self):
|
||||||
|
user = baker.make(User, user_permissions=[self.permission])
|
||||||
|
self.client.force_login(user)
|
||||||
|
self.assert_role_creation_ok()
|
||||||
|
|
||||||
|
def test_edit_group(self):
|
||||||
|
user = baker.make(User, groups=[self.edit_group])
|
||||||
|
self.client.force_login(user)
|
||||||
|
self.assert_role_creation_ok()
|
||||||
|
|
||||||
|
def test_role_linked_to_club_role(self):
|
||||||
|
user = baker.make(User, user_permissions=[self.permission])
|
||||||
|
self.client.force_login(user)
|
||||||
|
club_role = baker.make(ClubRole, is_board=True, club=self.club)
|
||||||
|
response = self.client.post(
|
||||||
|
self.url, data={"title": "foo", "max_choice": 1, "club_role": club_role.id}
|
||||||
|
)
|
||||||
|
assertRedirects(response, self.election_url)
|
||||||
|
roles = list(self.election.roles.all())
|
||||||
|
assert len(roles) == 1
|
||||||
|
assert roles[0].title == "foo"
|
||||||
|
assert roles[0].club_role == club_role
|
||||||
|
|
||||||
|
def test_permission_denied(self):
|
||||||
|
user = subscriber_user.make()
|
||||||
|
self.client.force_login(user)
|
||||||
|
self.assert_role_creation_denied()
|
||||||
|
|
||||||
|
def test_election_not_editable(self):
|
||||||
|
user = baker.make(User, user_permissions=[self.permission])
|
||||||
|
self.election.end_candidature = now() - timedelta(minutes=1)
|
||||||
|
self.election.save()
|
||||||
|
self.client.force_login(user)
|
||||||
|
self.assert_role_creation_denied()
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdateRole(TestCreateRole):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
# TestUpdateRole is just TestCreateRole, but with different parameters
|
||||||
|
cls.club = baker.make(Club)
|
||||||
|
cls.edit_group = baker.make(Group)
|
||||||
|
cls.election = baker.make(
|
||||||
|
Election,
|
||||||
|
clubs=[cls.club],
|
||||||
|
edit_groups=[cls.edit_group],
|
||||||
|
view_groups=[Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID)],
|
||||||
|
end_candidature=now() + timedelta(days=1),
|
||||||
|
)
|
||||||
|
cls.role = baker.make(Role, election=cls.election)
|
||||||
|
cls.url = reverse("election:update_role", kwargs={"role_id": cls.role.id})
|
||||||
|
cls.election_url = reverse(
|
||||||
|
"election:detail", kwargs={"election_id": cls.election.id}
|
||||||
|
)
|
||||||
|
cls.permission = Permission.objects.get(codename="change_role")
|
||||||
+25
-50
@@ -16,9 +16,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||||
|
|
||||||
from club.models import Membership
|
|
||||||
from core.auth.mixins import CanEditMixin, CanViewMixin
|
from core.auth.mixins import CanEditMixin, CanViewMixin
|
||||||
from core.views import FragmentMixin
|
|
||||||
from election.forms import (
|
from election.forms import (
|
||||||
ApplyRoleResultForm,
|
ApplyRoleResultForm,
|
||||||
CandidateForm,
|
CandidateForm,
|
||||||
@@ -232,11 +230,8 @@ class RoleCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
|
|||||||
def test_func(self):
|
def test_func(self):
|
||||||
if not self.election.is_vote_editable:
|
if not self.election.is_vote_editable:
|
||||||
return False
|
return False
|
||||||
if self.request.user.has_perm("election.add_role"):
|
user = self.request.user
|
||||||
return True
|
return user.has_perm("election.add_role") or user.can_edit(self.election)
|
||||||
return self.election.edit_groups.filter(
|
|
||||||
id__in=self.request.user.all_groups
|
|
||||||
).exists()
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
return super().get_form_kwargs() | {"election": self.election}
|
return super().get_form_kwargs() | {"election": self.election}
|
||||||
@@ -309,46 +304,30 @@ class CandidatureUpdateView(LoginRequiredMixin, CanEditMixin, UpdateView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoleUpdateView(CanEditMixin, UpdateView):
|
class RoleUpdateView(UserPassesTestMixin, UpdateView):
|
||||||
model = Role
|
model = Role
|
||||||
form_class = RoleForm
|
form_class = RoleForm
|
||||||
template_name = "election/role_form.jinja"
|
template_name = "election/role_form.jinja"
|
||||||
pk_url_kwarg = "role_id"
|
pk_url_kwarg = "role_id"
|
||||||
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
@cached_property
|
||||||
self.object = self.get_object()
|
def election(self):
|
||||||
if not self.object.election.is_vote_editable:
|
return self.get_object().election
|
||||||
raise PermissionDenied
|
|
||||||
return super().dispatch(request, *arg, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def test_func(self):
|
||||||
self.object = self.get_object()
|
if not self.election.is_vote_editable:
|
||||||
self.form = self.get_form()
|
return False
|
||||||
return self.render_to_response(self.get_context_data(form=self.form))
|
user = self.request.user
|
||||||
|
return user.has_perm("election.change_role") or user.can_edit(self.election)
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
self.object = self.get_object()
|
|
||||||
self.form = self.get_form()
|
|
||||||
if (
|
|
||||||
request.user.is_authenticated
|
|
||||||
and request.user.can_edit(self.object)
|
|
||||||
and self.form.is_valid()
|
|
||||||
):
|
|
||||||
return super().form_valid(self.form)
|
|
||||||
return self.form_invalid(self.form)
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs["election"] = self.object.election
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
|
||||||
return reverse_lazy(
|
|
||||||
"election:detail", kwargs={"election_id": self.object.election.id}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
return super().get_context_data(**kwargs) | {"election": self.object.election}
|
return super().get_context_data(**kwargs) | {"election": self.election}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
return super().get_form_kwargs() | {"election": self.election}
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
return reverse("election:detail", kwargs={"election_id": self.election.id})
|
||||||
|
|
||||||
|
|
||||||
# Delete Views
|
# Delete Views
|
||||||
@@ -410,9 +389,7 @@ class ElectionListDeleteView(CanEditMixin, DeleteView):
|
|||||||
return reverse("election:detail", kwargs={"election_id": self.election.id})
|
return reverse("election:detail", kwargs={"election_id": self.election.id})
|
||||||
|
|
||||||
|
|
||||||
class ApplyResultFragment(
|
class ApplyResultFragment(LoginRequiredMixin, UserPassesTestMixin, FormView):
|
||||||
LoginRequiredMixin, UserPassesTestMixin, FragmentMixin, FormView
|
|
||||||
):
|
|
||||||
template_name = "election/fragments/apply_result.jinja"
|
template_name = "election/fragments/apply_result.jinja"
|
||||||
form_class = ApplyRoleResultForm
|
form_class = ApplyRoleResultForm
|
||||||
|
|
||||||
@@ -429,6 +406,11 @@ class ApplyResultFragment(
|
|||||||
id__in=self.request.user.all_groups
|
id__in=self.request.user.all_groups
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if self.election.results_applied:
|
||||||
|
raise PermissionDenied
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
return super().get_form_kwargs() | {"election": self.election}
|
return super().get_form_kwargs() | {"election": self.election}
|
||||||
|
|
||||||
@@ -437,14 +419,7 @@ class ApplyResultFragment(
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
return super().get_context_data(**kwargs) | {
|
return super().get_context_data(**kwargs) | {"clubs": self.election.clubs.all()}
|
||||||
"already_applied": Membership.objects.filter(
|
|
||||||
role__election_roles__election=self.election,
|
|
||||||
end_date=None,
|
|
||||||
start_date__gte=self.election.end_date,
|
|
||||||
).exists(),
|
|
||||||
"clubs": self.election.clubs.all(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
return reverse(
|
return reverse(
|
||||||
|
|||||||
@@ -270,6 +270,10 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
|
"django.db.backends": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"handlers": ["log_to_stdout"],
|
||||||
|
},
|
||||||
"main": {
|
"main": {
|
||||||
"handlers": ["log_to_stdout"],
|
"handlers": ["log_to_stdout"],
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
|
|||||||
Reference in New Issue
Block a user