cof: Màj de la prod #851
14 changed files with 138 additions and 17 deletions
12
README.md
12
README.md
|
@ -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` :
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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' %}&{{ 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' %}&{{ 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' %}&{{ 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' %}&{{ 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' %}&{{ 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' %}&{{ 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' %}&{{ 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' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
Loading…
Add table
Reference in a new issue