How to show related object popup when the user clicks on selected autocomplete field options in Django admin? - django

I want to show the standard related object popup when the user clicks on a selected option in a Django admin autocomplete multi-select field, like it works when clicking the ForeignKey field pencil icon 🖉.
The models are as follows:
class Author(models.Model):
name = models.CharField(_('name'), max_length=160)
class Book(models.Model):
authors = models.ManyToManyField(Author, verbose_name=_('authors'), blank=True)
...
Is it possible to do this by extending Django admin?

I found that adding the required custom HTML was easier using the ModelSelect2Multiple widget from django-autocomplete-light (DAL).
The admin configuration is as follows:
from dal import autocomplete
#admin.register(Book)
class BookAdmin(admin.ModelAdmin):
class Media:
js = [
'/static/books/js/book-admin.js',
# other required JS files, see https://github.com/yourlabs/django-autocomplete-light/issues/1143#issuecomment-632755326
]
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == 'authors':
return forms.ModelMultipleChoiceField(
required=False,
label=Author._meta.verbose_name_plural.title(),
queryset=Author.objects.all(),
widget=autocomplete.ModelSelect2Multiple(
url='author-autocomplete',
attrs={'data-html': True}))
return super().formfield_for_foreignkey(db_field, request, **kwargs)
The DAL view is as follows:
from django.utils.safestring import mark_safe
from dal import autocomplete
from .models import Author
from django.urls import reverse
class AuthorAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_staff:
return Author.objects.none()
qs = Author.objects.all()
if self.q:
qs = qs.filter(name__icontains=self.q)
return qs
def get_selected_result_label(self, item):
change_url = reverse('admin:books_author_change', kwargs={'object_id': item.id})
return mark_safe('<span onclick="event.stopPropagation(); showRelatedObjectPopup({'
f"href: '{change_url}?_popup=1', id: 'change_id_author'"
f'}})">{item.name}</span>')
When selecting a new author in the Authors field in the Book change view in admin, the HTML element is managed by ModelSelect2Multiple, so the custom HTML is present and clicking on the newly selected author opens the popup as intended. But the custom HTML will not be present in existing selections, so the click handlers have to be added with jQuery in book-admin.js:
'use strict';
window.addEventListener("load", function () {
/**
* Show related object popup when user clicks on selected author name.
*/
(function ($) {
var $authorsSelect2Selections = $('div.form-row.field-authors .select2-selection__choice > span:nth-child(2)');
var $authorOptions = $('#id_authors > option');
$authorsSelect2Selections.click(function ($event) {
$event.stopPropagation();
var self = this;
// Find corresponding option by text comparison, assuming that author name is unique
var $result = $authorOptions.filter(function() {
return $(this).text() === self.textContent;
});
showRelatedObjectPopup({
href: '/admin/books/author/' + $result.val() + '/change/?_popup=1',
id: 'change_id_other_authors'
});
});
})(django.jQuery);
});
event.stopPropagation() prevents the Select2 dropdown from opening.
You also need to override dismissChangeRelatedObjectPopup and dismissAddRelatedObjectPopup in book-admin.js to avoid problems, here is an incomplete version:
/**
* Override Django related object popup dismissal functions with DAL amendments.
* Incomplete.
*/
(function ($) {
function dismissChangeRelatedObjectPopupForDAL(win, objId, newRepr, newId) {
var elem = document.getElementById(win.name);
if (elem && elem.options && elem.dataset.autocompleteLightUrl) { // this is a DAL element
$(elem.options).each(function () {
if (this.value === objId) {
this.textContent = newRepr;
// this.value = newId;
}
});
// FIXME: trigger('change') does not update the element as it should and removes popup code
// $(elem).trigger('change');
win.close();
} else {
dismissChangeRelatedObjectPopupOriginal(win, objId, newRepr, newId);
}
}
window.dismissChangeRelatedObjectPopupOriginal = window.dismissChangeRelatedObjectPopup;
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopupForDAL;
function dismissAddRelatedObjectPopupForDAL(win, newId, newRepr) {
var elem = document.getElementById(win.name);
if (elem && elem.options && elem.dataset.autocompleteLightUrl) { // this is a DAL element
elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
// FIXME: trigger('change') adds the new element, but removes popup code
$(elem).trigger('change');
win.close();
} else {
dismissAddRelatedObjectPopupOriginal(win, newId, newRepr);
}
}
window.dismissAddRelatedObjectPopupOriginal = window.dismissAddRelatedObjectPopup
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopupForDAL
})(django.jQuery);

