Added the ability to add the appropriate fields to a table if configured after an initial syncdb.

This commit is contained in:
Corey Oordt 2011-08-01 09:12:21 -04:00
parent cedd54e85d
commit fdc025d812
10 changed files with 269 additions and 3 deletions

View file

@ -0,0 +1,4 @@
from django.db.models.signals import post_syncdb
from categories.migration import migrate_app
post_syncdb.connect(migrate_app)

View file

@ -0,0 +1,28 @@
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
"""
Alter one or more models' tables with the registered attributes
"""
help = "Alter the tables for all registered models, or just specified models"
args = "[appname ...]"
can_import_settings = True
requires_model_validation = False
def handle(self, *args, **options):
"""
Alter the tables
"""
try:
from south.db import db
except ImportError:
raise ImproperlyConfigured("South must be installed for this command to work")
from categories.migration import migrate_app
from categories import model_registry
if args:
for app in args:
migrate_app(app)
else:
for app in model_registry:
migrate_app(app)

View file

@ -0,0 +1,26 @@
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
"""
Alter one or more models' tables with the registered attributes
"""
help = "Drop the given field from the given model's table"
args = "appname modelname fieldname"
can_import_settings = True
requires_model_validation = False
def handle(self, *args, **options):
"""
Alter the tables
"""
try:
from south.db import db
except ImportError:
raise ImproperlyConfigured("South must be installed for this command to work")
from categories.migration import drop_field
from categories import model_registry
if len(args) != 3:
print "You must specify an Application name, a Model name and a Field name"
drop_field(*args)

90
categories/migration.py Normal file
View file

@ -0,0 +1,90 @@
from django.db import models
from django.db.utils import DatabaseError
from south.db import db
from south.signals import post_migrate
from categories.fields import CategoryM2MField, CategoryFKField
from categories.models import Category
from categories import model_registry, field_registry
def migrate_app(app, *args, **kwargs):
"""
Migrate all models of this app registered
"""
# pull the information from the registry
fields = [fld for fld in field_registry.keys() if fld.startswith(app)]
# call the south commands to add the fields/tables
for fld in fields:
app_name, model_name, field_name = fld.split('.')
# Table is typically appname_modelname, but it could be different
# always best to be sure.
mdl = models.get_model(app_name, model_name)
if isinstance(field_registry[fld], CategoryFKField):
print "Adding ForeignKey %s to %s" % (field_name, model_name)
try:
db.start_transaction()
table_name = mdl._meta.db_table
field_registry[fld].default=-1
db.add_column(table_name, field_name, field_registry[fld], keep_default=False)
db.commit_transaction()
except DatabaseError, e:
db.rollback_transaction()
if "already exists" in str(e):
print "Already exists"
else:
raise e
elif isinstance(field_registry[fld], CategoryM2MField):
print "Adding Many2Many table between %s and %s" % (model_name, 'category')
table_name = "%s_%s" % (mdl._meta.db_table, 'categories')
try:
db.start_transaction()
db.create_table(table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
(model_name, models.ForeignKey(mdl, null=False)),
('category', models.ForeignKey(Category, null=False))
))
db.create_unique(table_name, ['%s_id' % model_name, 'category_id'])
db.commit_transaction()
except DatabaseError, e:
db.rollback_transaction()
if "already exists" in str(e):
print "Already exists"
else:
raise e
def drop_field(app_name, model_name, field_name):
"""
Drop the given field from the app's model
"""
# Table is typically appname_modelname, but it could be different
# always best to be sure.
mdl = models.get_model(app_name, model_name)
fld = "%s.%s.%s" % (app_name, model_name, field_name)
if isinstance(field_registry[fld], CategoryFKField):
print "Dropping ForeignKey %s from %s" % (field_name, model_name)
try:
db.start_transaction()
table_name = mdl._meta.db_table
db.delete_column(table_name, field_name)
db.commit_transaction()
except DatabaseError, e:
db.rollback_transaction()
raise e
elif isinstance(field_registry[fld], CategoryM2MField):
print "Dropping Many2Many table between %s and %s" % (model_name, 'category')
table_name = "%s_%s" % (mdl._meta.db_table, 'categories')
try:
db.start_transaction()
db.delete_table(table_name, cascade=False)
db.commit_transaction()
except DatabaseError, e:
db.rollback_transaction()
raise e
post_migrate.connect(migrate_app)

View file

