cof: Màj de la prod #851

Merged
lbailly merged 17 commits from master into cof-prod 2025-02-26 08:41:11 +01:00
14 changed files with 138 additions and 17 deletions

View file

@ -18,7 +18,7 @@ Il vous faudra installer pip, les librairies de développement de python ainsi
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
Debian et dérivées (Ubuntu, ...) : Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python3-pip python3-dev python3-venv sqlite3 sudo apt-get install python3-pip python3-dev python3-venv sqlite3 libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
@ -30,7 +30,15 @@ Pour l'activer, il faut taper
. venv/bin/activate . venv/bin/activate
depuis le même dossier. depuis le même dossier. Pour préparer l'environnement à l'utilisation de `./manage.py`
(qui permet de faire des tests en local), il faut également taper
export CREDENTIALS_DIRECTORY=$(realpath .credentials)
export DJANGO_SETTINGS_MODULE=gestioasso.settings.local
export GESTIOCOF_DEBUG=true
export GESTIOCOF_STATIC_ROOT=$(realpath .static)
export GESTIOBDS_DEBUG=true
export GESTIOBDS_STATIC_ROOT=$(realpath .static)
Vous pouvez maintenant installer les dépendances Python depuis le fichier Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` : `requirements-devel.txt` :

View file

@ -1,6 +1,7 @@
from django.urls import path from django.urls import path
from bds import views from bds import views
from shared.views import SympaListView
app_name = "bds" app_name = "bds"
urlpatterns = [ urlpatterns = [
@ -21,4 +22,10 @@ urlpatterns = [
name="members.expired", name="members.expired",
), ),
path("members/reset", views.ResetMembershipView.as_view(), name="members.reset"), path("members/reset", views.ResetMembershipView.as_view(), name="members.reset"),
# Sympa export view
path(
"sympa/members/",
SympaListView.as_view(filters={"bds__is_member": True}),
name="export.sympa",
),
] ]

View file

@ -27,6 +27,9 @@ ALLOWED_HOSTS = []
DEBUG = True DEBUG = True
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
SYMPA_PASSWORD = b"sympa"
SYMPA_USERNAME = b"sympa"
if TESTING: if TESTING:
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]

View file

@ -26,6 +26,9 @@ EMAIL_HOST = credentials.get("EMAIL_HOST")
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
SYMPA_PASSWORD = credentials["SYMPA_PASSWORD"].encode()
SYMPA_USERNAME = credentials["SYMPA_USERNAME"].encode()
## ##
# Installed Apps configuration # Installed Apps configuration

View file

@ -30,6 +30,9 @@ LDAP_SERVER_URL = credentials.get("LDAP_SERVER_URL")
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
SYMPA_PASSWORD = credentials["SYMPA_PASSWORD"].encode()
SYMPA_USERNAME = credentials["SYMPA_USERNAME"].encode()
## ##
# Installed Apps configuration # Installed Apps configuration
@ -201,7 +204,10 @@ CHANNEL_LAYERS = credentials.get_json(
}, },
) )
CORS_ORIGIN_WHITELIST = credentials.get("CORS_ORIGIN_WHITELIST", []) ASGI_APPLICATION = "gestioasso.routing.application"
CORS_ALLOWED_ORIGINS = credentials.get("CORS_ALLOWED_ORIGINS", [])
CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS]
SITE_ID = 1 SITE_ID = 1
@ -264,7 +270,7 @@ MAIL_DATA = {
"FROM": "Le BdA <bda@ens.fr>", "FROM": "Le BdA <bda@ens.fr>",
"REPLYTO": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>",
}, },
"rappel_negatif": { "kfet": {
"FROM": "La K-Fêt <chefs-k-fet@ens.fr>", "FROM": "La K-Fêt <chefs-k-fet@ens.fr>",
"REPLYTO": "La K-Fêt <chefs-k-fet@ens.fr>", "REPLYTO": "La K-Fêt <chefs-k-fet@ens.fr>",
}, },

View file

@ -22,10 +22,10 @@
<section class="actulist"> <section class="actulist">
{% if actus.has_previous %} {% if actus.has_previous %}
<a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus récentes" %}</a> <a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% if key != 'page' %}&amp;{{ key }}={{ value }}{% endif %}{% endfor %}">{% trans "Actualités plus récentes" %}</a>
{% endif %} {% endif %}
{% if actus.has_next %} {% if actus.has_next %}
<a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a> <a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% if key != 'page' %}&amp;{{ key }}={{ value }}{% endif %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a>
{% endif %} {% endif %}
{% for actu in page.actus %} {% for actu in page.actus %}
@ -44,10 +44,10 @@
{% endfor %} {% endfor %}
{% if actus.has_previous %} {% if actus.has_previous %}
<a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus récentes" %}</a> <a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% if key != 'page' %}&amp;{{ key }}={{ value }}{% endif %}{% endfor %}">{% trans "Actualités plus récentes" %}</a>
{% endif %} {% endif %}
{% if actus.has_next %} {% if actus.has_next %}
<a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a> <a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% if key != 'page' %}&amp;{{ key }}={{ value }}{% endif %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a>
{% endif %} {% endif %}
</section> </section>
{% endblock %} {% endblock %}

View file

@ -4,6 +4,16 @@ from django.views.generic.base import TemplateView
from django_cas_ng import views as django_cas_views from django_cas_ng import views as django_cas_views
from gestioncof import csv_views, views from gestioncof import csv_views, views
from shared.views import SympaListView
sympa_patterns = [
path(
f"{mailing}/",
SympaListView.as_view(filters={f"profile__mailing_{mailing}": True}),
name=f"sympa.{mailing}",
)
for mailing in ["bda", "bda_revente", "cof", "unernestaparis"]
]
export_patterns = [ export_patterns = [
path("members", views.export_members, name="export.members"), path("members", views.export_members, name="export.members"),
@ -162,4 +172,8 @@ urlpatterns = [
# Clubs # Clubs
# ----- # -----
path("clubs/", include(clubs_patterns)), path("clubs/", include(clubs_patterns)),
# -----
# Sympa export
# -----
path("sympa/", include(sympa_patterns)),
] ]

View file

@ -93,7 +93,7 @@ class DemandeSoireeForm(forms.Form):
def default_promo(): def default_promo():
now = date.today() now = date.today()
return now.month <= 8 and now.year - 1 or now.year return now.month <= 7 and now.year - 1 or now.year
def get_promo_choices(): def get_promo_choices():

View file

@ -1,3 +1,5 @@
from asgiref.sync import sync_to_async
from ..decorators import kfet_is_team from ..decorators import kfet_is_team
from ..utils import DjangoJsonWebsocketConsumer, PermConsumerMixin from ..utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
from .open import kfet_open from .open import kfet_open
@ -19,7 +21,7 @@ class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer):
"""Send current status on connect.""" """Send current status on connect."""
await super().connect() await super().connect()
group = "team" if kfet_is_team(self.user) else "base" group = "team" if await sync_to_async(kfet_is_team)(self.user) else "base"
await self.channel_layer.group_add(f"kfet.open.{group}", self.channel_name) await self.channel_layer.group_add(f"kfet.open.{group}", self.channel_name)

View file

@ -31,6 +31,28 @@
{% endif %} {% endif %}
</div> </div>
<aside>
<div class="heading">
{{ positive_count }}
<span class="sub">compte{{ positive_count|pluralize }} en positif</span>
</div>
<div class="heading">
{{ positives_sum|floatformat:2 }}€
<span class="sub">de positif total</span>
</div>
</aside>
<aside>
<div class="heading">
{{ negative_count }}
<span class="sub">compte{{ negative_count|pluralize }} en négatif</span>
</div>
<div class="heading">
{{ negatives_sum|floatformat:2 }}€
<span class="sub">de négatif total</span>
</div>
</aside>
{% endblock %} {% endblock %}
{% block main %} {% block main %}

View file

@ -1,6 +1,7 @@
import json import json
import math import math
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.core.cache import cache from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
@ -71,9 +72,6 @@ class DjangoJsonWebsocketConsumer(AsyncJsonWebsocketConsumer):
@classmethod @classmethod
async def encode_json(cls, content): async def encode_json(cls, content):
# Remove the type value, only used by Channels to choose the group to send to
content.pop("type")
return json.dumps(content, cls=DjangoJSONEncoder) return json.dumps(content, cls=DjangoJSONEncoder)
@ -95,7 +93,7 @@ class PermConsumerMixin:
"""Check permissions on connection.""" """Check permissions on connection."""
self.user = self.scope["user"] self.user = self.scope["user"]
if self.user.has_perms(self.perms_connect): if await sync_to_async(self.user.has_perms)(self.perms_connect):
await super().connect() await super().connect()
else: else:
await self.close() await self.close()

View file

@ -184,7 +184,16 @@ class DemandeSoireeView(FormView):
@teamkfet_required @teamkfet_required
def account(request): def account(request):
accounts = Account.objects.select_related("cofprofile__user").order_by("trigramme") accounts = Account.objects.select_related("cofprofile__user").order_by("trigramme")
return render(request, "kfet/account.html", {"accounts": accounts}) positive_accounts = Account.objects.filter(balance__gte=0).exclude(trigramme="#13")
negative_accounts = Account.objects.filter(balance__lt=0).exclude(trigramme="#13")
return render(request, "kfet/account.html", {
"accounts": accounts,
"positive_count": positive_accounts.count(),
"positives_sum": sum(acc.balance for acc in positive_accounts),
"negative_count": negative_accounts.count(),
"negatives_sum": sum(acc.balance for acc in negative_accounts),
})
@login_required @login_required
@ -1171,6 +1180,13 @@ def kpsul_perform_operations(request):
operationgroup.amount += operation.amount operationgroup.amount += operation.amount
if operation.type == Operation.DEPOSIT: if operation.type == Operation.DEPOSIT:
required_perms.add("kfet.perform_deposit") required_perms.add("kfet.perform_deposit")
if request.user.profile.account_kfet == on_acc:
data["errors"].append(
{
"code": "auto_deposit",
"message": ("Impossible de charger son propre trigramme"),
}
)
if operation.type == Operation.EDIT: if operation.type == Operation.EDIT:
required_perms.add("kfet.edit_balance_account") required_perms.add("kfet.edit_balance_account")
need_comment = True need_comment = True

View file

@ -2,6 +2,7 @@
set -euC set -euC
mkdir -p .static
python manage.py migrate --noinput python manage.py migrate --noinput
python manage.py sync_page_translation_fields python manage.py sync_page_translation_fields
python manage.py update_translation_fields python manage.py update_translation_fields

View file

@ -1,12 +1,53 @@
import base64
from collections import namedtuple from collections import namedtuple
from typing import Any
from dal import autocomplete from dal import autocomplete
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import Http404 from django.http import Http404, HttpResponse
from django.views.generic import TemplateView from django.views.generic import TemplateView, View
from shared.autocomplete import ModelSearch from shared.autocomplete import ModelSearch
User = get_user_model()
class SympaListView(View):
realm = "sympa"
username = settings.SYMPA_USERNAME
password = settings.SYMPA_PASSWORD
filters: dict[str, Any] = {}
def dispatch(self, request, *args, **kwargs):
if "HTTP_AUTHORIZATION" in request.META:
auth = request.META["HTTP_AUTHORIZATION"].split()
if len(auth) == 2 and auth[0].lower() == "basic":
name, passwd = base64.b64decode(auth[1]).split(b":")
if name == self.username and passwd == self.password:
return self.render_to_response(request, *args, **kwargs)
return HttpResponse(
status=401, headers={"WWW-Authenticate": f'Basic realm="{self.realm}"'}
)
def render_to_response(self, request, *args, **kwargs):
"""
Renders a list of emails in a text response.
"""
users = User.objects.filter(**self.filters)
return HttpResponse(
b"\n".join(u.email.encode("utf-8") for u in users if u.email),
content_type="text/plain",
)
class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView): class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView):
"""Compatibility layer between ModelSearch and Select2QuerySetView.""" """Compatibility layer between ModelSearch and Select2QuerySetView."""