From 15e13f978c9793c15478699b2bb3b3f54469005f Mon Sep 17 00:00:00 2001 From: nanoy Date: Sat, 6 Oct 2018 00:03:02 +0200 Subject: [PATCH] Initial commit pas initial --- .vscode/settings.json | 3 + coopeV3/settings.py | 3 + coopeV3/templatetags/__init__.py | 0 coopeV3/templatetags/vip.py | 40 ++++ coopeV3/urls.py | 1 + coopeV3/views.py | 2 +- coopeV3/widgets.py | 8 +- gestion/admin.py | 7 +- gestion/forms.py | 37 +++ gestion/migrations/0001_initial.py | 111 ++++----- gestion/models.py | 75 ++++--- gestion/templates/gestion/manage.html | 212 ++++++++++++++++++ gestion/templates/gestion/products_index.html | 55 +++++ gestion/templates/gestion/products_list.html | 55 +++++ gestion/urls.py | 7 + gestion/views.py | 108 ++++++++- preferences/admin.py | 5 + preferences/forms.py | 30 +++ preferences/migrations/0001_initial.py | 15 +- preferences/models.py | 12 +- .../preferences/cotisations_index.html | 35 +++ .../preferences/general_preferences.html | 89 ++++++++ .../preferences/payment_methods_index.html | 39 ++++ preferences/urls.py | 16 ++ preferences/views.py | 76 ++++++- requirements.txt | 1 + .../autocomplete_light/autocomplete.init.js | 162 +++++++++++++ static/autocomplete_light/forward.js | 183 +++++++++++++++ static/autocomplete_light/jquery.init.js | 36 +++ .../autocomplete_light/jquery.post-setup.js | 7 + static/autocomplete_light/select2.css | 9 + static/autocomplete_light/select2.js | 116 ++++++++++ static/css/autocomplete.css | 18 +- static/css/main.css | 44 ++-- static/jquery.js | 2 + static/manage.js | 58 +++++ staticfiles/css/main.css | 44 ++-- staticfiles/jquery.js | 2 + staticfiles/manage.js | 67 ++++++ templates/base.html | 3 +- templates/footer.html | 48 ++++ templates/form.html | 1 + templates/nav.html | 37 +++ templates/search_field.html | 8 + users/admin.py | 3 +- users/forms.py | 52 ++++- users/migrations/0001_initial.py | 31 ++- users/models.py | 36 ++- users/templates/users/index.html | 15 +- users/templates/users/profile.html | 78 ++++++- users/templates/users/schools_index.html | 33 +++ users/templates/users/users_index.html | 33 +++ users/urls.py | 18 ++ users/views.py | 201 ++++++++++++++++- 54 files changed, 2199 insertions(+), 188 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 coopeV3/templatetags/__init__.py create mode 100644 coopeV3/templatetags/vip.py create mode 100644 gestion/forms.py create mode 100644 gestion/templates/gestion/manage.html create mode 100644 gestion/templates/gestion/products_index.html create mode 100644 gestion/templates/gestion/products_list.html create mode 100644 preferences/forms.py create mode 100644 preferences/templates/preferences/cotisations_index.html create mode 100644 preferences/templates/preferences/general_preferences.html create mode 100644 preferences/templates/preferences/payment_methods_index.html create mode 100644 preferences/urls.py create mode 100644 static/autocomplete_light/autocomplete.init.js create mode 100644 static/autocomplete_light/forward.js create mode 100644 static/autocomplete_light/jquery.init.js create mode 100644 static/autocomplete_light/jquery.post-setup.js create mode 100644 static/autocomplete_light/select2.css create mode 100644 static/autocomplete_light/select2.js create mode 100644 static/jquery.js create mode 100644 static/manage.js create mode 100644 staticfiles/jquery.js create mode 100644 staticfiles/manage.js create mode 100644 templates/search_field.html create mode 100644 users/templates/users/schools_index.html create mode 100644 users/templates/users/users_index.html diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..12d55cf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/home/nanoy/.virtualenvs/coopeV3/bin/python" +} \ No newline at end of file diff --git a/coopeV3/settings.py b/coopeV3/settings.py index e5cb22a..8b51238 100644 --- a/coopeV3/settings.py +++ b/coopeV3/settings.py @@ -40,6 +40,9 @@ INSTALLED_APPS = [ 'gestion', 'users', 'preferences', + 'coopeV3', + 'dal', + 'dal_select2', ] MIDDLEWARE = [ diff --git a/coopeV3/templatetags/__init__.py b/coopeV3/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coopeV3/templatetags/vip.py b/coopeV3/templatetags/vip.py new file mode 100644 index 0000000..f8be55c --- /dev/null +++ b/coopeV3/templatetags/vip.py @@ -0,0 +1,40 @@ +from django import template + +from preferences.models import GeneralPreferences + +register = template.Library() + +@register.simple_tag +def president(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.president + +@register.simple_tag +def vice_president(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.vice_president + +@register.simple_tag +def treasurer(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.treasurer + +@register.simple_tag +def secretary(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.secretary + +@register.simple_tag +def brewer(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.brewer + +@register.simple_tag +def grocer(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.grocer + +@register.simple_tag +def global_message(): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + return gp.global_message diff --git a/coopeV3/urls.py b/coopeV3/urls.py index b09d44a..62c9708 100644 --- a/coopeV3/urls.py +++ b/coopeV3/urls.py @@ -23,4 +23,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('users/', include('users.urls')), path('gestion/', include('gestion.urls')), + path('preferences/', include('preferences.urls')), ] diff --git a/coopeV3/views.py b/coopeV3/views.py index 67f8796..e1da5b8 100644 --- a/coopeV3/views.py +++ b/coopeV3/views.py @@ -2,7 +2,7 @@ from django.shortcuts import redirect from django.urls import reverse def home(request): - if request.user is not None: + if request.user.is_authenticated: if(request.user.has_perm('gestion.can_manage')): return redirect(reverse('gestion:manage')) else: diff --git a/coopeV3/widgets.py b/coopeV3/widgets.py index 986bdc0..750e2f2 100644 --- a/coopeV3/widgets.py +++ b/coopeV3/widgets.py @@ -1,13 +1,11 @@ -from django.forms.widgets import Input +from django.forms.widgets import Select, Input from django.template import Context, Template from django.template.loader import get_template -class SearchField: - def __init__(self, url): - self.url = url +class SearchField(Input): def render(self, name, value, attrs=None): - super().render(name, value, attrs) + #super().render(name, value, attrs) template = get_template('search_field.html') context = Context({}) return template.render(context) diff --git a/gestion/admin.py b/gestion/admin.py index 8c38f3f..ff8c544 100644 --- a/gestion/admin.py +++ b/gestion/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin -# Register your models here. +from .models import Reload, Refund, Product, Keg + +admin.site.register(Reload) +admin.site.register(Refund) +admin.site.register(Product) +admin.site.register(Keg) diff --git a/gestion/forms.py b/gestion/forms.py new file mode 100644 index 0000000..ebdd877 --- /dev/null +++ b/gestion/forms.py @@ -0,0 +1,37 @@ +from django import forms +from django.contrib.auth.models import User + +from dal import autocomplete + +from .models import Reload, Refund, Product, Keg, Menu +from preferences.models import PaymentMethod +from coopeV3.widgets import SearchField + +class ReloadForm(forms.ModelForm): + class Meta: + model = Reload + fields = ("customer", "amount", "PaymentMethod") + +class RefundForm(forms.ModelForm): + class Meta: + model = Refund + fields = ("customer", "amount") + +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = "__all__" + +class KegForm(forms.ModelForm): + class Meta: + model = Keg + fields = "__all__" + +class MenuForm(forms.ModelForm): + class Meta: + model = Menu + fields = "__all__" + +class GestionForm(forms.Form): + client = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Client", widget=autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})) + paymentMethod = forms.ModelChoiceField(queryset=PaymentMethod.objects.all(), required=True, label="Moyen de paiement") diff --git a/gestion/migrations/0001_initial.py b/gestion/migrations/0001_initial.py index 965fc23..d155d5f 100644 --- a/gestion/migrations/0001_initial.py +++ b/gestion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1 on 2018-08-31 12:45 +# Generated by Django 2.1 on 2018-10-04 09:32 from django.conf import settings from django.db import migrations, models @@ -11,23 +11,11 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('preferences', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ - migrations.CreateModel( - name='Barrel', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20)), - ('stockHold', models.IntegerField(default=0)), - ('barcode', models.CharField(max_length=20, unique=True)), - ('amount', models.DecimalField(decimal_places=2, max_digits=5)), - ('capacity', models.IntegerField(default=30)), - ('active', models.BooleanField(default=False)), - ], - ), migrations.CreateModel( name='ConsumptionHistory', fields=[ @@ -39,14 +27,38 @@ class Migration(migrations.Migration): ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='consumption_taken', to=settings.AUTH_USER_MODEL)), ], ), + migrations.CreateModel( + name='Keg', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=20, unique=True, verbose_name='Nom')), + ('stockHold', models.IntegerField(default=0, verbose_name='Stock en soute')), + ('barcode', models.CharField(max_length=20, unique=True, verbose_name='Code barre')), + ('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Prix du fût')), + ('capacity', models.IntegerField(default=30, verbose_name='Capacité (L)')), + ('is_active', models.BooleanField(default=False, verbose_name='Actif')), + ], + ), + migrations.CreateModel( + name='KegHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('openingDate', models.DateTimeField(auto_now_add=True)), + ('quantitySold', models.DecimalField(decimal_places=2, max_digits=5)), + ('amountSold', models.DecimalField(decimal_places=2, max_digits=5)), + ('closingDate', models.DateTimeField()), + ('isCurrentKegHistory', models.BooleanField(default=True)), + ('Keg', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='gestion.Keg')), + ], + ), migrations.CreateModel( name='Menu', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('amount', models.DecimalField(decimal_places=2, max_digits=5)), - ('barcode', models.CharField(max_length=20, unique=True)), - ('is_active', models.BooleanField(default=False)), + ('name', models.CharField(max_length=255, verbose_name='Nom')), + ('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Montant')), + ('barcode', models.CharField(max_length=20, unique=True, verbose_name='Code barre')), + ('is_active', models.BooleanField(default=False, verbose_name='Actif')), ], ), migrations.CreateModel( @@ -66,17 +78,16 @@ class Migration(migrations.Migration): name='Product', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40)), - ('amount', models.DecimalField(decimal_places=2, max_digits=5)), - ('stockHold', models.IntegerField(default=0)), - ('stockBar', models.IntegerField(default=0)), - ('barcode', models.CharField(max_length=20, unique=True)), - ('category', models.CharField(choices=[('PP', 'Pinte Pression'), ('DP', 'Demi Pression'), ('GP', 'Galopin pression'), ('BT', 'Bouteille'), ('SO', 'Soft'), ('FO', 'Bouffe')], default='FO', max_length=2)), - ('needQuantityButton', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=True)), - ('is_beer', models.BooleanField(default=False)), + ('name', models.CharField(max_length=40, unique=True, verbose_name='Nom')), + ('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Prix de vente')), + ('stockHold', models.IntegerField(default=0, verbose_name='Stock en soute')), + ('stockBar', models.IntegerField(default=0, verbose_name='Stock en bar')), + ('barcode', models.CharField(max_length=20, unique=True, verbose_name='Code barre')), + ('category', models.CharField(choices=[('PP', 'Pinte Pression'), ('DP', 'Demi Pression'), ('GP', 'Galopin pression'), ('BT', 'Bouteille'), ('SO', 'Soft'), ('FO', 'Bouffe autre que panini'), ('PA', 'Bouffe pour panini')], default='FO', max_length=2, verbose_name='Catégorie')), + ('needQuantityButton', models.BooleanField(default=False, verbose_name='Bouton quantité')), + ('is_active', models.BooleanField(default=True, verbose_name='Actif')), ('volume', models.IntegerField(default=0)), - ('deg', models.DecimalField(decimal_places=2, default=0, max_digits=5)), + ('deg', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Degré')), ], ), migrations.CreateModel( @@ -84,8 +95,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('date', models.DateTimeField(auto_now_add=True)), - ('barrel', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='gestion.Barrel')), ('coopeman', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('keg', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='gestion.Keg')), ], ), migrations.CreateModel( @@ -93,20 +104,20 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('date', models.DateTimeField(auto_now_add=True)), - ('amount', models.DecimalField(decimal_places=2, max_digits=5)), + ('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Montant')), ('coopeman', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_realized', to=settings.AUTH_USER_MODEL)), - ('cutsomer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_taken', to=settings.AUTH_USER_MODEL)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_taken', to=settings.AUTH_USER_MODEL, verbose_name='Client')), ], ), migrations.CreateModel( name='Reload', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.DecimalField(decimal_places=2, max_digits=5)), + ('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Montant')), ('date', models.DateTimeField(auto_now_add=True)), - ('PaymentMethod', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='preferences.PaymentMethod')), + ('PaymentMethod', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='preferences.PaymentMethod', verbose_name='Moyen de paiement')), ('coopeman', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reload_realized', to=settings.AUTH_USER_MODEL)), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reload_taken', to=settings.AUTH_USER_MODEL)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reload_taken', to=settings.AUTH_USER_MODEL, verbose_name='Client')), ], ), migrations.CreateModel( @@ -119,7 +130,22 @@ class Migration(migrations.Migration): migrations.AddField( model_name='menu', name='articles', - field=models.ManyToManyField(to='gestion.Product'), + field=models.ManyToManyField(to='gestion.Product', verbose_name='Produits'), + ), + migrations.AddField( + model_name='keg', + name='demi', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='futd', to='gestion.Product', validators=[gestion.models.isDemi]), + ), + migrations.AddField( + model_name='keg', + name='galopin', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='futg', to='gestion.Product', validators=[gestion.models.isGalopin]), + ), + migrations.AddField( + model_name='keg', + name='pinte', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='futp', to='gestion.Product', validators=[gestion.models.isPinte]), ), migrations.AddField( model_name='consumptionhistory', @@ -136,19 +162,4 @@ class Migration(migrations.Migration): name='product', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='gestion.Product'), ), - migrations.AddField( - model_name='barrel', - name='demi', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='futd', to='gestion.Product', validators=[gestion.models.isDemi]), - ), - migrations.AddField( - model_name='barrel', - name='galopin', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='futg', to='gestion.Product', validators=[gestion.models.isGalopin]), - ), - migrations.AddField( - model_name='barrel', - name='pinte', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='futp', to='gestion.Product', validators=[gestion.models.isPinte]), - ), ] diff --git a/gestion/models.py b/gestion/models.py index 72961aa..b4fffc6 100644 --- a/gestion/models.py +++ b/gestion/models.py @@ -1,6 +1,8 @@ from django.db import models from django.contrib.auth.models import User from preferences.models import PaymentMethod +from django.core.exceptions import ValidationError + class Product(models.Model): P_PRESSION = 'PP' D_PRESSION = 'DP' @@ -8,28 +10,29 @@ class Product(models.Model): BOTTLE = 'BT' SOFT = 'SO' FOOD = 'FO' + PANINI = 'PA' TYPEINPUT_CHOICES_CATEGORIE = ( (P_PRESSION, "Pinte Pression"), (D_PRESSION, "Demi Pression"), (G_PRESSION, "Galopin pression"), (BOTTLE, "Bouteille"), (SOFT, "Soft"), - (FOOD, "Bouffe"), + (FOOD, "Bouffe autre que panini"), + (PANINI, "Bouffe pour panini"), ) - name = models.CharField(max_length=40) - amount = models.DecimalField(max_digits=5, decimal_places=2) - stockHold = models.IntegerField(default=0) - stockBar = models.IntegerField(default=0) - barcode= models.CharField(max_length=20, unique=True) - category = models.CharField(max_length=2, choices=TYPEINPUT_CHOICES_CATEGORIE, default=FOOD) - needQuantityButton = models.BooleanField(default=False) - is_active = models.BooleanField(default=True) - is_beer = models.BooleanField(default=False) + name = models.CharField(max_length=40, verbose_name="Nom", unique=True) + amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Prix de vente") + stockHold = models.IntegerField(default=0, verbose_name="Stock en soute") + stockBar = models.IntegerField(default=0, verbose_name="Stock en bar") + barcode= models.CharField(max_length=20, unique=True, verbose_name="Code barre") + category = models.CharField(max_length=2, choices=TYPEINPUT_CHOICES_CATEGORIE, default=FOOD, verbose_name="Catégorie") + needQuantityButton = models.BooleanField(default=False, verbose_name="Bouton quantité") + is_active = models.BooleanField(default=True, verbose_name="Actif") volume = models.IntegerField(default=0) - deg = models.DecimalField(default=0,max_digits=5, decimal_places=2) + deg = models.DecimalField(default=0,max_digits=5, decimal_places=2, verbose_name="Degré") def __str__(self): - return self.nom + return self.name def isPinte(id): @@ -43,7 +46,7 @@ def isPinte(id): def isDemi(id): product = Product.objects.get(id=id) - if produit.category != Product.D_PRESSION: + if product.category != Product.D_PRESSION: raise ValidationError( ('%(product)s n\'est pas un demi'), params={'product': product}, @@ -57,24 +60,32 @@ def isGalopin(id): params={'product': product}, ) -class Barrel(models.Model): - name = models.CharField(max_length=20) - stockHold = models.IntegerField(default=0) - barcode = models.CharField(max_length=20, unique=True) - amount = models.DecimalField(max_digits=5, decimal_places=2) - capacity = models.IntegerField(default=30) +class Keg(models.Model): + name = models.CharField(max_length=20, unique=True, verbose_name="Nom") + stockHold = models.IntegerField(default=0, verbose_name="Stock en soute") + barcode = models.CharField(max_length=20, unique=True, verbose_name="Code barre") + amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Prix du fût") + capacity = models.IntegerField(default=30, verbose_name="Capacité (L)") pinte = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="futp", validators=[isPinte]) demi = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="futd", validators=[isDemi]) galopin = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="futg", validators=[isGalopin],null=True, blank=True) - active= models.BooleanField(default=False) + is_active = models.BooleanField(default=False, verbose_name="Actif") def __str__(self): return self.name +class KegHistory(models.Model): + Keg = models.ForeignKey(Keg, on_delete=models.PROTECT) + openingDate = models.DateTimeField(auto_now_add=True) + quantitySold = models.DecimalField(decimal_places=2, max_digits=5) + amountSold = models.DecimalField(decimal_places=2, max_digits=5) + closingDate = models.DateTimeField() + isCurrentKegHistory = models.BooleanField(default=True) + class Reload(models.Model): - customer = models.ForeignKey(User, on_delete=models.PROTECT, related_name="reload_taken") - amount = models.DecimalField(max_digits=5, decimal_places=2) - PaymentMethod = models.ForeignKey(PaymentMethod, on_delete=models.PROTECT) + customer = models.ForeignKey(User, on_delete=models.PROTECT, related_name="reload_taken", verbose_name="Client") + amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Montant") + PaymentMethod = models.ForeignKey(PaymentMethod, on_delete=models.PROTECT, verbose_name="Moyen de paiement") coopeman = models.ForeignKey(User, on_delete=models.PROTECT, related_name="reload_realized") date = models.DateTimeField(auto_now_add=True) @@ -83,12 +94,12 @@ class Reload(models.Model): class Raming(models.Model): - barrel = models.ForeignKey(Barrel, on_delete=models.PROTECT) + keg = models.ForeignKey(Keg, on_delete=models.PROTECT) coopeman = models.ForeignKey(User, on_delete=models.PROTECT) date = models.DateTimeField(auto_now_add=True) def __str__(self): - return "Percussion d'un {0} effectué par {1} le {2}".format(self.barrel, self.coopeman, self.date) + return "Percussion d'un {0} effectué par {1} le {2}".format(self.keg, self.coopeman, self.date) class Stocking(models.Model): date = models.DateTimeField(auto_now_add=True) @@ -99,8 +110,8 @@ class Stocking(models.Model): class Refund(models.Model): date = models.DateTimeField(auto_now_add=True) - cutsomer = models.ForeignKey(User, on_delete=models.PROTECT, related_name="refund_taken") - amount = models.DecimalField(max_digits=5, decimal_places=2) + customer = models.ForeignKey(User, on_delete=models.PROTECT, related_name="refund_taken", verbose_name="Client") + amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Montant") coopeman = models.ForeignKey(User, on_delete=models.PROTECT, related_name="refund_realized") def __str__(self): @@ -108,11 +119,11 @@ class Refund(models.Model): class Menu(models.Model): - name = models.CharField(max_length=255) - amount = models.DecimalField(max_digits=5, decimal_places=2) - barcode = models.CharField(max_length=20, unique=True) - articles = models.ManyToManyField(Product) - is_active = models.BooleanField(default=False) + name = models.CharField(max_length=255, verbose_name="Nom") + amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Montant") + barcode = models.CharField(max_length=20, unique=True, verbose_name="Code barre") + articles = models.ManyToManyField(Product, verbose_name="Produits") + is_active = models.BooleanField(default=False, verbose_name="Actif") def __str__(self): return self.name diff --git a/gestion/templates/gestion/manage.html b/gestion/templates/gestion/manage.html new file mode 100644 index 0000000..ca522d7 --- /dev/null +++ b/gestion/templates/gestion/manage.html @@ -0,0 +1,212 @@ +{% extends "base.html" %} +{% load static %} + +{%block entete%}

