Django testing: post request with a Form with paramters - python

I want to test that my view redirects after I send it correct data. The problem is that I'm using a form that needs parameters while initializing. I have a method that builds me a response that I later use in the tests. The method works for my other views but I can't make it work with views that use Forms that need parameters to initialize them.
In my forms.py I have:
class SupportIssueForm(forms.ModelForm):
class Meta:
model = SupportIssue
fields = ('user', 'property', 'title', 'text', 'is_urgent', 'is_service')
def __init__(self, person_company, properties, is_property, *args, **kwargs):
super(SupportIssueForm, self).__init__(*args, **kwargs)
self.fields['is_service'].widget.attrs['class'] = 'custom-control-input'
...
My method for generating the response I'm later using in my test looks like this:
def generate_logged_in_user_post_response(self, data):
request = self.factory.post(self.url, data={**self.form_class_args[0], **data})
request.user = self.logged_in_user
return new_support_issue_view(request)
I also tried this:
def generate_logged_in_user_post_response(self, data):
form = SupportIssueForm(**self.form_class_args[0], data=data)
request = self.factory.post(self.url, form)
request.user = self.logged_in_user
return new_support_issue_view(request)
the self.form_class_args[0] is some dictionary I declare elsewhere:
{
"person_company": person_company, # <query object>
"properties": properties, # <query object>
'is_property': False
}

Related

Show soft warning from clean method of Django Formset for Model Form

I've got a model form and a formset that I'm using successfully without issue.
In my clean method of the formset I perform some logic - but one of the requirements that previously caused a ValidationError is now being changed and should just be a simple warning to the user in the template.
My model form and formset look like this:
class AlignmentForm(forms.ModelForm):
def __init__(self, *args, employee, **kwargs):
self.person = person
super().__init__(*args, **kwargs)
class Meta:
model = Alignment
fields = [
"alignment",
"start_date",
"end_date",
]
class AlignmentFormSet(forms.BaseModelFormSet):
def clean(self):
if any(self.errors):
return
# I've trimmed out additional logic and checks
# This is the area of concern
alignment_error = {}
for i, form in enumerate(self.forms):
if not end_date:
if alignment == "Wrong":
alignment_error = {
"alignment": alignment,
"added_message": "Corrected",
}
if alignment_error:
raise forms.ValidationError(
f"""
The alignment is
({alignment_error['alignment']}) and must
be ({alignment_error['added_message']}).
"""
)
return form
The current ValidationError needs to become just a message that is presented to the user in the template - but does not stop the form from validating and saving.
I've tried doing something like this:
if alignment_error:
messages.warning(
request,
f"""
The alignment is
({alignment_error['alignment']}) and must
be ({alignment_error['added_message']}).
""",
)
But that didn't work because I don't have access to request. Can I get access to it?
What is the best way to accomplish this? I still want to display the message, but just don't want it to prevent the form from saving.
EDIT TO ADD:
The views function looks like this: (the essential pieces)
def person(request, person_no):
person = get_user_model().objects.get(person_no=person_no)
formset = get_alignment_formset(person, request.POST)
if request.method == "POST":
# If add alignment is in form the util method will handle adding
# the extra form if add alignment not in post data actually validate
# and process the form
if "add_alignment" not in request.POST:
if formset.is_valid():
# If form is valid we want to get the person being updated
# current alignment
alignment_before = Alignment.get_active_primary(person)
for formset_form in formset:
if formset_form.has_changed():
alignment = formset_form.save(commit=False)
alignment.user = person
if not alignment.pk:
alignment.created_by = request.user
alignment.modified_by = request.user
alignment.save()
else:
alignment = formset_form.save(commit=False)
# do some logic with position control here
warnings, errors = update_foil_alignment_app(
request.user, person, alignment_before, alignment
)
if errors or warnings:
for error in errors:
messages.error(request, error)
for warning in warnings:
messages.warning(request, warning)
kwargs = {"person_no": person.person_no}
return redirect("app_name:person", **kwargs)
messages.success(
request,
f"""
You have successfully updated alignments for
{alignment.user.last_name}, {alignment.user.first_name}.
""",
)
kwargs = {"person_no": person.person_no}
return redirect("app_name:person", **kwargs)
The utils function looks like this:
def get_alignment_formset(person, post_data=None):
extra = 0
# If add alignment in post_data we add the extra form and clear out
# post data, we don't actually want to post only data
if post_data:
if "add_alignment" in post_data:
post_data = None
extra = 1
formset = modelformset_factory(
Alignment,
fields=("alignment", "start_date", "end_date"),
extra=extra,
form=AlignmentForm,
formset=AlignmentFormSet,
)
formset = formset(
post_data or None,
form_kwargs={"person": person},
queryset=Alignment.exclude_denied.filter(user=person).annotate(
num_end_date=Count("end_date")
).order_by(
"-end_date",
),
)
return formset
I believe I've been able to pass the request in appropriately at this point by doing the following:
Views.py:
def person(request, person_no):
person = get_user_model().objects.get(person_no=person_no)
formset = get_alignment_formset(person, request.POST, request)
Utils.py:
def get_alignment_formset(person, post_data=None, request=None):
extra = 0
# If add alignment in post_data we add the extra form and clear out
# post data, we don't actually want to post only data
if post_data:
if "add_alignment" in post_data:
post_data = None
extra = 1
formset = modelformset_factory(
Alignment,
fields=("alignment", "start_date", "end_date"),
extra=extra,
form=AlignmentForm,
formset=AlignmentFormSet,
)
formset = formset(
post_data or None,
form_kwargs={"person": person, "request": request},
queryset=Alignment.exclude_denied.filter(user=person).annotate(
num_end_date=Count("end_date")
).order_by(
"-end_date",
),
)
return formset
Forms.py:
class AlignmentForm(forms.ModelForm):
def __init__(self, *args, employee, request, **kwargs):
self.person = person
self.request = request
super().__init__(*args, **kwargs)
class AssignmentFormSet(forms.BaseModelFormSet):
def clean(self):
...
...
if alignment_error:
warning = "There was an issue"
messages.warning(form.request, warning)
It now shows up as a warning as expected.

