I would like to be able to change the slug using slugify if the slug already exist. This site will have multiple products named the same but when you call the product using get_object_or_404 you will get an error because two or more objects are being called at one time. To avoid this I need to auto increment the slugify if the slug already exist.
Can anyone help me with this?
class Product(models.Model):
product_name = models.CharField(max_length=500, blank=True)
product_description = models.TextField(blank=True)
company = models.ForeignKey(Company, blank=True, null=True)
category = models.ForeignKey(Category, blank=True, null=True)
manufacturer = models.ForeignKey(Manufacturer)
buy_link = models.URLField(max_length=1000, blank=True)
product_image_url = models.URLField(max_length=1000, blank=True)
price = models.CharField(max_length=30, blank=True)
orginal_price = models.CharField(max_length=30, blank=True)
stock = models.CharField(max_length=30, blank=True)
sku = models.CharField(max_length=250, blank=True)
slug = models.SlugField(max_length=500)
date_added = models.DateTimeField(auto_now_add=True, auto_now=False)
updated = models.DateTimeField(auto_now_add=False, auto_now=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.product_name)
super(Product, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('products:product_detail', args=[self.slug]) #kwargs={'slug': self.slug}
def __str__(self):
return self.product_name
What if you combine both product name and id, something like:
def save(self, *args, **kwargs):
self.slug = slugify("{obj.product_name}-{obj.id}".format(obj=self))
super(Product, self).save(*args, **kwargs)
Or, you can generate a slug, check if it exists and, if does, increment and append the counter - sample here and here.
Also, you should probably enforce the slug uniqueness:
slug = models.SlugField(max_length=500, unique=True)
You can use django-autoslug if you don't want to roll your own. Simply install the package with pip (pip install django-autoslug), import into your model file, and add it as a field. When set up with unique=True, if django-autoslug runs into a collision, it will append a number to the end of the slug, and increment that number if it already exists.
Example from the Github page:
from django.db.models import CharField, Model
from autoslug import AutoSlugField
class Article(Model):
title = CharField(max_length=200)
slug = AutoSlugField(populate_from='title', unique=True)
From the docs:
# globally unique, silently fix on conflict ("foo" --> "foo-1".."foo-n")
slug = AutoSlugField(unique=True)
Related
I have the Account model were I store information about preferred units.
However I also want to allow user to change the units for particular exercise which by default should be Account.units.
Here are my models:
class Account(models.Model):
"""Model to store user's data and preferences."""
UNIT_CHOICES = [
('metric', 'Metric'),
('imperial', 'Imperial')
]
uuid = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=False)
units = models.CharField(max_length=255, choices=UNIT_CHOICES, default=UNIT_CHOICES[0], null=False, blank=False)
weight_metric = models.FloatField(null=True, blank=True)
height_metric = models.FloatField(null=True, blank=True)
weight_imperial = models.FloatField(null=True, blank=True)
height_imperial = models.FloatField(null=True, blank=True)
def __str__(self):
return self.owner.email
class CustomExercise(models.Model):
UNIT_CHOICES = [
('metric', 'Metric'),
('imperial', 'Imperial')
]
uuid = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(Account, on_delete=models.CASCADE, null=False, blank=False)
preferred_units = models.CharField(max_length=255, choices=UNIT_CHOICES, default=owner.units, null=False, blank=False) # <- throws an error that "ForeignKey doesn't have units attribute."
name = models.CharField(max_length=255, null=False, blank=False)
measure_time = models.BooleanField(default=False)
measure_distance = models.BooleanField(default=False)
measure_weight = models.BooleanField(default=False)
measure_reps = models.BooleanField(default=False)
def __str__(self):
return f'{self.owner}:{self.name}'
As posted in code sample I tried to get that default value from ForeignKey, which not unexpectedly did not work out.
So my question is: what is the correct solution to implement this kind of feature?
I would not recommend storing duplicate values accross multiple models. You can easily access that value through a property method:
class CustomExercise(models.Model):
... # remove preferred_units field from model
#property
def preferred_units(self):
return self.owner.unit
Although you can not use it in queryset directly, still you can annotate the 'owner__unit' field in queryset or filter by it:
q = CustomExcercise.objects.annotate(preferred_units=F('owner__unit')).filter(preferred_units = 'kg')
q.values()
Displaying the value in Adminsite:
class CustomExerciseAdmin(admin.ModelAdmin):
fields = (..., 'preferred_units')
readonly_fields = ['preferred_units']
Two ways come to mind: overriding the model's save method or by using a pre_save signal. I would try the first one and if it doesn't work then the second one. The reason is that signals are notoriously difficult to debug so if you have alternatives you should always leave them as a last resort.
Ok so, I think this should work:
def save(self, *args, **kwargs):
self.preferred_units = self.owner.units
super(CustomExercise, self).save(*args, **kwargs
Otherwise:
#receiver(pre_save, sender=CustomExercise)
def assign_unit(sender, instance, **kwargs):
instance.preferred_units = instance.owner.units
The convention is to store your signals in signals.py in your app. Make sure to "activate" them from apps.py or they won't work. Here the docs.
I have such structure:
class Category(models.Model):
name = models.CharField(max_length=255, validators=[MinLengthValidator(3)])
parent = models.ForeignKey('self', blank=True, null=True,
related_name='children',
on_delete=models.CASCADE
)
slug = models.SlugField(max_length=255, null=False, unique=True)
class Product(models.Model):
name = models.CharField(max_length=255, validators=[MinLengthValidator(3)])
to_category = models.ForeignKey(Category, on_delete=models.SET_NULL,
blank=True, null=True,
)
slug = models.SlugField(max_length=255, null=False, unique=True)
I have created one category with slug "test". When I try to create new category with slug "test" I got warning message and it is Ok. But If I try to create product with slug "test" I dont have warning and this is not good in my case. Is there a solution or method to validate slug field for uniqueness with Product and Category model?
You can override the save method for each, and then check if the given slug already exists for a product or category.
def is_slug_unique(slug):
product_exists = Product.objects.filter(slug=slug).exists()
category_exists = Category.objects.filter(slug=slug).exists()
if product_exists or category_exists:
return False
else:
return True
class Category(models.Model)
...
def save(self, *args, **kwargs):
slug_unique = is_slug_unique(self.slug)
if not slug_unique:
# do something when the slug is not unique
else:
# do something when the slug is unique
super().save(*args, **kwargs)
class Product(models.Model)
...
def save(self, *args, **kwargs):
slug_unique = is_slug_unique(self.slug)
if not slug_unique:
# do something when the slug is not unique
else:
# do something when the slug is unique
super().save(*args, **kwargs)
An idea might be to create a Slug model that stores all the slugs, optionally with a backreference to the object:
class Slug(models.Model):
slug = models.SlugField(max_length=255, primary_key=True)
Then the slugs in your models are ForeignKeys to that Slug model, and you check if such slug already exists:
from django.core.exceptions import ValidationError
class Product(models.Model):
name = models.CharField(max_length=255, validators=[MinLengthValidator(3)])
to_category = models.ForeignKey(
Category, on_delete=models.SET_NULL, blank=True, null=True
)
slug = models.ForeignKey(Slug, on_delete=models.PROTECT)
def validate_slug(self):
if self.pk is not None and Slug.objects.filter(pk=self.slug_id).exclude(
product__pk=self.pk
):
raise ValidationError('The slug is already used.')
def clean(self, *args, **kwargs):
self.validate_slug()
return super().clean(*args, **kwargs)
def save(self, *args, **kwargs):
self.validate_slug()
return super().save(*args, **kwargs)
That being said, often overlapping slugs for different entity types are allowed.
I've multiple models in my Django project but only this given below model creating another instance on update instead of save. This is happening in Django's Admin panel, not on my custom UI. When I remove my save() method then it works fine but this way I won't be able to create slug.
Does anybody know what I'm doing wrong in here
class Course(models.Model):
title = models.CharField(max_length=200)
user = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='category')
slug = models.SlugField(max_length=200, unique=True, primary_key=True, auto_created=False)
short_description = models.TextField(blank=False, max_length=60)
description = models.TextField(blank=False)
outcome = models.CharField(max_length=200)
requirements = models.CharField(max_length=200)
language = models.CharField(max_length=200)
price = models.FloatField(validators=[MinValueValidator(9.99)])
level = models.CharField(max_length=20)
application_link = models.URLField(max_length=1000, blank=True, null=True)
brochure = models.FileField(upload_to='brochures/', blank=True, null=True)
thumbnail = models.ImageField(upload_to='thumbnails/')
video_url = models.CharField(max_length=100)
is_session_available = models.BooleanField(default=False)
session_url = models.CharField(max_length=250, default='')
is_published = models.BooleanField(default=True)
created_at = models.DateTimeField(default=now)
updated_at = models.DateTimeField(default=now)
def __str__(self):
return self.title
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(Course, self).save(*args, **kwargs)
instead of overriding save method you could do this:
admin.py
class CourseAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Course, CourseAdmin)
It is happening because it will generate new slug every time. You can do something like this:
def generate_slug(self):
self.slug = slugify(self.name)
def save(self, *args, **kwargs):
if not self.slug:
self.generate_slug() # This will generate slug.
return super().save(*args, **kwargs)
I am trying to create a blog post model and I added a schedule filed on Django model that I can schedule my post by date and time if schedule time == now. Then post should be verified and display to dashboard so for this I used def save function. But save function does not respond. When I tried to schedule a blog post from admin panel it did not change verified = True. Here is code what I did so far:
from django.utils import timezone
now = timezone.now() # get the current time
class Blog(models.Model):
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="post")
title = models.CharField(_("Title of blog post"),
max_length=250, null=True, blank=True)
header = models.CharField(
_("Blog title eg. TIPS, "), max_length=250, null=True, blank=True)
slug = models.SlugField(_("Slug of the title"), max_length=250,
unique_for_date='publish', null=True, blank=True)
photo = models.ImageField(_("Blog post main image"), default="img.png",
null=True, blank=True, upload_to='users/avatar')
read_time = models.TimeField(
_("Blog post read time"), null=True, blank=True)
category = models.ForeignKey(Category, verbose_name=_(
"Blog category list"), on_delete=models.CASCADE, null=True, blank=True)
publish = models.DateField()
tags = TaggableManager(blank=True)
description = HTMLField()
views = models.IntegerField(default="0") # <- here
verified = models.BooleanField(
_("Approved post before push on production"), default=False)
schedule = models.DateTimeField(
_("Schedule post by date and time"), auto_now=False, auto_now_add=False, null=True, blank=True)
class Meta:
verbose_name = _('blog')
verbose_name_plural = _('blogs')
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if self.schedule >= now:
self.verified = True
print(self.verified)
else:
self.slug = slugify(self.title) # this also not respond
super(Blog, self).save(*args, **kwargs)
what is now ? I don't see it defined. I think that the correct way to do it is
from django.utils.timezone import now
if self.schedule <= now():
do it
The save() function is only called when you save an object. This thus means that although an object has a self.schedule that is already passed the current timestamp, one should wait until the object is saved again (and that can take a long time).
It is better to annotate the queryset with a field that specifies that it is verified when self.scheduled is less than (or equal to) Now(). We thus can define a manager that injects the annotation, and remove the verified field:
from django.db.models import BooleanField, ExpressionWrapper, Q
from django.db.models.functions import Now
class BlogManager(models.Manager):
def get_queryset(self, *args, **kwargs):
return super().get_queryset(*args, **kwargs).annotate(
verified=ExpressionWrapper(Q(scheduled__lte=Now()), BooleanField())
)
class Blog(models.Model):
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="post")
title = models.CharField(_("Title of blog post"),
max_length=250, null=True, blank=True)
header = models.CharField(
_("Blog title eg. TIPS, "), max_length=250, null=True, blank=True)
slug = models.SlugField(_("Slug of the title"), max_length=250,
unique_for_date='publish', null=True, blank=True)
photo = models.ImageField(_("Blog post main image"), default="img.png",
null=True, blank=True, upload_to='users/avatar')
read_time = models.TimeField(
_("Blog post read time"), null=True, blank=True)
category = models.ForeignKey(Category, verbose_name=_(
"Blog category list"), on_delete=models.CASCADE, null=True, blank=True)
publish = models.DateField()
tags = TaggableManager(blank=True)
description = HTMLField()
views = models.IntegerField(default=0)
schedule = models.DateTimeField(
_("Schedule post by date and time"), auto_now=False, auto_now_add=False, null=True, blank=True)
objects = BlogManager()
In my app, I have a custom defined 'Group_Set' model, designed to group users together. I can't seem to retrieve the users in the groups though.
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True)
display_name = models.CharField(max_length=30, blank=True)
slug = models.CharField(max_length=30, blank=True)
bio = models.TextField(max_length=500, blank=True)
avatar = models.ImageField(upload_to = 'avatars/', default = 'avatars/default.jpg')
location = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
public = models.BooleanField(default = False)
def __str__(self):
return self.slug
class Group_Sets(models.Model):
name = models.CharField(max_length=30)
slug = models.CharField(max_length=30, blank=True)
bio = models.TextField(max_length=500, blank=True)
users = models.ManyToManyField(User, related_name='users')
public = models.BooleanField(default = False)
def __str__(self):
return self.slug
Once we get a Group_Set object, group = get_object_or_404(), all the other attributes work. However, group.users returns 'auth.User.none' even though I can see the relationship function in the database, and in the admin.
Can anyone see anything wrong with how I've set this up?
This was a dumb mistake on my part, you need to use 'group.users.all()', so theres that. Remember your related_name, and the all method.