dynamic FilePathField question - django

I have a model where the location of pdf directory I'm pointing to with my FilePathField is based on the "client" and "job_number" fields.
class CCEntry(models.Model):
client = models.CharField(default="C_Comm", max_length=64)
job_number = models.CharField(max_length=30, unique=False, blank=False, null=False)
filename = models.CharField(max_length=64, unique=False, blank=True, null=True)
pdf = models.FilePathField(path="site_media/jobs/%s %s", match=".*\.pdf$", recursive=True
#property
def pdf(self):
return "site_media/jobs/%s %s" % (self.client, self.job_number)
def __unicode__ (self):
return u'%s %s' % (self.client, self.filename)
class Admin:
pass
I've tried to pass the client and job_number data to the pdf field dynamically by using a #property method on the model class, but either my approach or my syntax is fualty because the entire pdf field disappears in the admin. Any pointers on what I'm doing wrong?

Based on your subsequent post on similar functionality in the FileField (see last link below)
And my inability to get any of the above to work, I'm gonna hazard a guess that it's not yet possible for the FilePathField field type.
I know that passing a callable works for most fields' 'default' parameters...
https://docs.djangoproject.com/en/dev/ref/models/fields/#default
... as it appears to work for the upload_to param of FieldField
(eg https://stackoverflow.com/questions/10643889/dynamic-upload-field-arguments/ ) andImageField` (eg Django - passing extra arguments into upload_to callable function )
Anyone interested in extending FilePathField to include this feature?

Anyone interested in extending FilePathField to include this feature?
I'd love to see this extension!
Just for the record, this is the solution that worked for me (django 1.3):
# models.py
class Analysis(models.Model):
run = models.ForeignKey(SampleRun)
# Directory name depends on the foreign key
# (directory was created outside Django and gets filled by a script)
bam_file = models.FilePathField(max_length=500, blank=True, null=True)
# admin.py
class CustomAnalysisModelForm(forms.ModelForm):
class Meta:
model = Analysis
def __init__(self, *args, **kwargs):
super(CustomAnalysisModelForm, self).__init__(*args, **kwargs)
# This is an update
if self.instance.id:
# set dynamic path
mypath = settings.DATA_PATH + self.instance.run.sample.name
self.fields['bam_file'] = forms.FilePathField(path=mypath, match=".*bam$", recursive=True)
class AnalysisAdmin(admin.ModelAdmin):
form = CustomAnalysisModelForm
Hope this helps somebody out there.

try to set the path value as callable function
def get_path(instance, filename):
return "site_media/jobs/%s_%s/%s" % (instance.client, instance.job_number, filename)
class CCEntry(models.Model):
....
pdf = models.FilePathField(path=get_path, match=".*\.pdf$", recursive=True)
but I'm not sure if this works, I didn't test it.

Added an implementation of this based on Django v1.9 FilePathField implementation:
from django.db.models import FilePathField
class DynamicFilePathField(FilePathField):
def __init__(self, verbose_name=None, name=None, path='', match=None,
recursive=False, allow_files=True, allow_folders=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
if callable(self.path):
self.pathfunc, self.path = self.path, self.path()
self.allow_files, self.allow_folders = allow_files, allow_folders
kwargs['max_length'] = kwargs.get('max_length', 100)
super(FilePathField, self).__init__(verbose_name, name, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(FilePathField, self).deconstruct()
if hasattr(self, "pathfunc"):
kwargs['path'] = self.pathfunc
return name, path, args, kwargs
And example use:
import os
from django.db import models
def get_data_path():
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'data'))
class GenomeAssembly(models.Model):
name = models.CharField(
unique=True,
max_length=32)
chromosome_size_file = DynamicFilePathField(
unique=True,
max_length=128,
path=get_data_path,
recursive=False)

Related

Triyng to slugify url from title posts in a Django blog

Made the slug variable for futures urls and did ./makemigrations and migrate and the parameter appears in the admin panel, but when I try to migrate after I made an empty "theblog" migration I get this error:
class Migration(migrations.Migration):
File "xxx/theblog/models.py", line 104, in Migration
migrations.RunPython(generate_slugs_for_old_posts, reverse=reverse_func),
TypeError: __init__() got an unexpected keyword argument 'reverse'
changed the slug parameter from null and blank to unique but that doesn't seem to be the problem now. I know that the problem is coming from get_success_url but really don't know how to fix it.
models.py:
from django.utils.text import slugify
class Post(models.Model):
title= models.CharField(max_length=100)
header_image = models.ImageField(null=True , blank=True, upload_to="images/")
title_tag= models.CharField(max_length=100)
author= models.ForeignKey(User, on_delete=models.CASCADE)
body = RichTextUploadingField(extra_plugins=
['youtube', 'codesnippet'], external_plugin_resources= [('youtube','/static/ckeditor/youtube/','plugin.js'), ('codesnippet','/static/ckeditor/codesnippet/','plugin.js')])
post_date = models.DateTimeField(auto_now_add=True)
category = models.CharField(max_length=50, default='uncategorized')
slug = models.SlugField(unique=True)
snippet = models.CharField(max_length=200)
status = models.IntegerField(choices=STATUS, default=0)
likes = models.ManyToManyField(User, blank=True, related_name='blog_posts')
def save(self, *args, **kwargs):
self.slug = self.generate_slug()
return super().save(*args, **kwargs)
def generate_slug(self, save_to_obj=False, add_random_suffix=True):
generated_slug = slugify(self.title)
random_suffix = ""
if add_random_suffix:
random_suffix = ''.join([
random.choice(string.ascii_letters + string.digits)
for i in range(5)
])
generated_slug += '-%s' % random_suffix
if save_to_obj:
self.slug = generated_slug
self.save(update_fields=['slug'])
return generated_slug
def generate_slugs_for_old_posts(apps, schema_editor):
Post = apps.get_model("theblog", "Post")
for post in Post.objects.all():
post.slug = slugify(post.title)
post.save(update_fields=['slug'])
def reverse_func(apps, schema_editor):
pass # just pass
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.RunPython(generate_slugs_for_old_posts, reverse=reverse_func),
]
First of all you must add nullable slug field to your Post model. Here is Django docs on slug field. You must implement way of generating slug values too. You can implement generate_slug(...) method on your model for example:
import string # for string constants
import random # for generating random strings
# other imports ...
from django.utils.text import slugify
# other imports ...
class Post(models.Model):
# ...
slug = models.SlugField(null=True, blank=True, unique=True)
# ...
def save(self, *args, **kwargs):
self.slug = self.generate_slug()
return super().save(*args, **kwargs)
def generate_slug(self, save_to_obj=False, add_random_suffix=True):
"""
Generates and returns slug for this obj.
If `save_to_obj` is True, then saves to current obj.
Warning: setting `save_to_obj` to True
when called from `.save()` method
can lead to recursion error!
`add_random_suffix ` is to make sure that slug field has unique value.
"""
# We rely on django's slugify function here. But if
# it is not sufficient for you needs, you can implement
# you own way of generating slugs.
generated_slug = slugify(self.title)
# Generate random suffix here.
random_suffix = ""
if add_random_suffix:
random_suffix = ''.join([
random.choice(string.ascii_letters + string.digits)
for i in range(5)
])
generated_slug += '-%s' % random_suffix
if save_to_obj:
self.slug = generated_slug
self.save(update_fields=['slug'])
return generated_slug
Now on every object save you will automatically generate slug for your object.
To deal with old posts that do not have slug field set. You must create custom migration using RunPython (Django docs):
First run this command
python manage.py makemigrations <APP_NAME> --empty
Replace <APP_NAME> with your actual app name where Post model is located.
It will generate an empty migration file:
from django.utils.text import slugify
from django.db import migrations
def generate_slugs_for_old_posts(apps, schema_editor):
Post = apps.get_model("<APP_NAME>", "Post") # replace <APP_NAME> with actual app name
# dummy way
for post in Post.objects.all():
# Do not try to use `generate_slug` method
# here, you probably will get error saying
# that Post does not have method called `generate_slug`
# as it is not the actual class you have defined in your
# models.py!
post.slug = slugify(post.title)
post.save(update_fields=['slug'])
def reverse_func(apps, schema_editor):
pass # just pass
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.RunPython(generate_slugs_for_old_posts, reverse=reverse_func),
]
After that point you may alter you slug field and make it non nullable:
class Post(models.Model):
# ...
slug = models.SlugField(unique=True)
# ...
Now python manage.py migrate, this will make slug fields non nullable for future posts, but it may give you a warning saying that you are trying to make existing column
non nullable. Here you have an option where it says "I have created custom migration" or something like that. Select it.
Now when your posts have slugs, you must fix your views so they accept slug parameters from url. The trick here is to make sure that your posts are also acceptable by ID. As someone already may have a link to some post with ID. If you remove URLs that takes an ID argument, then that someone might not be able to use that old link anymore.
you may also redirect old urls to the new ones
urls.py
[..]
from django.views.generic.base import RedirectView
urlpatterns = [
# Redirect old links:
path('article/<int:pk>', RedirectView.as_view(url='article/<slug:url>', permanent=True)),
# You won't need this path any more
# path('article/<int:pk>', ArticleDetailView.as_view(), name="article-detail"),
# The new path with slug
path('article/<slug:url>', ArticleDetailView.as_view(), name="article-detail"),
[..]
]
refer to https://docs.djangoproject.com/en/3.1/ref/class-based-views/base/#redirectview

