Django - Auto populating Many-To-Many-Field relation - python

I am trying to make a form that auto populates a many-to-many relation for my user model. The goal is to have a submit button that adds the views instance object (the SingelWorkout object) to a many-to-many field relation within my user model.
The view accurately displays the correct object, and the form appears as intended within the template. I do not wish for the user to see the many-to-many field selection. Aside from the submit button, I am trying to have all logic to occur on the backend. How would I assign an object instance to a field within a form? Would this occur in the views.py or the forms.py?
Here is why my user model looks like:
class FitnessUser(AbstractUser):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField(max_length=60)
age_category = models.ForeignKey(
AgeGroup,
on_delete=models.CASCADE,
blank=True,
null=True
)
goal = models.IntegerField(default=1 ,choices=Purpose.choices)
weight = models.CharField(max_length=30)
height = models.CharField(max_length=30)
gender = models.ForeignKey(
Gender,
on_delete=models.CASCADE,
blank=True,
null=True
)
exercise_frequency = models.IntegerField(default=1 ,choices=Frequency.choices)
template_id = models.ForeignKey(
Workout_template,
on_delete=models.CASCADE,
blank=True,
null=True
)
completed_workouts = models.ManyToManyField(SingleWorkout)
def get_absolute_url(self):
return reverse('detail', args=[self.id])
This is my form in forms.py:
class CustomWorkoutChangeForm(UserChangeForm):
class Meta(UserChangeForm):
model = FitnessUser
fields = ('completed_workouts',)
exclude = ('completed_workouts',)
UserChangeForm.password = None
This is how my view looks:
class WorkoutUpdateView(LoginRequiredMixin, UpdateView):
model = SingleWorkout
template_name = 'workout/daily_view.html'
form_class = CustomWorkoutChangeForm
success_url = reverse_lazy("template")
def get_context_data(self, **kwargs):
context = super(WorkoutUpdateView, self).get_context_data(**kwargs)
context['workout'] = SingleWorkout.objects.get(slug = self.kwargs['slug'])
return context
My html template looks like this:
{{workout}}
<br>
workout:
<br>
{{ workout.exercise_1 }}
<br>
{{ workout.description_1 }}
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Confirm">
</form>

Figured out a solution. I created a view that gets the instance object based on the objects url slug, and also gets the user by its pk. From there is adds the instance object to the users many to many field, then redirects back to the previous page.
New view created:
def update_workout_data(request, slug):
workout = SingleWorkout.objects.get(slug=slug)
endUser = FitnessUser.objects.get(pk = request.user.pk)
endUser.completed_workouts.add(workout)
endUser.save()
return redirect(reverse('daily', kwargs={'slug':workout.slug}))
Updated HTML appearance. I've also altered the html and its detail view so that the update link will redirect to a separate update view, depending on the need to add/remove the relation.
{% block content %}
Daily View
<br>
{{exercise}}
<br>
workout:
<br>
<br>
{% if exercise.name in workouts %}
<h5>Workout Already Completed</h5>
<form method="POST" action="{% url 'remove' slug=exercise.slug %}">
{% csrf_token %}
<button type="submit">Reset</button>
</form>
{% else %}
<form method="POST" action="{% url 'update' slug=exercise.slug %}">
{% csrf_token %}
<button type="submit">Complete</button>
</form>
{% endif %}
{% endblock content %}
Updated Detail View
def get_context_data(self, **kwargs):
context = super(WorkoutDetailView, self).get_context_data(**kwargs)
user = FitnessUser.objects.get(pk = self.request.user.pk)
context['exercise'] = SingleWorkout.objects.get(slug = self.kwargs['slug'])
context['workouts'] = {}
for workout in user.completed_workouts.all():
context['workouts'][workout.name] = workout
return context

Related

Django: Object is not saved to DB when submitting form with CreateView