Gestion de la Coopé™

{%endblock%} + +{% block navbar %} + +{% endblock %} + + +{% block content %} + + UP + + +
+
+
+
+

Transaction

+
+
+
+ {{gestion_form}} +
+
+
+

Récapitulatif

+
+
+
+ + + + + + + + + + + + + + + + + +
SoldeMontant total de la commandeSolde après la commandePayer
0€0€0€
+
+
+
+

Produits

+
+
+
+ + + + + + + + + + + + +
CodeBarreNom ProduitPrix UnitaireQuantitéSous-total
+
+
+
+
+
+ + + + {% for produit in bieresPression %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not bieresPression|divisibleby:4 %} + + {% endif %} + + {% for produit in bieresBouteille %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not bieresBouteille|divisibleby:4 %} + + {% endif %} + + {% for produit in panini %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not panini|divisibleby:4 %} + + {% endif %} + + {% for produit in soft %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not soft|divisibleby:4 %} + + {% endif %} + + + {% for produit in autreBouffe %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not autreBouffe|divisibleby:4 %} + + {% endif %} + {% if menus %} + + {% for produit in menus %} + {% if forloop.counter0|divisibleby:4 %} + + {% endif %} + + {% if forloop.counter|divisibleby:4 %} + + {% endif %} + {% endfor %} + {% if not menus|divisibleby:4 %} + + {% endif %} + {% endif %} + +
Bières pression
Bières bouteilles
Paninis
Softs
Bouffe
Menus
+
+
+
+
+
+
+{% if perms.gestion.cand_add_reload %} +
+
+

