How to define django-imagekit ImageSpecFields in parent/mixin class? - django

I'm using django-imagekit to provide thumbnail versions of uploaded images on my Django models.
I would like to define various thumbnail ImageSpecFields in a mixin that my models can then inherit. However, each model currently has a different name for the ImageField on which its ImageSpecFields would be based.
I've tried this:
from django.db import models
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
class ThumbnailModelMixin(models.Model):
IMAGE_SPEC_SOURCE_FIELD = None
list_thumbnail = ImageSpecField(
source=IMAGE_SPEC_SOURCE_FIELD,
processors=[ResizeToFit((80, 160)],
format="JPEG",
options={"quality": 80}
)
class Meta:
abstract = True
class Book(ThumbnailModelMixin):
IMAGE_SPEC_SOURCE_FIELD = "cover"
cover = models.ImageField(upload_to="books/")
class Event(ThumbnailModelMixin):
IMAGE_SPEC_SOURCE_FIELD = "ticket"
ticket = models.ImageField(upload_to="events/")
But this fails on page load with:
AttributeError: 'Book' object has no attribute 'list_thumbnail'
Is there a way to get inheritance like this to work?
There are at least two other solutions:
Don't use a mixin/parent, and include the ImageSpecFields on each child class - a lot of duplicate code.
Change the Book.cover and Event.ticket fields to have the same name, like image and use "image" for the ImageSpecField source parameter.
The latter sounds best, but I'm still curious as to whether there's a way to get the inheritance working?

Related

Filter on custom field across multiple models that import from Wagtail core Page

I have two custom Page models that share a field in common, for example:
class CustomPageModelOne(Page):
custom_field = models.IntegerField()
...
class CustomPageModelTwo(Page):
custom_field = models.IntegerField()
...
I need to run, ideally, a single filter across the two types of custom Page models. The Wagtail docs say I can use an exact_type method to specify multiple models inheriting from core Page, so I am trying some variations of the following:
Page.objects.exact_type(CustomPageModelOne, CustomPageModelTwo).filter(custom_field=123)
However, when I try to filter any QuerySet that uses both models, I get an error:
django.core.exceptions.FieldError: Cannot resolve keyword 'custom_field' into field.
How can I query across multiple Wagtail custom Page models that share a field in common?
Note: I have considered creating an abstract class inheriting from Page, but cannot import that abstract model in the file where it is needed.
Abstract class example:
class CustomFieldModel(Page):
custom_field = models.IntegerField()
class Meta:
abstract = True
class CustomPageModelOne(CustomFieldModel):
pass
class CustomPageModelTwo(CustomFieldModel):
pass
As you do Page.objects... you can only filter on fields of the Page model and subclasses of Page
To filter specifically on fields of your CustomPageModelOne, you would have to use CustomPageModel.objects... where that model has that field and both your custom page models are subclasses from
Apparently Page.objects.exact_type is only returning a queryset based upon Page, which makes sense because exact_type has no way of knowing what fields would occur on models descended from Page. I would suggest the following as an alternative approach if re-architecting your models is not an option:
from itertools import chain
model_one_results = CustomPageModelOne.objects.filter(custom_field=123)
model_two_results = CustomPageModelTwo.objects.filter(custom_field=123)
all_results = list(chain(model_one_results, model_two_results))

Django: add index to meta field

I have the following model:
from model_utils.models import TimeStampedModel
class MyModel(TimeStampedModel):
....
name = models.CharField(max_length=100)
....
This model is based on Django Utils (https://django-model-utils.readthedocs.org/en/latest/models.html#timestampedmodel) which adds created and modified fields to my model.
What I need know is way to add a db_index to the modified field of my model. But I can't modify the TimeStampedModel file because it is part of an external dependency.
Do you know any easy solution for this kind of issue?
Thank you.
Yes you can using model Meta.
if you don't need to inherit meta from the TimeStampedModel just use this:
class Meta:
...
otherwise you need to explicitly tell django to look into the parent's meta first like this:
class Meta(TimeStampedModel.Meta):
this might be a hackish solution but maybe you can try using index_together to make django create an index for your model:
like this:
class Meta(TimeStampedModel.Meta):
index_together = [
["modified",],
]
try it out and tell me if it worked
EDIT:
Another solution coming from: How to override the default value of a Model Field from an Abstract Base Class:
try adding this to your MyModel class
MyModel._meta.get_field('modified').db_index = True

Attribute error when attempting to filter posts

I'm writing a basic Django CMS (purely for my own use as a coding exercise). As part of this I have an abstract 'Displayable' class which both my Post and Comment classes extend:
class Displayable(models.Model):
text = models.TextField()
created = models.DateTimeField(editable=False)
modified = models.DateTimeField(editable=False)
class Meta:
abstract = True
class Post(Displayable):
title = models.CharField(max_length=128)
author = models.ForeignKey(User)
The view code is as follows, with 'year' being a four-digit number from the parameterised URL (e.g. '2014').
from django.shortcuts import render
from django.http import HttpResponse
from django.template import RequestContext
from django.shortcuts import render_to_response
from blog.models import Post
def year_archive(request,year):
context = RequestContext(request)
context_dict = {'year':year}
posts = Post.objects.filter(Post.created.year==year)
# This will be a template call once I get the post filtering working!
return HttpResponse("This is the archive view for the year"+year)
When I attempt to access such a page on the development server (e.g. /blog/2010/) I get an AttributeError:
type object 'Post' has no attribute 'created'
The problem is that, at least as far as I understand how inheritance works in Django, 'Post' does (or at least should) have an attribute 'created'. The IDE I'm using (IDEA with the Python module installed) is quite happy that such an attribute exists (it even comes up in autocompletion), so I can't work out why the Django dev server is saying it does not.
You should read the documentation on filters. The way you want to filter is like this:
posts = Post.objects.filter(created__year=year)
The Django ORM automatically converts these keyword arguments to the right lookups in the database.
Now as to why the Post class has no attribute created: Django models are quite complicated classes. The first time they are imported, a metaclass creates the actual class you use, created from the class definition you've written. Part of this is that all the Field subclasses defined in your class definition get removed, added to the _meta.fields attribute, and that the original attribute gets set to the actual value that the field describes.
That means the fields are no longer available on the class itself. If a field belongs to a superclass, you'll get the error you saw, but if it belonged to the Post class, you'd still get AttributeError: the 'created' attribute can only be accessed from Post instances. This way Django prevents you from setting/getting field values as class attributes.

Overriding the admin Media class

Given an admin media class that sets up a rich text editor, like:
class TutorialAdmin(admin.ModelAdmin):
fields...
class Media:
js = ['/paths/to/tinymce.js',]
I would like the ability to selectively override js depending on a field value in the model it references. I've added a "use_editor" boolean to the Tutorial model. The question is, how can I detect whether the current instance has that bool set? I'd like to end up with something like:
class Media:
if self.use_editor:
js = ['/path/to/tinymce.js',]
else:
js = ''
Ideas? Thanks.
Many thanks to Sam Lai on django-users, I finally have a working solution for this. Turns out to be trickier than expected because you can't directly access field values on the instance from within the Admin class - you need to do it by redefining the form used by the Admin class. In addition, you'll need to use _media rather than "class Media:" to set the media property.
The goal is to detect the current instance value of the use_visual_editor field and turn javascript paths on or off depending on its value (so authors can turn off the visual editor on a per-record basis). Here's the final working solution:
models.py
class Tutorial(models.Model):
use_visual_editor = models.BooleanField()
forms.py
from django import forms
from tutorials.models import Tutorial
class TutorialAdminForm(forms.ModelForm):
class Meta:
model = Tutorial
def _media(self):
if self.instance.use_visual_editor == True:
js = ['/paths/to/javascript',]
else:
js = ['']
return forms.Media(js=js)
media = property(_media)
admin.py
from django import forms
....
class TutorialAdmin(admin.ModelAdmin):
form = TutorialAdminForm
Works perfectly!
An alternative approach, given you're using TinyMCE, is to use an additional JS file that adds a 'mceNoEditor' class to textareas you don't want to convert to rich text.
eg
class fooAdmin(admin.Modeladmin)
class Media:
js = ['/path/to/admin-styling.js',
'/paths/to/tinymce.js',]
In your tinymce.js init, you need to ensure there's a class defined for disabling the editor, such as:
editor_deselector : "mceNoEditor",
and in the admin-styling.js file have some kind of jQuery call in the document ready handler that finds certain elements and adds that class before TinyMCE is invoked.
Usually you can do this with the 'id_foo' identifier. eg, if you have a model field called additional_notes:
$('textarea#id_additional_notes').addClass('mceNoEditor');
It's possible to use more sophisticated jQuery selectors too, of course.
HTH
Steve

Model formsets and Date fields

I have a model formset on a model with a couple of date fields - which again is a DateTimeField in the model.
But when it displays in the template, it is shown as a text field.
How can I show it as a drop down box ?
WFormSet = inlineformset_factory(UserProfile, W, can_delete=False,exclude=[<list of fields to be excluded>], extra=3)
This is how I am intializing the modelformset.
How do I override these settings
The usual way to do this is to override the default field types in your ModelForm definition.
The example below would work if you had a DateField in your model (I note you have a DateTimeField... I'll come back to that in a sec). You're going to be specifying the exact same field type as would normally be specified, but you'll pass a different widget to the form field constructor.
from django.db import models
from django import forms
from django.forms.extras import SelectDateWidget
class MyModel(models.Model):
a_date_field = models.DateField()
class MyModelForm(forms.ModelForm):
a_date_field = forms.DateField(widget=SelectDateWidget())
class Meta:
model = MyModel
There isn't, to my knowledge, an equivalent widget provided for the DateTimeField in Django 1.0.x. In this case, you'll want to make a custom widget, perhaps subclassing SelectDateWidget. I note from a quick google on SelectDateTimeWidget that there have been several others who've been making what appear to be the widget you're seeking. I've not used them, so caveat emptor, but something like this SelectDateTimeWidget patch might be a good place to start.
Edit: When using a ModelFormset or InlineModelFormset, you can still achieve this by passing form=MyModelForm to the inlineformet_factory function:
MyModelFormset = inlineformset_factory(MyParentModel, MyModel, form=MyModelForm)
This works because the two model formset factories actually call the regular formset_factory constructor in their own implementations. It's a little hard to figure out because it's not explicitly stated in the docs... rather the Django docs allude to this ability by mentioning in passing that the model formset factories extend the normal formset_factory. Whenever I'm in doubt, I open django/forms/models.py to check out the full signature of the formset factory functions.