Django - Generating random, unique slug field for each model object - python

I have a Model called ExampleModel in Django, and want each of the model objects to be uniquely identified. Though, I don't want the ID of the object visible to the user in the URL; and so for this reason I want the objects slug to be a unique, randomly generated integer with 8 digits which will go in the views URL. This is different from other questions I've seen because this means not producing a slug string that is based on the model object's name//content itself.
Models.py:
class ExampleModel(models.Model):
user = models.ForeignKey(UserModel, related_name='examplemodel', on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=50, verbose_name='Title')
slug = models.SlugField(unique=True, blank=True, null=True)
Currently the value of the slug is null so I don't have to set a default slug for all of the current ExampleModel objects.
This is quite vague understandably, however I haven't been able to find any guides/tutorials that may work for my exact situation.
Thanks for any help/guidance provided
Edit
Here's my views.py:
def model_create(request):
user=request.user.id
if request.user.is_authenticated:
try:
example = request.user.examplemodel
except ExampleProfile.DoesNotExist:
example = ExampleProfile(user)
if request.method == 'POST':
form = NewForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('/dashboard/')
else:
return render(request, 'create.html', {'form': form})
else:
form = NewForm()
return render(request, 'create.html', {'form': form})
else:
return redirect('/users/login/?next=')
Edit 2 Models.py (Save method):
def save(self, *args, **kwargs):
if self.user is None: # Set default reference
self.user = UserModel.objects.get(id=1)
super(ExampleModel, self).save(*args, **kwargs)

Django has a get_random_string function built in that can generate the random string needed for your slug.
As Sebastian Wozny mentions, you want to call this as you override the save method. The basics are:
from django.utils.crypto import get_random_string
# ...
the_slug = get_random_string(8,'0123456789') # 8 characters, only digits.
That's not actual working code. In more detail a real models.py would look like the below. Note that I haven't limited myself to digits and I'm doing a checks both for unqueness and to make sure it doesn't spell anythig bad:
from django.db import models
from django.utils.crypto import get_random_string
# ...
class SomeModelWithSlug(models.Model):
slug = models.SlugField(max_length=5,blank=True,) # blank if it needs to be migrated to a model that didn't already have this
# ...
def save(self, *args, **kwargs):
""" Add Slug creating/checking to save method. """
slug_save(self) # call slug_save, listed below
Super(SomeModelWithSlug, self).save(*args, **kwargs)
# ...
def slug_save(obj):
""" A function to generate a 5 character slug and see if it has been used and contains naughty words."""
if not obj.slug: # if there isn't a slug
obj.slug = get_random_string(5) # create one
slug_is_wrong = True
while slug_is_wrong: # keep checking until we have a valid slug
slug_is_wrong = False
other_objs_with_slug = type(obj).objects.filter(slug=obj.slug)
if len(other_objs_with_slug) > 0:
# if any other objects have current slug
slug_is_wrong = True
naughty_words = list_of_swear_words_brand_names_etc
if obj.slug in naughty_words:
slug_is_wrong = True
if slug_is_wrong:
# create another slug and check it again
obj.slug = get_random_string(5)

If you override the save method, every time the object updates slug changes, If you don't want that then doing it like this only sets the slug the first time:
def slug_generator():
return ''.join(random.choices(string.ascii_lowercase + string.digits + string.ascii_uppercase, k=20))
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slug_generator()
super(Item, self).save()
super(Item, self).save()

Override save:
def save(self, *args, **kwargs):
try:
self.slug = ''.join(str(random.randint(0, 9)) for _ in range(8))
super().save(*args, **kwargs)
except IntegrityError:
self.save(*args, **kwargs)
This might need some more safeguards against IntegrityErrors though.
If you can live with two saves:
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
try:
self.slug = ''.join(str(random.randint(0, 9)) for _ in range(8))
super().save(*args, **kwargs)
except IntegrityError:
self.save(*args, **kwargs)

Related

Using Validators in Django not working properly

