commit
999775c2d5
24
.travis.yml
24
.travis.yml
|
|
@ -5,25 +5,49 @@ python:
|
||||||
- "3.2"
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
env:
|
env:
|
||||||
- DJANGO=">=1.4,<1.5"
|
- DJANGO=">=1.4,<1.5"
|
||||||
- DJANGO=">=1.5,<1.6"
|
- DJANGO=">=1.5,<1.6"
|
||||||
- DJANGO=">=1.6,<1.7"
|
- DJANGO=">=1.6,<1.7"
|
||||||
- DJANGO=">=1.7,<1.8"
|
- DJANGO=">=1.7,<1.8"
|
||||||
- DJANGO=">=1.8,<1.9"
|
- DJANGO=">=1.8,<1.9"
|
||||||
|
- DJANGO=">=1.9a1,<1.10"
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=">=1.4,<1.5"
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=">=1.5,<1.6"
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=">=1.6,<1.7"
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=">=1.7,<1.8"
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=">=1.8,<1.9"
|
||||||
|
|
||||||
- python: "3.4"
|
- python: "3.4"
|
||||||
env: DJANGO=">=1.4,<1.5"
|
env: DJANGO=">=1.4,<1.5"
|
||||||
|
- python: "3.4"
|
||||||
|
env: DJANGO=">=1.9a1,<1.10"
|
||||||
|
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO=">=1.4,<1.5"
|
env: DJANGO=">=1.4,<1.5"
|
||||||
|
- python: "3.3"
|
||||||
|
env: DJANGO=">=1.9a1,<1.10"
|
||||||
|
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO=">=1.4,<1.5"
|
env: DJANGO=">=1.4,<1.5"
|
||||||
|
- python: "3.2"
|
||||||
|
env: DJANGO=">=1.9a1,<1.10"
|
||||||
|
|
||||||
- python: "2.6"
|
- python: "2.6"
|
||||||
env: DJANGO=">=1.7,<1.8"
|
env: DJANGO=">=1.7,<1.8"
|
||||||
- python: "2.6"
|
- python: "2.6"
|
||||||
env: DJANGO=">=1.8,<1.9"
|
env: DJANGO=">=1.8,<1.9"
|
||||||
|
- python: "2.6"
|
||||||
|
env: DJANGO=">=1.9a1,<1.10"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install django$DJANGO coverage==3.6
|
- pip install django$DJANGO coverage==3.6
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ Contributors
|
||||||
* Evan Borgstrom
|
* Evan Borgstrom
|
||||||
* Gavin Wahl
|
* Gavin Wahl
|
||||||
* Germán M. Bravo
|
* Germán M. Bravo
|
||||||
|
* Hugo Osvaldo Barrera
|
||||||
* Jacob Rief
|
* Jacob Rief
|
||||||
* Jedediah Smith (proxy models support)
|
* Jedediah Smith (proxy models support)
|
||||||
* John Furr
|
* John Furr
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Advanced features
|
||||||
In the examples below, these models are being used::
|
In the examples below, these models are being used::
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from polymorphic import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
|
|
||||||
class ModelA(PolymorphicModel):
|
class ModelA(PolymorphicModel):
|
||||||
field1 = models.CharField(max_length=10)
|
field1 = models.CharField(max_length=10)
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ manager class, just derive your manager from ``PolymorphicManager`` instead of
|
||||||
``models.Manager``. As with vanilla Django, in your model class, you should
|
``models.Manager``. As with vanilla Django, in your model class, you should
|
||||||
explicitly add the default manager first, and then your custom manager::
|
explicitly add the default manager first, and then your custom manager::
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel, PolymorphicManager
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from polymorphic.manager import PolymorphicManager
|
||||||
|
|
||||||
class TimeOrderedManager(PolymorphicManager):
|
class TimeOrderedManager(PolymorphicManager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super(TimeOrderedManager,self).get_queryset()
|
qs = super(TimeOrderedManager,self).get_queryset()
|
||||||
return qs.order_by('-start_date') # order the queryset
|
return qs.order_by('-start_date') # order the queryset
|
||||||
|
|
@ -41,9 +42,10 @@ base models, as long as these are polymorphic. This means that all
|
||||||
managers defined in polymorphic base models continue to work as
|
managers defined in polymorphic base models continue to work as
|
||||||
expected in models inheriting from this base model::
|
expected in models inheriting from this base model::
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel, PolymorphicManager
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from polymorphic.manager import PolymorphicManager
|
||||||
|
|
||||||
class TimeOrderedManager(PolymorphicManager):
|
class TimeOrderedManager(PolymorphicManager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super(TimeOrderedManager,self).get_queryset()
|
qs = super(TimeOrderedManager,self).get_queryset()
|
||||||
return qs.order_by('-start_date') # order the queryset
|
return qs.order_by('-start_date') # order the queryset
|
||||||
|
|
@ -77,7 +79,9 @@ which is the queryset class the manager should use. Just as with vanilla Django,
|
||||||
you may define your own custom queryset classes. Just use PolymorphicQuerySet
|
you may define your own custom queryset classes. Just use PolymorphicQuerySet
|
||||||
instead of Django's QuerySet as the base class::
|
instead of Django's QuerySet as the base class::
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from polymorphic.manager import PolymorphicManager
|
||||||
|
from polymorphic.query import PolymorphicQuerySet
|
||||||
|
|
||||||
class MyQuerySet(PolymorphicQuerySet):
|
class MyQuerySet(PolymorphicQuerySet):
|
||||||
def my_queryset_method(...):
|
def my_queryset_method(...):
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Making Your Models Polymorphic
|
||||||
|
|
||||||
Use ``PolymorphicModel`` instead of Django's ``models.Model``, like so::
|
Use ``PolymorphicModel`` instead of Django's ``models.Model``, like so::
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
|
|
||||||
class Project(PolymorphicModel):
|
class Project(PolymorphicModel):
|
||||||
topic = models.CharField(max_length=30)
|
topic = models.CharField(max_length=30)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from django.conf.urls import patterns, include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = [
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
url(r'^$', RedirectView.as_view(url=reverse_lazy('admin:index'), permanent=False)),
|
url(r'^$', RedirectView.as_view(url=reverse_lazy('admin:index'), permanent=False)),
|
||||||
)
|
]
|
||||||
|
|
|
||||||
|
|
@ -42,36 +42,34 @@ class ModelAAdmin(PolymorphicParentModelAdmin):
|
||||||
admin.site.register(ModelA, ModelAAdmin)
|
admin.site.register(ModelA, ModelAAdmin)
|
||||||
|
|
||||||
|
|
||||||
if 'Model2A' in globals():
|
class Model2AChildAdmin(PolymorphicChildModelAdmin):
|
||||||
class Model2AChildAdmin(PolymorphicChildModelAdmin):
|
base_model = Model2A
|
||||||
base_model = Model2A
|
|
||||||
|
|
||||||
class Model2AAdmin(PolymorphicParentModelAdmin):
|
class Model2AAdmin(PolymorphicParentModelAdmin):
|
||||||
base_model = Model2A
|
base_model = Model2A
|
||||||
list_filter = (PolymorphicChildModelFilter,)
|
list_filter = (PolymorphicChildModelFilter,)
|
||||||
child_models = (
|
child_models = (
|
||||||
(Model2A, Model2AChildAdmin),
|
(Model2A, Model2AChildAdmin),
|
||||||
(Model2B, Model2AChildAdmin),
|
(Model2B, Model2AChildAdmin),
|
||||||
(Model2C, Model2AChildAdmin),
|
(Model2C, Model2AChildAdmin),
|
||||||
)
|
)
|
||||||
|
|
||||||
admin.site.register(Model2A, Model2AAdmin)
|
admin.site.register(Model2A, Model2AAdmin)
|
||||||
|
|
||||||
|
|
||||||
if 'UUIDModelA' in globals():
|
class UUIDModelAChildAdmin(PolymorphicChildModelAdmin):
|
||||||
class UUIDModelAChildAdmin(PolymorphicChildModelAdmin):
|
base_model = UUIDModelA
|
||||||
base_model = UUIDModelA
|
|
||||||
|
|
||||||
class UUIDModelAAdmin(PolymorphicParentModelAdmin):
|
class UUIDModelAAdmin(PolymorphicParentModelAdmin):
|
||||||
base_model = UUIDModelA
|
base_model = UUIDModelA
|
||||||
list_filter = (PolymorphicChildModelFilter,)
|
list_filter = (PolymorphicChildModelFilter,)
|
||||||
child_models = (
|
child_models = (
|
||||||
(UUIDModelA, UUIDModelAChildAdmin),
|
(UUIDModelA, UUIDModelAChildAdmin),
|
||||||
(UUIDModelB, UUIDModelAChildAdmin),
|
(UUIDModelB, UUIDModelAChildAdmin),
|
||||||
(UUIDModelC, UUIDModelAChildAdmin),
|
(UUIDModelC, UUIDModelAChildAdmin),
|
||||||
)
|
)
|
||||||
|
|
||||||
admin.site.register(UUIDModelA, UUIDModelAAdmin)
|
admin.site.register(UUIDModelA, UUIDModelAAdmin)
|
||||||
|
|
||||||
|
|
||||||
class ProxyChildAdmin(PolymorphicChildModelAdmin):
|
class ProxyChildAdmin(PolymorphicChildModelAdmin):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9b1 on 2015-10-23 22:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import polymorphic.showfields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Model2A',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('field1', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModelA',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('field1', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='nModelA',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('field1', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Project',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('topic', models.CharField(max_length=30)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(polymorphic.showfields.ShowFieldContent, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProxyBase',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=200)),
|
||||||
|
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.proxybase_set+', to='contenttypes.ContentType')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('title',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UUIDModelA',
|
||||||
|
fields=[
|
||||||
|
('uuid_primary_key', models.UUIDField(primary_key=True, serialize=False)),
|
||||||
|
('field1', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ArtProject',
|
||||||
|
fields=[
|
||||||
|
('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Project')),
|
||||||
|
('artist', models.CharField(max_length=30)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.project',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Model2B',
|
||||||
|
fields=[
|
||||||
|
('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Model2A')),
|
||||||
|
('field2', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.model2a',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModelB',
|
||||||
|
fields=[
|
||||||
|
('modela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.ModelA')),
|
||||||
|
('field2', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.modela',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='nModelB',
|
||||||
|
fields=[
|
||||||
|
('nmodela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.nModelA')),
|
||||||
|
('field2', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
bases=('pexp.nmodela',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ResearchProject',
|
||||||
|
fields=[
|
||||||
|
('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Project')),
|
||||||
|
('supervisor', models.CharField(max_length=30)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.project',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UUIDModelB',
|
||||||
|
fields=[
|
||||||
|
('uuidmodela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.UUIDModelA')),
|
||||||
|
('field2', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.uuidmodela',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='uuidmodela',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.uuidmodela_set+', to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.project_set+', to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modela',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.modela_set+', to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='model2a',
|
||||||
|
name='polymorphic_ctype',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.model2a_set+', to='contenttypes.ContentType'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProxyA',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
},
|
||||||
|
bases=('pexp.proxybase',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProxyB',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
},
|
||||||
|
bases=('pexp.proxybase',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Model2C',
|
||||||
|
fields=[
|
||||||
|
('model2b_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Model2B')),
|
||||||
|
('field3', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.model2b',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModelC',
|
||||||
|
fields=[
|
||||||
|
('modelb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.ModelB')),
|
||||||
|
('field3', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.modelb',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='nModelC',
|
||||||
|
fields=[
|
||||||
|
('nmodelb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.nModelB')),
|
||||||
|
('field3', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
bases=('pexp.nmodelb',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UUIDModelC',
|
||||||
|
fields=[
|
||||||
|
('uuidmodelb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.UUIDModelB')),
|
||||||
|
('field3', models.CharField(max_length=10)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('pexp.uuidmodelb',),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9b1 on 2015-10-24 01:42
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pexp', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modelc',
|
||||||
|
name='field4',
|
||||||
|
field=models.ManyToManyField(related_name='related_c', to='pexp.ModelB'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet
|
from polymorphic.models import PolymorphicModel
|
||||||
from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
|
from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
|
||||||
|
|
||||||
class Project(ShowFieldContent, PolymorphicModel):
|
class Project(ShowFieldContent, PolymorphicModel):
|
||||||
|
|
@ -18,6 +19,7 @@ class ModelB(ModelA):
|
||||||
field2 = models.CharField(max_length=10)
|
field2 = models.CharField(max_length=10)
|
||||||
class ModelC(ModelB):
|
class ModelC(ModelB):
|
||||||
field3 = models.CharField(max_length=10)
|
field3 = models.CharField(max_length=10)
|
||||||
|
field4 = models.ManyToManyField(ModelB, related_name='related_c')
|
||||||
|
|
||||||
class nModelA(models.Model):
|
class nModelA(models.Model):
|
||||||
field1 = models.CharField(max_length=10)
|
field1 = models.CharField(max_length=10)
|
||||||
|
|
@ -26,27 +28,25 @@ class nModelB(nModelA):
|
||||||
class nModelC(nModelB):
|
class nModelC(nModelB):
|
||||||
field3 = models.CharField(max_length=10)
|
field3 = models.CharField(max_length=10)
|
||||||
|
|
||||||
# for Django 1.2+, test models with same names in different apps
|
class Model2A(PolymorphicModel):
|
||||||
# (the other models with identical names are in polymorphic/tests.py)
|
field1 = models.CharField(max_length=10)
|
||||||
from django import VERSION as django_VERSION
|
class Model2B(Model2A):
|
||||||
if not (django_VERSION[0]<=1 and django_VERSION[1]<=1):
|
field2 = models.CharField(max_length=10)
|
||||||
class Model2A(PolymorphicModel):
|
class Model2C(Model2B):
|
||||||
field1 = models.CharField(max_length=10)
|
field3 = models.CharField(max_length=10)
|
||||||
class Model2B(Model2A):
|
|
||||||
field2 = models.CharField(max_length=10)
|
|
||||||
class Model2C(Model2B):
|
|
||||||
field3 = models.CharField(max_length=10)
|
|
||||||
|
|
||||||
try: from polymorphic.test_tools import UUIDField
|
if django.VERSION < (1,8):
|
||||||
except: pass
|
from polymorphic.tools_for_tests import UUIDField
|
||||||
if 'UUIDField' in globals():
|
else:
|
||||||
class UUIDModelA(ShowFieldTypeAndContent, PolymorphicModel):
|
from django.db.models import UUIDField
|
||||||
uuid_primary_key = UUIDField(primary_key = True)
|
|
||||||
field1 = models.CharField(max_length=10)
|
class UUIDModelA(ShowFieldTypeAndContent, PolymorphicModel):
|
||||||
class UUIDModelB(UUIDModelA):
|
uuid_primary_key = UUIDField(primary_key = True)
|
||||||
field2 = models.CharField(max_length=10)
|
field1 = models.CharField(max_length=10)
|
||||||
class UUIDModelC(UUIDModelB):
|
class UUIDModelB(UUIDModelA):
|
||||||
field3 = models.CharField(max_length=10)
|
field2 = models.CharField(max_length=10)
|
||||||
|
class UUIDModelC(UUIDModelB):
|
||||||
|
field3 = models.CharField(max_length=10)
|
||||||
|
|
||||||
class ProxyBase(PolymorphicModel):
|
class ProxyBase(PolymorphicModel):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,6 @@ Please see LICENSE and AUTHORS for more information.
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import django
|
import django
|
||||||
from .polymorphic_model import PolymorphicModel
|
|
||||||
from .manager import PolymorphicManager
|
|
||||||
from .query import PolymorphicQuerySet
|
|
||||||
from .query_translate import translate_polymorphic_Q_object
|
|
||||||
from .showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
|
from .showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
|
||||||
from .showfields import ShowFields, ShowFieldTypes, ShowFieldsAndTypes # import old names for compatibility
|
from .showfields import ShowFields, ShowFieldTypes, ShowFieldsAndTypes # import old names for compatibility
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ ModelAdmin code to display polymorphic models.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.helpers import AdminForm, AdminErrorList
|
from django.contrib.admin.helpers import AdminForm, AdminErrorList
|
||||||
from django.contrib.admin.widgets import AdminRadioSelect
|
from django.contrib.admin.widgets import AdminRadioSelect
|
||||||
|
|
@ -18,6 +18,7 @@ from django.utils.encoding import force_text
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
import django
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -268,7 +269,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def change_view(self, request, object_id, *args, **kwargs):
|
def change_view(self, request, object_id, *args, **kwargs):
|
||||||
"""Redirect the change view to the real admin."""
|
"""Redirect the change view to the real admin."""
|
||||||
# between Django 1.3 and 1.4 this method signature differs. Hence the *args, **kwargs
|
|
||||||
real_admin = self._get_real_admin(object_id)
|
real_admin = self._get_real_admin(object_id)
|
||||||
return real_admin.change_view(request, object_id, *args, **kwargs)
|
return real_admin.change_view(request, object_id, *args, **kwargs)
|
||||||
|
|
||||||
|
|
@ -296,17 +296,28 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
||||||
urls = super(PolymorphicParentModelAdmin, self).get_urls()
|
urls = super(PolymorphicParentModelAdmin, self).get_urls()
|
||||||
info = _get_opt(self.model)
|
info = _get_opt(self.model)
|
||||||
|
|
||||||
# Patch the change URL so it's not a big catch-all; allowing all custom URLs to be added to the end.
|
# Patch the change view URL so it's not a big catch-all; allowing all
|
||||||
# The url needs to be recreated, patching url.regex is not an option Django 1.4's LocaleRegexProvider changed it.
|
# custom URLs to be added to the end. This is done by adding '/$' to the
|
||||||
new_change_url = url(r'^{0}/$'.format(self.pk_regex), self.admin_site.admin_view(self.change_view), name='{0}_{1}_change'.format(*info))
|
# end of the regex. The url needs to be recreated, patching url.regex
|
||||||
for i, oldurl in enumerate(urls):
|
# is not an option Django 1.4's LocaleRegexProvider changed it.
|
||||||
if oldurl.name == new_change_url.name:
|
if django.VERSION < (1, 9):
|
||||||
urls[i] = new_change_url
|
# On Django 1.9, the change view URL has been changed from
|
||||||
|
# /<app>/<model>/<pk>/ to /<app>/<model>/<pk>/change/, which is
|
||||||
|
# why we can skip this workaround for Django >= 1.9.
|
||||||
|
new_change_url = url(
|
||||||
|
r'^{0}/$'.format(self.pk_regex),
|
||||||
|
self.admin_site.admin_view(self.change_view),
|
||||||
|
name='{0}_{1}_change'.format(*info)
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, oldurl in enumerate(urls):
|
||||||
|
if oldurl.name == new_change_url.name:
|
||||||
|
urls[i] = new_change_url
|
||||||
|
|
||||||
# Define the catch-all for custom views
|
# Define the catch-all for custom views
|
||||||
custom_urls = patterns('',
|
custom_urls = [
|
||||||
url(r'^(?P<path>.+)$', self.admin_site.admin_view(self.subclass_view))
|
url(r'^(?P<path>.+)$', self.admin_site.admin_view(self.subclass_view))
|
||||||
)
|
]
|
||||||
|
|
||||||
# At this point. all admin code needs to be known.
|
# At this point. all admin code needs to be known.
|
||||||
self._lazy_setup()
|
self._lazy_setup()
|
||||||
|
|
@ -383,7 +394,8 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
||||||
context = {
|
context = {
|
||||||
'title': _('Add %s') % force_text(opts.verbose_name),
|
'title': _('Add %s') % force_text(opts.verbose_name),
|
||||||
'adminform': adminForm,
|
'adminform': adminForm,
|
||||||
'is_popup': "_popup" in request.REQUEST,
|
'is_popup': ("_popup" in request.POST or
|
||||||
|
"_popup" in request.GET),
|
||||||
'media': mark_safe(media),
|
'media': mark_safe(media),
|
||||||
'errors': AdminErrorList(form, ()),
|
'errors': AdminErrorList(form, ()),
|
||||||
'app_label': opts.app_label,
|
'app_label': opts.app_label,
|
||||||
|
|
@ -465,7 +477,7 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin):
|
||||||
kwargs.setdefault('form', self.base_form or self.form)
|
kwargs.setdefault('form', self.base_form or self.form)
|
||||||
|
|
||||||
# prevent infinite recursion in django 1.6+
|
# prevent infinite recursion in django 1.6+
|
||||||
if not self.declared_fieldsets:
|
if not getattr(self, 'declared_fieldsets', None):
|
||||||
kwargs.setdefault('fields', None)
|
kwargs.setdefault('fields', None)
|
||||||
|
|
||||||
return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
|
return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
|
@ -529,7 +541,8 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
# If subclass declares fieldsets, this is respected
|
# If subclass declares fieldsets, this is respected
|
||||||
if self.declared_fieldsets or not self.base_fieldsets:
|
if (hasattr(self, 'declared_fieldset') and self.declared_fieldsets) \
|
||||||
|
or not self.base_fieldsets:
|
||||||
return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj)
|
return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj)
|
||||||
|
|
||||||
# Have a reasonable default fieldsets,
|
# Have a reasonable default fieldsets,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,229 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
IMPORTANT:
|
Seamless Polymorphic Inheritance for Django Models
|
||||||
|
==================================================
|
||||||
|
|
||||||
The models.py module is not used anymore.
|
Please see README.rst and DOCS.rst for further information.
|
||||||
Please use the following import method in your apps:
|
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel, ...
|
Or on the Web:
|
||||||
|
http://chrisglass.github.com/django_polymorphic/
|
||||||
|
http://github.com/chrisglass/django_polymorphic
|
||||||
|
|
||||||
|
Copyright:
|
||||||
|
This code and affiliated files are (C) by Bert Constantin and individual contributors.
|
||||||
|
Please see LICENSE and AUTHORS for more information.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from .base import PolymorphicModelBase
|
||||||
|
from .manager import PolymorphicManager
|
||||||
|
from .query_translate import translate_polymorphic_Q_object
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
### PolymorphicModel
|
||||||
|
|
||||||
|
class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)):
|
||||||
|
"""
|
||||||
|
Abstract base class that provides polymorphic behaviour
|
||||||
|
for any model directly or indirectly derived from it.
|
||||||
|
|
||||||
|
For usage instructions & examples please see documentation.
|
||||||
|
|
||||||
|
PolymorphicModel declares one field for internal use (polymorphic_ctype)
|
||||||
|
and provides a polymorphic manager as the default manager
|
||||||
|
(and as 'objects').
|
||||||
|
|
||||||
|
PolymorphicModel overrides the save() and __init__ methods.
|
||||||
|
|
||||||
|
If your derived class overrides any of these methods as well, then you need
|
||||||
|
to take care that you correctly call the method of the superclass, like:
|
||||||
|
|
||||||
|
super(YourClass,self).save(*args,**kwargs)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing)
|
||||||
|
polymorphic_model_marker = True
|
||||||
|
|
||||||
|
# for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery
|
||||||
|
polymorphic_query_multiline_output = False
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
# avoid ContentType related field accessor clash (an error emitted by model validation)
|
||||||
|
polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False,
|
||||||
|
related_name='polymorphic_%(app_label)s.%(class)s_set+')
|
||||||
|
|
||||||
|
# some applications want to know the name of the fields that are added to its models
|
||||||
|
polymorphic_internal_model_fields = ['polymorphic_ctype']
|
||||||
|
|
||||||
|
# Note that Django 1.5 removes these managers because the model is abstract.
|
||||||
|
# They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers()
|
||||||
|
objects = PolymorphicManager()
|
||||||
|
base_objects = models.Manager()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def translate_polymorphic_Q_object(self_class, q):
|
||||||
|
return translate_polymorphic_Q_object(self_class, q)
|
||||||
|
|
||||||
|
def pre_save_polymorphic(self):
|
||||||
|
"""Normally not needed.
|
||||||
|
This function may be called manually in special use-cases. When the object
|
||||||
|
is saved for the first time, we store its real class in polymorphic_ctype.
|
||||||
|
When the object later is retrieved by PolymorphicQuerySet, it uses this
|
||||||
|
field to figure out the real class of this object
|
||||||
|
(used by PolymorphicQuerySet._get_real_instances)
|
||||||
|
"""
|
||||||
|
if not self.polymorphic_ctype_id:
|
||||||
|
self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False)
|
||||||
|
pre_save_polymorphic.alters_data = True
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""Overridden model save function which supports the polymorphism
|
||||||
|
functionality (through pre_save_polymorphic)."""
|
||||||
|
self.pre_save_polymorphic()
|
||||||
|
return super(PolymorphicModel, self).save(*args, **kwargs)
|
||||||
|
save.alters_data = True
|
||||||
|
|
||||||
|
def get_real_instance_class(self):
|
||||||
|
"""
|
||||||
|
Normally not needed.
|
||||||
|
If a non-polymorphic manager (like base_objects) has been used to
|
||||||
|
retrieve objects, then the real class/type of these objects may be
|
||||||
|
determined using this method.
|
||||||
|
"""
|
||||||
|
# the following line would be the easiest way to do this, but it produces sql queries
|
||||||
|
# return self.polymorphic_ctype.model_class()
|
||||||
|
# so we use the following version, which uses the ContentType manager cache.
|
||||||
|
# Note that model_class() can return None for stale content types;
|
||||||
|
# when the content type record still exists but no longer refers to an existing model.
|
||||||
|
try:
|
||||||
|
model = ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class()
|
||||||
|
except AttributeError:
|
||||||
|
# Django <1.6 workaround
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Protect against bad imports (dumpdata without --natural) or other
|
||||||
|
# issues missing with the ContentType models.
|
||||||
|
if model is not None \
|
||||||
|
and not issubclass(model, self.__class__) \
|
||||||
|
and not issubclass(model, self.__class__._meta.proxy_for_model):
|
||||||
|
raise RuntimeError("ContentType {0} for {1} #{2} does not point to a subclass!".format(
|
||||||
|
self.polymorphic_ctype_id, model, self.pk,
|
||||||
|
))
|
||||||
|
return model
|
||||||
|
|
||||||
|
def get_real_concrete_instance_class_id(self):
|
||||||
|
model_class = self.get_real_instance_class()
|
||||||
|
if model_class is None:
|
||||||
|
return None
|
||||||
|
return ContentType.objects.get_for_model(model_class, for_concrete_model=True).pk
|
||||||
|
|
||||||
|
def get_real_concrete_instance_class(self):
|
||||||
|
model_class = self.get_real_instance_class()
|
||||||
|
if model_class is None:
|
||||||
|
return None
|
||||||
|
return ContentType.objects.get_for_model(model_class, for_concrete_model=True).model_class()
|
||||||
|
|
||||||
|
def get_real_instance(self):
|
||||||
|
"""Normally not needed.
|
||||||
|
If a non-polymorphic manager (like base_objects) has been used to
|
||||||
|
retrieve objects, then the complete object with it's real class/type
|
||||||
|
and all fields may be retrieved with this method.
|
||||||
|
Each method call executes one db query (if necessary)."""
|
||||||
|
real_model = self.get_real_instance_class()
|
||||||
|
if real_model == self.__class__:
|
||||||
|
return self
|
||||||
|
return real_model.objects.get(pk=self.pk)
|
||||||
|
|
||||||
|
def __init__(self, * args, ** kwargs):
|
||||||
|
"""Replace Django's inheritance accessor member functions for our model
|
||||||
|
(self.__class__) with our own versions.
|
||||||
|
We monkey patch them until a patch can be added to Django
|
||||||
|
(which would probably be very small and make all of this obsolete).
|
||||||
|
|
||||||
|
If we have inheritance of the form ModelA -> ModelB ->ModelC then
|
||||||
|
Django creates accessors like this:
|
||||||
|
- ModelA: modelb
|
||||||
|
- ModelB: modela_ptr, modelb, modelc
|
||||||
|
- ModelC: modela_ptr, modelb, modelb_ptr, modelc
|
||||||
|
|
||||||
|
These accessors allow Django (and everyone else) to travel up and down
|
||||||
|
the inheritance tree for the db object at hand.
|
||||||
|
|
||||||
|
The original Django accessors use our polymorphic manager.
|
||||||
|
But they should not. So we replace them with our own accessors that use
|
||||||
|
our appropriate base_objects manager.
|
||||||
|
"""
|
||||||
|
super(PolymorphicModel, self).__init__(*args, ** kwargs)
|
||||||
|
|
||||||
|
if self.__class__.polymorphic_super_sub_accessors_replaced:
|
||||||
|
return
|
||||||
|
self.__class__.polymorphic_super_sub_accessors_replaced = True
|
||||||
|
|
||||||
|
def create_accessor_function_for_model(model, accessor_name):
|
||||||
|
def accessor_function(self):
|
||||||
|
attr = model.base_objects.get(pk=self.pk)
|
||||||
|
return attr
|
||||||
|
return accessor_function
|
||||||
|
|
||||||
|
subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.db.models.fields.related import ReverseOneToOneDescriptor, ForwardManyToOneDescriptor
|
||||||
|
except ImportError:
|
||||||
|
# django < 1.9
|
||||||
|
from django.db.models.fields.related import (
|
||||||
|
SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor,
|
||||||
|
ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor,
|
||||||
|
)
|
||||||
|
for name, model in subclasses_and_superclasses_accessors.items():
|
||||||
|
orig_accessor = getattr(self.__class__, name, None)
|
||||||
|
if type(orig_accessor) in [ReverseOneToOneDescriptor, ForwardManyToOneDescriptor]:
|
||||||
|
#print >>sys.stderr, '---------- replacing', name, orig_accessor, '->', model
|
||||||
|
setattr(self.__class__, name, property(create_accessor_function_for_model(model, name)))
|
||||||
|
|
||||||
|
def _get_inheritance_relation_fields_and_models(self):
|
||||||
|
"""helper function for __init__:
|
||||||
|
determine names of all Django inheritance accessor member functions for type(self)"""
|
||||||
|
|
||||||
|
def add_model(model, field_name, result):
|
||||||
|
result[field_name] = model
|
||||||
|
|
||||||
|
def add_model_if_regular(model, field_name, result):
|
||||||
|
if (issubclass(model, models.Model)
|
||||||
|
and model != models.Model
|
||||||
|
and model != self.__class__
|
||||||
|
and model != PolymorphicModel):
|
||||||
|
add_model(model, field_name, result)
|
||||||
|
|
||||||
|
def add_all_super_models(model, result):
|
||||||
|
for super_cls, field_to_super in model._meta.parents.items():
|
||||||
|
if field_to_super is not None: #if not a link to a proxy model
|
||||||
|
field_name = field_to_super.name #the field on model can have a different name to super_cls._meta.module_name, if the field is created manually using 'parent_link'
|
||||||
|
add_model_if_regular(super_cls, field_name, result)
|
||||||
|
add_all_super_models(super_cls, result)
|
||||||
|
|
||||||
|
def add_all_sub_models(super_cls, result):
|
||||||
|
for sub_cls in super_cls.__subclasses__(): #go through all subclasses of model
|
||||||
|
if super_cls in sub_cls._meta.parents: #super_cls may not be in sub_cls._meta.parents if super_cls is a proxy model
|
||||||
|
field_to_super = sub_cls._meta.parents[super_cls] #get the field that links sub_cls to super_cls
|
||||||
|
if field_to_super is not None: # if filed_to_super is not a link to a proxy model
|
||||||
|
super_to_sub_related_field = field_to_super.rel
|
||||||
|
if super_to_sub_related_field.related_name is None:
|
||||||
|
#if related name is None the related field is the name of the subclass
|
||||||
|
to_subclass_fieldname = sub_cls.__name__.lower()
|
||||||
|
else:
|
||||||
|
#otherwise use the given related name
|
||||||
|
to_subclass_fieldname = super_to_sub_related_field.related_name
|
||||||
|
|
||||||
|
add_model_if_regular(sub_cls, to_subclass_fieldname, result)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
add_all_super_models(self.__class__, result)
|
||||||
|
add_all_sub_models(self.__class__, result)
|
||||||
|
return result
|
||||||
|
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Seamless Polymorphic Inheritance for Django Models
|
|
||||||
==================================================
|
|
||||||
|
|
||||||
Please see README.rst and DOCS.rst for further information.
|
|
||||||
|
|
||||||
Or on the Web:
|
|
||||||
http://chrisglass.github.com/django_polymorphic/
|
|
||||||
http://github.com/chrisglass/django_polymorphic
|
|
||||||
|
|
||||||
Copyright:
|
|
||||||
This code and affiliated files are (C) by Bert Constantin and individual contributors.
|
|
||||||
Please see LICENSE and AUTHORS for more information.
|
|
||||||
"""
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
from .base import PolymorphicModelBase
|
|
||||||
from .manager import PolymorphicManager
|
|
||||||
from .query_translate import translate_polymorphic_Q_object
|
|
||||||
|
|
||||||
###################################################################################
|
|
||||||
### PolymorphicModel
|
|
||||||
|
|
||||||
class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)):
|
|
||||||
"""
|
|
||||||
Abstract base class that provides polymorphic behaviour
|
|
||||||
for any model directly or indirectly derived from it.
|
|
||||||
|
|
||||||
For usage instructions & examples please see documentation.
|
|
||||||
|
|
||||||
PolymorphicModel declares one field for internal use (polymorphic_ctype)
|
|
||||||
and provides a polymorphic manager as the default manager
|
|
||||||
(and as 'objects').
|
|
||||||
|
|
||||||
PolymorphicModel overrides the save() and __init__ methods.
|
|
||||||
|
|
||||||
If your derived class overrides any of these methods as well, then you need
|
|
||||||
to take care that you correctly call the method of the superclass, like:
|
|
||||||
|
|
||||||
super(YourClass,self).save(*args,**kwargs)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing)
|
|
||||||
polymorphic_model_marker = True
|
|
||||||
|
|
||||||
# for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery
|
|
||||||
polymorphic_query_multiline_output = False
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
# avoid ContentType related field accessor clash (an error emitted by model validation)
|
|
||||||
polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False,
|
|
||||||
related_name='polymorphic_%(app_label)s.%(class)s_set+')
|
|
||||||
|
|
||||||
# some applications want to know the name of the fields that are added to its models
|
|
||||||
polymorphic_internal_model_fields = ['polymorphic_ctype']
|
|
||||||
|
|
||||||
# Note that Django 1.5 removes these managers because the model is abstract.
|
|
||||||
# They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers()
|
|
||||||
objects = PolymorphicManager()
|
|
||||||
base_objects = models.Manager()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def translate_polymorphic_Q_object(self_class, q):
|
|
||||||
return translate_polymorphic_Q_object(self_class, q)
|
|
||||||
|
|
||||||
def pre_save_polymorphic(self):
|
|
||||||
"""Normally not needed.
|
|
||||||
This function may be called manually in special use-cases. When the object
|
|
||||||
is saved for the first time, we store its real class in polymorphic_ctype.
|
|
||||||
When the object later is retrieved by PolymorphicQuerySet, it uses this
|
|
||||||
field to figure out the real class of this object
|
|
||||||
(used by PolymorphicQuerySet._get_real_instances)
|
|
||||||
"""
|
|
||||||
if not self.polymorphic_ctype_id:
|
|
||||||
self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False)
|
|
||||||
pre_save_polymorphic.alters_data = True
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""Overridden model save function which supports the polymorphism
|
|
||||||
functionality (through pre_save_polymorphic)."""
|
|
||||||
self.pre_save_polymorphic()
|
|
||||||
return super(PolymorphicModel, self).save(*args, **kwargs)
|
|
||||||
save.alters_data = True
|
|
||||||
|
|
||||||
def get_real_instance_class(self):
|
|
||||||
"""
|
|
||||||
Normally not needed.
|
|
||||||
If a non-polymorphic manager (like base_objects) has been used to
|
|
||||||
retrieve objects, then the real class/type of these objects may be
|
|
||||||
determined using this method.
|
|
||||||
"""
|
|
||||||
# the following line would be the easiest way to do this, but it produces sql queries
|
|
||||||
# return self.polymorphic_ctype.model_class()
|
|
||||||
# so we use the following version, which uses the ContentType manager cache.
|
|
||||||
# Note that model_class() can return None for stale content types;
|
|
||||||
# when the content type record still exists but no longer refers to an existing model.
|
|
||||||
try:
|
|
||||||
model = ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class()
|
|
||||||
except AttributeError:
|
|
||||||
# Django <1.6 workaround
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Protect against bad imports (dumpdata without --natural) or other
|
|
||||||
# issues missing with the ContentType models.
|
|
||||||
if model is not None \
|
|
||||||
and not issubclass(model, self.__class__) \
|
|
||||||
and not issubclass(model, self.__class__._meta.proxy_for_model):
|
|
||||||
raise RuntimeError("ContentType {0} for {1} #{2} does not point to a subclass!".format(
|
|
||||||
self.polymorphic_ctype_id, model, self.pk,
|
|
||||||
))
|
|
||||||
return model
|
|
||||||
|
|
||||||
def get_real_concrete_instance_class_id(self):
|
|
||||||
model_class = self.get_real_instance_class()
|
|
||||||
if model_class is None:
|
|
||||||
return None
|
|
||||||
return ContentType.objects.get_for_model(model_class, for_concrete_model=True).pk
|
|
||||||
|
|
||||||
def get_real_concrete_instance_class(self):
|
|
||||||
model_class = self.get_real_instance_class()
|
|
||||||
if model_class is None:
|
|
||||||
return None
|
|
||||||
return ContentType.objects.get_for_model(model_class, for_concrete_model=True).model_class()
|
|
||||||
|
|
||||||
def get_real_instance(self):
|
|
||||||
"""Normally not needed.
|
|
||||||
If a non-polymorphic manager (like base_objects) has been used to
|
|
||||||
retrieve objects, then the complete object with it's real class/type
|
|
||||||
and all fields may be retrieved with this method.
|
|
||||||
Each method call executes one db query (if necessary)."""
|
|
||||||
real_model = self.get_real_instance_class()
|
|
||||||
if real_model == self.__class__:
|
|
||||||
return self
|
|
||||||
return real_model.objects.get(pk=self.pk)
|
|
||||||
|
|
||||||
def __init__(self, * args, ** kwargs):
|
|
||||||
"""Replace Django's inheritance accessor member functions for our model
|
|
||||||
(self.__class__) with our own versions.
|
|
||||||
We monkey patch them until a patch can be added to Django
|
|
||||||
(which would probably be very small and make all of this obsolete).
|
|
||||||
|
|
||||||
If we have inheritance of the form ModelA -> ModelB ->ModelC then
|
|
||||||
Django creates accessors like this:
|
|
||||||
- ModelA: modelb
|
|
||||||
- ModelB: modela_ptr, modelb, modelc
|
|
||||||
- ModelC: modela_ptr, modelb, modelb_ptr, modelc
|
|
||||||
|
|
||||||
These accessors allow Django (and everyone else) to travel up and down
|
|
||||||
the inheritance tree for the db object at hand.
|
|
||||||
|
|
||||||
The original Django accessors use our polymorphic manager.
|
|
||||||
But they should not. So we replace them with our own accessors that use
|
|
||||||
our appropriate base_objects manager.
|
|
||||||
"""
|
|
||||||
super(PolymorphicModel, self).__init__(*args, ** kwargs)
|
|
||||||
|
|
||||||
if self.__class__.polymorphic_super_sub_accessors_replaced:
|
|
||||||
return
|
|
||||||
self.__class__.polymorphic_super_sub_accessors_replaced = True
|
|
||||||
|
|
||||||
def create_accessor_function_for_model(model, accessor_name):
|
|
||||||
def accessor_function(self):
|
|
||||||
attr = model.base_objects.get(pk=self.pk)
|
|
||||||
return attr
|
|
||||||
return accessor_function
|
|
||||||
|
|
||||||
subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models()
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django.db.models.fields.related import ReverseOneToOneDescriptor, ForwardManyToOneDescriptor
|
|
||||||
except ImportError:
|
|
||||||
# django < 1.9
|
|
||||||
from django.db.models.fields.related import (
|
|
||||||
SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor,
|
|
||||||
ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor,
|
|
||||||
)
|
|
||||||
for name, model in subclasses_and_superclasses_accessors.items():
|
|
||||||
orig_accessor = getattr(self.__class__, name, None)
|
|
||||||
if type(orig_accessor) in [ReverseOneToOneDescriptor, ForwardManyToOneDescriptor]:
|
|
||||||
#print >>sys.stderr, '---------- replacing', name, orig_accessor, '->', model
|
|
||||||
setattr(self.__class__, name, property(create_accessor_function_for_model(model, name)))
|
|
||||||
|
|
||||||
def _get_inheritance_relation_fields_and_models(self):
|
|
||||||
"""helper function for __init__:
|
|
||||||
determine names of all Django inheritance accessor member functions for type(self)"""
|
|
||||||
|
|
||||||
def add_model(model, field_name, result):
|
|
||||||
result[field_name] = model
|
|
||||||
|
|
||||||
def add_model_if_regular(model, field_name, result):
|
|
||||||
if (issubclass(model, models.Model)
|
|
||||||
and model != models.Model
|
|
||||||
and model != self.__class__
|
|
||||||
and model != PolymorphicModel):
|
|
||||||
add_model(model, field_name, result)
|
|
||||||
|
|
||||||
def add_all_super_models(model, result):
|
|
||||||
for super_cls, field_to_super in model._meta.parents.items():
|
|
||||||
if field_to_super is not None: #if not a link to a proxy model
|
|
||||||
field_name = field_to_super.name #the field on model can have a different name to super_cls._meta.module_name, if the field is created manually using 'parent_link'
|
|
||||||
add_model_if_regular(super_cls, field_name, result)
|
|
||||||
add_all_super_models(super_cls, result)
|
|
||||||
|
|
||||||
def add_all_sub_models(super_cls, result):
|
|
||||||
for sub_cls in super_cls.__subclasses__(): #go through all subclasses of model
|
|
||||||
if super_cls in sub_cls._meta.parents: #super_cls may not be in sub_cls._meta.parents if super_cls is a proxy model
|
|
||||||
field_to_super = sub_cls._meta.parents[super_cls] #get the field that links sub_cls to super_cls
|
|
||||||
if field_to_super is not None: # if filed_to_super is not a link to a proxy model
|
|
||||||
super_to_sub_related_field = field_to_super.rel
|
|
||||||
if super_to_sub_related_field.related_name is None:
|
|
||||||
#if related name is None the related field is the name of the subclass
|
|
||||||
to_subclass_fieldname = sub_cls.__name__.lower()
|
|
||||||
else:
|
|
||||||
#otherwise use the given related name
|
|
||||||
to_subclass_fieldname = super_to_sub_related_field.related_name
|
|
||||||
|
|
||||||
add_model_if_regular(sub_cls, to_subclass_fieldname, result)
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
add_all_super_models(self.__class__, result)
|
|
||||||
add_all_sub_models(self.__class__, result)
|
|
||||||
return result
|
|
||||||
|
|
@ -43,6 +43,14 @@ def transmogrify(cls, obj):
|
||||||
###################################################################################
|
###################################################################################
|
||||||
### PolymorphicQuerySet
|
### PolymorphicQuerySet
|
||||||
|
|
||||||
|
def _query_annotations(query):
|
||||||
|
try:
|
||||||
|
return query.annotations
|
||||||
|
except AttributeError:
|
||||||
|
# Django < 1.8
|
||||||
|
return query.aggregates
|
||||||
|
|
||||||
|
|
||||||
class PolymorphicQuerySet(QuerySet):
|
class PolymorphicQuerySet(QuerySet):
|
||||||
"""
|
"""
|
||||||
QuerySet for PolymorphicModel
|
QuerySet for PolymorphicModel
|
||||||
|
|
@ -137,7 +145,18 @@ class PolymorphicQuerySet(QuerySet):
|
||||||
qs = self.non_polymorphic()
|
qs = self.non_polymorphic()
|
||||||
return super(PolymorphicQuerySet, qs).aggregate(*args, **kwargs)
|
return super(PolymorphicQuerySet, qs).aggregate(*args, **kwargs)
|
||||||
|
|
||||||
# Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.^
|
if django.VERSION >= (1, 9):
|
||||||
|
# On Django < 1.9, 'qs.values(...)' returned a new special ValuesQuerySet
|
||||||
|
# object, which our polymorphic modifications didn't apply to.
|
||||||
|
# Starting with Django 1.9, the copy returned by 'qs.values(...)' has the
|
||||||
|
# same class as 'qs', so our polymorphic modifications would apply.
|
||||||
|
# We want to leave values queries untouched, so we set 'polymorphic_disabled'.
|
||||||
|
def _values(self, *args, **kwargs):
|
||||||
|
clone = super(PolymorphicQuerySet, self)._values(*args, **kwargs)
|
||||||
|
clone.polymorphic_disabled = True
|
||||||
|
return clone
|
||||||
|
|
||||||
|
# Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.
|
||||||
# The resulting objects are required to have a unique primary key within the result set
|
# The resulting objects are required to have a unique primary key within the result set
|
||||||
# (otherwise an error is thrown).
|
# (otherwise an error is thrown).
|
||||||
# The "polymorphic" keyword argument is not supported anymore.
|
# The "polymorphic" keyword argument is not supported anymore.
|
||||||
|
|
@ -197,7 +216,7 @@ class PolymorphicQuerySet(QuerySet):
|
||||||
for base_object in base_result_objects:
|
for base_object in base_result_objects:
|
||||||
ordered_id_list.append(base_object.pk)
|
ordered_id_list.append(base_object.pk)
|
||||||
|
|
||||||
# check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...)
|
# check if id of the result object occurres more than once - this can happen e.g. with base_objects.extra(tables=...)
|
||||||
if not base_object.pk in base_result_objects_by_id:
|
if not base_object.pk in base_result_objects_by_id:
|
||||||
base_result_objects_by_id[base_object.pk] = base_object
|
base_result_objects_by_id[base_object.pk] = base_object
|
||||||
|
|
||||||
|
|
@ -239,8 +258,8 @@ class PolymorphicQuerySet(QuerySet):
|
||||||
if real_class != real_concrete_class:
|
if real_class != real_concrete_class:
|
||||||
real_object = transmogrify(real_class, real_object)
|
real_object = transmogrify(real_class, real_object)
|
||||||
|
|
||||||
if self.query.aggregates:
|
if _query_annotations(self.query):
|
||||||
for anno_field_name in six.iterkeys(self.query.aggregates):
|
for anno_field_name in six.iterkeys(_query_annotations(self.query)):
|
||||||
attr = getattr(base_result_objects_by_id[o_pk], anno_field_name)
|
attr = getattr(base_result_objects_by_id[o_pk], anno_field_name)
|
||||||
setattr(real_object, anno_field_name, attr)
|
setattr(real_object, anno_field_name, attr)
|
||||||
|
|
||||||
|
|
@ -255,8 +274,8 @@ class PolymorphicQuerySet(QuerySet):
|
||||||
resultlist = [results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results]
|
resultlist = [results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results]
|
||||||
|
|
||||||
# set polymorphic_annotate_names in all objects (currently just used for debugging/printing)
|
# set polymorphic_annotate_names in all objects (currently just used for debugging/printing)
|
||||||
if self.query.aggregates:
|
if _query_annotations(self.query):
|
||||||
annotate_names = list(six.iterkeys(self.query.aggregates)) # get annotate field list
|
annotate_names = list(six.iterkeys(_query_annotations(self.query))) # get annotate field list
|
||||||
for real_object in resultlist:
|
for real_object in resultlist:
|
||||||
real_object.polymorphic_annotate_names = annotate_names
|
real_object.polymorphic_annotate_names = annotate_names
|
||||||
|
|
||||||
|
|
@ -289,7 +308,7 @@ class PolymorphicQuerySet(QuerySet):
|
||||||
if self.polymorphic_disabled:
|
if self.polymorphic_disabled:
|
||||||
for o in base_iter:
|
for o in base_iter:
|
||||||
yield o
|
yield o
|
||||||
raise StopIteration
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
base_result_objects = []
|
base_result_objects = []
|
||||||
|
|
@ -309,7 +328,7 @@ class PolymorphicQuerySet(QuerySet):
|
||||||
yield o
|
yield o
|
||||||
|
|
||||||
if reached_end:
|
if reached_end:
|
||||||
raise StopIteration
|
return
|
||||||
|
|
||||||
def __repr__(self, *args, **kwargs):
|
def __repr__(self, *args, **kwargs):
|
||||||
if self.model.polymorphic_query_multiline_output:
|
if self.model.polymorphic_query_multiline_output:
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ def translate_polymorphic_field_path(queryset_model, field_path):
|
||||||
# so no tripple ClassName___field was intended.
|
# so no tripple ClassName___field was intended.
|
||||||
try:
|
try:
|
||||||
# rel = (field_object, model, direct, m2m)
|
# rel = (field_object, model, direct, m2m)
|
||||||
field = queryset_model._meta.get_field_by_name(classname)[0]
|
field = queryset_model._meta.get_field(classname)
|
||||||
if isinstance(field, RelatedObject):
|
if isinstance(field, RelatedObject):
|
||||||
# Can also test whether the field exists in the related object to avoid ambiguity between
|
# Can also test whether the field exists in the related object to avoid ambiguity between
|
||||||
# class names and field names, but that never happens when your class names are in CamelCase.
|
# class names and field names, but that never happens when your class names are in CamelCase.
|
||||||
|
|
@ -234,7 +234,7 @@ def _create_model_filter_Q(modellist, not_instance_of=False):
|
||||||
if not modellist:
|
if not modellist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from .polymorphic_model import PolymorphicModel
|
from .models import PolymorphicModel
|
||||||
|
|
||||||
if type(modellist) != list and type(modellist) != tuple:
|
if type(modellist) != list and type(modellist) != tuple:
|
||||||
if issubclass(modellist, PolymorphicModel):
|
if issubclass(modellist, PolymorphicModel):
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ from __future__ import print_function
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
import re
|
||||||
import django
|
import django
|
||||||
from django.utils.unittest import skipIf
|
try:
|
||||||
|
from unittest import skipIf
|
||||||
|
except ImportError:
|
||||||
|
# python<2.7
|
||||||
|
from django.utils.unittest import skipIf
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
@ -15,9 +19,15 @@ from django.db import models
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from polymorphic.manager import PolymorphicManager
|
||||||
|
from polymorphic.query import PolymorphicQuerySet
|
||||||
from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
|
from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
|
||||||
from polymorphic.tools_for_tests import UUIDField
|
try:
|
||||||
|
from django.db.models import UUIDField
|
||||||
|
except ImportError:
|
||||||
|
# django<1.8
|
||||||
|
from polymorphic.tools_for_tests import UUIDField
|
||||||
|
|
||||||
|
|
||||||
class PlainA(models.Model):
|
class PlainA(models.Model):
|
||||||
|
|
@ -206,15 +216,15 @@ class Bottom(Middle):
|
||||||
author = models.CharField(max_length=50)
|
author = models.CharField(max_length=50)
|
||||||
|
|
||||||
class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel):
|
class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel):
|
||||||
uuid_primary_key = UUIDField(primary_key = True)
|
uuid_primary_key = UUIDField(primary_key = True, default=uuid.uuid1)
|
||||||
topic = models.CharField(max_length = 30)
|
topic = models.CharField(max_length = 30)
|
||||||
class UUIDArtProject(UUIDProject):
|
class UUIDArtProject(UUIDProject):
|
||||||
artist = models.CharField(max_length = 30)
|
artist = models.CharField(max_length = 30)
|
||||||
class UUIDResearchProject(UUIDProject):
|
class UUIDResearchProject(UUIDProject):
|
||||||
supervisor = models.CharField(max_length = 30)
|
supervisor = models.CharField(max_length = 30)
|
||||||
|
|
||||||
class UUIDPlainA(models.Model):
|
class UUIDPlainA(models.Model):
|
||||||
uuid_primary_key = UUIDField(primary_key = True)
|
uuid_primary_key = UUIDField(primary_key = True, default=uuid.uuid1)
|
||||||
field1 = models.CharField(max_length=10)
|
field1 = models.CharField(max_length=10)
|
||||||
class UUIDPlainB(UUIDPlainA):
|
class UUIDPlainB(UUIDPlainA):
|
||||||
field2 = models.CharField(max_length=10)
|
field2 = models.CharField(max_length=10)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# Compatibility module for Django < 1.8
|
||||||
|
|
||||||
####################################################################
|
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
@ -9,6 +6,7 @@ from django.db import models
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class UUIDVersionError(Exception):
|
class UUIDVersionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,12 @@ if not settings.configured:
|
||||||
TEMPLATE_LOADERS = (
|
TEMPLATE_LOADERS = (
|
||||||
'django.template.loaders.app_directories.Loader',
|
'django.template.loaders.app_directories.Loader',
|
||||||
),
|
),
|
||||||
TEMPLATE_CONTEXT_PROCESSORS = default_settings.TEMPLATE_CONTEXT_PROCESSORS + (
|
TEMPLATE_CONTEXT_PROCESSORS=(
|
||||||
'django.core.context_processors.request',
|
# list() is only needed for older versions of django where this is
|
||||||
|
# a tuple:
|
||||||
|
list(default_settings.TEMPLATE_CONTEXT_PROCESSORS) + [
|
||||||
|
'django.core.context_processors.request',
|
||||||
|
]
|
||||||
),
|
),
|
||||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner' if django.VERSION >= (1,7) else 'django.test.simple.DjangoTestSuiteRunner',
|
TEST_RUNNER = 'django.test.runner.DiscoverRunner' if django.VERSION >= (1,7) else 'django.test.simple.DjangoTestSuiteRunner',
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue