I have two models, Room and Image. Image is a generic model that can tack onto any other model. I want to give users a form to upload an image when they post information about a room. I've written code that works, but I'm afraid I've done it the hard way, and specifically in a way that violates DRY.
Was hoping someone who's a little more familiar with django forms could point out where I've gone wrong.
Update:
I've tried to clarify why I chose this design in comments to the current answers. To summarize:
I didn't simply put an ImageField on the Room model because I wanted more than one image associated with the Room model. I chose a generic Image model because I wanted to add images to several different models. The alternatives I considered were were multiple foreign keys on a single Image class, which seemed messy, or multiple Image classes, which I thought would clutter my schema. I didn't make this clear in my first post, so sorry about that.
Seeing as none of the answers so far has addressed how to make this a little more DRY I did come up with my own solution which was to add the upload path as a class attribute on the image model and reference that every time it's needed.
# Models
class Image(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
image = models.ImageField(_('Image'),
height_field='',
width_field='',
upload_to='uploads/images',
max_length=200)
class Room(models.Model):
name = models.CharField(max_length=50)
image_set = generic.GenericRelation('Image')
# The form
class AddRoomForm(forms.ModelForm):
image_1 = forms.ImageField()
class Meta:
model = Room
# The view
def handle_uploaded_file(f):
# DRY violation, I've already specified the upload path in the image model
upload_suffix = join('uploads/images', f.name)
upload_path = join(settings.MEDIA_ROOT, upload_suffix)
destination = open(upload_path, 'wb+')
for chunk in f.chunks():
destination.write(chunk)
destination.close()
return upload_suffix
def add_room(request, apartment_id, form_class=AddRoomForm, template='apartments/add_room.html'):
apartment = Apartment.objects.get(id=apartment_id)
if request.method == 'POST':
form = form_class(request.POST, request.FILES)
if form.is_valid():
room = form.save()
image_1 = form.cleaned_data['image_1']
# Instead of writing a special function to handle the image,
# shouldn't I just be able to pass it straight into Image.objects.create
# ...but it doesn't seem to work for some reason, wrong syntax perhaps?
upload_path = handle_uploaded_file(image_1)
image = Image.objects.create(content_object=room, image=upload_path)
return HttpResponseRedirect(room.get_absolute_url())
else:
form = form_class()
context = {'form': form, }
return direct_to_template(request, template, extra_context=context)
Why don't you just use ImageField? I don't see the need for the Image class.
# model
class Room(models.Model):
name = models.CharField(max_length=50)
image = models.ImageField(upload_to="uploads/images/")
# form
from django import forms
class UploadFileForm(forms.Form):
name = forms.CharField(max_length=50)
image = forms.FileField()
Take a look at Basic file uploads and How do I use image and file fields?
You don't have to use the Image class. As DZPM suggested, convert the image field to an ImageField. You also need to make some changes to the view.
Instead of using an upload handler, you can create a Image object with the uploaded data and attach the Image object to the Room object.
To save the Image object you need to do something like this in the view:
from django.core.files.base import ContentFile
if request.FILES.has_key('image_1'):
image_obj = Image()
image_obj.file.save(request.FILES['image_1'].name,\
ContentFile(request.FILES['image_1'].read()))
image_obj.save()
room_obj.image_set.create(image_obj)
room_obj.save()
Also, I think instead of the GenericRelation, you should use a ManyToManyField, in which case the syntax for adding an Image to a Room will change slightly.
What about using two forms on the page: one for the room and one for the image?
You'll just have to make the generic foreign key fields of the image form not required, and fill in their values in the view after saving the room.
Django does support your use case at least up to a point:
formsets display repeated forms
model formsets handle repeated model forms
inline formsets bind model formsets to related objects of an instance
generic inline formsets do the same for generic relations
Generic inline formsets were introduced in changeset [8279]. See the changes to unit tests to see how they are used.
With generic inline formsets you'll also be able to display multiple already saved images for existing rooms in your form.
Inline formsets seem to expect an existing parent instance in the instance= argument. The admin interface does let you fill in inlines before saving the parent instance, so there must be a way to achieve that. I've just never tried that myself.
I found this page look for a solution to this same problem.
Here is my info -- hopefully helps some.
MODELS: Image, Review, Manufacturer, Profile
I want Review, Manufacturer, Profile to have a relationship to the Image model. But you have to beable to have multiple Images per object. (Ie, One Review can have 5 images a different Review can have 3, etc)
Originally I did a
images = ManyToManyField(Image)
in each of the other models. This works fine, but sucks for admin (combo select box). This may be a solution for you though. I dont like it for what I'm trying to do.
The other thing I'm working on now is having multiple foreign keys.
class Image(models.Model):
description = models.TextField(blank=True)
image = models.ImageField(upload_to="media/")
user_profile = models.ForeignKey(UserProfile)
mfgr = models.ForeignKey(Manufacturer)
review = models.ForeignKey(Review)
but like you said. This is pretty sloppy looking and I just dont like it.
One other thing I just found but don't have my brain completely wrapped around (and not sure how transparent it is after implementation is Generic Relationships (or Generic Foreign Keys), which may be a solution. Well once I comprehend it all. Need more caffeine.
http://www.djangoproject.com/documentation/models/generic_relations/
Let me know if you get this sorted out or any of this helps. Thanks!
Let me know if this helps or you have a different solutions.
Ok I figured it out with some more reading... I feel like you want to do exactly what I have done so here it is.
I'll be using GenericForeignKeys for this.
First the imports for models.py
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
Now add the following to your Image Model
class Image(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
This lets this model be just that, a Generic Foreign Key for any number of models.
Then add the following to all the models you want to have related images
images = generic.GenericRelation(Image)
Now in admin.py you need to add the following things.
from django.contrib.contenttypes.generic import GenericTabularInline
class ImageInline(GenericTabularInline):
model = Image
extra = 3
ct_field_name = 'content_type'
id_field_name = 'object_id'
And then include it in a admin declaration
class ReviewAdmin(admin.ModelAdmin):
inlines = [ImageInline]
And thats it. Its working great over here. Hope this helps man!
.adam.
Use two forms, one for the room and one for the image:
class Image(models.Model)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
image = models.ImageField(upload_to='')
class UploadImage(forms.ModelForm):
class Meta:
model = Image
fields = ('image')
class Room(models.Model):
name = models.CharField(max_length=50)
images = models.ManyToManyField(Image)
class RoomForm(forms.ModelForm):
class Meta:
model = Room
in the views
if request.method == "POST":
##2 form, una per l'annuncio ed una per la fotografia
form = RoomForm(request.POST)
image_form = UploadImage(request.POST, request.FILES)
#my_logger.debug('form.is_valid() : ' + str(form.is_valid()))
if form.is_valid() and image_form.is_valid():
##save room
room = room.save()
##save image
image = image_form.save()
##ManyToMany
room.images = [image]
room.save()
Related
class A(Model):
to_b = ManyToManyField('B', blank=True, through='AtoB')
class B(Model):
to_a = ManyToManyField('A', blank=True, through='AtoB')
class AtoB(Model):
a = ForeignKey('A', on_delete=CASCADE)
b = ForeignKey('B', on_delete=CASCADE)
usr = ForeignKey(settings.USER, on_delete=CASCADE)
# some other fields
Im making a django application.
This is roughly equivalent to what i have in my models.py
I need m2m relation between A and B to go through another model because i need to store additional data there.
Now there is a problem - when i try to save instance of model A in my custom view, relations with B instances are not saved no matter whether i pick them or not.
And when i go to http://127.0.0.1:8000/admin and try to create instance of A from there, i dont even see proper field (should be <select multiple> i guess) for selecting relations with B.
Can someone please explain me why relations are not saved, and not even displayed in /admin?
Here is code roughly equivalent to what i have in views.py:
class Create(CreateView):
model = None # A or B
template_name = 'something.html'
def form_valid(self, form):
self.object = form.save(commit=False)
form.save()
return HttpResponseRedirect(self.get_success_url())
in urls.py i specify additional arguments like this:
views.Create.as_view(model=models.A, fields=['to_b'])
It is not going to work. If you are using your own ManytoMany intermediary table, you have to manually manage and save the objects yourself. Using Django's builtin functions won't work.
Save Object A, then save Object B and then save the relation in your AtoB table (which is also an object).
a_to_b = AtoB.objects.create(a=object_a, b=object_b, user=self.request.user)
print(a_to_b)
[...] Note that if you are using an intermediate
model for a many-to-many relationship, some of the related manager’s
methods are disabled, so some of these examples won’t work with such
models.
https://docs.djangoproject.com/en/1.10/topics/db/examples/many_to_many/
Your error is explained here: https://docs.djangoproject.com/en/1.10/topics/db/models/#intermediary-manytomany
I study the CBV in django.
I want a user can upload images to a certain card of an apartment.
So The model was created:
class Photo(models.Model):
photo_path = models.ImageField(verbose_name='Фотография')
photo_user = models.ForeignKey(User, verbose_name='Агент, добавивший фото')
photo_flat = models.ForeignKey(Flat, verbose_name='Квартира')
photo_description = models.CharField(verbose_name='Описание', max_length=200, null=True, blank=True)
The initial data of the form are photo_user and the photo_flat.
When I try to save one or multiple pictures through a form on the page I've got AttributeError.
'NoneType' object has no attribute 'pk'
My ModelForm looks like this:
class PhotoUploadModelForm(forms.ModelForm):
class Meta:
model = Photo
fields = ['photo_path','photo_user','photo_flat','photo_description'
My CreateView:
class PhotoUploadView(CreateView):
form_class = PhotoUploadModelForm
def get_initial(self):
return {'photo_user': self.request.user,
'photo_flat': self.object.pk
}
def get_success_url(self):
return reverse('ha:flatdetail')
and my urls.py
url(r'^dev/flat/(?P<flat_id>[0-9]+)/$', views_dev.flat_ajax, name='flatdetail'),
url(r'^dev/photo-update/$', views_dev.PhotoUploadView.as_view(), name='image-update')
I understand the error but has not enough skills to handle it. am I correct that self.object.pk would be the id of the picture but not the apartment?
Can I get the id of the apartment?
You're misinterpreting the error, but that is partly because what you're doing doesn't make a lot of sense.
self.object can only refer to the object that this view is concerned with, which in this case is the Photo; but of course that photo doesn't exist yet when you do get_initial, because it hasn't been created. It seems that you want to use the ID of a specific Flat object; in which case you would need to pass that ID to the view, ie via the URL. You do this already in your flat_ajax view; you should do exactly the same thing here, and then you will be able to reference the ID via self.kwargs['flat_id'].
Note that get_initial isn't really the best place to be doing this, anyway. The User and the Flat object are not values that you want to pass to the form so that they populate fields initially but let the user change them; they are values you want to automatically associate with the object on save. So, this should really be done in form_valid, as shown in the documentation:
def form_valid(self, form):
form.instance.photo_user = self.request.user
form.instance.photo_flat_id = self.kwargs['flat_id']
return super(PhotoUploadView, self).form_valid(form)
If you do it this way, you should remove photo_user and photo_flat from the fields listed in the form.
I am working on a webcomics platform, and I need to allow users to upload multiple images in one post.
Ideally - keep it as simple as possible, so that a person wouldn't have to refresh the page to upload each image, or create and save a post before adding images.
If a user could delete or reorder images it would be nice.
Also, I need to make it scalable, so that I wouldn't have problems with it later.
Can you give me advice on how to do it properly?
Should images have their own model, and be connected to the post with foreign key? (not sure if this makes sense, seems kinda ugly)
Or should I just keep a list of image urls in the some type of field on a post model, and then create some sort of cdn and upload it there?
Any advice is really appreciated.
If you're talking about scale then you need to figure out what the best practice is for serving images.
I would recommend that you use S3 to serve your static files (this will include your user-uploaded files too). Follow this tutorial which shows you how to set this up from scratch.
Now, digging into your schema, you will need both a Post and PostImage model:
models.py:
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=50)
body = models.TextField()
class PostImage(models.Model):
image = models.ImageField(
upload_to='<path_from_s3_tutorial>'
)
width_x = models.IntegerField()
width_y = models.IntegerField()
# Foreign Key to Post
post = models.ForeignKey('Post', null=True, blank=True)
Then when you will need to create a class in forms.py that uses an Inline Formset which allows you to upload multiple images when you create a post, as below:
>>> from myapp.models import Post, PostImage
>>> from django.forms import inlineformset_factory
>>>
>>> PostImageFormSet = inlineformset_factory(Post, PostImage, fields=('image',))
>>> post = Post.objects.get(id=13)
>>> formset = PostImageFormSet(instance=post)
You will then validate your formset in your view like so:
views.py
def create_post(request, post_id):
post = Post.objects.get(pk=post_id)
PostImageInlineFormSet = inlineformset_factory(Post, PostImage, fields=('image',))
if request.method == "POST":
formset = PostImageFormSet(request.POST, request.FILES, instance=post)
if formset.is_valid():
formset.save()
return redirect('index')
else:
formset = PostImageFormSet(instance=post)
return render(request, 'manage_books.html', {'formset': formset})
Note: Most of this example was modified from the one in the Django docs which I linked above.
I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.
Django 1.1
models.py:
class Property(models.Model):
name = models.CharField()
addr = models.CharField()
phone = models.CharField()
etc....
class PropertyComment(models.Model):
user = models.ForeignKey(User)
prop = models.ForeignKey(Property)
text = models.TextField()
etc...
I have a form which needs to display several entries from my Property model each with a corresponding PropertyComment form to collect a user's comments on that property. In other words, allowing a User to comment on multiple Property instances on the same page.
This seems outside the intended usage of an Inline formset since it is multi-model to multi-model vs. single-model to multi-model. It seems like trying to iterate through the Property instances and create an inline formset for each is not only clunky, but I'm not even sure it could work.
Any ideas on where to start on this?
Have you thought about using the comment framework:
http://docs.djangoproject.com/en/dev/ref/contrib/comments/
If that doesnt work for you then maybe look into inlineformset_factory:
http://docs.djangoproject.com/en/dev/topics/forms/modelforms/#inline-formsets
from django.forms.models import inlineformset_factory
PropertyCommentFormSet = inlineformset_factory(Property, PropertyComment)
property= Property.objects.get(name=u'some property name')
formset = PropertyCommentFormSet(instance=property)
etc...