Django get_or_create only if form constraints are met - django

I have a form that asks for a song's Artist, Title and Mix. Artist and Title are required fields but Mix is not. The form should only save if Artist, Title and Mix does not exists. If the form has either empty Artist or Title field it should show "This field is required" on submit. The issue I'm having is if the Title field is empty but Artist is populated, it'll still create the Artist object with get_or_create (See ###forms.py below). How do I only create Artist object if the form is valid?
###########models.py
class Artist (models.Model):
name = models.CharField(max_length=100)
class Track (models.Model):
artist = models.ForeignKey(Artist, blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Artist")
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Submitted by", default=1)
title = models.CharField(max_length=100, verbose_name="Title")
mix = models.CharField(max_length=100, blank=True, verbose_name="Mix")
###########views.py
class TrackCreateView(SuccessMessageMixin, AjaxCreateView):
form_class = ProfileForm
success_message = "Thank you for submitting track: %(artist)s - %(title)s - %(mix)s"
def get_initial(self):
self.initial.update({ 'user': self.request.user })
return self.initial
def get_success_message(self, cleaned_data):
return self.success_message % dict(cleaned_data,
artist=self.object.artist,
title=self.object.title,
)
###########forms.py
class ProfileForm(forms.ModelForm):
class Meta:
model = Track
fields = [
"artist",
"title",
"mix",
]
artist = forms.CharField(widget=forms.TextInput(attrs={'maxlength': '100',}))
def __init__(self, *args, **kwargs):
self.user = kwargs['initial']['user']
super(ProfileForm, self).__init__(*args, **kwargs)
# Set layout for fields.
my_field_text= [
('artist', 'Artist', ''),
('title', 'Title', ''),
('mix', 'Mix', ''),
]
for x in my_field_text:
self.fields[x[0]].label=x[1]
self.fields[x[0]].help_text=x[2]
self.helper = FormHelper()
self.helper.layout = Layout(
Div(
Div('artist', css_class="col-sm-4"),
Div('title', css_class="col-sm-4"),
Div('mix', css_class="col-sm-4"),
css_class = 'row'
),
)
def save(self, commit=True):
obj = super(ProfileForm, self).save(False)
obj.user = self.user
commit and obj.save()
return obj
def clean(self):
cleaned_data = super(ProfileForm, self).clean()
artist = self.cleaned_data.get('artist')
title = self.cleaned_data.get('title')
mix = self.cleaned_data.get('mix')
if artist and title:
title = ' '.join([w.title() if w.islower() else w for w in title.split()])
if mix:
mix = ' '.join([w.title() if w.islower() else w for w in mix.split()])
if Track.objects.filter(artist=artist, title=title, mix=mix).exists():
msg = "Record with Artist and Title already exists."
if mix:
msg = "Record with Artist, Title & Mix already exists."
self.add_error('mix', msg)
self.add_error('artist', msg)
self.add_error('title', msg)
if not artist:
raise forms.ValidationError("Artist is a required field.")
else:
artist, created = Artist.objects.get_or_create(name=artist)
self.cleaned_data['artist'] = artist
self.cleaned_data['title'] = title
self.cleaned_data['mix'] = mix
return self.cleaned_data

How about changing your comparison, by first checking if your form is valid in clean()?
def clean(self):
...
if not artist:
raise ValidationError("artist is a required field")
if not title:
raise ValidationError("title is a required field")
...
The above makes it a two-step process for the user, since if a user leaves both artist and title blank, they ony get the artist notice.
You could make a better (sub) if statement and a combined ValidationError, or solve that by using clean_artist and clean_title, just for raising the ValidationError (not using get_or_create in the field clean methods):
def clean_artist(self):
# no get_or_create here
...
if not artist:
raise ValidationError("artist is a required field")
def clean_title(self):
# no get_or_create here
...
if not title:
raise ValidationError("title is a required field")
def clean(self):
...
if title and artist:
# get_or_create stuff here
...
This way, you should get both errors independently, but the get_or_create is still done in the main clean, only if title and artist are valid.

Related

Django3.1 error when I try to save post with tags

I have a view inside posts app where I try to save a post with tags. Whenever I add a new tag to the post, I get this error:
value error at create
My view is this one:
class PostCreateView(CreateView):
template_name = 'posts/create.html'
form_class = PostCreationForm
model = Post
def get_success_url(self):
return reverse('posts:detail', kwargs={"slug": self.object.slug})
def form_valid(self, form):
form.instance.user = self.request.user
form.save() # this is where the error occurs
tags = self.request.POST.get("tag").split(",")
for tag in tags:
current_tag = Tag.objects.filter(slug=slugify(tag))
if current_tag.count() < 1:
create_tag = Tag.objects.create(title=tag)
form.instance.tag.add(create_tag)
else:
existed_tag = Tag.objects.get(slug=slugify(tag))
form.instance.tag.add(existed_tag)
return super(PostCreateView, self).form_valid(form)
The form I'm using is as follow:
class PostCreationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PostCreationForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.field_class = 'form-group'
self.helper.layout = Layout(
Field('title', css_class="form-control", placeholder='Post title'),
Field('content', css_class="form-control", placeholder='Post content'),
Field('category', css_class="form-control"),
Field('image', css_class="form-control"),
Field('tag', css_class="form-control", placeholder='tag1, tag2')
)
self.helper.add_input(Submit('submit', 'Create New Post', css_class='btn btn-underline-primary'))
tag = forms.CharField()
class Meta:
model = Post
fields = ['title', 'content', 'category', 'image', 'tag']
This is the Post model:
class Post(models.Model):
title = models.CharField(max_length=150, unique=True)
content = RichTextUploadingField()
# content = models.TextField()
publish_date = models.DateTimeField(auto_now_add=True)
image = models.ImageField(blank=True, null=True, upload_to='uploads/')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(default="slug", editable=False)
category = models.ForeignKey(Category, on_delete=models.CASCADE, default=1, related_name='posts')
tag = models.ManyToManyField(Tag, related_name='posts', blank=True)
slider_post = models.BooleanField(default=False)
hit = models.PositiveIntegerField(default=0)
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
def __str__(self):
return self.title
def post_tag(self):
return ', '.join(str(tag) for tag in self.tag.all())
def comment_count(self):
return self.comments.all().count()
How can I fix this error: "Field 'id' expected a number but got 't'." (where 't' is the first letter from my first tag: 'test'. If I use another tag, then the error display the first letter of that word).
Remove the field tag from fields = ['title', 'content', 'category', 'image', 'tag']
The reason this works is that by including it, Django automatically creates a ModelMultipleChoiceField named tag.
However, since you are manually handling extracting and saving the tags (using tags = self.request.POST.get("tag").split(",")), excluding it from Meta.fields guarantees that Django does not also try to handle this field.
As such, another solution would be to let Django handle the tag form field by completely removing your custom save() method.

Exclude fields for Django model, only on creation

I am building a notification system for a company, where admin users can create Projects and add users to them. The Project model has 9 attributes but I only want to show 3 or 4 fields when a Project is created, but show them all when an existing Project is updated.
This change will only need to be reflected on the Django admin site, so I have extended the ProjectAdmin with my own ProjectForm, where I extend the init method to check if it is a new instance and if so remove certain fields.
# models.py
class Project(models.Model):
project_number = models.IntegerField()
name = models.CharField(max_length=100)
permit = models.CharField(max_length=100, blank=True, default='')
is_active = models.BooleanField(default=True)
users = models.ManyToManyField(CustomUser, blank=True, related_name='project_users')
# add a default
levels = models.ManyToManyField('Level', blank=True, related_name='project_levels')
total_contract_hours = models.IntegerField(default=0, blank=True, verbose_name='Total Design Hours')
hours_used = models.IntegerField(default=0, blank=True, verbose_name='Total Design Hours Used')
notes = models.ManyToManyField('notes.ProjectNote', related_name='core_project_notes', blank=True)
history = HistoricalRecords()
def __str__(self):
ret_str = "{} {}".format(self.project_number, self.name)
if self.permit:
ret_str += " | Permit: {}".format(self.permit)
return ret_str
# admin.py
class ProjectForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
attrs = {'class': 'form-control', 'required': True}
if self.instance and self.instance.pk is None:
# creating project
exclude = ['is_active', 'users', 'levels', 'hours_used', 'notes']
for field in exclude:
try:
del self.fields[field]
except ValueError:
print('{} does not exist'.format(field))
for field in self.fields.values():
field.widget.attrs = attrs
class Meta:
model = Project
fields = ['project_number', 'name', 'total_contract_hours']
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
fields = ['project_number', 'name', 'permit', 'is_active', 'users', 'levels', 'total_contract_hours', 'hours_used', 'notes']
As I stated I only want basic Project fields on creation, but show all attributed when updating existing Project. With just these changes, I now get a KeyError:
KeyError: "Key 'is_active' not found in 'ProjectForm'. Choices are:
name, permit, project_number, total_contract_hours."
However, when I print the available fields it returns an OrderedDict with all of the model attributes as keys. What am I doing wrong? Thanks!
I figured it out, the field must be in listed in Meta and then you just set the field to be a hidden field.
class ProjectForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
print("Adding project")
if not self.instance or self.instance.pk is None:
for name, field in self.fields.items():
if name in ['design_manager', ]:
field.widget = forms.HiddenInput()
class Meta:
model = Project
fields = ['project_number', 'name', 'design_manager', 'total_contract_hours']
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
def save_model(self, request, obj, form, change):
obj.design_manager = request.user
super().save_model(request, obj, form, change)

Django: Add queryset to inlineformsets

I want to make a queryset on a field in an inline formset.. I have Inovice and Product models and InvoiceDetails model to link the manytomany relation between them.
here are the models:
class Invoices(models.Model):
"""The Invoice Creation Class."""
invoice_number = models.CharField(
_('invoice_number'), max_length=100, unique=True, null=True)
....
class Products(models.Model):
"""Product Creation Class."""
company = models.ForeignKey(Company, default=1)
barcode = models.CharField(_('barcode'), max_length=200, null=True)
....
class InvoiceDetail(models.Model):
invoice = models.ForeignKey(Invoices, related_name='parent_invoice')
product = models.ForeignKey(Products, related_name='parent_product')
quantity_sold = models.IntegerField(_('quantity_sold'))
...
when crearting an invoice i have inline formsets for the products which create an invoice details for every product.. now i want to filter the products that appear for the user to choose from them by the company. i searched a lot on how to override the queryset of inline formsets but found nothing useful for my case.
my forms:
class InvoiceForm(forms.ModelForm):
class Meta:
model = Invoices
fields = ('customer', 'invoice_due_date', 'discount', 'type')
def __init__(self, *args, **kwargs):
self.agent = kwargs.pop('agent')
super(InvoiceForm, self).__init__(*args, **kwargs)
def clean_customer(self):
.....
def clean(self):
......
class BaseDetailFormSet(forms.BaseInlineFormSet):
def clean(self):
......
DetailFormset = inlineformset_factory(Invoices,
InvoiceDetail,
fields=('product', 'quantity_sold'),
widgets= {'product': forms.Select(
attrs={
'class': 'search',
'data-live-search': 'true'
})},
formset=BaseDetailFormSet,
extra=1)
and use it in the views like that:
if request.method == 'POST':
invoice_form = InvoiceForm(
request.POST, request.FILES, agent=request.user)
detail_formset = DetailFormset(
request.POST)
.......
else:
invoice_form = InvoiceForm(agent=request.user)
detail_formset = DetailFormset()
so, how can it filter the products that show in detail_formset by company?
I solved it be passing the user to init and loop on forms to override the queryset.
def __init__(self, *args, **kwargs):
self.agent = kwargs.pop('agent')
super(BaseDetailFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.fields['product'].queryset = Products.objects.filter(company=self.agent.company)
in views:
detail_formset = DetailFormset(agent=request.user)

How to raise forms validation error when ever the user try's to create an object with the object twice

I'v used the unique together model meta, it works but it just comes up with this error. i want to raise a forms validation error, rather than a IntegrityError.
IntegrityError at /name/
UNIQUE constraint failed: canvas_canvas.user_id, canvas_canvas.canvas_name
Request Method: POST
Request URL: http://127.0.0.1:8000/name/
Exception Type: IntegrityError
Exception Value:
UNIQUE constraint failed: canvas_canvas.user_id, canvas_canvas.canvas_name
Exception Location: C:\Users\AppData\Local\Programs\Python\Python35-32\lib\site-packages\django\db\backends\sqlite3\base.py in execute, line 337
class Canvas(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
canvas_name = models.CharField(
max_length=100,
validators=[
# validate_canvas_title,
RegexValidator(
regex=CANVAS_REGEX,
message='Canvas must only contain Alpahnumeric characters',
code='invalid_canvas_title'
)],
)
slug = models.SlugField(max_length=100, blank=True)
background_image = models.ImageField(
upload_to=upload_location,
null=True,
blank=True,
)
# sort catergory into alphabetical order
category = models.ForeignKey('category.Category', default=1, blank=True)
followers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='canvas_followed_by', blank=True)
class Meta:
unique_together = ['user', 'canvas_name']
form
class CanvasModelForm(ModelForm):
class Meta:
model = Canvas
fields = ['canvas_name', 'category', 'background_image']
widgets = {
'canvas_name': TextInput(attrs={'class': 'form-input'}),
'category': Select(attrs={'class': 'form-input'}),
}
view
user = get_object_or_404(User, username=username)
form_create = CanvasModelCreateForm(request.POST or None)
if form_create.is_valid():
instance = form_create.save(commit=False)
instance.user = request.user
instance.save()
return redirect('canvases:canvas', username=request.user.username, slug=instance.slug)
template = 'pages/profile.html'
context = {
'user': user,
'form_create': form_create,
}
return render(request, template, context)
You could do this by passing request.user into the form and use it for validating the canvas_name.
You need to override the form's __init__ method to take an extra keyword argument, user. This stores the user in the form, where it's required, and from where you can access it in your clean method.
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(CanvasModelCreateForm, self).__init__(*args, **kwargs)
def clean_canvas_name(self):
canvas_name = self.cleaned_data.get('canvas_name')
if Canvas.objects.get(user=self.user, canvas_name=canvas_name).exists():
raise forms.ValidationError(u'Canvas with same name already exists.')
return canvas_name
And you should change in your view like this so,
form_create = CanvasModelCreateForm(request.POST, user=request.user)

Django Rest Framework - display prepopulated slug field

I have a slug field for a model that I would like returned in the object representation but NOT as part of the form input in the browsable API. It is generated by a slugify method on the model.
When I mark it as read only in it's ModelSerializer by adding it to Meta using read_only_fields=('slug',) trying to add new fields in the browseable api form yields "This field is required."
The serializer for reference is below:
class CategorySerializer(serializers.HyperlinkedModelSerializer):
slug = serializers.SlugField(read_only=True, required=False)
def to_representation(self, obj):
self.fields['children'] = CategorySerializer(obj, many=True, read_only=True)
return super(CategorySerializer, self).to_representation(obj)
class Meta:
model = Category
fields = ('pk', 'url', 'title', 'slug', 'parent', 'children', 'active', 'icon')
read_only_fields = ('children','slug',)
What is a simple solution to show the field in the representation and not the browseable api form given the above?
For reference, here is my model:
#python_2_unicode_compatible
class CategoryBase(mptt_models.MPTTModel):
parent = mptt_fields.TreeForeignKey( 'self', blank=True, null=True, related_name='children', verbose_name=_('parent'))
title = models.CharField(max_length=100, verbose_name=_('name'))
slug = models.SlugField(verbose_name=_('slug'), null=True)
active = models.BooleanField(default=True, verbose_name=_('active'))
objects = CategoryManager()
tree = TreeManager()
def save(self, *args, **kwargs):
"""
While you can activate an item without activating its descendants,
It doesn't make sense that you can deactivate an item and have its
decendants remain active.
"""
if not self.slug:
self.slug = slugify(self.title)
super(CategoryBase, self).save(*args, **kwargs)
if not self.active:
for item in self.get_descendants():
if item.active != self.active:
item.active = self.active
item.save()
def __str__(self):
ancestors = self.get_ancestors()
return ' > '.join([force_text(i.title) for i in ancestors] + [self.title, ])
class Meta:
abstract = True
unique_together = ('parent', 'slug')
ordering = ('tree_id', 'lft')
class MPTTMeta:
order_insertion_by = 'title'
class Category(CategoryBase):
icon = IconField(null=True, blank=True)
order = models.IntegerField(default=0)
#property
def short_title(self):
return self.title
def get_absolute_url(self):
"""Return a path"""
from django.core.urlresolvers import NoReverseMatch
try:
prefix = reverse('categories_tree_list')
except NoReverseMatch:
prefix = '/'
ancestors = list(self.get_ancestors()) + [self, ]
return prefix + '/'.join([force_text(i.slug) for i in ancestors]) + '/'
def save(self, *args, **kwargs):
super(Category, self).save(*args, **kwargs)
class Meta(CategoryBase.Meta):
verbose_name = _('category')
verbose_name_plural = _('categories')