#208 Améliore l'interface d'admin des objets

Fermé
jocelyn veut fusionner 9 commits à partir de FFDN/jd-enhance-inventory-admin vers FFDN/master
3 fichiers modifiés avec 171 ajouts et 58 suppressions
  1. 2 3
      coin/settings_base.py
  2. 136 54
      hardware_provisioning/admin.py
  3. 33 1
      hardware_provisioning/models.py

+ 2 - 3
coin/settings_base.py

@@ -166,9 +166,8 @@ INSTALLED_APPS = (
     # Uncomment the next line to enable admin documentation:
     # Uncomment the next line to enable admin documentation:
     #'django.contrib.admindocs',
     #'django.contrib.admindocs',
     'polymorphic',
     'polymorphic',
-    # 'south',
-    'autocomplete_light', #Automagic autocomplete foreingkey form component
-    'activelink', #Detect if a link match actual page
+    'autocomplete_light',  # Automagic autocomplete foreingkey form component
+    'activelink',          # Detect if a link match actual page
     'compat',
     'compat',
     'hijack',
     'hijack',
 
 

+ 136 - 54
hardware_provisioning/admin.py

@@ -2,10 +2,12 @@
 
 
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-
-from django.contrib import admin
+from django.conf.urls import url
+from django.shortcuts import get_object_or_404
+from django.contrib import admin, messages
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.forms import ModelChoiceField
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
 import autocomplete_light
 import autocomplete_light
 
 
 from .models import ItemType, Item, Loan, Storage
 from .models import ItemType, Item, Loan, Storage
@@ -17,6 +19,21 @@ User = get_user_model()
 admin.site.register(ItemType)
 admin.site.register(ItemType)
 
 
 
 
+def give_back_loan(request, id):
+    # could be better : could not be a POST, and could not rely on HTTP_REFERER
+    # We could for that rely on django-inline-actions, but the version
+    # we could use with our Django 1.9 is outdated and buggy
+    # We could also offer an intermediate page to specify the storage
+    redirect_url = request.META['HTTP_REFERER']
+    loan = get_object_or_404(Loan, pk=id)
+    loan.item.give_back()
+    messages.success(
+        request,
+        "{} a bien été marqué comme rendu".format(loan.item),
+    )
+    return HttpResponseRedirect(redirect_url)
+
+
 class OwnerFilter(admin.SimpleListFilter):
 class OwnerFilter(admin.SimpleListFilter):
     title = "Propriétaire"
     title = "Propriétaire"
     parameter_name = 'owner'
     parameter_name = 'owner'
@@ -56,14 +73,113 @@ class AvailabilityFilter(admin.SimpleListFilter):
             return queryset
             return queryset
 
 
 
 
+class CurrentLoanInline(admin.TabularInline):
+    model = Loan
+    extra = 0
+    fields = ('user', 'item', 'short_date', 'notes', 'action_buttons')
+    readonly_fields = ('user', 'item', 'short_date', 'notes', 'action_buttons')
+    verbose_name_plural = "Emprunt en cours"
+    show_change_link = True
+
+    def get_queryset(self, request):
+        qs = super(CurrentLoanInline, self).get_queryset(request)
+        return qs.running()
+
+    def has_add_permission(self, request, obj=None):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def action_buttons(self, obj):
+        if obj.is_running():
+            return """<a class="button "href="{}">Déclarer rendu</a>""".format(
+                reverse('admin:loan-give_back', args=[obj.pk]))
+        else:
+            return ''
+    action_buttons.short_description = 'Actions'
+    action_buttons.allow_tags = True
+
+
+class LoanHistoryInline(admin.TabularInline):
+    model = Loan
+    extra = 0
+    fields = ('user', 'item', 'short_date', 'short_date_end', 'notes')
+    readonly_fields = ('user', 'item', 'short_date', 'short_date_end', 'notes')
+    ordering = ['-loan_date_end']
+    verbose_name_plural = "Historique de prêt de cet objet"
+    show_change_link = True
+    classes = ['collapse']  # Django >= 1.10-ready
+
+    def get_queryset(self, request):
+        qs = super(LoanHistoryInline, self).get_queryset(request)
+        return qs.finished()
+
+    def has_add_permission(self, request, obj=None):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+
+class AddLoanInline(admin.StackedInline):
+    model = Loan
+    extra = 1
+    max_num = 1
+    fields = ('user', 'item', 'loan_date', 'notes')
+    verbose_name_plural = "Déclarer le prêt de cet objet"
+    classes = ['collapse']  # Django >= 1.10-ready
+
+    form = autocomplete_light.modelform_factory(Loan, fields='__all__')
+
+    def get_queryset(self, request):
+        qs = super(AddLoanInline, self).get_queryset(request)
+        return qs.none()
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+
+class BorrowerFilter(admin.SimpleListFilter):
+    title = 'Adhérent emprunteur'
+    parameter_name = 'user'
+
+    def _filter_loans(self, items_queryset, user_pk=None):
+        qs = Loan.objects.running().filter(item__in=items_queryset)
+        if user_pk is not None:
+            qs.filter(user=user_pk)
+        return qs
+
+    def lookups(self, request, model_admin):
+        # Get relevant (and authorized) users only
+        relevant_items = model_admin.get_queryset(request)
+        users = set()
+        for loan in self._filter_loans(relevant_items):
+            users.add((loan.user.pk, loan.user))
+        return users
+
+    def queryset(self, request, queryset):
+        if self.value():
+            loans_qs = self._filter_loans(queryset).filter(
+                user__pk=self.value(),
+            )
+            return queryset.filter(loans__in=loans_qs)
+        else:
+            return queryset
+
+
 @admin.register(Item)
 @admin.register(Item)
 class ItemAdmin(admin.ModelAdmin):
 class ItemAdmin(admin.ModelAdmin):
     list_display = (
     list_display = (
-        'designation', 'mac_address', 'serial', 'owner',
-        'buy_date', 'deployed', 'is_available', 'storage')
+        'designation',
+        'current_borrower',
+        'get_mac_and_serial',
+        'deployed', 'is_available', 'storage',
+        'buy_date', 'owner',
+    )
     list_filter = (
     list_filter = (
         AvailabilityFilter, 'type', 'storage',
         AvailabilityFilter, 'type', 'storage',
-        'buy_date', OwnerFilter)
+        'buy_date', BorrowerFilter, OwnerFilter)
     search_fields = (
     search_fields = (
         'designation', 'mac_address', 'serial',
         'designation', 'mac_address', 'serial',
         'owner__email', 'owner__nickname',
         'owner__email', 'owner__nickname',
@@ -73,11 +189,23 @@ class ItemAdmin(admin.ModelAdmin):
 
 
     form = autocomplete_light.modelform_factory(Loan, fields='__all__')
     form = autocomplete_light.modelform_factory(Loan, fields='__all__')
 
 
+    inlines = [AddLoanInline, CurrentLoanInline, LoanHistoryInline]
+
     def give_back(self, request, queryset):
     def give_back(self, request, queryset):
         for item in queryset.filter(loans__loan_date_end=None):
         for item in queryset.filter(loans__loan_date_end=None):
             item.give_back()
             item.give_back()
     give_back.short_description = 'Rendre le matériel'
     give_back.short_description = 'Rendre le matériel'
 
 
+    def get_urls(self):
+        urls = super(ItemAdmin, self).get_urls()
+        my_urls = [
+            url(
+                r'^give_back/(?P<id>.+)$',
+                self.admin_site.admin_view(give_back_loan),
+                name='loan-give_back'),
+        ]
+        return my_urls + urls
+
 
 
 class StatusFilter(admin.SimpleListFilter):
 class StatusFilter(admin.SimpleListFilter):
     title = 'Statut'
     title = 'Statut'
@@ -110,54 +238,6 @@ class StatusFilter(admin.SimpleListFilter):
             return queryset
             return queryset
 
 
 
 
-class BorrowerFilter(admin.SimpleListFilter):
-    title = 'Adhérent emprunteur'
-    parameter_name = 'user'
-
-    def lookups(self, request, model_admin):
-        users = set()
-        for loan in model_admin.get_queryset(request):
-            users.add((loan.user.pk, loan.user))
-        return users
-
-    def queryset(self, request, queryset):
-        if self.value():
-            return queryset.filter(user=self.value())
-        else:
-            return queryset
-
-
-class ItemChoiceField(ModelChoiceField):
-    # On surcharge cette méthode pour afficher mac et n° de série dans le menu
-    # déroulant de sélection d'un objet dans la création d'un prêt.
-    def label_from_instance(self, obj):
-        return obj.designation + ' ' + obj.get_mac_and_serial()
-
-@admin.register(Loan)
-class LoanAdmin(admin.ModelAdmin):
-    list_display = ('item', 'get_mac_and_serial', 'user', 'loan_date', 'loan_date_end')
-    list_filter = (StatusFilter, BorrowerFilter, 'item__designation')
-    search_fields = (
-        'item__designation',
-        'user__nickname', 'user__username',
-        'user__first_name', 'user__last_name', )
-    actions = ['end_loan']
-
-    def end_loan(self, request, queryset):
-        queryset.filter(loan_date_end=None).update(
-            loan_date_end=datetime.now())
-    end_loan.short_description = 'Mettre fin au prêt'
-
-    form = autocomplete_light.modelform_factory(Loan, fields='__all__')
-
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'item':
-            kwargs['queryset'] = Item.objects.all()
-            return ItemChoiceField(**kwargs)
-        else:
-            return super(LoanAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
-
-
 @admin.register(Storage)
 @admin.register(Storage)
 class StorageAdmin(admin.ModelAdmin):
 class StorageAdmin(admin.ModelAdmin):
     list_display = ('name', 'truncated_notes', 'items_count')
     list_display = ('name', 'truncated_notes', 'items_count')
@@ -187,8 +267,10 @@ class LoanInline(admin.TabularInline):
     def has_delete_permission(self, request, obj=None):
     def has_delete_permission(self, request, obj=None):
         return False
         return False
 
 
+
 class MemberAdmin(coin.members.admin.MemberAdmin):
 class MemberAdmin(coin.members.admin.MemberAdmin):
     inlines = coin.members.admin.MemberAdmin.inlines + [LoanInline]
     inlines = coin.members.admin.MemberAdmin.inlines + [LoanInline]
 
 
+
 admin.site.unregister(coin.members.admin.Member)
 admin.site.unregister(coin.members.admin.Member)
 admin.site.register(coin.members.admin.Member, MemberAdmin)
 admin.site.register(coin.members.admin.Member, MemberAdmin)

+ 33 - 1
hardware_provisioning/models.py

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 
 
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from django.db.models import Q
 from django.db.models import Q
 from django.conf import settings
 from django.conf import settings
@@ -102,6 +103,14 @@ class Item(models.Model):
     is_available.boolean = True
     is_available.boolean = True
     is_available.short_description = 'disponible'
     is_available.short_description = 'disponible'
 
 
+    def current_borrower(self):
+        current_loan = self.loans.running().first()
+        if current_loan:
+            return current_loan.user
+        else:
+            return None
+    current_borrower.short_description = 'Prêté à'
+
     def get_mac_and_serial(self):
     def get_mac_and_serial(self):
         mac = self.mac_address
         mac = self.mac_address
         serial = self.serial
         serial = self.serial
@@ -109,6 +118,7 @@ class Item(models.Model):
             return "{} / {}".format(mac, serial)
             return "{} / {}".format(mac, serial)
         else:
         else:
             return mac or serial or ''
             return mac or serial or ''
+    get_mac_and_serial.short_description = 'Adresse MAC / n° de série'
 
 
     class Meta:
     class Meta:
         verbose_name = 'objet'
         verbose_name = 'objet'
@@ -152,9 +162,19 @@ class Loan(models.Model):
 
 
     def get_mac_and_serial(self):
     def get_mac_and_serial(self):
         return self.item.get_mac_and_serial()
         return self.item.get_mac_and_serial()
-
     get_mac_and_serial.short_description = "Adresse MAC / n° de série"
     get_mac_and_serial.short_description = "Adresse MAC / n° de série"
 
 
+    def short_date(self):
+        return '{:%d/%m/%y}'.format(self.loan_date)
+    short_date.short_description = "Emprunté le…"
+
+    def short_date_end(self):
+        if self.loan_date_end:
+            return '{:%d/%m/%y}'.format(self.loan_date_end)
+        else:
+            return None
+    short_date_end.short_description = "Rendu le…"
+
     def user_can_close(self, user):
     def user_can_close(self, user):
         return (not self.item.is_available()) and (self.user == user)
         return (not self.item.is_available()) and (self.user == user)
 
 
@@ -163,6 +183,18 @@ class Loan(models.Model):
     is_running.boolean = True
     is_running.boolean = True
     is_running.short_description = 'En cours ?'
     is_running.short_description = 'En cours ?'
 
 
+    def clean(self):
+        current_loan = self.item.get_current_loan()
+        if (
+                self.is_running()
+                and
+                current_loan
+                and
+                self.item.get_current_loan() != self
+        ):
+            raise ValidationError(
+                "Il y a déjà un emprunt en cours sur cet objet")
+
     class Meta:
     class Meta:
         verbose_name = 'prêt d’objet'
         verbose_name = 'prêt d’objet'
         verbose_name_plural = 'prêts d’objets'
         verbose_name_plural = 'prêts d’objets'