mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-21 21:53:30 +00:00
send mail to inactive users
This commit is contained in:
parent
6a64e05247
commit
5a8052ae47
@ -49,6 +49,18 @@ class BillingInfoAdmin(admin.ModelAdmin):
|
|||||||
autocomplete_fields = ("customer",)
|
autocomplete_fields = ("customer",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(AccountDump)
|
||||||
|
class AccountDumpAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"customer",
|
||||||
|
"warning_mail_sent_at",
|
||||||
|
"warning_mail_error",
|
||||||
|
"dump_operation",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("customer",)
|
||||||
|
list_filter = ("warning_mail_error",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Counter)
|
@admin.register(Counter)
|
||||||
class CounterAdmin(admin.ModelAdmin):
|
class CounterAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "club", "type")
|
list_display = ("name", "club", "type")
|
||||||
|
0
counter/management/__init__.py
Normal file
0
counter/management/__init__.py
Normal file
0
counter/management/commands/__init__.py
Normal file
0
counter/management/commands/__init__.py
Normal file
88
counter/management/commands/dump_warning_mail.py
Normal file
88
counter/management/commands/dump_warning_mail.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import logging
|
||||||
|
from smtplib import SMTPException
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Exists, OuterRef, Subquery
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.timezone import localdate, now
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from counter.models import AccountDump
|
||||||
|
from subscription.models import Subscription
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Send mail to inactive users, warning them that their account is about to be dumped.
|
||||||
|
|
||||||
|
This command should be automated with a cron task.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.logger = logging.getLogger("account_dump_mail")
|
||||||
|
self.logger.setLevel(logging.INFO)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
ongoing_dump_operation = AccountDump.objects.ongoing().filter(
|
||||||
|
customer__user=OuterRef("pk")
|
||||||
|
)
|
||||||
|
users = list(
|
||||||
|
User.objects
|
||||||
|
.filter_inactive()
|
||||||
|
.filter(customer__amount__gt=0)
|
||||||
|
.exclude(Exists(ongoing_dump_operation))
|
||||||
|
.annotate(
|
||||||
|
last_subscription_date=Subquery(
|
||||||
|
Subscription.objects.filter(member=OuterRef("pk"))
|
||||||
|
.order_by("-subscription_end")
|
||||||
|
.values("subscription_end")[:1]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.select_related("customer")
|
||||||
|
)
|
||||||
|
self.stdout.write(f"{len(users)} users will be warned of their account dump")
|
||||||
|
dumps = []
|
||||||
|
for user in users:
|
||||||
|
is_success = self._send_mail(user)
|
||||||
|
dumps.append(
|
||||||
|
AccountDump(
|
||||||
|
customer_id=user.id,
|
||||||
|
warning_mail_sent_at=now(),
|
||||||
|
warning_mail_error=not is_success,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AccountDump.objects.bulk_create(dumps)
|
||||||
|
self.stdout.write("Finished !")
|
||||||
|
|
||||||
|
def _send_mail(self, user: User) -> bool:
|
||||||
|
"""Send the warning email to the given user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the mail was successfully sent, else False
|
||||||
|
"""
|
||||||
|
message = render_to_string(
|
||||||
|
"counter/account_dump_warning_mail.jinja",
|
||||||
|
{
|
||||||
|
"balance": user.customer.amount,
|
||||||
|
"last_subscription_date": user.last_subscription_date,
|
||||||
|
"dump_date": localdate() + settings.SITH_ACCOUNT_DUMP_DELTA,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# sending mails one by one is long and ineffective,
|
||||||
|
# but it makes easier to know which emails failed (and how).
|
||||||
|
# Also, there won't be that much mails sent (except on the first run)
|
||||||
|
send_mail(
|
||||||
|
_("Clearing of your AE account"),
|
||||||
|
message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[user.email],
|
||||||
|
)
|
||||||
|
self.logger.info(f"Mail successfully sent to {user.email}")
|
||||||
|
return True
|
||||||
|
except SMTPException as e:
|
||||||
|
self.logger.error(f"failed mail to {user.email} :\n{e}")
|
||||||
|
return False
|
@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-06 14:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("counter", "0023_billinginfo_phone_number")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AccountDump",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"warning_mail_sent_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
help_text="When the mail warning that the account was about to be dumped was sent."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"warning_mail_error",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Set this to True if the warning mail received an error",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"customer",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="dumps",
|
||||||
|
to="counter.customer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dump_operation",
|
||||||
|
models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The operation that emptied the account.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="counter.selling",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="accountdump",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("dump_operation", None)),
|
||||||
|
fields=("customer",),
|
||||||
|
name="unique_ongoing_dump",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -26,7 +26,7 @@ from dict2xml import dict2xml
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Exists, F, OuterRef, QuerySet, Sum, Value
|
from django.db.models import Exists, F, OuterRef, Q, QuerySet, Sum, Value
|
||||||
from django.db.models.functions import Concat, Length
|
from django.db.models.functions import Concat, Length
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -211,6 +211,51 @@ class BillingInfo(models.Model):
|
|||||||
return '<?xml version="1.0" encoding="UTF-8" ?>' + xml
|
return '<?xml version="1.0" encoding="UTF-8" ?>' + xml
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDumpQuerySet(models.QuerySet):
|
||||||
|
def ongoing(self) -> Self:
|
||||||
|
"""Filter dump operations that are not completed yet."""
|
||||||
|
return self.filter(dump_operation=None)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDump(models.Model):
|
||||||
|
"""The process of dumping an account."""
|
||||||
|
|
||||||
|
customer = models.ForeignKey(
|
||||||
|
Customer, related_name="dumps", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
warning_mail_sent_at = models.DateTimeField(
|
||||||
|
help_text=_(
|
||||||
|
"When the mail warning that the account was about to be dumped was sent."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
warning_mail_error = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("Set this to True if the warning mail received an error"),
|
||||||
|
)
|
||||||
|
dump_operation = models.OneToOneField(
|
||||||
|
"Selling",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
help_text=_("The operation that emptied the account."),
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = AccountDumpQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["customer"],
|
||||||
|
condition=Q(dump_operation=None),
|
||||||
|
name="unique_ongoing_dump",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
status = "ongoing" if self.dump_operation is None else "finished"
|
||||||
|
return f"{self.customer} - {status}"
|
||||||
|
|
||||||
|
|
||||||
class ProductType(models.Model):
|
class ProductType(models.Model):
|
||||||
"""A product type.
|
"""A product type.
|
||||||
|
|
||||||
|
43
counter/templates/counter/account_dump_warning_mail.jinja
Normal file
43
counter/templates/counter/account_dump_warning_mail.jinja
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<p>
|
||||||
|
Bonjour,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans date=last_subscription_date|date(DATETIME_FORMAT) %}
|
||||||
|
You received this email because your last subscription to the
|
||||||
|
Students' association ended on {{ date }}.
|
||||||
|
{% endtrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans date=dump_date|date(DATETIME_FORMAT) %}
|
||||||
|
In accordance with the Internal Regulations, the balance of any
|
||||||
|
inactive AE account for more than 2 years automatically goes back
|
||||||
|
to the AE.
|
||||||
|
The money present on your account will therefore be recovered in full
|
||||||
|
on {{ date }}.
|
||||||
|
{% endtrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans %}However, if your subscription is renewed by this date,
|
||||||
|
your right to keep the money in your AE account will be renewed.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if balance >= 10 %}
|
||||||
|
<p>
|
||||||
|
{% trans %}You can also request a refund by sending an email to
|
||||||
|
<a href="mailto:ae@utbm.fr">ae@utbm.fr</a>
|
||||||
|
before the aforementioned date.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans %}Sincerely{% endtrans %},
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
L'association des étudiants de l'UTBM <br>
|
||||||
|
6, Boulevard Anatole France <br>
|
||||||
|
90000 Belfort
|
||||||
|
</p>
|
7
counter/tests/test_account_dump.py
Normal file
7
counter/tests/test_account_dump.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_account_dump():
|
||||||
|
# TODO write the fucking test
|
||||||
|
pass
|
File diff suppressed because it is too large
Load Diff
@ -37,6 +37,7 @@ import binascii
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
@ -229,13 +230,20 @@ LOGGING = {
|
|||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "simple",
|
"formatter": "simple",
|
||||||
},
|
},
|
||||||
|
"dump_mail_file": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.FileHandler",
|
||||||
|
"filename": "account_dump_mail.log",
|
||||||
|
"formatter": "simple",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
"main": {
|
"main": {
|
||||||
"handlers": ["log_to_stdout"],
|
"handlers": ["log_to_stdout"],
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"propagate": True,
|
"propagate": True,
|
||||||
}
|
},
|
||||||
|
"account_dump_mail": {"handlers": ["dump_mail_file", "log_to_stdout"]},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,6 +506,8 @@ SITH_COUNTER_CASH_SUMMARY_LENGTH = 50
|
|||||||
|
|
||||||
SITH_ACCOUNT_INACTIVITY_DELTA = relativedelta(years=2)
|
SITH_ACCOUNT_INACTIVITY_DELTA = relativedelta(years=2)
|
||||||
"""Time before which a user account is considered inactive"""
|
"""Time before which a user account is considered inactive"""
|
||||||
|
SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30)
|
||||||
|
"""timedelta between the warning mail and the actual account dump"""
|
||||||
|
|
||||||
# Defines which product type is the refilling type, and thus increases the account amount
|
# Defines which product type is the refilling type, and thus increases the account amount
|
||||||
SITH_COUNTER_PRODUCTTYPE_REFILLING = 3
|
SITH_COUNTER_PRODUCTTYPE_REFILLING = 3
|
||||||
|
Loading…
Reference in New Issue
Block a user