======================================================== The Power of Django Admin (Even For Non-Django Projects) ======================================================== :Author: Steven C. Wilcox :Version: 1.0 [for PyCon 2008 (2008-03-15)] :Copyright: This work is licensed under the Creative Commons Attribution 3.0 United States License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/us/ or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. The Admin interface for Django is powerful and can be useful for building stand-alone applications or as the official web-admin interface for non-Django projects. This will be a quick tour of the capabilities and limitations of the Django Admin interface as well as the database introspection utility that can be used to quickly get a Django application setup. .. contents:: Introduction ============ Author Background ----------------- For more information about Steven Wilcox, visit http://devpicayune.com. Purpose ------- This paper (and corresponding talk) is designed to shed light on some of the very robust features that are quickly gained by utilizing the supplied Admin Interface in the Django Web Framework. Because the Admin portion of the framework is so powerful and thorough, there are actually quite a few applications that could be built solely on the Admin screens. Also, due to the speed a project can be setup with the Admin Interface and the additional database introspection tool provided with Django, it's possible to quickly create web-based administration screens for an existing web or non-web project that's been originally created in any language so long as the database is supported by Django. When properly utilized, the Admin Interface then becomes both an incredibly useful tool for standalone applications, administration screens for existing or legacy projects, and also an invaluable weapon for Python advocacy within an organization. Assumptions ----------- - You know a little something about databases and you know at least a beginners level knowledge of Python - You know how to use the internet to search for anything not covered - Some of the information presented here could be incredibly wrong Big Notice ---------- **ATTENTION**: All code samples in this paper are presented using the Django ``NewForms-Admin`` which is currently just an active branch of the Django project with plans to merge into the trunk prior to version 1.0. While I hesitated to show information that was based on a version of Django that might not be suitable for production use at this moment, it's likely that within a matter of weeks, this will become the standard syntax and model for Admin use. `Check the NewForms-Admin branch `_ for more information. Also note that I've decided to just keep all my code in the ``models.py`` file. That's probably not good form, but for demonstration and teaching purposes, I believe it's more concise to keep it all in one file. What's Not Covered ------------------ - Setting up or installing Django - Django as a whole - A complete review of everything that can be with Django's ORM - A complete analysis of the entire Admin Interface supplied by Django - The Answer to Life, the Universe and Everything... oh wait, that's 42. The Quick Admin Interface Tour ============================== The Django Admin Interface provides two high level functions: 1. a security structure for restricting access rights at both the users and group level 2. ability to add/edit/delete data in tables in the database Security -------- The security model in Django is built around the concept of users and groups. This is obviously, a pretty common and easily understood paradigm. Of note is the fact that a user can belong to multiple groups rather than just one. The security model in Django (with respect to the Admin interface) also makes a couple of assumptions. As stated in The Django Book[1] regarding the Admin interface: This is a Web-based interface, limited to trusted site administrators, that enables the adding, editing and deletion of site content. So the biggest security assumption made is that the users are "trusted". But one of the reasons this paper/talk exists is that for a certain but fairly large set of applications, all the users of a particular application may be "trusted" to one extent or another. It's for this reason, that we can consider building an entire application using just the Admin interface (or at least relying on it for 90-99% of it). .. image:: http://devpicayune.com/images/django_login.png With regard to security and the Admin site, one important point to remember is that users must have the "Staff Status" checked for them to be able to utilize the Admin interface. The other shortcut around the group membership is administrator status which basically enables all rights for a user. In what may also be considered part of the Django Admin interface, there is a log kept as a basic form of audit trail that logs users actions on the data that includes the user, object, and which fields may have been changed. This log contains action messages like "changed description" (for a field change) or "added date 'dad birthday'" (for the addition of a related child record). Alternate Authentication ~~~~~~~~~~~~~~~~~~~~~~~~ As soon the topic of users and authentication comes up, the more enterprise-oriented folks will begin asking about alternate ways to authenticate users. While the out-of-box system doesn't immediately grant the ability to authenticate against another system, several very easy to apply changes can be found to enable, for example, LDAP-based authentication. For most of these basic alternate authentication mechanisms, the Django tables that are used to manage users and groups and rights are all still present and the alternate authentication is used purely for just validation of the user id and password. [#]_ .. [#] Check `here `_ and `possibly here `_ Data Manipulation ----------------- The bulk of what people think of as the Admin interface is the actual ability to deal with application data as part of web-interface. The Django Admin interface provides the ability to add/edit/delete records in tables that are defined in a Django application (using Django ORM). There are a ton of configurable features and options that can be defined for these screens that affect how the browse lists function and look as well as the restrictions, validations, etc... made on records being added or changed. It's this full-featured data manipulation interface for non-technical users that we so desperately need for so many applications. And Django's Admin is obviously *very* complete and out-of-the-box beautiful versus so many other frameworks. It's just a logical choice when you need to have a good looking and full featured Admin interface built in an extremely short amount of time. Getting Started =============== The purpose of this document is not to teach you to use Django, so you can refer to any of the more competant resources [#]_ to really get a proper feel for getting started on a new application and some of the options available as you setup Django projects and applications. .. [#] `The Django Book `_ is always a great place to start! Regardless of whether you're writing a brand new application or whether you're wanting to build an admin interface for existing project, you still have to start with a new project and application in Django. As a reminder, Django has a term of "project" for the container of the overall system which can contain one or more applications within that project. If done correctly, it's very possible to have applications which can then be portable and re-used within other projects. Let's assume as our starting point, that you've followed the `normal steps `_ to create a new project (mine will be called 'mydemo') and a new application called 'addtracker'. We'll also assume that you've setup the settings.py file for the project to point to a database and configured the user ID and password in the database to match the settings. This is something you must do anyway for all django projects (again, there is plenty of documentation on this). In my example, I'll be using MySQL and I have a database ('schema' in MySQL) called demo (with a user: demo and password: demo). So the database section of my settings.py file looks like this:: DATABASE_ENGINE = 'mysql' DATABASE_NAME = 'demo' DATABASE_USER = 'demo' DATABASE_PASSWORD = 'demo DATABASE_HOST = '' #just using my machine as the database server DATABASE_PORT = '' ORM'd and Dangerous =================== The key to working with database-based applications in Django and utilizing the Admin interface is Django's ORM (Object Relational Mapping). Once the database tables, fields and relationships are correctly setup and configured using Django ORM, the Admin site and any other Django activity is pretty straight forward. The ORM is basically all configured in your application in the models.py file within the application directory. This is where the models are setup. From Scratch ------------ If the database tables you are going to be using for your application don't exist yet, you can follow the `standard instructions for building a new Django application `_. Part of those instructions have you writing out definitions for the data in the models.py for the application you are building. For our example application, I setup something like this as a starting point in the models.py:: from django.db import models class Address(models.Model): short_name = models.CharField(max_length=20) long_name = models.CharField(max_length=80) addr_line_1 = models.CharField(max_length=80,blank=True) addr_line_2 = models.CharField(max_length=80,blank=True) city = models.CharField(max_length=50) state = models.USStateField() zip = models.CharField(max_length=10,blank=True) class PhoneType(models.Model): description = models.CharField(max_length=20) class Phone(models.Model): description = models.CharField(max_length=80) number = models.PhoneNumberField() number_type = models.ForeignKey(PhoneType) address = models.ForeignKey(Address) class Date(models.Model): description = models.CharField(max_length=80) start_date = models.DateField() annual_event = models.BooleanField() address = models.ForeignKey(Address) class Group(models.Model): description = models.CharField(max_length=80) addresses = models.ManyToManyField(Address) class Note(models.Model): note_text = models.TextField() address = models.ForeignKey(Address) The only semi-custom thing I've done is indicated that some of the fields are not required by including the ``blank=True`` property in the definition of some of the fields. At this point, Admin is not enabled, we're just defining our ORM models for our database. If you run syncdb command at this point, django will setup the basic framework support tables in the database, but you'll notice it doesn't even include our new models:: $ ./manage.py syncdb Creating table auth_message Creating table auth_group Creating table auth_user Creating table auth_permission Creating table django_content_type Creating table django_session Creating table django_site You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (Leave blank to use 'steven'): E-mail address: demo@site.demo Password: Password (again): Superuser created successfully. Installing index for auth.Message model Installing index for auth.Permission model Loading 'initial_data' fixtures... No fixtures found. Pay particular attention to the superuser setting because you'll need that later to login to the Admin interface. Set that carefully. Edit the settings.py and add the addtracker application as an installed application so it should now look like this:: INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'addtracker', ) Then rerun the syncdb application:: $ ./manage.py syncdb Creating table addtracker_group Creating table addtracker_note Creating table addtracker_phone Creating table addtracker_phonetype Creating table addtracker_address Creating table addtracker_date Installing index for addtracker.Note model Installing index for addtracker.Phone model Installing index for addtracker.Date model Loading 'initial_data' fixtures... No fixtures found. Now we actually have tables in our database that map with our models. Importing (or Rather "Inspecting") ---------------------------------- The other possible way of dealing with getting the database layout correctly setup in Django's ORM is the "inspectdb" command available as part of the manage.py application that is part of each django project. For this example, I've created a new project called mydemo2 and an application called legacyapp to demonstrate setting up django based on existing database tables. The settings.py will be configured to point to a database schema called demo2 (again MySQL). The tables have a layout like this: =========== =========== **authors** ----------------------- *name* varchar(20) nickname varchar(20) description text =========== =========== =========== =========== **entries** ----------------------- *id* integer title varchar(200) postDate timestamp modifyDate timestamp contents text author varchar(20) =========== =========== After setting up the settings.py similar to before (although this time my data is in the ``demo2`` schema), we can run the inspectdb utility to give us a leg up on our models.py:: $ ./manage.py inspectdb # This is an auto-generated Django model module. # You'll have to do the following manually to clean this up: # * Rearrange models' order # * Make sure each model has one field with primary_key=True # Feel free to rename the models, but don't rename db_table values or field names. # # Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]' # into your database. from django.db import models class Authors(models.Model): name = models.CharField(max_length=60, primary_key=True) nickname = models.CharField(max_length=60) description = models.TextField() class Meta: db_table = u'authors' class Entries(models.Model): id = models.IntegerField(primary_key=True) title = models.CharField(max_length=600) postdate = models.DateTimeField() modifydate = models.DateTimeField() contents = models.TextField() author = models.CharField(max_length=60) class Meta: db_table = u'entries' This is pretty close to right. For some reason, in my particular version, you can see it was multiplying the max_length by 3, so that's got to be tweaked (`issue 5725 `_). The other thing here is that while it correctly identifies the primary fields, ``inspectdb`` is unable to detect that the ``id`` field is auto numbered. Also, because I'm using a MyISAM storage engine for MySQL, there is no true database support for foreign keys, so ``inspectdb`` can't detect those either. So making a couple of changes to the above version of models.py we now have:: from django.db import models class Authors(models.Model): name = models.CharField(max_length=20, primary_key=True) nickname = models.CharField(max_length=20) description = models.TextField() class Meta: db_table = u'authors' class Entries(models.Model): id = models.AutoField(primary_key=True) title = models.CharField(max_length=200) postdate = models.DateTimeField() modifydate = models.DateTimeField() contents = models.TextField() author = models.ForeignKey(Authors,db_column='author') class Meta: db_table = u'entries' First, I fixed the ``id`` field in Entries to be an ``AutoField`` since it is auto-numbered by the database. We wouldn't want the admin interface demand that we enter a number there. Second, I changed the ``author`` field to be a ``ForeignKey`` to Authors. If the author field in Entries actually linked to a non-primary key, Django even offers an option called ``to_field`` so that a specific field can be specified to link to. I also had to specify the the option ``db_column`` to specify that the name of the field in the database really was just 'author' because under the covers, Django is used naming those types of fields with an '_id' appended to the name of the field. Depending on how the postdate and modifydate fields are used, you may have to decide to deal with those. If the legacy application normally fills those, and now your application is being requested to fill those, you'll have to accomodate that. So let's say that the postdate field is filled when the record is originally created by the user, and the modifydate field is always updated anytime the record is added or edited. And we're obviously stating that the database isn't taking care of that for us in this case, that the application is expected to do that. Without wandering out of the models.py and without changing the table, we can handle that also:: from django.db import models import datetime class Authors(models.Model): name = models.CharField(max_length=20, primary_key=True) nickname = models.CharField(max_length=20) description = models.TextField() class Meta: db_table = u'authors' def __str__(self): return self.name class Entries(models.Model): id = models.AutoField(primary_key=True) title = models.CharField(max_length=200) postdate = models.DateTimeField(editable=False) modifydate = models.DateTimeField(editable=False) contents = models.TextField() author = models.ForeignKey(Authors,db_column='author') class Meta: db_table = u'entries' def save(self): if not self.id: self.postdate = datetime.date.today() self.modifydate = datetime.datetime.today() super(Entries, self).save() In this version, we're sub-classing the ``save`` method of the model. Prior to doing the normal saving, we set the postdate to today's date and the modifydate to the current date and time (more of a true timestamp). But also note that in the field definitions, I added the ``editable=False`` option so that those fields wouldn't be manually entered on the Admin form. [#]_ .. [#] thanks to `James Bennett's tip `_ Caution ~~~~~~~ Depending on the rights granted to the user account that your Django project will be using, you could very easily end up seriously messing up some data. If you're working with existing data and tables, you'll want avoid commands like ``sqlreset`` that can overwrite the definitions of tables and clear tables. Limitations of inspectdb and the ORM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Relationship Detection ^^^^^^^^^^^^^^^^^^^^^^ ``inspectdb`` can detect foreign keys in databases that support those like MySQL with the InnoDB engine or Oracle or PostgreSQL. For MySQL with the MyISAM storage engine or SQLite, no foreign keys are detected. In fact with SQLite, ``inspectdb`` can't detect a primary key, so that has to be set manually as well. Even with our example, though, you can see that most standard foreign key references can be dealt with given the number of optional parameters that Django provides for models in the ORM. Primary Key Must be a Single Field ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The primary key for each table must be a single field (and of course each table **must** have a primary key). Currently, there is no way in the Django ORM to specify or support a multi-field primary key. One obvious workaround to this, if you have the ability to do so is use your own special version of a table that combines the values of the fields into one field so that you can later push those records back to the original table. Single Database Limitation ^^^^^^^^^^^^^^^^^^^^^^^^^^ Django currently expects to only deal with one database or one database schema at a time. There has been talk and work on supporting multiple schemas in MySQL, for instance, and some have managed to do it with small tweaks to the Django code, but it's currently not still got true support. With Oracle, there is support for tablespaces, specifically for supporting indexes that are stored in separate tablespaces. Work Arounds ^^^^^^^^^^^^ I don't have a magic pill for each of these issues or probably others that I haven't covered. Instances where there are incompatibilities, it really is time to kick in some creative problem solving. My suggestion is asking yourself several questions: - What tables really need to be accessed by the Django Admin Interface? - How often is the data changed or needs to be changed? - How current does the data from the rest of the system need to be? Chances are if the current system already involves regularly scheduled imports or transfers of data to/from other databases or csv's or spreadsheets (yuck), then you're probably in a good position to still utilize some form of Django application to enter/maintain data. Even if there is an import/export process still involved, having a true web app on a real database is bound to be vastly superior to folks passing around a spreadsheet document. By relying heavily on batching and separation, you can allow your Django application to live a more separate environment which certainly makes the *Risk Police* in your organization happier while still getting you the functionality you need. Enabling Admin ============== For the remainder, I'll focus on our first project and application but obviously, these techniques apply, regardless of how you got the ORM setup initially. In settings.py you should add django.contrib.admin as an installed application:: INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.admin', 'addtracker', ) Then one more syncdb and we'll have all the database tables we need:: $ ./manage.py syncdb Creating table django_admin_log Installing index for admin.LogEntry model Loading 'initial_data' fixtures... No fixtures found. To be able to actually navigate in our browser to the admin site, we've got to enable a pattern to get to the admin site:: from django.conf.urls.defaults import * from django.contrib import admin urlpatterns = patterns('', ('^admin/(.*)', admin.site.root), ) To enable Admin for a model (the correct terminology here is apparently *register*), we need to add the following lines:: from django.contrib import admin admin.site.register(Address) admin.site.register(Date) admin.site.register(PhoneType) admin.site.register(Phone) admin.site.register(Group) admin.site.register(Note) This enables each of models for use in the Admin interface. So if we run the server and login we get: .. image:: http://devpicayune.com/images/django_enabled_admin.png And if we add an address record it's really not a bad looking form: .. image:: http://devpicayune.com/images/django_add_form_entry.png The field names are a bit turse and we can't easily get to any of the child records directly from here, but it is usable. .. image:: http://devpicayune.com/images/django_browse_list_bad.png Yuck! Our new address record is listed as Address object. There are two ways of dealing with this problem and really both of them ought be done. The first way to deal with this, is to define a string (think __str__ ) representation of the object. This is extremely easy and really ought to be done for all objects since in parent child scenarios Django used the string representation for selecting a record. But to really make this browse look nice, we're going to start using true Admin options. This is again, one of the ways the NewForms-Admin differs from the old style. Many of the options have the same names, so looking at the Django Book (reference) or other code examples, won't totally be a waste of time because it shows you what can be done and a hint as to what is used to make it happen. To define Admin options for a particular model, you need to define a new class (not a nested class) in the models.py (or whereever you decide you're going to define Admin related options since it doesn't have to be in models.py):: class Address(models.Model): short_name = models.CharField(max_length=20, verbose_name="last name", help_text="name used for sorting purposes") long_name = models.CharField(max_length=80, help_text="the text that would appear on an envelope") addr_line_1 = models.CharField(max_length=80,blank=True,verbose_name="address line 1") addr_line_2 = models.CharField(max_length=80,blank=True,verbose_name="address line 2") city = models.CharField(max_length=50) state = models.USStateField() zip = models.CharField(max_length=10,blank=True) def __str__(self): return self.short_name class Meta: verbose_name_plural = 'addresses' class AddressOptions(admin.ModelAdmin): search_fields = ['short_name','long_name','city'] list_display = ('short_name','city','state') ordering = ('short_name',) list_filter = ('state',) fieldsets = (('Name', {'fields':('short_name', 'long_name')}), ('Address', {'fields':('addr_line_1', 'addr_line_2', 'city', 'state', 'zip')}), ) admin.site.register(Address,AddressOptions) In the Address model, I've specified a ``verbose_name`` for some of the fields to have it make more sense to the user. I've also added ``help_text`` attributes to a couple of the fields to further explain their use. This information gets displayed in the Admin interface. Then, a method definition for ``__str__`` was added. For now, all that's returned is the short_name field but that's obviously better than 'Address object' for everything. A child class called Meta was added and `verbose_name_plural` was specified as 'Addresses' so that it would stop showing up as 'Addresss'. I've added a new class called AddressOptions (which inherits from admin.ModelAdmin) and I've specified several options. Also, the admin.site.register call has the added AddressOptions added as an additional parameter. * **search_fields** : specifies which fields can be searched on from the list view * **list_display** : specifies which fields to use in the browse list * **fieldsets** : specifies which fields should be displayed in which order and how they should be grouped in the Admin form. Optionally, ``fields`` can be used instead with just a tuple of field names to specify which fields and which order * **ordering** : specifies the default display order. The cool feature is that many of the other list_display fields can be sorted on * **list_filter** : specifies which fields should be available for use as a filter Looking at the web-pages now, it's obvious the difference the settings make: .. image:: http://devpicayune.com/images/django_refined_browse_list.png .. image:: http://devpicayune.com/images/django_better_form.png We now have a pretty decent looking browse and form for the Addresses. But for so many of the related tables, it seems really silly that we should have to go to a separate browse and form to add those child records. Inline Child Records -------------------- The NewForms-Admin makes inline child records very logical and easy to implement. We'll look at Phone child. We want to be able to add and edit related phone records when we're on the address record form. For this particular purpose, the ``admin.TabularInline`` class is perfect. We just define a class that inherits from ``admin.TabularInline`` and then specify the options we want:: class PhoneInline(admin.TabularInline): model = Phone extra = 3 class AddressOptions(admin.ModelAdmin): search_fields = ['short_name','long_name','city'] list_display = ('short_name','city','state') ordering = ('short_name',) list_filter = ('state',) fieldsets = (('Name', {'fields':('short_name', 'long_name')}), ('Address', {'fields':('addr_line_1', 'addr_line_2', 'city', 'state', 'zip')}), ) inlines = [PhoneInline] Then, our ``AddressOptions`` class we specify a new option called ``inlines`` which is really just a list of all inline child record classes. So far, we're just working on Phone records so we add the new ``PhoneInline`` object in our list of inlines. .. image:: http://devpicayune.com/images/django_phone_inline.png Now we've got the ability to add/edit/delete phone records as needed from within the parent record. By the way, the ``TabularInline`` versus the ``StackedInline`` is really about the field representation. The ``TabularInline`` has a grid sort of field (as you can see in the screen shot) where fields are spread across the screen as if they were in a table of a data (hence the term 'tabular' I guess). 'Stacked' is like having the admin screen for that child record right there embedded as the field and value pairs go down rather than across. So in our example application, I'll just add the notes and dates children on there. The final bit of customization needed to have a useable application is a tweak to the Group form. The default group form has a small box where you Ctrl-Click on the members you want or don't want in the group. It works but likely won't pass the spouse-friendly user interface test. So, we'll tweak that by specifying in its Admin options class that we want a horizontal selection inteferface so that it looks like this: .. image:: http://devpicayune.com/images/django_groups.png After some final tweaks, this is the final version of the ``models.py``:: from django.db import models from django.contrib import admin class Address(models.Model): short_name = models.CharField(max_length=20, verbose_name="last name", help_text="name used for sorting purposes") long_name = models.CharField(max_length=80, help_text="the text that would appear on an envelope") addr_line_1 = models.CharField(max_length=80,blank=True,verbose_name="address line 1") addr_line_2 = models.CharField(max_length=80,blank=True,verbose_name="address line 2") city = models.CharField(max_length=50) state = models.USStateField() zip = models.CharField(max_length=10,blank=True) def __str__(self): return self.short_name + ' (' + self.city + ', ' + self.state + ')' class Meta: verbose_name_plural = 'addresses' class PhoneType(models.Model): description = models.CharField(max_length=20) def __str__(self): return self.description class Phone(models.Model): description = models.CharField(max_length=80) number = models.PhoneNumberField() number_type = models.ForeignKey(PhoneType) address = models.ForeignKey(Address) def __str__(self): return str(self.number) class Date(models.Model): description = models.CharField(max_length=80) start_date = models.DateField() annual_event = models.BooleanField() address = models.ForeignKey(Address) class Group(models.Model): description = models.CharField(max_length=80) addresses = models.ManyToManyField(Address) def __str__(self): return self.description class Note(models.Model): note_text = models.TextField() address = models.ForeignKey(Address) class DateInline(admin.TabularInline): model = Date fields = ('start_date','description','annual_event') extra = 2 class NoteInline(admin.StackedInline): model = Note extra = 1 class PhoneInline(admin.TabularInline): model = Phone extra = 3 class AddressOptions(admin.ModelAdmin): search_fields = ['short_name','long_name','city'] list_display = ('short_name','city','state') ordering = ('short_name',) list_filter = ('state',) fieldsets = (('Name', {'fields':('short_name', 'long_name')}), ('Address', {'fields':('addr_line_1', 'addr_line_2', 'city', 'state', 'zip')}), ) inlines = [PhoneInline,DateInline,NoteInline] class GroupOptions(admin.ModelAdmin): filter_horizontal = ('addresses',) admin.site.register(Address,AddressOptions) admin.site.register(PhoneType) admin.site.register(Group,GroupOptions) It isn't perfect but in what ended up being an hour or less of coding and playing with settings, we have a useable and decent looking application. If you want to customize the "Django administration" text throughout the system to match you can find that information on-line, of course. There are also some `guides to changing and customizing the CSS `_ used to style the Admin interface so that you can really make it feel like your own application. And finally, you can take the existing Admin templates and use them as a starting point for additional views and forms as reports or special input screens. Summary ======= Following the standard "getting started" guides for Django, possibly using the services of the ``inspectdb`` command, and modifying just 3 files (settings.py, urls.py, and models.py), you can have a very feature rich web-based admin interface for either an existing database or a new set of data that you need to modify. Whether you use this new found knowledge to impress your spouse or your bewildered co-workers, or just tuck away for a rainy day, the power of the Django Admin interface can now be yours.