I am using wagtails wagtail-generic-chooser to create customChoosers for my data models and it is working great whenever I am referencing other modelAdmin models.
However, I have come across a situation where I have a Lexis model with a field that has a FK link to itself. The idea is to have a Lexis term, and then there can be related lexis terms connected to it. It works fine with a normal FieldPanel but this isn't a very good UI experience when there are hundreds of lexis terms. Accordingly, I wanted to create a custom LexisChooser for this field. However, the issue I've run into is according to the documentation in order to create a functional widget, I am required to create both a view and adminChooser that references the model the ChooserPanel is connected to.
https://github.com/wagtail/wagtail-generic-chooser#chooser-widgets-model-based
This makes sense, however, when I then try to import my LexisChooser into my Lexis model to use the LexisChooser as a widget, I get the error below.
ImportError: cannot import name 'Lexis' from 'lexis.models'
I realize this is due to a circular import error issue because I have subclasses that are importing the Lexis Class in order to build the LexisChooser widget and then I am trying to import that widget into the Lexis Class.
I know this isn't a bug with Wagtail nor is it a problem with wagtail-generic-chooser, However, does anyone have any idea how I can refactor the code to make this function so that I can use a LexisChooser on a field of the Lexis Model.
Below is my code.
views.py create a view
from django.utils.translation import ugettext_lazy as _
from generic_chooser.views import ModelChooserViewSet
from lexis.models import Lexis
class LexisChooserViewSet(ModelChooserViewSet):
icon = 'user'
model = Lexis
page_title = _("Choose A Lexis Term")
per_page = 20
order_by = 'term'
fields = ['term']
wagtail_hooks.py register view
from wagtail.core import hooks
from .views import LexisChooserViewSet
#hooks.register('register_admin_viewset')
def register_lexis_chooser_viewset():
return LexisChooserViewSet('lexis_chooser', url_prefix='lexis-chooser')
widgets.py create a widget
from django.utils.translation import ugettext_lazy as _
from generic_chooser.widgets import AdminChooser
from lexis.models import Lexis
class LexisChooser(AdminChooser):
choose_one_text = _('Choose a Lexis')
choose_another_text = _('Choose another Lexis')
link_to_chosen_text = _('Edit this Lexis')
model = Lexis
choose_modal_url_name = 'lexis_chooser:choose'
lexis/models.py use widget
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
from wagtail.core.models import Orderable
from modelcluster.models import ClusterableModel
from chooser_panels.widgets import LexisChooser
# Orderable link to multiple other linked lexis terms
class LexisLink(Orderable):
page = ParentalKey("lexis.Lexis", related_name="lexis_link")
term_link = models.ForeignKey(
'lexis.Lexis',
on_delete=models.SET_NULL,
related_name='term_linked',
null=True
)
panels = [
FieldPanel("term_link", widget=LexisChooser)
]
class Lexis(ClusterableModel):
template = "lexis/lexis_page.html"
term = models.CharField(max_length=100, blank=True, null=True)
panels = [
FieldPanel("term"),
InlinePanel('lexis_link', label='Linked Lexis Terms'),
]
def __str__(self):
return self.term
class Meta:
verbose_name = "Lexis"
verbose_name_plural = "Lexis"
This, unfortunately, results in a circular import error:
ImportError: cannot import name 'Lexis' from 'lexis.models'
On researching this error, I found that people recommend importing Lexis within the class as required rather than at the top of each file, but that doesn't seem to work with the subclassing as outlined above, because I get the same error.
If you have any ideas on how I can refactor the code to make it work and not create the circular import error it would be much appreciated.
I am running
Django 3,
python 3.7,
wagtail 2.8
Thank you
Split your models file into two separate files containing Lexis and LexisLink, as demonstrated in the documentation.
Then LexisLink can refer to LexisChooser while it cleanly refers to the Lexis model.
Related
I have read some relevant posts on the issue on mutual imports (circular). I am working on a django app, here's how I run to an issue:
I have two apps, first one articles, second one tags
my article model have a many to many field, to indicate relevant tags:
articles/models.py
from django.db import models
from tags.models import Tag
class Article(models.Model):
tags = models.ManyToManyField(Tag)
however, in my tags app, I also need to import Article model to achieve the methods:
tags/models.py
from django.db import models
from articles.models import Article
# Create your models here.
class Tag(models.Model):
title = models.CharField(max_length=100)
content = models.CharField(max_length=255)
def getAritcleLength():
pass
def getQuestionLength():
pass
I usually use a module to combine those class definitions, normally won't run into issue according to method resolution order. However, in django the workflow we need to put classes into separated folders, I will be so glad for any suggestion.
Don't import the Tag model in the Article model, but use the string reference of the class instead.
# articles/models.py
from django.db import models
class Article(models.Model):
tags = models.ManyToManyField('Tag') OR: you can use app_name.model_name format as well
Another way is to use Django's method to get the model from a string and then use that variable in place for the model reference.
Django: Get model from string?
Try to delete this string
from articles.models import Article
Here are my simplified models :
from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation)
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Thing(models.Model):
'''
Our 'Thing' class
with a link (generic relationship) to an abstract config
'''
name = models.CharField(
max_length=128, blank=True,
verbose_name=_(u'Name of my thing'))
# Link to our configs
config_content_type = models.ForeignKey(
ContentType,
null=True,
blank=True)
config_object_id = models.PositiveIntegerField(
null=True,
blank=True)
config_object = GenericForeignKey(
'config_content_type',
'config_object_id')
class Config(models.Model):
'''
Base class for custom Configs
'''
class Meta:
abstract = True
name = models.CharField(
max_length=128, blank=True,
verbose_name=_(u'Config Name'))
thing = GenericRelation(
Thing,
related_query_name='config')
class FirstConfig(Config):
pass
class SecondConfig(Config):
pass
And Here's the admin:
from django.contrib import admin
from .models import FirstConfig, SecondConfig, Thing
class FirstConfigInline(admin.StackedInline):
model = FirstConfig
class SecondConfigInline(admin.StackedInline):
model = SecondConfig
class ThingAdmin(admin.ModelAdmin):
model = Thing
def get_inline_instances(self, request, obj=None):
'''
Returns our Thing Config inline
'''
if obj is not None:
m_name = obj.config_object._meta.model_name
if m_name == "firstconfig":
return [FirstConfigInline(self.model, self.admin_site), ]
elif m_name == "secondconfig":
return [SecondConfigInline(self.model, self.admin_site), ]
return []
admin.site.register(Thing, ThingAdmin)
So far, I've a Thing object with a FirstConfig object linked together.
The code is simplified: in an unrelevant part I manage to create my abstract Config at a Thing creation and set the right content_type / object_id.
Now I'd like to see this FirstConfig instance as an inline (FirstConfigInline) in my ThingAdmin.
I tried with the django.contrib.contenttypes.admin.GenericStackedInline, though it does not work with my current models setup.
I tried to play around with the fk_name parameter of my FirstConfigInline.
Also, as you can see, I tried to play around with the 'thing' GenericRelation attribute on my Config Model, without success..
Any idea on how to proceed to correctly setup the admin?
According to the Django Docs you have to define the ct_fk_field and the ct_field if they were changed from the default values. So it may be enough to set ct_field to config_content_type.
Hope it works!
edit: Those values have to be declared in the Inline:
class SecondConfigInline(admin.StackedInline):
model = SecondConfig
ct_fk_field = "config_object_id"
ct_field = "config_content_type"
edit2:
I just realized an error in my assumption. Usually you should declare the Foreignkey on the Inline-model. Depending on the rest of your code you could just remove the generic Foreignkey on Thing+the genericRelation on Config and declare a normal Foreignkey on the Config-Basemodel.
This question is old, but I'll give it a try anyway.
I think the solution depends on what kind of relation you intend to create between Thing and your Config subclasses.
many-to-one/one-to-many
The way it is currently set up, it looks like a many-to-one relation: each Thing points to a single Config subclass, and many Things can point to the same Config subclass. Due to the generic relation, each Thing can point to a different model (not necessarily a Config subclass, unless you do some extra work).
In this case I guess it would make more sense to put the inline on the admin for the Config. That is, create a GenericStackedInline for Thing (which has the GenericForeignkey), and add the inline to a ConfigAdmin, which you can then use for all Config subclasses. Also see the example below. The generic inline will then automatically set the correct content_type and object_id.
many-to-many
On the other hand, if you are looking for a many-to-many relation between Thing and each Config subclass, then I would move the GenericForeignkey into a separate many-to-many table (lets call it ThingConfigRelation).
A bit of code says more than a thousand words, so let's split up your Thing class as follows:
class Thing(models.Model):
name = models.CharField(max_length=128)
class ThingConfigRelation(models.Model):
thing = models.ForeignKey(to=Thing, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, null=True, blank=True,
on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True, blank=True)
config_object = GenericForeignKey(ct_field='content_type',
fk_field='object_id')
Now it does make sense to add an inline to the ThingAdmin. The following is a bare-bones example of an admin that works for both sides of the relation:
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericStackedInline
from .models import Thing, FirstConfig, SecondConfig, ThingConfigRelation
class ConventionalTCRInline(admin.StackedInline):
model = ThingConfigRelation
extra = 0
class GenericTCRInline(GenericStackedInline):
model = ThingConfigRelation
extra = 0
class ThingAdmin(admin.ModelAdmin):
inlines = [ConventionalTCRInline]
class ConfigAdmin(admin.ModelAdmin):
inlines = [GenericTCRInline]
admin.site.register(Thing, ThingAdmin)
admin.site.register(FirstConfig, ConfigAdmin)
admin.site.register(SecondConfig, ConfigAdmin)
Note that we use the conventional inline for the ForeignKey-side of the relation (i.e. in ThingAdmin), and we use the generic inline for the GenericForeignKey-side (in ConfigAdmin).
A tricky bit would be filtering the content_type and object_id fields on the ThingAdmin.
... something completely different:
Another option might be to get rid of the GenericForeignKey altogether and use some kind of single-table inheritance implementation with plain old ForeignKeys instead, a bit like this.
I'm trying to import a model from another Django app in my project. However, when I try to import, I keep getting an error for:
ImportError No module named trunk.profiles.models.
However, when I cmnd click on the model on my IDE it takes me to the model. So it recognizes where the model is coming from, but I think for some reason Django is not recognizing the path.
Here is my code from my models.py which I'm trying to import another model, Profiles from a different Django app:
from django.db import models
from trunk.profiles.models import Profiles # source of error
class ContentObject(models.Model):
course_name = models.CharField(max_length15)
course_topic = models.CharField(max_length = 30)
op_UserName = models.ForeignKey(Profiles)
Add trunk.profiles to Your INSTALLED_APPS
settings.py
INSTALLED_APPS = [
...
'trunk.profiles'
]
TIP
Instead of import model, specify a model with the full application label
from django.db import models
class ContentObject(models.Model):
course_name = models.CharField(max_length15)
course_topic = models.CharField(max_length = 30)
op_UserName = models.ForeignKey('trunk.profiles.Profiles')
I'm trying to extend default Django's model with a new field.
In localsite/models.py I have the following code:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from satchmo_store.contact.models import Organization
class OrganizationExtra(models.Model):
organization = models.OneToOneField(Organization,
verbose_name=_('Organization'), primary_key=True )
vat_number = models.CharField(_('VAT'), max_length=12)
Followed with run of ./manage.py syncdb which did created a new table for above model. So far so good.
Now I'm trying to add this new field in related Organization view in the admin interface.
The following code registers the new menu, however the new vat_number field is not displayed in view of the related Organization model.
from django.contrib import admin
from localsite.models import ProductResource, OrganizationExtra
admin.site.register(OrganizationExtra)
The original Organization model is registered with
from satchmo_store.contact.models import Organization
from django.contrib import admin
class OrganizationOptions(admin.ModelAdmin):
list_filter = ['type', 'role']
list_display = ['name', 'type', 'role']
admin.site.register(Organization, OrganizationOptions)
Any idea how to insert my new field without touching original Satchmo sources ?
See the docs as usual.
One possible way is to create new MyOrganization derived from Organization and register it in place of satchmo one
Your models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _
from satchmo_store.contact.models import Organization
class MyOrganization(Organization):
vat_number = models.CharField(_('VAT'), max_length=12)
Your admin.py
from django.contrib import admin
from localsite.models import MyOrganization
from satchmo_store.contact.models import Organization
from satchmo_store.contact.admin import OrganizationOptions
admin.site.unregister(Organization)
admin.site.register(MyOrganization, OrganizationOptions)
Another possible solution (if you wish to stick with OrganizationExtra) is to create custom form for Organization for admin interface and again reregister model. By it seems to me as more boilerplate and result will be the same.
NB: in both cases DB structure would be the same, i.e. extra table would be created.
What is the best approach to extending the Site model in django? Creating a new model and ForeignKey the Site or there another approach that allows me to subclass the Site model?
I prefer subclassing, because relationally I'm more comfortable, but I'm concerned for the impact it will have with the built-in Admin.
I just used my own subclass of Site and created a custom admin for it.
Basically, when you subclass a model in django it creates FK pointing to parent model and allows to access parent model's fields transparently- the same way you'd access parent class attributes in pyhon.
Built in admin won't suffer in any way, but you'll have to un-register Sites ModelAdmin and register your own ModelAdmin.
If you only want to change behaviour of the object, but not add any new fields, you should consider using a "proxy model" (new in Django 1.1). You can add extra Python methods to existing models, and more:
This is what proxy model inheritance is for: creating a proxy for the original model. You can create, delete and update instances of the proxy model and all the data will be saved as if you were using the original (non-proxied) model. The difference is that you can change things like the default model ordering or the default manager in the proxy, without having to alter the original.
Read more in the documentation.
As of Django 2.2 there still no simple straight way to extend Site as can be done for User. Best way to do it now is to create new entity and put parameters there. This is the only way if you want to leverage existing sites support.
class SiteProfile(models.Model):
title = models.TextField()
site = models.OneToOneField(Site, on_delete=models.CASCADE)
You will have to create admin for SiteProfile. Then add some SiteProfile records with linked Site. Now you can use site.siteprofile.title anywhere where you have access to current site from model.
You can have another model like SiteProfile which has a OneToOne relation with Site.
It has been a long time since the question was asked, but I think there is not yet (Django 3.1) an easy solution for it like creating a custom user model. In this case, creating a custom user model inheriting from django.contrib.auth.models.AbstractUser model and changing AUTH_USER_MODEL (in settings) to the newly created custom user model solves the issue.
However, it can be achieved for also Site model with a long solution written below:
SOLUTION
Suppose that you have an app with the name core. Use that app for all of the code below, except the settings file.
Create a SiteProfile model with a site field having an OneToOne relation with the Site model. I have also changed its app_label meta so it will be seen under the Sites app in the admin.
# in core.models
...
from django.contrib.sites.models import Site
from django.db import models
class SiteProfile(models.Model):
"""SiteProfile model is OneToOne related to Site model."""
site = models.OneToOneField(
Site, on_delete=models.CASCADE, primary_key=True,
related_name='profiles', verbose_name='site')
long_name = models.CharField(
max_length=255, blank=True, null=True)
meta_name = models.CharField(
max_length=255, blank=True, null=True)
def __str__(self):
return self.site.name
class Meta:
app_label = 'sites' # make it under sites app (in admin)
...
Register the model in the admin. (in core.admin)
What we did until now was good enough if you just want to create a site profile model. However, you will want the first profile to be created just after migration. Because the first site is created, but not the first profile related to it. If you don't want to create it by hand, you need the 3rd step.
Write below code in core.apps.py:
# in core.apps
...
from django.conf import settings
from django.db.models.signals import post_migrate
def create_default_site_profile(sender, **kwargs):
"""after migrations"""
from django.contrib.sites.models import Site
from core.models import SiteProfile
site = Site.objects.get(id=getattr(settings, 'SITE_ID', 1))
if not SiteProfile.objects.exists():
SiteProfile.objects.create(site=site)
class CoreConfig(AppConfig):
name = 'core'
def ready(self):
post_migrate.connect(create_default_site_profile, sender=self)
from .signals import (create_site_profile) # now create the second signal
The function (create_default_site_profile) will automatically create the first profile related to the first site after migration, using the post_migrate signal. However, you will need another signal (post_save), the last row of the above code.
If you do this step, your SiteProfile model will have a full connection with the Site model. A SiteProfile object is automatically created/updated when any Site object is created/updated. The signal is called from apps.py with the last row.
# in core.signals
from django.contrib.sites.models import Site
from django.db.models.signals import post_save, post_migrate
from django.dispatch import receiver
from .models import SiteProfile
#receiver(post_save, sender=Site)
def create_site_profile(sender, instance, **kwargs):
"""This signal creates/updates a SiteProfile object
after creating/updating a Site object.
"""
siteprofile, created = SiteProfile.objects.update_or_create(
site=instance
)
if not created:
siteprofile.save()
Would you like to use it on templates? e.g.
{{ site.name }}
Then you need the 5th and 6th steps.
Add the below code in settings.py > TEMPLATES > OPTIONS > context_processors
'core.context_processors.site_processor'
# in settings.py
TEMPLATES = [
{
# ...
'OPTIONS': {
'context_processors': [
# ...
# custom processor for getting the current site
'core.context_processors.site_processor',
],
},
},
]
Create a context_processors.py file in the core app with the code below.
A try-catch block is needed (catch part) to make it safer. If you delete all sites from the database you will have an error both in admin and on the front end pages. Error is Site matching query does not exist. So the catch block creates one if it is empty.
This solution may not be fully qualified if you have a second site and it is deleted. This solution only creates a site with id=1.
# in core.context_processors
from django.conf import settings
from django.contrib.sites.models import Site
def site_processor(request):
try:
return {
'site': Site.objects.get_current()
}
except:
Site.objects.create(
id=getattr(settings, 'SITE_ID', 1),
domain='example.com', name='example.com')
You can now use the site name, domain, meta_name, long_name, or any field you added, in your templates.
# e.g.
{{ site.name }}
{{ site.profiles.long_name }}
It normally adds two DB queries, one for File.objects and one for FileProfile.objects. However, as it is mentioned in the docs,
Django is clever enough to cache the current site at the first request and it serves the cached data at the subsequent calls.
https://docs.djangoproject.com/en/3.1/ref/contrib/sites/#caching-the-current-site-object
Apparently, you can also create a models.py file in a folder that you add to INSTALLED_APPS, with the following content:
from django.contrib.sites.models import Site as DjangoSite, SiteManager
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.http.request import split_domain_port
# our site model
class Site(DjangoSite):
settings = models.JSONField(blank=True, default={})
port = models.PositiveIntegerField(null=True)
protocol = models.CharField(default='http', max_length=5)
#property
def url(self):
if self.port:
host = f'{self.domain}:{self.port}'
else:
host = self.domain
return f'{self.protocol}://{host}/'
# patch django.contrib.sites.models.Site.objects to use our Site class
DjangoSite.objects.model = Site
# optionnal: override get_current to auto create site instances
old_get_current = SiteManager.get_current
def get_current(self, request=None):
try:
return old_get_current(self, request)
except (ImproperlyConfigured, Site.DoesNotExist):
if not request:
return Site(domain='localhost', name='localhost')
host = request.get_host()
domain, port = split_domain_port(host)
Site.objects.create(
name=domain.capitalize(),
domain=host,
port=port,
protocol=request.META['wsgi.url_scheme'],
)
return old_get_current(self, request)
SiteManager.get_current = get_current
In my opinion, the best way to doing this is by writing a model related to the site model using inheritance
First, add the site id to the Django settings file
SITE_ID = 1
now create a model in one of your apps
from django.db import models
from django.contrib.sites.models import Site
class Settings(Site):
field_a = models.CharField(max_length=150, null=True)
field_b = models.CharField(max_length=150, null=True)
class Meta:
verbose_name_plural = 'settings'
db_table = 'core_settings' # core is name of my app
def __str__(self) -> str:
return 'Settings'
then edit the apps.py file of that app
from django.apps import AppConfig
from django.db.models.signals import post_migrate
def build_settings(sender, **kwargs):
from django.contrib.sites.models import Site
from .models import Settings
if Settings.objects.count() < 1:
Settings.objects.create(site_ptr=Site.objects.first())
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'project.apps.core'
def ready(self) -> None:
post_migrate.connect(build_settings, sender=self)
now every time you run migrations a row will be auto-generated in core_settings that have a one to one relationship with your Site model
and now you can access your settings like this
Site.objects.get_current().settings.access_id
optional: if have only one site
unregister site model from admin site and disable deleting and creating settings model in admin site
from django.contrib import admin
from . import models
from django.contrib.sites.models import Site
admin.site.unregister(Site)
#admin.register(models.Settings)
class SettingAdminModel(admin.ModelAdmin):
def has_delete_permission(self, request,obj=None) -> bool:
return False
def has_add_permission(self, request) -> bool:
return False