@ -0,0 +1,23 @@
.. _adding_the_fields:
=================================
Adding the fields to the database
=================================
While Django will create the appropriate columns and tables if you configure Django Categories first, many times that is not possible. You could also reset the table, but you would loose all data in it. There is another way.
Enter South
***********
`South <http://south.aeracode.org/>`_ is a Django application for managing database schema changes. South's default behavior is for managing permanent changes to a model's structure. In the case of dynamically adding a field or fields to a model, this doesn't work because you are not making the change permanent. And probably don't want to.
Django Categories has a management command to create any missing fields. It requires South because it uses the South's API for making changes to databases. The management command is simple: ``python manage.py add_category_fields [app]``\ . If you do not include an app name, it will attempt to do all applications configured.
Running this command several times will not hurt any data or cause any errors.
Reconfiguring Fields
********************
You can make changes to the field configurations as long as they do not change the underlying database structure. For example, adding a ``related_name`` (see :ref:`registering_a_m2one_relationship`\ ) because it only affects Django code. Changing the name of the field, however, is a different matter.
Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): ``python manage.py drop_category_field app_name model_name field_name``

View file

@ -35,6 +35,35 @@ You can't do it as both at the same time, though.
Connecting your model with Django-Categories
============================================
Because there are a few additional methods and attributes that your model needs, you can't simply create a ``ForeignKey`` to ``Category``, even though that is eventually what happens.
There are two ways to add Category fields to an application. If you are in control of the code (it's your application) or you are willing to take control of the code (fork someone else's app) you can make a :ref:`hard_coded_connection`\ .
For 3rd-party apps or even your own apps that you don't wish to add Django-Categories as a dependency, you can configure a :ref:`lazy_connection`\ .
.. _hard_coded_connection:
Hard Coded Connection
*********************
Hard coded connections are done in the exact same way you handle any other foreign key or many-to-many relations to a model.
.. code-block:: python
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey('categories.Category')
Don't forget to add the field or fields to your ``ModelAdmin`` class as well.
.. _lazy_connection:
Lazy Connection
***************
Lazy connections are done through configuring Django Categories in the project's ``settings.py`` file. When the project starts up, the configured fields are dynamically added to the configured models and admin.
If you do this before you have created the database (before you ran ``manage.py syncdb``), the fields will also be in the tables. If you do this after you have already created all the tables, you can run ``manage.py add_category_fields`` to create the fields (this requires Django South to be installed).
You add a many-to-one or many-to-many relationship with Django Categories using the ``CATEGORIES_SETTINGS['FK_REGISTRY']`` and ``CATEGORIES_SETTINGS['M2M_REGISTRY']`` settings respectively. For more information see :ref:`registering_models`\ .

View file

@ -6,4 +6,6 @@ Reference
:maxdepth: 2
:glob:
settings
management_commands
models
settings

View file

@ -0,0 +1,38 @@
.. _management-commands:
===================
Management Commands
===================
.. _import_categories:
import_categories
=================
**Usage:** ``./manage.py import_categories /path/to/file.txt [/path/to/file2.txt]``
Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs. The first line in the file cannot start with a space or tab and you can't mix spaces and tabs.
.. _add_category_fields:
add_category_fields
===================
**Usage:** ``./manage.py add_category_fields [app1 app2 ...]``
Add missing registered category fields to the database table of a specified application or all registered applications.
Requires Django South.
.. _drop_category_fields:
drop_category_fields
===================
**Usage:** ``./manage.py drop_category_field app_name model_name field_name``
Drop the ``field_name`` field from the ``app_name_model_name`` table, if the field is currently registered in ``CATEGORIES_SETTINGS``\ .
Requires Django South.

View file

@ -95,6 +95,8 @@ Registering one or more Many-to-Many Category fields to a Model
}
}
.. _registering_a_m2one_relationship:
Registering a many-to-one relationship
======================================

View file

@ -35,6 +35,30 @@ You can't do it as both at the same time, though.
Connecting your model with Django-Categories
============================================
Because there are a few additional methods and attributes that your model needs, you can't simply create a ``ForeignKey`` to ``Category``, even though that is eventually what happens.
There are two ways to add Category fields to an application. If you are in control of the code (it's your application) or you are willing to take control of the code (fork someone else's app) you can make a :ref:`hard_coded_connection`\ .
For 3rd-party apps or even your own apps that you don't wish to add Django-Categories as a dependency, you can configure a :ref:`lazy_connection`\ .
.. _hard_coded_connection:
Hard Coded Connection
*********************
Hard coded connections are done in the exact same way you handle any other foreign key or many-to-many relations to a model.
.. code-block:: python
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey('categories.Category')
.. _lazy_connection:
Lazy Connection
***************
You add a many-to-one or many-to-many relationship with Django Categories using the ``CATEGORIES_SETTINGS['FK_REGISTRY']`` and ``CATEGORIES_SETTINGS['M2M_REGISTRY']`` settings respectively. For more information see :ref:`registering_models`\ .