Rechargement client

+
+
+ {% csrf_token %} + {{reload_form}} +
+ +
+
+{% endif %} +{% if perms.gestion.can_refund %} +
+
+

Remboursement client

+
+
+ {% csrf_token %} + {{refund_form}} +
+ +
+
+{% endif %} +{{gestion_form.media}} + +{%endblock%} diff --git a/gestion/templates/gestion/products_index.html b/gestion/templates/gestion/products_index.html new file mode 100644 index 0000000..629dbe8 --- /dev/null +++ b/gestion/templates/gestion/products_index.html @@ -0,0 +1,55 @@ +{% extends 'base.html' %} +{% block entete %}

Gestion des produits

{% endblock %} +{% block navbar%} + +{% endblock %} +{% block content %} +
+
+

Produits

+
+ Actions possibles : + +
+
+
+

Futs

+
+ Actions possibles : + +
+
+
+

Menus

+
+ Actions possibles : + +
+
+
+

Stocks

+
+ Actions possibles : + +
+{% endblock %} diff --git a/gestion/templates/gestion/products_list.html b/gestion/templates/gestion/products_list.html new file mode 100644 index 0000000..0c26c7f --- /dev/null +++ b/gestion/templates/gestion/products_list.html @@ -0,0 +1,55 @@ +{% extends 'base.html' %} +{% block entete %}

