Many to many - proper usage - django

I have models like this:
User:
#somefields
apps = models.ManyToManyField(App)
Here, User is created initially during registration. He links apps using many to many in a separate page where user selects(multiple) only apps, no other fields.
What is the best way to populate apps in Django ?
For example, if we can create User also, we can simply use modelform where .save(commit=False) will create user and .save_m2m() will link apps. But here we dont need to create User. We will be having user already, we only need to add apps. How ?
Newly added details:
I know how to do with normal "form" and normal web way. But I want to write quality code so want to know. If there is nothing wrong in using normal form then its fine.

There are multiple ways to do it, but for example:
class UserAppForm(ModelForm):
class Meta:
model = User
fields = ['apps']
And in your view, you configure the routing so that you can:
Get the ID of the user to select
Raise an error if you do not find the ID (Instead of creating a new one)
Initialize the form with the user
For example:
def user_add_app(request, pk):
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
form = UserAppForm(request.POST, instance=user)
if form.is_valid():
user = form.save()
return HttpResponseRedirect(<some redirect>)
else:
form = EmployeeForm(instance=user)
return render(request, "<some html file>", {'form': form})

Maybe I did not fully understand the question (Please clarify if it's not the case), but there is no problem having two forms on the same model:
One form to create the User, without selecting apps (Which will be empty)
One form to add Apps to the User, taking into account that it already exists (And is not created at this time).
Does it answer the question ?

You can create a ModelForm form with model = App and a User field. Now when creating the form set an initial value for User :
form = AppForm(initial={'user': 1})

Related

Passing anonymous users from one view to another in Django 3.0.5

I'm trying to create a fully anonymous survey, where the survey participant enters a landing site (index.html), clicks a link and is directed to a survey view. On this survey (pre_test.html) page, I want to assign a new Participant object with a primary key and link that to the Survey model via a foreign key. Because this Survey isn't the main part of my study, I want to send that Participant object to a new view, where the Participant primary key is again used as a foreign key to link to another model (call it Task).
What I've tried so far in the views.py is:
def pre_test(request):
if request.method == "POST":
participant = Participants()
participant.save()
participant_pk = participant.pk
form = PreTestQuestionnaireForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.save()
post_primary = PreTestQuestionnaire(pk=post.pk)
post_primary.Analyst_id = Participants(pk=participant_pk)
post_primary.save()
request.session['user'] = participant_pk
return HttpResponseRedirect(reverse('main:picture_test'))
else:
form = PreTestQuestionnaireForm()
return render(request, 'study/pre_test.html', {'form': form})
def picture_test(request):
obj = Participants(Unique_ID=request.session.get('user')) # Unique_ID is the pk I've set for Participants
but when calling print(obj) all I get is Participants object (None). What am I missing in using the session? Should I not be using sessions in this way at all, or should I create actual users in another table without giving them passwords and other data? Keeping the users anonymous is essential and I want to avoid cookies as much as possible, although I can write code to remove cookies after each session.
I believe I have found the culprit.
I had set SESSION_COOKIE_SECURE=True in the settings.py file, which blocks cookies from working well in localhost development and thus using request.session.get() doesn't return anything.

Django form: ask for confirmation before committing to db

Update: The solution can be found as a separate answer
I am making a Django form to allow users to add tvshows to my db. To do this I have a Tvshow model, a TvshowModelForm and I use the generic class-based views CreateTvshowView/UpdateTvshowView to generate the form.
Now comes my problem: lets say a user wants to add a show to the db, e.g. Game of Thrones. If a show by this title already exists, I want to prompt the user for confirmation that this is indeed a different show than the one in the db, and if no similar show exists I want to commit it to the db. How do I best handle this confirmation?
Some of my experiments are shown in the code below, but maybe I am going about this the wrong way. The base of my solution is to include a hidden field force, which should be set to 1 if the user gets prompted if he is sure he wants to commit this data, so that I can read out whether this thing is 1 to decide whether the user clicked submit again, thereby telling me that he wants to store it.
I would love to hear what you guy's think on how to solve this.
views.py
class TvshowModelForm(forms.ModelForm):
force = forms.CharField(required=False, initial=0)
def __init__(self, *args, **kwargs):
super(TvshowModelForm, self).__init__(*args, **kwargs)
class Meta:
model = Tvshow
exclude = ('user')
class UpdateTvshowView(UpdateView):
form_class = TvshowModelForm
model = Tvshow
template_name = "tvshow_form.html"
#Only the user who added it should be allowed to edit
def form_valid(self, form):
self.object = form.save(commit=False)
#Check for duplicates and similar results, raise an error/warning if one is found
dup_list = get_object_duplicates(Tvshow, title = self.object.title)
if dup_list:
messages.add_message(self.request, messages.WARNING,
'A tv show with this name already exists. Are you sure this is not the same one? Click submit again once you\'re sure this is new content'
)
# Experiment 1, I don't know why this doesn't work
# form.fields['force'] = forms.CharField(required=False, initial=1)
# Experiment 2, does not work: cleaned_data is not used to generate the new form
# if form.is_valid():
# form.cleaned_data['force'] = 1
# Experiment 3, does not work: querydict is immutable
# form.data['force'] = u'1'
if self.object.user != self.request.user:
messages.add_message(self.request, messages.ERROR, 'Only the user who added this content is allowed to edit it.')
if not messages.get_messages(self.request):
return super(UpdateTvshowView, self).form_valid(form)
else:
return super(UpdateTvshowView, self).form_invalid(form)
Solution
Having solved this with the help of the ideas posted here as answers, in particular those by Alexander Larikov and Chris Lawlor, I would like to post my final solution so others might benefit from it.
It turns out that it is possible to do this with CBV, and I rather like it. (Because I am a fan of keeping everything OOP) I have also made the forms as generic as possible.
First, I have made the following forms:
class BaseConfirmModelForm(BaseModelForm):
force = forms.BooleanField(required=False, initial=0)
def clean_force(self):
data = self.cleaned_data['force']
if data:
return data
else:
raise forms.ValidationError('Please confirm that this {} is unique.'.format(ContentType.objects.get_for_model(self.Meta.model)))
class TvshowModelForm(BaseModelForm):
class Meta(BaseModelForm.Meta):
model = Tvshow
exclude = ('user')
"""
To ask for user confirmation in case of duplicate title
"""
class ConfirmTvshowModelForm(TvshowModelForm, BaseConfirmModelForm):
pass
And now making suitable views. The key here was the discovery of get_form_class as opposed to using the form_class variable.
class EditTvshowView(FormView):
def dispatch(self, request, *args, **kwargs):
try:
dup_list = get_object_duplicates(self.model, title = request.POST['title'])
if dup_list:
self.duplicate = True
messages.add_message(request, messages.ERROR, 'Please confirm that this show is unique.')
else:
self.duplicate = False
except KeyError:
self.duplicate = False
return super(EditTvshowView, self).dispatch(request, *args, **kwargs)
def get_form_class(self):
return ConfirmTvshowModelForm if self.duplicate else TvshowModelForm
"""
Classes to create and update tvshow objects.
"""
class CreateTvshowView(CreateView, EditTvshowView):
pass
class UpdateTvshowView(EditTvshowView, UpdateObjectView):
model = Tvshow
I hope this will benefit others with similar problems.
I will post it as an answer. In your form's clean method you can validate user's data in the way you want. It might look like that:
def clean(self):
# check if 'force' checkbox is not set on the form
if not self.cleaned_data.get('force'):
dup_list = get_object_duplicates(Tvshow, title = self.object.title)
if dup_list:
raise forms.ValidationError("A tv show with this name already exists. "
"Are you sure this is not the same one? "
"Click submit again once you're sure this "
"is new content")
You could stick the POST data in the user's session, redirect to a confirmation page which contains a simple Confirm / Deny form, which POSTs to another view which processes the confirmation. If the update is confirmed, pull the POST data out of the session and process as normal. If update is cancelled, remove the data from the session and move on.
I have to do something similar and i could do it using Jquery Dialog (to show if form data would "duplicate" things) and Ajax (to post to a view that make the required verification and return if there was a problem or not). If data was possibly duplicated, a dialog was shown where the duplicated entries appeared and it has 2 buttons: Confirm or Cancel. If someone hits in "confirm" you can continue with the original submit (for example, using jquery to submit the form). If not, you just close the dialog and let everything as it was.
I hope it helps and that you understand my description.... If you need help doing this, tell me so i can copy you an example.
An alternative, and cleaner than using a vaidationerror, is to use Django's built in form Wizard functionality: https://django-formtools.readthedocs.io/en/latest/wizard.html
This lets you link multiple forms together and act on them once they are all validated.

Pre-filled user info in Django Comments?

The site I'm building uses the standard user management framework and
the standard comments framework.
What I'd like to see happen is the comments form rendered with the
user's name and email address pre-filled if they are already signed in
(or have the fields hidden entirely - kinda like theregister's comments system!).
Easy enough?
If you want to use django-comments with logged-in users, the post_comment does already populate some fields right when being authenticated, you just need to make some tweaks. There are some instructions on how to do that!
If user are signed in, you can use something like this in your view:
user = request.user
profile = user.get_profile()
if request.method == 'POST':
edit_form = YourForm(data = request.POST, user = user)
if edit_form.is_valid():
...
else:
dict = {'email':user.email, 'username':user.username}
form = YourForm(user = user, data = dict)
tpl_dict = {'form' : form,}
return render_to_response('template.html', tpl_dict)
it will fill the form fields 'username' and 'email'
When I'm signed into my django site it automatically loads my username and email address.

Django ModelForms - 'instance' not working as expected

I have a modelform that will either create a new model or edit an existing one - this is simple and should work, but for some reason I'm getting a new instance every time.
The scenario is this is the first step in an ecommerce order. The user must fill out some info describing the order (which is stored in the model). I create the model, save it, then redirect to the next view for the user to enter their cc info. I stick the model in the session so I don't have to do a DB lookup in the next view. There is a link in the template for the second (cc info) view that lets the user go back to the first view to edit their order.
# forms.py
class MyForm(forms.ModelForm):
class Meta:
fields = ('field1', 'field2')
model = MyModel
# views.py
def create_or_update(request):
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
m = form.save(commit=False)
# update some other fields that aren't in the form
m.field3 = 'blah'
m.field4 = 'blah'
m.save()
request.session['m'] = m
return HttpResponseRedirect(reverse('enter_cc_info'))
# invalid form, render template
...
else:
# check to see if we're coming back to edit an existing model
# this part works, I get an instance as expected
m = request.session.get('m', None)
if m:
instance = get_object_or_None(MyModel, id=m.id)
if instance:
form = MyForm(instance=instance)
else:
# can't find it in the DB, but it's in the session
form = MyForm({'field1': m.field1, 'field2': m.field2})
else:
form = MyForm()
# render the form
...
If I step through in the debugger when I go back to the view to edit an order that the form is created with the instance set to the previously created model, as expected. However, when the form is processed in the subsequent POST, it creates a new instance of the model when form.save() is called.
I believe this is because I've restricted the fields in the form, so there is nowhere in the rendered HTML to store the id (or other reference) to the existing model. However, I tried adding both a 'pk' and an 'id' field (not at the same time), but then my form doesn't render at all.
I suspect I'm making this more complicated than it needs to be, but I'm stuck at the moment and could use some feedback. Thanks in advance.
This is interesting. Here is my stab at it. Consider this line:
form = MyForm(request.POST)
Can you inspect the contents of request.POST? Specifically, check if there is any information regarding which instance of the model is being edited. You'll find that there is none. In other words, each time you save the form on POST a new instance will be created.
Why does this happen? When you create a form passing the instance=instance keyword argument you are telling the Form class to return an instance for an instance of the model. However when you render the form to the template, this information is used only to fill in the fields. That is, the information about the specific instance is lost. Naturally when you post pack there is way to connect to the old instance.
How can you prevent this? A common idiom is to use the primary key as part of the URL and look up an instance on POST. Then create the form. In your case this would mean:
def create_or_update(request, instance_id):
# ^^^^^
# URL param
if request.method == 'POST':
instance = get_object_or_None(Model, pk = instance_id)
# ^^^^^
# Look up the instance
form = MyForm(request.POST, instance = instance)
# ^^^^^^^
# pass the instance now.
if form.is_valid():
....

Allow changing of User fields (like email) with django-profiles

Django lets you create a model foreign-keyed to User and define it in settings as the official "profile" model holding additional data for user accounts. django-profiles lets you easily display/create/edit that profile data. But the user's primary email address is part of their main account, not part of their extended profile. Therefore when you put
{{ form }}
in the profile/edit_profile template, the primary email address does not show up. You can retrieve it manually with
{{ user.email }}
but changes to it aren't saved back to the account upon submit of course. I'm assuming a custom ModelForm has been created, such as:
class ProfileForm(ModelForm):
class Meta:
model = Parent
exclude = ('family','user','board_pos','comm_job',)
and that ProfileForm is being passed to django-profiles' view code with a urlconf like:
('^profiles/edit', 'profiles.views.edit_profile', {'form_class': ProfileForm,}),
The same problem would come up if you wanted to let users change their first or last names. What's the best way to let users change their own email addresses or names when using django-profiles?
Here's the solution we ended up using:
# urls.py
# First match /profiles/edit before django-profiles gets it so we can pass in our custom form object.
('^profiles/edit', 'profiles.views.edit_profile', {'form_class': ProfileForm,}),
(r'^profiles/', include('profiles.urls')),
Now we override the save method in the form itself, so that when the form is saved, the email address is pushed into the saving user's User object at the same time. Graceful.
# forms.py , trimmed for brevity
class ProfileForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
try:
self.fields['email'].initial = self.instance.user.email
except User.DoesNotExist:
pass
email = forms.EmailField(label="Primary email")
class Meta:
model = Parent
def save(self, *args, **kwargs):
"""
Update the primary email address on the related User object as well.
"""
u = self.instance.user
u.email = self.cleaned_data['email']
u.save()
profile = super(ProfileForm, self).save(*args,**kwargs)
return profile
Works perfectly. Thanks mandric.
I think that implementing a Separate page just for change of email is best, since it would need to be verified etc...
If you would like to enable users to modify all their profile info together with their main email address, then you need to create your own Form (ModelForm will not work here). I suggest you start doing this and post a question when you get stuck.
Start by copying all the fields out of django-profile model into your custom form, and add the users primary email field.
You will have to "override" the django-profile edit url and basically copy the html template if there is one.
Another option (bad) would be to hack django-profiles app and change it there. But that will, likely, introduce a lot of bugs, and will render your app unapgradable.
I think the easiest way would definitely be to use a form. Use the form to display their current email address (which they could change), and then use your view to extract the request, retrieve the appropriate profile belonging to that user by matching some other parameter you could pass to the template, and then storing the new data and saving the model.