Related

How to get the currentUser provided by spring security in Grails 2 unit testing

Hi guys i am on trouble about getting the current user provided by spring.
Here's my unit test code
void "Test if adding project will sucess"() {
given:
def createProjectMock = mockFor(UserService)
createProjectMock.demand.createNewProject { Map projectMap ->
return true
}
controller.userService = createProjectMock.createMock()
when: "saveProject is execute"
controller.saveProject()
then: "page will to the list to view the saved project"
response.redirectedUrl == '/user/index2'
}
Here's my controller
def saveProject(ProjectActionCommand projectCmd) {
def currentUser = springSecurityService.currentUser
if (projectCmd.hasErrors()) {
render view: 'createProject', model: [projectInstance: projectCmd, user:currentUser]
} else {
def getProjectMap = [:]
getProjectMap = [
projectName: params.projectName,
user: currentUser
]
def saveProject = userService.createNewProject(getProjectMap)
if (saveProject) {
redirect view: 'index2'
} else {
render 'Error upon saving'
}
}
}
And here's my service
Project createNewProject(Map projectMap){
def createProject = new Project()
createProject.with {
projectName = projectMap.projectName
user = projectMap.user
}
createProject.save(failOnError:true, flush: true)
}
And i always getting this error:
Cannot get property 'currentUser' on null object.
Hope you can help me. Thanks
Cannot get property 'currentUser' on null object.
means that you haven't mocked springSecurityService. Let's do it in setup section (I assume it may be useful also in other methods in this class):
def springSecurityService
def setup() {
springSecurityService = Mock(SpringSecurityService)
controller.springSecurityService = springSecurityService
}
At this point your code is going to work. However remember that you can always mock also the actual logged user and test it at any point:
User user = Mock(User)
springSecurityService.currentUser >> user

Angular 5 & Django REST - Issue uploading files