Gestion des produits

{% endblock %} +{% block navbar%} + +{% endblock %} +{% block content %} +
+
+

Liste des produits

+
+ Actions possibles : + +
+
+
+

Futs

+
+ Actions possibles : + +
+
+
+

Menus

+
+ Actions possibles : + +
+
+
+

Stocks

+
+ Actions possibles : + +
+{% endblock %} diff --git a/gestion/urls.py b/gestion/urls.py index 4e9d48c..3e0e17b 100644 --- a/gestion/urls.py +++ b/gestion/urls.py @@ -5,4 +5,11 @@ from . import views app_name="gestion" urlpatterns = [ path('manage', views.manage, name="manage"), + path('reload', views.reload, name="reload"), + path('refund', views.refund, name="refund"), + path('productsIndex', views.productsIndex, name="productsIndex"), + path('addProduct', views.addProduct, name="addProduct"), + path('addKeg', views.addKeg, name="addKeg"), + path('addMenu', views.addMenu, name="addMenu"), + path('getProduct/', views.getProduct, name="getProduct"), ] diff --git a/gestion/views.py b/gestion/views.py index c8dd192..59e8b6d 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -1,4 +1,108 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect +from django.contrib import messages +from django.urls import reverse +from django.http import HttpResponse +from django.contrib.auth.models import User + +import json +from dal import autocomplete + +from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm +from .models import Product, Menu, Keg def manage(request): - return render(request, "base.html") + gestion_form = GestionForm(request.POST or None) + reload_form = ReloadForm(request.POST or None) + refund_form = RefundForm(request.POST or None) + bieresPression = [] + bieresBouteille = Product.objects.filter(category=Product.BOTTLE).filter(is_active=True) + panini = Product.objects.filter(category=Product.PANINI).filter(is_active=True) + food = Product.objects.filter(category=Product.FOOD).filter(is_active=True) + soft = Product.objects.filter(category=Product.SOFT).filter(is_active=True) + menus = Menu.objects.filter(is_active=True) + kegs = Keg.objects.filter(is_active=True) + for keg in kegs: + if(keg.pinte): + bieresPression.append(keg.pinte) + if(keg.demi): + bieresPression.append(keg.demi) + if(keg.galopin): + bieresPression.append(keg.galopin) + return render(request, "gestion/manage.html", {"gestion_form": gestion_form, "reload_form": reload_form, "refund_form": refund_form, "bieresPression": bieresPression, "bieresBouteille": bieresBouteille, "panini": panini, "food": food, "soft": soft, "menus": menus}) + +def reload(request): + reload_form = ReloadForm(request.POST or None) + if(reload_form.is_valid()): + reloadEntry = reload_form.save(commit=False) + reloadEntry.coopeman = request.user + reloadEntry.save() + user = reload_form.cleaned_data['customer'] + amount = reload_form.cleaned_data['amount'] + user.profile.credit += amount + user.save() + messages.success(request,"Le compte de " + user.username + " a bien été crédité de " + str(amount) + "€") + else: + messages.error(request, "Le rechargement a échoué") + return redirect(reverse('gestion:manage')) + +def refund(request): + refund_form = RefundForm(request.POST or None) + if(refund_form.is_valid()): + user = refund_form.cleaned_data['customer'] + amount = refund_form.cleaned_data['amount'] + if(amount <= user.profile.balance): + refundEntry = refund_form.save(commit = False) + refundEntry.coopeman = request.user + refundEntry.save() + user.profile.credit -= amount + user.save() + messages.success(request, "Le compte de " + user.username + " a bien été remboursé de " + str(amount) + "€") + else: + messages.error(request, "Impossible de rembourser l'utilisateur " + user.username + " de " + str(amount) + "€ : il n'a que " + str(user.profile.balance) + "€ sur son compte.") + else: + messages.error(request, "Le remboursement a échoué") + return redirect(reverse('gestion:manage')) + +def productsIndex(request): + return render(request, "gestion/products_index.html") + +def addProduct(request): + form = ProductForm(request.POST or None) + if(form.is_valid()): + form.save() + messages.success(request, "Le produit a bien été ajouté") + return redirect(reverse('gestion:productsIndex')) + return render(request, "form.html", {"form": form, "form_title": "Ajout d'un produit", "form_button": "Ajouter"}) + +def productsList(request): + products = Product.objects.all() + return render(request, "gestion/products_list.html", {"products": products}) + +def getProduct(request, barcode): + product = Product.objects.get(barcode=barcode) + data = json.dumps({"pk": product.pk, "barcode" : product.barcode, "name": product.name, "amount" : float(product.amount)}) + return HttpResponse(data, content_type='application/json') + + +########## Kegs ########## + +def addKeg(request): + form = KegForm(request.POST or None) + if(form.is_valid()): + keg = form.save() + messages.success(request, "Le fût " + keg.name + " a bien été ajouté") + return redirect(reverse('gestion:productsIndex')) + return render(request, "form.html", {"form":form, "form_title": "Ajout d'un fût", "form_button": "Ajouter"}) + + +########## Menus ########## + +def addMenu(request): + form = MenuForm(request.POST or None) + extra_css = "#id_articles{height:200px;}" + if(form.is_valid()): + menu = form.save() + messages.success(request, "Le menu " + menu.name + " a bien été ajouté") + return redirect(reverse('gestion:productsIndex')) + return render(request, "form.html", {"form":form, "form_title": "Ajout d'un menu", "form_button": "Ajouter", "extra_css": extra_css}) + diff --git a/preferences/admin.py b/preferences/admin.py index 8c38f3f..773c1f0 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from .models import PaymentMethod, GeneralPreferences, Cotisation + +admin.site.register(PaymentMethod) +admin.site.register(GeneralPreferences) +admin.site.register(Cotisation) # Register your models here. diff --git a/preferences/forms.py b/preferences/forms.py new file mode 100644 index 0000000..6271cab --- /dev/null +++ b/preferences/forms.py @@ -0,0 +1,30 @@ +from django import forms + +from .models import Cotisation, PaymentMethod, GeneralPreferences + +class CotisationForm(forms.ModelForm): + class Meta: + model = Cotisation + fields = "__all__" + +class PaymentMethodForm(forms.ModelForm): + class Meta: + model = PaymentMethod + fields = "__all__" + + +class GeneralPreferencesForm(forms.ModelForm): + class Meta: + model = GeneralPreferences + fields = "__all__" + widgets = { + 'global_message': forms.Textarea(attrs={'placeholder': 'Message global à afficher sur le site'}), + 'active_message': forms.Textarea(attrs={'placeholder': 'Ce message s\'affichera si le site n\'est pas actif'}), + 'president': forms.TextInput(attrs={'placeholder': 'Président'}), + 'vice_president': forms.TextInput(attrs={'placeholder': 'Vice-président'}), + 'secretary': forms.TextInput(attrs={'placeholder': 'Secrétaire'}), + 'treasurer': forms.TextInput(attrs={'placeholder': 'Trésorier'}), + 'brewer': forms.TextInput(attrs={'placeholder': 'Maître brasseur'}), + 'grocer': forms.TextInput(attrs={'placeholder': 'Epic épicier'}), + } + diff --git a/preferences/migrations/0001_initial.py b/preferences/migrations/0001_initial.py index 786a002..d587112 100644 --- a/preferences/migrations/0001_initial.py +++ b/preferences/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1 on 2018-08-31 12:45 +# Generated by Django 2.1 on 2018-10-04 09:32 from django.db import migrations, models @@ -11,6 +11,14 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Cotisation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=5, null=True, verbose_name='Montant')), + ('duration', models.PositiveIntegerField(verbose_name='Durée de la cotisation (jours)')), + ], + ), migrations.CreateModel( name='GeneralPreferences', fields=[ @@ -30,7 +38,10 @@ class Migration(migrations.Migration): name='PaymentMethod', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), + ('name', models.CharField(max_length=255, verbose_name='Nom')), + ('is_active', models.BooleanField(default=True)), + ('is_usable_in_cotisation', models.BooleanField(default=True)), + ('affect_balance', models.BooleanField(default=False)), ], ), ] diff --git a/preferences/models.py b/preferences/models.py index fe5e8a0..0c5dbb7 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -1,7 +1,10 @@ from django.db import models class PaymentMethod(models.Model): - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, verbose_name="Nom") + is_active = models.BooleanField(default=True, verbose_name="Actif") + is_usable_in_cotisation = models.BooleanField(default=True, verbose_name="Utilisable pour les cotisations") + affect_balance = models.BooleanField(default=False, verbose_name="Affecte le solde") def __str__(self): return self.name @@ -16,3 +19,10 @@ class GeneralPreferences(models.Model): secretary = models.CharField(max_length=255, blank=True) brewer = models.CharField(max_length=255, blank=True) grocer = models.CharField(max_length=255, blank=True) + +class Cotisation(models.Model): + amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, verbose_name="Montant") + duration = models.PositiveIntegerField(verbose_name="Durée de la cotisation (jours)") + + def __str__(self): + return "Cotisation de " + str(self.duration) + " jours pour le prix de " + str(self.amount) + "€" diff --git a/preferences/templates/preferences/cotisations_index.html b/preferences/templates/preferences/cotisations_index.html new file mode 100644 index 0000000..aa7d130 --- /dev/null +++ b/preferences/templates/preferences/cotisations_index.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block entete %}