I have a CreateView to create artists but when submitting the form, nothing happens.
models.py
class Artist(models.Model):
name = models.CharField(max_length=222, unique=True)
slug = models.SlugField(unique=True, null=True)
age = models.PositiveIntegerField(null=True,blank=True)
location = models.CharField(max_length=222, null=True, blank=True)
bio = models.TextField(null=True,blank=True)
booking_fee_per_hour = models.PositiveIntegerField(verbose_name="Booking Fee per hour")
def __str__(self):
return self.name
views.py
class ArtistsAddView(views.CreateView):
template_name = 'add_artist.html'
model = Artist
fields = '__all__'
success_url = '/'
templates -> add-artist.html
<form method="post" action="{% url 'artist add' %}">
<p>{{ form.name.label_tag }} {{ form.name }}</p>
<p>{{ form.age.label_tag }} {{ form.age }}</p>
<p>{{ form.location.label_tag }} {{ form.location }}</p>
<p>{{ form.bio.label_tag }} {{ form.bio }}</p>
<input type="hidden" name="next" value="{{ next }}">
<button>Submit</button>
{% csrf_token %}
</form>
I intentionally hid 2 of the fields: slug and booking_fee_per_hour. I would like to make them available only for admins.
If I write:
<form method="post" action="{% url 'artist add' %}">
{{ form.as_p }}
<input type="hidden" name="next" value="{{ next }}">
<button>Submit</button>
{% csrf_token %}
</form>
then the artist is saved to the DB but also the normal user can see slug and booking_fee_per_hour.
Could you please help me?
I think that as the add-artist.html template does not include some fields that are mandatory like slug & booking_fee_per_hour (no blank=True, null = True), the generic CreateView cannot saves the model.
Here are several workarounds depending on your app concerns and what is easy to do for you:
Make fields optional in the model (blank=True, null = True) & populate them later with the correct values
Provide default values for those fields in the model (default=...)
provide default values in the template with hidden fields <input type="hidden" name="booking_fee_per_hour" value="????">
provide default values in the view. For example you can override the post method of the class and providing values on the fly when saving. https://docs.djangoproject.com/en/4.1/ref/class-based-views/mixins-editing/#django.views.generic.edit.ProcessFormView
class ArtistsAddView(views.CreateView):
template_name = 'add_artist.html'
model = Artist
fields = '__all__'
success_url = '/'
def post(request, *args, **kwargs)
post_data = request.POST.copy() # make post data mutable
# Add the needed data
post_data['booking_fee_per_hour'] = ...
...
# save
request.POST = post_data
super().post(request, *args, **kwargs)

Django: Access related object attributes from a form field in a template?