I developed an Angular application where the user can handle brands.
When creating/updating a brand, the user can also upload a logo. All data are sent to the DB via a REST API built using the Django REST Framework.
Using the Django REST Framework API website I'm able to upload files, but using Angular when I send data thu the API I get an error.
I also tried to encode the File object to base64 using FileReader, but I get the same error from Django.
Can you help me understanding the issue?
Models:
export class Brand {
id: number;
name: string;
description: string;
is_active: boolean = true;
is_customer_brand: boolean = false;
logo_img: Image;
}
export class Image {
id: number;
img: string; // URL path to the image (full size)
img_md: string; // medium size
img_sm: string; // small
img_xs: string; // extra-small/thumbnail
}
Service:
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { Brand } from './brand';
const endpoint = 'http://127.0.0.1:8000/api/brands/'
#Injectable()
export class BrandService {
private brands: Array<Brand>;
constructor(private http: Http) { }
list(): Observable<Array<Brand>> {
return this.http.get(endpoint)
.map(response => {
this.brands = response.json() as Brand[];
return response.json();
})
.catch(this.handleError);
}
create(brand: Brand): Observable<Brand> {
console.log(brand);
return this.http.post(endpoint+'create/', brand)
.map(response => response.json())
.catch(this.handleError);
}
get(id): Observable<Brand> {
return this.http.get(endpoint+id)
.map(response => response.json())
.catch(this.handleError);
}
private handleError(error:any, caught:any): any {
console.log(error, caught);
}
}
Error from the browser console:
"{"logo_img":{"img":["The submitted data was not a file. Check the
encoding type on the form."]}}"
Django Serializer:
class BrandSerializer(ModelSerializer):
is_active = BooleanField(required=False)
logo_img = ImageSerializer(required=False, allow_null=True)
class Meta:
model = Brand
fields = [
'id',
'name',
'description',
'is_active',
'is_customer_brand',
'logo_img',
]
def update(self, instance, validated_data):
image = validated_data.get('logo_img',None)
old_image = None
if image:
image = image.get('img',None)
brand_str = validated_data['name'].lower().replace(' ','-')
ext = validated_data['logo_img']['img'].name.split('.')[-1].lower()
filename = '{0}.{1}'.format(brand_str,ext)
user = None
request = self.context.get('request')
if request and hasattr(request, 'user'):
user = request.user
image_serializer_class = create_image_serializer(path='logos', filename=filename, created_by=user, img_config = {'max_w':3000.0,'max_h':3000.0,'max_file_size':1.5,'to_jpeg':False})
image_serializer = image_serializer_class(data=validated_data['logo_img'])
image_serializer.is_valid()
validated_data['logo_img'] = image_serializer.save()
old_image = instance.logo_img
super(BrandSerializer, self).update(instance,validated_data)
if old_image: # Removing old logo
old_image.img.delete()
old_image.img_md.delete()
old_image.img_sm.delete()
old_image.img_xs.delete()
old_image.delete()
return instance
def create(self, validated_data):
image = validated_data.get('logo_img',None)
print(image)
if image:
print(image)
image = image.get('img',None)
print(image)
brand_str = validated_data['name'].lower().replace(' ','-')
ext = validated_data['logo_img']['img'].name.split('.')[-1].lower()
filename = '{0}.{1}'.format(brand_str,ext)
user = None
request = self.context.get('request')
if request and hasattr(request, 'user'):
user = request.user
image_serializer_class = create_image_serializer(path='logos', filename=filename, created_by=user, img_config = {'max_w':3000.0,'max_h':3000.0,'max_file_size':1.5,'to_jpeg':False})
image_serializer = image_serializer_class(data=validated_data['logo_img'])
image_serializer.is_valid()
validated_data['logo_img'] = image_serializer.save()
return super(BrandSerializer, self).create(validated_data)
When posting a new brand to the server with files, I have three main choices:
Base64 encode the file, at the expense of increasing the data size by around 33%.
Send the file first in a multipart/form-data POST, and return an ID to the client. The client then sends the metadata with the ID, and the server re-associates the file and the metadata.
Send the metadata first, and return an ID to the client. The client then sends the file with the ID, and the server re-associates the file and the metadata.
The Base64 encoding will involve unacceptable payload.
So I choose to use multipart/form-data.
Here's how I implemented it in Angular's service:
create(brand: Brand): Observable<Brand> {
let headers = new Headers();
let formData = new FormData(); // Note: FormData values can only be string or File/Blob objects
Object.entries(brand).forEach(([key, value]) => {
if (key === 'logo_img') {
formData.append('logo_img_file', value.img);
} else {
formData.append(key, value);
});
return this.http.post(endpoint+'create/', formData)
.map(response => response.json())
.catch(this.handleError);
}
IMPORTANT NOTE: Since there's no way to have nested fields using FormData, I cannot append formData.append('logo_img', {'img' : FILE_OBJ }). I had change the API in order to receive the file in one field called logo_img_file.
Hope that my issue helped someone.

Saving user object to post Graphql/Apollo Vue

I am trying to save a user id to a new biz. I keep getting a 400 error and can not figure out why. I am using django for the backend with graphql and apollo client for the front with vue js. I am able to get the owner id but not able to save it for some reason.
Create Biz Mutation Apollo
export const CREATE_BIZ_MUTATION = gql`
mutation CreateBizMutation($name: String!, $owner: ID!) {
createBiz(name: $name, ownerId: $owner) {
name
}
}`
Create Biz mutation Django
class CreateBiz(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
code = graphene.String()
owner = graphene.Field(UserType)
class Arguments:
name = graphene.String()
def mutate(self, info, name):
user = get_user(info) or None
code = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(6))
biz = Biz(
code = code,
name = name,
owner = user
)
biz.save()
return CreateBiz(
id= biz.id,
name = biz.name,
code = biz.code,
owner = biz.owner
)
Create Biz Component
createBiz () {
const owner = localStorage.getItem(DJANGO_USER_ID)
if (!owner) {
console.error('No user logged in')
return
}
const { name } = this.$data
this.$apollo.mutate({
mutation: CREATE_BIZ_MUTATION,
variables: {
name,
owner
}
}).catch((error) => {
console.log(error)
})
}
}

