I HAVE scoured the web and if there is a solution I have not found it. Perhaps the answer is right in front of me but I don't see it and I'm really baffled at this issue.
Problem: I have a model:
class Campaign(models.Model):
emailaddress = models.CharField(max_length=100, default='', verbose_name='ADF Email', blank=True)
xlsfile = models.FileField(upload_to='xlsfiles/', blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
dealerimage = models.ForeignKey('vipapp.savedImage', blank=True, null=True, related_name='campaign')
I do not override the admin add/change form currently. Perhaps I should, we'll see.
When I submit a new campaign an xlsfile and dealerimage are uploaded and should be required. I use if(self.xlsfile): in the overridden save() on the campaign model to check for the optional xlsfile and if it exists I have a considerable amount of code that parses the xlsfile, puts the records into another model called weblist and also sends the user a modified excel file (I am using both xlsfile and excel file naming on purpose in this post).
The problem is this: When the user edits a campaign using the default admin change form the excel file is listed in the xlsfile field. On submit, the system appears to try again to upload the excel file and that file may no longer actually exist on the system from which the user is editing. On edit, the user may not want to upload a new excel file as this would overwrite the other model (weblist).
Question: How would I clear this field in the change form yet not delete it from the object when the user submits? I cannot use exclude because the user MAY want to upload a new excel file.
Question: What is self.xlsfile in the save() on the campaign model? I mean, how do I tell the difference between what is in the database already (maybe obj.xlsfile) and what is coming in from the change form POST? Is new data in the post self.xlsfile or is that what is in the object already? I know I should be able to determine these things on my own but I've not been able to reliable tell if my test is correct so that it gives me the correct answer.
How do I effectively clear the field when the form loads? The user MAY want to replace the xls and therefore delete the weblist records but this is not typical. Replacing the weblist instances in the weblist model is dangerous because these contain generated codes that have already been sent to customers so this rarely happens, so if there is no new excel file the weblist instances should not be touched. If the user does upload a new excel file the current weblist instances should be deleted and re-written however.
exclude on the edit form is not an option because again, xlsfile may sometimes need to be updated though this is rare. I don't want to delete anything, but I don't want the form to show the xls file or make any changes unless the user explicitly uploads a new xlsfile. I may have repeated myself here but I just want to be clear.
I suspect the answer here will affect the dealerimage field as I will probably understand this better in general. Any explanatory info here would be awesome. Also, providing me links to Working with Forms or The Forms API or Form and field Validation pages will likely be unhelpful as I have read these documents. Pointing to an exact answer that I may have missed in those pages however may provide an Ah-ha moment though :)
I figured this out...
I just overrode get_form so that xlsfile field would always be set to blank on load of my CampaignEditForm:
def get_form(self, request, obj=None, **kwargs):
if obj:
obj.xlsfile = ''
kwargs['form'] = CampaignEditForm
else:
kwargs['form'] = CampaignAddForm
return super(campaignAdmin, self).get_form(request, obj, **kwargs)
Then in the save() of my campaign model I just explicitly set the fields I wanted to save, leaving out xlsfile if it was blank so any value in the DB never got deleted unless it was replaced:
if self.xlsfile:
super(Campaign, self).save(*args,**kwargs)
else:
super(Campaign, self).save(update_fields=["dealername", "campaignname", "dealeraddress", "dealercity",
"dealerstate", "dealerzipcode", "dealerphone", "dealeremail1",
"dealeremail2", "dealeremail3", "dealeremail4", "dealeremail5",
"adf", "adfemail", "created", "dealerimage"])
Related
I have 2 models in my Project with Many to Many relationship. On saving model Event, I read from the event_attendees file and add it to the attendees field in the Event. No errors/exceptions shown but attendee is not added to the attendees field. Do I need to save the model again after altering with the attendees field? If so, how to do that (calling save method from add_attendees will cause the program into infinite loop)?
class Attendee(models.Model):
name = models.CharField(max_length=100)
class Event(models.Model):
name = models.CharField(max_length=100)
event_attendees = models.FileField(upload_to='documents/', blank=True)
attendees = models.ManyToManyField(Attendee, blank=True)
def save(self, *args, **kwargs):
super().save()
self.add_attendees()
def add_attendees(self):
with open(self.event_attendees.url[1:]) as csv_file:
# Some code here
for row in csv_reader:
# Some code here
attendee = Attendee(name=name)
attendee.save()
self.attendees.add(attendee)
print(self.attendees.all()) # attendee added
print(attendee.event_attended) # event present with attendee
#Refresh template to check changes -> Changes lost
It's the Attendee object that you haven't saved.
You can shortcut it by using the create method on the m2m field:
for row in csv_reader:
self.attendees.create(name=whatever)
(Note, please don't blindly catch exceptions. Django will already do that and report a useful error page. Only catch the exceptions you are actually going to deal with.)
Apparently, the feature worked when I used non-admin web dashboard. While using by-default-created /admin dashboard, this feature was not working. I am assuming from the results that the admin side code calls different methods while saving the model object even though I have overridden the save method (and hence my save method along with other methods should be called). I will update with more info if I find it.
Django 1.11.6
I'm developing a document archive. So, I calculate checksums to control that documents are still present, that they are not corrupt etc.
Another side of this is that I can control whether a file has already been uploaded. It is important: no need to duplicate the files.
I upload files through Django admin. There is a form prepared.
But the problem is: form validators seems to be not very useful here.
At least I can't invent how to use a form validator here.
But post_save signal are useful: here we have a file already uploaded. We calculate the checksum of the file (using md5).
But if unique=True for the checksum, IntegrityError is risen.
It is Ok and predictable. But could you help me understand how to notify the user about this? Any method would suffice. It is just for our staff: no need to organize a brilliant html layout. But what is important is to show that the uploaded file coincides with an existing file with the following id.
Could you help me with this?
models.py
class FileChecksum(models.Model):
checksum = models.CharField(blank=True,
null=False,
unique=True,
max_length=255,
verbose_name=_("checksum"))
def __str__(self):
return self.checksum
class Image(models.Model):
file = models.ImageField(blank=False,
verbose_name=_("Image"),
max_length=255,
upload_to=get_sheet_path)
file_checksum = models.OneToOneField(FileChecksum,
blank=True,
null=True)
#property
def checksum(self):
pass
#checksum.setter
def checksum(self, new_checksum):
pass
signals.py
#receiver(post_save, sender=Image)
def save_file_checksum(sender, instance, **kwargs):
new_checksum = get_checksum(instance.file.path)
instance.checksum = new_checksum
admin.py
class ImageForm(FileMixin,
ModelForm):
model = Image
class ImageAdmin(admin.ModelAdmin):
form = ImageForm
admin.site.register(Image, ImageAdmin)
You have to calculate it before you save. That way your form can benefit from it and form validation will take care of bubbling up the error to the user. Decouple the get_checksum function away from the signals and calculate it as part of the form validation.
I had the same situation where I had a non-trivial hash calculation. So as soon as all the other basic validation stuff was done, I calculated the has with a function and if it clashed, made it part of the form (invariants) validation. If it didn't clash, it got used in about-to-be-created instance.
But I also, made it so that if you try to create an instance of the model, but don't have a hash set, a pre_save signal would create it. Useful for code paths where the item isn't coming in from a regular form.
Right now I'm using Django's built in admin system to manage users, to which I've attached a profile to contain additional data using the following:
class Profile(models.Model):
user = models.OneToOneField(User, editable = False)
# Data fields here...
As it stands the User and Profile pk (and accordingly id number) will be the same if and only if the profile is created right after the user is created. I could guarantee that this would be the case during the registration process, and while that would cover most uses, creating users with the admin interface could cause mismatched ids to occur. Thus this does not seem like a very robust way to solve this problem and I'd like to hardcode the pk's to be the same. I'm not sure how to do this.
I thought the following would work:
profile_id = models.IntegerField(default=user.pk, editable = False,
primary_key = True)
But it gives me the error:
AttributeError: 'OneToOneField' has no attribute 'pk'
What's the best way to guarantee that the profile and user have the same pk? Note: I'd really rather not deal with extending the base user model as using the OneToOneField to link the two seems to be sufficient for all my needs.
Thanks!
[edit]
My reasoning for asking the question:
My immediate problem was that I wanted a dictionary of values of the User's Profile, which I was retrieving usingprofile_values = Profile.objects.filter(pk=user.id).values()[0]. This highlighted the bug, and I "hacked" around it last night using pk=user.profile.id instead. In the light of the morning this does not seem like such a terrible hack. However, it seems like having pk discrepancies could lead to quiet and hard to catch bugs down the line, and thus forcing them to match up would be a Good Idea. But I'm new to Django so I'd entirely accept that it is, in fact, never a problem if you're writing your code correctly. That said, for almost academic reasons, I'd be curious to see how this might be solved.
[/edit]
Like you already agree that it was never a problem because we have a OneToOne mapping between the two models.
So when you need to get the profile obj corresponding to a User:
profile_values = Profile.objects.get(user_id=user)
assuming,
class Profile(models.Model):
user = models.OneToOneField(User)
...
If your column name is not user, then use the corresponding name in get query.
Still if you are curious as to how to achieve same pk for both models, then we can set a signal on every save of User model. See the documentation.
def create_profile(sender, **kwargs):
if kwargs["created"]:
p = Profile(user=kwargs["instance"], ...)
p.save()
django.db.models.signals.post_save.connect(create_profile, sender=User)
create_profile() will be called every time any User object is saved.
In this function, we create Profile object only if a new User instance has been created.
If we start from blank slate, then I think this will always make sure that a Profile exists for every User and is created right after User was created; which in turn will give same pk for both models.
pk is a parameter in a filter() query, but not a field name. You probably want to use user.id.
So I've got a UserProfile in Django that has certain fields that are required by the entire project - birthday, residence, etc. - and it also contains a lot of information that doesn't actually have any importance as far as logic goes - hometown, about me, etc. I'm trying to make my project a bit more flexible and applicable to more situations than my own, and I'd like to make it so that administrators of a project instance can add any fields they like to a UserProfile without having to directly modify the model. That is, I'd like an administrator of a new instance to be able to create new attributes of a user on the fly based on their specific needs. Due to the nature of the ORM, is this possible?
Well a simple solution is to create a new model called UserAttribute that has a key and a value, and link it to the UserProfile. Then you can use it as an inline in the django-admin. This would allow you to add as many new attributes to a UserProfile as you like, all through the admin:
models.py
class UserAttribute(models.Model):
key = models.CharField(max_length=100, help_text="i.e. Age, Name etc")
value = models.TextField(max_length=1000)
profile = models.ForeignKey(UserProfile)
admin.py
class UserAttributeInline(admin.StackedInline):
model = UserAttribute
class UserProfile(admin.ModelAdmin):
inlines = [UserAttibuteInline,]
This would allow an administrator to add a long list of attributes. The limitations are that you cant's do any validation on the input(outside of making sure that it's valid text), you are also limited to attributes that can be described in plain english (i.e. you won't be able to perform much login on them) and you won't really be able to compare attributes between UserProfiles (without a lot of Database hits anyway)
You can store additional data in serialized state. This can save you some DB hits and simplify your database structure a bit. May be the best option if you plan to use the data just for display purposes.
Example implementation (not tested)::
import yaml
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField('auth.User', related_name='profile')
_additional_info = models.TextField(default="", blank=True)
#property
def additional_info(self):
return yaml.load(self._additional_info)
#additional_info.setter
def additional_info(self, user_info_dict):
self._additional_info = yaml.dump(user_info_dict)
When you assign to profile.additional_info, say, a dictionary, it gets serialized and stored in _additional_info instead (don't forget to save the instance later). And then, when you access additional_info, you get that python dictionary.
I guess, you can also write a custom field to deal with this.
UPDATE (based on your comment):
So it appears that the actual problem here is how to automatically create and validate forms for user profiles. (It remains regardless on whether you go with serialized options or complex data structure.)
And since you can create dynamic forms without much trouble[1], then the main question is how to validate them.
Thinking about it... Administrator will have to specify validators (or field type) for each custom field anyway, right? So you'll need some kind of a configuration option—say,
CUSTOM_PROFILE_FIELDS = (
{
'name': 'user_ip',
'validators': ['django.core.validators.validate_ipv4_address'],
},
)
And then, when you're initializing the form, you define fields with their validators according to this setting.
[1] See also this post by Jacob Kaplan-Moss on dynamic form generation. It doesn't deal with validation, though.
In summary, what I'm trying to accomplish here is:
To perform transformation over uploaded files' names for organization and security's sake
Not to keep old files in storage forever
Let's say that you have a Company model which has a logo. Something like
class Company(models.Model):
name = ...
logo = models.FileField(blank=True, null=True)
Now, since I'm a bit paranoid and I don't really like the fact that uploaded files get the name given by the (potentially evil) user, I add a upload_to parameter that points to a function that's something like
def logo_getfilename(instance, filename):
extension = ... # get just the original extension from the file
return 'logos/' + str(uuid.uuid4()) + extension
Ok, so now only the view is missing!
def company_edit(request, ...):
company = ... # get the company and stuff
if request.method == 'POST':
form = CompanyAdminForm(request.POST, request.FILES, instance=company)
last_file_path = None
if not company.logo is None:
last_file_path = company.logo.path
# ^^ after calling is_valid(), file_path gets changed to
# the would-be-default-behavior
if form.is_valid():
# first we write the new file
form.save()
# now we remove the old one
os.unlink(last_file_path)
Although this is currently working, I'm not really comfortable with it because
I'm using os.unlink() instead of FieldFile.delete() which seems wrong
I'm assuming a local filesystem storage
I'm still not doing anything against naming collisions (they may still happen)
I'm disregarding multiple chunks and assuming that form.save() will deal with everything
I'm not considering transactional behavior (previous file should be deleted only after the .save() model changes are commited to the database
I feel there are some problems there I don't even know about
So to acheive these simple (and not as uncommon as that) goals, what would be your recommendations?
Instead of making a random username, why not use the primary key for the Company? When they upload a new file, just have it overwrite the existing one (which I think it will do automatically as long as you change the file name before you save). This should remove your need to do os.unlink and (maybe) stop your transactional worries.