Gestion des cotisations

{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Liste des cotisations

+
+ Créer une cotisation

+
+ + + + + + + + + + {% for cotisation in cotisations %} + + + + + + {% endfor %} + +
Durée de cotisationPrixAdministration
{{ cotisation.duration }} jours{{ cotisation.amount }} €Modifier Supprimer
+
+
+{% endblock %} diff --git a/preferences/templates/preferences/general_preferences.html b/preferences/templates/preferences/general_preferences.html new file mode 100644 index 0000000..94229dd --- /dev/null +++ b/preferences/templates/preferences/general_preferences.html @@ -0,0 +1,89 @@ +{% extends 'base.html' %} +{% block entete %} +

Administration

+{% endblock %} +{% block nav %} + +{% endblock %} + +{% block content %} +
+ {% csrf_token %} +
+
+
+
+

Message global

+
+
+
+ {{form.global_message}} +
+
+
+
+
+
+
+
+
+

Site actif

+
+
+
+ {{form.is_active}} + +
+
+
+
+ {{form.active_message}} +
+
+
+
+
+
+
+
+
+

Bureau

+
+
+
+ {{form.president}} +
+
+ {{form.vice_president}} +
+
+
+
+ {{form.secretary}} +
+
+ {{form.treasurer}} +
+
+
+
+ {{form.grocer}} +
+
+ {{form.brewer}} +
+
+
+
+ +
+
+
+
+
+
+{% endblock %} diff --git a/preferences/templates/preferences/payment_methods_index.html b/preferences/templates/preferences/payment_methods_index.html new file mode 100644 index 0000000..f5ef170 --- /dev/null +++ b/preferences/templates/preferences/payment_methods_index.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block entete %}

