mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 07:41:14 +00:00
repair user merging tool (#498)
This commit is contained in:
parent
585923c827
commit
a73fe598ef
@ -22,6 +22,7 @@
|
||||
#
|
||||
#
|
||||
from __future__ import annotations
|
||||
from django.db.models import Sum, F
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
@ -90,12 +91,9 @@ class Customer(models.Model):
|
||||
about the relation between a User (not a Customer,
|
||||
don't mix them) and a Product.
|
||||
"""
|
||||
return self.user.subscriptions.last() and (
|
||||
date.today()
|
||||
- self.user.subscriptions.order_by("subscription_end")
|
||||
.last()
|
||||
.subscription_end
|
||||
) < timedelta(days=90)
|
||||
subscription = self.user.subscriptions.order_by("subscription_end").last()
|
||||
time_diff = date.today() - subscription.subscription_end
|
||||
return subscription is not None and time_diff < timedelta(days=90)
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
|
||||
@ -151,12 +149,16 @@ class Customer(models.Model):
|
||||
super(Customer, self).save(*args, **kwargs)
|
||||
|
||||
def recompute_amount(self):
|
||||
self.amount = 0
|
||||
for r in self.refillings.all():
|
||||
self.amount += r.amount
|
||||
for s in self.buyings.filter(payment_method="SITH_ACCOUNT"):
|
||||
self.amount -= s.quantity * s.unit_price
|
||||
self.save()
|
||||
refillings = self.refillings.aggregate(sum=Sum(F("amount")))["sum"]
|
||||
self.amount = refillings if refillings is not None else 0
|
||||
purchases = (
|
||||
self.buyings.filter(payment_method="SITH_ACCOUNT")
|
||||
.annotate(amount=F("quantity") * F("unit_price"))
|
||||
.aggregate(sum=Sum(F("amount")))
|
||||
)["sum"]
|
||||
if purchases is not None:
|
||||
self.amount -= purchases
|
||||
self.save()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("core:user_account", kwargs={"user_id": self.user.pk})
|
||||
|
@ -21,7 +21,212 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your tests here.
|
||||
from club.models import Club
|
||||
from core.models import User, RealGroup
|
||||
from counter.models import Customer, Product, Selling, Counter, Refilling
|
||||
from subscription.models import Subscription
|
||||
|
||||
|
||||
class MergeUserTest(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
call_command("populate")
|
||||
cls.ae = Club.objects.get(unix_name="ae")
|
||||
cls.eboutic = Counter.objects.get(name="Eboutic")
|
||||
cls.barbar = Product.objects.get(code="BARB")
|
||||
cls.barbar.selling_price = 2
|
||||
cls.barbar.save()
|
||||
cls.root = User.objects.get(username="root")
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.to_keep = User(username="to_keep", password="plop", email="u.1@utbm.fr")
|
||||
self.to_delete = User(username="to_del", password="plop", email="u.2@utbm.fr")
|
||||
self.to_keep.save()
|
||||
self.to_delete.save()
|
||||
self.client.login(username="root", password="plop")
|
||||
|
||||
def test_simple(self):
|
||||
self.to_delete.first_name = "Biggus"
|
||||
self.to_keep.last_name = "Dickus"
|
||||
self.to_keep.nick_name = "B'ian"
|
||||
self.to_keep.address = "Jerusalem"
|
||||
self.to_delete.parent_address = "Rome"
|
||||
self.to_delete.address = "Rome"
|
||||
subscribers = RealGroup.objects.get(name="Subscribers")
|
||||
mde_admin = RealGroup.objects.get(name="MDE admin")
|
||||
sas_admin = RealGroup.objects.get(name="SAS admin")
|
||||
self.to_keep.groups.add(subscribers.id)
|
||||
self.to_delete.groups.add(mde_admin.id)
|
||||
self.to_keep.groups.add(sas_admin.id)
|
||||
self.to_delete.groups.add(sas_admin.id)
|
||||
self.to_delete.save()
|
||||
self.to_keep.save()
|
||||
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
|
||||
res = self.client.post(reverse("rootplace:merge"), data)
|
||||
self.assertRedirects(res, self.to_keep.get_absolute_url())
|
||||
self.assertFalse(User.objects.filter(pk=self.to_delete.pk).exists())
|
||||
self.to_keep = User.objects.get(pk=self.to_keep.pk)
|
||||
# fields of to_delete should be assigned to to_keep
|
||||
# if they were not set beforehand
|
||||
self.assertEqual("Biggus", self.to_keep.first_name)
|
||||
self.assertEqual("Dickus", self.to_keep.last_name)
|
||||
self.assertEqual("B'ian", self.to_keep.nick_name)
|
||||
self.assertEqual("Jerusalem", self.to_keep.address)
|
||||
self.assertEqual("Rome", self.to_keep.parent_address)
|
||||
self.assertEqual(3, self.to_keep.groups.count())
|
||||
groups = list(self.to_keep.groups.all())
|
||||
expected = [subscribers, mde_admin, sas_admin]
|
||||
self.assertCountEqual(groups, expected)
|
||||
|
||||
def test_both_subscribers_and_with_account(self):
|
||||
Customer(user=self.to_keep, account_id="11000l", amount=0).save()
|
||||
Customer(user=self.to_delete, account_id="12000m", amount=0).save()
|
||||
Refilling(
|
||||
amount=10,
|
||||
operator=self.root,
|
||||
customer=self.to_keep.customer,
|
||||
counter=self.eboutic,
|
||||
).save()
|
||||
Refilling(
|
||||
amount=20,
|
||||
operator=self.root,
|
||||
customer=self.to_delete.customer,
|
||||
counter=self.eboutic,
|
||||
).save()
|
||||
Selling(
|
||||
label="barbar",
|
||||
counter=self.eboutic,
|
||||
club=self.ae,
|
||||
product=self.barbar,
|
||||
customer=self.to_keep.customer,
|
||||
seller=self.root,
|
||||
unit_price=2,
|
||||
quantity=2,
|
||||
payment_method="SITH_ACCOUNT",
|
||||
).save()
|
||||
Selling(
|
||||
label="barbar",
|
||||
counter=self.eboutic,
|
||||
club=self.ae,
|
||||
product=self.barbar,
|
||||
customer=self.to_delete.customer,
|
||||
seller=self.root,
|
||||
unit_price=2,
|
||||
quantity=4,
|
||||
payment_method="SITH_ACCOUNT",
|
||||
).save()
|
||||
today = date.today()
|
||||
# both subscriptions began last month and shall end in 5 months
|
||||
Subscription(
|
||||
member=self.to_keep,
|
||||
subscription_type="un-semestre",
|
||||
payment_method="EBOUTIC",
|
||||
subscription_start=today - timedelta(30),
|
||||
subscription_end=today + timedelta(5 * 30),
|
||||
).save()
|
||||
Subscription(
|
||||
member=self.to_delete,
|
||||
subscription_type="un-semestre",
|
||||
payment_method="EBOUTIC",
|
||||
subscription_start=today - timedelta(30),
|
||||
subscription_end=today + timedelta(5 * 30),
|
||||
).save()
|
||||
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
|
||||
res = self.client.post(reverse("rootplace:merge"), data)
|
||||
self.to_keep = User.objects.get(pk=self.to_keep.id)
|
||||
self.assertRedirects(res, self.to_keep.get_absolute_url())
|
||||
# to_keep had 10€ at first and bought 2 barbar worth 2€ each
|
||||
# to_delete had 20€ and bought 4 barbar
|
||||
# total should be 10 - 4 + 20 - 8 = 18
|
||||
self.assertAlmostEqual(18, self.to_keep.customer.amount, delta=0.0001)
|
||||
self.assertEqual(2, self.to_keep.customer.buyings.count())
|
||||
self.assertEqual(2, self.to_keep.customer.refillings.count())
|
||||
self.assertTrue(self.to_keep.is_subscribed)
|
||||
# to_keep had 5 months of subscription remaining and received
|
||||
# 5 more months from to_delete, so he should be subscribed for 10 months
|
||||
self.assertEqual(
|
||||
today + timedelta(10 * 30),
|
||||
self.to_keep.subscriptions.order_by("subscription_end")
|
||||
.last()
|
||||
.subscription_end,
|
||||
)
|
||||
|
||||
def test_godfathers(self):
|
||||
users = list(User.objects.all()[:4])
|
||||
self.to_keep.godfathers.add(users[0])
|
||||
self.to_keep.godchildren.add(users[1])
|
||||
self.to_delete.godfathers.add(users[2])
|
||||
self.to_delete.godfathers.add(self.to_keep)
|
||||
self.to_delete.godchildren.add(users[3])
|
||||
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
|
||||
res = self.client.post(reverse("rootplace:merge"), data)
|
||||
self.assertRedirects(res, self.to_keep.get_absolute_url())
|
||||
self.to_keep = User.objects.get(pk=self.to_keep.id)
|
||||
self.assertCountEqual(list(self.to_keep.godfathers.all()), [users[0], users[2]])
|
||||
self.assertCountEqual(
|
||||
list(self.to_keep.godchildren.all()), [users[1], users[3]]
|
||||
)
|
||||
|
||||
def test_keep_has_no_account(self):
|
||||
Customer(user=self.to_delete, account_id="12000m", amount=0).save()
|
||||
Refilling(
|
||||
amount=20,
|
||||
operator=self.root,
|
||||
customer=self.to_delete.customer,
|
||||
counter=self.eboutic,
|
||||
).save()
|
||||
Selling(
|
||||
label="barbar",
|
||||
counter=self.eboutic,
|
||||
club=self.ae,
|
||||
product=self.barbar,
|
||||
customer=self.to_delete.customer,
|
||||
seller=self.root,
|
||||
unit_price=2,
|
||||
quantity=4,
|
||||
payment_method="SITH_ACCOUNT",
|
||||
).save()
|
||||
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
|
||||
res = self.client.post(reverse("rootplace:merge"), data)
|
||||
self.to_keep = User.objects.get(pk=self.to_keep.id)
|
||||
self.assertRedirects(res, self.to_keep.get_absolute_url())
|
||||
# to_delete had 20€ and bought 4 barbar worth 2€ each
|
||||
# total should be 20 - 8 = 12
|
||||
self.assertTrue(hasattr(self.to_keep, "customer"))
|
||||
self.assertAlmostEqual(12, self.to_keep.customer.amount, delta=0.0001)
|
||||
|
||||
def test_delete_has_no_account(self):
|
||||
Customer(user=self.to_keep, account_id="12000m", amount=0).save()
|
||||
Refilling(
|
||||
amount=20,
|
||||
operator=self.root,
|
||||
customer=self.to_keep.customer,
|
||||
counter=self.eboutic,
|
||||
).save()
|
||||
Selling(
|
||||
label="barbar",
|
||||
counter=self.eboutic,
|
||||
club=self.ae,
|
||||
product=self.barbar,
|
||||
customer=self.to_keep.customer,
|
||||
seller=self.root,
|
||||
unit_price=2,
|
||||
quantity=4,
|
||||
payment_method="SITH_ACCOUNT",
|
||||
).save()
|
||||
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
|
||||
res = self.client.post(reverse("rootplace:merge"), data)
|
||||
self.to_keep = User.objects.get(pk=self.to_keep.id)
|
||||
self.assertRedirects(res, self.to_keep.get_absolute_url())
|
||||
# to_keep had 20€ and bought 4 barbar worth 2€ each
|
||||
# total should be 20 - 8 = 12
|
||||
self.assertTrue(hasattr(self.to_keep, "customer"))
|
||||
self.assertAlmostEqual(12, self.to_keep.customer.amount, delta=0.0001)
|
||||
|
@ -23,72 +23,114 @@
|
||||
#
|
||||
#
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.edit import FormView
|
||||
from django.views.generic import ListView
|
||||
from django.urls import reverse
|
||||
from ajax_select.fields import AutoCompleteSelectField
|
||||
from django import forms
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from ajax_select.fields import AutoCompleteSelectField
|
||||
|
||||
from core.models import User, OperationLog, SithFile
|
||||
from core.views import CanEditPropMixin
|
||||
from core.models import User, OperationLog
|
||||
from counter.models import Customer
|
||||
|
||||
from forum.models import ForumMessageMeta
|
||||
|
||||
|
||||
def merge_users(u1, u2):
|
||||
u1.nick_name = u1.nick_name or u2.nick_name
|
||||
u1.date_of_birth = u1.date_of_birth or u2.date_of_birth
|
||||
u1.home = u1.home or u2.home
|
||||
u1.sex = u1.sex or u2.sex
|
||||
u1.pronouns = u1.pronouns or u2.pronouns
|
||||
u1.tshirt_size = u1.tshirt_size or u2.tshirt_size
|
||||
u1.role = u1.role or u2.role
|
||||
u1.department = u1.department or u2.department
|
||||
u1.dpt_option = u1.dpt_option or u2.dpt_option
|
||||
u1.semester = u1.semester or u2.semester
|
||||
u1.quote = u1.quote or u2.quote
|
||||
u1.school = u1.school or u2.school
|
||||
u1.promo = u1.promo or u2.promo
|
||||
u1.forum_signature = u1.forum_signature or u2.forum_signature
|
||||
u1.second_email = u1.second_email or u2.second_email
|
||||
u1.phone = u1.phone or u2.phone
|
||||
u1.parent_phone = u1.parent_phone or u2.parent_phone
|
||||
u1.address = u1.address or u2.address
|
||||
u1.parent_address = u1.parent_address or u2.parent_address
|
||||
def __merge_subscriptions(u1: User, u2: User):
|
||||
"""
|
||||
Give all the subscriptions of the second user to first one
|
||||
If some subscriptions are still active, update their end date
|
||||
to increase the overall subscription time of the first user.
|
||||
|
||||
Some examples :
|
||||
- if u1 is not subscribed, his subscription end date become the one of u2
|
||||
- if u1 is subscribed but not u2, nothing happen
|
||||
- if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months,
|
||||
he shall then be subscribed for 5 months
|
||||
"""
|
||||
last_subscription = (
|
||||
u1.subscriptions.filter(
|
||||
subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
|
||||
)
|
||||
.order_by("subscription_end")
|
||||
.last()
|
||||
)
|
||||
if last_subscription is not None:
|
||||
subscription_end = last_subscription.subscription_end
|
||||
for subscription in u2.subscriptions.filter(
|
||||
subscription_end__gte=timezone.now()
|
||||
):
|
||||
subscription.subscription_start = subscription_end
|
||||
if subscription.subscription_start > timezone.now().date():
|
||||
remaining = subscription.subscription_end - timezone.now().date()
|
||||
else:
|
||||
remaining = (
|
||||
subscription.subscription_end - subscription.subscription_start
|
||||
)
|
||||
subscription_end += remaining
|
||||
subscription.subscription_end = subscription_end
|
||||
subscription.save()
|
||||
u2.subscriptions.all().update(member=u1)
|
||||
|
||||
|
||||
def __merge_pictures(u1: User, u2: User) -> None:
|
||||
SithFile.objects.filter(owner=u2).update(owner=u1)
|
||||
if u1.profile_pict is None and u2.profile_pict is not None:
|
||||
u1.profile_pict, u2.profile_pict = u2.profile_pict, None
|
||||
if u1.scrub_pict is None and u2.scrub_pict is not None:
|
||||
u1.scrub_pict, u2.scrub_pict = u2.scrub_pict, None
|
||||
if u1.avatar_pict is None and u2.avatar_pict is not None:
|
||||
u1.avatar_pict, u2.avatar_pict = u2.avatar_pict, None
|
||||
u2.save()
|
||||
u1.save()
|
||||
for u in u2.godfathers.all():
|
||||
u1.godfathers.add(u)
|
||||
|
||||
|
||||
def merge_users(u1: User, u2: User) -> User:
|
||||
"""
|
||||
Merge u2 into u1
|
||||
This means that u1 shall receive everything that belonged to u2 :
|
||||
|
||||
- pictures
|
||||
- refills of the sith account
|
||||
- purchases of any item bought on the eboutic or the counters
|
||||
- subscriptions
|
||||
- godfathers
|
||||
- godchildren
|
||||
|
||||
If u1 had no account id, he shall receive the one of u2.
|
||||
If u1 and u2 were both in the middle of a subscription, the remaining
|
||||
durations stack
|
||||
If u1 had no profile picture, he shall receive the one of u2
|
||||
"""
|
||||
for field in u1._meta.fields:
|
||||
if not field.is_relation and not u1.__dict__[field.name]:
|
||||
u1.__dict__[field.name] = u2.__dict__[field.name]
|
||||
for group in u2.groups.all():
|
||||
u1.groups.add(group.id)
|
||||
for godfather in u2.godfathers.exclude(id=u1.id):
|
||||
u1.godfathers.add(godfather)
|
||||
for godchild in u2.godchildren.exclude(id=u1.id):
|
||||
u1.godchildren.add(godchild)
|
||||
__merge_subscriptions(u1, u2)
|
||||
__merge_pictures(u1, u2)
|
||||
u2.invoices.all().update(user=u1)
|
||||
c_src = Customer.objects.filter(user=u2).first()
|
||||
if c_src is not None:
|
||||
c_dest, created = Customer.get_or_create(u1)
|
||||
c_src.refillings.update(customer=c_dest)
|
||||
c_src.buyings.update(customer=c_dest)
|
||||
c_dest.recompute_amount()
|
||||
if created:
|
||||
# swap the account numbers, so that the user keep
|
||||
# the id he is accustomed to
|
||||
tmp_id = c_src.account_id
|
||||
# delete beforehand in order not to have a unique constraint violation
|
||||
c_src.delete()
|
||||
c_dest.account_id = tmp_id
|
||||
u1.save()
|
||||
for i in u2.invoices.all():
|
||||
for f in i._meta.local_fields: # I have sadly not found anything better :/
|
||||
if f.name == "date":
|
||||
f.auto_now = False
|
||||
u1.invoices.add(i)
|
||||
u1.save()
|
||||
s1 = User.objects.filter(id=u1.id).first()
|
||||
s2 = User.objects.filter(id=u2.id).first()
|
||||
for s in s2.subscriptions.all():
|
||||
s1.subscriptions.add(s)
|
||||
s1.save()
|
||||
c1 = Customer.objects.filter(user__id=u1.id).first()
|
||||
c2 = Customer.objects.filter(user__id=u2.id).first()
|
||||
if c1 and c2:
|
||||
for r in c2.refillings.all():
|
||||
c1.refillings.add(r)
|
||||
c1.save()
|
||||
for s in c2.buyings.all():
|
||||
c1.buyings.add(s)
|
||||
c1.save()
|
||||
elif c2 and not c1:
|
||||
c2.user = u1
|
||||
c1 = c2
|
||||
c1.save()
|
||||
c1.recompute_amount()
|
||||
u2.delete()
|
||||
u2.delete() # everything remaining in u2 gets deleted thanks to on_delete=CASCADE
|
||||
return u1
|
||||
|
||||
|
||||
@ -128,9 +170,8 @@ class MergeUsersView(FormView):
|
||||
form_class = MergeForm
|
||||
|
||||
def dispatch(self, request, *arg, **kwargs):
|
||||
res = super(MergeUsersView, self).dispatch(request, *arg, **kwargs)
|
||||
if request.user.is_root:
|
||||
return res
|
||||
return super().dispatch(request, *arg, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -140,7 +181,7 @@ class MergeUsersView(FormView):
|
||||
return super(MergeUsersView, self).form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("core:user_profile", kwargs={"user_id": self.final_user.id})
|
||||
return self.final_user.get_absolute_url()
|
||||
|
||||
|
||||
class DeleteAllForumUserMessagesView(FormView):
|
||||
|
@ -165,7 +165,4 @@ class Subscription(models.Model):
|
||||
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root
|
||||
|
||||
def is_valid_now(self):
|
||||
return (
|
||||
self.subscription_start <= date.today()
|
||||
and date.today() <= self.subscription_end
|
||||
)
|
||||
return self.subscription_start <= date.today() <= self.subscription_end
|
||||
|
Loading…
Reference in New Issue
Block a user