Django: copy a model object using views and forms - python

I have a Django (1.8) model that has some class based generic views: list, update, delete, detail, create.
https://docs.djangoproject.com/en/1.8/ref/class-based-views/
On the detail or list view, I have a button that I want to do this:
Create a copy of the object
Load the data into a form and display for the user to edit/save the new object (could use the existing update or create views, or a new one?)
I can clone the model with this info:
How do I clone a Django model instance object and save it to the database?
But I can't make the leap to get this done by starting at a view and ending at a form with the copied object data.
Thanks!
partial views.py
class List(ListView):
model = Announcement
template_name = 'announcements/list.html'
class Create(CreateView):
model = Announcement
form_class = AnnouncementForm
template_name = 'announcements/form.html'
def form_valid(self, form):
data = form.save(commit=False)
data.author = self.request.user
data.save()
return super(Create, self).form_valid(form)
class Update(UpdateView):
model = Announcement
form_class = AnnouncementForm
template_name = 'announcements/form_update.html'
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(Update, self).dispatch(*args, **kwargs)
partial forms.py
class AnnouncementForm(forms.ModelForm):
class Meta:
model = Announcement
exclude = ['author']
partial list.html
{% for object in object_list %}
<p>object.title</p>
<a class="btn btn-danger" href="{% url 'announcements:delete' object.id %}" role="button">Delete</a>
<a class="btn btn-info" href="{% url 'announcements:update' object.id %}" role="button">Edit</a>
<a class="btn btn-primary" href="" role="button">Copy</a>
{% endfor %}
What I hit the "Copy" button in list.html, I want to duplicate the object and open the new duplicate in a form for editing.

It think I figured it out!
urls.py
#eg: myapp/5/copy/
#where 5 is the item I want to copy
url(r'^(?P<id>[0-9]+)/copy/$', views.item_copy, name='item_copy'),
views.py:
def item_copy(request, id):
new_item = get_object_or_404(MyModel, pk = id)
new_item.pk = None #autogen a new pk (item_id)
new_item.name = "Copy of " + new_item.name #need to change uniques
form = MyForm(request.POST or None, instance = new_item)
if form.is_valid():
form.save()
return redirect('my_view')
context = {
"form": form,
#other context
}
return render(request, "form.html", context)

class CopyView(ManageAnnouncement, DeleteView):
def dispatch(self, *args, **kwargs):
obj = self.get_object()
obj.pk = None
copy = obj.save()
return HttpResponseRedirect('/announcement/edit/%s' %(copy.id))
# Change the redirect page to the one you need.
I have inherited a base class called ManageAnnouncement. You can put methods or variables common to multiple classes in an abstract base class and inherit it in adding, editing deleting, copying etc, so that code gets 'dry'.

Related

django: unable to save new user

I'm trying to build a view that shows a form for creating a new user.
Next are the template I'm using, urls.py and the code attempts at views.py and forms.py.
user_form.html
<form action="{% url 'app:register' %}" method="post">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="Register">
</form>
urls.py
urlpatterns = [
...
path('newuser/',views.NewUser.as_view(), name='newuser'),
...
]
First attempt:
views.py
class NewUser(generic.CreateView):
model = User
fields = ['username','email','password']
template_name = 'app/user_form.html'
success_url = reverse_lazy('register')
This one didn't return errors, but was unable to save the new user data.
Second try involved creating a form for the occasion:
forms.py
class NewUserForm(forms.Form):
username = forms.CharField(max_length=100)
email = forms.EmailField()
password = forms.PasswordInput()
views.py
class NewUser(generic.CreateView):
model = User
form_class = NewUserForm
template_name = 'app/user_form.html'
success_url = reverse_lazy('register')
This one returned an error: TypeError at /newuser/ BaseForm.__init__() got an unexpected keyword argument 'instance'
Third:
forms.py
class NewUserForm(forms.ModelForm):
class Meta:
model = User
fields = ['username','email','password']
views.py
class NewUser(generic.CreateView):
model = User
form_class = NewUserForm
template_name = 'app/user_form.html'
success_url = reverse_lazy('cadastro')
Can't save a new User instance either.
I've also noticed that the model = User line in this last try can be removed.
I'm using the User.objects.all() query in the terminal and the admin page to check for new users.
What am I not doing?
You should make the POST request to the newuser view, so:
<form action="{% url 'app:newuser' %}" method="post">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="Register">
</form>
You can however not use a simple ModelForm: passwords in Django are hashed and the ModelForm will not do that, or at least not automatically.
You can make use of the UserCreationForm [Django-doc] to do the hashing properly, this field also uses two password fields which will be validated.
If you want to implement a custom ModelForm, then you will need to implement the password hashing functionality in the model form:
class NewUserForm(forms.ModelForm):
class Meta:
model = User
fields = ['username','email','password']
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data['password'])
if commit:
user.save()
return user
and then plug this into the CreateView with:
class NewUserView(generic.CreateView):
model = User
form_class = NewUserForm
template_name = 'app/user_form.html'
success_url = reverse_lazy('cadastro')
Note: In Django, class-based views (CBV) often have a …View suffix, to avoid a clash with the model names.
Therefore you might consider renaming the view class to NewUserView, instead of NewUser.
when you use CreateView on a model, it calls create method.
but User model is different. User model doesn't have create, instead have create_user. and CreateView doesn't know about it.
i suggest that you use View class like this:
class UserRegisterView(View):
form_class = UserRegistrationForm
template_name = 'account/register.html'
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect('home:home')
return super().dispatch(request, *args, **kwargs)
def get(self, request):
form = self.form_class()
return render(request, self.template_name, {'form':form})
def post(self, request):
form = self.form_class(request.POST)
if form.is_valid():
cd = form.cleaned_data
User.objects.create_user(cd['username'], cd['email'], cd['password1'])
messages.success(request, 'you registered successfully', 'success')
return redirect('home:home')
return render(request, self.template_name, {'form':form})
this snippet totally works.

