Commit 2e62bc71 authored by ljf's avatar ljf
Browse files

Merge branch 'jd-enhance-inventory-admin' into 'master'

Améliore l'interface d'admin des objets

Closes #189

See merge request ffdn/coin!6
parents 06a285b1 4da67e01
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -166,7 +166,6 @@ INSTALLED_APPS = (
    # Uncomment the next line to enable admin documentation:
    #'django.contrib.admindocs',
    'polymorphic',
    # 'south',
    'autocomplete_light',  # Automagic autocomplete foreingkey form component
    'activelink',          # Detect if a link match actual page
    'compat',
+182 −70
Original line number Diff line number Diff line
@@ -2,10 +2,12 @@

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.forms import ModelChoiceField
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
import autocomplete_light

from .models import ItemType, Item, Loan, Storage
@@ -17,8 +19,47 @@ User = get_user_model()
admin.site.register(ItemType)


def _give_back_success_messages(items, request):
    for item in items:
        messages.success(request, "{} a bien été marqué comme rendu.".format(item))
    if len(items) > 1:
        messages.warning(
            request,
            "N'oubliez pas de renseigner, pour chaque retour, le nouvel emprunteur "
            + "ou le nouveau lieu de stockage.",
        )
    else:
        messages.warning(
            request,
            "N'oubliez pas de renseigner le nouvel emprunteur "
            + "ou le nouveau lieu de stockage.",
        )



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
    loan = get_object_or_404(Loan, pk=id)
    try:
        redirect_url = request.META['HTTP_REFERER']
    except KeyError:
        # Fallback if no referer header is present
        redirect_url = reverse(
            'admin:hardware_provisioning_item_change',
            args=[loan.item.id]
        )

    loan.item.give_back()
    _give_back_success_messages([loan.item], request)

    return HttpResponseRedirect(redirect_url)


class OwnerFilter(admin.SimpleListFilter):
    title = "Propriétaire"
    title = "propriétaire"
    parameter_name = 'owner'

    def lookups(self, request, model_admin):
@@ -56,14 +97,113 @@ class AvailabilityFilter(admin.SimpleListFilter):
            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 = "Prêt 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 = 'détenteur actuel'
    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)
class ItemAdmin(admin.ModelAdmin):
    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 = (
        AvailabilityFilter, 'type', 'storage',
        'buy_date', OwnerFilter)
        'buy_date', BorrowerFilter, OwnerFilter)
    search_fields = (
        'designation', 'mac_address', 'serial',
        'owner__email', 'owner__nickname',
@@ -73,11 +213,25 @@ class ItemAdmin(admin.ModelAdmin):

    form = autocomplete_light.modelform_factory(Loan, fields='__all__')

    inlines = [AddLoanInline, CurrentLoanInline, LoanHistoryInline]

    def give_back(self, request, queryset):
        for item in queryset.filter(loans__loan_date_end=None):
        items = queryset.filter(loans__loan_date_end=None)
        for item in items:
            item.give_back()
        _give_back_success_messages(items, request)
    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):
    title = 'Statut'
@@ -110,54 +264,6 @@ class StatusFilter(admin.SimpleListFilter):
            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)
class StorageAdmin(admin.ModelAdmin):
    list_display = ('name', 'truncated_notes', 'items_count')
@@ -169,26 +275,32 @@ class StorageAdmin(admin.ModelAdmin):
            return obj.notes
    truncated_notes.short_description = 'notes'

class LoanInline(admin.TabularInline):
    model = Loan

class MemberLoanHistoryInline(LoanHistoryInline):
    verbose_name_plural = "Historique de prêt de matériel"


class MemberCurrentLoanInline(CurrentLoanInline):
    verbose_name_plural = "Prêts de matériel en cours"


class MemberAddLoanInline(AddLoanInline):
    extra = 0
    exclude = ('notes',)
    readonly_fields = ('item', 'get_mac_and_serial', 'loan_date', 'loan_date_end', 'is_running')
    max_num = 10
    verbose_name_plural = "Déclarer un prêt de matériel"

    show_change_link = True

    def get_queryset(self, request):
        qs = super(LoanInline, self).get_queryset(request)
        return qs.order_by('-loan_date_end')