How to make django querysets with dynamic filters?

So basically I have a website in Django that is a storefront and the end user has three filters to use. A product type filter (pants, shoes, shirts, etc), a delivery filter (yes/no), and a location/popularity filter.
Currently in my views.py I have this method.
if request.is_ajax():
if request.GET.get('filter') == 'shirts':
latest_entries = Entry.objects.filter(entrytype="shirts")
context = {'latest_entries': latest_entries}
return render(request, 'storefrontload.html', context)
if request.GET.get('filter') == 'pants':
latest_entries = Entry.objects.filter(entrytype="pants")
context = {'latest_entries': latest_entries}
return render(request, 'storefrontload.html', context)
if request.GET.get('filter') == 'shoes':
latest_entries = Entry.objects.filter(entrytype="shoes")
context = {'latest_entries': latest_entries}
return render(request, 'storefrontload.html', context)
As you can see, this handles the first filter. The problem I'm having is if I select, let's say 'pants', it filters by pants but disregards what was selected in the other two filters. Another example, let's say I select pants, the page populates with the results of that filter. But if I then go to the delivery filter and select "Yes", the page populates with items that are only deliverable, but the "pants" filter is forgotten.
What I'm trying to figure how is how to create a queryset in my views that remembers the other two querysets (if that makes sense).
The only way I can think of to do this is to create True/False flags for each value in each filter, and then add probably 100 lines of if/then statements checking each flag. There has got to be a better way.
UPDATE:
This is how I am passing the filter value from my template.
function filter(type) {
$.get("/storefront/?filter="+type, function(data) {
var $data = data;
$('.grid').children().remove();
$('.grid').append( $data ).masonry( 'appended', $data, true ).masonry( 'layout' );
});
}
//Product Filter
$("#shirts").unbind().click(function () {
filter("shirts");
return false;
});
$("#pants").unbind().click(function () {
filter("pants");
return false;
});
$("#shoes").unbind().click(function () {
filter("shoes");
return false;
});
//Delivery Filter
$("#deliveryyes").unbind().click(function () {
filter("deliveryyes");
return false;
});
$("#deliveryno").unbind().click(function () {
filter("deliveryno");
return false;
});
In my views.py, this will not work:
entry_types = request.GET.getlist('filter')
latest_entries = Entry.objects.filter(entrytype__in=entry_types)
Because I will need to filter by entrytype('pants', 'shirts', shoes') AND deliveryoption ('deliveryyes', 'deliveryno'). Each filter has it's own column in my model.
models.py
class Entry(models.Model):
headline= models.CharField(max_length=200,)
body_text = models.TextField()
author=models.ForeignKey(settings.AUTH_USER_MODEL, related_name='entryauthors')
pub_date=models.DateTimeField(auto_now_add=True)
zipcode =models.IntegerField(null=True, max_length=10)
!!! entrytype = models.CharField(null=True, max_length=10)
!!! deliveryoption=models.CharField(null=True, max_length=5)
You can pass the comma-separated list of the filter values:
/mylist/filter=shirts,pants
And then get entries using the __in lookup:
entry_types = request.GET.get('filter', '').split(',')
latest_entries = Entry.objects.filter(entrytype__in=entry_types)
Or use the the getlist() method of the QueryDict:
/mylist/filter=shirts&filter=pants
With the same ORM call:
entry_types = request.GET.getlist('filter')
latest_entries = Entry.objects.filter(entrytype__in=entry_types)
UPDATE: To pass the multiple types to the view save them in the array and use join() method to get the comma-separated string of them:
var types = []; // currently shown entry types
function filter(type) {
// add or remove the items from the grid
var index = types.indexOf(type);
if (index > -1) {
types.splice(index, 1); // remove the type from the list
} else {
types.push(type); // add the type to the filter
}
$.get("/storefront/?filter="+types.join(","), function(data) {
...
}
}
UPDATE 2: If you use two fields to filter the queryset then you have to create two separate lists in your view:
filters = request.GET.getlist('filter')
entry_types = [f for f in filters if not f.startswith('delivery')]
delivery_types = [f for f in filters if f.startswith('delivery')]
latest_entries = Entry.objects.all()
if entry_types:
latest_entries = latest_entries.filter(entrytype__in=entry_types)
if delivery_types:
latest_entries = latest_entries.filter(deliverytype__in=delivery_types)
JavaScript code will work without any touches.
It is perfectly valid for a parameter to show up multiple times in a request. Use QueryDict.getlist() to get a list containing all values.
http://example.com/?foo=12&foo=34
...
print >>sys.stderr, request.GET.getlist('foo')

How to add forreign key fields like it happens in django admin site?

Actually am trying to add a foreign key field in my form like how it happens in django admin site . When you click on the green " + " button it opens up a new pop up window where you add the respective field .
My models are like :
class DealType(models.Model):
label = models.CharField(max_length = 100)
def __unicode__(self):
return self.label
class Deal(models.Model):
label = models.ForeignKey(DealType, blank = True, null = True)
.
.
.
And i want to add DealType while i fill up my DealForm .
I think you have to create a seperate view to create a DealType.
In your DealForm you add a link to open that view.
...
I took a look at an admin page from a project of mine.
html
<img src="/static/admin/img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"/>
Javascript
taken from
<script type="text/javascript" src="/static/admin/js/admin/RelatedObjectLookups.js"> </script>
function showAddAnotherPopup(triggeringLink) {
var name = triggeringLink.id.replace(/^add_/, '');
name = id_to_windowname(name);
href = triggeringLink.href
if (href.indexOf('?') == -1) {
href += '?_popup=1';
} else {
href += '&_popup=1';
}
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
win.focus();
return false;
}
This opens a new window with the view with the add form.
This view should add the DealType and then close the window using the following function also found in the same javascript file
function dismissAddAnotherPopup(win, newId, newRepr) {
// newId and newRepr are expected to have previously been escaped by
// django.utils.html.escape.
newId = html_unescape(newId);
newRepr = html_unescape(newRepr);
var name = windowname_to_id(win.name);
var elem = document.getElementById(name);
if (elem) {
if (elem.nodeName == 'SELECT') {
var o = new Option(newRepr, newId);
elem.options[elem.options.length] = o;
o.selected = true;
} else if (elem.nodeName == 'INPUT') {
if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
elem.value += ',' + newId;
} else {
elem.value = newId;
}
}
} else {
var toId = name + "_to";
elem = document.getElementById(toId);
var o = new Option(newRepr, newId);
SelectBox.add_to_cache(toId, o);
SelectBox.redisplay(toId);
}
win.close();
}
This is just backtracked from the admin panel but it should get you started.
...
Found a guide that walks you through the process here which probably explains alot better. (havn't read it)