Wczoraj byłem zmuszony szybko dodać dodatkową funkcjonalność do jednej z rozwijanych przez nas aplikacji. Widok, o który chodziło opiera się na automatycznym, scaffoldowanym widoku administratora. Brzmi prosto?
Problem polegał na tym, że dostosowywanie logiki administratora jest dopiero w fazie planów (gałąź admin-newforms). W związku z tym, rozbudowanie go o własną logikę jest teoretycznie na tym etapie niewykonalne.
Przekopałem się więc przez większość kodu modułu django.contrib.admin i okazało się, że można Django nieco oszukać. Widoki są funkcjami, z czego korzysta się często przy generic views.
Nie można po nich dziedziczyć, ale można wołać jeden z drugiego.
Brzydki hack
Do pliku urls.py na samym początku (przed sekcją admin) dopisujemy:
(r'^admin/aplikacja/model/add/', 'serwis.aplikacja.admin_views.add_stage'),
Następnie zakładamy plik serwis/aplikacja/admin_views.py:
from django.contrib.admin.views import main def add_stage(request, *args, **kwargs): post = request.POST.copy() if request.method == 'POST': post['user'] = request.user.id request.POST = post return main.add_stage(request, 'aplikacja', 'Model', *args, **kwargs)
Voila!
Parę słów wytłumaczenia
Pierwsza część jest chyba oczywista, w tym przypadku nadpisujemy widok dodawania dla konkretnego modelu. Ponieważ Django przetwarza mapę rutingu w takiej kolejności, w jakiej są podane regułki, nasza linijka przesłoni znajdujące się niżej automatyczne reguły wbudowanego panelu administratora.
Druga część to utworzenie osobnego pliku dla widoków administracyjnych (nie musi to być osobny plik, ale to dobra praktyka). W pliku tym umieszczamy definicję własnego widoku add_stage.
W powyższym przykładzie dodawany jest automatycznie właściciel dla tworzonej instancji modelu. Nie bardzo można zrobić to w samym modelu, bo model jest bezstanowy i nie ma dostępu do stanu aplikacji. Poza tym, chcemy, żeby pole to było ustawiane tylko w tym przypadku.
Obiekt request.POST jest domyślnie immutable, więc tworzymy jego kopię i właśnie tę kopię modyfikujemy, a następnie podmieniamy oryginalny obiekt.
Ostatnia linijka powoduje wywołanie oryginalnego, magicznego
widoku administratora, dostarczonego przez Django. Z tą różnicą, że przekazujemy do niego nazwę aplikacji i modelu. W przypadku wywołania bezpośredniego, pochodzą one z wyrażenia regularnego w konfiguracji urls.py, tutaj musimy podać je jawnie.


by cezio
23 lut 2007 at 15:27
a propos hackowania django.contrib.admin, a w szczególności model.User, to jest też ciekawa lektura na http://www.b-list.org/weblog/2007/02/20/about-model-subclassing
by Brut[all]
24 mar 2007 at 19:21
Powiem szczerze, że dziwi mnie, czemu uważasz, że to taki straszny hack jest.
Właśnie dlatego w Django wszystko tu jest taki proste (generic views są najzwyklejszymi funkcjami, do których Django się najzwyczajniej odwołuje), aby można było z tym wyczyniać różne cuda.
Mnie się zdarzało już co najmniej parę razy nadpisywać działanie jakiegoś generic view poprzez napisanie własnej funkcji, która z tamtego korzystała.
Dajmy na to prosty przykład: chcemy mieć podstronę wyświetlającą informacje o jakimś obiekcie, ale tylko dla osób zalogowanych. Nie możemy skorzystać z django.views.generic.list_detail.object_detail, więc co, piszemy wszystko od początku? Nie, tworzymy funkcję, która nie robi nic innego, tylko wywołuje object_detail z tymi samymi parametrami, traktujemy ją dekoratorem login_required i już. Właściwie, to z naszej nowej funkcji możemy teraz korzystać jako z nowego generic view, wykorzystywać wszędzie, gdzie chcemy mieć object_detail tylko dla zalogowanych
(właściwie, to dla pojedyńczej sytuacji nawet nie trzeba tworzyć dodatkowej funkcji — w samym urls.py można od razu wstawić funkcję dekorującą z naszym object_detail jako parametr)
Podmienianie danych POST to już co innego — tu faktycznie stosujemy brzydki trik i przymus jego wykorzystania faktycznie jest spowodowany brakami w django.contrib.admin, ale jeśli chodzi o samo modyfikowanie widoku admina… to wydaje mi się jak najbardziej normalne.
Aha.
Jeśli mogę się przyczepić, to uważam, że jeszcze jedną rzecz mógłbyś zrobić dużo lepiej.
Normalna funkcja add_stage z admina przyjmuje w parametrach nazwę aplikacji i modelu. Ty, nadpisując ją, pozmieniałeś te parametry, co wydaje mi się zupełnie bez sensu — to chyba normalne, że jeśli coś dziedziczymy, czy nadpisujemy, to staramy się zostawić na wejściu i wyjściu dokładnie to samo?
Dużo lepiej by było, gdybyś zrobił tak:
return main.add_stage(request, *args, **kwargs)
Po prostu tak — standardowa zasada przy nadpisywaniu: „co przyszło, to wyjdzie”, czyli wszystko załatwiają za nas świetne *args, **kwargs Pythona.
Oczywiście w takim razie i do nadpisującej funkcji muszą przyjść odpowiednie parametry. W takim razie modyfikujemy jeszcze urls.py:
(r’^admin/(aplikacja)/(model)/add/’, ‘serwis.aplikacja.admin_views.add_stage’)
lub dodajemy nazwy ‘aplikacja’ i ‘model’ w słowniku w trzecim parametrze, choć to pierwsze rozwiązanie dużo lepsze.
Zyskujesz na tym:
1. Możliwość wykorzystania dokładnie tej samej modyfikacji dla innej aplikacji/modelu, gdybyś kiedyś chciał.
2. W cholerę i jeszcze trochę logiki, poprawności programistycznej, czy jakkolwiek tego jeszcze nie nazwiemy.