I'm building a page that allows users to edit Task and related Activity records (one task can have many activities), all on the same page. Here are extracts from my code...
models.py
from django.contrib.auth.models import User
class Task(models.Model):
category = models.CharField(max_length=300)
description = models.CharField(max_length=300)
class Activity(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
title = models.CharField(max_length=150)
notes = models.TextField(blank=True)
owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
The activity "owner" is linked to a User from the Django standard user model.
views.py
def manage_task(request, pk):
task = Task.objects.get(pk = pk)
TaskInlineFormSet = inlineformset_factory(Task, Activity,
form = ActivityForm)
if request.method == "POST":
form = TaskForm(request.POST, instance = task)
formset = TaskInlineFormSet(request.POST, instance = task)
if form.has_changed() and form.is_valid():
form.save()
if formset.has_changed() and formset.is_valid():
formset.save()
return redirect('manage_task',pk=task.id)
else:
form = TaskForm(instance = task)
formset = TaskInlineFormSet(instance = task)
context = {'task': task, 'task_form': form, 'formset': formset}
return render(request, 'tasks/manage_task.html', context)
And manage_task.html excerpt:
<h2>{{ task.category }}</h2>
<form method="post">
{% csrf_token %}
{{ task_form.description }}
{% for form in formset %}
{{ form.id }}
{{ form.title }}</br>
{{ form.notes }}</br>
{% if user.id == form.owner.value %}
You own this Activity!</br>
{% else %}
{{ form.owner.first_name }} owns this Activity</br>
{% endif %}
{% endfor %}
<input class="save" type="submit" value="SAVE">
</form>
Perhaps obviously, {{ form.owner.first_name }} doesn't return anything. form.owner understandably renders as a select drop down with all the Users listed - it's some sort of iterable object so it doesn't have a first_name attribute.
I think I need to have the first_name field in each form in the formset so it ties up properly with the correct Activity form.
I feel like I might have to adapt the view in some way to go through all the forms in the formset and use the owner.id (if it exists) to access the related User and add the first_name attribute as an extra field in the form somehow? That doesn't feel very Djangoish, there must be a better way.
How can I use the form field form.owner to get the first_name attribute from the User object that's related to the Activity so I can use it in the template?
You should not take such value from form directly. It may be risky (i.e. changing real-life). But if you are aware of it and ok with that, try pointing to specific instance of form:
{{ form.instance.owner.first_name }}

Python Django Form submit button not working as desired

I have been trying to learn Django.
I am stuck on this form part. A form has been created that allows the user to create an Album object where they can fill in the Artist, Album Name, Genre and upload an Album Logo. When I fill in the fields and then click submit, it should then redirect me to the details page for that particular Album that just got created. But nothing appears to happen when clicking the submit button and the object does not get created.
Here is the models.py code that contains an Album class with 4 fields; artist, album_name, genre and album_logo.
from django.db import models
from django.urls import reverse
# Create your models here.
class Album(models.Model):
artist = models.CharField(max_length=250)
album_name = models.CharField(max_length=500)
genre = models.CharField(max_length=100)
album_logo = models.ImageField()
def get_absolute_url(self):
return reverse('music:detail', kwargs={'pk':self.pk})
def __str__(self):
return self.album_name + " - " + self.artist
class Song(models.Model):
album = models.ForeignKey(Album, on_delete=models.CASCADE)
file_type = models.CharField(max_length=100)
song_title = models.CharField(max_length=250)
is_favourite = models.BooleanField(default=False)
def __str__(self):
return self.song_title
Here is the album_form.html code which contains the actual form. I have not used crispy_forms as I am not familiar with Bootstrap though I know CSS.
{% extends 'music/base.html' %}
{% block title %}Add a New Album{% endblock %}
{% block body %}
<form class="formContainer" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
{% if field.label != 'Album logo' %}
<label for="field{{ forloop.counter }}">{{ field.label }}</label>
<input type="text" id="field{{ forloop.counter }}" name="" value="">
{% else %}
<label for="field{{ forloop.counter }}">{{ field.label }}</label>
<input type="file" id="field{{ forloop.counter }}" name="" value="" accept="image/*">
{% endif %}
<br>
{% endfor %}
<input type="submit" id="submitBtn" name="" value="Add">
</form>
{% endblock %}
This is views.py code where I have made use of class based views and not function based views.
from django.views import generic
from .models import Album, Song
# Create your views here.
class IndexView(generic.ListView):
template_name = 'music/index.html'
queryset = Album.objects.all()
context_object_name = 'all_albums'
class DetailView(generic.DetailView):
model = Album
template_name = 'music/detail.html'
class AlbumCreate(generic.CreateView):
model = Album
fields = ['artist', 'album_name', 'genre', 'album_logo']
def form_valid(self, form):
return super().form_valid(form)
and finally this is my urls.py code:
from django.urls import path, include
from . import views
app_name='music'
urlpatterns = [
#/music/
path('', views.IndexView.as_view(), name='index'),
#/music/5/
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
#/music/album/add/
path('album/add/', views.AlbumCreate.as_view(), name='album-add')
]
After clicking the submit button in the form, it should take me to the "detail" url for the primary key of the Album that got created. Am I missing something here?
In your views.py you need to override the get_success_url function in your CreateView and pass the id as an argument while redirecting.
class AlbumCreate(generic.CreateView):
model = Album
fields = ['artist', 'album_name', 'genre', 'album_logo']
def form_valid(self, form):
return super().form_valid(form)
def get_success_url(self):
return reverse('music:detail', args=(self.object.id,))
Seems you forgot to put action to your <form> tag
Try this
<form class="formContainer" action='{% url 'music:album-add'%}' method="post" enctype="multipart/form-data">
Edit: Also add success url using get_success_url function in your AlbumCreate view to redirect user to album detail page, like was mentioned in above answer
from django.urls import reverse_lazy
...
class AlbumCreate(generic.CreateView):
...
def get_success_url(self, **kwargs):
return reverse_lazy('music:detail', args = (self.object.id,))

Replace text entry for datetime to calendar date picker icon in Django form

I've this template in my Django application for adding a training session:
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<h1>New session</h1>
<form action="" method="post">{% csrf_token %}
{{ form|crispy }}
<input class="btn btn-success" type="submit" value="Save" />
</form>
<p />
{% endblock content %}
The form contains a datetime field which appears as follows:
Is it possible to change this so instead of entering the datetime as text it can be selected from a calendar type icon? If so, how is this done?
This is my view:
class SessionCreateView(CreateView):
model = ClubSession
template_name = 'session_new.html'
fields = ['location', 'coach', 'date', 'details']
This is my model:
class ClubSession(models.Model):
location = models.CharField(max_length=200)
coach = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
date = models.DateTimeField(default=now)
details = models.TextField()
def __str__(self):
return self.location
def get_absolute_url(self):
return reverse('session_detail', args=[str(self.id)])
With crispy forms, I think you will need a form for this.
class ClubSessionForm(forms.ModelForm):
class Meta:
model = ClubSession
fields = ['location', 'coach', 'date', 'details']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date'].widget.attrs.update({'type': 'datetime-local'})
class SessionCreateView(CreateView):
model = ClubSession
form_class = ClubSessionForm
template_name = 'session_new.html'
Docs
Keep in mind that not all browsers support <input type="datetime-local"> - I believe only Chrome and Opera do. If you need it working in all browsers, you'll need a JS solution.

Search Field in Django Python

First, I have to say that is this my first application in Django. So my knowledge is still limited.
I have this home page where it shows all the data in my model. The model name is "Asset".
I am trying to have a search field inside the home page.
models.py
class Asset(models.Model):
asset_desc = models.CharField(max_length=120, null=False)
BEIRUT = 'Beirut'
SAIDA = 'Saida'
HALBA = "Halba"
base_choice = ((SAIDA, "Saida"), (BEIRUT, "Beirut"), (HALBA, "Halba"))
asset_base = models.CharField(max_length=120, null=False, choices=base_choice)
created_date = models.DateField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
asset_user = models.CharField(max_length=120, blank=True)
slug = models.SlugField()
def save(self, *args, **kwargs):
self.slug = slugify(self.asset_desc)
super(Asset, self).save(*args, **kwargs)
def __str__(self):
return self.asset_desc
views.py
def search_asset(request):
if 'q' in request.GET and request.GET['q']:
q = request.GET['q']
assets = Asset.objects.filter(asset_desc__icontains=q)
context = {'desc': assets}
return render(request, 'home.html', context)
html for the search field:
<form method="GET" class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="Search..."id="search_box" name="q">
urls.py
url(r'^search/$', "asset.views.search_asset", name="home")
Please any help on why it is not showing the result. I am using Django 1.9.
some corrections:
you dont need null=False for TextField() and CharField(), since they never save Null to database but empty string. so you can remove null=False
the search url name is home which logically not really approriate. it should be changed to search or search_view and then you can refer to it via url tag:
action="{% url 'search' %}"
this is useful if someone should look over your code. "Readability counts" ;)
and finally, put this to your home.html (actually you must already have it)
{% for asset in desc %}
<div>
{{ asset.asset_desc }} <br>
{{ asset.base_choice }} <br>
{{ asset.asset_user }}
</div>
{% endfor %}
I hope, this helps
You have not provided the template or the HTML portion where you list the results. You should consider the name of you context variable, but by following your name, you should list the results like this:
{% for asset in desc %}
<div>
{{ asset }}
</div>
{% endfor %}
Anything else looks correct.
Hope it helps

Categories