start of work

This commit is contained in:
imperosol 2024-11-19 00:41:49 +01:00
parent 0270fb764e
commit cbbbdc4bbf
13 changed files with 173 additions and 152 deletions

View File

@ -529,13 +529,15 @@ class User(AbstractBaseUser):
return False return False
@cached_property @cached_property
def can_create_subscription(self): def can_create_subscription(self) -> bool:
from club.models import Club from club.models import Membership
for club in Club.objects.filter(id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS): return (
if club in self.clubs_with_rights: Membership.objects.board()
return True .ongoing()
return False .filter(club_id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS)
.exists()
)
@cached_property @cached_property
def is_launderette_manager(self): def is_launderette_manager(self):

View File

@ -1,3 +1,4 @@
import "htmx-ext-response-targets/response-targets";
import htmx from "htmx.org"; import htmx from "htmx.org";
Object.assign(window, { htmx }); Object.assign(window, { htmx });

View File

@ -406,7 +406,7 @@ class Galaxy(models.Model):
cls.logger.debug(f"\t\t> Scaled distance: {value}") cls.logger.debug(f"\t\t> Scaled distance: {value}")
return int(value) return int(value)
def rule(self, picture_count_threshold=10) -> None: def rule(self, picture_count_threshold=-1) -> None:
"""Main function of the Galaxy. """Main function of the Galaxy.
Iterate over all the rulable users to promote them to citizens. Iterate over all the rulable users to promote them to citizens.

6
package-lock.json generated
View File

@ -22,6 +22,7 @@
"d3-force-3d": "^3.0.5", "d3-force-3d": "^3.0.5",
"easymde": "^2.18.0", "easymde": "^2.18.0",
"glob": "^11.0.0", "glob": "^11.0.0",
"htmx-ext-response-targets": "^2.0.1",
"htmx.org": "^2.0.3", "htmx.org": "^2.0.3",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"jquery-ui": "^1.14.0", "jquery-ui": "^1.14.0",
@ -4140,6 +4141,11 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/htmx-ext-response-targets": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/htmx-ext-response-targets/-/htmx-ext-response-targets-2.0.1.tgz",
"integrity": "sha512-uCMw098+0xcrs7UW/s8l8hqj5wfOaVnVV7286cS+TNMNguo8fQpi/PEaZuT4VUysIiRcjj4pcTkuaP6Q9iJ3XA=="
},
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz",

View File

