| 682 | |
| 683 | == Merged newforms-admin into trunk == |
| 684 | |
| 685 | As of [7967] we have merged newforms-admin into trunk. Here is list of things that have changed: |
| 686 | |
| 687 | {{{ |
| 688 | #!python |
| 689 | |
| 690 | # OLD: |
| 691 | from django.conf.urls.defaults import * |
| 692 | |
| 693 | urlpatterns = patterns('', |
| 694 | (r'^admin/', include('django.contrib.admin.urls')), |
| 695 | ) |
| 696 | |
| 697 | # NEW: |
| 698 | from django.conf.urls.defaults import * |
| 699 | from django.contrib import admin |
| 700 | |
| 701 | admin.autodiscover() |
| 702 | |
| 703 | urlpatterns = patterns('', |
| 704 | (r'^admin/(.*)', admin.site.root), |
| 705 | ) |
| 706 | }}} |
| 707 | |
| 708 | Note that, in this above URLconf example, we're dealing with the object {{{django.contrib.admin.site}}}. This is an instance of {{{django.contrib.admin.AdminSite}}}, which is a class that lets you specify admin-site functionality. The object {{{django.contrib.admin.site}}} is a default {{{AdminSite}}} instance that is created for you automatically, but you can also create other instances as you see fit. |
| 709 | |
| 710 | We use the {{{admin.autodiscover()}}} call above to force the import of the {{{admin.py}}} module of each {{{INSTALLED_APPS}}} entry. This won't be needed if you use your own {{{AdminSite}}} instance since you will likely be importing those modules explicily in a project-level {{{admin.py}}}. This was added in [7872]. |
| 711 | |
| 712 | Previously, there was one "global" version of the admin site, which used all models that contained a {{{class Admin}}}. This new scheme allows for much more fine-grained control over your admin sites, allowing you to have multiple admin sites in the same Django instance. |
| 713 | |
| 714 | In this example, we create two {{{AdminSite}}} instances, registering different models with both. Assume {{{Book}}}, {{{Author}}}, {{{Musician}}} and {{{Instrument}}} are Django model classes (not instances). |
| 715 | |
| 716 | {{{ |
| 717 | #!python |
| 718 | |
| 719 | # project-level admin.py |
| 720 | |
| 721 | from django.contrib import admin |
| 722 | from myproject.myapp.models import Book, Author |
| 723 | from myproject.anotherapp.models import Musician, Instrument |
| 724 | |
| 725 | site1 = admin.AdminSite() |
| 726 | site1.register(Book) |
| 727 | site1.register(Author) |
| 728 | |
| 729 | site2 = admin.AdminSite() |
| 730 | site2.register(Musician) |
| 731 | site2.register(Instrument) |
| 732 | |
| 733 | # URLconf |
| 734 | |
| 735 | from django.conf.urls.defaults import * |
| 736 | from myproject.admin import site1, site2 |
| 737 | |
| 738 | urlpatterns = patterns('', |
| 739 | (r'^book_admin/(.*)', site1.root), |
| 740 | (r'^music_admin/(.*)', site2.root), |
| 741 | ) |
| 742 | }}} |
| 743 | |
| 744 | With this example, if you go to {{{/book_admin/}}}, you'll get a Django admin site for the {{{Book}}} and {{{Author}}} models. If you go to {{{/music_admin/}}}, you'll get a Django admin site for the {{{Musician}}} and {{{Instrument}}} models. |
| 745 | |
| 746 | Admin options -- the inner {{{class Admin}}} -- have changed, too. Models no longer use an inner class to declare their admin site options. In fact, '''all admin functionality has been decoupled from the model syntax'''! How, then, do we declare admin options? Like this: |
| 747 | |
| 748 | {{{ |
| 749 | #!python |
| 750 | # a sample models.py file |
| 751 | from django.db import models |
| 752 | |
| 753 | class Author(models.Model): |
| 754 | first_name = models.CharField(max_length=30) |
| 755 | last_name = models.CharField(max_length=30) |
| 756 | |
| 757 | def __unicode__(self): |
| 758 | return u'%s %s' % (self.first_name, self.last_name) |
| 759 | |
| 760 | class Book(models.Model): |
| 761 | title = models.CharField(max_length=100) |
| 762 | author = models.ForeignKey(Author) |
| 763 | |
| 764 | # a sample admin.py file (in same app) |
| 765 | from django.contrib import admin |
| 766 | from myproject.myapp.models import Author, Book |
| 767 | |
| 768 | class BookAdmin(admin.ModelAdmin): |
| 769 | list_display = ('title', 'author') |
| 770 | ordering = ('title',) |
| 771 | |
| 772 | admin.site.register(Author) |
| 773 | admin.site.register(Book, BookAdmin) |
| 774 | }}} |
| 775 | |
| 776 | In this example, we register both {{{Author}}} and {{{Book}}} with the {{{AdminSite}}} instance {{{django.contrib.admin.site}}}. {{{Author}}} doesn't need any custom admin options, so we just call {{{admin.site.register(Author)}}}. {{{Book}}}, on the other hand, has some custom admin options, so we define a {{{BookAdmin}}} class and pass that class as a second argument to {{{admin.site.register()}}}. |
| 777 | |
| 778 | You'll notice the {{{BookAdmin}}} class looks a lot like the old-style {{{class Admin}}}. Almost all of the old {{{class Admin}}} options work exactly the same, with one or two exceptions. (For the options that have changed, we've made them '''much''' more powerful.) In addition to the classic options such as {{{list_display}}} and {{{ordering}}}, the {{{ModelAdmin}}} class introduces a wealth of extra hooks you can use to customize the admin site for that particular model. For example: |
| 779 | |
| 780 | {{{ |
| 781 | #!python |
| 782 | |
| 783 | class BookAdmin(admin.ModelAdmin): |
| 784 | list_display = ('title', 'author') |
| 785 | ordering = ('title',) |
| 786 | |
| 787 | def has_change_permission(self, request, obj=None): |
| 788 | """ |
| 789 | John can only edit books by Roald Dahl. |
| 790 | """ |
| 791 | if obj and request.user.username == 'john': |
| 792 | return obj.author.last_name == 'Dahl' |
| 793 | return super(BookAdmin, self).has_change_permission(request, obj) |
| 794 | }}} |
| 795 | |
| 796 | Look at the class {{{ModelAdmin}}} in the file [source:/django/branches/newforms-admin/django/contrib/admin/options.py django/contrib/admin/options.py] to see all of the methods you can override. This is exciting stuff. |
| 797 | |
| 798 | == To-do list == |
| 799 | |
| 800 | * See [http://code.djangoproject.com/query?status=new&status=assigned&status=reopened&keywords=%7Enfa-blocker&order=priority list of tickets] blocking the merge to trunk. |
| 801 | * See [http://code.djangoproject.com/query?status=new&status=assigned&status=reopened&keywords=%7Enfa-someday&order=priority list of tickets] that will be looked at after a merge to trunk. |
| 802 | * See [http://code.djangoproject.com/query?status=new&status=assigned&status=reopened&status=closed&keywords=%7Enfa-fixed&order=priority list of tickets] that have been fixed. |
| 803 | |
| 804 | == Backwards-incompatible changes == |
| 805 | |
| 806 | This is a (currently incomplete) list of backwards-incompatible changes made in this branch. |
| 807 | |
| 808 | === Changed Admin.manager option to more flexible hook === |
| 809 | |
| 810 | As of [4342], the {{{manager}}} option to {{{class Admin}}} no longer exists. This option was undocumented, but we're mentioning the change here in case you used it. In favor of this option, {{{class Admin}}} may now define a ```queryset``` method: |
| 811 | |
| 812 | {{{ |
| 813 | #!python |
| 814 | |
| 815 | class BookAdmin(admin.ModelAdmin): |
| 816 | def queryset(self, request): |
| 817 | """ |
| 818 | Filter based on the current user. |
| 819 | """ |
| 820 | return self.model._default_manager.filter(user=request.user) |
| 821 | }}} |
| 822 | |
| 823 | === Changed prepopulate_from to be defined in the Admin class, not database field classes === |
| 824 | |
| 825 | As of [4446], the {{{prepopulate_from}}} option to database fields no longer exists. It's been discontinued in favor of the new {{{prepopulated_fields}}} option on {{{class Admin}}}. The new {{{prepopulated_fields}}} option, if given, should be a dictionary mapping field names to lists/tuples of field names. This change was made in an effort to remove admin-specific options from the model itself. Here's an example comparing old syntax and new syntax: |
| 826 | |
| 827 | {{{ |
| 828 | #!python |
| 829 | |
| 830 | # OLD: |
| 831 | class MyModel(models.Model): |
| 832 | first_name = models.CharField(max_length=30) |
| 833 | last_name = models.CharField(max_length=30) |
| 834 | slug = models.CharField(max_length=60, prepopulate_from=('first_name', 'last_name')) |
| 835 | |
| 836 | class Admin: |
| 837 | pass |
| 838 | |
| 839 | # NEW: |
| 840 | class MyModel(models.Model): |
| 841 | first_name = models.CharField(max_length=30) |
| 842 | last_name = models.CharField(max_length=30) |
| 843 | slug = models.CharField(max_length=60) |
| 844 | |
| 845 | from django.contrib import admin |
| 846 | |
| 847 | class MyModelAdmin(admin.ModelAdmin): |
| 848 | prepopulated_fields = {'slug': ('first_name', 'last_name')} |
| 849 | |
| 850 | admin.site.register(MyModel, MyModelAdmin) |
| 851 | }}} |
| 852 | |
| 853 | === Moved admin doc views into django.contrib.admindocs === |
| 854 | |
| 855 | As of [4585], the documentation views for the Django admin site were moved into a new package, {{{django.contrib.admindocs}}}. |
| 856 | |
| 857 | The admin docs, which aren't documented very well, were located at {{{docs/}}} in the admin site. They're also linked-to by the "Documentation" link in the upper right of default admin templates. |
| 858 | |
| 859 | Because we've moved the doc views, you now have to activate admin docs explicitly. Do this by adding the following line to your URLconf: |
| 860 | |
| 861 | {{{ |
| 862 | #!python |
| 863 | (r'^admin/doc/', include('django.contrib.admindocs.urls')), |
| 864 | }}} |
| 865 | |
| 866 | You have to add this line before {{{r'^admin(.*)'}}} otherwise it won't work. |
| 867 | |
| 868 | === Renamed 'fields' to 'fieldsets', and changed type of 'classes' value === |
| 869 | |
| 870 | 'Fields' is used to order and group fields in the change form layout.[[BR]] |
| 871 | It is still available in the new admin, but it accepts only a list of fields. [[BR]] |
| 872 | In case one uses fieldsets to organize the fields, one needs to use 'fieldsets' instead. [[BR]] |
| 873 | Also, if 'classes' is specified in a field specification, then the type of its value needs to be changed from a string to a tuple of strings when migrating to the new 'fieldsets' specification. |
| 874 | |
| 875 | An example: |
| 876 | {{{ |
| 877 | #!python |
| 878 | |
| 879 | # OLD: |
| 880 | class MyModelA(models.Model): |
| 881 | class Admin: |
| 882 | fields = ('field1','field2','field3','field4') |
| 883 | |
| 884 | class MyModelB(models.Model): |
| 885 | class Admin: |
| 886 | fields = ( |
| 887 | ('group1', {'fields': ('field1','field2'), 'classes': 'collapse'}), |
| 888 | ('group2', {'fields': ('field3','field4'), 'classes': 'collapse wide'}), |
| 889 | ) |
| 890 | |
| 891 | # NEW: |
| 892 | class MyModelAdmin(admin.ModelAdmin): |
| 893 | fields = ('field1', 'field2', 'field3', 'field4') # Renaming is optional |
| 894 | |
| 895 | class AnotherModelAdmin(admin.ModelAdmin): |
| 896 | fieldsets = ( |
| 897 | ('group1', {'fields': ('field1','field2'), 'classes': ('collapse',)}), |
| 898 | ('group2', {'fields': ('field3','field4'), 'classes': ('collapse', 'wide')}), |
| 899 | ) |
| 900 | }}} |
| 901 | |
| 902 | === Inline editing === |
| 903 | |
| 904 | The syntax is now different and much, much better. |
| 905 | Here is an example: |
| 906 | {{{ |
| 907 | #!python |
| 908 | from django.contrib import admin |
| 909 | |
| 910 | class ChildInline(admin.TabularInline): |
| 911 | model = Child |
| 912 | extra = 3 |
| 913 | |
| 914 | class ParentAdmin(admin.ModelAdmin): |
| 915 | model = Parent |
| 916 | inlines = [ChildInline] |
| 917 | }}} |
| 918 | See [http://code.djangoproject.com/browser/django/branches/newforms-admin/docs/admin.txt#L506 this documentation] for more details on field options for inline classes |
| 919 | |
| 920 | === Refactored inner Admin ```js``` option to media definitions === |
| 921 | |
| 922 | In [5926] a new method of dealing with media definitions was added. It is now |
| 923 | much more flexible and allows media on more than just a {{{ModelAdmin}}} |
| 924 | classes. |
| 925 | |
| 926 | An example: |
| 927 | {{{ |
| 928 | #!python |
| 929 | |
| 930 | # OLD: |
| 931 | class MyModel(models.Model): |
| 932 | # not relavent, but here for show |
| 933 | field1 = models.CharField(max_length=100) |
| 934 | |
| 935 | class Admin: |
| 936 | js = ( |
| 937 | "/static/my_code.js", |
| 938 | ) |
| 939 | |
| 940 | # NEW: |
| 941 | # in admin.py |
| 942 | |
| 943 | class MyModelAdmin(admin.ModelAdmin): |
| 944 | class Media: |
| 945 | js = ( |
| 946 | "/static/my_code.js", |
| 947 | ) |
| 948 | }}} |
| 949 | |
| 950 | One very subtle thing to note is previously with trunk the documentation stated: |
| 951 | |
| 952 | If you use relative URLs — URLs that don’t start with {{{http://}}} or {{{/}}} — then the admin site will automatically prefix these links with {{{settings.ADMIN_MEDIA_PREFIX}}}. |
| 953 | |
| 954 | Which is still partially true with newforms-admin, but now when using relative URLs {{{settings.MEDIA_URL}}} is prepended and '''not''' {{{settings.ADMIN_MEDIA_PREFIX}}}. If you are still looking for the old behavior you can accomplish it by doing the following: |
| 955 | |
| 956 | {{{ |
| 957 | #!python |
| 958 | |
| 959 | from django.conf import settings |
| 960 | |
| 961 | class MyModelAdmin(admin.ModelAdmin): |
| 962 | class Media: |
| 963 | js = ( |
| 964 | settings.ADMIN_MEDIA_PREFIX + "some_file.js", |
| 965 | ) |
| 966 | }}} |
| 967 | |
| 968 | Make sure the value of {{{settings.ADMIN_MEDIA_PREFIX}}} is a proper absolute URL otherwise it will be treated the same as a relative URL. |
| 969 | |
| 970 | === Moved raw_id_admin from the model definition === |
| 971 | |
| 972 | The syntax is now separated from the definition of your models. |
| 973 | |
| 974 | An example: |
| 975 | {{{ |
| 976 | #!python |
| 977 | |
| 978 | # OLD: |
| 979 | class MyModel(models.Model): |
| 980 | field1 = models.ForeignKey(AnotherModel, raw_id_admin=True) |
| 981 | |
| 982 | class Admin: |
| 983 | pass |
| 984 | |
| 985 | # NEW: |
| 986 | class MyModelAdmin(admin.ModelAdmin): |
| 987 | model = MyModel |
| 988 | raw_id_fields = ('field1',) |
| 989 | }}} |
| 990 | |
| 991 | === ```django.contrib.auth``` is now using newforms === |
| 992 | |
| 993 | In [7191] ```django.contrib.auth``` has been converted to use newforms as |
| 994 | opposed to using oldforms. If you are relying on the oldforms, you will need |
| 995 | to modify your code/templates to work with newforms. |
| 996 | |
| 997 | === Moved radio_admin from the model definition === |
| 998 | |
| 999 | An example: |
| 1000 | {{{ |
| 1001 | #!python |
| 1002 | |
| 1003 | # OLD: |
| 1004 | class MyModel(models.Model): |
| 1005 | field1 = models.ForeignKey(AnotherModel, radio_admin=models.VERTICAL) |
| 1006 | |
| 1007 | class Admin: |
| 1008 | pass |
| 1009 | |
| 1010 | # NEW: |
| 1011 | class MyModelAdmin(admin.ModelAdmin): |
| 1012 | model = MyModel |
| 1013 | radio_fields = {'field1': admin.VERTICAL} |
| 1014 | }}} |
| 1015 | |
| 1016 | === Moved filter_interface from the model definition === |
| 1017 | An example: |
| 1018 | {{{ |
| 1019 | #!python |
| 1020 | |
| 1021 | # OLD: |
| 1022 | class MyModel(models.Model): |
| 1023 | field1 = models.ManyToManyField(AnotherModel, filter_interface=models.VERTICAL) |
| 1024 | field2 = models.ManyToManyField(YetAnotherModel, filter_interface=models.HORIZONTAL) |
| 1025 | |
| 1026 | # NEW: |
| 1027 | class MyModelAdmin(admin.ModelAdmin): |
| 1028 | filter_vertical = ('field1',) |
| 1029 | filter_horizontal = ('field2',) |
| 1030 | }}} |
| 1031 | |