Custom Django FileField url widget won't upload any file in admin change_form

I have a Files table with information about uploaded files in a remote directory. This is the model for that table:
class Files(models.Model):
id = models.AutoField(primary_key=True)
subjectid = models.ForeignKey('Subjects', models.DO_NOTHING, db_column='subjectid')
filetypeid = models.ForeignKey(FileTypes, models.DO_NOTHING, db_column='filetypeid')
filedescid = models.ForeignKey(FileDescription, models.DO_NOTHING, db_column='filedescid')
filepath = models.CharField(max_length=45, blank=True, null=True)
filename = models.FileField(upload_to='attachments/', blank=True, null=True)
ispublic = models.IntegerField(choices=YESNO)
extra_info = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
return self.filename.name or ''
class Meta:
managed = False
db_table = 'files'
verbose_name_plural = 'files'
I've created my own URL widget to replace the Django FileField url shown as 'Currently:' in the change_form template. The link points to a view that downloads the file. So far, so good it works but the problem is that when I try to add a new file I can select the new file with the Browse file button but when I click on Save the field filename field is empty and no file is uploaded.
class MyAdminURLFieldWidget(URLInput):
template_name = 'admin/widgets/url.html'
def __init__(self, attrs=None):
#final_attrs = {'class': 'vURLField'}
final_attrs = {'type': 'file'}
if attrs is not None:
final_attrs.update(attrs)
super(MyAdminURLFieldWidget, self).__init__(attrs=final_attrs)
def get_context(self, name, value, attrs):
context = super(MyAdminURLFieldWidget, self).get_context(name, value, attrs)
context['current_label'] = _('Currently:')
context['change_label'] = _('Change:')
context['widget']['href'] = smart_urlquote('/DownloadView/' + str(value.instance.id) + '/attachment/') if value else ''
return context
class FilesAdmin(admin.ModelAdmin):
list_display = ('id', '_animalid', '_filename', '_filedesc', 'ispublic', 'extra_info')
search_fields = ('subjectid__animalid','filename')
list_per_page = 50
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'filename':
request = kwargs.pop("request", None)
kwargs['widget'] = MyAdminURLFieldWidget
return db_field.formfield(**kwargs)
else:
return super(FilesAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def _animalid(self, obj):
return obj.subjectid.animalid
def _filename(self, obj):
return obj.filename.name
def _filedesc(self, obj):
return obj.filedescid.description
Can anybody tell what I'm missing here?
Hi lost Django community,
I will answer my own question since it seems that nobody was able to realize the answer. It happens that as newbie I'm following examples I've found here and there but there is little about sub-classing the FileField associated widget. So, after diving deep into the Django code I've found the answer. In my case the problem was I derived my MyAdminURLFieldWidget from URLInput instead of the correct subclass ClearableFileInput.
You are welcome.

upload image to a folder with model's title name django [duplicate]

I'm trying to set up my uploads so that if user joe uploads a file it goes to MEDIA_ROOT/joe as opposed to having everyone's files go to MEDIA_ROOT. The problem is I don't know how to define this in the model. Here is how it currently looks:
class Content(models.Model):
name = models.CharField(max_length=200)
user = models.ForeignKey(User)
file = models.FileField(upload_to='.')
So what I want is instead of '.' as the upload_to, have it be the user's name.
I understand that as of Django 1.0 you can define your own function to handle the upload_to but that function has no idea of who the user will be either so I'm a bit lost.
Thanks for the help!
You've probably read the documentation, so here's an easy example to make it make sense:
def content_file_name(instance, filename):
return '/'.join(['content', instance.user.username, filename])
class Content(models.Model):
name = models.CharField(max_length=200)
user = models.ForeignKey(User)
file = models.FileField(upload_to=content_file_name)
As you can see, you don't even need to use the filename given - you could override that in your upload_to callable too if you liked.
This really helped. For a bit more brevity's sake, decided to use lambda in my case:
file = models.FileField(
upload_to=lambda instance, filename: '/'.join(['mymodel', str(instance.pk), filename]),
)
A note on using the 'instance' object's pk value. According to the documentation:
In most cases, this object will not have been saved to the database yet, so if it uses the default AutoField, it might not yet have a value for its primary key field.
Therefore the validity of using pk depends on how your particular model is defined.
If you have problems with migrations you probably should be using #deconstructible decorator.
import datetime
import os
import unicodedata
from django.core.files.storage import default_storage
from django.utils.deconstruct import deconstructible
from django.utils.encoding import force_text, force_str
#deconstructible
class UploadToPath(object):
def __init__(self, upload_to):
self.upload_to = upload_to
def __call__(self, instance, filename):
return self.generate_filename(filename)
def get_directory_name(self):
return os.path.normpath(force_text(datetime.datetime.now().strftime(force_str(self.upload_to))))
def get_filename(self, filename):
filename = default_storage.get_valid_name(os.path.basename(filename))
filename = force_text(filename)
filename = unicodedata.normalize('NFKD', filename).encode('ascii', 'ignore').decode('ascii')
return os.path.normpath(filename)
def generate_filename(self, filename):
return os.path.join(self.get_directory_name(), self.get_filename(filename))
Usage:
class MyModel(models.Model):
file = models.FileField(upload_to=UploadToPath('files/%Y/%m/%d'), max_length=255)
I wanted to change the upload path in runtime, and none of the solutions were suitable for this need.
this is what I've done:
class Content(models.Model):
name = models.CharField(max_length=200)
user = models.ForeignKey(User)
file = models.FileField(upload_to=DynamicUploadPath.get_file_path)
class ContentSerializer(serializers.ModelSerializer):
class Meta:
model = Content
fields = '__all__'
class UploadDir(models.TextChoices):
PRODUCT = 'PRD', _('Product')
USER_PROFILE = 'UP', _('User Profile')
class DynamicUploadPath:
dir: UploadDir = None
#classmethod
def get_file_path(cls, instance, filename):
return str(cls.dir.name.lower() + '/' + filename)
def set_DynamicUploadPath(dir: UploadDir):
DynamicUploadPath.dir = dir
class UploadFile(APIView):
parser_classes = (MultiPartParser, FormParser)
def post(self, request):
# file save path: MEDIA_ROOT/product/filename
set_DynamicUploadPath(UploadDir.PRODUCT)
# file save path: MEDIA_ROOT/user_profile/filename
# set_DynamicUploadPath(UploadDir.USER_PROFILE)
serializer = ContentSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
If you have a user instance, let there be a quick setup to generate
<model-slug>/<username>-<first_name>-<last_name>/filename-random.png
eg:
/medias/content/ft0004-john-doe/filename-lkl9237.png
def upload_directory_name(instance, filename):
user = getattr(instance, 'user', None)
if user:
name = f"{user.username}-{user.get_full_name().replace(' ', '-')}"
else:
name=str(instance)
model_name = instance._meta.verbose_name.replace(' ', '-')
return str(os.path.pathsep).join([model_name, name, filename])
class Content(models.Model):
name = models.CharField(max_length=200)
user = models.ForeignKey(User)
file = models.FileField(upload_to=upload_directory_name)
[A Modified Version of #SmileyChris ]

Django models as optgroup choices

I am trying to get my installed models as opt group choices.
I have to do it in my model, not form.
so far I tried.
def get_installed_model_choices():
returnval = []
for app in settings.SYSTEM_INSTALLED_APPS:
app_config = apps.get_app_config(app)
app_models = app_config.get_models()
model_choices = []
for model in app_models:
model_choices.append([model._meta.model_name, model._meta.verbose_name])
returnval.append([app_config.verbose_name, model_choices])
print returnval
return returnval
And in my model field.
model = custom_model_fields.PanNoneBlankCharField(choices = get_installed_model_choices(),verbose_name=_('model'),
max_length=20)
I get this error.
"Models for app '%s' haven't been imported yet." % self.label)
Second approach:
This is comething I saw on the internet,
def __init__(self, *args, **kwargs):
super(PanUserModelPermissions, self).__init__(*args, **kwargs)
self._meta.get_field('model')._choices = \
lazy(get_installed_model_choices, list)()
This didnt work either, had no affect
I dont want choices to be static, is there anyway to achieve this?
if you just want to choice from your models,you can use content_type as:
from django.contrib.contenttypes.models import ContentType
content_type = models.ForeignKey(ContentType,
null=True,
blank=True,
verbose_name=u'content_type ')
more info here.

Auto Generated Slugs in Django Admin

I have an app that will one day allow front-end crud, which will create the slug with slugify. Right now though, all the object creation is being done in the admin area and I was wondering if there is a way to auto generate slugs while creating and saving an object from within admin?
Here is the method for slugify for the front-end; not sure if its even relevant. Thank you.
def create_slug(instance, new_slug=None):
slug = slugify(instance.title)
if new_slug is not None:
slug = new_slug
qs = Veteran.objects.filter(slug=slug).order_by('-id')
exists = qs.exists()
if exists:
new_slug = '%s-%s' % (slug, qs.first().id)
return create_slug(instance, new_slug=new_slug)
return slug
Having just used this on another answer, I have exactly the right code in my clipboard. I do exactly this for one of my models:
from django.utils.text import slugify
class Event(models.Model):
date = models.DateField()
location_title = models.TextField()
location_code = models.TextField(blank=True, null=True)
picture_url = models.URLField(blank=True, null=True, max_length=250)
event_url = models.SlugField(unique=True, max_length=250)
def __str__(self):
return self.event_url + " " + str(self.date)
def save(self, *args, **kwargs):
self.event_url = slugify(self.location_title+str(self.date))
super(Event, self).save(*args, **kwargs)
Above solutions break validation in the Django Admin interface. I suggest:
from django import forms
from django.http.request import QueryDict
from django.utils.text import slugify
from .models import Article
class ArticleForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ArticleForm, self).__init__(*args, **kwargs)
# Ensure that data is a regular Python dictionary so we can
# modify it later.
if isinstance(self.data, QueryDict):
self.data = self.data.copy()
# We assume here that the slug is only generated once, when
# saving the object. Since they are used in URLs they should
# not change when valid.
if not self.instance.pk and self.data.get('title'):
self.data['slug'] = slugify(self.data['title'])
class Meta:
model = Article
exclude = []