@ -14,7 +14,9 @@
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"sideEffects": [".css"], "sideEffects": [
".css"
],
"imports": { "imports": {
"#openapi": "./staticfiles/generated/openapi/index.ts", "#openapi": "./staticfiles/generated/openapi/index.ts",
"#core:*": "./core/static/bundled/*", "#core:*": "./core/static/bundled/*",
@ -47,6 +49,7 @@
"easymde": "^2.18.0", "easymde": "^2.18.0",
"glob": "^11.0.0", "glob": "^11.0.0",
"htmx.org": "^2.0.3", "htmx.org": "^2.0.3",
"htmx-ext-response-targets": "^2.0.1",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"jquery-ui": "^1.14.0", "jquery-ui": "^1.14.0",
"jquery.shorten": "^1.0.0", "jquery.shorten": "^1.0.0",

View File

@ -1,9 +1,6 @@
from pydantic import TypeAdapter from pydantic import TypeAdapter
from core.views.widgets.select import ( from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
AutoCompleteSelect,
AutoCompleteSelectMultiple,
)
from sas.models import Album from sas.models import Album
from sas.schemas import AlbumSchema from sas.schemas import AlbumSchema

View File

@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import User from core.models import User
from core.views.forms import SelectDate, SelectDateTime from core.views.forms import SelectDateTime
from core.views.widgets.select import AutoCompleteSelectUser from core.views.widgets.select import AutoCompleteSelectUser
from subscription.models import Subscription from subscription.models import Subscription
@ -21,37 +21,15 @@ class SelectionDateForm(forms.Form):
) )
class SubscriptionForm(forms.ModelForm): class SubscriptionNewUserForm(forms.Form):
class Meta:
model = Subscription
fields = ["member", "subscription_type", "payment_method", "location"]
widgets = {"member": AutoCompleteSelectUser}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["member"].required = False
self.fields |= forms.fields_for_model(
User,
fields=["first_name", "last_name", "email", "date_of_birth"],
widgets={"date_of_birth": SelectDate},
)
def clean_member(self):
subscriber = self.cleaned_data.get("member")
if subscriber:
subscriber = User.objects.filter(id=subscriber.id).first()
return subscriber
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
if ( if (
cleaned_data.get("member") is None "last_name" not in self.errors.as_data()
and "last_name" not in self.errors.as_data()
and "first_name" not in self.errors.as_data() and "first_name" not in self.errors.as_data()
and "email" not in self.errors.as_data() and "email" not in self.errors.as_data()
and "date_of_birth" not in self.errors.as_data() and "date_of_birth" not in self.errors.as_data()
): ):
self.errors.pop("member", None)
if self.errors: if self.errors:
return cleaned_data return cleaned_data
if User.objects.filter(email=cleaned_data.get("email")).first() is not None: if User.objects.filter(email=cleaned_data.get("email")).first() is not None:
@ -70,19 +48,17 @@ class SubscriptionForm(forms.ModelForm):
u.set_password(str(random.randrange(1000000, 10000000))) u.set_password(str(random.randrange(1000000, 10000000)))
u.save() u.save()
cleaned_data["member"] = u cleaned_data["member"] = u
elif cleaned_data.get("member") is not None:
self.errors.pop("last_name", None)
self.errors.pop("first_name", None) class SubscriptionForm(forms.ModelForm):
self.errors.pop("email", None) """Form to add a subscription to an existing user."""
self.errors.pop("date_of_birth", None)
if cleaned_data.get("member") is None: template_name = "subscription/fragments/existing_user_form.html"
# This should be handled here,
# but it is done in the Subscription model's clean method class Meta:
# TODO investigate why! model = Subscription
raise ValidationError( fields = ["member", "subscription_type", "payment_method", "location"]
_( widgets = {"member": AutoCompleteSelectUser}
"You must either choose an existing "
"user or create a new one properly" def clean_subscription_type(self):
) raise ValidationError("ptdr non")
)
return cleaned_data

View File

