I'm trying to test my Comment Form with FileField.
class CommentForm(forms.ModelForm):
files = forms.FileField(
widget=ClearableFileInput(
attrs={"multiple": True, "class": "form-control", "id": "formFile"}
),
required=False,
label=_("Files"),
)
def __init__(self, *args, **kwargs):
self.ticket = None
super().__init__(*args, **kwargs)
class Meta:
model = Comment
fields = ["body", "files"]
labels = {"body": _("Comment body")}
def save(self, commit=True, new_status=None):
instance = super(CommentForm, self).save(commit=False)
if commit:
instance.save()
file_list = []
for file in self.files.getlist("files") or self.initial.get("files") or []:
file_instance = self.save_attachment(file=file)
file_list.append(file_instance)
if len(file_list) > 0:
async_task(add_attachments, file_list, instance.ticket.get_pk())
Writing test without attachment upload is easy, but problem starts when I want to test my form with files. I could not test whether async_task is called, and when I try to mock files and pass them to the Form, the attachment object does not apper in test database
Here is my test:
#patch("myapp.forms.comments.async_task", autospec=True)
def test_create_comment_with_attachment(self, async_task):
file_mock = mock.MagicMock(spec=File)
file_mockname = "test_name.pdf"
form = CommentForm(
data={
"body": "Test comment",
"files": mock.MagicMock(file_mock)
}
)
form.ticket = self.ticket
self.assertTrue(form.is_valid())
form.save()
attachment = Attachment.objects.last()
print(attachment)
The result of printing attachment is None, while I expected my file_mock to appear.
Can anyone help my with testing attachment upload?
Related
here I am using model forms and trying to make my placeholder dynamic.
my approach is to take request data, pass it into widgets with f string.
what I am trying to achieve is
{'placeholder': f"commenting as {request.user.username}"}
HERE IS MY CODE.
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ("body",)
widgets = {
"body": forms.TextInput(
attrs={
"placeholder": "Enter your comment",
"class": "comment-form-text",
}
),
}
labels = {
"body": "",
}
This is how I usually pass the request object in a form.
Note: all you need is the CommentForm.__init__ and calling it with CommentForm(request.POST, request=request)
I just added the custom save, but commented it out, to show you can also access it there and do some cool things! :-)
forms.py
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ("body",)
widgets = {
"body": forms.TextInput(
attrs={
"class": "comment-form-text",
}
),
}
labels = {
"body": "",
}
def __init__(self, *args, **kwargs):
# # Keeping track of if it's an edit form or not ( Not Required, but handy )
# self.is_edit = True if 'instance' in kwargs else False
# Store Request Object
self.request = kwargs.pop('request') if 'request' in kwargs else None
super(CommentForm, self).__init__(*args, **kwargs)
# You can add *Any* custom attribute here to any field
self.fields['body'].widget.attrs={'placeholder': 'commenting as {0}'.format(self.request.user.username)}
# # Just showing that you can also use it in a Custom Save Method :-)
# def save(self, commit=True):
# obj = super(CommentForm, self).save(commit=False)
#
# # Note: Keeping track of **if** it's an edit so we don't re-add to the field!
# if not self.is_edit:
# # Use Request to fill a field (New)
# obj.creator = request.user
# else:
# # Use request to fill a field (edit)
# obj.last_editor = request.user
views.py
def commentformview(request):
form = CommentForm(data=request.POST or None, request=request)
if request.method == 'POST':
if form.is_valid():
form.save()
# redirect
data = {
'form': form,
}
return render(request, 'commentform.html', data)
I can't find out how to do this, I have a list view, that when you click on one of the list objects, it takes you to an update page, but I can't work out how you pass the instance so to it so that and data posted goes to that instance on the database. CBV do this automatically in a hidden black box way, and I can't see how it is done for a function based view.
Model
class Project(models.Model):
date_published = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=128, unique=True)
slug = models.SlugField(max_length=64)
def save(self, *args, **kwargs):
if not self.id:
self.slug = slugify(self.title)
super(Project, self).save(*args, **kwargs)
def __str__(self):
return self.title
Form
class ProjectUpdateForm(forms.ModelForm):
class Meta:
model = Update
fields = [
'project',
'category',
'update'
]
View
def project_update_view(request, slug):
obj = Project.objects.get(slug=slug)
form = ProjectUpdateForm(request.POST or None)
if form.is_valid():
form.save()
context = {
"form": form,
"object": obj
}
return render(request, 'project_portal/project_update.html', context)
url:
path('<slug:slug>/update/', project_update_view, name='project-update'),
So I want to be able to do away with the 'project' field in the Form because the user is already looking at that instance he shouldn't have to then pick it in the form.
Remove the project in the field, and set in in the view, like:
class ProjectUpdateForm(forms.ModelForm):
class Meta:
model = Update
fields = [
# 'project',
'category',
'update'
]
In the view, you can then set the project attribute of the instance manually:
def project_update_view(request, slug):
obj = Project.objects.get(slug=slug)
if request.method == 'POST':
form = ProjectUpdateForm(request.POST)
form.instance.project = obj
if form.is_valid():
form.save()
return redirect('success-url')
else:
form = ProjectUpdateForm()
context = {
"form": form,
"object": obj
}
return render(request, 'project_portal/project_update.html', context)
Some extra notes:
do not use request.POST or None, since a POST request can be valid and have no POST parameters;
in case the POST is successful, you should implement a Post/Redirect/Get pattern [wiki].
I am using model form and I'm trying to bypass validation of one particular field.
I have ascertained that I need use the clean() method to bypass the field. however when im printing out the cleaned data the assigned_subnets field is not in the dictionary, it is missing
actions:
I create the assigned subnet field manually in forms.py. Then using jquery I alter that form field and add more select options to it post all the options. The additional values posted were not part of the original field choices, hence the error.
output from print:
{'site_data': None, 'switches': None, 'hostname': 'STR--RTR-01', 'template': <ConfigTemplates: STR-RTR-01>, 'model': <DeviceModel: Cisco - 4431>, 'install_date': datetime.date(2016, 5, 26), 'ospf_area': None, 'snmp_data': <SNMPData: XXXX>, 'available_subnets': <QuerySet []>}
forms.py
class DeviceForm(forms.ModelForm):
class Meta:
model = DeviceData
fields = ['site_data', 'switches', 'hostname', 'template', 'model', 'install_date','ospf_area','snmp_data']
def clean(self):
super(DeviceForm, self).clean()
print(self.cleaned_data)
if self.cleaned_data.get('assigned_subnets') in self._errors:
del self._errors['assigned_subnets']
return self.cleaned_data
def __init__(self, *args, **kwargs):
site_id = kwargs.pop('site_id', None)
device_id = kwargs.pop('device_id', None)
self.is_add = kwargs.pop("is_add", False)
super(DeviceForm, self).__init__(*args, **kwargs)
devicesubnet = Subnets.objects.filter(devicesubnets__device_id=device_id)
sitesubnet = Subnets.objects.filter(sitesubnets__site_id=site_id)
common_subnets = list(set(devicesubnet) & set(sitesubnet))
subnet_id_list = []
for s in common_subnets: subnet_id_list.append(s.id)
available_subnet_data = sitesubnet.exclude(id__in=subnet_id_list)
assigned_choices = []
devicesubnet_data = DeviceSubnets.objects.filter(device_id=device_id)
for choice in devicesubnet_data:
assigned_choices.append((choice.subnet.id,choice.subnet.subnet))
self.fields['available_subnets'] = forms.ModelMultipleChoiceField(
label='Available Subnets',
queryset=available_subnet_data,
widget = forms.SelectMultiple(
attrs = {'class': 'form-control', 'size' : '15'}
)
)
self.fields['assigned_subnets'] = forms.MultipleChoiceField(
label='Assigned Subnets',
choices=assigned_choices,
widget = forms.SelectMultiple(
attrs = {'class': 'form-control', 'size' : '15'}
)
)
self.fields['available_subnets'].required = False
self.fields['assigned_subnets'].required = False
self.helper = FormHelper(self)
self.helper.form_id = 'device_form'
self.helper.form_method = 'POST'
...
views.py
class EditDevice(UpdateView):
model = DeviceData
form_class = DeviceForm
template_name = "config/device_form.html"
#method_decorator(user_passes_test(lambda u: u.has_perm('config.edit_device')))
def dispatch(self, *args, **kwargs):
self.site_id = self.kwargs['site_id']
self.site = get_object_or_404(SiteData, pk=self.site_id)
return super(EditDevice, self).dispatch(*args, **kwargs)
def get_success_url(self, **kwargs):
return reverse_lazy("config:device_details", args=(self.site_id,))
def form_valid(self, form):
form.instance.site_data = self.site
assigned_subnets = form.cleaned_data['assigned_subnets']
print(assigned_subnets)
return super(EditDevice, self).form_valid(form)
def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs()
kwargs['site_id'] = self.site_id
kwargs['device_id'] = self.object.pk
return kwargs
...
EDIT:
what im trying to do is like in the image below. I have a list of available subnets and a list of chosen (assigned) subnets.
I don think this form widget exists outside of Django admin? so Ive created the two fields manually and used jquery to move a subnet from available to assigned.
then I get the assigned subnets and update the DB. however I get the errors when I alter the assigned field
I have uncovered a few problems with the way Tastypie is creating bundle objects:
1) When I do a PUT to the detail endpoint of a Card resource (see below tests.py) without including the "id" field, instead of updating the resource from the URI, a new resource is created.
2) Another more significant issue is, if I include the "id" field, then the resource indicated in the "id" is updated and not the resource at the URI; this means I can update any resource by pointing to any other detail URI (I haven't built in auth yet, but this may be an authorization problem in the future).
The first test below fails (unless I remove default=uuid1_as_base64 in the model field definition and implement the logic in the model's save method) and the second test below passes even though it shouldn't pass.
What is happening here?
tests.py
def test_PUT_detail(self):
# Test 1: PUT to detail endpoint of card 1 without an ID field
put_data = {'text': 'wakka wakka'}
response = self.api_client.put(
uri='/api/v1/cards/rpHBJNOkEeKr2hTa6Uod1w/', # card 1 uri
data=put_data
)
self.assertEqual(self.card_1.read_file(), 'wakka wakka')
# Test 2: PUT to detail endpoint of card 1 with ID field of card 2
put_data = {'id': 'twt_UtOkEeKsuxTa6Uod1w', 'text': 'wakka wakka'}
response = self.api_client.put(
uri='/api/v1/cards/rpHBJNOkEeKr2hTa6Uod1w/', # still card 1 uri
data=put_data
)
self.assertEqual(self.card_2.read_file(), 'wakka wakka')
Bundle objects created during the above tests:
#Test 1
<Bundle for obj: 'rpHBJNOkEeKr2hTa6Uod1w' and with data: '{'text': u'wakka wakka', 'pk': u'rpHBJNOkEeKr2hTa6Uod1w'}'>
#Test 2 (notice how both the id field and pk field are included)
<Bundle for obj: 'twt_UtOkEeKsuxTa6Uod1w' and with data: '{'text': u'wakka wakka', 'id': u'twt_UtOkEeKsuxTa6Uod1w', 'pk': u'rpHBJNOkEeKr2hTa6Uod1w'}'>
api.py
class CardResource(ModelResource):
text = fields.CharField()
def __init__(self, *args, **kwargs):
super(CardResource, self).__init__(*args, **kwargs)
self.fields['id'].read_only = True
class Meta:
queryset = Card.objects.all()
fields = ['id', 'text']
resource_name = 'cards'
list_allowed_methods = ['get', 'post']
detail_allowed_methods = ['get', 'put', 'delete']
authorization = Authorization()
validation = CardValidation()
include_resource_uri = False
def dehydrate_text(self, bundle):
return bundle.obj.read_file()
def save(self, *args, **kwargs):
bundle = super(CardResource, self).save(*args, **kwargs)
bundle.obj.write_file(bundle.data['text'])
return bundle
models.py
class Card(TimeStampedModel):
id = models.CharField(max_length=22, db_index=True, primary_key=True,
default=uuid1_as_base64) # default is a callable
def file_path(self):
return '/'.join([settings.CARD_ROOT, self.id + '.txt'])
def read_file(self):
try:
with open(self.file_path(), 'rb') as f:
content = f.read()
return content
except IOError:
self.write_file(text='')
return self.read_file()
def write_file(self, text=''):
with open(self.file_path(), 'wb') as f:
f.write(text)
def __unicode__(self):
return u'%s' % self.id
How to set first default rows/values in django admin's inline?
class Employee(models.Model):
username = models.CharField(_('Username'), max_length=150, null=False, blank=False)
email = models.CharField(_('Email'), max_length=150, null=False, blank=False)
class Details(models.Model):
employee = models.ForeignKey(Employee, verbose_name=_('Employee'), blank=False, null=False)
label = models.CharField(_('Label'), max_length=150, null=False, blank=False)
value = models.CharField(_('Value'), max_length=150, null=False, blank=False)
class DetailsFormset(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.initial = [
{ 'label': 'first name'},
{'label': 'last name'},
{'label': 'job',}]
super(DetailsFormset, self).__init__(*args, **kwargs)
class DetailsInline(admin.TabularInline):
model = Details
formset = DetailsFormset
fieldsets = [
['', {'fields': ['employee', 'label', 'value']}]
]
class EmployeeAdmin(admin.ModelAdmin):
inlines = [DetailsInline]
but this row doesn't work
self.initial = [
{ 'label': 'first name'},
{'label': 'last name'},
{'label': 'job',}]
How do I set default values using django admin?
from django.utils.functional import curry
class DetailsInline(admin.TabularInline):
model = Details
formset = DetailsFormset
extra = 3
def get_formset(self, request, obj=None, **kwargs):
initial = []
if request.method == "GET":
initial.append({
'label': 'first name',
})
formset = super(DetailsInline, self).get_formset(request, obj, **kwargs)
formset.__init__ = curry(formset.__init__, initial=initial)
return formset
From here: Pre-populate an inline FormSet?
If what you need is to define default values for the new forms that are created you can redefine the empty_form property of a InlineFormSet:
class MyDefaultFormSet(django.forms.models.BaseInlineFormSet):
#property
def empty_form(self):
form = super(MyDefaultFormSet, self).empty_form
# you can access self.instance to get the model parent object
form.fields['label'].initial = 'first name'
# ...
return form
class DetailsInline(admin.TabularInline):
formset = MyDefaultFormSet
Now, every time you add a new form it contains the initial data you provided it with. I've tested this on django 1.5.
I tried many suggestions from Stackoverflow (Django=4.x), not working for me.
Here is what I did.
class MilestoneFormSet(forms.models.BaseInlineFormSet):
model = Milestone
def __init__(self, *args, **kwargs):
super(MilestoneFormSet, self).__init__(*args, **kwargs)
if not self.instance.pk:
self.initial = [
{'stage': '1.Plan', 'description': 'Requirements gathering', },
{'stage': '2.Define', 'description': 'Validate requirement', },
]
class MilestoneInline(admin.TabularInline):
model = Milestone
formset = MilestoneFormSet
def get_extra(self, request, obj=None, **kwargs):
extra = 0 #default 0
if not obj: #new create only
extra = 2 #2 records defined in __init__
return extra
I hope this works for everyone.
To provide a static default for all instances in the inline, I found a simpler solution that just sets it in a form:
class DetailsForm(django_forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['label'] = 'first_name'
class DetailsInline(admin.TabularInline):
form = DetailsForm
# ...
I think this doesn't work for the OP's particular case because each form has a different value for the 'label' field, but I hope it can be useful for anyone coming to this page in the future.