# Enrich the MemberAdmin with hardware-related information
_MemberAdmin = admin.site._registry[coin.members.admin.Member].__class__

    def has_add_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False
class MemberAdmin(_MemberAdmin):
    inlines = _MemberAdmin.inlines + [
        MemberCurrentLoanInline,
        MemberAddLoanInline,
        MemberLoanHistoryInline,
    ]

class MemberAdmin(coin.members.admin.MemberAdmin):
    inlines = coin.members.admin.MemberAdmin.inlines + [LoanInline]

admin.site.unregister(coin.members.admin.Member)
admin.site.register(coin.members.admin.Member, MemberAdmin)
+17 −0
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-

import autocomplete_light
from models import Item


# This will generate a MemberAutocomplete class
autocomplete_light.register(Item,
                            # Just like in ModelAdmin.search_fields
                            search_fields=[
                                'designation', 'mac_address', 'serial'],
                            attrs={
                                # This will set the input placeholder attribute:
                                'placeholder': "Nom / adresse MAC / n° de série de l'objet",
                                'data-autocomplete-minimum-characters': 3,
                            },
)
+44 −2
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
from django.conf import settings
@@ -74,6 +75,14 @@ class Item(models.Model):
    objects = ItemQuerySet().as_manager()

    def __unicode__(self):
        if self.serial and self.mac_address:
            return '{} ({} / {})'.format(
                self.designation, self.mac_address, self.serial
            )
        elif self.serial or self.mac_address:
            return '{} ({})'.format(
                self.designation, self.mac_address or self.serial
            )
        return self.designation

    def save(self, *args, **kwargs):
@@ -102,6 +111,14 @@ class Item(models.Model):
    is_available.boolean = True
    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):
        mac = self.mac_address
        serial = self.serial
@@ -109,6 +126,7 @@ class Item(models.Model):
            return "{} / {}".format(mac, serial)
        else:
            return mac or serial or ''
    get_mac_and_serial.short_description = 'Adresse MAC / n° de série'

    class Meta:
        verbose_name = 'objet'
@@ -141,7 +159,9 @@ class Loan(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, verbose_name='membre',
        related_name='loans', null=True, on_delete=models.SET_NULL)
    loan_date = models.DateTimeField(verbose_name='date de prêt')
    loan_date = models.DateTimeField(
        verbose_name='date de prêt',
        default=timezone.now)
    loan_date_end = models.DateTimeField(verbose_name='date de fin de prêt',
                                     null=True, blank=True)
    notes = models.TextField(null=True, blank=True)
@@ -152,9 +172,19 @@ class Loan(models.Model):

    def get_mac_and_serial(self):
        return self.item.get_mac_and_serial()

    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):
        return (not self.item.is_available()) and (self.user == user)

@@ -163,6 +193,18 @@ class Loan(models.Model):
    is_running.boolean = True
    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:
        verbose_name = 'prêt d’objet'
        verbose_name_plural = 'prêts d’objets'
+4 −8
Original line number Diff line number Diff line
@@ -22,24 +22,20 @@
<table id="member_loans" class="full-width">
    <thead>
        <tr>
            <th>Type de matériel</th>
            <th>Matériel prêté</th>
            <th>Type de matériel</th>
            <th>Date de prêt</th>
            {% if view == 'old' %}<th>Date retour</th>{% endif %}
            <th>Addr. MAC</th>
            <th>Num. de série</th>
            {% if view != 'old' %}<th>Actions</th>{% endif %}
        </tr>
    </thead>
    <tbody>
        {% for loan in loans %}
        <tr>
            <td>{{ loan.item.type }}</td>
            <td>{{ loan.item }}</a></td>
            <td>{{ loan.loan_date }}</td>
            {% if view == 'old' %}<td>{{ loan.loan_date_end }}</td>{% endif %}
            <td>{{ loan.item.mac_address|default:"n/a" }}</td>
            <td>{{ loan.item.serial|default:"n/a" }}</td>
            <td>{{ loan.item.type }}</td>
            <td>{{ loan.loan_date|date:'j N Y' }}</td>
            {% if view == 'old' %}<td>{{ loan.loan_date_end|date:'j N Y' }}</td>{% endif %}
            {% if view != 'old' %}
            <td class="actions">
              <div class="button-group">
Loading