I have recently learning about Validators and how they work but I am trying to add a function to my blog project to raise an error when a bad word is used.
I have a list of bad words in a txt and added the code to be in the models.py the problem is that nothing is blocked for some reason I am not sure of.
Here is the models.py
class Post(models.Model):
title = models.CharField(max_length=100, unique=True)
---------------other unrelated------------------------
def validate_comment_text(text):
with open("badwords.txt") as f:
censored_word = f.readlines()
words = set(re.sub("[^\w]", " ", text).split())
if any(censored_word in words for censored_word in CENSORED_WORDS):
raise ValidationError(f"{censored_word} is censored!")
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
content = models.TextField(max_length=300, validators=[validate_comment_text])
updated = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now=True)
here is the views.py:
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html" # <app>/<model>_<viewtype>.html
def get_context_data(self, *args, **kwargs):
context = super(PostDetailView, self).get_context_data()
post = get_object_or_404(Post, slug=self.kwargs['slug'])
comments = Comment.objects.filter(
post=post).order_by('-id')
total_likes = post.total_likes()
liked = False
if post.likes.filter(id=self.request.user.id).exists():
liked = True
if self.request.method == 'POST':
comment_form = CommentForm(self.request.POST or None)
if comment_form.is_valid():
content = self.request.POST.get('content')
comment_qs = None
comment = Comment.objects.create(
post=post, user=self.request.user, content=content)
comment.save()
return HttpResponseRedirect("blog/post_detail.html")
else:
comment_form = CommentForm()
context["comments"] = comments
context["comment_form"] = comment_form
context["total_likes"] = total_likes
context["liked"] = liked
return context
def get(self, request, *args, **kwargs):
res = super().get(request, *args, **kwargs)
self.object.incrementViewCount()
if self.request.is_ajax():
context = self.get_context_data(self, *args, **kwargs)
html = render_to_string('blog/comments.html', context, request=self.request)
return JsonResponse({'form': html})
return res
class PostCommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
def form_valid(self, form):
post = get_object_or_404(Post, slug=self.kwargs['slug'])
form.instance.user = self.request.user
form.instance.post = post
return super().form_valid(form)
Here is my trial which didn't work
def validate_comment_text(sender,text, instance, **kwargs):
instance.full_clean()
with open("badwords.txt") as f:
CENSORED_WORDS = f.readlines()
words = set(re.sub("[^\w]", " ", text).split())
if any(censored_word in words for censored_word in CENSORED_WORDS):
raise ValidationError(f"{censored_word} is censored!")
pre_save.connect(validate_comment_text, dispatch_uid='validate_comment_text')
I am new learner so if you could provide some explanation to the answer I would be grateful so that I can avoid repeating the same mistakes.
I'm sure there are many ways to handle this, but I finally decided to adopt a common practice in all my Django projects:
when a Model requires validation, I override clean() to collect all validation logic in a single place and provide appropriate error messages.
In clean(), you can access all model fields, and do not need to return anything; just raise ValidationErrors as required:
from django.db import models
from django.core.exceptions import ValidationError
class MyModel(models.Model):
def clean(self):
if (...something is wrong in "self.field1" ...) {
raise ValidationError({'field1': "Please check field1"})
}
if (...something is wrong in "self.field2" ...) {
raise ValidationError({'field2': "Please check field2"})
}
if (... something is globally wrong in the model ...) {
raise ValidationError('Error message here')
}
The admin already takes advantages from this, calling clean() from ModelAdmin.save_model(),
and showing any error in the change view; when a field is addressed by the ValidationError,
the corresponding widget will be emphasized in the form.
To run the very same validation when saving a model programmatically, just override save() as follows:
class MyModel(models.Model):
def save(self, *args, **kwargs):
self.full_clean()
...
return super().save(*args, **kwargs)
Proof:
file models.py
from django.db import models
class Model1(models.Model):
def clean(self):
print("Inside Model1.clean()")
def save(self, *args, **kwargs):
print('Enter Model1.save() ...')
super().save(*args, **kwargs)
print('Leave Model1.save() ...')
return
class Model2(models.Model):
def clean(self):
print("Inside Model2.clean()")
def save(self, *args, **kwargs):
print('Enter Model2.save() ...')
self.full_clean()
super().save(*args, **kwargs)
print('Leave Model2.save() ...')
return
file test.py
from django.test import TestCase
from project.models import Model1
from project.models import Model2
class SillyTestCase(TestCase):
def test_save_model1(self):
model1 = Model1()
model1.save()
def test_save_model2(self):
model2 = Model2()
model2.save()
Result:
❯ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Enter Model1.save() ...
Leave Model1.save() ...
.Enter Model2.save() ...
Inside Model2.clean()
Leave Model2.save() ...
.
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
Destroying test database for alias 'default'...
Validators run only when you use ModelForm. If you directly call comment.save(), validator won't run. link to docs
So either you need to validate the field using ModelForm or you can add a pre_save signal and run the validation there (you'll need to manually call the method, or use full_clean to run the validations).
Something like:
from django.db.models.signals import pre_save
def validate_model(sender, instance, **kwargs):
instance.full_clean()
pre_save.connect(validate_model, dispatch_uid='validate_models')

Inline formset causing error: 'NoneType' object has no attribute 'is_ajax'

I am using bootstrap-modal-forms to show a user a formset with some inline forms. It is possible for the user to save the form if data is only entered into the original form, but if the inline formset has data then I get the following error:
'NoneType' object has no attribute 'is_ajax'
The inline formset was working correctly before I tried to implement them in the modal form. The problem seems to arise only when the inline formset (projectimages) is saved it is a NoneType.
My views.py
class ProjectCreate(BSModalCreateView):
form_class = ProjectForm
template_name = 'project_form.html'
success_message = 'Success: %(project_name)s was created.'
def get_success_url(self):
return reverse_lazy('project-detail', kwargs={'project': self.object.slug})
def get_context_data(self, **kwargs):
data = super(ProjectCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['projectimages'] = ProjectFormSet(self.request.POST, self.request.FILES,)
else:
data['projectimages'] = ProjectFormSet()
return data
def form_valid(self, form):
form.instance.date_created = timezone.now()
context = self.get_context_data()
projectimages = context['projectimages']
with transaction.atomic():
self.object = form.save()
if projectimages.is_valid():
projectimages.instance = self.object
projectimages.save()
return super(ProjectCreate, self).form_valid(form)
My forms.py
class ProjectForm(BSModalForm):
class Meta:
model = Project
exclude = ['date_created', 'slug']
ProjectFormSet = inlineformset_factory(
Project,
ProjectImage,
can_delete=True,
form=ProjectForm,
extra=1,
)
My models.py
class Project(models.Model):
project_name = models.CharField(max_length=100)
date_created = models.DateTimeField('Created on')
slug = models.SlugField(unique=True)
def __str__(self):
return self.project_name
def save(self, *args, **kwargs):
self.slug = slugify(str(self))
super(Project, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('project-list')
class ProjectImage(models.Model):
image = models.ImageField(verbose_name='Additional Images', upload_to=project_directory_path)
project = models.ForeignKey(Project, on_delete=models.PROTECT)
annotation = models.CharField(max_length=200, blank=True)
I expect the user to be able to add as many images to the modal formset as they like.
The BSModalForm expects you to initialise it with the request. This happens in your BSModalCreateView for the main form but not for your formset, because you initialise it manually.
So when initialising, just add the form_kwargs attribute:
if self.request.POST:
data['projectimages'] = ProjectFormSet(
self.request.POST, self.request.FILES,
form_kwargs={'request': self.request})
else:
data['projectimages'] = ProjectFormSet(form_kwargs={'request': self.request})
Note that I think the form you set in ProjectFormSet is wrong, because it should be a form for a ProjectImage model, not a Project. It should actually be called ProjectImageFormSet to better reflect what it is.
You probably want to remove form=ProjectForm as it probably doesn't need to be a BSModalForm (not sure about that). In that case you should not pass the request in form_kwargs. If not, you just need to create another ProjectImageForm class.
Finally, you should not return super().form_valid() because that will save the main form a second time (you already did). Do the redirect yourself.

Why my edit form not updating data, but able to create new instance. I try every thing on google but still not working

I have a form for creating the object, and it works fine. When I use that form for editing the object, it doesn't work. I did debugging the post method,and it also works fine, the form is valid, the redirect work, the success message appear, but not update the object. The form instance is also work correctly. It just don't update
# models.py
class Item(models.Model):
status_choices = (
('rent','Rent'),
('give', 'Give'),
('share','Share'),
)
item_types = (
('book','Book'),
('movie','Movie',),
('data','Data'),
('other','Other'),
)
title = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(max_length=200, db_index=True, blank=True,unique=True)
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
cover = models.ImageField(upload_to='items/%Y/%m/%d',blank=True)
link = models.URLField(blank=True)
description = models.TextField(max_length=500, blank=True)
status = models.CharField(max_length=10,choices=status_choices,default='Share')
item = models.CharField(max_length=10, choices=item_types,default='Data', verbose_name='Item Type')
publish = models.DateTimeField(auto_now=True,null=True)
class Meta:
ordering = ('-publish',)
index_together = (('id', 'slug'),)
def __str__(self):
return '{} : <{}> for {}'.format(self.title,self.item,self.status)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Item, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('item_edit', kwargs={'slug':self.slug})
#forms.py
class ItemShareForm(forms.ModelForm):
class Meta:
model = Item
fields = ('title', 'cover', 'link', 'description', 'status', 'item')
widgets = {
'description' : forms.Textarea(),
}
#views.py
#login_required
def item_edit(request,slug):
instance = get_object_or_404(Item,slug=slug)
if request.method == 'POST': #check the request method
edit_form = ItemShareForm(request.POST ,instance=instance)
if edit_form.is_valid(): # check the form validation
update_item = edit_form.save(commit=False)
update_item.owner = request.user #assign the owner
update_item.save() # update the instance
messages.success(request,'Your item has been updated successfully') # writing the message to user
return redirect('/')
else:
messages.error(request,'Error updating your item...')
else:
edit_form = ItemShareForm(instance=instance)
return render(request, 'account/share.html',{'itemform':edit_form})*
You have overridden you model save method like so:
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Item, self).save(*args, **kwargs)
This means that the object will only ever get saved if the slug is empty - i.e., it will only ever get saved once. Any future calls to save will not execute that if block, and nothing will happen.
You probably mean to do this instead - note indentation of the last line:
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Item, self).save(*args, **kwargs)

Django forms ChoiceField not selecting the wanted value

I created a django form (IssueForm) which is meant to be used to register an object which is instance of one of my models (Issue). Following are the model:
model.py
class Issue(models.Model):
TYPE_FIELDS = [
("Math", "Math"),
("Physics", "Physics"),
("Programming", "Programming"),
("Arts", "Arts")
]
issue_text = models.TextField(default="Please insert text")
issue_description = models.TextField(default="Newly created")
issue_deadline = models.DateField()
issue_field = models.CharField(max_length=30, choices=TYPE_FIELDS)
published_by = models.ForeignKey(User, on_delete=models.CASCADE, default=None)
def __str__(self):
return self.issue_description
the form used:
forms.py
class IssueForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
self.user = user
super(IssueForm, self).__init__(*args, **kwargs)
TYPE_FIELDS = [
("Math", "Math"),
("Physics", "Physics"),
("Programming", "Programming"),
("Arts", "Arts")
]
issue_text = forms.CharField(widget=forms.Textarea, required=True)
issue_description = forms.CharField(widget=forms.Textarea, required=True)
issue_deadline = forms.DateField(required=True)
issue_fields = forms.ChoiceField(choices=TYPE_FIELDS, required=True)
class Meta:
model = Issue
fields = [
'issue_text',
'issue_description',
'issue_deadline',
'issue_fields'
]
def save(self, commit=True):
issue = super(IssueForm, self).save(commit=False)
issue.issue_text = self.cleaned_data['issue_text']
issue.issue_description = self.cleaned_data['issue_description']
issue.issue_deadline = self.cleaned_data['issue_deadline']
issue.issue_fields = self.cleaned_data['issue_fields']
if commit:
issue.published_by = self.user
issue.save()
return issue
and the related view:
views.py
def create_issue(request):
if ExtendedUser.objects.filter(user=request.user).exists():
if request.method == 'POST':
form = IssueForm(request.user, request.POST)
if form.is_valid():
form.save()
return redirect("/issues")
else:
form = IssueForm(request.user)
args = {'form': form}
return render(request, "issues/create_issue.html", args)
else:
raise Http404("You are not allowed to perform this action")
The forms works for every field in the model, they are all registered right, except for issue_fields. If i try giving a default value to the field in the model, that is the value that is saved on the database, otherwise I just get an empty field. Also I believe the problem comes from the form used, because if i try to create a new issue from the django admin interface it works just fine.
I feel like it's one of those silly mistakes, but I'm just starting with django and python in general and cannot figure it out on my own.
Thank you for your time!!
The field on your model is called issue_field, but you set issue_fields.
Note that also you are doing far more work here than necessary. Your save method completely duplicates what the superclass does already; you should remove all that code except for the setting of the user value.
enter code hereIf you want to use Choices, you haven't to write one more time list of choices in your forms.py file.
This is an example :
#In your models.py file
LIST_CHOICE = (('A','A'), ('B','B'))
class Test(models.Model) :
foo = models.CharField(choices=LIST_CHOICE, verbose_name="foo")
and
#In your form.py file
TestForm(forms.Modelform) :
class Meta :
model = Test
fields = ['foo']
It's not necessary to overwrite LIST_CHOICE in your form file ;)
So, dont touch to your model.py file, but in your form.py file, just write :
class IssueForm(forms.ModelForm):
issue_text = forms.CharField(widget=forms.Textarea)
issue_description = forms.CharField(widget=forms.Textarea)
def __init__(self, user, *args, **kwargs):
self.user = user
super(IssueForm, self).__init__(*args, **kwargs)
class Meta:
model = Issue
fields = [
'issue_text',
'issue_description',
'issue_deadline',
'issue_fields'
]
Don't forget to remove s in issue_field ;)

Django - DetailView and Templates

So, my issue is that I have a detailview, which displays a specific post from my database. I then, used get_context_data to then grab db values from a different model; however, it outputs something strange in my template.
What can I change in the template, in order for it to list every correct db value from that other model?
models.py
class Projects(models.Model):
user = models.ForeignKey(User)
slug = models.SlugField()
project_title = models.CharField(max_length=30)
project_shortdesc = models.CharField(max_length=248)
project_desc = models.TextField()
def save(self):
super(Projects, self).save()
date = datetime.date.today()
self.slug = '%i%i%i%s' % (
date.year, date.month, date.day, slugify(self.project_title)
)
super(Projects, self).save()
class ProjectsToDo(models.Model):
project_tododate = models.DateField()
project_tododesc = models.TextField(max_length = 500)
project_id = models.ManyToManyField(Projects)
views.py
class ProjectDetail(generic.DetailView):
model = Projects
context_object_name = 'indprojects'
template_name = 'projectpage.html'
def get_context_data(self, *args, **kwargs):
context = super(ProjectDetail, self).get_context_data(*args, **kwargs)
context['todolist'] = ProjectsToDo.objects.order_by('project_tododate')
context['todoform'] = ProjectToDoForm()
context['form'] = ProjectForm(instance=Projects.objects.get(slug=self.kwargs['slug']))
return context
def get_queryset(self):
return Projects.objects.filter(user=self.request.user)
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(ProjectDetail, self).dispatch(request, *args, **kwargs)
template
{{todolist}}
This from the template, outputs:
[<ProjectsToDo: ProjectsToDo object>, <ProjectsToDo: ProjectsToDo object>]
I've tried {{todolist.project_tododesc}}, and both outputted no data. I'm not really sure how to go about fixing this, any help would be appreciated.
I think the problem is that you did not define a __str__() method if you are using Python 3 (or __unicode__() in Python 2.x) in class ProjectsToDo.
When you try to print a model instance, what happens is Django will look at __str__() to decide what to display.
See here for more about __str__()

Categories

Resources