From 3a5eceba83057f4705c0f8120cad90a7b34a435f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 May 2019 21:15:58 +0200 Subject: [PATCH 01/13] Delete unused models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certains modèles n'étaient pas utilisés dans le code, on en profite pour les virer. --- kfet/models.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index 5a8ea858..e25dea22 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -384,16 +384,6 @@ class Checkout(models.Model): return ret -class CheckoutTransfer(models.Model): - from_checkout = models.ForeignKey( - Checkout, on_delete=models.PROTECT, related_name="transfers_from" - ) - to_checkout = models.ForeignKey( - Checkout, on_delete=models.PROTECT, related_name="transfers_to" - ) - amount = models.DecimalField(max_digits=6, decimal_places=2) - - class CheckoutStatement(models.Model): by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") checkout = models.ForeignKey( @@ -526,16 +516,6 @@ class Article(models.Model): return to_ukf(self.price) -class ArticleRule(models.Model): - article_on = models.OneToOneField( - Article, on_delete=models.PROTECT, related_name="rule_on" - ) - article_to = models.OneToOneField( - Article, on_delete=models.PROTECT, related_name="rule_to" - ) - ratio = models.PositiveSmallIntegerField() - - class Inventory(models.Model): articles = models.ManyToManyField( Article, through="InventoryArticle", related_name="inventories" From 85b1e974ff964711b53062c1f65dde94f07bdf29 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 May 2019 22:31:07 +0200 Subject: [PATCH 02/13] Change deletion behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tous les `on_delete` étaient mis à PROTECT, ce qui faisait qu'on ne pouvait rien supprimer... On les met à CASCADE pour tous les modèles secondaires (`AccountNegative`, `CheckoutStatement`, `InventoryArticle`, `SupplierArticle`, `Order` et `OrderArticle`) et pour les inventaires créés à partir d'une commande. Pour les modèles qui demandent une validation, et pour les `Operation`s, on met à NULL le compte ou l'article associé (cela ne change pas le total d'une opération, qui est la partie importante à garder). --- kfet/migrations/0066_on_delete_actions.py | 133 ++++++++++++++++++++++ kfet/models.py | 26 ++--- 2 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 kfet/migrations/0066_on_delete_actions.py diff --git a/kfet/migrations/0066_on_delete_actions.py b/kfet/migrations/0066_on_delete_actions.py new file mode 100644 index 00000000..157e1247 --- /dev/null +++ b/kfet/migrations/0066_on_delete_actions.py @@ -0,0 +1,133 @@ +# Generated by Django 2.2 on 2019-05-23 13:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("kfet", "0065_choices_promo")] + + operations = [ + migrations.RemoveField(model_name="checkouttransfer", name="from_checkout"), + migrations.RemoveField(model_name="checkouttransfer", name="to_checkout"), + migrations.AlterField( + model_name="accountnegative", + name="account", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="negative", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="checkoutstatement", + name="checkout", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="statements", + to="kfet.Checkout", + ), + ), + migrations.AlterField( + model_name="inventory", + name="order", + field=models.OneToOneField( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="inventory", + to="kfet.Order", + ), + ), + migrations.AlterField( + model_name="inventoryarticle", + name="article", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="kfet.Article" + ), + ), + migrations.AlterField( + model_name="inventoryarticle", + name="inventory", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="kfet.Inventory" + ), + ), + migrations.AlterField( + model_name="operation", + name="article", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="operations", + to="kfet.Article", + ), + ), + migrations.AlterField( + model_name="operationgroup", + name="valid_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="order", + name="supplier", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="orders", + to="kfet.Supplier", + ), + ), + migrations.AlterField( + model_name="orderarticle", + name="article", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="kfet.Article" + ), + ), + migrations.AlterField( + model_name="orderarticle", + name="order", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="kfet.Order" + ), + ), + migrations.AlterField( + model_name="supplierarticle", + name="article", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="kfet.Article" + ), + ), + migrations.AlterField( + model_name="supplierarticle", + name="supplier", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="kfet.Supplier" + ), + ), + migrations.AlterField( + model_name="transfergroup", + name="valid_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="kfet.Account", + ), + ), + migrations.DeleteModel(name="ArticleRule"), + migrations.DeleteModel(name="CheckoutTransfer"), + ] diff --git a/kfet/models.py b/kfet/models.py index e25dea22..f87068a6 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -310,7 +310,7 @@ class AccountNegative(models.Model): objects = AccountNegativeManager() account = models.OneToOneField( - Account, on_delete=models.PROTECT, related_name="negative" + Account, on_delete=models.CASCADE, related_name="negative" ) start = models.DateTimeField(blank=True, null=True, default=None) balance_offset = models.DecimalField( @@ -387,7 +387,7 @@ class Checkout(models.Model): class CheckoutStatement(models.Model): by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") checkout = models.ForeignKey( - Checkout, on_delete=models.PROTECT, related_name="statements" + Checkout, on_delete=models.CASCADE, related_name="statements" ) balance_old = models.DecimalField( "ancienne balance", max_digits=6, decimal_places=2 @@ -525,7 +525,7 @@ class Inventory(models.Model): # Optional order = models.OneToOneField( "Order", - on_delete=models.PROTECT, + on_delete=models.CASCADE, related_name="inventory", blank=True, null=True, @@ -540,8 +540,8 @@ class Inventory(models.Model): class InventoryArticle(models.Model): - inventory = models.ForeignKey(Inventory, on_delete=models.PROTECT) - article = models.ForeignKey(Article, on_delete=models.PROTECT) + inventory = models.ForeignKey(Inventory, on_delete=models.CASCADE) + article = models.ForeignKey(Article, on_delete=models.CASCADE) stock_old = models.IntegerField() stock_new = models.IntegerField() stock_error = models.IntegerField(default=0) @@ -572,8 +572,8 @@ class Supplier(models.Model): class SupplierArticle(models.Model): - supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT) - article = models.ForeignKey(Article, on_delete=models.PROTECT) + supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) + article = models.ForeignKey(Article, on_delete=models.CASCADE) at = models.DateTimeField(auto_now_add=True) price_HT = models.DecimalField( max_digits=7, decimal_places=4, blank=True, null=True, default=None @@ -588,7 +588,7 @@ class SupplierArticle(models.Model): class Order(models.Model): supplier = models.ForeignKey( - Supplier, on_delete=models.PROTECT, related_name="orders" + Supplier, on_delete=models.CASCADE, related_name="orders" ) articles = models.ManyToManyField( Article, through="OrderArticle", related_name="orders" @@ -601,8 +601,8 @@ class Order(models.Model): class OrderArticle(models.Model): - order = models.ForeignKey(Order, on_delete=models.PROTECT) - article = models.ForeignKey(Article, on_delete=models.PROTECT) + order = models.ForeignKey(Order, on_delete=models.CASCADE) + article = models.ForeignKey(Article, on_delete=models.CASCADE) quantity_ordered = models.IntegerField() quantity_received = models.IntegerField(default=0) @@ -613,7 +613,7 @@ class TransferGroup(models.Model): comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( Account, - on_delete=models.PROTECT, + on_delete=models.SET_NULL, related_name="+", blank=True, null=True, @@ -661,7 +661,7 @@ class OperationGroup(models.Model): comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( Account, - on_delete=models.PROTECT, + on_delete=models.SET_NULL, related_name="+", blank=True, null=True, @@ -697,7 +697,7 @@ class Operation(models.Model): # Optional article = models.ForeignKey( Article, - on_delete=models.PROTECT, + on_delete=models.SET_NULL, related_name="operations", blank=True, null=True, From 63fff6ca7c62d6d6e52b6847fcbb6e77e35d3d28 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 23 May 2019 16:09:19 +0200 Subject: [PATCH 03/13] Setup deleted account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pour pouvoir supprimer un compte, on crée un compte dummy qui a pour but de recevoir les objets non supprimables (caisses, transferts/opérations pour statistiques, etc.). Lors de la délétion d'un compte, tout est transféré sur le dummy, qui est créé via migration. --- kfet/__init__.py | 2 + kfet/migrations/0067_deleted_account.py | 32 +++++++ kfet/migrations/0068_on_delete_account.py | 103 ++++++++++++++++++++++ kfet/models.py | 37 ++++---- 4 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 kfet/migrations/0067_deleted_account.py create mode 100644 kfet/migrations/0068_on_delete_account.py diff --git a/kfet/__init__.py b/kfet/__init__.py index 42ea33b1..47a6b0b8 100644 --- a/kfet/__init__.py +++ b/kfet/__init__.py @@ -1 +1,3 @@ default_app_config = "kfet.apps.KFetConfig" +KFET_DELETED_TRIGRAMME = "☠☠☠" +KFET_DELETED_USERNAME = "kfet_deleted_user" diff --git a/kfet/migrations/0067_deleted_account.py b/kfet/migrations/0067_deleted_account.py new file mode 100644 index 00000000..155034f9 --- /dev/null +++ b/kfet/migrations/0067_deleted_account.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2 on 2019-05-23 13:54 + +from django.db import migrations, models + +from kfet import KFET_DELETED_TRIGRAMME, KFET_DELETED_USERNAME + + +def setup_kfet_deleted_user(apps, schema_editor): + """ + Setup models instances for the kfet deleted account. + + Username and trigramme are retrieved from kfet.__init__ module. + Other data are registered here. + """ + User = apps.get_model("auth", "User") + CofProfile = apps.get_model("gestioncof", "CofProfile") + Account = apps.get_model("kfet", "Account") + + user, _ = User.objects.update_or_create( + username=KFET_DELETED_USERNAME, defaults={"first_name": "Compte K-Fêt supprimé"} + ) + profile, _ = CofProfile.objects.update_or_create(user=user) + account, _ = Account.objects.update_or_create( + cofprofile=profile, defaults={"trigramme": KFET_DELETED_TRIGRAMME} + ) + + +class Migration(migrations.Migration): + + dependencies = [("kfet", "0066_on_delete_actions")] + + operations = [migrations.RunPython(setup_kfet_deleted_user)] diff --git a/kfet/migrations/0068_on_delete_account.py b/kfet/migrations/0068_on_delete_account.py new file mode 100644 index 00000000..8bb79af6 --- /dev/null +++ b/kfet/migrations/0068_on_delete_account.py @@ -0,0 +1,103 @@ +# Generated by Django 2.2 on 2019-05-23 16:17 + +from django.db import migrations, models + +import kfet.models + + +class Migration(migrations.Migration): + + dependencies = [("kfet", "0067_deleted_account")] + + operations = [ + migrations.AlterField( + model_name="checkout", + name="created_by", + field=models.ForeignKey( + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="checkoutstatement", + name="by", + field=models.ForeignKey( + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="inventory", + name="by", + field=models.ForeignKey( + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="operation", + name="addcost_for", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="addcosts", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="operation", + name="canceled_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="operationgroup", + name="on_acc", + field=models.ForeignKey( + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="opesgroup", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="transfer", + name="canceled_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="transfer", + name="from_acc", + field=models.ForeignKey( + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="transfers_from", + to="kfet.Account", + ), + ), + migrations.AlterField( + model_name="transfer", + name="to_acc", + field=models.ForeignKey( + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="transfers_to", + to="kfet.Account", + ), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index f87068a6..afbbe142 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -12,6 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from gestioncof.models import CofProfile +from . import KFET_DELETED_TRIGRAMME from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken # noqa from .config import kfet_config @@ -267,12 +268,6 @@ class Account(models.Model): self.password = hash_password(clear_password) - # Surcharge de delete - # Pas de suppression possible - # Cas à régler plus tard - def delete(self, *args, **kwargs): - pass - def update_negative(self): if self.real_balance < 0: if hasattr(self, "negative") and not self.negative.start: @@ -299,6 +294,10 @@ class Account(models.Model): self.trigramme = trigramme +def get_deleted_account(): + return Account.objects.get(trigramme=KFET_DELETED_TRIGRAMME) + + class AccountNegativeManager(models.Manager): """Manager for AccountNegative model.""" @@ -350,7 +349,9 @@ class CheckoutQuerySet(models.QuerySet): class Checkout(models.Model): - created_by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") + created_by = models.ForeignKey( + Account, on_delete=models.SET(get_deleted_account), related_name="+" + ) name = models.CharField(max_length=45) valid_from = models.DateTimeField() valid_to = models.DateTimeField() @@ -385,7 +386,9 @@ class Checkout(models.Model): class CheckoutStatement(models.Model): - by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") + by = models.ForeignKey( + Account, on_delete=models.SET(get_deleted_account), related_name="+" + ) checkout = models.ForeignKey( Checkout, on_delete=models.CASCADE, related_name="statements" ) @@ -520,7 +523,9 @@ class Inventory(models.Model): articles = models.ManyToManyField( Article, through="InventoryArticle", related_name="inventories" ) - by = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="+") + by = models.ForeignKey( + Account, on_delete=models.SET(get_deleted_account), related_name="+" + ) at = models.DateTimeField(auto_now_add=True) # Optional order = models.OneToOneField( @@ -626,16 +631,18 @@ class Transfer(models.Model): TransferGroup, on_delete=models.PROTECT, related_name="transfers" ) from_acc = models.ForeignKey( - Account, on_delete=models.PROTECT, related_name="transfers_from" + Account, + on_delete=models.SET(get_deleted_account), + related_name="transfers_from", ) to_acc = models.ForeignKey( - Account, on_delete=models.PROTECT, related_name="transfers_to" + Account, on_delete=models.SET(get_deleted_account), related_name="transfers_to" ) amount = models.DecimalField(max_digits=6, decimal_places=2) # Optional canceled_by = models.ForeignKey( Account, - on_delete=models.PROTECT, + on_delete=models.SET(get_deleted_account), null=True, blank=True, default=None, @@ -649,7 +656,7 @@ class Transfer(models.Model): class OperationGroup(models.Model): on_acc = models.ForeignKey( - Account, on_delete=models.PROTECT, related_name="opesgroup" + Account, on_delete=models.SET(get_deleted_account), related_name="opesgroup" ) checkout = models.ForeignKey( Checkout, on_delete=models.PROTECT, related_name="opesgroup" @@ -706,7 +713,7 @@ class Operation(models.Model): article_nb = models.PositiveSmallIntegerField(blank=True, null=True, default=None) canceled_by = models.ForeignKey( Account, - on_delete=models.PROTECT, + on_delete=models.SET(get_deleted_account), related_name="+", blank=True, null=True, @@ -715,7 +722,7 @@ class Operation(models.Model): canceled_at = models.DateTimeField(blank=True, null=True, default=None) addcost_for = models.ForeignKey( Account, - on_delete=models.PROTECT, + on_delete=models.SET(get_deleted_account), related_name="addcosts", blank=True, null=True, From 08ac0ac8908fda6a9f45a160b78de5cb2713cb93 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 May 2019 14:30:00 +0200 Subject: [PATCH 04/13] Vues de suppression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On rajoute un bouton de suppression d'un compte utilisable avec la perm `kfet.delete_account`, avec message de vérif. On en profite pour cleanup un peu le css de `jconfirm`. --- kfet/static/kfet/css/base/fixed.css | 8 ++++++ kfet/static/kfet/css/libs/jconfirm-kfet.css | 14 +++++++-- kfet/templates/kfet/left_account.html | 28 ++++++++++++++++++ kfet/urls.py | 6 ++++ kfet/views.py | 32 ++++++++++++++++++++- 5 files changed, 84 insertions(+), 4 deletions(-) diff --git a/kfet/static/kfet/css/base/fixed.css b/kfet/static/kfet/css/base/fixed.css index d198c50f..270331d8 100644 --- a/kfet/static/kfet/css/base/fixed.css +++ b/kfet/static/kfet/css/base/fixed.css @@ -57,12 +57,20 @@ aside .heading .sub { aside .buttons { margin-left: -15px; margin-right: -15px; + flex-wrap: wrap; } aside .buttons > * { flex: 0 1 auto !important; } +aside .buttons > hr { + flex-basis: 100%; + height: 0; + margin: 0; + border: 0; +} + /* Aside - Text */ diff --git a/kfet/static/kfet/css/libs/jconfirm-kfet.css b/kfet/static/kfet/css/libs/jconfirm-kfet.css index d2803434..a50e22d6 100644 --- a/kfet/static/kfet/css/libs/jconfirm-kfet.css +++ b/kfet/static/kfet/css/libs/jconfirm-kfet.css @@ -23,11 +23,19 @@ } .jconfirm .jconfirm-box .content-pane { - margin:0 !important; + border-bottom:1px solid #ddd; + margin: 0px !important; } .jconfirm .jconfirm-box .content { - border-bottom:1px solid #ddd; + padding: 5px; +} + +.jconfirm .jconfirm-box .content div.warning { + font-size: 16px; + font-weight: bold; + text-align: center; + margin: 5px 0px; } .jconfirm .jconfirm-box input { @@ -43,7 +51,7 @@ } .jconfirm .jconfirm-box .buttons { - margin-top:-5px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */ + margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */ padding:0; height:40px; } diff --git a/kfet/templates/kfet/left_account.html b/kfet/templates/kfet/left_account.html index 18438ff1..716c96cc 100644 --- a/kfet/templates/kfet/left_account.html +++ b/kfet/templates/kfet/left_account.html @@ -18,6 +18,15 @@ Créditer + {% if perms.kfet.delete_account %} +
+ +
+ {% csrf_token %} +
+ {% endif %}
@@ -92,5 +101,24 @@ $( function() { $(this).addClass('focus'); }); + // Delete button + $('#button-delete').click(function() { + $.confirm({ + title: 'Confirmer la suppression', + content: ` +
+ Cette opération est irréversible ! +
+ Toutes les données associées à ce compte seront anonymisées. + `, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + confirm: function() { + $('#account-delete-form').submit(); + } + }) + }) }); diff --git a/kfet/urls.py b/kfet/urls.py index e3e3ad2d..44bbc153 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -57,6 +57,12 @@ urlpatterns = [ views.account_update, name="kfet.account.update", ), + # Account - Delete + path( + "accounts//delete", + views.AccountDeleteView.as_view(), + name="kfet.account.delete", + ), # Account - Groups path("accounts/groups", views.account_group, name="kfet.account.group"), path( diff --git a/kfet/views.py b/kfet/views.py index 9ce17f47..eb058eb4 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -7,6 +7,7 @@ from urllib.parse import urlencode from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.models import Permission, User from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import PermissionDenied @@ -20,7 +21,7 @@ from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic import DetailView, FormView, ListView, TemplateView from django.views.generic.detail import BaseDetailView -from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.edit import CreateView, DeleteView, UpdateView from gestioncof.models import CofProfile from kfet import consumers @@ -467,6 +468,35 @@ def account_update(request, trigramme): }, ) + # Account - Delete + + +class AccountDeleteView(PermissionRequiredMixin, DeleteView): + model = Account + slug_field = "trigramme" + slug_url_kwarg = "trigramme" + success_url = reverse_lazy("kfet.account") + success_message = "Compte supprimé avec succès !" + permission_required = "kfet.delete_account" + + def get(self, request, *args, **kwargs): + return redirect("kfet.account.read", self.kwargs.get(self.get_slug_field())) + + def delete(self, request, *args, **kwargs): + self.object = self.get_object() + if self.object.balance > 0.01: + messages.error( + request, + "Impossible de supprimer un compte " + "avec une balance strictement positive !", + ) + return redirect("kfet.account.read", self.object.trigramme) + + # SuccessMessageMixin does not work with DeleteView, see : + # https://code.djangoproject.com/ticket/21926 + messages.success(request, self.success_message) + return super().delete(request, *args, **kwargs) + class AccountNegativeList(ListView): queryset = AccountNegative.objects.select_related( From 52521e89a6cb43597d33ab9d97938ef8750bb4a8 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 May 2019 16:16:20 +0200 Subject: [PATCH 05/13] Add some restrictions on deletion --- kfet/models.py | 2 +- kfet/views.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index afbbe142..719a3b0d 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -152,7 +152,7 @@ class Account(models.Model): @property def readable(self): - return self.trigramme != "GNR" + return self.trigramme not in [KFET_DELETED_TRIGRAMME, KFET_GENERIC_TRIGRAMME] @property def is_team(self): diff --git a/kfet/views.py b/kfet/views.py index eb058eb4..f0cb796c 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -24,7 +24,7 @@ from django.views.generic.detail import BaseDetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView from gestioncof.models import CofProfile -from kfet import consumers +from kfet import KFET_DELETED_TRIGRAMME, consumers from kfet.config import kfet_config from kfet.decorators import teamkfet_required from kfet.forms import ( @@ -79,6 +79,7 @@ from kfet.models import ( ) from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest +from .auth import KFET_GENERIC_TRIGRAMME from .auth.views import ( # noqa AccountGroupCreate, AccountGroupUpdate, @@ -492,6 +493,15 @@ class AccountDeleteView(PermissionRequiredMixin, DeleteView): ) return redirect("kfet.account.read", self.object.trigramme) + if self.object.trigramme in [ + "LIQ", + KFET_GENERIC_TRIGRAMME, + KFET_DELETED_TRIGRAMME, + "#13", + ]: + messages.error(request, "Impossible de supprimer un trigramme protégé !") + return redirect("kfet.account.read", self.object.trigramme) + # SuccessMessageMixin does not work with DeleteView, see : # https://code.djangoproject.com/ticket/21926 messages.success(request, self.success_message) From f12370a6cd948909aa1169be348fbe40a34954e4 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 May 2019 18:01:04 +0200 Subject: [PATCH 06/13] Tests (!!) --- kfet/tests/test_views.py | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 5c55f150..47604cfe 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -8,6 +8,8 @@ from django.test import Client, TestCase from django.urls import reverse from django.utils import timezone +from .. import KFET_DELETED_TRIGRAMME +from ..auth import KFET_GENERIC_TRIGRAMME from ..config import kfet_config from ..models import ( Account, @@ -340,6 +342,61 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): self.assertForbiddenKfet(r) +class AccountDeleteViewTests(ViewTestCaseMixin, TestCase): + url_name = "kfet.account.delete" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/delete" + + auth_user = "team1" + auth_forbidden = [None, "user", "team"] + http_methods = ["GET", "POST"] + with_liq = True + + def get_users_extra(self): + return { + "user1": create_user("user1", "001"), + "team1": create_team("team1", "101", perms=["kfet.delete_account"]), + "trez": create_user("trez", "#13"), + } + + def test_get_redirects(self): + r = self.client.get(self.url) + self.assertRedirects(r, reverse("kfet.account.read", kwargs=self.url_kwargs)) + + def test_post_ok(self): + r = self.client.post(self.url, {}) + self.assertRedirects(r, reverse("kfet.account")) + + with self.assertRaises(Account.DoesNotExist): + self.accounts["user1"].refresh_from_db() + + def test_protected_accounts(self): + for trigramme in ["LIQ", "#13", KFET_GENERIC_TRIGRAMME, KFET_DELETED_TRIGRAMME]: + if Account.objects.get(trigramme=trigramme).readable: + expected_code = 200 + else: + expected_code = 403 + r = self.client.post( + reverse(self.url_name, kwargs={"trigramme": trigramme}), {} + ) + self.assertRedirects( + r, + reverse("kfet.account.read", kwargs={"trigramme": trigramme}), + target_status_code=expected_code, + ) + # Devrait être redondant avec le précédent, mais on sait jamais + self.assertTrue(Account.objects.filter(trigramme=trigramme).exists()) + + def test_nonempty_accounts(self): + self.accounts["user1"].balance = 1 + self.accounts["user1"].save() + + r = self.client.post(self.url, {}) + self.assertRedirects(r, reverse("kfet.account.read", kwargs=self.url_kwargs)) + # Shouldn't throw an error + self.accounts["user1"].refresh_from_db() + + class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.account.group" url_expected = "/k-fet/accounts/groups" From 123e2b84df6a6b1cf801c05a5224c77e01f6a624 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 May 2019 18:35:04 +0200 Subject: [PATCH 07/13] Rename view to fit conventions --- kfet/urls.py | 2 +- kfet/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/urls.py b/kfet/urls.py index 44bbc153..04d1cf82 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -60,7 +60,7 @@ urlpatterns = [ # Account - Delete path( "accounts//delete", - views.AccountDeleteView.as_view(), + views.AccountDelete.as_view(), name="kfet.account.delete", ), # Account - Groups diff --git a/kfet/views.py b/kfet/views.py index f0cb796c..d1a40a82 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -472,7 +472,7 @@ def account_update(request, trigramme): # Account - Delete -class AccountDeleteView(PermissionRequiredMixin, DeleteView): +class AccountDelete(PermissionRequiredMixin, DeleteView): model = Account slug_field = "trigramme" slug_url_kwarg = "trigramme" From 65dd7e5fa3ee54b389b200aaa7e7007a87cab60f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 May 2019 19:32:57 +0200 Subject: [PATCH 08/13] Suppression d'article MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On fait pareil que précédemment pour les articles, en rajoutant une vie de délétion + de quoi afficher qu'un article a été supprimé. N.B. : le formatage automatique de VSCode fait plein de changements, donc pourquoi pas les garder. --- kfet/static/kfet/js/history.js | 50 ++++++------- kfet/templates/kfet/article_read.html | 100 ++++++++++++++++---------- kfet/urls.py | 6 ++ kfet/views.py | 14 ++++ 4 files changed, 109 insertions(+), 61 deletions(-) diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 8559f050..1c6495a7 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -1,28 +1,28 @@ -function KHistory(options={}) { +function KHistory(options = {}) { $.extend(this, KHistory.default_options, options); this.$container = $(this.container); - this.reset = function() { + this.reset = function () { this.$container.html(''); }; - this.addOpeGroup = function(opegroup) { + this.addOpeGroup = function (opegroup) { var $day = this._getOrCreateDay(opegroup['at']); var $opegroup = this._opeGroupHtml(opegroup); $day.after($opegroup); var trigramme = opegroup['on_acc_trigramme']; - var is_cof = opegroup['is_cof']; - for (var i=0; i Éditer + {% if perms.kfet.delete_account %} + +
+ {% csrf_token %} +
+ {% endif %}
  • Prix: {{ article.price }}€ - +
  • Stock: {{ article.stock }}
  • En vente: {{ article.is_sold|yesno|title }}
  • @@ -63,35 +72,35 @@
    -
    -
    -
    -
    +
    +
    +
    +
    -

    Inventaires récents

    -
    - {% include "kfet/article_inventories_snippet.html" with inventoryarts=inventoryarts|slice:5 %} -
    +

    Inventaires récents

    +
    + {% include "kfet/article_inventories_snippet.html" with inventoryarts=inventoryarts|slice:5 %} +
    -
    -
    +
    +
    -

    Derniers prix fournisseurs

    -
    - {% include "kfet/article_suppliers_snippet.html" with supplierarts=supplierarts|slice:5 %} -
    +

    Derniers prix fournisseurs

    +
    + {% include "kfet/article_suppliers_snippet.html" with supplierarts=supplierarts|slice:5 %} +
    -
    -
    -
    -
    +
    +
    +
    +
    -
    -
    -

    Ventes

    -
    -
    -
    +
    +
    +

    Ventes

    +
    +
    +
    @@ -110,26 +119,45 @@
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/kfet/urls.py b/kfet/urls.py index 04d1cf82..681b7c31 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -186,6 +186,12 @@ urlpatterns = [ teamkfet_required(views.ArticleUpdate.as_view()), name="kfet.article.update", ), + # Article - Delete + path( + "articles//delete", + views.ArticleDelete.as_view(), + name="kfet.article.delete", + ), # Article - Statistics path( "articles//stat/sales/list", diff --git a/kfet/views.py b/kfet/views.py index d1a40a82..f517bbfe 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -877,6 +877,20 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): return super().form_valid(form) +class ArticleDelete(PermissionRequiredMixin, DeleteView): + model = Article + success_url = reverse_lazy("kfet.article") + success_message = "Article supprimé avec succès !" + permission_required = "kfet.delete_article" + + def get(self, request, *args, **kwargs): + return redirect("kfet.article.read", self.kwargs.get(self.pk_url_kwarg)) + + def delete(self, request, *args, **kwargs): + messages.success(request, self.success_message) + return super().delete(request, *args, **kwargs) + + # ----- # K-Psul # ----- From d4be8b426e8db65132982ca94854e983b8fdc271 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 May 2019 19:41:24 +0200 Subject: [PATCH 09/13] Tests pour la suppression d'articles --- kfet/tests/test_views.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 47604cfe..1d3d21a2 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1345,6 +1345,42 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): self.assertForbiddenKfet(r) +class ArticleDeleteViewTests(ViewTestCaseMixin, TestCase): + url_name = "kfet.article.delete" + + auth_user = "team1" + auth_forbidden = [None, "user", "team"] + + @property + def url_kwargs(self): + return {"pk": self.article.pk} + + @property + def url_expected(self): + return "/k-fet/articles/{}/delete".format(self.article.pk) + + def get_users_extra(self): + return {"team1": create_team("team1", "101", perms=["kfet.delete_article"])} + + def setUp(self): + super().setUp() + self.category = ArticleCategory.objects.create(name="Category") + self.article = Article.objects.create( + name="Article", category=self.category, stock=5, price=Decimal("2.5") + ) + + def test_get_redirects(self): + r = self.client.get(self.url) + self.assertRedirects(r, reverse("kfet.article.read", kwargs=self.url_kwargs)) + + def test_post_ok(self): + r = self.client.post(self.url, {}) + self.assertRedirects(r, reverse("kfet.article")) + + with self.assertRaises(Article.DoesNotExist): + self.article.refresh_from_db() + + class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.article.stat.sales.list" From 51fe9cc9f8f7eae0d47a332b1e8de47e1f7e552e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 29 May 2019 18:31:03 +0200 Subject: [PATCH 10/13] Changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index ec8f6077..138789e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +- On peut supprimer des comptes et des articles K-Fêt - Passage à Django2 - Dev : on peut désactiver la barre de debug avec une variable shell - Remplace les CSS de Google par des polices de proximité From f3dbb72f69176407babc8d6dde70fe0294bffc4d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Jun 2019 22:43:47 +0200 Subject: [PATCH 11/13] Consistency for on_delete attributes --- kfet/migrations/0066_on_delete_actions.py | 36 ----------------------- kfet/migrations/0068_on_delete_account.py | 24 +++++++++++++++ kfet/models.py | 6 ++-- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/kfet/migrations/0066_on_delete_actions.py b/kfet/migrations/0066_on_delete_actions.py index 157e1247..e2d635e2 100644 --- a/kfet/migrations/0066_on_delete_actions.py +++ b/kfet/migrations/0066_on_delete_actions.py @@ -29,18 +29,6 @@ class Migration(migrations.Migration): to="kfet.Checkout", ), ), - migrations.AlterField( - model_name="inventory", - name="order", - field=models.OneToOneField( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="inventory", - to="kfet.Order", - ), - ), migrations.AlterField( model_name="inventoryarticle", name="article", @@ -67,18 +55,6 @@ class Migration(migrations.Migration): to="kfet.Article", ), ), - migrations.AlterField( - model_name="operationgroup", - name="valid_by", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="kfet.Account", - ), - ), migrations.AlterField( model_name="order", name="supplier", @@ -116,18 +92,6 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, to="kfet.Supplier" ), ), - migrations.AlterField( - model_name="transfergroup", - name="valid_by", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="kfet.Account", - ), - ), migrations.DeleteModel(name="ArticleRule"), migrations.DeleteModel(name="CheckoutTransfer"), ] diff --git a/kfet/migrations/0068_on_delete_account.py b/kfet/migrations/0068_on_delete_account.py index 8bb79af6..b8cfdb76 100644 --- a/kfet/migrations/0068_on_delete_account.py +++ b/kfet/migrations/0068_on_delete_account.py @@ -70,6 +70,18 @@ class Migration(migrations.Migration): to="kfet.Account", ), ), + migrations.AlterField( + model_name="operationgroup", + name="valid_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), migrations.AlterField( model_name="transfer", name="canceled_by", @@ -100,4 +112,16 @@ class Migration(migrations.Migration): to="kfet.Account", ), ), + migrations.AlterField( + model_name="transfergroup", + name="valid_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=models.SET(kfet.models.get_deleted_account), + related_name="+", + to="kfet.Account", + ), + ), ] diff --git a/kfet/models.py b/kfet/models.py index 719a3b0d..5d8ad3cb 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -530,7 +530,7 @@ class Inventory(models.Model): # Optional order = models.OneToOneField( "Order", - on_delete=models.CASCADE, + on_delete=models.PROTECT, related_name="inventory", blank=True, null=True, @@ -618,7 +618,7 @@ class TransferGroup(models.Model): comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( Account, - on_delete=models.SET_NULL, + on_delete=models.SET(get_deleted_account), related_name="+", blank=True, null=True, @@ -668,7 +668,7 @@ class OperationGroup(models.Model): comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( Account, - on_delete=models.SET_NULL, + on_delete=models.SET(get_deleted_account), related_name="+", blank=True, null=True, From c4948be1f7c9b2fe93fa8171f842eeeb84c9f360 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Jun 2019 22:59:43 +0200 Subject: [PATCH 12/13] Use http_methods_allowed attribute --- kfet/tests/test_views.py | 4 ++-- kfet/views.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 1d3d21a2..ec6565d7 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -359,9 +359,9 @@ class AccountDeleteViewTests(ViewTestCaseMixin, TestCase): "trez": create_user("trez", "#13"), } - def test_get_redirects(self): + def test_get_405(self): r = self.client.get(self.url) - self.assertRedirects(r, reverse("kfet.account.read", kwargs=self.url_kwargs)) + self.assertEqual(r.status_code, 405) def test_post_ok(self): r = self.client.post(self.url, {}) diff --git a/kfet/views.py b/kfet/views.py index f517bbfe..24f0871d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -480,8 +480,7 @@ class AccountDelete(PermissionRequiredMixin, DeleteView): success_message = "Compte supprimé avec succès !" permission_required = "kfet.delete_account" - def get(self, request, *args, **kwargs): - return redirect("kfet.account.read", self.kwargs.get(self.get_slug_field())) + http_method_names = ["post"] def delete(self, request, *args, **kwargs): self.object = self.get_object() From 56bc281b30bb2f3f5ecbe00c0910df6aca70f001 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Jun 2019 23:00:10 +0200 Subject: [PATCH 13/13] Utilise >= gnagnagna --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 24f0871d..ff71f1e0 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -484,7 +484,7 @@ class AccountDelete(PermissionRequiredMixin, DeleteView): def delete(self, request, *args, **kwargs): self.object = self.get_object() - if self.object.balance > 0.01: + if self.object.balance >= 0.01: messages.error( request, "Impossible de supprimer un compte "