Django restframework: ReadOnlyModelViewSet create HTTP_403 but expected HTTP_405

I am creating a REST-API with django and the django rest framework, therefore I am using ReadOnlyModelViewSet. In my unit test for testing create / POST method, I am expecting HTTP_405_METHOD_NOT_ALLOWED but actually it is HTTP_403_FORBIDDEN. For the update / PUT it is HTTP_405 as expected. Is this behavior the default behavior or do I have a dumb bug?
And yes the user is authenticated.
The Viewset:
class StudentSolutionReadOnlyModelViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StudentSolution.objects.all()
serializer_class = StudentSolutionSerializer
def get_permissions(self):
if self.request.method == 'POST':
return (permissions.IsAuthenticated(), )
return (permissions.IsAuthenticated(), IsStudentSolutionOwnerOrAdmin(),)
def get_queryset(self):
if self.request.user.is_staff:
return StudentSolution.objects.all(
course_assignment=self.kwargs['course_assignment_pk']
).order_by('student__last_name', 'course_assignment__due_date')
return StudentSolution.objects.filter(
student=self.request.user,
course_assignment=self.kwargs['course_assignment_pk']
).order_by('course_assignment__due_date')
Edit 1:
class StudentSolutionReadOnlyModelViewSetTests(TestCase):
def setup(self):
#[..]
def test_create_user_not_allowed(self):
data = {
'course_assignment': self.course_assignment.id,
'student': self.user.id
}
url = self.generate_url(self.course_assignment.id)
self.csrf_client.credentials(HTTP_AUTHORIZATION='JWT ' + self.user_token)
resp = self.csrf_client.post(
self.url,
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(resp.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
I am using the same authentication and csrf stuff for all methods, as well as generate_url method.

some troubles with CreateView in the Django

class Biochemical_analysis_of_blood(CreateView):
model = BiochemicalAnalysisOfBlood
form_class = BiochemicalAnalysisOfBloodForm
template_name = "biochemical_analysis_of_blood.html"
success_url = reverse_lazy("patients")
def get_context_data(self, **kwargs):
context = super(Biochemical_analysis_of_blood, self).get_context_data(**kwargs)
patient = Patient.objects.get(id=1)
context["patient"] = patient
return context
def post(self, request, *args, **kwargs):
analysis = Analyzes()
sid = transaction.savepoint()
analysis.name = request.POST["name"]
analysis.patient_id = Patient.objects.get(id=1)
analysis.who_send = request.POST["who_send"]
analysis.who_is_doctor = request.POST["who_is_doctor"]
analysis.lab_user_id = Doctor.objects.get(id=request.POST["lab_user_id"])
analysis.additional_lab_user = request.POST["lab_user_add"]
analysis.date = '2017-06-18'
analysis.type = 3
analysis.date_analysis = '2017-06-18'
analysis.save()
return super(Biochemical_analysis_of_blood, self).post(request, *args, **kwargs)
I have next algorithm:
Render BiochemicalAnalysisOfBloodForm to the user
When he fills fields and presses button "save" I create a new instance of Analyzes() and fill it programmatically and when in the post method I call super().post() then users data will be written to the model BiochemicalAnalysisOfBlood automatically? But I have next error:
NOT NULL constraint failed:
laboratory_biochemicalanalysisofblood.analysis_id
How can I in hand mode add to the model to the field "analysis" the early created instance of Analyzes()? I don't understand this class to the end where I can find information about all it's opportunities
The main part of your algorithm should reside in your form, because you want to pass the analysis_id to the instance being saved
class BiochemicalAnalysisOfBloodForm(ModelForm):
def save(self, commit=True):
analysis = Analyzes()
sid = transaction.savepoint()
analysis.name = self.data["name"]
analysis.patient_id = Patient.objects.get(id=1)
analysis.who_send = self.data["who_send"]
analysis.who_is_doctor = self.data["who_is_doctor"]
analysis.lab_user_id = Doctor.objects.get(id=self.data["lab_user_id"])
analysis.additional_lab_user = self.data["lab_user_add"]
analysis.date = '2017-06-18'
analysis.type = 3
analysis.date_analysis = '2017-06-18'
analysis.save()
# Your analysis is created, attach it to the form instance object
self.instance.analysis_id = analysis.id
return super().save(commit)
Before doing the super().post you can modify the request.POST data to include your analysis id:
request.POST['analysis_id'] = analysis.id
that might help.
Also note that if the form validation fails in super().post, you will still have created an Analysis object which might not be useful. You could use override the form_invalid method of the CreateView to handle this.

Django - Getting parameters from URL

I am passing the parameters in URL to my view like this:
127.0.0.1:8000/cars/?model_number=13375
and getting it like this:
class GetCarDetails(View):
def get(self, *args, **kwargs):
model_number = "";
if request.GET.get('model_number'):
model_number = request.GET.get('model_number')
But I want to pass it now like this:
127.0.0.1:8000/cars/13375/
And I want Django to treat the 13375 like model number
You need to define it at the url's:
url(r'^cars/(?P<pk>[0-9]+)/$', views.GetCarDetails.as_view(), name="getcardetails"),
And in your views:
class GetCarDetails(View):
...
def get_context_data(self, **kwargs):
context = super(GetCarDetails, self).get_context_data(**kwargs)
context["model_number"] = self.kwargs['pk'];
return context
As told by #Anentropic in comments, more detailed info here

Update the Queryset of a Django-Select2 AutoModelSelect2Field

I can't figure out how to update the queryset of a AutoModelSelect2Field dynamically. I'm getting really strange results. For example, sometimes the select2 box will return the correct, filtered results, and sometimes it will return NO results when I enter the same characters.
my code:
#views.py
form = MyForm()
#forms.py
class MyField(AutoModelSelect2Field):
search_fields = ['name__icontains']
max_results = 10
class MyForm(forms.Form):
my_field = MyField(
queryset=project.objects.none(),
required=True,
widget=AutoHeavySelect2Widget(
select2_options={
'width': '100%',
}
)
)
def __init__(self, *args, **kwargs):
qs = kwargs.pop('queryset')
self.base_fields['my_field'].queryset = qs
super(MyForm, self).__init__(*args, **kwargs)
#self.fields['my_field'].queryset = qs
#self.base_fields['my_field'].queryset = qs
A few of the things I've tried -
update from the view:
#views.py
form = MyForm()
form.base_fields['my_field'].queryset = new_qs
and:
form = MyForm()
form.fields['my_field'].queryset = new_qs
pass the qs to the form:
#views.py
form = MyForm(queryset=Project.objects.filter(project_type=pt))
# see above code for forms.py
I've also tried setting the initial qs to all objects:
class MyForm(forms.Form):
my_field = MyField(
queryset=project.objects,
...
But I get the same problem, 90% of the time I get the results of the initial queryset, rather than the filtered objects based on the new qs.
We were able to find a pretty straightforward way to get the dropdown options to filter by additional fields (ie. first select country and then have the state dropdown only showing states from the selected country)
It was inspired by a suggestion from here (where we also posted this solution):
https://github.com/applegrew/django-select2/issues/22
in forms.py:
class StateChoices(AutoModelSelect2Field):
queryset = State.objects
def get_results(self, request, term, page, context):
country = request.GET.get('country', '')
states = State.objects.filter(country=country, name__istartswith=term)
s2_results = [(s.id, s.name, {}) for s in states]
return ('nil', False, s2_results)
the form field:
# only include this when you would have a screen where the country
# is preset and would not change, but you want to use it as a search context
country = forms.ModelChoiceField(queryset=Country.objects.all(),
widget=forms.HiddenInput())
state = StateChoices(widget = AutoHeavySelect2Widget(
select2_options = {
'minimumInputLength': 1,
'ajax': {
'dataType': 'json',
'quietMillis': 100,
'data': JSFunctionInContext('s2_state_param_gen'),
'results': JSFunctionInContext('django_select2.process_results'),
}
}))
in our javascript:
function s2_state_param_gen(term, page) {
var proxFunc = $.proxy(django_select2.get_url_params, this);
results = proxFunc(term, page, 's2_condition_param_gen');
results.country = $('#id_country').val();
return results;
}

Categories

Resources