@ -0,0 +1,12 @@
{#<form#}
{# hx-post="{{ url("subscription:subscription-fragment-existing-user") }}"#}
{# hx-swap="innerHTML"#}
{# hx-target="#existing-user-subscription-form-fields"#}
{# id="existing-user-subscription-form"#}
{#>#}
{# {% csrf_token %}#}
{# <div id="existing-user-subscription-form-fields">#}
{{ form.as_div }}
{# </div>#}
{# <input type="submit" value="{% trans %}Save{% endtrans %}">#}
{#</form>#}

View File

@ -4,59 +4,65 @@
{% trans %}New subscription{% endtrans %} {% trans %}New subscription{% endtrans %}
{% endblock %} {% endblock %}
{# The following statics are bundled with our autocomplete select.
However, if one tries to swap a form by another, then the urls in script-once
and link-once disappear.
So we give them here.
If the aforementioned bug is resolved, you can remove this. #}
{% block additional_js %}
<script defer src="{{ static("webpack/core/components/ajax-select-index.ts") }}"></script>
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("webpack/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
{% endblock %}
{% block content %} {% block content %}
<h3>{% trans %}New subscription{% endtrans %}</h3> <h3>{% trans %}New subscription{% endtrans %}</h3>
<div id="user_info"></div> <div id="user_info"></div>
<form action="" method="post" id="subscription_form"> <div id="subscription-form" x-data="{existing_member: true}" hx-ext="response-targets">
{% csrf_token %} <form
{{ form.non_field_errors() }} hx-post="{{ url("subscription:subscription-fragment-existing-user") }}"
<p>{{ form.member.errors }}<label for="{{ form.member.name }}">{{ form.member.label }}</label> {{ form.member }}</p> hx-swap="innerHTML"
<div id="new_member"> hx-target-2*="#user_infos"
<p>{{ form.first_name.errors }}<label for="{{ form.first_name.name }}">{{ form.first_name.label }}</label> {{ form.first_name }}</p> hx-target-3*="#user_infos"
<p>{{ form.last_name.errors }}<label for="{{ form.last_name.name }}">{{ form.last_name.label }}</label> {{ form.last_name }}</p> hx-disabled-elt="find input[type='submit']"
<p>{{ form.email.errors }}<label for="{{ form.email.name }}">{{ form.email.label }}</label> {{ form.email }}</p> id="existing-user-subscription-form"
<p>{{ form.date_of_birth.errors }}<label for="{{ form.date_of_birth.name }}">{{ form.date_of_birth.label}}</label> {{ form.date_of_birth }}</p> >
</div> {% csrf_token %}
<p>{{ form.subscription_type.errors }}<label for="{{ form.subscription_type.name }}">{{ form.subscription_type.label }}</label> {{ form.subscription_type }}</p> <div id="existing-user-subscription-form-fields">
<p>{{ form.payment_method.errors }}<label for="{{ form.payment_method.name }}">{{ form.payment_method.label }}</label> {{ {{ existing_user_form }}
form.payment_method }}</p> </div>
<p>{% trans %}Eboutic is reserved to specific users. In doubt, don't use it.{% endtrans %}</p> <input type="submit" value="{% trans %}Save{% endtrans %}">
<p>{{ form.location.errors }}<label for="{{ form.location.name }}">{{ form.location.label }}</label> {{ form.location }}</p> </form>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> </div>
</form> {# <template x-if="!$refs.member_search?.widget?.items?.length">#}
{# <div id="new-member" @click="console.log($refs.member_search.widget.items)">#}
{# <p>#}
{# {{ existing_user_form.first_name.errors }}#}
{# <label for="{{ existing_user_form.first_name.name }}">{{ existing_user_form.first_name.label }}</label>#}
{# {{ existing_user_form.first_name }}#}
{# </p>#}
{# <p>#}
{# {{ existing_user_form.last_name.errors }}#}
{# <label for="{{ existing_user_form.last_name.name }}">{{ existing_user_form.last_name.label }}</label>#}
{# {{ existing_user_form.last_name }}#}
{# </p>#}
{# <p>#}
{# {{ existing_user_form.email.errors }}#}
{# <label for="{{ existing_user_form.email.name }}">{{ existing_user_form.email.label }}</label>#}
{# {{ existing_user_form.email }}#}
{# </p>#}
{# <p>#}
{# {{ existing_user_form.date_of_birth.errors }}#}
{# <label for="{{ existing_user_form.date_of_birth.name }}">{{ existing_user_form.date_of_birth.label}}</label>#}
{# {{ existing_user_form.date_of_birth }}#}
{# </p>#}
{# </div>#}
{# </template>#}
{% endblock %} {% endblock %}
{% block script %} {#{% block script %}#}
{{ super() }} {# {{ super() }}#}
<script type="text/javascript" charset="utf-8"> {##}
$( function() { {#{% endblock %}#}
select = $("#id_member");
member_block = $("#subscription_form #new_member");
user_info = $("#user_info");
function display_new_member() {
if (select.val()) {
member_block.hide();
member_block.children().each(function() {
$(this).children().each(function() {
$(this).removeAttr('required');
});
});
user_info.load("/user/"+select.val()+"/mini");
user_info.show();
} else {
member_block.show();
member_block.children().each(function() {
$(this).children().each(function() {
$(this).prop('required', true);
});
});
user_info.empty();
user_info.hide();
}
}
select.on("change", display_new_member);
display_new_member();
} );
</script>
{% endblock %}

View File

@ -15,10 +15,19 @@
from django.urls import path from django.urls import path
from subscription.views import NewSubscription, SubscriptionsStatsView from subscription.views import (
CreateSubscriptionExistingUserFragment,
NewSubscription,
SubscriptionsStatsView,
)
urlpatterns = [ urlpatterns = [
# Subscription views # Subscription views
path("", NewSubscription.as_view(), name="subscription"), path("", NewSubscription.as_view(), name="subscription"),
path(
"existing-user/",
CreateSubscriptionExistingUserFragment.as_view(),
name="subscription-fragment-existing-user",
),
path("stats/", SubscriptionsStatsView.as_view(), name="stats"), path("stats/", SubscriptionsStatsView.as_view(), name="stats"),
] ]

View File

@ -13,85 +13,96 @@
# #
# #
import secrets
from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic.edit import CreateView, FormView from django.utils.timezone import localdate
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from subscription.forms import SelectionDateForm, SubscriptionForm from subscription.forms import (
SelectionDateForm,
SubscriptionForm,
SubscriptionNewUserForm,
)
from subscription.models import Subscription from subscription.models import Subscription
class NewSubscription(CreateView): class CanCreateSubscriptionMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.can_create_subscription
class NewSubscription(CanCreateSubscriptionMixin, TemplateView):
template_name = "subscription/subscription.jinja" template_name = "subscription/subscription.jinja"
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"existing_user_form": SubscriptionForm(),
"new_user_form": SubscriptionNewUserForm(),
}
class CreateSubscriptionExistingUserFragment(CanCreateSubscriptionMixin, FormView):
"""Create a subscription for a user who already exists."""
form_class = SubscriptionForm form_class = SubscriptionForm
success_url = reverse_lazy("subscription:subscription-fragment-existing-user")
def dispatch(self, request, *arg, **kwargs): def render_to_response(self, context, **response_kwargs):
if request.user.can_create_subscription: print(context["form"])
return super().dispatch(request, *arg, **kwargs) print(str(context["form"]))
raise PermissionDenied print(context["form"].render())
return HttpResponse(context["form"].render(), **response_kwargs)
def get_initial(self): # def dispatch(self, request, *args, **kwargs):
if "member" in self.request.GET: # s = SubscriptionForm()
return { # print(s)
"member": self.request.GET["member"], # return super().dispatch(request, *args, **kwargs)
"subscription_type": "deux-semestres",
}
return {"subscription_type": "deux-semestres"}
def form_valid(self, form): # def form_valid(self, form):
form.instance.subscription_start = Subscription.compute_start( # duration = settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][
duration=settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][ # "duration"
"duration" # ]
], # form.instance.subscription_start = Subscription.compute_start(
user=form.instance.member, # duration=duration, user=form.instance.member
) # )
form.instance.subscription_end = Subscription.compute_end( # form.instance.subscription_end = Subscription.compute_end(
duration=settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][ # duration=duration,
"duration" # start=form.instance.subscription_start,
], # user=form.instance.member,
start=form.instance.subscription_start, # )
user=form.instance.member, # return super().form_valid(form)
)
return super().form_valid(form)
class SubscriptionsStatsView(FormView): class SubscriptionsStatsView(FormView):
template_name = "subscription/stats.jinja" template_name = "subscription/stats.jinja"
form_class = SelectionDateForm form_class = SelectionDateForm
success_url = reverse_lazy("subscriptions:stats")
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
import datetime self.start_date = localdate()
self.start_date = datetime.datetime.today()
self.end_date = self.start_date self.end_date = self.start_date
res = super().dispatch(request, *arg, **kwargs)
if request.user.is_root or request.user.is_board_member: if request.user.is_root or request.user.is_board_member:
return res return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied raise PermissionDenied
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.form = self.get_form() self.form = self.get_form()
self.start_date = self.form["start_date"] self.start_date = self.form["start_date"]
self.end_date = self.form["end_date"] self.end_date = self.form["end_date"]
res = super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
if request.user.is_root or request.user.is_board_member:
return res
raise PermissionDenied
def get_initial(self): def get_initial(self):
init = { return {
"start_date": self.start_date.strftime("%Y-%m-%d %H:%M:%S"), "start_date": self.start_date.strftime("%Y-%m-%d %H:%M:%S"),
"end_date": self.end_date.strftime("%Y-%m-%d %H:%M:%S"), "end_date": self.end_date.strftime("%Y-%m-%d %H:%M:%S"),
} }
return init
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
from subscription.models import Subscription
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["subscriptions_total"] = Subscription.objects.filter( kwargs["subscriptions_total"] = Subscription.objects.filter(
subscription_end__gte=self.end_date, subscription_start__lte=self.start_date subscription_end__gte=self.end_date, subscription_start__lte=self.start_date
@ -100,6 +111,3 @@ class SubscriptionsStatsView(FormView):
kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD
kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS
return kwargs return kwargs
def get_success_url(self, **kwargs):
return reverse_lazy("subscriptions:stats")

View File

@ -85,6 +85,7 @@ export default {
inject({ inject({
// biome-ignore lint/style/useNamingConvention: that's how it's called // biome-ignore lint/style/useNamingConvention: that's how it's called
Alpine: "alpinejs", Alpine: "alpinejs",
htmx: "htmx.org"
}), }),
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [