= Removing the magic = The "magic-removal" branch has made several sweeping changes to the Django codebase, removing warts that Django accumulated over the years. Most changes involve the database API and removing some of its unneeded magic, and other changes involve improving the framework's simplicity and usability. As of May 1, 2006, these changes have been integrated into the Django development version (Subversion's trunk), and they'll be released officially in the next Django release, 0.95. This document explains all changes. Additional resource: the [wiki:MagicRemovalCheatSheet "magic-removal" cheat sheet], which can be used as a reference for conversion to the "magic-removal" branch. It contains links to this document organized by functional areas. [[TOC(inline, RemovingTheMagic)]] == How to get the branch == If you're running Django 0.91 at the moment and want to start playing with the new, magic-removal version, just check out Django's development version using this command: {{{ svn co http://code.djangoproject.com/svn/django/trunk/ }}} If you have been working from a checkout of the magic-removal branch and want to switch your checkout over to the new trunk, cd to your magic-removal directory and then: {{{ svn switch http://code.djangoproject.com/svn/django/trunk/ }}} '''Note to Windows users:''' There have been confirmed reports of a 0.95 SVN checkout not correctly overwriting older versions. To be safe, remove your old trunk folder before checking out. === Using two versions of Django side-by-side === Here's one way to use Django 0.91 and the magic-removal trunk versions on the same machine. This assumes a release such as 0.90 or 0.91 is installed: {{{ # Get the development/trunk code somewhere on your filesystem. In this example, we use /home/python/django. $ cd /home/python/django $ svn co http://code.djangoproject.com/svn/django/trunk # This will have created a "trunk" directory. # Whenever you want to use trunk, set the environment variable {{{PYTHONPATH}}} to the directory containing trunk. export PYTHONPATH=/home/python/django/trunk }}} == Overview == The biggest changes are: * The magic package {{{django.models}}} no longer exists. To use models, just import the model class from wherever it lives on the Python path. Similarly, the magic modules (such as {{{django.models.polls}}} in the tutorial) no longer exist; now, you interact directly with the model class. * All automatic pluralization is gone. * The database API has changed in several ways. * Various packages, such as the Django template system (previously in {{{django.core.template}}}), have been moved around to make importing less verbose and easier to remember. == Database changes you'll need to make == To upgrade from a previous Django installation, you'll need to make some database changes. Obviously, this doesn't apply if you're starting from scratch. === Rename core database tables === We've renamed a bunch of the core Django tables. Due to functionality differences between the various database backends, the SQL to perform the necessary tasks varies slightly between engines. To upgrade in SQLite, execute this SQL in your database (some steps are more involved because SQLite has only limited ALTER TABLE functionality. We have to instead create working tables, move the data, and then replace the old tables with the new ones: {{{ ALTER TABLE auth_groups RENAME TO auth_group; ALTER TABLE auth_groups_permissions RENAME TO auth_group_permissions; ALTER TABLE auth_messages RENAME TO auth_message; ALTER TABLE auth_permissions RENAME TO auth_permission; ALTER TABLE auth_users RENAME TO auth_user; ALTER TABLE auth_users_groups RENAME TO auth_user_groups; ALTER TABLE auth_users_user_permissions RENAME TO auth_user_user_permissions; ALTER TABLE content_types RENAME TO django_content_type; ALTER TABLE core_sessions RENAME TO django_session; ALTER TABLE django_flatpages RENAME TO django_flatpage; ALTER TABLE django_flatpages_sites RENAME TO django_flatpage_sites; ALTER TABLE django_redirects RENAME TO django_redirect; ALTER TABLE sites RENAME TO django_site; CREATE TABLE "django_content_type_new" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "app_label" varchar(100) NOT NULL, "model" varchar(100) NOT NULL, UNIQUE ("app_label", "model") ); INSERT INTO django_content_type_new (id,name,app_label,model) SELECT id,name,package,python_module_name FROM django_content_type; ALTER TABLE django_content_type rename to django_content_type_old; ALTER TABLE django_content_type_new rename to django_content_type; DROP TABLE django_content_type_old; DROP TABLE packages; ALTER TABLE auth_permission ADD COLUMN content_type_id INTEGER; }}} To upgrade in MySQL , execute this SQL in your database: {{{ ALTER TABLE auth_groups RENAME TO auth_group; ALTER TABLE auth_groups_permissions RENAME TO auth_group_permissions; ALTER TABLE auth_messages RENAME TO auth_message; ALTER TABLE auth_permissions RENAME TO auth_permission; ALTER TABLE auth_users RENAME TO auth_user; ALTER TABLE auth_users_groups RENAME TO auth_user_groups; ALTER TABLE auth_users_user_permissions RENAME TO auth_user_user_permissions; ALTER TABLE content_types RENAME TO django_content_type; ALTER TABLE core_sessions RENAME TO django_session; ALTER TABLE django_flatpages RENAME TO django_flatpage; ALTER TABLE django_flatpages_sites RENAME TO django_flatpage_sites; ALTER TABLE django_redirects RENAME TO django_redirect; ALTER TABLE sites RENAME TO django_site; DROP TABLE packages; ALTER TABLE django_content_type CHANGE package app_label VARCHAR(100) NOT NULL, CHANGE python_module_name model VARCHAR(100) NOT NULL; ALTER TABLE auth_permission ADD COLUMN content_type_id INTEGER; }}} PostgreSQL is different, because you have to rename sequences, too. To upgrade in PostgreSQL, execute this SQL in your database: {{{ BEGIN; ALTER TABLE auth_groups RENAME TO auth_group; ALTER TABLE auth_groups_id_seq RENAME TO auth_group_id_seq; ALTER TABLE auth_group ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_group ALTER COLUMN id SET DEFAULT nextval('public.auth_group_id_seq'::text); ALTER TABLE auth_groups_permissions RENAME TO auth_group_permissions; ALTER TABLE auth_groups_permissions_id_seq RENAME TO auth_group_permissions_id_seq; ALTER TABLE auth_group_permissions ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_group_permissions ALTER COLUMN id SET DEFAULT nextval('public.auth_group_permissions_id_seq'::text); ALTER TABLE auth_messages RENAME TO auth_message; ALTER TABLE auth_messages_id_seq RENAME TO auth_message_id_seq; ALTER TABLE auth_message ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_message ALTER COLUMN id SET DEFAULT nextval('public.auth_message_id_seq'::text); ALTER TABLE auth_permissions RENAME TO auth_permission; ALTER TABLE auth_permissions_id_seq RENAME TO auth_permission_id_seq; ALTER TABLE auth_permission ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_permission ALTER COLUMN id SET DEFAULT nextval('public.auth_permission_id_seq'::text); ALTER TABLE auth_users RENAME TO auth_user; ALTER TABLE auth_users_id_seq RENAME TO auth_user_id_seq; ALTER TABLE auth_user ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_user ALTER COLUMN id SET DEFAULT nextval('public.auth_user_id_seq'::text); ALTER TABLE auth_users_groups RENAME TO auth_user_groups; ALTER TABLE auth_users_groups_id_seq RENAME TO auth_user_groups_id_seq; ALTER TABLE auth_user_groups ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_user_groups ALTER COLUMN id SET DEFAULT nextval('public.auth_user_groups_id_seq'::text); ALTER TABLE auth_users_user_permissions RENAME TO auth_user_user_permissions; ALTER TABLE auth_users_user_permissions_id_seq RENAME TO auth_user_user_permissions_id_seq; ALTER TABLE auth_user_user_permissions ALTER COLUMN id DROP DEFAULT; ALTER TABLE auth_user_user_permissions ALTER COLUMN id SET DEFAULT nextval('public.auth_user_user_permissions_id_seq'::text); ALTER TABLE content_types RENAME TO django_content_type; ALTER TABLE content_types_id_seq RENAME TO django_content_type_id_seq; ALTER TABLE django_content_type ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_content_type ALTER COLUMN id SET DEFAULT nextval('public.django_content_type_id_seq'::text); ALTER TABLE core_sessions RENAME TO django_session; ALTER TABLE django_flatpages RENAME TO django_flatpage; ALTER TABLE django_flatpages_id_seq RENAME TO django_flatpage_id_seq; ALTER TABLE django_flatpage ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_flatpage ALTER COLUMN id SET DEFAULT nextval('public.django_flatpage_id_seq'::text); ALTER TABLE django_flatpages_sites RENAME TO django_flatpage_sites; ALTER TABLE django_flatpages_sites_id_seq RENAME TO django_flatpage_sites_id_seq; ALTER TABLE django_flatpage_sites ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_flatpage_sites ALTER COLUMN id SET DEFAULT nextval('public.django_flatpage_sites_id_seq'::text); ALTER TABLE django_redirects RENAME TO django_redirect; ALTER TABLE django_redirects_id_seq RENAME TO django_redirect_id_seq; ALTER TABLE django_redirect ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_redirect ALTER COLUMN id SET DEFAULT nextval('public.django_redirect_id_seq'::text); ALTER TABLE auth_permission DROP COLUMN package; -- First try this: ALTER TABLE django_content_type DROP CONSTRAINT "content_types_package_fkey"; -- If that didn't work, do this: ALTER TABLE django_content_type DROP CONSTRAINT "$1"; ALTER TABLE django_content_type ADD COLUMN app_label varchar(100); UPDATE django_content_type SET app_label=package; ALTER TABLE django_content_type ALTER COLUMN app_label SET NOT NULL; ALTER TABLE django_content_type DROP COLUMN package; DROP TABLE packages; ALTER TABLE sites RENAME TO django_site; ALTER TABLE sites_id_seq RENAME TO django_site_id_seq; ALTER TABLE django_site ALTER COLUMN id DROP DEFAULT; ALTER TABLE django_site ALTER COLUMN id SET DEFAULT nextval('public.django_site_id_seq'::text); ALTER TABLE django_content_type rename python_module_name to model; ALTER TABLE auth_permission ADD COLUMN content_type_id INTEGER references django_content_type(id); COMMIT; }}} === Changes to the SQL for the comments app === The comments app, still undocumented, also requires some database changes. In PostgreSQL: {{{ BEGIN; ALTER TABLE comments RENAME TO comments_comment; ALTER TABLE comments_id_seq RENAME TO comments_comment_id_seq; ALTER TABLE comments_karma RENAME TO comments_karmascore; ALTER TABLE comments_karma_id_seq RENAME TO comments_karmascore_id_seq; ALTER TABLE comments_free RENAME TO comments_freecomment; ALTER TABLE comments_free_id_seq RENAME TO comments_freecomment_id_seq; ALTER TABLE comments_moderator_deletions RENAME TO comments_moderatordeletion; ALTER TABLE comments_moderator_deletions_id_seq RENAME TO comments_moderatordeletion_id_seq; ALTER TABLE comments_user_flags RENAME TO comments_userflag; ALTER TABLE comments_user_flags_id_seq RENAME TO comments_userflag_id_seq; COMMIT; }}} In other databases: {{{ ALTER TABLE comments RENAME TO comments_comment; ALTER TABLE comments_karma RENAME TO comments_karmascore; ALTER TABLE comments_free RENAME TO comments_freecomment; ALTER TABLE comments_moderator_deletions RENAME TO comments_moderatordeletion; ALTER TABLE comments_user_flags RENAME TO comments_userflag; }}} === Auth_permissions case changes === Changeset [2745] changed the {{{verbose_name}}}s of two models in the {{{django.contrib.auth}}} app -- {{{Group}}} and {{{User}}} -- to be lowercased instead of uppercased ({{{"users"}}} instead of {{{"Users"}}}). This breaks the {{{syncdb}}} function of manage.py due to the disparity in existing installations of the magic-removal branch. To fix your {{{auth_permissions}}} table, execute these SQL statements: {{{ BEGIN; UPDATE auth_permission SET name='Can add group' WHERE codename='add_group'; UPDATE auth_permission SET name='Can change group' WHERE codename='change_group'; UPDATE auth_permission SET name='Can delete group' WHERE codename='delete_group'; UPDATE auth_permission SET name='Can add user' WHERE codename='add_user'; UPDATE auth_permission SET name='Can change user' WHERE codename='change_user'; UPDATE auth_permission SET name='Can delete user' WHERE codename='delete_user'; UPDATE django_content_type SET name='group' WHERE model='groups'; UPDATE django_content_type SET name='user' WHERE model='users'; COMMIT; }}} === django_content_type table changes === For every record in the {{{django_content_type}}} table (formerly {{{content_types}}}), rename the {{{model}}} field so that it's a singular version of the word. For example: {{{ UPDATE django_content_type SET model='group' WHERE model='groups'; UPDATE django_content_type SET model='user' WHERE model='users'; UPDATE django_content_type SET model='flatpage' WHERE model='flatpagess'; }}} === SQLite: Drop Auth_permission.Package column === '''User 'Rajesh Dhawan' notes that, in SQLite, the Foreign Key reference on column {{{package}}} from the {{{auth_permissions}}} table still causes problems after following all the above database conversions. Executing the following conversion routine eliminates the {{{package}}} column (SQLite doesn't support dropping of columns):''' {{{ CREATE TABLE "auth_permission_new" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "content_type_id" integer, "codename" varchar(100) NOT NULL, UNIQUE ("content_type_id", "codename") ); insert into auth_permission_new (id, name, codename) select id, name, codename from auth_permission; alter table auth_permission rename to auth_permission_old; alter table auth_permission_new rename to auth_permission; drop table auth_permission_old; }}} === Entries in your Content Types table will now be wrong === If you've been using the comments system, your comments will be related back to the objects via the content types table. These won't work after your upgrade because of the pluralization discrepancy between the new and the old: you'll get failures in {{{ContentType.get_object_for_this_type}}} complaining that {{{None}}} doesn't have a {{{_default_manager}}} attribute, and when you fix the arguments to {{{comment_form}}} and {{{get_comment_list}}} arguments you'll get more breakage because the {{{django_content_type}}} table is now out of whack. If you can't be bothered writing your own SQL to fix this, try [http://django.pastebin.com/762068 fix_content_types.py]; it seems to work for me. - [mailto:garth@deadlybloodyserious.com Garth]. === Database table-naming scheme has been changed === Database table names formerly were created by joining the {{{app_label}}} and {{{module_name}}}. Example: {{{polls_polls}}}. Because there's no longer any concept of {{{module_name}}}, database table names are now formed by joining the {{{app_label}}} and model class name (lower case). Example: {{{polls_poll}}}. As always, this behavior can be overridden on a per-model basis by specifying the {{{db_table}}} attribute in {{{class Meta}}} in your model. To upgrade, you'll either have to explicitly set {{{db_table}}} in your models or rename your database tables to fit the new naming scheme Django expects. We'd recommend setting {{{db_table}}}, because it's easier. == Code changes you'll need to make == === Model class and Field classes renamed/relocated === Change your models to import from {{{django.db.models}}} instead of {{{django.core.meta}}}. {{{ #!python from django.db import models class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) }}} === Interact directly with model classes, not with magic modules === Import the model class directly from the module in which it was defined. No more {{{django.models.*}}} magic. {{{ #!python from myproject.people.models import Person p = Person(first_name='John', last_name='Smith') p.save() }}} === Include template extension explicitly === Now, whenever referencing templates by name, you now pass the template's *full name* -- including its extension -- instead of the file's name without an extension. Given a template {{{polls/poll_detail.html}}}... Old: {{{ get_template('polls/poll_detail') select_template(['polls/poll_detail', 'polls/poll_detail2']) render_to_response('polls/poll_detail', {'poll': p}) }}} New: {{{ get_template('polls/poll_detail.html') select_template(['polls/poll_detail.html', 'polls/poll_detail2.html']) render_to_response('polls/poll_detail.html', {'poll': p}) }}} This is changed in templates themselves, too -- notably in the {{{ {% extends %} }}} and {{{ {% include %} }}} template tags: Old: {{{ {% extends "base" %} {% include "some_snippet" %} }}} New: {{{ {% extends "base.html" %} {% include "some_snippet.html" %} }}} As a result of this change, the {{{TEMPLATE_FILE_EXTENSION}}} setting is gone, because there's no need for it. Clearly, this also means you can start putting whatever extension you want on templates. So if you'd like your templates to have a {{{.txt}}} extension, go for it. If you'd like to keep using {{{.html}}}, that's fine too. If you want to invent your own extension, or not even *use* an extension, be our guest. All the templates for the {{{django.contrib}}} apps -- most notably, the admin site -- still use {{{.html}}} extensions. Custom template tags created via {{{inclusion_tag()}}} should note the explicit template name (with the extension). For example, use {{{inclusion_tag('foo/bar.html')}}} instead of {{{inclusion_tag('foo/bar')}}}. If you're using custom {{{template_name}}} arguments to generic views, don't forget to change those calls in your URLconf, too. Finally, note the syndication framework, which looks for templates with the name of the slug of each feed, now requires an '.html' extension on those templates. For example, if your feed has a slug {{{"hello"}}}, the syndication framework will look for the templates {{{feeds/hello_title.html}}} and {{{feeds/hello_description.html}}}. This is backwards-compatible. === Namespace simplification === {{{django.utils.httpwrappers}}} has moved to {{{django.http}}}. {{{django.core.exceptions.Http404}}} has moved to {{{django.http.Http404}}}. {{{django.core.template}}} has moved to {{{django.template}}}. {{{django.core.formfields}}} has moved to {{{django.forms}}}. {{{django.core.extensions}}} has moved to {{{django.shortcuts}}}. {{{django.core.extensions.DjangoContext}}} has been renamed to {{{RequestContext}}} and moved to {{{django.template.RequestContext}}}. You'll need to remove ".core." from your {{{TEMPLATE_LOADERS}}} settings values. Old: {{{ #!python TEMPLATE_LOADERS = ( 'django.core.template.loaders.filesystem.load_template_source', 'django.core.template.loaders.app_directories.load_template_source', # 'django.core.template.loaders.eggs.load_template_source', ) }}} New: {{{ #!python TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', # 'django.template.loaders.eggs.load_template_source', ) }}} Custom template tag definitions need a similar change. Old: {{{ #!python from django.core import template register = template.Library() }}} New: {{{ #!python from django.template import Library register = Library() }}} The "auth" and "core" models have been split and moved to {{{django.contrib}}} as follows: * {{{django.models.auth}}} has moved to {{{django.contrib.auth.models}}}. * {{{django.models.core.sites}}} has moved to {{{django.contrib.sites.models}}}. * {{{django.models.core.contenttypes}}} has moved to {{{django.contrib.contenttypes.models}}}. * {{{django.models.core.packages}}} has moved to {{{django.contrib.contenttypes.models}}}. (Note that "packages" are going away before magic-removal is done.) Session middleware has moved from {{{django.middleware.sessions.SessionMiddleware}}} to {{{django.contrib.sessions.middleware.SessionMiddleware}}}. Make sure to update your {{{MIDDLEWARE_CLASSES}}} setting, if you're using sessions. If you get the following errors upon starting the webserver: {{{ admin.logentry: 'user' has relation with model User, which has not been installed admin.logentry: 'content_type' has relation with model ContentType, which has not been installed }}} you need to add the apps {{{'django.contrib.contenttypes'}}} and {{{'django.contrib.auth'}}} to your {{{INSTALLED_APPS}}}. Also, the {{{Session}}} model has moved from django/models/core.py to django/contrib/sessions/models.py. If you're accessing the {{{Session}}} model for some reason, note that location change. === The 'packages' module is no more === Packages no longer exist (they were redundant). If you've done lookups against content-types or permissions you'll need to modify your code slightly: || Old || New || || {{{contenttypes.get_list(package__label__exact='foo')}}} || {{{ContentType.objects.filter(package__exact='foo')}}} || || {{{permissions.get_list(package__label__exact='foo')}}} || {{{Permission.objects.filter(package__exact='foo')}}} || === Model location changed === In previous Django versions, models lived in a {{{models/}}} subdirectory of your app package. Now, they should live in a file {{{models.py}}} directly within your app package. === Changes to model syntax === * {{{class META}}} should now be {{{class Meta}}}. The latter is easier on the eyes. * The following are no longer valid parameters to {{{class Meta}}} and should be removed: * {{{module_name}}} * {{{admin}}} (See "Moved admin options to 'class Admin'" below.) * {{{exceptions}}} (Just put your exceptions in the module that contains the models and access them normally.) * {{{module_constants}}} (Just put your constants in the module that contains the models and access them normally.) * {{{where_constraints}}} (Just use a custom manager. See "Custom managers, and multiple managers" below.) === Moved admin options to 'class Admin' === Instead of {{{admin=meta.Admin}}} in the {{{class META}}}, all admin options are in an inner {{{class Admin}}}. Old: {{{ #!python class Person(meta.Model): first_name = meta.CharField(maxlength=30) last_name = meta.CharField(maxlength=30) class META: admin = meta.Admin( list_display = ('first_name', 'last_name') ) }}} New: {{{ #!python class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) class Admin: list_display = ('first_name', 'last_name') }}} If you're using the admin interface but aren't specifying any admin options, just put a {{{pass}}} in that inner class. Old: {{{ #!python class Person(meta.Model): first_name = meta.CharField(maxlength=30) last_name = meta.CharField(maxlength=30) class META: admin = meta.Admin() }}} New: {{{ #!python class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) class Admin: pass }}} === `__repr__()` replaced by `__str__()` === You should use `__str__()` where `__repr__()` was formerly used, i.e., as a string representation of the model. `__repr__()` returns to its standard Python purpose of returning a string suitable for debugging purposes (e.g., ``); unless you want to customize the returned string, explicitly overriding `__repr__` in a model class is unnecessary. For example:: {{{ class BlogPost(models.Model): [...] def __repr__(self): return '''''' % (self.title, self.comments_enabled) def __str__(self): return self.title }}} === Database connection relocated/renamed === For any code that uses the raw database connection, use {{{django.db.connection}}} instead of {{{django.core.db.db}}}. Old: {{{ #!python from django.core.db import db cursor = db.cursor() }}} New: {{{ #!python from django.db import connection cursor = connection.cursor() }}} Backend-specific functions, if you should need them, are available at {{{django.db.backend}}}. Old: {{{ #!python from django.core import db db.quote_name('foo') }}} New: {{{ #!python from django.db import backend backend.quote_name('foo') }}} Also, the various backend functionality has been split into three separate modules for each backend -- {{{base.py}}}, {{{creation.py}}} and {{{introspection.py}}}. This is purely for performance and memory savings, so that basic, everyday Django usage doesn't have to load the introspective functionality into memory. === `datetime` and `db` modules must be imported explicitly === Formerly, each model method magically had access to the {{{datetime}}} module and to the variable {{{db}}}, which represents the current database connection. Now, those have to be imported explicitly. Old: {{{ #!python def some_method(self): print datetime.datetime.now() cursor = db.cursor() cursor.execute("UPDATE something;") }}} New: {{{ #!python import datetime from django.db import connection # ... def some_method(self): print datetime.datetime.now() cursor = connection.cursor() cursor.execute("UPDATE something;") }}} === Descriptor fields === All "table-level" functions -- ways of retrieving records tablewide rather than performing instance-specific tasks -- are now accessed via a model class's {{{objects}}} attribute. They aren't direct methods of a model instance object because we want to keep the "table-wide" and "row-specific" namespaces separate. A model class's {{{objects}}} attribute is an instance of {{{django.db.models.manager.Manager}}}. A manager has the following methods, all of which return a {{{QuerySet}}} instance. * {{{all()}}} -- Returns a {{{QuerySet}}} of all objects in the database. This is like the old {{{get_list()}}}. Takes no arguments. * {{{filter(**kwargs)}}} -- Returns a {{{QuerySet}}}, filtered by the given keyword arguments. Lookup arguments are in the same style as previously, e.g. {{{pubdate__year=2005}}}, except you can leave off {{{__exact}}} as a convenience. For example, {{{name='John'}}} and {{{name__exact='John'}}} are equivalent. Note that for lookups between applications you can't omit {{{__exact}}}. * {{{exclude(**kwargs)}}} is the same as {{{filter()}}}, but returns objects where the given arguments are not true. * {{{order_by(*fieldnames)}}} -- Returns a {{{QuerySet}}} * {{{count()}}} -- Returns the count of all objects in the database. * {{{dates(field_name, kind)}}} -- Like the old {{{get_FIELD_list()}}} for date fields. For example, old-school {{{get_pubdate_list('year')}}} is now {{{dates('pubdate', 'year')}}}. * {{{delete()}}} -- Deletes all objects. * {{{distinct()}}} -- Returns a {{{QuerySet}}} with DISTINCT set. * {{{extra(select=None, where=None, params=None, tables=None)}}} -- Sets the {{{select}}}, {{{where}}}, {{{params}}} and {{{tables}}} arguments, which are in the same format as before. * {{{get(**kwargs)}}} -- Like the old {{{get_object()}}}. Returns an object or raises {{{DoesNotExist}}} on error. * {{{in_bulk(id_list)}}} -- Like the old {{{get_in_bulk()}}}. * {{{iterator()}}} -- Returns a generator that iterators over results. * {{{select_related()}}} -- Returns a {{{QuerySet}}} with the "select related" option (which acts the same as before) set. * {{{values(*fieldnames)}}} -- Like the old {{{get_values()}}}. Each {{{QuerySet}}} has the following methods, which return a clone of the query set with the appropriate changes made: * {{{filter(**kwargs)}}} * {{{order_by(*fieldnames)}}} * {{{iterator()}}} * {{{count()}}} * {{{get(**kwargs)}}} * {{{delete()}}} * {{{filter(**kwargs)}}} * {{{select_related()}}} * {{{order_by(*fieldnames)}}} * {{{distinct()}}} * {{{extra(select=None, where=None, params=None, tables=None)}}} Here are some examples, which use the following models: {{{ #!python class Reporter(models.Model): fname = models.CharField(maxlength=30) lname = models.CharField(maxlength=30) class Site(models.Model): name = models.CharField(maxlength=20) class Article(models.Model): headline = models.CharField(maxlength=50) reporter = models.ForeignKey(Reporter) pub_date = models.DateField() sites = models.ManyToManyField(Site) }}} || '''Old syntax''' || '''New syntax''' || || {{{reporters.get_list()}}} || {{{Reporter.objects.all()}}} || || {{{reporters.get_list(fname__exact='John')}}} || {{{Reporter.objects.filter(fname='John')}}} || || {{{reporters.get_list(order_by=('-lname', 'fname'))}}} || {{{Reporter.objects.order_by('-lname', 'fname')}}} || || {{{reporters.get_list(fname__exact='John', order_by=('lname',))}}} || {{{Reporter.objects.filter(fname='John').order_by('lname')}}} || || {{{reporters.get_object(pk=3)}}} || {{{Reporter.objects.get(pk=3)}}} || || {{{reporters.get_object(complex=(Q(...)|Q(...)))}}} || {{{Reporter.objects.get(Q(...)|Q(...))}}} || || {{{reporters.get_object(fname__contains='John')}}} || {{{Reporter.objects.get(fname__contains='John')}}} || || {{{reporters.get_list(fname__ne='John')}}} || {{{Reporter.objects.exclude(fname='John')}}} (note that {{{ne}}} is no longer a valid lookup type) || || (not previously possible) || {{{Reporter.objects.exclude(fname__contains='n')}}} || || {{{reporters.get_list(distinct=True)}}} || {{{Reporter.objects.distinct()}}} || || {{{reporters.get_list(offset=10, limit=5)}}} || {{{Reporter.objects.all()[10:15]}}}|| || {{{reporters.get_values()}}} || {{{Reporter.objects.values()}}} || || {{{reporters.get_in_bulk([1, 2])}}} || {{{Reporter.objects.in_bulk([1, 2])}}} || || {{{reporters.get_in_bulk([1, 2], fname__exact='John')}}} || {{{Reporter.objects.filter(fname='John').in_bulk([1, 2])}}} || || '''Date lookup''' || || || {{{articles.get_pub_date_list('year')}}} || {{{Article.objects.dates('pub_date', 'year')}}} || || '''Latest-object lookup''' || || || {{{articles.get_latest()}}} (required {{{get_latest_by}}} in model) || {{{Article.objects.latest()}}} (with {{{get_latest_by}}} in model) || || (Not previously possible) || {{{Article.objects.latest('pub_date')}}} # Latest by pub_date (overrides {{{get_latest_by}}} field in model) || || '''Many-to-one related lookup''' || || || {{{article_obj.reporter_id}}} || {{{article_obj.reporter.id}}} || || {{{article_obj.get_reporter()}}} || {{{article_obj.reporter}}} || || {{{reporter_obj.get_article_list()}}} || {{{reporter_obj.article_set.all()}}} || || {{{reporter_obj.get_article_list(headline__exact='Hello')}}} || {{{reporter_obj.article_set.filter(headline='Hello')}}} || || {{{reporter_obj.get_article_count()}}} || {{{reporter_obj.article_set.count()}}} || || {{{reporter_obj.add_article(headline='Foo')}}} || {{{reporter_obj.article_set.create(headline='Foo')}}} || || (Alternate syntax) || {{{reporter_obj.article_set.add(article_obj)}}} || || ("values" lookup, etc., not previously possible) || {{{reporter_obj.article_set.values()}}} || || '''Many-to-many related lookup''' || || || {{{article_obj.get_site_list()}}} || {{{article_obj.sites.all()}}} || || {{{article_obj.set_sites([s1.id, s2.id])}}} || {{{article_obj.sites.clear(); article_obj.sites.add(s1); article_obj.sites.add(s2)}}} || || {{{article_obj.set_sites([s1.id]) # deletion}}} || {{{article_obj.sites.remove(s2)}}} || || {{{site_obj.get_article_list()}}} || {{{site_obj.article_set.all()}}} || Note that related-object lookup uses the default manager of the related object, which means the API for accessing related objects is completely consistent with the API for accessing objects via a manager. Also note that managers can't be accessed from instances: {{{ #!python p = Person.objects.get(pk=1) p.objects.all() # Raises AttributeError }}} === Override default manager name ('objects') === If a model already has an {{{objects}}} attribute, you'll need to specify an alternate name for the {{{objects}}} manager. {{{ #!python class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) objects = models.TextField() people = models.Manager() p = Person(first_name='Mary', last_name='Jones', objects='Hello there.') p.save() p.objects == 'Hello there.' Person.people.all() }}} === Custom managers, and multiple managers === You can create as many managers as you want. When necessary (such as on the admin), Django will use the first one defined, in order. If you define at least one custom manager, it will not get the default "objects" manager. {{{ #!python class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) people = models.Manager() fun_people = SomeOtherManager() }}} If a manager needs to access its associated model class, it should use {{{self.model}}}. Example: {{{ #!python class PersonManager(models.Manager): def get_fun_person(self): try: return self.get(fun=True) except self.model.DoesNotExist: print "Doesn't exist." }}} ==== Using a custom manager for the admin ==== Sometimes you'll want to use a different manager for the admin displays (e.g. to display only objects matching some criteria in the admin). You can do this by defining the {{{manager}}} option in your {{{Admin}}} declaration: {{{ #!python class LivingPeopleManager(models.Manager): def get_query_set(self): return super(LivingPeopleManager, self).get_query_set().filter(is_alive=True) class Person(models.Model): name = models.CharField(maxlength=50) is_alive = models.BooleanField() class Admin: manager = LivingPeopleManager() }}} (see "You can override default QuerySets" for more on QuerySets) === Overriding save() and delete() model methods === Proper subclassing of methods now works, so you can subclass the automatic {{{save()}}} and {{{delete()}}} methods. This removes the need for the {{{_pre_save()}}}, {{{_post_save()}}}, {{{_pre_delete()}}} and {{{_post_delete()}}} hooks -- all of which have been removed. Example: {{{ #!python class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) def save(self): self.do_something() super(Person, self).save() # Call the "real" save() method. self.do_something_else() }}} You can even skip saving (as requested in #1014). {{{ #!python class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) def save(self): if datetime.date.today() > datetime.date(2005, 1, 1): super(Person, self).save() # Call the "real" save() method. else: # Don't save. pass }}} === Renamed !DoesNotExist exception === Instead of {{{people.PersonDoesNotExist}}}, it's {{{Person.DoesNotExist}}}. Old: {{{ #!python from django.models.myapp import people try: people.get_object(pk=1) except people.PersonDoesNotExist: print "Not there" }}} New: {{{ #!python from path.to.myapp.models import Person try: Person.objects.get(pk=1) except Person.DoesNotExist: print "Not there" }}} === Moved admin URLconf to shorten its path === You'll need to change your URLconf to {{{include}}} the new location. * Old: {{{django.contrib.admin.urls.admin}}} * New: {{{django.contrib.admin.urls}}} === get_object_or_404 and get_list_or_404 take model classes === Formerly, these helper methods took a model module as their first positional argument. Now, they expect a model class. Old: {{{ #!python get_object_or_404(polls, pk=1) }}} New: {{{ #!python get_object_or_404(Poll, pk=1) }}} === Changed the parameters you pass to generic views === Because there's no longer a concept of {{{module_name}}}, the "info_dicts" passed to [http://www.djangoproject.com/documentation/generic_views/ generic views] no longer accept {{{"app_label"}}} and {{{"module_name"}}}. Instead, pass the parameter {{{"queryset"}}}, which should be a {{{QuerySet}}} instance. Old: {{{ #!python info_dict = { 'app_label': 'blog', 'module_name': 'entries' } }}} New: {{{ #!python from myproject.blog.models import Entry info_dict = {'queryset': Entry.objects.all()} }}} === Changed template names in generic views === Because there's no longer a concept of {{{module_name}}}, [http://www.djangoproject.com/documentation/generic_views/ generic views] no longer create templates based on the {{{module_name}}}. Wherever they used {{{module_name}}}, they now use {{{model_name}}}, a lowercase version of the model name. Note that {{{app_label}}} remains the same. These examples assume models live in {{{myproject/blog/models.py}}}. * Old: {{{blog/entries_archive.html}}} * New: {{{blog/entry_archive.html}}} === Moved settings into an instance === Settings have moved out of a dedicated module {{{django.conf.settings}}} into an instance in the {{{django.conf}}} module. So now you need to import the {{{settings}}} object and reference settings as attributes of that instance. * Old: {{{from django.conf.settings import LANGUAGE_CODE}}} * New: {{{from django.conf import settings}}} Wrappers around the Django machinery can make use of this by exchanging the settings instance with a proxy instance that delegates attribute access to a per-thread or per-location global. === Removed !SilentVariableFailure exception === Old behavior: Any exception that subclasses {{{django.core.template.SilentVariableFailure}}} fails silently in the template system. New behavior: Any exception that has a {{{silent_variable_failure}}} attribute fails silently in the template system. {{{django.core.template.SilentVariableFailure}}} no longer exists. === request.user is now set via middleware === It used to be set in the mod_python and wsgi handlers. You will need to add {{{"django.contrib.auth.middleware.AuthenticationMiddleware"}}} somewhere '''after''' {{{"django.contrib.sessions.middleware.SessionMiddleware"}}} in {{{MIDDLEWARE_CLASSES}}} in your settings.py file. Otherwise accessing {{{request.user}}} will raise an {{{AttributeError}}}. === Authentication has been consolidated === Previously, pieces of the authentication system resided in at least 4 different places. Everything has now been consolidated into {{{django.contrib.auth}}} as follows: * {{{django.parts.auth.formfields.AuthenticationForm}}} has moved to {{{django.contrib.auth.forms}}} * {{{django.parts.auth.anonymoususers.AnonymousUser}}} has moved to {{{django.contrib.auth.models}}} * {{{django.views.auth.login.*}}} has moved to {{{django.contrib.auth.views}}} * {{{django.views.decorators.auth.*}}} has moved to {{{django.contrib.auth.decorators}}} * {{{django.views.registration.passwords.PasswordResetForm}}} has moved to {{{django.contrib.auth.forms}}} * {{{django.views.registration.passwords.PasswordChangeForm}}} has moved to {{{django.contrib.auth.forms}}} * {{{django.views.registration.passwords.password_reset}}} has moved to {{{django.contrib.auth.views}}} * {{{django.views.registration.passwords.password_reset_done}}} has moved to {{{django.contrib.auth.views}}} * {{{django.views.registration.passwords.password_change}}} has moved to {{{django.contrib.auth.views}}} * {{{django.views.registration.passwords.password_change_done}}} has moved to {{{django.contrib.auth.views}}} If you are using any of these classes or functions, you will need to update your code accordingly. === Changed interface to manipulators === Old: {{{ #!python from django.core import formfields from django.models.PROJECT import MODELMODULE ... manipulator = MODELMODULE.AddManipulator() ... form = formfields.FormWrapper(manipulator, new_data, errors) }}} New: {{{ #!python from django.forms import FormWrapper from PROJECT.APP.models import MODELNAME ... manipulator = MODELNAME.AddManipulator() ... form = FormWrapper(manipulator, new_data, errors) }}} === Slightly changed django.VERSION === The variable {{{django.VERSION}}} has changed from a tuple of four elements to a tuple of three elements. Old: {{{VERSION = (0, 9, 1, 'magic-removal')}}} New: {{{VERSION = (0, 95, 'post-magic-removal')}}} == New functionality you can start using == === Models support properties === Unlike before, properties are supported on models. {{{ #!python from django.db import models class Person(models.Model): first_name = models.CharField(maxlength=30) last_name = models.CharField(maxlength=30) def _get_full_name(self): return "%s %s" % (self.first_name, self.last_name) full_name = property(_get_full_name) }}} === You can override default !QuerySets === You can specify the default !QuerySet (see "Descriptor fields" above) that a manager uses. For example: {{{ #!python class PublishedBookManager(models.Manager): def get_query_set(self): return super(PublishedBookManager, self).get_query_set().filter(is_published=True) class Book(models.Model): title = models.CharField(maxlength=50) author = models.CharField(maxlength=30) is_published = models.BooleanField() published_objects = PublishedBookManager() }}} == Documentation status == The documentation on djangoproject.com has been updated to focus on trunk. [http://www.djangoproject.com/documentation/0_91/ Documentation for 0.91] has been archived. A few improvements still need to be made to the new documentation, however. Adrian is proofreading each document to make sure all is well. Here's the status. ||Document||Edited by Adrian|| ||add_ons.txt||Yes|| ||admin_css.txt||Yes|| ||apache_auth.txt||Yes|| ||authentication.txt||Yes|| ||cache.txt||Yes|| ||contributing.txt||Yes|| ||db-api.txt||Yes|| ||design_philosophies.txt||Yes|| ||django-admin.txt||Yes|| ||email.txt||Yes|| ||faq.txt||Yes|| ||flatpages.txt||Yes|| ||forms.txt||Yes|| ||generic_views.txt||Yes|| ||i18n.txt||Yes|| ||install.txt||Yes|| ||legacy_databases.txt||Yes|| ||middleware.txt||Yes|| ||model-api.txt||Yes|| ||modpython.txt||Yes|| ||outputting_csv.txt||Yes|| ||outputting_pdf.txt||Yes|| ||overview.txt||Yes|| ||redirects.txt||Yes|| ||request_response.txt||Yes|| ||sessions.txt||Yes|| ||settings.txt||Yes|| ||static_files.txt||Yes|| ||syndication_feeds.txt||Yes|| ||templates_python.txt||Yes|| ||templates.txt||Yes|| ||transactions.txt||Yes|| ||tutorial01.txt||'''No'''|| ||tutorial02.txt||'''No'''|| ||tutorial03.txt||'''No'''|| ||tutorial04.txt||'''No'''|| ||url_dispatch.txt||Yes|| The [http://www.djangoproject.com/documentation/models/ model examples], which are generated from Django's unit tests, also need to be updated. This is just a matter of updating the script that generates the examples from the tests.