Gestion des moyens de paiement

{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Liste des moyens de paiement

+
+ Créer un moyen de paiement

+
+ + + + + + + + + + + + {% for pm in paymentMethods %} + + + + + + + + {% endfor %} + +
NomActif ?Utilisable dans les cotisationsAffecte le soldeAdministration
{{ pm.name }} {{ pm.is_active | yesno:"Oui, Non"}}{{ pm.is_usable_in_cotisation | yesno:"Oui, Non" }}{{ pm.affect_balance | yesno:"Oui, Non" }}Modifier Supprimer
+
+
+{% endblock %} diff --git a/preferences/urls.py b/preferences/urls.py new file mode 100644 index 0000000..e45f5b4 --- /dev/null +++ b/preferences/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from . import views + +app_name="preferences" +urlpatterns = [ + path('generalPreferences', views.generalPreferences, name="generalPreferences"), + path('cotisationsIndex', views.cotisationsIndex, name="cotisationsIndex"), + path('addCotisation', views.addCotisation, name="addCotisation"), + path('editCotisation/', views.editCotisation, name="editCotisation"), + path('deleteCotisation/', views.deleteCotisation, name="deleteCotisation"), + path('paymentMethodsIndex', views.paymentMethodsIndex, name="paymentMethodsIndex"), + path('addPaymentMethod', views.addPaymentMethod, name="addPaymentMethod"), + path('editPaymentMethod/', views.editPaymentMethod, name="editPaymentMethod"), + path('deletePaymentMethod/', views.deletePaymentMethod, name="deletePaymentMethod"), +] diff --git a/preferences/views.py b/preferences/views.py index 91ea44a..c16aa75 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -1,3 +1,75 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from django.urls import reverse -# Create your views here. +from .models import GeneralPreferences, Cotisation, PaymentMethod + +from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm + +def generalPreferences(request): + gp,_ = GeneralPreferences.objects.get_or_create(pk=1) + form = GeneralPreferencesForm(request.POST or None, instance=gp) + if(form.is_valid()): + form.save() + return render(request, "preferences/general_preferences.html", {"form": form}) + +########## Cotisations ########## + +def cotisationsIndex(request): + cotisations = Cotisation.objects.all() + return render(request, "preferences/cotisations_index.html", {"cotisations": cotisations}) + +def addCotisation(request): + form = CotisationForm(request.POST or None) + if(form.is_valid()): + cotisation = form.save() + messages.success(request, "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été créée") + return redirect(reverse('preferences:cotisationsIndex')) + return render(request, "form.html", {"form": form, "form_title": "Création d'une cotisation", "form_button": "Créer"}) + +def editCotisation(request, pk): + cotisation = get_object_or_404(Cotisation, pk=pk) + form = CotisationForm(request.POST or None, instance=cotisation) + if(form.is_valid()): + cotisation = form.save() + messages.success(request, "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été modifiée") + return redirect(reverse('preferences:cotisationsIndex')) + return render(request, "form.html", {"form": form, "form_title": "Modification d'une cotisation", "form_button": "Modifier"}) + +def deleteCotisation(request,pk): + cotisation = get_object_or_404(Cotisation, pk=pk) + message = "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été supprimée" + cotisation.delete() + messages.success(request, message) + return redirect(reverse('preferences:cotisationsIndex')) + + +########## Payment Methods ########## + +def paymentMethodsIndex(request): + paymentMethods = PaymentMethod.objects.all() + return render(request, "preferences/payment_methods_index.html", {"paymentMethods": paymentMethods}) + +def addPaymentMethod(request): + form = PaymentMethodForm(request.POST or None) + if(form.is_valid()): + paymentMethod = form.save() + messages.success(request, "Le moyen de paiement " + paymentMethod.name + " a bien été crée") + return redirect(reverse('preferences:paymentMethodsIndex')) + return render(request, "form.html", {"form": form, "form_title": "Création d'un moyen de paiement", "form_button": "Créer"}) + +def editPaymentMethod(request, pk): + paymentMethod = get_object_or_404(PaymentMethod, pk=pk) + form = PaymentMethodForm(request.POST or None, instance=paymentMethod) + if(form.is_valid()): + paymentMethod = form.save() + messages.success(request, "Le moyen de paiment " + paymentMethod.name + " a bien été modifié") + return redirect(reverse('preferences:paymentMethodsIndex')) + return render(request, "form.html", {"form": form, "form_title": "Modification d'un moyen de paiement", "form_button": "Modifier"}) + +def deletePaymentMethod(request,pk): + paymentMethod = get_object_or_404(PaymentMethod, pk=pk) + message = "Le moyen de paiement " + paymentMethod.name + " a bien été supprimé" + paymentMethod.delete() + messages.success(request, message) + return redirect(reverse('preferences:paymentMethodsIndex')) diff --git a/requirements.txt b/requirements.txt index 7569b6f..226d374 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Django==2.1 +django-autocomplete-light==3.3.2 pytz==2018.5 diff --git a/static/autocomplete_light/autocomplete.init.js b/static/autocomplete_light/autocomplete.init.js new file mode 100644 index 0000000..8f413f7 --- /dev/null +++ b/static/autocomplete_light/autocomplete.init.js @@ -0,0 +1,162 @@ +/* +This script garantees that this will be called once in django admin. +However, its the callback's responsability to clean up if the +element was cloned with data - which should be the case. +*/ + +;(function ($) { + $.fn.getFormPrefix = function() { + /* Get the form prefix for a field. + * + * For example: + * + * $(':input[name$=owner]').getFormsetPrefix() + * + * Would return an empty string for an input with name 'owner' but would return + * 'inline_model-0-' for an input named 'inline_model-0-owner'. + */ + var parts = $(this).attr('name').split('-'); + var prefix = ''; + + for (var i in parts) { + var testPrefix = parts.slice(0, -i).join('-'); + if (! testPrefix.length) continue; + testPrefix += '-'; + + var result = $(':input[name^=' + testPrefix + ']') + + if (result.length) { + return testPrefix; + } + } + + return ''; + } + + $.fn.getFormPrefixes = function() { + /* + * Get the form prefixes for a field, from the most specific to the least. + * + * For example: + * + * $(':input[name$=owner]').getFormPrefixes() + * + * Would return: + * - [''] for an input named 'owner'. + * - ['inline_model-0-', ''] for an input named 'inline_model-0-owner' (i.e. nested with a nested inline). + * - ['sections-0-items-0-', 'sections-0-', ''] for an input named 'sections-0-items-0-product' + * (i.e. nested multiple time with django-nested-admin). + */ + var parts = $(this).attr('name').split('-').slice(0, -1); + var prefixes = []; + + for (i = 0; i < parts.length; i += 2) { + var testPrefix = parts.slice(0, -i || parts.length).join('-'); + if (!testPrefix.length) + continue; + + testPrefix += '-'; + + var result = $(':input[name^=' + testPrefix + ']') + + if (result.length) + prefixes.push(testPrefix); + } + + prefixes.push(''); + + return prefixes; + } + + var initialized = []; + + function initialize(element) { + if (typeof element === 'undefined' || typeof element === 'number') { + element = this; + } + + if (window.__dal__initListenerIsSet !== true || initialized.indexOf(element) >= 0) { + return; + } + + $(element).trigger('autocompleteLightInitialize'); + initialized.push(element); + } + + if (!window.__dal__initialize) { + window.__dal__initialize = initialize; + + $(document).ready(function () { + $('[data-autocomplete-light-function=select2]:not([id*="__prefix__"])').each(initialize); + }); + + $(document).bind('DOMNodeInserted', function (e) { + $(e.target).find('[data-autocomplete-light-function=select2]:not([id*="__prefix__"])').each(initialize); + }); + } + + // using jQuery + function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = $.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + document.csrftoken = getCookie('csrftoken'); + if (document.csrftoken === null) { + // Try to get CSRF token from DOM when cookie is missing + var $csrf = $('form :input[name="csrfmiddlewaretoken"]'); + if ($csrf.length > 0) { + document.csrftoken = $csrf[0].value; + } + } +})(yl.jQuery); + +// Does the same thing as django's admin/js/autocomplete.js, but uses yl.jQuery. +(function($) { + 'use strict'; + var init = function($element, options) { + var settings = $.extend({ + ajax: { + data: function(params) { + return { + term: params.term, + page: params.page + }; + } + } + }, options); + $element.select2(settings); + }; + + $.fn.djangoAdminSelect2 = function(options) { + var settings = $.extend({}, options); + $.each(this, function(i, element) { + var $element = $(element); + init($element, settings); + }); + return this; + }; + + $(function() { + // Initialize all autocomplete widgets except the one in the template + // form used when a new formset is added. + $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); + }); + + $(document).on('formset:added', (function() { + return function(event, $newFormset) { + return $newFormset.find('.admin-autocomplete').djangoAdminSelect2(); + }; + })(this)); +}(yl.jQuery)); diff --git a/static/autocomplete_light/forward.js b/static/autocomplete_light/forward.js new file mode 100644 index 0000000..9fe247c --- /dev/null +++ b/static/autocomplete_light/forward.js @@ -0,0 +1,183 @@ +;(function($, yl) { + yl.forwardHandlerRegistry = yl.forwardHandlerRegistry || {}; + + yl.registerForwardHandler = function(name, handler) { + yl.forwardHandlerRegistry[name] = handler; + }; + + yl.getForwardHandler = function(name) { + return yl.forwardHandlerRegistry[name]; + }; + + function getForwardStrategy(element) { + var checkForCheckboxes = function() { + var all = true; + $.each(element, function(ix, e) { + if ($(e).attr("type") !== "checkbox") { + all = false; + } + }); + return all; + }; + + if (element.length === 1 && + element.attr("type") === "checkbox" && + element.attr("value") === undefined) { + // Single checkbox without 'value' attribute + // Boolean field + return "exists"; + } else if (element.length === 1 && + element.attr("multiple") !== undefined) { + // Multiple by HTML semantics. E. g. multiple select + // Multiple choice field + return "multiple"; + } else if (checkForCheckboxes()) { + // Multiple checkboxes or one checkbox with 'value' attribute. + // Multiple choice field represented by checkboxes + return "multiple"; + } else { + // Other cases + return "single"; + } + } + + /** + * Get fields with name `name` relative to `element` with considering form + * prefixes. + * @param element the element + * @param name name of the field + * @returns jQuery object with found fields or empty jQuery object if no + * field was found + */ + yl.getFieldRelativeTo = function(element, name) { + var prefixes = $(element).getFormPrefixes(); + + for (var i = 0; i < prefixes.length; i++) { + var fieldSelector = "[name=" + prefixes[i] + name + "]"; + var field = $(fieldSelector); + + if (field.length) { + return field; + } + } + + return $(); + }; + + /** + * Get field value which is put to forwarded dictionary + * @param field the field + * @returns forwarded value + */ + yl.getValueFromField = function(field) { + var strategy = getForwardStrategy(field); + var serializedField = $(field).serializeArray(); + + var getSerializedFieldElementAt = function (index) { + // Return serializedField[index] + // or null if something went wrong + if (serializedField.length > index) { + return serializedField[index]; + } else { + return null; + } + }; + + var getValueOf = function (elem) { + // Return elem.value + // or null if something went wrong + if (elem.hasOwnProperty("value") && + elem.value !== undefined + ) { + return elem.value; + } else { + return null; + } + }; + + var getSerializedFieldValueAt = function (index) { + // Return serializedField[index].value + // or null if something went wrong + var elem = getSerializedFieldElementAt(index); + if (elem !== null) { + return getValueOf(elem); + } else { + return null; + } + }; + + if (strategy === "multiple") { + return serializedField.map( + function (item) { + return getValueOf(item); + } + ); + } else if (strategy === "exists") { + return serializedField.length > 0; + } else { + return getSerializedFieldValueAt(0); + } + }; + + yl.getForwards = function(element) { + var forwardElem, + forwardList, + forwardedData, + divSelector, + form; + divSelector = "div.dal-forward-conf#dal-forward-conf-for-" + + element.attr("id"); + form = element.length > 0 ? $(element[0].form) : $(); + + forwardElem = + form.find(divSelector).find('script'); + if (forwardElem.length === 0) { + return; + } + try { + forwardList = JSON.parse(forwardElem.text()); + } catch (e) { + return; + } + + if (!Array.isArray(forwardList)) { + return; + } + + forwardedData = {}; + + $.each(forwardList, function(ix, field) { + var srcName, dstName; + if (field.type === "const") { + forwardedData[field.dst] = field.val; + } else if (field.type === "self") { + if (field.hasOwnProperty("dst")) { + dstName = field.dst; + } else { + dstName = "self"; + } + forwardedData[dstName] = yl.getValueFromField(element); + } else if (field.type === "field") { + srcName = field.src; + if (field.hasOwnProperty("dst")) { + dstName = field.dst; + } else { + dstName = srcName; + } + var forwardedField = yl.getFieldRelativeTo(element, srcName); + + if (!forwardedField.length) { + return; + } + + forwardedData[dstName] = yl.getValueFromField(forwardedField); + } else if (field.type === "javascript") { + var handler = yl.getForwardHandler(field.handler); + forwardedData[field.dst || field.handler] = handler(element); + } + + }); + return JSON.stringify(forwardedData); + }; + +})(yl.jQuery, yl); diff --git a/static/autocomplete_light/jquery.init.js b/static/autocomplete_light/jquery.init.js new file mode 100644 index 0000000..926d8d3 --- /dev/null +++ b/static/autocomplete_light/jquery.init.js @@ -0,0 +1,36 @@ +var yl = yl || {}; +if (typeof django !== 'undefined' && typeof django.jQuery !== 'undefined') { + // If django.jQuery is already defined, use it. + yl.jQuery = django.jQuery; +} +else { + // We include jquery itself in our widget's media, because we need it. + // Normally, we expect our widget's reference to admin/js/vendor/jquery/jquery.js + // to be skipped, because django's own code has already included it. + // However, if django.jQuery is NOT defined, we know that jquery was not + // included before we did it ourselves. This can happen if we're not being + // rendered in a django admin form. + // However, someone ELSE'S jQuery may have been included before ours, in + // which case we must ensure that our jquery doesn't override theirs, since + // it might be a newer version that other code on the page relies on. + // Thus, we must run jQuery.noConflict(true) here to move our jQuery out of + // the way. + yl.jQuery = jQuery.noConflict(true); +} + +// In addition to all of this, we must ensure that the global jQuery and $ are +// defined, because Select2 requires that. jQuery will only be undefined at +// this point if only we or django included it. +if (typeof jQuery === 'undefined') { + jQuery = yl.jQuery; + $ = yl.jQuery; +} +else { + // jQuery IS still defined, which means someone else also included jQuery. + // In this situation, we need to store the old jQuery in a + // temp variable, set the global jQuery to our yl.jQuery, then let select2 + // set itself up. We restore the global jQuery to its original value in + // jquery.post-setup.js. + dal_jquery_backup = jQuery.noConflict(true); + jQuery = yl.jQuery; +} diff --git a/static/autocomplete_light/jquery.post-setup.js b/static/autocomplete_light/jquery.post-setup.js new file mode 100644 index 0000000..eac6f5a --- /dev/null +++ b/static/autocomplete_light/jquery.post-setup.js @@ -0,0 +1,7 @@ +if (typeof dal_jquery_backup !== 'undefined') { + // We made a backup of the original global jQuery before forcing it to our + // yl.jQuery value. Now that select2 has been set up, we need to restore + // our backup to its rightful place. + jQuery = dal_jquery_backup; + $ = dal_jquery_backup; +} diff --git a/static/autocomplete_light/select2.css b/static/autocomplete_light/select2.css new file mode 100644 index 0000000..471e1a1 --- /dev/null +++ b/static/autocomplete_light/select2.css @@ -0,0 +1,9 @@ +.select2-container { + min-width: 20em; +} + +ul li.select2-selection__choice, +ul li.select2-search { + /* Cancel out django's style */ + list-style-type: none; +} diff --git a/static/autocomplete_light/select2.js b/static/autocomplete_light/select2.js new file mode 100644 index 0000000..34d60c4 --- /dev/null +++ b/static/autocomplete_light/select2.js @@ -0,0 +1,116 @@ +;(function ($) { + if (window.__dal__initListenerIsSet) + return; + + $(document).on('autocompleteLightInitialize', '[data-autocomplete-light-function=select2]', function() { + var element = $(this); + + // Templating helper + function template(text, is_html) { + if (is_html) { + var $result = $(''); + $result.html(text); + return $result; + } else { + return text; + } + } + + function result_template(item) { + return template(item.text, + element.attr('data-html') !== undefined || element.attr('data-result-html') !== undefined + ); + } + + function selected_template(item) { + if (item.selected_text !== undefined) { + return template(item.selected_text, + element.attr('data-html') !== undefined || element.attr('data-selected-html') !== undefined + ); + } else { + return result_template(item); + } + return + } + + var ajax = null; + if ($(this).attr('data-autocomplete-light-url')) { + ajax = { + url: $(this).attr('data-autocomplete-light-url'), + dataType: 'json', + delay: 250, + + data: function (params) { + var data = { + q: params.term, // search term + page: params.page, + create: element.attr('data-autocomplete-light-create') && !element.attr('data-tags'), + forward: yl.getForwards(element) + }; + + return data; + }, + processResults: function (data, page) { + if (element.attr('data-tags')) { + $.each(data.results, function(index, value) { + value.id = value.text; + }); + } + + return data; + }, + cache: true + }; + } + + $(this).select2({ + tokenSeparators: element.attr('data-tags') ? [','] : null, + debug: true, + containerCssClass: ':all:', + placeholder: element.attr('data-placeholder') || '', + language: element.attr('data-autocomplete-light-language'), + minimumInputLength: element.attr('data-minimum-input-length') || 0, + allowClear: ! $(this).is('[required]'), + templateResult: result_template, + templateSelection: selected_template, + ajax: ajax, + tags: Boolean(element.attr('data-tags')), + }); + + $(this).on('select2:selecting', function (e) { + var data = e.params.args.data; + + if (data.create_id !== true) + return; + + e.preventDefault(); + + var select = $(this); + + $.ajax({ + url: $(this).attr('data-autocomplete-light-url'), + type: 'POST', + dataType: 'json', + data: { + text: data.id, + forward: yl.getForwards($(this)) + }, + beforeSend: function(xhr, settings) { + xhr.setRequestHeader("X-CSRFToken", document.csrftoken); + }, + success: function(data, textStatus, jqXHR ) { + select.append( + $('