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 |
46 |
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 |
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 |