Combining two separate forms in one Django view?

This question have been answered before, e.g here: Proper way to handle multiple forms on one page in Django
So before it gets marked as a duplicate. I'll try to explain why its different.
I've got three tables, Project, ProjectUser and User. ProjectUser is a join table to indicate what users belongs to what project.
I'm trying to create a view that lets users update project details (e.g. name of project), and also add users to the project (which is indicated by a dropdown that shows all available users like the standard one for models with foreign keys in the django admin panel). All works fine until I'm trying to pass an id from the views to the formclass and submit.
views.py
class ProjectUpdateView(UpdateView):
form_class = ProjectUpdateForm
second_form_class = ProjectUserAddForm
template_name = 'projects/project_edit.html'
success_url = reverse_lazy('projects:list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
id_ = self.kwargs.get("id")
project = Project.objects.get(id=id_)
if 'form' not in context:
context['form'] = self.form_class()
if 'form2' not in context:
team = Organization.objects.get(id=project.organization_id)
context['form2'] = self.second_form_class(queryset=team) # <-- here is where I wish to pass a queryset, which fails when trying to submit form2.
context['project_users'] = ProjectUser.objects.filter(project__id=project.id).select_related("project")
context['team'] = Organization.objects.get(id=project.organization_id)
return context
def get_object(self):
id_ = self.kwargs.get("id")
return get_object_or_404(Project, id=id_)
def form_invalid(self, **kwargs):
return self.render_to_response(self.get_context_data(**kwargs))
def form_valid(self, form):
project_id = self.kwargs.get("id")
if self.request.POST.get("form2") == 'Add':
ProjectUser.objects.create(user_id=self.request.POST.get("user"), project_id=project_id)
form.save()
success_url = reverse("projects:edit", args=(project_id,))
return HttpResponseRedirect(success_url)
def post(self, request, *args, **kwargs):
# get the user instance
self.object = self.get_object()
# determine which form is being submitted
# uses the name of the form's submit button
if 'form' in request.POST:
# get the primary form
form_class = self.get_form_class()
form_name = 'form'
else:
# get the secondary form
form_class = self.second_form_class
form_name = 'form2'
# get the form
form = self.get_form(form_class)
# validate
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(**{form_name: form})
projects_edit.html
<form action="{% url 'projects:edit' project.id %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{form.name|as_crispy_field}}
<input name="form" value="Update" type="submit"></input>
</form>
<form action="{% url 'projects:edit' project.id %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{form2.user}}
<input name="form2" value="Add" type="submit"></input>
</form>
forms.py
class ProjectUpdateForm(ModelForm):
class Meta:
model = Project
fields = ["name"]
class ProjectUserAddForm(ModelForm):
def __init__(self, queryset, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['user'].queryset = User.objects.filter(organizations_organizationuser__organization__id=queryset.id) # here is where I wish to pass the id of the queryset from the form class
class Meta:
model = ProjectUser
fields = ["user"]
Rendering the forms works just fine with the desired queryset, but when I try to submit the second form (adding a user to the ProjectUserForm, I just get a
__init__() missing 1 required positional argument: 'queryset' error.
Any ideas on how to solve this? Perhaps I'm making it way more complicated than it should
I have also added a screenshot if it helps: https://imgur.com/a/uqu0UeB

Django: How to assign foreign key in URL to a Createview form

I'm trying to create a review system to items on the site.
in order to do that I created 2 models one for the item (Festival) and one for the reviews. the item is a foreign key of the review.
What I'm trying to do is passing the PK of the item in the url and set it as a foreign key of the review.
I'm currently getting this error:
NoReverseMatch at /festivals/55/
Reverse for 'create_review' with arguments '(55,)' not found. 1 pattern(s) tried: ['festivals/create/review/$']
I spent ages trying to solve this :/
Views.py:
class CreateReview(LoginRequiredMixin, SelectRelatedMixin, generic.CreateView):
form_class = forms.ReviewFormCreate
model = models.Review
template_name = 'festival_list/review_form.html'
success_url = '/'
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
form.instance.festival = get_object_or_404(models.Festival, pk=self.kwargs['pk'])
return super(CreateReview, self).form_valid(form)
urls.py
path('create/review/<int:pk>',views.CreateReview.as_view(),name='create_review'),
Models
class Review(models.Model):
user = models.ForeignKey(
User,
related_name='Reviews',
on_delete=models.CASCADE,
)
festival = models.ForeignKey(
Festival,
related_name='Festival_Reviews',
on_delete=models.CASCADE
)
template link:
<a class="btn btn-success" href="{% url 'festival_list:create_review' festival.pk %}">Add Review</a>
Many thanks for your help =]
You need to add the primary key in the url, for example:
path(
'create/review/<int:pk>',
views.CreateReview.as_view(),
name='create_review'
),
So the URL that will be derived will look like create/review/123, with 123 the primary key of the festival.
The url of the review should then look like:
<a class="btn btn-success" href="{% url 'festival_list:create_review' pk=festival.pk %}">Add Review</a>
That being said, it is likely that your Form will not accept this, since the festival is not filled in, you thus should remove the festival:
class CreateReview(LoginRequiredMixin, SelectRelatedMixin, generic.CreateView):
form_class = forms.ReviewFormCreate
model = models.Review
template_name = 'festival_list/review_form.html'
success_url = '/'
def create_form(self, *args, **kwargs):
form = super(CreateReview, self).create_form(*args, **kwargs)
form.instance.user = self.request.user
form.instance.festival_id = self.kwargs['pk']
return form
and thus remove the festival and user from the form itself (you probably do not want users to tamper with it, since otherwise people could post a review as if it was another user).
I got the same problem because I used this url again in my form page.
Like this:
<form
action="{% url 'readersclub_review_create_urlpattern'}"
method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Submit Review</button>
</form>
But I did not figure out how to fix it.

How to filter a model through multiple forms?

I have a single Car model which I'd like to filter through interdependent ModelChoiceField's:
class Car(models.Model):
make = models.CharField(max_length=50)
model = models.CharField(max_length=50)
platform = models.CharField(max_length=50)
Forms.py:
class MakeSelectForm(forms.ModelForm):
make = forms.ModelChoiceField(queryset=Car.objects.values_list('make',flat=True).distinct())
class Meta:
model = Car
fields = ["make"]
class ModelSelectForm(forms.ModelForm):
model = forms.ModelChoiceField(queryset=Car.objects.values_list('model',flat=True).distinct())
class Meta:
model = Car
fields = ["make", "model"]
Views.py:
def make_select_view(request):
form = MakeSelectForm()
make = None
if request.method == "POST":
form = MakeSelectForm(request.POST)
if form.is_valid():
make = form.cleaned_data['make']
return render(request, "reviews/makeselect.html", {"form": form, "make": make})
def model_select_view(request, make):
form = ModelSelectForm()
model = None
if request.method == "POST":
form = MakeSelectForm(request.POST)
if form.is_valid():
model = form.cleaned_data['model']
return render(request, "reviews/modelselect.html", {"form": form, "model": model})
URL's:
urlpatterns = [
url(r'^$', views.make_select_view, name="make-select"),
url(r'^(?P<make>\w+)/$', views.model_select_view, name="model-select"),
]
Makeselect.html:
<form action="{% url 'reviews:model-select' make %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Select" />
</form>
Now, I have to pass "make" argument of the first form when posted, to the second view, and then use it to filter through Car instances with that make. But here all I pass is "None", and get Select a valid choice. That choice is not one of the available choices. error in the second form.
Any suggestion or feedback will be welcomed and greatly appreciated.
Thank you.
First point: model forms are for creating / editing models, so you should use plain forms here. Your error comes from having the make field in your ModelSelectForm but not setting its value anywhere. Also, ModelChoiceField is meant to retrieve a model instance, not field's value, so you really want a ChoiceField here.
Second point, since your goal is to display filtered informations - not to create or edit anything -, you should use GET queries (like for any "search" feature actually).
For your second form to work as expected (once ported to a plain Form with a single model field), you'll need to pass the make value to the form and, in the form's __init__(), update the model fields choices to the filtered queryset.
Also since you'll be using GET as form's method, you'll have to check wether the form has been submitted at all before deciding to instanciate it with or without the request.GET data, else the users would get error messages on first display before they even had a chance to submit anything. This is usually solved using either a name and value for the form's submit button or a hidden field in the form itself:
Forms:
class ModelSelectForm(forms.Form):
model = forms.ChoiceField()
def __init__(self, *args, **kwargs):
make = kwargs.pop("make", None)
if not make:
raise ValueError("expected a 'make' keyword arg")
super(ModelSelectForm, self).__init__(*args, **kwargs)
qs = Car.objects.filter(make=make).values_list('model',flat=True).distinct()
choices = [(value, value) for value in qs]
self.fields["model"].choices = choices
Views:
def model_select_view(request, make):
model = None
if request.GET.get("submitted", None):
form = ModelSelectForm(request.GET, make=make)
if form.is_valid():
model = form.cleaned_data['model']
else:
form = ModelSelectForm(make=make)
context = {"form": form, "model": model, "make: make}
return render(request, "reviews/modelselect.html", context)
Templates:
<form action="{% url 'reviews:model-select' make %}" method="GET">
{% csrf_token %}
<input type="hidden" name="submitted" value="1" />
{{ form.as_p }}
<input type="submit" value="Select" />
</form>
wrt/ your question about "passing 'make' to the second view": there's nowhere in your code snippet where you direct the user to the model-select view, but I assume that what you want is the user being redirected to it once he successfully selected the "make" in the first view. If yes, your first view's code should handle the case on successful form submission, ie:
def make_select_view(request):
if request.GET.get("submitted", None):
form = MakeSelectForm(request.GET)
if form.is_valid():
make = form.cleaned_data['make']
# send the user to the model selection view
return redirect("reviews:model-select", make=make)
else:
form = MakeSelectForm()
context = {"form": form}
return render(request, "reviews/makeselect.html", context)
Since the formatting of the snippet that I posted in comment got messed up so, I am writing that as an answer here.
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(GameForm, self).__init__(*args, **kwargs)
if not self.request.user.is_staff:
self.fields['publisher'].queryset = Publisher.objects.filter(id=self.request.user.id)

customizing django admin ChangeForm template / adding custom content

I'm able to insert (lame) static text onto the change form admin page, but I'd really like it to use the context of the current object being edited!
For instance, I want to format on the MyObject change form a URL to include the ID from a ForeignKey connected object (obj) as a link.
My admin objects:
class MyObjectChangeForm(forms.ModelForm):
class Meta:
model = MyObject
fields = ('field1', 'obj',)
class MyObjectAdmin(admin.ModelAdmin):
form = MyObjectChangeForm
list_display = ('field1', 'obj')
def render_change_form(self, request, context, *args, **kwargs):
self.change_form_template = 'admin/my_change_form.html'
extra = {'lame_static_text': "something static",}
context.update(extra)
return super(MyObjectAdmin, self).render_change_form(request,
context, *args, **kwargs)
My template templates/admin/my_change_form.html:
{% extends "admin/change_form.html" %}
{% block form_top %}
{{ lame_static_text }}
<a href="http://example.com/abc/{{ adminform.data.obj.id }}?"/>View Website</a>
{% endblock %}
The {{adminform.data.obj.id}} call obviously doesn't work, but I'd like something along those lines.
How do I insert dynamic context from the current object into the admin change form?
Add your extra context in change_view
class MyObjectAdmin(admin.ModelAdmin):
# A template for a very customized change view:
change_form_template = 'admin/my_change_form.html'
def get_dynamic_info(self):
# ...
pass
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['osm_data'] = self.get_dynamic_info()
return super(MyObjectAdmin, self).change_view(
request, object_id, form_url, extra_context=extra_context,
)
I believe the magic variable you seek is 'original', this contains the python object the change form is editing:
<a href="http://example.com/abc/{{ original.id }}?"/>View Website</a>

Categories