├── LICENSE ├── README.md ├── docs ├── .gitignore ├── Makefile ├── _static │ ├── book-covers │ │ ├── book-cover-api-320.png │ │ ├── book-cover-dac-320.png │ │ ├── book-cover-doc-320.png │ │ └── book-cover-mta-320.png │ ├── css │ │ └── custom.css │ └── js │ │ └── custom.js ├── _templates │ ├── breadcrumbs.html │ └── footer.html ├── access_no_is_staff.png ├── access_no_perms.png ├── access_one_perm.png ├── action_button_message.png ├── action_buttons.png ├── action_buttons.rst ├── add_actions.rst ├── add_model_twice.rst ├── book-cover.png ├── boolean_field_fixed.png ├── boolean_fields.rst ├── calculated_field.png ├── calculated_fields.rst ├── change_text.rst ├── changeview_readonly.png ├── changeview_readonly.rst ├── conf.py ├── current_user.rst ├── custom_button.png ├── custom_button.rst ├── database_view.png ├── database_view.rst ├── date_based_filtering.rst ├── date_filtering.png ├── disable_pagination.rst ├── edit_multiple_models.png ├── edit_multiple_models.rst ├── export.rst ├── export_action.png ├── export_selected.png ├── filter_calculated_fixed.png ├── filter_fk_dropdown.png ├── filter_fk_dropdown.rst ├── filtering_calculated_fields.rst ├── fk_display.png ├── fk_display.rst ├── icrease_row_count_to_one.png ├── imagefield.png ├── imagefield.rst ├── imagefield_fixed.png ├── images │ ├── default_listview.png │ ├── default_login.png │ ├── default_title.png │ ├── logo_fixed.png │ ├── plural.png │ ├── plural_fixed.png │ ├── remove_default_apps.png │ ├── remove_default_apps_fixed.png │ └── umsra_logo.png ├── import.rst ├── import_1.png ├── import_2.png ├── increase_row_count.rst ├── index.rst ├── introduction.rst ├── logo.rst ├── many_fks.rst ├── many_to_many.rst ├── models.rst ├── nested_inlines.rst ├── no_filtering.png ├── no_sorting.png ├── object_url.rst ├── one_to_one_inline.png ├── one_to_one_inline.rst ├── only_one.rst ├── optimize_queries.rst ├── ordering.png ├── override_default_templates.rst ├── override_save.rst ├── plural_text.rst ├── questions.txt ├── raw_id_fields_1.png ├── raw_id_fields_2.png ├── related_fk_display.png ├── remove_add_delete.rst ├── remove_add_perms.png ├── remove_default.rst ├── remove_delete_selected.png ├── remove_delete_selected.rst ├── restrict_parts.rst ├── set_ordering.rst ├── single_admin_multiple_model.png ├── single_admin_multiple_models.rst ├── sorting_calculated_field.png ├── sorting_calculated_fields.rst ├── specific_users.rst ├── two_admin.rst ├── uneditable_existing.png ├── uneditable_existing.rst └── uneditable_field.rst └── heroes_and_monsters ├── .gitignore ├── README.md ├── entities ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180221_1830.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── events ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── heroes_and_monsters ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── requirements.txt ├── static └── umsra_logo.png └── templates ├── admin ├── base_site.html └── csv_form.html └── entities ├── heroes_changelist.html └── villain_changeform.html /LICENSE: -------------------------------------------------------------------------------- 1 | This work is licensed under a CC-BY-SA 4.0 (Creative Commons Attribution-ShareAlike 4.0 International License) licence. 2 | The full text is available at: https://creativecommons.org/licenses/by-sa/4.0/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Django admin cookbook 2 | 3 | 4 | Django admin cookbook is a set of recipes of how to do things with Django. 5 | They take the form of about 50 questions of the form 6 | `How to do X with Django admin`. 7 | 8 | We have a set of models which we use across the book for answering these questions. 9 | 10 | ### The setup 11 | 12 | You have been hired by the United Mytic and Supernormal Research Agency - The UMSRA. UMSRA researches and documents mytic and supernormal events. You have been tasked with creating a web app where UMSRA researchers can document their findings and look up their collegue's research. 13 | 14 | ### The models 15 | 16 | You plan to write a set of models and an associated admin for UMSRA researchers. You come up with two apps `entities` and `events`. The models are 17 | 18 | 19 | #### Events 20 | 21 | 22 | from django.db import models 23 | from entities.models import Hero, Villain 24 | 25 | class Epic(models.Model): 26 | name = models.CharField(max_length=255) 27 | participating_heroes = models.ManyToManyField(Hero) 28 | participating_villains = models.ManyToManyField(Villain) 29 | 30 | 31 | class Event(models.Model): 32 | epic = models.ForeignKey(Epic, on_delete=models.CASCADE) 33 | details = models.TextField() 34 | years_ago = models.PositiveIntegerField() 35 | 36 | 37 | class EventHero(models.Model): 38 | event = models.ForeignKey(Event, on_delete=models.CASCADE) 39 | hero = models.ForeignKey(Hero, on_delete=models.CASCADE) 40 | is_primary = models.BooleanField() 41 | 42 | 43 | class EventVillain(models.Model): 44 | event = models.ForeignKey(Event, on_delete=models.CASCADE) 45 | hero = models.ForeignKey(Villain, on_delete=models.CASCADE) 46 | is_primary = models.BooleanField() 47 | 48 | 49 | #### Entities 50 | 51 | from django.db import models 52 | 53 | 54 | class Category(models.Model): 55 | name = models.CharField(max_length=100) 56 | 57 | 58 | class Origin(models.Model): 59 | name = models.CharField(max_length=100) 60 | 61 | 62 | class Entity(models.Model): 63 | GENDER_MALE = "Male" 64 | GENDER_FEMALE = "Female" 65 | GENDER_OTHERS = "Others/Unknown" 66 | 67 | name = models.CharField(max_length=100) 68 | alternative_name = models.CharField( 69 | max_length=100, null=True, blank=True 70 | ) 71 | 72 | 73 | category = models.ForeignKey(Category, on_delete=models.CASCADE) 74 | origin = models.ForeignKey(Origin, on_delete=models.CASCADE) 75 | gender = models.CharField( 76 | max_length=100, 77 | choices=( 78 | (GENDER_MALE, GENDER_MALE), 79 | (GENDER_FEMALE, GENDER_FEMALE), 80 | (GENDER_OTHERS, GENDER_OTHERS), 81 | ) 82 | ) 83 | description = models.TextField() 84 | 85 | class Meta: 86 | abstract = True 87 | 88 | 89 | class Hero(Entity): 90 | 91 | is_immortal = models.BooleanField(default=True) 92 | 93 | benevolence_factor = models.PositiveSmallIntegerField( 94 | help_text="How benevolent this hero is?" 95 | ) 96 | arbitrariness_factor = models.PositiveSmallIntegerField( 97 | help_text="How arbitrary this hero is?" 98 | ) 99 | # relationships 100 | father = models.ForeignKey( 101 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 102 | ) 103 | mother = models.ForeignKey( 104 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 105 | ) 106 | spouse = models.ForeignKey( 107 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 108 | ) 109 | 110 | 111 | class Villain(Entity): 112 | is_immortal = models.BooleanField(default=False) 113 | 114 | malevolence_factor = models.PositiveSmallIntegerField( 115 | help_text="How malevolent this villain is?" 116 | ) 117 | power_factor = models.PositiveSmallIntegerField( 118 | help_text="How powerful this villain is?" 119 | ) 120 | is_unique = models.BooleanField(default=True) 121 | count = models.PositiveSmallIntegerField(default=1) 122 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Djangoadmincookbook 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/book-covers/book-cover-api-320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/_static/book-covers/book-cover-api-320.png -------------------------------------------------------------------------------- /docs/_static/book-covers/book-cover-dac-320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/_static/book-covers/book-cover-dac-320.png -------------------------------------------------------------------------------- /docs/_static/book-covers/book-cover-doc-320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/_static/book-covers/book-cover-doc-320.png -------------------------------------------------------------------------------- /docs/_static/book-covers/book-cover-mta-320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/_static/book-covers/book-cover-mta-320.png -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .other-books{ 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | 7 | .other-books img{ 8 | margin-top: 10px; 9 | margin-left: 5px; 10 | margin-right: 5px; 11 | padding-left: 5px; 12 | padding-right: 5px; 13 | padding-top: 10px; 14 | border: 1px solid; 15 | border-color: black; 16 | } 17 | 18 | .contact-top{ 19 | float: right; 20 | } 21 | -------------------------------------------------------------------------------- /docs/_static/js/custom.js: -------------------------------------------------------------------------------- 1 | (function(s,u,m,o,j,v){j=u.createElement(m);v=u.getElementsByTagName(m)[0];j.async=1;j.src=o;j.dataset.sumoSiteId='9eb358d9f8384f5634e306290170a5799459c468e30c6f23ac7eafcb14285cbd';v.parentNode.insertBefore(j,v)})(window,document,'script','//load.sumo.com/'); 2 | -------------------------------------------------------------------------------- /docs/_templates/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {%- extends "sphinx_rtd_theme/breadcrumbs.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • {{ _('Books') }} »
  • 5 |
  • {{ _('Django Admin Cookbook') }} »
  • 6 | {% for doc in parents %} 7 |
  • {{ doc.title }} »
  • 8 | {% endfor %} 9 |
  • {{ title }}
  • 10 | {% endblock %} 11 | 12 | 13 | {% block breadcrumbs_aside %} 14 | hello@agiliq.com 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /docs/_templates/footer.html: -------------------------------------------------------------------------------- 1 | {%- extends "sphinx_rtd_theme/footer.html" %} 2 | 3 | {%- block extrafooter %} 4 |

    5 | Read more books at https://books.agiliq.com 6 |

    7 | 8 |
    9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /docs/access_no_is_staff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/access_no_is_staff.png -------------------------------------------------------------------------------- /docs/access_no_perms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/access_no_perms.png -------------------------------------------------------------------------------- /docs/access_one_perm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/access_one_perm.png -------------------------------------------------------------------------------- /docs/action_button_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/action_button_message.png -------------------------------------------------------------------------------- /docs/action_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/action_buttons.png -------------------------------------------------------------------------------- /docs/action_buttons.rst: -------------------------------------------------------------------------------- 1 | How to add Custom Action Buttons (not actions) to Django Admin list page? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | UMSRA has decided that given sufficient kryptonite, all Heroes are mortal. 5 | However, they want to be able to change their mind and say all heroes are immortal. 6 | 7 | You have been asked to add two buttons - One which makes all heroes mortal, and one which makes all immortal. Since it affects all heroes irrespective of the selection, this needs to be a separate button, not an action dropdown. 8 | 9 | First, we will change the template on the :code:`HeroAdmin` so we can add two buttons.:: 10 | 11 | @admin.register(Hero) 12 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 13 | change_list_template = "entities/heroes_changelist.html" 14 | 15 | Then we will override the :code:`get_urls`, and add the :code:`set_immortal` and :code:`set_mortal` methods on the model admin. They will serve as the two view methods.:: 16 | 17 | def get_urls(self): 18 | urls = super().get_urls() 19 | my_urls = [ 20 | path('immortal/', self.set_immortal), 21 | path('mortal/', self.set_mortal), 22 | ] 23 | return my_urls + urls 24 | 25 | def set_immortal(self, request): 26 | self.model.objects.all().update(is_immortal=True) 27 | self.message_user(request, "All heroes are now immortal") 28 | return HttpResponseRedirect("../") 29 | 30 | def set_mortal(self, request): 31 | self.model.objects.all().update(is_immortal=False) 32 | self.message_user(request, "All heroes are now mortal") 33 | return HttpResponseRedirect("../") 34 | 35 | Finally, we create the :code:`entities/heroes_changelist.html` template by extending the :code:`admin/change_list.html`.:: 36 | 37 | 38 | {% extends 'admin/change_list.html' %} 39 | 40 | {% block object-tools %} 41 |
    42 |
    43 | {% csrf_token %} 44 | 45 |
    46 |
    47 | {% csrf_token %} 48 | 49 |
    50 |
    51 |
    52 | {{ block.super }} 53 | {% endblock %} 54 | 55 | 56 | .. image:: action_buttons.png 57 | 58 | And after using the `make_mortal` action, the Heroes are all mortal and you see this message. 59 | 60 | .. image:: action_button_message.png 61 | -------------------------------------------------------------------------------- /docs/add_actions.rst: -------------------------------------------------------------------------------- 1 | How to add additional actions in Django admin? 2 | +++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | Django admin allows you to add additional actions which allow you to do bulk actions. 5 | You have been asked to add an action which will mark multiple :code:`Hero` s as immortal. 6 | 7 | You can do this by adding the action as method to ModelAdmin 8 | and adding the method as a string to :code:`actions` :: 9 | 10 | actions = ["mark_immortal"] 11 | 12 | def mark_immortal(self, request, queryset): 13 | queryset.update(is_immortal=True) 14 | 15 | -------------------------------------------------------------------------------- /docs/add_model_twice.rst: -------------------------------------------------------------------------------- 1 | How to add a model twice to Django admin? 2 | +++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | 5 | You need to add the :code:`Hero` model twice to the admin, one as a regular admin area, and one as read only admin. (Some user will potentially see only the read only admin.) 6 | 7 | If you have try to register the same model twice:: 8 | 9 | admin.site.register(Hero) 10 | admin.site.register(Hero) 11 | 12 | you will get an error like this:: 13 | 14 | raise AlreadyRegistered('The model %s is already registered' % model.__name__) 15 | 16 | THe solution is to sublass the :code:`Hero` model as a ProxyModel.:: 17 | 18 | # In models.py 19 | class HeroProxy(Hero): 20 | 21 | class Meta: 22 | proxy = True 23 | 24 | ... 25 | # In admin.py 26 | @admin.register(Hero) 27 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 28 | list_display = ("name", "is_immortal", "category", "origin", "is_very_benevolent") 29 | .... 30 | 31 | 32 | @admin.register(HeroProxy) 33 | class HeroProxyAdmin(admin.ModelAdmin): 34 | readonly_fields = ("name", "is_immortal", "category", "origin", 35 | ...) 36 | 37 | -------------------------------------------------------------------------------- /docs/book-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/book-cover.png -------------------------------------------------------------------------------- /docs/boolean_field_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/boolean_field_fixed.png -------------------------------------------------------------------------------- /docs/boolean_fields.rst: -------------------------------------------------------------------------------- 1 | How to show “on” or “off” icons for calculated boolean fields? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | In the previous chapter, :doc:`filtering_calculated_fields` you added a boolean field.:: 5 | 6 | def is_very_benevolent(self, obj): 7 | return obj.benevolence_factor > 75 8 | 9 | Which looks like this 10 | 11 | .. image:: filter_calculated_fixed.png 12 | 13 | The :code:`is_very_benevolent` field shows the string `True` and `False`, unlike the builtin BooleanFields which show an on and off indicator. 14 | To fix this, you add a :code:`boolean` attribute on your method. You final modeladmin looks like this:: 15 | 16 | @admin.register(Hero) 17 | class HeroAdmin(admin.ModelAdmin): 18 | list_display = ("name", "is_immortal", "category", "origin", "is_very_benevolent") 19 | list_filter = ("is_immortal", "category", "origin", IsVeryBenevolentFilter) 20 | 21 | def is_very_benevolent(self, obj): 22 | return obj.benevolence_factor > 75 23 | 24 | is_very_benevolent.boolean = True 25 | 26 | And your admin looks like this 27 | 28 | .. image:: boolean_field_fixed.png 29 | -------------------------------------------------------------------------------- /docs/calculated_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/calculated_field.png -------------------------------------------------------------------------------- /docs/calculated_fields.rst: -------------------------------------------------------------------------------- 1 | How to show calculated fields on listview page? 2 | =========================================================== 3 | 4 | You have an admin for the :code:`Origin` model like this:: 5 | 6 | @admin.register(Origin) 7 | class OriginAdmin(admin.ModelAdmin): 8 | list_display = ("name",) 9 | 10 | 11 | Apart from the name, we also want to show the number of heroes and number of villains for each origin, which is not a DB field on :code:`Origin`. 12 | You can do this in two ways. 13 | 14 | 15 | Adding a method to the model 16 | ++++++++++++++++++++++++++++++++++++++++++ 17 | 18 | You can add two methods to your :code:`Origin` model like this:: 19 | 20 | 21 | def hero_count(self,): 22 | return self.hero_set.count() 23 | 24 | def villain_count(self): 25 | return self.villain_set.count() 26 | 27 | And change :code:`list_display` to :code:`list_display = ("name", "hero_count", "villain_count")`. 28 | 29 | 30 | Adding a method to the ModelAdmin 31 | ++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | If you don't want to add method to the model, you can do instead add the method to the ModelAdmin. :: 34 | 35 | 36 | 37 | def hero_count(self, obj): 38 | return obj.hero_set.count() 39 | 40 | def villain_count(self, obj): 41 | return obj.villain_set.count() 42 | 43 | 44 | The :code:`list_display`, as earlier, changes to :code:`list_display = ("name", "hero_count", "villain_count")`. 45 | 46 | Performance considerations for calculated_fields 47 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 48 | 49 | With either of the above approaches, you would be running two exta queries per object (One per calculated field). You can find how to optimize this in 50 | :doc:`optimize_queries`. 51 | 52 | 53 | With any of these changes your admin looks like this: 54 | 55 | .. image:: calculated_field.png 56 | -------------------------------------------------------------------------------- /docs/change_text.rst: -------------------------------------------------------------------------------- 1 | How to change 'Django administration' text? 2 | ================================================ 3 | 4 | By default Django admin shows 'Django administration'. You have been asked to replace this with 'UMSRA Administration' 5 | 6 | The text is at these pages: 7 | 8 | - Login Page 9 | - The listview page 10 | - The HTML title tag 11 | 12 | Login, Listview and Changeview Page 13 | ++++++++++++++++++++++++++++++++++++++++++++ 14 | 15 | By default it looks like this and is set to :code:`“Django administration”` 16 | 17 | .. image:: images/default_login.png 18 | 19 | :code:`site_header` can be set to change this. 20 | 21 | Listview Page 22 | ++++++++++++++++++++++ 23 | 24 | By default it looks like this and is set to :code:`“Site administration”` 25 | 26 | .. image:: images/default_listview.png 27 | 28 | :code:`index_title` can be set to change this. 29 | 30 | 31 | 32 | HTML title tag 33 | ++++++++++++++++++++++ 34 | 35 | By default it looks like this and is set to :code:`“Django site admin”` 36 | 37 | .. image:: images/default_title.png 38 | 39 | 40 | :code:`site_title` can be set to change this. 41 | 42 | 43 | We can make the three changes in urls.py:: 44 | 45 | 46 | admin.site.site_header = "UMSRA Admin" 47 | admin.site.site_title = "UMSRA Admin Portal" 48 | admin.site.index_title = "Welcome to UMSRA Researcher Portal" 49 | 50 | -------------------------------------------------------------------------------- /docs/changeview_readonly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/changeview_readonly.png -------------------------------------------------------------------------------- /docs/changeview_readonly.rst: -------------------------------------------------------------------------------- 1 | How to mark a field as readonly in admin? 2 | ++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | UMSRA has temporarily decided to stop tracking the family trees of mythological entities. You have been asked to make the :code:`father`, :code:`mother` and :code:`spouse` fields readonly. 5 | 6 | You can do this by:: 7 | 8 | @admin.register(Hero) 9 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 10 | ... 11 | readonly_fields = ["father", "mother", "spouse"] 12 | 13 | Your create form looks like this: 14 | 15 | .. image:: changeview_readonly.png 16 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # -- General configuration ------------------------------------------------ 6 | 7 | # If your documentation needs a minimal Sphinx version, state it here. 8 | # 9 | # needs_sphinx = '1.0' 10 | 11 | # Add any Sphinx extension module names here, as strings. They can be 12 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 13 | # ones. 14 | extensions = [] 15 | 16 | # Add any paths that contain templates here, relative to this directory. 17 | templates_path = ['_templates',] 18 | 19 | 20 | # The suffix(es) of source filenames. 21 | # You can specify multiple suffix as a list of string: 22 | # 23 | # source_suffix = ['.rst', '.md'] 24 | source_suffix = '.rst' 25 | 26 | # The master toctree document. 27 | master_doc = 'index' 28 | 29 | # General information about the project. 30 | project = 'Django Admin Cookbook' 31 | copyright = '2018, Agiliq' 32 | author = 'Agiliq' 33 | 34 | # The version info for the project you're documenting, acts as replacement for 35 | # |version| and |release|, also used in various other places throughout the 36 | # built documents. 37 | # 38 | # This will track the Django version against which this is written. 39 | version = '2.0' 40 | 41 | release = '2.0' 42 | 43 | # The language for content autogenerated by Sphinx. Refer to documentation 44 | # for a list of supported languages. 45 | # 46 | # This is also used if you do content translation via gettext catalogs. 47 | # Usually you set "language" from the command line for these cases. 48 | language = None 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This patterns also effect to html_static_path and html_extra_path 53 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 54 | 55 | # The name of the Pygments (syntax highlighting) style to use. 56 | pygments_style = 'perldoc' 57 | 58 | # If true, `todo` and `todoList` produce output, else they produce nothing. 59 | todo_include_todos = False 60 | 61 | 62 | # -- Options for HTML output ---------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | # 67 | html_theme = 'sphinx_rtd_theme' 68 | 69 | # Theme options are theme-specific and customize the look and feel of a theme 70 | # further. For a list of options available for each theme, see the 71 | # documentation. 72 | # 73 | # html_theme_options = {} 74 | 75 | # Add any paths that contain custom static files (such as style sheets) here, 76 | # relative to this directory. They are copied after the builtin static files, 77 | # so a file named "default.css" will overwrite the builtin "default.css". 78 | html_static_path = ['_static'] 79 | 80 | # Custom sidebar templates, must be a dictionary that maps document names 81 | # to template names. 82 | # 83 | # This is required for the alabaster theme 84 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 85 | html_sidebars = { 86 | '**': [ 87 | 'relations.html', # needs 'show_related': True theme option to display 88 | 'searchbox.html', 89 | ] 90 | } 91 | 92 | # -- Options for LaTeX output --------------------------------------------- 93 | 94 | latex_elements = { 95 | # The paper size ('letterpaper' or 'a4paper'). 96 | # 97 | # 'papersize': 'letterpaper', 98 | 99 | # The font size ('10pt', '11pt' or '12pt'). 100 | # 101 | # 'pointsize': '10pt', 102 | 103 | # Additional stuff for the LaTeX preamble. 104 | # 105 | # 'preamble': '', 106 | 107 | # Latex figure (float) alignment 108 | # 109 | # 'figure_align': 'htbp', 110 | } 111 | 112 | # Grouping the document tree into LaTeX files. List of tuples 113 | # (source start file, target name, title, 114 | # author, documentclass [howto, manual, or own class]). 115 | latex_documents = [ 116 | (master_doc, 'Djangoadmincookbook.tex', 'Django Admin Cookbook', 117 | 'Agiliq', 'howto'), 118 | ] 119 | # -- Options for RTD (Read The Docs) service --------------------------------------------- 120 | RTD_NEW_THEME = True 121 | 122 | # -- Custom marketing js --- 123 | def setup(app): 124 | app.add_javascript('js/custom.js') 125 | app.add_stylesheet('css/custom.css') 126 | 127 | 128 | # Design customizations 129 | 130 | html_theme_options = { 131 | 'display_version': False, 132 | } 133 | 134 | html_show_sphinx = False 135 | -------------------------------------------------------------------------------- /docs/current_user.rst: -------------------------------------------------------------------------------- 1 | How to associate model with current user while saving? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | The :code:`Hero` model has the following field.:: 5 | 6 | added_by = models.ForeignKey(settings.AUTH_USER_MODEL, 7 | null=True, blank=True, on_delete=models.SET_NULL) 8 | 9 | 10 | You want the :code:`added_by` field to be automatically set to current user whenever object is created from admin. You can do this.:: 11 | 12 | def save_model(self, request, obj, form, change): 13 | if not obj.pk: 14 | # Only set added_by during the first save. 15 | obj.added_by = request.user 16 | super().save_model(request, obj, form, change) 17 | 18 | If instead you wanted to always save the current user, you can do.:: 19 | 20 | def save_model(self, request, obj, form, change): 21 | obj.added_by = request.user 22 | super().save_model(request, obj, form, change) 23 | 24 | If you also want to hide the :code:`added_by` field to not show up on the change form, you can do.:: 25 | 26 | 27 | @admin.register(Hero) 28 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 29 | ... 30 | exclude = ['added_by',] 31 | 32 | -------------------------------------------------------------------------------- /docs/custom_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/custom_button.png -------------------------------------------------------------------------------- /docs/custom_button.rst: -------------------------------------------------------------------------------- 1 | How to add a custom button to Django change view page? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | :code:`Villain` has a field called :code:`is_unique`:: 5 | 6 | class Villain(Entity): 7 | ... 8 | is_unique = models.BooleanField(default=True) 9 | 10 | 11 | You want to add a button on Villain change form page called "Make Unique", which make this Villain unique. 12 | Any other villain with the same name should be deleted. 13 | 14 | You start by extending the :code:`change_form` to add a new button.:: 15 | 16 | {% extends 'admin/change_form.html' %} 17 | 18 | {% block submit_buttons_bottom %} 19 | {{ block.super }} 20 |
    21 | 22 |
    23 | {% endblock %} 24 | 25 | Then you can override :code:`response_change` and connect your template to the :code:`VillainAdmin`.:: 26 | 27 | @admin.register(Villain) 28 | class VillainAdmin(admin.ModelAdmin, ExportCsvMixin): 29 | ... 30 | change_form_template = "entities/villain_changeform.html" 31 | 32 | 33 | def response_change(self, request, obj): 34 | if "_make-unique" in request.POST: 35 | matching_names_except_this = self.get_queryset(request).filter(name=obj.name).exclude(pk=obj.id) 36 | matching_names_except_this.delete() 37 | obj.is_unique = True 38 | obj.save() 39 | self.message_user(request, "This villain is now unique") 40 | return HttpResponseRedirect(".") 41 | return super().response_change(request, obj) 42 | 43 | This is how your admin looks now. 44 | 45 | .. image:: custom_button.png 46 | -------------------------------------------------------------------------------- /docs/database_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/database_view.png -------------------------------------------------------------------------------- /docs/database_view.rst: -------------------------------------------------------------------------------- 1 | How to add a database view to Django admin? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | You have a database view, created as this:: 5 | 6 | create view entities_entity as 7 | select id, name from entities_hero 8 | union 9 | select 10000+id as id, name from entities_villain 10 | 11 | 12 | It has all the names from :code:`Hero` and :code:`Villain`. The id's for Villain are set to :code:`10000+id as id` 13 | because we don't intend to cross 10000 Heroes:: 14 | 15 | sqlite> select * from entities_entity; 16 | 1|Krishna 17 | 2|Vishnu 18 | 3|Achilles 19 | 4|Thor 20 | 5|Zeus 21 | 6|Athena 22 | 7|Apollo 23 | 10001|Ravana 24 | 10002|Fenrir 25 | 26 | Then you add a :code:`managed=False` model:: 27 | 28 | class AllEntity(models.Model): 29 | name = models.CharField(max_length=100) 30 | 31 | class Meta: 32 | managed = False 33 | db_table = "entities_entity" 34 | 35 | And add it to admin.:: 36 | 37 | @admin.register(AllEntity) 38 | class AllEntiryAdmin(admin.ModelAdmin): 39 | list_display = ("id", "name") 40 | 41 | And your admin looks like this 42 | 43 | .. image:: database_view.png 44 | -------------------------------------------------------------------------------- /docs/date_based_filtering.rst: -------------------------------------------------------------------------------- 1 | How to add date based filtering in Django admin? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | You can add a date based filtering on any date field by setting the :code:`date_hierarchy`.:: 5 | 6 | @admin.register(Hero) 7 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 8 | ... 9 | date_hierarchy = 'added_on' 10 | 11 | 12 | It looks like this: 13 | 14 | .. image:: date_filtering.png 15 | 16 | This can be very costly with a large number of objects. As an alternative, you can subclass :code:`SimpleListFilter`, and allow filtering only on years or the months. 17 | -------------------------------------------------------------------------------- /docs/date_filtering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/date_filtering.png -------------------------------------------------------------------------------- /docs/disable_pagination.rst: -------------------------------------------------------------------------------- 1 | How to disable django admin pagination? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | If you want to completely disable pagination on a admin listview page, you can do this. :: 5 | 6 | 7 | import sys 8 | ... 9 | 10 | @admin.register(Hero) 11 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 12 | ... 13 | 14 | list_per_page = sys.maxsize 15 | 16 | You can also read :doc:`increase_row_count`. 17 | -------------------------------------------------------------------------------- /docs/edit_multiple_models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/edit_multiple_models.png -------------------------------------------------------------------------------- /docs/edit_multiple_models.rst: -------------------------------------------------------------------------------- 1 | How to edit mutiple models from one Django admin? 2 | ===================================================== 3 | 4 | To be able to edit multiple objects from one Django admin, you need to use inlines. 5 | 6 | You have the :code:`Category` model, and you need to add and edit :code:`Villain` models inside the admin for Category. You can do:: 7 | 8 | 9 | class VillainInline(admin.StackedInline): 10 | model = Villain 11 | 12 | @admin.register(Category) 13 | class CategoryAdmin(admin.ModelAdmin): 14 | ... 15 | 16 | inlines = [VillainInline] 17 | 18 | You can see the form to add and edit :code:`Villain` inside the :code:`Category` admin. If the Inline model has alot of fields, 19 | use :code:`StackedInline` else use :code:`TabularInline`. 20 | 21 | 22 | 23 | .. image:: edit_multiple_models.png 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/export.rst: -------------------------------------------------------------------------------- 1 | How to export CSV from Django admin? 2 | ++++++++++++++++++++++++++++++++++++ 3 | 4 | You have been asked to add ability to export :code:`Hero` and :code:`Villain` from the admin. 5 | There are a number of third party apps which allow doing this, but its quite easy without adding another dependency. 6 | You will add an admin action to :code:`HeroAdmin` and :code:`VillanAdmin`. 7 | 8 | An admin action always has this signature :code:`def admin_action(modeladmin, request, queryset):`, alternatively you can add it directly as a method on the :code:`ModelAdmin` like this:: 9 | 10 | class SomeModelAdmin(admin.ModelAdmin): 11 | 12 | def admin_action(self, request, queryset): 13 | 14 | 15 | To add csv export to :code:`HeroAdmin` you can do something like this:: 16 | 17 | actions = ["export_as_csv"] 18 | 19 | def export_as_csv(self, request, queryset): 20 | pass 21 | 22 | export_as_csv.short_description = "Export Selected" 23 | 24 | This adds an action called export selected, which looks like this: 25 | 26 | .. image:: export_selected.png 27 | 28 | You will then change the :code:`export_as_csv` to this:: 29 | 30 | import csv 31 | from django.http import HttpResponse 32 | ... 33 | 34 | def export_as_csv(self, request, queryset): 35 | 36 | meta = self.model._meta 37 | field_names = [field.name for field in meta.fields] 38 | 39 | response = HttpResponse(content_type='text/csv') 40 | response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) 41 | writer = csv.writer(response) 42 | 43 | writer.writerow(field_names) 44 | for obj in queryset: 45 | row = writer.writerow([getattr(obj, field) for field in field_names]) 46 | 47 | return response 48 | 49 | This exports all of the selected rows. If you notice, :code:`export_as_csv` doens't have anything specific to :code:`Hero`, 50 | so you can extract the method to a mixin. 51 | 52 | With the changes, your code looks like this:: 53 | 54 | 55 | class ExportCsvMixin: 56 | def export_as_csv(self, request, queryset): 57 | 58 | meta = self.model._meta 59 | field_names = [field.name for field in meta.fields] 60 | 61 | response = HttpResponse(content_type='text/csv') 62 | response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) 63 | writer = csv.writer(response) 64 | 65 | writer.writerow(field_names) 66 | for obj in queryset: 67 | row = writer.writerow([getattr(obj, field) for field in field_names]) 68 | 69 | return response 70 | 71 | export_as_csv.short_description = "Export Selected" 72 | 73 | 74 | @admin.register(Hero) 75 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 76 | list_display = ("name", "is_immortal", "category", "origin", "is_very_benevolent") 77 | list_filter = ("is_immortal", "category", "origin", IsVeryBenevolentFilter) 78 | actions = ["export_as_csv"] 79 | 80 | ... 81 | 82 | 83 | @admin.register(Villain) 84 | class VillainAdmin(admin.ModelAdmin, ExportCsvMixin): 85 | list_display = ("name", "category", "origin") 86 | actions = ["export_as_csv"] 87 | 88 | You can add such an export to other models by subclassing from :code:`ExportCsvMixin` 89 | -------------------------------------------------------------------------------- /docs/export_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/export_action.png -------------------------------------------------------------------------------- /docs/export_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/export_selected.png -------------------------------------------------------------------------------- /docs/filter_calculated_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/filter_calculated_fixed.png -------------------------------------------------------------------------------- /docs/filter_fk_dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/filter_fk_dropdown.png -------------------------------------------------------------------------------- /docs/filter_fk_dropdown.rst: -------------------------------------------------------------------------------- 1 | How to filter FK dropdown values in django admin? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | Your :code:`Hero` model has a FK to :code:`Category`. 5 | So all category objects will show in the admin dropdown for category. If instead, you wanted to see only a subset, 6 | Django allows you to customize that by overriding :code:`formfield_for_foreignkey`:: 7 | 8 | @admin.register(Hero) 9 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 10 | ... 11 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 12 | if db_field.name == "category": 13 | kwargs["queryset"] = Category.objects.filter(name__in=['God', 'Demi God']) 14 | return super().formfield_for_foreignkey(db_field, request, **kwargs) 15 | 16 | .. image:: filter_fk_dropdown.png 17 | -------------------------------------------------------------------------------- /docs/filtering_calculated_fields.rst: -------------------------------------------------------------------------------- 1 | How to enable filtering on calculated fields? 2 | =========================================================== 3 | 4 | 5 | You have a :code:`Hero` admin which looks like this:: 6 | 7 | @admin.register(Hero) 8 | class HeroAdmin(admin.ModelAdmin): 9 | list_display = ("name", "is_immortal", "category", "origin", "is_very_benevolent") 10 | list_filter = ("is_immortal", "category", "origin",) 11 | 12 | def is_very_benevolent(self, obj): 13 | return obj.benevolence_factor > 75 14 | 15 | It has one calculated field :code:`is_very_benevolent`, and your admin looks like this 16 | 17 | .. image:: no_filtering.png 18 | 19 | You have added filtering on the fields which come from the models, but you also want to add filtering on the calculated field. To do this, you will need to subclass 20 | :code:`SimpleListFilter` like this:: 21 | 22 | 23 | class IsVeryBenevolentFilter(admin.SimpleListFilter): 24 | title = 'is_very_benevolent' 25 | parameter_name = 'is_very_benevolent' 26 | 27 | def lookups(self, request, model_admin): 28 | return ( 29 | ('Yes', 'Yes'), 30 | ('No', 'No'), 31 | ) 32 | 33 | def queryset(self, request, queryset): 34 | value = self.value() 35 | if value == 'Yes': 36 | return queryset.filter(benevolence_factor__gt=75) 37 | elif value == 'No': 38 | return queryset.exclude(benevolence_factor__gt=75) 39 | return queryset 40 | 41 | And then change your :code:`list_filter` to :code:`list_filter = ("is_immortal", "category", "origin", IsVeryBenevolentFilter)`. 42 | 43 | With this you can filter on the calculated field, and your admin looks like this: 44 | 45 | .. image:: filter_calculated_fixed.png 46 | -------------------------------------------------------------------------------- /docs/fk_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/fk_display.png -------------------------------------------------------------------------------- /docs/fk_display.rst: -------------------------------------------------------------------------------- 1 | How to change ForeignKey display text in dropdowns? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | :code:`Hero` has a FK to :code:`Catgeory`. In the dropdown, rather than just the name, you want to show the text "Category: ". 5 | 6 | You can change the :code:`__str__` method on :code:`Category`, but you only want this change in the admin. 7 | You can do this by creating a subclassing :code:`forms.ModelChoiceField` with a custom :code:`label_from_instance`.:: 8 | 9 | 10 | class CategoryChoiceField(forms.ModelChoiceField): 11 | def label_from_instance(self, obj): 12 | return "Category: {}".format(obj.name) 13 | 14 | You can then override :code:`formfield_for_foreignkey` to use this field type for category.:: 15 | 16 | 17 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 18 | if db_field.name == 'category': 19 | return CategoryChoiceField(queryset=Category.objects.all()) 20 | return super().formfield_for_foreignkey(db_field, request, **kwargs) 21 | 22 | Your admin look like this. 23 | 24 | 25 | .. image:: fk_display.png 26 | -------------------------------------------------------------------------------- /docs/icrease_row_count_to_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/icrease_row_count_to_one.png -------------------------------------------------------------------------------- /docs/imagefield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/imagefield.png -------------------------------------------------------------------------------- /docs/imagefield.rst: -------------------------------------------------------------------------------- 1 | How to show image from Imagefield in Django admin. 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | In your :code:`Hero` model, you have an image field.:: 5 | 6 | headshot = models.ImageField(null=True, blank=True, upload_to="hero_headshots/") 7 | 8 | By default it shows up like this: 9 | 10 | .. image:: imagefield.png 11 | 12 | 13 | You have been asked to change it to that the actual image also shows up on the change page. You can do it likethis:: 14 | 15 | 16 | @admin.register(Hero) 17 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 18 | 19 | readonly_fields = [..., "headshot_image"] 20 | 21 | def headshot_image(self, obj): 22 | return mark_safe(''.format( 23 | url = obj.headshot.url, 24 | width=obj.headshot.width, 25 | height=obj.headshot.height, 26 | ) 27 | ) 28 | 29 | With this change, your imagefield looks like this: 30 | 31 | .. image:: imagefield_fixed.png 32 | -------------------------------------------------------------------------------- /docs/imagefield_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/imagefield_fixed.png -------------------------------------------------------------------------------- /docs/images/default_listview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/default_listview.png -------------------------------------------------------------------------------- /docs/images/default_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/default_login.png -------------------------------------------------------------------------------- /docs/images/default_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/default_title.png -------------------------------------------------------------------------------- /docs/images/logo_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/logo_fixed.png -------------------------------------------------------------------------------- /docs/images/plural.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/plural.png -------------------------------------------------------------------------------- /docs/images/plural_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/plural_fixed.png -------------------------------------------------------------------------------- /docs/images/remove_default_apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/remove_default_apps.png -------------------------------------------------------------------------------- /docs/images/remove_default_apps_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/remove_default_apps_fixed.png -------------------------------------------------------------------------------- /docs/images/umsra_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/images/umsra_logo.png -------------------------------------------------------------------------------- /docs/import.rst: -------------------------------------------------------------------------------- 1 | How to import CSV using Django admin? 2 | ++++++++++++++++++++++++++++++++++++++ 3 | 4 | 5 | You have been asked to allow csv imports on the :code:`Hero` admin. You will do this by adding a link to the :code:`Hero` changelist page, which will take to a page with an upload form. You will write a handler for the `POST` action to create the objects from the csv.:: 6 | 7 | 8 | class CsvImportForm(forms.Form): 9 | csv_file = forms.FileField() 10 | 11 | @admin.register(Hero) 12 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 13 | ... 14 | change_list_template = "entities/heroes_changelist.html" 15 | 16 | def get_urls(self): 17 | urls = super().get_urls() 18 | my_urls = [ 19 | ... 20 | path('import-csv/', self.import_csv), 21 | ] 22 | return my_urls + urls 23 | 24 | def import_csv(self, request): 25 | if request.method == "POST": 26 | csv_file = request.FILES["csv_file"] 27 | reader = csv.reader(csv_file) 28 | # Create Hero objects from passed in data 29 | # ... 30 | self.message_user(request, "Your csv file has been imported") 31 | return redirect("..") 32 | form = CsvImportForm() 33 | payload = {"form": form} 34 | return render( 35 | request, "admin/csv_form.html", payload 36 | ) 37 | 38 | 39 | Then you create the :code:`entities/heroes_changelist.html` template, by overriding the :code:`admin/change_list.html` template like this.:: 40 | 41 | {% extends 'admin/change_list.html' %} 42 | 43 | {% block object-tools %} 44 | Import CSV 45 |
    46 | {{ block.super }} 47 | {% endblock %} 48 | 49 | 50 | Finally you create the :code:`csv_form.html` like this.:: 51 | 52 | {% extends 'admin/base.html' %} 53 | 54 | {% block content %} 55 |
    56 |
    57 | {{ form.as_p }} 58 | {% csrf_token %} 59 | 60 | 61 |
    62 |
    63 |
    64 | 65 | {% endblock %} 66 | 67 | With these changes, you get a link on the :code:`Hero` changelist page. 68 | 69 | .. image:: import_1.png 70 | 71 | And the import form apge looks like this. 72 | 73 | .. image:: import_2.png 74 | -------------------------------------------------------------------------------- /docs/import_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/import_1.png -------------------------------------------------------------------------------- /docs/import_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/import_2.png -------------------------------------------------------------------------------- /docs/increase_row_count.rst: -------------------------------------------------------------------------------- 1 | How to show larger number of rows on listview page? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | You have been asked to increase the number of heroes one can see on a single page to 250. (The default is 100). You can do this by:: 5 | 6 | 7 | @admin.register(Hero) 8 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 9 | ... 10 | list_per_page = 250 11 | 12 | You can also set it to a smaller value. If we set it to 1 as :code:`list_per_page = 1` the admin looks like this. 13 | 14 | .. image:: icrease_row_count_to_one.png 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Admin Cookbook 2 | =========================================================== 3 | 4 | .. image:: book-cover.png 5 | 6 | 7 | Django Admin Cookbook - How to do things with Django admin. 8 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 | 10 | This is a book about doing things with Django admin. It takes the form of about forty questions and common tasks with Django admin we answer. 11 | 12 | The chapters are based on a common set of models, which you can read in detail here (:doc:`models`). In short, we have two apps, 13 | :code:`events` and :code:`entities`. The models are 14 | 15 | * Events: :code:`Epic`, :code:`Event`, :code:`EventHero`, :code:`EventVillian` 16 | * Entities: :code:`Category`, :code:`Origin`, :code:`Hero`, :code:`Villain` 17 | 18 | .. toctree:: 19 | :maxdepth: 1 20 | 21 | introduction 22 | 23 | Text and Design 24 | +++++++++++++++++++++ 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | :numbered: 29 | 30 | change_text 31 | plural_text 32 | two_admin 33 | remove_default 34 | logo 35 | override_default_templates 36 | 37 | Calculated fields 38 | +++++++++++++++++++++ 39 | 40 | .. toctree:: 41 | :maxdepth: 1 42 | :numbered: 43 | 44 | calculated_fields 45 | optimize_queries 46 | sorting_calculated_fields 47 | filtering_calculated_fields 48 | boolean_fields 49 | 50 | Bulk and custom actions 51 | ++++++++++++++++++++++++++++++++++++++++++ 52 | 53 | .. toctree:: 54 | :maxdepth: 1 55 | :numbered: 56 | 57 | add_actions 58 | export 59 | remove_delete_selected 60 | action_buttons 61 | import 62 | 63 | Permissions 64 | +++++++++++++++++++++ 65 | 66 | .. toctree:: 67 | :maxdepth: 1 68 | :numbered: 69 | 70 | specific_users 71 | restrict_parts 72 | only_one 73 | remove_add_delete 74 | 75 | Multiple models and inlines 76 | ++++++++++++++++++++++++++++++++++++++++++ 77 | 78 | .. toctree:: 79 | :maxdepth: 1 80 | :numbered: 81 | 82 | edit_multiple_models 83 | one_to_one_inline 84 | nested_inlines 85 | single_admin_multiple_models 86 | 87 | 88 | 89 | Listview Page 90 | ++++++++++++++++++++++++++++++++++++++++++ 91 | 92 | .. toctree:: 93 | :maxdepth: 1 94 | :numbered: 95 | 96 | increase_row_count 97 | disable_pagination 98 | date_based_filtering 99 | many_to_many 100 | 101 | Changeview Page 102 | ++++++++++++++++++++++++++++++++++++++++++ 103 | 104 | .. toctree:: 105 | :maxdepth: 1 106 | :numbered: 107 | 108 | imagefield 109 | current_user 110 | changeview_readonly 111 | uneditable_field 112 | uneditable_existing 113 | filter_fk_dropdown 114 | many_fks 115 | fk_display 116 | custom_button 117 | 118 | 119 | Misc 120 | ++++++++++++++++++++++++++++++++++++++++++ 121 | 122 | .. toctree:: 123 | :maxdepth: 1 124 | :numbered: 125 | 126 | object_url 127 | add_model_twice 128 | override_save 129 | database_view 130 | set_ordering 131 | 132 | 133 | Indices and tables 134 | +++++++++++++++++++++ 135 | .. toctree:: 136 | :maxdepth: 1 137 | 138 | models 139 | 140 | * :ref:`genindex` 141 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============== 3 | 4 | Django Admin Cookbook is a book about doing things with Django admin. It is targeted towards intermediate Django developers, 5 | who have some experience with Django admin, but are looking to expand their knowledge of Django admin and achieve mastery of Django admin. 6 | 7 | It takes the form of question and answers about common tasks you might do with Django admin. All the chapters are based on a common set of models, which you can read in detail here (:doc:`models`). In short, we have two apps, 8 | :code:`events` and :code:`entities`. The models are 9 | 10 | * Events: :code:`Epic`, :code:`Event`, :code:`EventHero`, :code:`EventVillain` 11 | * Entities: :code:`Category`, :code:`Origin`, :code:`Hero`, :code:`Villain` 12 | 13 | 14 | How to use this book 15 | +++++++++++++++++++++++ 16 | 17 | You can read this book either from start to end, or search for the things you need to do and only read those chapters. Each chapter focusses on a single, specific task. 18 | 19 | In either case, you should read the :code:`entities/models.py` and :code:`events/models.py` first. 20 | -------------------------------------------------------------------------------- /docs/logo.rst: -------------------------------------------------------------------------------- 1 | How to add a logo to Django admin? 2 | =========================================================== 3 | 4 | Your higher ups at UMSRA love the admin you have created till now, but marketing wants to put the UMSRA logo on all admin pages. 5 | 6 | You need to override the default templates provided by Django. In your django settings, you code::`TEMPLATES` setting looks like this. :: 7 | 8 | TEMPLATES = [ 9 | { 10 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 11 | 'DIRS': [], 12 | 'APP_DIRS': True, 13 | 'OPTIONS': { 14 | 'context_processors': [ 15 | 'django.template.context_processors.debug', 16 | 'django.template.context_processors.request', 17 | 'django.contrib.auth.context_processors.auth', 18 | 'django.contrib.messages.context_processors.messages', 19 | ], 20 | }, 21 | }, 22 | ] 23 | 24 | This means that Django will look for templates in a directory called :code:`templates` inside each app, but you can override that by setting a value for :code:`TEMPLATES.DIRS`. 25 | 26 | We change the :code:`'DIRS': [],` to :code:`'DIRS': [os.path.join(BASE_DIR, 'templates/')],`, and create the :code:`templates` folder. If your :code:`STATICFILES_DIRS` is empty set it to:: 27 | 28 | STATICFILES_DIRS = [ 29 | os.path.join(BASE_DIR, "static"), 30 | ] 31 | 32 | Now copy the :code:`base_site.html` from the admin app to :code:`templates\admin` folder you just created. Replace thre default text in `branding` block with:: 33 | 34 |

    35 | 36 | 37 | 38 |

    39 | 40 | With the changes your base_site.html will look like this:: 41 | 42 | {% extends "admin/base.html" %} 43 | 44 | {% load staticfiles %} 45 | 46 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 47 | 48 | {% block branding %} 49 |

    50 | 51 | 52 | 53 |

    54 | {% endblock %} 55 | 56 | {% block nav-global %}{% endblock %} 57 | 58 | And your admin will look like this 59 | 60 | .. image:: images/logo_fixed.png 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/many_fks.rst: -------------------------------------------------------------------------------- 1 | How to manage a model with a FK with a large number of objects? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | You can create a large number of categories like this:: 5 | 6 | categories = [Category(**{"name": "cat-{}".format(i)}) for i in range(100000)] 7 | Category.objects.bulk_create(categories) 8 | 9 | 10 | Now as :code:`Category` has more than 100000 objects, when you go to the :code:`Hero` admin, it will have category dropdown with 100000 selections. 11 | This will make the page both slow and the dropdown hard to use. 12 | 13 | You can change how admin handles it by setting the :code:`raw_id_fields`:: 14 | 15 | @admin.register(Hero) 16 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 17 | ... 18 | raw_id_fields = ["category"] 19 | 20 | This change the Hero admin to look like: 21 | 22 | .. image:: raw_id_fields_1.png 23 | 24 | Add the popup looks like this 25 | 26 | .. image:: raw_id_fields_2.png 27 | -------------------------------------------------------------------------------- /docs/many_to_many.rst: -------------------------------------------------------------------------------- 1 | How to show many to many or reverse FK fields on listview page? 2 | ================================================================================ 3 | 4 | For heroes you can track their father using this field:: 5 | 6 | father = models.ForeignKey( 7 | "self", related_name="children", null=True, blank=True, on_delete=models.SET_NULL 8 | ) 9 | 10 | You have been asked to show the childeren of each Hero, on the listview page. Hero objects have the :code:`children` reverse FK attribute, 11 | but you can't add that to the :code`:`list_display`. You need to add an attribute to ModelAdmin and use that in :code:`list_display`. You can do it like this:: 12 | 13 | 14 | @admin.register(Hero) 15 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 16 | ... 17 | 18 | def children_display(self, obj): 19 | return ", ".join([ 20 | child.name for child in obj.children.all() 21 | ]) 22 | children_display.short_description = "Children" 23 | 24 | You will see a column for children like this: 25 | 26 | .. image:: related_fk_display.png 27 | 28 | You can use the same method for M2M relations as well. You should also read :doc:`object_url`. 29 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | Models used in this book 2 | =========================== 3 | 4 | 5 | App entities 6 | ++++++++++++++ 7 | 8 | The models are:: 9 | 10 | 11 | class Category(models.Model): 12 | name = models.CharField(max_length=100) 13 | 14 | class Meta: 15 | verbose_name_plural = "Categories" 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | 21 | class Origin(models.Model): 22 | name = models.CharField(max_length=100) 23 | 24 | def __str__(self): 25 | return self.name 26 | 27 | 28 | class Entity(models.Model): 29 | GENDER_MALE = "Male" 30 | GENDER_FEMALE = "Female" 31 | GENDER_OTHERS = "Others/Unknown" 32 | 33 | name = models.CharField(max_length=100) 34 | alternative_name = models.CharField( 35 | max_length=100, null=True, blank=True 36 | ) 37 | 38 | 39 | category = models.ForeignKey(Category, on_delete=models.CASCADE) 40 | origin = models.ForeignKey(Origin, on_delete=models.CASCADE) 41 | gender = models.CharField( 42 | max_length=100, 43 | choices=( 44 | (GENDER_MALE, GENDER_MALE), 45 | (GENDER_FEMALE, GENDER_FEMALE), 46 | (GENDER_OTHERS, GENDER_OTHERS), 47 | ) 48 | ) 49 | description = models.TextField() 50 | 51 | def __str__(self): 52 | return self.name 53 | 54 | class Meta: 55 | abstract = True 56 | 57 | 58 | class Hero(Entity): 59 | 60 | class Meta: 61 | verbose_name_plural = "Heroes" 62 | 63 | is_immortal = models.BooleanField(default=True) 64 | 65 | benevolence_factor = models.PositiveSmallIntegerField( 66 | help_text="How benevolent this hero is?" 67 | ) 68 | arbitrariness_factor = models.PositiveSmallIntegerField( 69 | help_text="How arbitrary this hero is?" 70 | ) 71 | # relationships 72 | father = models.ForeignKey( 73 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 74 | ) 75 | mother = models.ForeignKey( 76 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 77 | ) 78 | spouse = models.ForeignKey( 79 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 80 | ) 81 | 82 | 83 | class Villain(Entity): 84 | is_immortal = models.BooleanField(default=False) 85 | 86 | malevolence_factor = models.PositiveSmallIntegerField( 87 | help_text="How malevolent this villain is?" 88 | ) 89 | power_factor = models.PositiveSmallIntegerField( 90 | help_text="How powerful this villain is?" 91 | ) 92 | is_unique = models.BooleanField(default=True) 93 | count = models.PositiveSmallIntegerField(default=1) 94 | 95 | 96 | App events 97 | +++++++++++ 98 | 99 | The models are:: 100 | 101 | 102 | 103 | class Epic(models.Model): 104 | name = models.CharField(max_length=255) 105 | participating_heroes = models.ManyToManyField(Hero) 106 | participating_villains = models.ManyToManyField(Villain) 107 | 108 | 109 | class Event(models.Model): 110 | epic = models.ForeignKey(Epic, on_delete=models.CASCADE) 111 | details = models.TextField() 112 | years_ago = models.PositiveIntegerField() 113 | 114 | 115 | class EventHero(models.Model): 116 | event = models.ForeignKey(Event, on_delete=models.CASCADE) 117 | hero = models.ForeignKey(Hero, on_delete=models.CASCADE) 118 | is_primary = models.BooleanField() 119 | 120 | 121 | class EventVillain(models.Model): 122 | event = models.ForeignKey(Event, on_delete=models.CASCADE) 123 | hero = models.ForeignKey(Villain, on_delete=models.CASCADE) 124 | is_primary = models.BooleanField() 125 | -------------------------------------------------------------------------------- /docs/nested_inlines.rst: -------------------------------------------------------------------------------- 1 | How to add nested inlines in Django admin? 2 | ========================================== 3 | 4 | 5 | You have yor models defined like this:: 6 | 7 | class Category(models.Model): 8 | ... 9 | 10 | class Hero(models.Model): 11 | category = models.ForeignKey(Catgeory) 12 | ... 13 | 14 | class HeroAcquaintance(models.Model): 15 | hero = models.OneToOneField(Hero, on_delete=models.CASCADE) 16 | ... 17 | 18 | You want to have one admin page to create :code:`Category`, :code:`Hero` and :code:`HeroAcquaintance` objects. However, Django doesn't support nested inline with Foreign Keys or One To One relations which span more than one levels. You have a few options, 19 | 20 | 21 | You can change the :code:`HeroAcquaintance` model, so that it has a direct FK to :code:`Category`, something like this:: 22 | 23 | class HeroAcquaintance(models.Model): 24 | hero = models.OneToOneField(Hero, on_delete=models.CASCADE) 25 | category = models.ForeignKey(Category) 26 | 27 | def save(self, *args, **kwargs): 28 | self.category = self.hero.category 29 | super().save(*args, **kwargs) 30 | 31 | 32 | Then you can attach :code:`HeroAcquaintanceInline` to :code:`CategoryAdmin`, and get a kind of nested inline. 33 | 34 | Alternatively, there are a few third party Django apps which allow nested inline. A quick Github or DjangoPackages search will find one which suits your needs and tastes. 35 | 36 | -------------------------------------------------------------------------------- /docs/no_filtering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/no_filtering.png -------------------------------------------------------------------------------- /docs/no_sorting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/no_sorting.png -------------------------------------------------------------------------------- /docs/object_url.rst: -------------------------------------------------------------------------------- 1 | How to get Django admin urls for specific objects? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | You have a children column displaying the names of each heroes' children. You have been asked to link each children to the change page. You can do it like this.:: 5 | 6 | @admin.register(Hero) 7 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 8 | ... 9 | 10 | def children_display(self, obj): 11 | display_text = ", ".join([ 12 | "{}".format( 13 | reverse('admin:{}_{}_change'.format(obj._meta.app_label, obj._meta.model_name), 14 | args=(child.pk,)), 15 | child.name) 16 | for child in obj.children.all() 17 | ]) 18 | if display_text: 19 | return mark_safe(display_text) 20 | return "-" 21 | 22 | The :code:`reverse('admin:{}_{}_change'.format(obj._meta.app_label, obj._meta.model_name), args=(child.pk,))`, gives the change url for an object. 23 | 24 | The other options are 25 | 26 | * Delete: :code:`reverse('admin:{}_{}_delete'.format(obj._meta.app_label, obj._meta.model_name), args=(child.pk,))` 27 | * History: :code:`reverse('admin:{}_{}_history'.format(obj._meta.app_label, obj._meta.model_name), args=(child.pk,))` 28 | -------------------------------------------------------------------------------- /docs/one_to_one_inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/one_to_one_inline.png -------------------------------------------------------------------------------- /docs/one_to_one_inline.rst: -------------------------------------------------------------------------------- 1 | How to add One to One relation as admin inline? 2 | ================================================ 3 | 4 | OneToOneFields can be set as inlines in the same way as a FK. However, only one side of the OneToOneField can be set as the inline model. 5 | 6 | You have a :code:`HeroAcquaintance` model which has a One to one relation to hero like this.:: 7 | 8 | 9 | 10 | class HeroAcquaintance(models.Model): 11 | "Non family contacts of a Hero" 12 | hero = models.OneToOneField(Hero, on_delete=models.CASCADE) 13 | .... 14 | 15 | You can add this as inline to Hero like this:: 16 | 17 | class HeroAcquaintanceInline(admin.TabularInline): 18 | model = HeroAcquaintance 19 | 20 | 21 | @admin.register(Hero) 22 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 23 | ... 24 | inlines = [HeroAcquaintanceInline] 25 | 26 | 27 | .. image:: one_to_one_inline.png 28 | -------------------------------------------------------------------------------- /docs/only_one.rst: -------------------------------------------------------------------------------- 1 | How to allow creating only one object from the admin? 2 | ===================================================== 3 | 4 | The UMSRA admin has asked you to limit the number of categories to only one. They want every entity to be of the same category. 5 | 6 | You can do this by:: 7 | 8 | MAX_OBJECTS = 1 9 | 10 | def has_add_permission(self, request): 11 | if self.model.objects.count() >= MAX_OBJECTS: 12 | return False 13 | return super().has_add_permission(request) 14 | 15 | This would hide the add buton as soon as one object is created. You can set :code:`MAX_OBJECTS` to any value to ensure that 16 | larger number of objects can't be ceated than :code:`MAX_OBJECTS`. 17 | -------------------------------------------------------------------------------- /docs/optimize_queries.rst: -------------------------------------------------------------------------------- 1 | How to optimize queries in Django admin? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | If you have a lot of calculated fields in your admin, you can be running multiple queries per object leading to your admin can becoming quite slow. 5 | To fix this you can override the :code:`get_queryset` method on model admin to annotate the calculated fields. 6 | 7 | Lets take the example of this :code:`ModelAdmin` we have for :code:`Origin`:: 8 | 9 | @admin.register(Origin) 10 | class OriginAdmin(admin.ModelAdmin): 11 | list_display = ("name", "hero_count", "villain_count") 12 | 13 | def hero_count(self, obj): 14 | return obj.hero_set.count() 15 | 16 | def villain_count(self, obj): 17 | return obj.villain_set.count() 18 | 19 | 20 | This adds two extra queries per row in your listview page. To fix this you can override the :code:`get_queryset` to annotate the counted fields, 21 | and then use the annotated fields in your ModelAdmin methods. 22 | 23 | With the changes, your ModelAdmin field looks like this:: 24 | 25 | 26 | @admin.register(Origin) 27 | class OriginAdmin(admin.ModelAdmin): 28 | list_display = ("name", "hero_count", "villain_count") 29 | 30 | def get_queryset(self, request): 31 | queryset = super().get_queryset(request) 32 | queryset = queryset.annotate( 33 | _hero_count=Count("hero", distinct=True), 34 | _villain_count=Count("villain", distinct=True), 35 | ) 36 | return queryset 37 | 38 | def hero_count(self, obj): 39 | return obj._hero_count 40 | 41 | def villain_count(self, obj): 42 | return obj._villain_count 43 | 44 | There are no per object extra queries. Your admin continues to look like it did before the :code:`annotate` call. 45 | 46 | .. image:: calculated_field.png 47 | 48 | -------------------------------------------------------------------------------- /docs/ordering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/ordering.png -------------------------------------------------------------------------------- /docs/override_default_templates.rst: -------------------------------------------------------------------------------- 1 | How to override Django admin templates? 2 | =========================================================== 3 | 4 | https://docs.djangoproject.com/en/dev/ref/contrib/admin/#overriding-admin-templates 5 | -------------------------------------------------------------------------------- /docs/override_save.rst: -------------------------------------------------------------------------------- 1 | How to override save behaviour for Django admin? 2 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | :code:`ModelAdmin` has a :code:`save_model` method, which is used for creating and updating model objects. 5 | By overriding this, you can customize the save behaviour for admin. 6 | 7 | 8 | The :code:`Hero` model has the following field.:: 9 | 10 | added_by = models.ForeignKey(settings.AUTH_USER_MODEL, 11 | null=True, blank=True, on_delete=models.SET_NULL) 12 | 13 | 14 | If you want to always save the current user whenever the Hero is updated, you can do.:: 15 | 16 | def save_model(self, request, obj, form, change): 17 | obj.added_by = request.user 18 | super().save_model(request, obj, form, change) 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/plural_text.rst: -------------------------------------------------------------------------------- 1 | How to set the plural text for a model? 2 | =========================================================== 3 | 4 | By default admin will show the name of your model appended with an "s", aka the plural form of your model. It looks like this 5 | 6 | .. image:: images/plural.png 7 | 8 | You have been asked to set the correct plural spellings: `Categories` and `Heroes` 9 | 10 | You can do this by setting the :code:`verbose_name_plural` in your models. Change that in your models.py.:: 11 | 12 | 13 | class Category(models.Model): 14 | ... 15 | 16 | class Meta: 17 | verbose_name_plural = "Categories" 18 | 19 | 20 | class Hero(Entity): 21 | ... 22 | 23 | class Meta: 24 | verbose_name_plural = "Heroes" 25 | 26 | 27 | With the changes your Admin will look like this. 28 | 29 | 30 | .. image:: images/plural_fixed.png 31 | -------------------------------------------------------------------------------- /docs/questions.txt: -------------------------------------------------------------------------------- 1 | ### Text and Design 2 | 3 | * How to change 'Django administration' text? 4 | * How to set the plural text for a model? 5 | * How to add a logo to Django admin? 6 | * How to create two independent admin sites? 7 | * How to remove default apps from Django admin? 8 | * How to override Django admin templates? 9 | 10 | 11 | ### Calculated fields 12 | 13 | * How to show calculated fields on listview page? 14 | * How to show many to many fields on listview page? 15 | * How to enable sorting on calculated fields? 16 | * How to enable filtering on calculated fields? 17 | 18 | ### Misc 19 | 20 | * How to get Django admin urls for specific objects? 21 | * How to add a model twice to Django admin? 22 | * How to override save behaviour for Django admin? 23 | * How to add a database view to Django admin? 24 | * How to optimize queries in Django admin? 25 | * How to set ordering of Apps and models in Django admin dashboard. 26 | 27 | ### Permissions 28 | 29 | * How to restrict Django admin to specific users? 30 | * How to restrict access to parts of Django admin? 31 | * How to allow creating only one object from the admin? 32 | * How to remove the 'Add'/'Delete' button for a model? 33 | 34 | 35 | ### Multiple models and inlines 36 | 37 | * How to edit mutiple models from one Django admin? 38 | * How to add One to One relation as admin inline? 39 | * How to add nested inlines in Django admin? 40 | * How to create a single Django admin from two different models? 41 | 42 | 43 | ### Bulk and custom actions 44 | 45 | * How to export CSV from Django admin? 46 | * How to import CSV using Django admin? 47 | * How to remove the delete selected action in Django admin? 48 | * How to add additional actions in Django admin? 49 | * How to add Custom Action Buttons (Not actions) to Django Admin list page? 50 | 51 | ### Listview Page 52 | 53 | * How to show larger number of rows on listview page? 54 | * How to disable django admin pagination? 55 | * How to add date base filtering in Django admin? 56 | * How to show “on” or “off” icons for calculated boolean fields? 57 | 58 | ### Changeview Page 59 | 60 | * How to show image from Imagefield in Django admin. 61 | * How to associate model with current user while saving? 62 | * How to mark a field as readonly in admin? 63 | * How to show an uneditable field in admin? 64 | * How to make a field editable while creating, but read only in existing objects? 65 | * How to filter FK dropdown values in django admin? 66 | * How to manage a model with a FK with a large number of objects? 67 | * How to change ForeignKey display text in dropdowns? 68 | * How to add a custom button to Django change view page? 69 | 70 | ### Misc 71 | 72 | * How to get Django admin urls for specific objects? 73 | * How to add a model twice to Django admin? 74 | * How to override save behaviour for Django admin? 75 | * How to add a database view to Django admin? 76 | * How to optimize queries in Django admin? 77 | * How to set ordering of Apps and models in Django admin dashboard. 78 | 79 | 80 | formfield_for_foreignkey 81 | models.ForeignKey(ForeignStufg, verbose_name='your text') 82 | -------------------------------------------------------------------------------- /docs/raw_id_fields_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/raw_id_fields_1.png -------------------------------------------------------------------------------- /docs/raw_id_fields_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/raw_id_fields_2.png -------------------------------------------------------------------------------- /docs/related_fk_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/related_fk_display.png -------------------------------------------------------------------------------- /docs/remove_add_delete.rst: -------------------------------------------------------------------------------- 1 | How to remove the 'Add'/'Delete' button for a model? 2 | ==================================================== 3 | 4 | 5 | The UMSRA management has added all the Category and Origin objects and wants to disable any further addition and deletion. 6 | They have asked you to disable 'Add' and 'Delete' buttons. You can do this by overriding the 7 | :code:`has_add_permission` and :code:`has_delete_permission` in the Django admin.:: 8 | 9 | 10 | 11 | def has_add_permission(self, request): 12 | return False 13 | 14 | def has_delete_permission(self, request, obj=None): 15 | return False 16 | 17 | 18 | With these changes, the admin looks like this 19 | 20 | .. image:: remove_add_perms.png 21 | 22 | Note the removed `Add` buttons. The add and delete buttons also get removed from the detail pages. You can also read :doc:`remove_delete_selected`. 23 | 24 | -------------------------------------------------------------------------------- /docs/remove_add_perms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/remove_add_perms.png -------------------------------------------------------------------------------- /docs/remove_default.rst: -------------------------------------------------------------------------------- 1 | How to remove default apps from Django admin? 2 | =========================================================== 3 | 4 | Django will include :code:`django.contrib.auth` in :code:`INSTALLED_APPS`, 5 | which means `User` and `Groups` models are included in admin automatically. 6 | 7 | .. image:: images/remove_default_apps.png 8 | 9 | 10 | If you want to remove it, you will have to unregister them. :: 11 | 12 | from django.contrib.auth.models import User, Group 13 | 14 | 15 | admin.site.unregister(User) 16 | admin.site.unregister(Group) 17 | 18 | 19 | After making these changes, your admin should look like this. 20 | 21 | 22 | .. image:: images/remove_default_apps_fixed.png 23 | -------------------------------------------------------------------------------- /docs/remove_delete_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/remove_delete_selected.png -------------------------------------------------------------------------------- /docs/remove_delete_selected.rst: -------------------------------------------------------------------------------- 1 | How to remove the delete selected action in Django admin? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | By default Django adds a `Delete Selected` action to the listview page. You have been asked to remove 5 | the action from the :code:`Hero` admin. 6 | 7 | The method :code:`ModelAdmin.get_actions` returns the actions shown. By overriding this method, to remove :code:`delete_selected` 8 | We can remove it form the dropdown. Your code looks like this with the changes.:: 9 | 10 | def get_actions(self, request): 11 | actions = super().get_actions(request) 12 | if 'delete_selected' in actions: 13 | del actions['delete_selected'] 14 | return actions 15 | 16 | And your admin looks like this 17 | 18 | .. image:: export_selected.png 19 | 20 | You should also read :doc:`remove_add_delete`. 21 | -------------------------------------------------------------------------------- /docs/restrict_parts.rst: -------------------------------------------------------------------------------- 1 | How to restrict access to parts of Django admin? 2 | ================================================= 3 | 4 | You can enable and restrict access to specific parts of Django admin using the permission system. 5 | When a model is added, by default, Django creates three permissions. :code:`add, change and delete` 6 | 7 | 8 | Admin uses these permissions to decide access for users. For a user with :code:`is_superuser=False`, and no permissions, the admin looks like this 9 | 10 | .. image:: access_no_perms.png 11 | 12 | If you add a permission :code:`user.user_permissions.add(Permission.objects.get(codename="add_hero"))`, the admin starts looking like this 13 | 14 | .. image:: access_one_perm.png 15 | 16 | You can add more complex logic to restrict access by changing these methods:: 17 | 18 | def has_add_permission(self, request): 19 | ... 20 | 21 | def has_change_permission(self, request, obj=None): 22 | ... 23 | 24 | def has_delete_permission(self, request, obj=None): 25 | ... 26 | 27 | def has_module_permission(self, request): 28 | ... 29 | -------------------------------------------------------------------------------- /docs/set_ordering.rst: -------------------------------------------------------------------------------- 1 | How to set ordering of Apps and models in Django admin dashboard. 2 | ================================================================= 3 | 4 | Django, by default, orders the models in admin alphabetically. So the order of models in :code:`Event` admin is Epic, EventHero, EventVillain, Event 5 | 6 | Instead you want the order to be 7 | 8 | * EventHero, EventVillain, Epic then event. 9 | 10 | The template used to render the admin index page is :code:`admin/index.html` and the view function is 11 | :code:`ModelAdmin.index`. :: 12 | 13 | def index(self, request, extra_context=None): 14 | """ 15 | Display the main admin index page, which lists all of the installed 16 | apps that have been registered in this site. 17 | """ 18 | app_list = self.get_app_list(request) 19 | context = { 20 | **self.each_context(request), 21 | 'title': self.index_title, 22 | 'app_list': app_list, 23 | **(extra_context or {}), 24 | } 25 | 26 | request.current_app = self.name 27 | 28 | return TemplateResponse(request, self.index_template or 29 | 'admin/index.html', context) 30 | 31 | 32 | The method :code:`get_app_list`, set the order of the models.:: 33 | 34 | def get_app_list(self, request): 35 | """ 36 | Return a sorted list of all the installed apps that have been 37 | registered in this site. 38 | """ 39 | app_dict = self._build_app_dict(request) 40 | 41 | # Sort the apps alphabetically. 42 | app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower()) 43 | 44 | # Sort the models alphabetically within each app. 45 | for app in app_list: 46 | app['models'].sort(key=lambda x: x['name']) 47 | 48 | return app_list 49 | 50 | So to set the order we override :code:`get_app_list` as:: 51 | 52 | 53 | class EventAdminSite(AdminSite): 54 | def get_app_list(self, request): 55 | """ 56 | Return a sorted list of all the installed apps that have been 57 | registered in this site. 58 | """ 59 | ordering = { 60 | "Event heros": 1, 61 | "Event villains": 2, 62 | "Epics": 3, 63 | "Events": 4 64 | } 65 | app_dict = self._build_app_dict(request) 66 | # a.sort(key=lambda x: b.index(x[0])) 67 | # Sort the apps alphabetically. 68 | app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower()) 69 | 70 | # Sort the models alphabetically within each app. 71 | for app in app_list: 72 | app['models'].sort(key=lambda x: ordering[x['name']]) 73 | 74 | return app_list 75 | 76 | The code :code:`app['models'].sort(key=lambda x: ordering[x['name']])` sets the fixed ordering. Your app now looks like this. 77 | 78 | .. image:: ordering.png 79 | -------------------------------------------------------------------------------- /docs/single_admin_multiple_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/single_admin_multiple_model.png -------------------------------------------------------------------------------- /docs/single_admin_multiple_models.rst: -------------------------------------------------------------------------------- 1 | How to create a single Django admin from two different models? 2 | ================================================================= 3 | 4 | 5 | :code:`Hero` has a FK to :code:`Category`, so you can select a category from Hero admin. 6 | If you want to also be able to create :code:`Category` objects from Hero admin, you can change the form for Hero admin, and customise the 7 | :code:`save_model` behaviour.:: 8 | 9 | class HeroForm(forms.ModelForm): 10 | category_name = forms.CharField() 11 | 12 | class Meta: 13 | model = Hero 14 | exclude = ["category"] 15 | 16 | 17 | @admin.register(Hero) 18 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 19 | form = HeroForm 20 | .... 21 | 22 | def save_model(self, request, obj, form, change): 23 | category_name = form.cleaned_data["category_name"] 24 | category, _ = Category.objects.get_or_create(name=category_name) 25 | obj.category = category 26 | super().save_model(request, obj, form, change) 27 | 28 | 29 | With this change, your admin looks like below and has allows creating or updating category from the Hero admin. 30 | 31 | .. image:: single_admin_multiple_model.png 32 | -------------------------------------------------------------------------------- /docs/sorting_calculated_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/sorting_calculated_field.png -------------------------------------------------------------------------------- /docs/sorting_calculated_fields.rst: -------------------------------------------------------------------------------- 1 | How to enable sorting on calculated fields? 2 | =========================================================== 3 | 4 | Django adds sorting capabilities on fields which are attributes on the models. 5 | When you add a calculated field Django doesn't know how to do a :code:`order_by`, so it doesn't add sorting capability on that field. 6 | 7 | If you want to add sorting on a calculated field, you have to tell Django what to pass to :code:`order_by`. You can do this by setting the 8 | :code:`admin_order_field` attribute on the calculated field method. 9 | 10 | You start from the admin you wrote in the previous chapter (:doc:`optimize_queries`).:: 11 | 12 | hero_count.admin_order_field = '_hero_count' 13 | villain_count.admin_order_field = '_villain_count' 14 | 15 | 16 | With these changes your admin becomes:: 17 | 18 | @admin.register(Origin) 19 | class OriginAdmin(admin.ModelAdmin): 20 | list_display = ("name", "hero_count", "villain_count") 21 | 22 | def get_queryset(self, request): 23 | queryset = super().get_queryset(request) 24 | queryset = queryset.annotate( 25 | _hero_count=Count("hero", distinct=True), 26 | _villain_count=Count("villain", distinct=True), 27 | ) 28 | return queryset 29 | 30 | def hero_count(self, obj): 31 | return obj._hero_count 32 | 33 | def villain_count(self, obj): 34 | return obj._villain_count 35 | 36 | hero_count.admin_order_field = '_hero_count' 37 | villain_count.admin_order_field = '_villain_count' 38 | 39 | 40 | Here is the admin sorted on :code:`hero_count` 41 | 42 | .. image:: sorting_calculated_field.png 43 | -------------------------------------------------------------------------------- /docs/specific_users.rst: -------------------------------------------------------------------------------- 1 | How to restrict Django admin to specific users? 2 | ================================================ 3 | 4 | Django admin allows access to users marked as :code:`is_staff=True`. 5 | To disable a user from being able to access the admin, you should set :code:`is_staff=False`. 6 | 7 | This holds true even if the user is a superuser. :code:`is_superuser=True`. If a non-staff tries to access the admin, they see a message like this. 8 | 9 | .. image:: access_no_is_staff.png 10 | -------------------------------------------------------------------------------- /docs/two_admin.rst: -------------------------------------------------------------------------------- 1 | How to create two independent admin sites? 2 | =========================================================== 3 | 4 | The usual way to create admin pages is to put all models in a single admin. However it is possible to have multiple admin sites in a single Django app. 5 | 6 | Right now our :code:`entity` and :code:`event` models are in same place. UMSRA has two distinct group researching `Events` and `Entities`, and so wants to split the admins. 7 | 8 | 9 | We will keep the default admin for `entities` and create a new subclass of :code:`AdminSite` for `events`. 10 | 11 | In our :code:`events/admin.py` we do:: 12 | 13 | from django.contrib.admin import AdminSite 14 | class EventAdminSite(AdminSite): 15 | site_header = "UMSRA Events Admin" 16 | site_title = "UMSRA Events Admin Portal" 17 | index_title = "Welcome to UMSRA Researcher Events Portal" 18 | 19 | event_admin_site = EventAdminSite(name='event_admin') 20 | 21 | 22 | event_admin_site.register(Epic) 23 | event_admin_site.register(Event) 24 | event_admin_site.register(EventHero) 25 | event_admin_site.register(EventVillain) 26 | 27 | And change the :code:`urls.py` to :: 28 | 29 | from events.admin import event_admin_site 30 | 31 | 32 | urlpatterns = [ 33 | path('entity-admin/', admin.site.urls), 34 | path('event-admin/', event_admin_site.urls), 35 | ] 36 | 37 | 38 | This separates the admin. Both admins are available at their respective urls, :code:`/entity-admin/` and :code:`event-admin/`. 39 | -------------------------------------------------------------------------------- /docs/uneditable_existing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/docs/uneditable_existing.png -------------------------------------------------------------------------------- /docs/uneditable_existing.rst: -------------------------------------------------------------------------------- 1 | How to make a field editable while creating, but read only in existing objects? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | You need to make the :code:`name` and :code:`category` read only once a :code:`Hero` is created. However duing the first write the fields needs to be editable. 5 | 6 | You can do this by overriding :code:`get_readonly_fields` method, like this:: 7 | 8 | def get_readonly_fields(self, request, obj=None): 9 | if obj: 10 | return ["name", "category"] 11 | else: 12 | return [] 13 | 14 | :code:`obj` is :code:`None` during the object creation, but set to the object being edited during an edit. 15 | -------------------------------------------------------------------------------- /docs/uneditable_field.rst: -------------------------------------------------------------------------------- 1 | How to show an uneditable field in admin? 2 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | 4 | If you have a field with :code:`editable=False` in your model, that field, by default, is hidden in the change page. This also happens with any field marked as :code:`auto_now` or :code:`auto_now_add`, because that sets the :code:`editable=False` on these fields. 5 | 6 | If you want these fields to show up on the change page, you can add them to :code:`readonly_fields`.:: 7 | 8 | @admin.register(Villain) 9 | class VillainAdmin(admin.ModelAdmin, ExportCsvMixin): 10 | ... 11 | readonly_fields = ["added_on"] 12 | 13 | With this change the Villain admin looks like this: 14 | 15 | .. image:: uneditable_existing.png 16 | -------------------------------------------------------------------------------- /heroes_and_monsters/.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | *.pyc 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /heroes_and_monsters/README.md: -------------------------------------------------------------------------------- 1 | ## Running the project locally 2 | 3 | You should follow these steps to run the project locally. 4 | 5 | ### Create a virtual environment 6 | 7 | You should create a virtual environment, preferably with Python 3.6 8 | 9 | If you use **virtualenvwrapper**, you could do: 10 | 11 | mkvirtualenv --python=/usr/local/bin/python3 django-admin-cookbook 12 | 13 | ### Install the requirements 14 | 15 | pip install -r requirements.txt 16 | 17 | ### Run migrations 18 | 19 | python manage.py migrate 20 | 21 | ### Run server and access admin 22 | 23 | python manage.py runserver 24 | 25 | You can access admins at: 26 | 27 | http://localhost:8000/entity-admin 28 | 29 | http://localhost:8000/event-admin 30 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/heroes_and_monsters/entities/__init__.py -------------------------------------------------------------------------------- /heroes_and_monsters/entities/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db.models import Count 3 | from django import forms 4 | from django.shortcuts import render, redirect 5 | 6 | from .models import Hero, Villain, Category, Origin, HeroProxy, AllEntity, HeroAcquaintance 7 | 8 | import csv 9 | import sys 10 | from django.http import HttpResponse, HttpResponseRedirect 11 | from django.utils.safestring import mark_safe 12 | from django.urls import path, reverse 13 | 14 | 15 | class IsVeryBenevolentFilter(admin.SimpleListFilter): 16 | title = 'is_very_benevolent' 17 | parameter_name = 'is_very_benevolent' 18 | 19 | def lookups(self, request, model_admin): 20 | return ( 21 | ('Yes', 'Yes'), 22 | ('No', 'No'), 23 | ) 24 | 25 | def queryset(self, request, queryset): 26 | value = self.value() 27 | if value == 'Yes': 28 | return queryset.filter(benevolence_factor__gt=75) 29 | elif value == 'No': 30 | return queryset.exclude(benevolence_factor__gt=75) 31 | return queryset 32 | 33 | 34 | class ExportCsvMixin: 35 | 36 | def export_as_csv(self, request, queryset): 37 | 38 | meta = self.model._meta 39 | field_names = [field.name for field in meta.fields] 40 | 41 | response = HttpResponse(content_type='text/csv') 42 | response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) 43 | writer = csv.writer(response) 44 | 45 | writer.writerow(field_names) 46 | for obj in queryset: 47 | row = writer.writerow([getattr(obj, field) for field in field_names]) 48 | 49 | return response 50 | 51 | export_as_csv.short_description = "Export Selected" 52 | 53 | 54 | class ImportCsvMixin: 55 | 56 | pass 57 | 58 | 59 | class HeroAcquaintanceInline(admin.TabularInline): 60 | model = HeroAcquaintance 61 | 62 | class HeroForm(forms.ModelForm): 63 | category_name = forms.CharField() 64 | 65 | class Meta: 66 | model = Hero 67 | exclude = ["category"] 68 | 69 | class CategoryChoiceField(forms.ModelChoiceField): 70 | def label_from_instance(self, obj): 71 | return "Category: {}".format(obj.name) 72 | 73 | 74 | class CsvImportForm(forms.Form): 75 | csv_file = forms.FileField() 76 | 77 | 78 | @admin.register(Hero) 79 | class HeroAdmin(admin.ModelAdmin, ExportCsvMixin): 80 | # form = HeroForm 81 | 82 | list_display = ("name", "is_immortal", "category", "origin", "is_very_benevolent", "children_display") 83 | list_filter = ("is_immortal", "category", "origin", IsVeryBenevolentFilter) 84 | actions = ["mark_immortal"] 85 | date_hierarchy = 'added_on' 86 | inlines = [HeroAcquaintanceInline] 87 | raw_id_fields = ["category"] 88 | 89 | exclude = ['added_by',] 90 | 91 | # fields = ["headshot_image"] 92 | 93 | readonly_fields = ["headshot_image"] 94 | 95 | change_list_template = "entities/heroes_changelist.html" 96 | 97 | def headshot_image(self, obj): 98 | return mark_safe(''.format( 99 | url = obj.headshot.url, 100 | width=obj.headshot.width, 101 | height=obj.headshot.height, 102 | ) 103 | ) 104 | 105 | def children_display(self, obj): 106 | display_text = ", ".join([ 107 | "{}".format( 108 | reverse('admin:{}_{}_change'.format(obj._meta.app_label, obj._meta.model_name), 109 | args=(child.pk,)), 110 | child.name) 111 | for child in obj.children.all() 112 | ]) 113 | if display_text: 114 | return mark_safe(display_text) 115 | return "-" 116 | 117 | children_display.short_description = "Children" 118 | 119 | # def get_readonly_fields(self, request, obj=None): 120 | # if obj: 121 | # return ["name", "category"] 122 | # else: 123 | # return [] 124 | 125 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 126 | if db_field.name == 'category': 127 | return CategoryChoiceField(queryset=Category.objects.all()) 128 | return super().formfield_for_foreignkey(db_field, request, **kwargs) 129 | 130 | 131 | def import_csv(self, request): 132 | if request.method == "POST": 133 | csv_file = request.FILES["csv_file"] 134 | reader = csv.reader(csv_file) 135 | # Create Hero objects from passed in data 136 | # ... 137 | self.message_user(request, "Your csv file has been imported") 138 | return redirect("..") 139 | form = CsvImportForm() 140 | payload = {"form": form} 141 | return render( 142 | request, "admin/csv_form.html", payload 143 | ) 144 | 145 | def get_urls(self): 146 | urls = super().get_urls() 147 | my_urls = [ 148 | path('immortal/', self.set_immortal), 149 | path('mortal/', self.set_mortal), 150 | path('import-csv/', self.import_csv), 151 | ] 152 | return my_urls + urls 153 | 154 | def set_immortal(self, request): 155 | self.model.objects.all().update(is_immortal=True) 156 | self.message_user(request, "All heroes are now immortal") 157 | return HttpResponseRedirect("../") 158 | 159 | def set_mortal(self, request): 160 | self.model.objects.all().update(is_immortal=False) 161 | self.message_user(request, "All heroes are now mortal") 162 | return HttpResponseRedirect("../") 163 | 164 | def save_model(self, request, obj, form, change): 165 | category_name = form.cleaned_data["category_name"] 166 | if not obj.pk: 167 | # Only set added_by during the first save. 168 | obj.added_by = request.user 169 | category, _ = Category.objects.get_or_create(name=category_name) 170 | obj.category = category 171 | super().save_model(request, obj, form, change) 172 | 173 | 174 | def mark_immortal(self, request, queryset): 175 | queryset.update(is_immortal=True) 176 | 177 | def get_actions(self, request): 178 | actions = super().get_actions(request) 179 | if 'delete_selected' in actions: 180 | del actions['delete_selected'] 181 | return actions 182 | 183 | def is_very_benevolent(self, obj): 184 | return obj.benevolence_factor > 75 185 | 186 | 187 | # def has_add_permission(self, request): 188 | # return has_hero_access(request.user) 189 | 190 | # def has_change_permission(self, request, obj=None): 191 | # return has_hero_access(request.user) 192 | 193 | # def has_delete_permission(self, request, obj=None): 194 | # return has_hero_access(request.user) 195 | 196 | # def has_module_permission(self, request): 197 | # return has_hero_access(request.user) 198 | 199 | 200 | is_very_benevolent.boolean = True 201 | 202 | 203 | @admin.register(HeroProxy) 204 | class HeroProxyAdmin(admin.ModelAdmin): 205 | list_display = ("name", "is_immortal", "category", "origin",) 206 | readonly_fields = ("name", "is_immortal", "category", "origin",) 207 | 208 | 209 | 210 | @admin.register(Villain) 211 | class VillainAdmin(admin.ModelAdmin, ExportCsvMixin): 212 | list_display = ("name", "category", "origin") 213 | actions = ["export_as_csv"] 214 | 215 | readonly_fields = ["added_on"] 216 | change_form_template = "entities/villain_changeform.html" 217 | 218 | 219 | def response_change(self, request, obj): 220 | if "_make-unique" in request.POST: 221 | matching_names_except_this = self.get_queryset(request).filter(name=obj.name).exclude(pk=obj.id) 222 | matching_names_except_this.delete() 223 | obj.is_umique = True 224 | obj.save() 225 | self.message_user(request, "This villain is now unique") 226 | return HttpResponseRedirect(".") 227 | super().response_change() 228 | 229 | 230 | class VillainInline(admin.StackedInline): 231 | model = Villain 232 | 233 | @admin.register(Category) 234 | class CategoryAdmin(admin.ModelAdmin): 235 | list_display = ("name",) 236 | 237 | inlines = [VillainInline] 238 | 239 | def has_add_permission(self, request): 240 | return False 241 | 242 | def has_delete_permission(self, request, obj=None): 243 | return False 244 | 245 | 246 | @admin.register(Origin) 247 | class OriginAdmin(admin.ModelAdmin): 248 | list_display = ("name", "hero_count", "villain_count") 249 | 250 | def has_add_permission(self, request): 251 | return False 252 | 253 | def has_delete_permission(self, request, obj=None): 254 | return False 255 | 256 | def get_queryset(self, request): 257 | queryset = super().get_queryset(request) 258 | queryset = queryset.annotate( 259 | _hero_count=Count("hero", distinct=True), 260 | _villain_count=Count("villain", distinct=True), 261 | ) 262 | return queryset 263 | 264 | def hero_count(self, obj): 265 | return obj._hero_count 266 | 267 | def villain_count(self, obj): 268 | return obj._villain_count 269 | 270 | hero_count.admin_order_field = '_hero_count' 271 | villain_count.admin_order_field = '_villain_count' 272 | 273 | 274 | @admin.register(AllEntity) 275 | class AllEntiryAdmin(admin.ModelAdmin): 276 | list_display = ("id", "name") 277 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EntitiesConfig(AppConfig): 5 | name = 'entities' 6 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-31 11:13 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Category', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100)), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='Hero', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('name', models.CharField(max_length=100)), 27 | ('alternative_name', models.CharField(blank=True, max_length=100, null=True)), 28 | ('gender', models.CharField(choices=[('Male', 'Male'), ('Female', 'Female'), ('Others/Unknown', 'Others/Unknown')], max_length=100)), 29 | ('description', models.TextField()), 30 | ('is_immortal', models.BooleanField(default=True)), 31 | ('benevolence_factor', models.PositiveSmallIntegerField(help_text='How benevolent this hero is?')), 32 | ('arbitrariness_factor', models.PositiveSmallIntegerField(help_text='How arbitrary this hero is?')), 33 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.Category')), 34 | ('father', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='entities.Hero')), 35 | ('mother', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='entities.Hero')), 36 | ], 37 | options={ 38 | 'abstract': False, 39 | }, 40 | ), 41 | migrations.CreateModel( 42 | name='Origin', 43 | fields=[ 44 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 45 | ('name', models.CharField(max_length=100)), 46 | ], 47 | ), 48 | migrations.CreateModel( 49 | name='Villain', 50 | fields=[ 51 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('name', models.CharField(max_length=100)), 53 | ('alternative_name', models.CharField(blank=True, max_length=100, null=True)), 54 | ('gender', models.CharField(choices=[('Male', 'Male'), ('Female', 'Female'), ('Others/Unknown', 'Others/Unknown')], max_length=100)), 55 | ('description', models.TextField()), 56 | ('is_immortal', models.BooleanField(default=False)), 57 | ('malevolence_factor', models.PositiveSmallIntegerField(help_text='How malevolent this villain is?')), 58 | ('power_factor', models.PositiveSmallIntegerField(help_text='How powerful this villain is?')), 59 | ('is_unique', models.BooleanField(default=True)), 60 | ('count', models.PositiveSmallIntegerField(default=1)), 61 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.Category')), 62 | ('origin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.Origin')), 63 | ], 64 | options={ 65 | 'abstract': False, 66 | }, 67 | ), 68 | migrations.AddField( 69 | model_name='hero', 70 | name='origin', 71 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.Origin'), 72 | ), 73 | migrations.AddField( 74 | model_name='hero', 75 | name='spouse', 76 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='entities.Hero'), 77 | ), 78 | ] 79 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/migrations/0002_auto_20180221_1830.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-02-21 18:30 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('entities', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='AllEntity', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=100)), 21 | ], 22 | options={ 23 | 'db_table': 'entities_entity', 24 | 'managed': False, 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='HeroAcquaintance', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='HeroProxy', 35 | fields=[ 36 | ], 37 | options={ 38 | 'proxy': True, 39 | 'indexes': [], 40 | }, 41 | bases=('entities.hero',), 42 | ), 43 | migrations.AlterModelOptions( 44 | name='category', 45 | options={'verbose_name_plural': 'Categories'}, 46 | ), 47 | migrations.AlterModelOptions( 48 | name='hero', 49 | options={'verbose_name_plural': 'Heroes'}, 50 | ), 51 | migrations.AddField( 52 | model_name='hero', 53 | name='added_by', 54 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 55 | ), 56 | migrations.AddField( 57 | model_name='hero', 58 | name='added_on', 59 | field=models.DateField(auto_now=True), 60 | ), 61 | migrations.AddField( 62 | model_name='hero', 63 | name='headshot', 64 | field=models.ImageField(blank=True, null=True, upload_to='hero_headshots/'), 65 | ), 66 | migrations.AddField( 67 | model_name='villain', 68 | name='added_by', 69 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 70 | ), 71 | migrations.AddField( 72 | model_name='villain', 73 | name='added_on', 74 | field=models.DateField(auto_now=True), 75 | ), 76 | migrations.AlterField( 77 | model_name='hero', 78 | name='father', 79 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='entities.Hero'), 80 | ), 81 | migrations.AddField( 82 | model_name='heroacquaintance', 83 | name='detractors', 84 | field=models.ManyToManyField(related_name='_heroacquaintance_detractors_+', to='entities.Hero'), 85 | ), 86 | migrations.AddField( 87 | model_name='heroacquaintance', 88 | name='friends', 89 | field=models.ManyToManyField(related_name='_heroacquaintance_friends_+', to='entities.Hero'), 90 | ), 91 | migrations.AddField( 92 | model_name='heroacquaintance', 93 | name='hero', 94 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='entities.Hero'), 95 | ), 96 | migrations.AddField( 97 | model_name='heroacquaintance', 98 | name='main_anatagonists', 99 | field=models.ManyToManyField(related_name='_heroacquaintance_main_anatagonists_+', to='entities.Villain'), 100 | ), 101 | ] 102 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/heroes_and_monsters/entities/migrations/__init__.py -------------------------------------------------------------------------------- /heroes_and_monsters/entities/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django.conf import settings 4 | 5 | 6 | class Category(models.Model): 7 | name = models.CharField(max_length=100) 8 | 9 | class Meta: 10 | verbose_name_plural = "Categories" 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | 16 | class Origin(models.Model): 17 | name = models.CharField(max_length=100) 18 | 19 | def __str__(self): 20 | return self.name 21 | 22 | 23 | class Entity(models.Model): 24 | GENDER_MALE = "Male" 25 | GENDER_FEMALE = "Female" 26 | GENDER_OTHERS = "Others/Unknown" 27 | 28 | name = models.CharField(max_length=100) 29 | alternative_name = models.CharField( 30 | max_length=100, null=True, blank=True 31 | ) 32 | 33 | 34 | category = models.ForeignKey(Category, on_delete=models.CASCADE) 35 | origin = models.ForeignKey(Origin, on_delete=models.CASCADE) 36 | gender = models.CharField( 37 | max_length=100, 38 | choices=( 39 | (GENDER_MALE, GENDER_MALE), 40 | (GENDER_FEMALE, GENDER_FEMALE), 41 | (GENDER_OTHERS, GENDER_OTHERS), 42 | ) 43 | ) 44 | description = models.TextField() 45 | 46 | added_by = models.ForeignKey(settings.AUTH_USER_MODEL, 47 | null=True, blank=True, on_delete=models.SET_NULL) 48 | added_on = models.DateField(auto_now=True) 49 | 50 | def __str__(self): 51 | return self.name 52 | 53 | class Meta: 54 | abstract = True 55 | 56 | 57 | class Hero(Entity): 58 | 59 | class Meta: 60 | verbose_name_plural = "Heroes" 61 | 62 | is_immortal = models.BooleanField(default=True) 63 | 64 | benevolence_factor = models.PositiveSmallIntegerField( 65 | help_text="How benevolent this hero is?" 66 | ) 67 | arbitrariness_factor = models.PositiveSmallIntegerField( 68 | help_text="How arbitrary this hero is?" 69 | ) 70 | 71 | headshot = models.ImageField(null=True, blank=True, upload_to="hero_headshots/") 72 | 73 | # relationships 74 | father = models.ForeignKey( 75 | "self", related_name="children", null=True, blank=True, on_delete=models.SET_NULL 76 | ) 77 | mother = models.ForeignKey( 78 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 79 | ) 80 | spouse = models.ForeignKey( 81 | "self", related_name="+", null=True, blank=True, on_delete=models.SET_NULL 82 | ) 83 | 84 | 85 | class HeroProxy(Hero): 86 | 87 | class Meta: 88 | proxy = True 89 | 90 | class Villain(Entity): 91 | is_immortal = models.BooleanField(default=False) 92 | 93 | malevolence_factor = models.PositiveSmallIntegerField( 94 | help_text="How malevolent this villain is?" 95 | ) 96 | power_factor = models.PositiveSmallIntegerField( 97 | help_text="How powerful this villain is?" 98 | ) 99 | is_unique = models.BooleanField(default=True) 100 | count = models.PositiveSmallIntegerField(default=1) 101 | 102 | 103 | class HeroAcquaintance(models.Model): 104 | "Non family contacts of a Hero" 105 | hero = models.OneToOneField(Hero, on_delete=models.CASCADE) 106 | 107 | friends = models.ManyToManyField(Hero, related_name="+") 108 | detractors = models.ManyToManyField(Hero, related_name="+") 109 | main_anatagonists = models.ManyToManyField(Villain, related_name="+") 110 | 111 | 112 | class AllEntity(models.Model): 113 | name = models.CharField(max_length=100) 114 | 115 | class Meta: 116 | managed = False 117 | db_table = "entities_entity" 118 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /heroes_and_monsters/entities/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /heroes_and_monsters/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/heroes_and_monsters/events/__init__.py -------------------------------------------------------------------------------- /heroes_and_monsters/events/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | from django.contrib.auth.models import User, Group 5 | admin.site.unregister(User) 6 | admin.site.unregister(Group) 7 | 8 | 9 | 10 | from django.contrib.admin import AdminSite 11 | from .models import Epic, Event, EventHero, EventVillain 12 | 13 | 14 | class EventAdminSite(AdminSite): 15 | site_header = "UMSRA Events Admin2" 16 | site_title = "UMSRA Events Admin Portal" 17 | index_title = "Welcome to UMSRA Researcher Events Portal" 18 | 19 | 20 | def get_app_list(self, request): 21 | """ 22 | Return a sorted list of all the installed apps that have been 23 | registered in this site. 24 | """ 25 | ordering = { 26 | "Event heros": 1, 27 | "Event villains": 2, 28 | "Epics": 3, 29 | "Events": 4 30 | } 31 | app_dict = self._build_app_dict(request) 32 | # a.sort(key=lambda x: b.index(x[0])) 33 | # Sort the apps alphabetically. 34 | app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower()) 35 | 36 | # Sort the models alphabetically within each app. 37 | for app in app_list: 38 | app['models'].sort(key=lambda x: ordering[x['name']]) 39 | 40 | return app_list 41 | 42 | event_admin_site = EventAdminSite(name='event_admin') 43 | 44 | 45 | event_admin_site.register(Epic) 46 | event_admin_site.register(Event) 47 | event_admin_site.register(EventHero) 48 | event_admin_site.register(EventVillain) 49 | -------------------------------------------------------------------------------- /heroes_and_monsters/events/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EventsConfig(AppConfig): 5 | name = 'events' 6 | -------------------------------------------------------------------------------- /heroes_and_monsters/events/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-31 11:13 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('entities', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Epic', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=255)), 21 | ('participating_heroes', models.ManyToManyField(to='entities.Hero')), 22 | ('participating_villains', models.ManyToManyField(to='entities.Villain')), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Event', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('details', models.TextField()), 30 | ('years_ago', models.PositiveIntegerField()), 31 | ('epic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Epic')), 32 | ], 33 | ), 34 | migrations.CreateModel( 35 | name='EventHero', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('is_primary', models.BooleanField()), 39 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), 40 | ('hero', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.Hero')), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='EventVillain', 45 | fields=[ 46 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 47 | ('is_primary', models.BooleanField()), 48 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), 49 | ('hero', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.Villain')), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /heroes_and_monsters/events/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/heroes_and_monsters/events/migrations/__init__.py -------------------------------------------------------------------------------- /heroes_and_monsters/events/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from entities.models import Hero, Villain 3 | 4 | class Epic(models.Model): 5 | name = models.CharField(max_length=255) 6 | participating_heroes = models.ManyToManyField(Hero) 7 | participating_villains = models.ManyToManyField(Villain) 8 | 9 | 10 | class Event(models.Model): 11 | epic = models.ForeignKey(Epic, on_delete=models.CASCADE) 12 | details = models.TextField() 13 | years_ago = models.PositiveIntegerField() 14 | 15 | 16 | class EventHero(models.Model): 17 | event = models.ForeignKey(Event, on_delete=models.CASCADE) 18 | hero = models.ForeignKey(Hero, on_delete=models.CASCADE) 19 | is_primary = models.BooleanField() 20 | 21 | 22 | class EventVillain(models.Model): 23 | event = models.ForeignKey(Event, on_delete=models.CASCADE) 24 | hero = models.ForeignKey(Villain, on_delete=models.CASCADE) 25 | is_primary = models.BooleanField() 26 | -------------------------------------------------------------------------------- /heroes_and_monsters/events/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /heroes_and_monsters/events/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /heroes_and_monsters/heroes_and_monsters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/heroes_and_monsters/heroes_and_monsters/__init__.py -------------------------------------------------------------------------------- /heroes_and_monsters/heroes_and_monsters/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for heroes_and_monsters project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'jj3%i453q08gh20n75pt27dx8k*6d4d+gepevvv6t)b#42oy(8' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'entities', 42 | 'events', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'heroes_and_monsters.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [os.path.join(BASE_DIR, 'templates/')], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'heroes_and_monsters.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | STATICFILES_DIRS = [ 125 | os.path.join(BASE_DIR, "static"), 126 | ] 127 | 128 | MEDIA_URL = "/media/" 129 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 130 | -------------------------------------------------------------------------------- /heroes_and_monsters/heroes_and_monsters/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from django.conf import settings 4 | from django.conf.urls.static import static 5 | 6 | admin.site.site_header = "UMSRA Admin" 7 | admin.site.site_title = "UMSRA Admin Portal" 8 | admin.site.index_title = "Welcome to UMSRA Researcher Portal" 9 | 10 | from events.admin import event_admin_site 11 | 12 | 13 | urlpatterns = [ 14 | path('entity-admin/', admin.site.urls), 15 | path('event-admin/', event_admin_site.urls), 16 | ] 17 | 18 | if settings.DEBUG is True: 19 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 20 | -------------------------------------------------------------------------------- /heroes_and_monsters/heroes_and_monsters/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for heroes_and_monsters project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "heroes_and_monsters.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /heroes_and_monsters/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "heroes_and_monsters.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /heroes_and_monsters/requirements.txt: -------------------------------------------------------------------------------- 1 | django==2.0.1 2 | Pillow -------------------------------------------------------------------------------- /heroes_and_monsters/static/umsra_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/django-admin-cookbook/434bc6f633a0a40bb2d664b767ef59728f15527f/heroes_and_monsters/static/umsra_logo.png -------------------------------------------------------------------------------- /heroes_and_monsters/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% load staticfiles %} 4 | 5 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 6 | 7 | {% block branding %} 8 |

    9 | 10 | 11 | 12 |

    13 | {% endblock %} 14 | 15 | {% block nav-global %}{% endblock %} 16 | -------------------------------------------------------------------------------- /heroes_and_monsters/templates/admin/csv_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base.html' %} 2 | 3 | {% block content %} 4 |
    5 |
    6 | {{ form.as_p }} 7 | {% csrf_token %} 8 | 9 | 10 |
    11 |
    12 |
    13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /heroes_and_monsters/templates/entities/heroes_changelist.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_list.html' %} 2 | 3 | 4 | {% block object-tools %} 5 | Import CSV 6 |
    7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /heroes_and_monsters/templates/entities/villain_changeform.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | 3 | {% block submit_buttons_bottom %} 4 | {{ block.super }} 5 |
    6 | 7 |
    8 | {% endblock %} 9 | --------------------------------------------------------------------------------