I am creating a dynamic form (using JSON format) in Django, the form needs to save multiple images using Django Storage and save the reference to the file in the JSON field.
This is what I have right now, it works but is really ugly, Django already do this, I can't figure out how to re-use same functionality.
Class SomeModel(models.ModelForm):
results = JSONField()
class DynamicJsonForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# ... Add dynamic fields to the form
self.extra = [] # Save the reference for the fields
class Meta:
model = SomeModel
exclude = ("results",)
def save(self, commit=True):
results = {}
for extra in self.extra:
value = self.cleaned_data.get(extra)
question = some_query.question
if "photo" in extra and value: # value = photo
filename, ext = value.name.split(".")
filename = "media/the/path/to/photos/{}_{}.{}".format(filename, uuid4().hex, ext)
uploaded_file = SimpleUploadedFile(filename, value.read(), value.content_type)
image = Image.open(uploaded_file)
if image.mode in ("RGBA", "P"):
image = image.convert("RGB")
image.save(fp=filename)
results[question][extra] = filename
else:
results[question][extra] = value
self.instance.results = results
return super().save(commit)
This actually works, it saves the data to the JSONField (results) and saves the image to local file system storage.
How can this be improved to use Django Storage and make it simple? Using Django Model.save() seems a lot easier, but I can't use a normal ImageField() since it needs to be dynamic
I mixed FileSystemStorage with forms.ImageField, not a perfect solution at all, but looks better.
fields.py
import os
from django import forms
from django.conf import settings
from django.core.files.storage import FileSystemStorage
class FormFileSystemStorageImageField(forms.ImageField, FileSystemStorage):
def __init__(self, location=None, *args, **kwargs):
super().__init__(*args, **kwargs) # Call ImageField __init__, I wonder how to call second parent's __init__
self._orig_location = location
self._location = os.path.join(settings.MEDIA_ROOT, location)
def storage_path(self, name):
return os.path.join(self._orig_location, name)
forms.py
from .fields import FormFileSystemStorageImageField
# ... Same as question code
def save(self, commit=True):
results = {}
for extra in self.extra:
value = self.cleaned_data.get(extra)
question = some_query.question
if "photo" in extra and value: # value = photo
image_field = self.fields.get(extra)
image_field.save(value.name, value)
results[question][extra] = image_field.storage_path(value.name)
else:
results[question][extra] = value
self.instance.results = results
return super().save(commit)
Related
I'm new to Django and I'm having a hard time understanding forms when the data to choose from are not taken from the database nor user input that they're generated on the go.
I currently have a template with a single ChoiceField. The data inside this field aren't fixed and they're calculated on the go once the page is requested. To calculate it I need the username of the User who is logged in. Basically, the calculation returns a list of lists in the form of ((title, id),(title,id),(title,id)), etc. that I need to put into the ChoiceField to make the User choose from one of the options.
Now, I'm not understanding how to pass the calculated list of lists to the form. I've tried to add the calculations inside the form as below but it is clearly the wrong way.
The main issue is that, to calculate my list of lists, I need the request value, and I don't know how to access it from the form.
Another idea was to add the generate_selection function inside the init but then I don't know how to pass main_playlist to being able to add it to ChoiceField
Below my not working forms.py
forms.py
class ChoosePlaylistForm(forms.Form):
playlists = forms.ChoiceField(choices=HERE_SHOULD_GO_main_playlist)
def generate_selection(self):
sp_auth, cache_handler = spotify_oauth2(self.request)
spotify = spotipy.Spotify(oauth_manager=sp_auth)
user_playlists = spotify.current_user_playlists(limit=10)
main_playlist = []
for playlists in user_playlists["items"]:
playlists_list = []
playlists_list.append(playlists['name'])
playlists_list.append(playlists['id'])
main_playlist.append(playlists_list)
return main_playlist
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(ChoosePlaylistForm, self).__init__(*args, **kwargs)
class Meta:
model = User
fields = ('playlists',)
The views should be something like below so I'm able to pass the request
views.py
form = ChoosePlaylistForm(request=request)
Maybe overriding the field choices in the form constructor would work:
class ChoosePlaylistForm(forms.Form):
playlists = forms.ChoiceField(choices=())
class Meta:
model = User
fields = ('playlists',)
def __init__(self, *args, request=None, **kwargs):
super(ChoosePlaylistForm, self).__init__(*args, **kwargs)
self.request = request
self.fields['playlists'].choices = self.generate_selection()
def generate_selection(self):
sp_auth, cache_handler = spotify_oauth2(self.request)
spotify = spotipy.Spotify(oauth_manager=sp_auth)
user_playlists = spotify.current_user_playlists(limit=10)
choices = []
for playlist in user_playlists["items"]:
playlist_choice = (playlist["name"], playlist["id"])
choices.append(playlist_choice)
return choices
I am using django modeltranslation on a FileField.
I would like this file to be uploaded in a path /path/to/file/<lang>/file.ext and I guess the best way is to extract the lang from the fieldname (file_en, file_it, file_fr, ...) where upload_to is operating.
# models.py
def upload_method(instance, filename):
lang = "" # how to get this variable?
return f"/path/to/file/{lang}/file.ext"
class Obj(models.Model):
file = models.FileField(upload_to=upload_method)
# translation.py
#register(models.Obj)
class ObjTranslationOptions(TranslationOptions):
fields = ("file", )
Something like this should work.
from modeltranslation.translator import translator
from django.db.models import FileField
import os
class TranslationMeta(type):
def __init__(self, name, bases, attrs):
for attrname, attrvalue in attrs.items():
if self.is_translated_field(name, attrname):
field = attrvalue
if isinstance(field, FileField):
self.update_upload_to(field, attrname)
super().__init__(name, bases, attrs)
def is_translated_field(self, class_name, attr_name):
opts = translator.get_options_for_model(self)
return attr_name in opts.get_field_names()
def update_upload_to(self, field, attr_name):
opts = translator.get_options_for_model(self)
translated_fields = opts.fields[attr_name]
for trans_field in translated_fields:
# print(trans_field.name)
# print(trans_field.language)
trans_field.upload_to = self.custom_upload_to(field.upload_to, trans_field.language)
def custom_upload_to(self, base_upload_to, language):
# If the original upload_to parameter is a callable,
# return a function that calls the original upload_to
# function and inserts the language as the final folder
# in the path
# If the original upload_to function returned /path/to/file.png,
# then the final path will be /path/to/en/file.png for the
# english field
if callable(base_upload_to):
def upload_to(instance, filename):
path = base_upload_to(instance, filename)
return os.path.join(
os.path.dirname(path),
language,
os.path.basename(path))
return upload_to
# If the original upload_to parameter is a path as a string,
# insert the language as the final folder in the path
# /path/to/file.png becomes /path/to/en/file.png for the
# english field
else:
return os.path.join(
os.path.dirname(base_upload_to),
language,
os.path.basename(base_upload_to))
# This is how you would use this class
class MyModel(models.Model, metaclass=TranslationMeta):
field = FileField()
m = MyModel(models.Model)
print(m.field.upload_to)
It uses introspection to dynamically override the upload_to parameter of every language-specific FileField generated by django-modeltranslation behind the scenes.
With this model for instance:
class MyModel(models.Model):
field = FileField(upload_to=...)
if you have defined field as a translatable field by adding
from modeltranslation.translator import register, TranslationOptions
from . import models
#register(models.MyModel)
class MyModelTranslationOptions(TranslationOptions):
fields = ("field",)
in translation.py, django-modeltranslation will generate something like
class MyModel(models.Model):
field = FileField(upload_to=...)
field_en = FileField(upload_to=...)
field_fr = FileField(upload_to=...)
if you have en and fr defined in your LANGUAGES settings.
If the upload_to parameter passed to the FileField was a path as a string, it is overridden with the same path in which a folder for the language is inserted.
If it is a function, then the folder for the language is inserted in the path returned by this function.
For instance if you have
class MyModel(models.Model):
field = FileField(upload_to="/path/to/file.png")
or
def get_upload_path(instance, filename):
return "path/to/file.png"
class MyModel(models.Model):
field = FileField(upload_to=get_upload_path)
then, in both cases:
the English version of the file will be stored under /path/to/en/file.png
the French version of the file will be stored under /path/to/fr/file.png
Try to use get_language method:
from django.utils.translation import get_language
def upload_method(instance, filename):
lang = get_language()
return f"/path/to/file/{lang}/file.ext"
I need to override variables (or pass dynamic data) to imported class.
filters.py
import django_filters
from .models import Gate, Tram, OperationArea, Bogie
from distutils.util import strtobool
from django import forms
class GateFilter(django_filters.FilterSet):
# Prepare dynamic lists with choices
tram_list = [(id, number) for id, number in Tram.objects.all().values_list('id', 'number')]
bogie_list = [(id, number) for id, number in Bogie.objects.all().values_list('id', 'number')]
area_list = [(id, area) for id, area in OperationArea.objects.all().values_list('id', 'area')]
# Generate fields
tram = django_filters.MultipleChoiceFilter(choices=tram_list, label=u'Tramwaj')
car = django_filters.MultipleChoiceFilter(choices=Gate.CAR_SYMBOLS, label=u'Człon')
bogie = django_filters.MultipleChoiceFilter(choices=bogie_list, label=u'Wózek')
bogie_type = django_filters.MultipleChoiceFilter(choices=Gate.BOGIE_TYPES, label=u'Typ wózka')
area = django_filters.MultipleChoiceFilter(choices=area_list, label=u'Obszar')
operation_no = django_filters.CharFilter(label=u'Numer operacji', widget=forms.TextInput(attrs={'size': '16px'}))
status = django_filters.MultipleChoiceFilter(choices=Gate.GATE_STATUSES, label=u'Status')
rating = django_filters.MultipleChoiceFilter(choices=Gate.GATE_GRADES, label=u'Ocena')
class Meta:
pass
views.py
from .filters import GateFilter
class GateListView(generic.ListView):
queryset = None
gate_type = None
template_name = 'qapp/gate/list.html'
context_object_name = 'gate_list'
paginate_by = 20
def get_queryset(self):
# Type is stored in database as big-letter word, so 'bjc' != 'BJC'.
if self.gate_type.upper() == 'BJW':
ordering = ['bogie', 'bogie_type']
else:
ordering = ['tram', 'car']
queryset = Gate.objects.filter(type=self.gate_type.upper()).order_by(*ordering)
self.gate_list = GateFilter(self.request.GET, queryset=queryset)
return self.gate_list.qs.distinct()
def get_context_data(self, **kwargs):
context = super(GateListView, self).get_context_data(**kwargs)
# Return Gate.type to template.
context['gate_type'] = self.gate_type
# Return object (for generating form) to template.
context['gate_list_filter'] = self.gate_list
return context
As you can see, in the filters.py, the data for variables tram_list, bogie_list and area_list are dynamic (fetched from database).
But during importing this class to views.py, this data becomes static.
I tried to override this values:
using #classmethod decorator in class GateFilter, and calling it
before setting self.gate_list object,
in views.py using GateFilter.tram_list (and the rest) notation,
No luck.
I can't use reload() function, due to import type (from .filters import GateFilter).
Currently for update lists in filters.py I need to rerun whole app.
This is unacceptable for business logic of my app.
This is the wrong approach. Rather, you should be using the filters that are aware of querysets and that evaluate them when required: ModelChoiceFilter and ModelMultipleChoiceFilter.
class GateFilter(django_filters.FilterSet):
team = django_filters.ModelMultipleChoiceFilter(queryset=Tram.objects.all())
I am currently working on a project where I generate pandas DataFrames as results of analysis. I am developing in Django and would like to use a "data" field in a "Results" model to store the pandas DataFrame.
It appears that HDF5(HDF Store) is the most efficient way to store my pandas DataFrames. However, I do not know how to create the custom field in my model to save it. I will show simplified views.py and models.py below to illustrate.
models.py
class Result(model.Model):
scenario = models.ForeignKey(Scenario)
# HOW DO I Store HDFStore
data = models.HDF5Field()
views.py
class AnalysisAPI(View):
model = Result
def get(self, request):
request_dict = request.GET.dict()
scenario_id = request_dict['scenario_id']
scenario = Scenario.objects.get(pk=scenario_id)
result = self.model.objects.get(scenario=scenario)
analysis_results_df = result.data['analysis_results_df']
return JsonResponse(
analysis_results_df.to_json(orient="records")
)
def post(self, request):
request_dict = request.POST.dict()
scenario_id = request_dict['scenario_id']
scenario = Scenario.objects.get(pk=scenario_id)
record_list = request_dict['record_list']
analysis_results_df = run_analysis(record_list)
data = HDFStore('store.h5')
data['analysis_results_df'] = analysis_results_df
new_result = self.model(scenario=scenario, data=data)
new_result.save()
return JsonResponse(
dict(status="OK", message="Analysis results saved.")
)
I appreciate any help and I am also open to another storage method, such as Pickle, with similar performance provided I can use it with Django.
You can create a custom Model field that saves your data to a file in storage and saves the relative file path to the database.
Here is how you could subclass models.CharField in your app's fields.py:
import os
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.db import models
from django.utils.translation import gettext_lazy as _
class DataFrameField(models.CharField):
"""
custom field to save Pandas DataFrame to the hdf5 file format
as advised in the official pandas documentation:
http://pandas.pydata.org/pandas-docs/stable/io.html#io-perf
"""
attr_class = DataFrame
default_error_messages = {
"invalid": _("Please provide a DataFrame object"),
}
def __init__(
self,
verbose_name=None,
name=None,
upload_to="data",
storage=None,
unique_fields=[],
**kwargs
):
self.storage = storage or default_storage
self.upload_to = upload_to
self.unique_fields = unique_fields
kwargs.setdefault("max_length", 100)
super().__init__(verbose_name, name, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if kwargs.get("max_length") == 100:
del kwargs["max_length"]
if self.upload_to != "data":
kwargs["upload_to"] = self.upload_to
if self.storage is not default_storage:
kwargs["storage"] = self.storage
kwargs["unique_fields"] = self.unique_fields
return name, path, args, kwargs
The __init__ and deconstruct methods are very much inspired by the Django original FileField. There is an additional unique_fields parameter that is useful for creating predictable unique file names.
def from_db_value(self, value, expression, connection):
"""
return a DataFrame object from the filepath saved in DB
"""
if value is None:
return value
return self.retrieve_dataframe(value)
def get_absolute_path(self, value):
"""
return absolute path based on the value saved in the Database.
"""
return self.storage.path(value)
def retrieve_dataframe(self, value):
"""
return the pandas DataFrame and add filepath as property to Dataframe
"""
# read dataframe from storage
absolute_filepath = self.get_absolute_path(value)
dataframe = read_hdf(absolute_filepath)
# add relative filepath as instance property for later use
dataframe.filepath = value
return dataframe
You load the DataFrame to memory from storage with the from_db_value method based on the file path saved in the database.
When retrieving the DataFrame, you also add the file path as instance property to it, so that you can use that value when saving the DataFrame back to the database.
def pre_save(self, model_instance, add):
"""
save the dataframe field to an hdf5 field before saving the model
"""
dataframe = super().pre_save(model_instance, add)
if dataframe is None:
return dataframe
if not isinstance(dataframe, DataFrame):
raise ValidationError(
self.error_messages["invalid"], code="invalid",
)
self.save_dataframe_to_file(dataframe, model_instance)
return dataframe
def get_prep_value(self, value):
"""
save the value of the dataframe.filepath set in pre_save
"""
if value is None:
return value
# save only the filepath to the database
if value.filepath:
return value.filepath
def save_dataframe_to_file(self, dataframe, model_instance):
"""
write the Dataframe into an hdf5 file in storage at filepath
"""
# try to retrieve the filepath set when loading from the database
if not dataframe.get("filepath"):
dataframe.filepath = self.generate_filepath(model_instance)
full_filepath = self.storage.path(dataframe.filepath)
# Create any intermediate directories that do not exist.
# shamelessly copied from Django's original Storage class
directory = os.path.dirname(full_filepath)
if not os.path.exists(directory):
try:
if self.storage.directory_permissions_mode is not None:
# os.makedirs applies the global umask, so we reset it,
# for consistency with file_permissions_mode behavior.
old_umask = os.umask(0)
try:
os.makedirs(directory, self.storage.directory_permissions_mode)
finally:
os.umask(old_umask)
else:
os.makedirs(directory)
except FileExistsError:
# There's a race between os.path.exists() and os.makedirs().
# If os.makedirs() fails with FileExistsError, the directory
# was created concurrently.
pass
if not os.path.isdir(directory):
raise IOError("%s exists and is not a directory." % directory)
# save to storage
dataframe.to_hdf(full_filepath, "df", mode="w", format="fixed")
def generate_filepath(self, instance):
"""
return a filepath based on the model's class name, dataframe_field and unique fields
"""
# create filename based on instance and field name
class_name = instance.__class__.__name__
# generate unique id from unique fields:
unique_id_values = []
for field in self.unique_fields:
unique_field_value = getattr(instance, field)
# get field value or id if the field value is a related model instance
unique_id_values.append(
str(getattr(unique_field_value, "id", unique_field_value))
)
# filename, for example: route_data_<uuid>.h5
filename = "{class_name}_{field_name}_{unique_id}.h5".format(
class_name=class_name.lower(),
field_name=self.name,
unique_id="".join(unique_id_values),
)
# generate filepath
dirname = self.upload_to
filepath = os.path.join(dirname, filename)
return self.storage.generate_filename(filepath)
Save the DataFrame to an hdf5 file with the pre_save method and save the file path to the Database in get_prep_value.
In my case it helped to use a uuid Model Field to create the unique file name, because for new model instances, the pk was not yet available in the pre-save method, but the uuid value was.
You can then use this field in your models.py:
from .fields import DataFrameField
# track data as a pandas DataFrame
data = DataFrameField(null=True, upload_to="data", unique_fields=["uuid"])
Please note that you cannot use this field in the Django admin or in a Model form. That would require additional work on a custom form Widget to edit the DataFrame content in the front-end, probably as a table.
Also beware that for tests, I had to override the MEDIA_ROOT setting with a temporary directory using tempfile to prevent creating useless files in the actual media folder.
It's not HDF5, but check out picklefield:
from picklefield.fields import PickledObjectField
class Result(model.Model):
scenario = models.ForeignKey(Scenario)
data = PickledObjectField(blank=True, null=True)
https://pypi.python.org/pypi/django-picklefield
I'd like to save my files using the primary key of the entry.
Here is my code:
def get_nzb_filename(instance, filename):
if not instance.pk:
instance.save() # Does not work.
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (instance.pk, name_slug)
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
I know the first time an object is saved the primary key isn't available, so I'm willing to take the extra hit to save the object just to get the primary key, and then continue on.
The above code doesn't work. It throws the following error:
maximum recursion depth exceeded while calling a Python object
I'm assuming this is an infinite loop. Calling the save method would call the get_nzb_filename method, which would again call the save method, and so on.
I'm using the latest version of the Django trunk.
How can I get the primary key so I can use it to save my uploaded files?
Update #muhuk:
I like your solution. Can you help me implement it? I've updated my code to the following and the error is 'File' object has no attribute 'create'. Perhaps I'm using what you've written out of context?
def create_with_pk(self):
instance = self.create()
instance.save()
return instance
def get_nzb_filename(instance, filename):
if not instance.pk:
create_with_pk(instance)
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (instance.pk, name_slug)
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename, blank=True, null=True)
name = models.CharField(max_length=256)
Instead of enforcing the required field in my model I'll do it in my Form class. No problem.
It seems you'll need to pre-generate your File models with empty file fields first. Then pick up one and save it with the given file object.
You can have a custom manager method like this;
def create_with_pk(self):
instance = self.create()
instance.save() # probably this line is unneeded
return instance
But this will be troublesome if either of your fields is required. Because you are initially creating a null object, you can't enforce required fields on the model level.
EDIT
create_with_pk is supposed to be a custom manager method, in your code it is just a regular method. Hence self is meaningless. It is all properly documented with examples.
You can do this by setting upload_to to a temporary location and by creating a custom save method.
The save method should call super first, to generate the primary key (this will save the file to the temporary location). Then you can rename the file using the primary key and move it to it's proper location. Call super one more time to save the changes and you are good to go! This worked well for me when I came across this exact issue.
For example:
class File( models.Model ):
nzb = models.FileField( upload_to='temp' )
def save( self, *args, **kwargs ):
# Call save first, to create a primary key
super( File, self ).save( *args, **kwargs )
nzb = self.nzb
if nzb:
# Create new filename, using primary key and file extension
oldfile = self.nzb.name
dot = oldfile.rfind( '.' )
newfile = str( self.pk ) + oldfile[dot:]
# Create new file and remove old one
if newfile != oldfile:
self.nzb.storage.delete( newfile )
self.nzb.storage.save( newfile, nzb )
self.nzb.name = newfile
self.nzb.close()
self.nzb.storage.delete( oldfile )
# Save again to keep changes
super( File, self ).save( *args, **kwargs )
Context
Had the same issue.
Solved it attributing an id to the current object by saving the object first.
Method
create a custom upload_to function
detect if object has pk
if not, save instance first, retrieve the pk and assign it to the object
generate your path with that
Sample working code :
class Image(models.Model):
def upload_path(self, filename):
if not self.pk:
i = Image.objects.create()
self.id = self.pk = i.id
return "my/path/%s" % str(self.id)
file = models.ImageField(upload_to=upload_path)
You can create pre_save and post_save signals. Actual file saving will be in post_save, when pk is already created.
Do not forget to include signals in app.py so they work.
Here is an example:
_UNSAVED_FILE_FIELD = 'unsaved_file'
#receiver(pre_save, sender=File)
def skip_saving_file_field(sender, instance: File, **kwargs):
if not instance.pk and not hasattr(instance, _UNSAVED_FILE_FIELD):
setattr(instance, _UNSAVED_FILE_FIELD, instance.image)
instance.nzb = None
#receiver(post_save, sender=File)
def save_file_field(sender, instance: Icon, created, **kwargs):
if created and hasattr(instance, _UNSAVED_FILE_FIELD):
instance.nzb = getattr(instance, _UNSAVED_FILE_FIELD)
instance.save()
Here are 2 possible solutions:
Retrieve id before inserting a row
For simplicity I use postgresql db, although it is possible to adjust implementation for your db backend.
By default django creates id as bigserial (or serial depending on DEFAULT_AUTO_FIELD). For example, this model:
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
Produces the following DDL:
CREATE TABLE "example_file" ("id" bigserial NOT NULL PRIMARY KEY, "nzb" varchar(100) NOT NULL, "name" varchar(256) NOT NULL);
There is no explicit sequence specification. By default bigserial creates sequence name in the form of tablename_colname_seq (example_file_id_seq in our case)
The solution is to retrieve this id using nextval :
def get_nextval(model, using=None):
seq_name = f"{model._meta.db_table}_id_seq"
if using is None:
using = "default"
with connections[using].cursor() as cursor:
cursor.execute("select nextval(%s)", [seq_name])
return cursor.fetchone()[0]
And set it before saving the model:
class File(models.Model):
# fields definition
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if not self.pk:
self.pk = get_nextval(self, using=using)
force_insert = True
super().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
Note that we rely on force_insert behavior, so make sure to read documentation and cover your code with tests:
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ModelForm
from django.test import TestCase
from example import models
class FileForm(ModelForm):
class Meta:
model = models.File
fields = (
"nzb",
"name",
)
class FileTest(TestCase):
def test(self):
form = FileForm(
{
"name": "picture",
},
{
"nzb": SimpleUploadedFile("filename", b"content"),
},
)
self.assertTrue(form.is_valid())
form.save()
self.assertEqual(models.File.objects.count(), 1)
f = models.File.objects.first()
self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
Insert without nzt then update with actual nzt value
The idea is self-explanatory - we basically pop nzt on the object creation and save object again after we know id:
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
nzb = None
if not self.pk:
nzb = self.nzb
self.nzb = None
super().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
if nzb:
self.nzb = nzb
super().save(
force_insert=False,
force_update=True,
using=using,
update_fields=["nzb"],
)
Test is updated to check actual queries:
def test(self):
form = FileForm(
{
"name": "picture",
},
{
"nzb": SimpleUploadedFile("filename", b"content"),
},
)
self.assertTrue(form.is_valid())
with CaptureQueriesContext(connection) as ctx:
form.save()
self.assertEqual(models.File.objects.count(), 1)
f = models.File.objects.first()
self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
self.assertEqual(len(ctx.captured_queries), 2)
insert, update = ctx.captured_queries
self.assertEqual(
insert["sql"],
'''INSERT INTO "example_file" ("nzb", "name") VALUES ('', 'picture') RETURNING "example_file"."id"''',
)
self.assertRegexpMatches(
update["sql"],
rf"""UPDATE "example_file" SET "nzb" = 'files/{f.pk}_picture(.*)\.nzb' WHERE "example_file"."id" = {f.pk}""",
)
Ty, is there a reason you rolled your own slugify filter?
Django ships with a built-in slugify filter, you can use it like so:
from django.template.defaultfilters import slugify
slug = slugify(some_string)
Not sure if you were aware it was available to use...
You can use the next available primary key ID:
class Document(models.Model):
def upload_path(self, filename):
if not self.pk:
document_next_id = Document.objects.order_by('-id').first().id + 1
self.id = self.pk = document_next_id
return "my/path/document-%s" % str(self.pk)
document = models.FileField(upload_to=upload_path)
Details
My example is a modification of #vinyll's answer, however, the problem Giles mentioned in his comment (two objects being created) is resolved here.
I am aware that my answer is not perfect, and there can be issues with the "next available ID", e.g., when more users will attempt to submit many forms at once. Giles's answer is more robust, mine is simpler (no need to generate temp files, then moving files, and deleting them). For simpler applications, this will be enough.
Also credits to Tjorriemorrie for the clear example on how to get the next available ID of an object.
Well I'm not sure of my answer but -
use nested models, if you can -
class File(models.Model):
name = models.CharField(max_length=256)
class FileName(models.Model):
def get_nzb_filename(instance, filename):
return instance.name
name = models.ForeignKey(File)
nzb = models.FileField(upload_to=get_nzb_filename)
And in create method -
File_name = validated_data.pop(file_name_data)
file = File.objects.create(validated_data)
F = FileName.objects.create(name=file, **File_name)