Multiple choices in flask-admin form_choices - flask

Well, I recently approached to flask-admin and I cannot figure out how to solve this issue. I know that I can use form_choices to restrict the possible values for a text-field by specifying a list of tuples. Anyway, form_choices allows to select only one value at a time. How can I specify that in some cases I may need of a comma-separated list of values?
I tried this workaround:
form_args = {
'FIELD': {
'render_kw': {"multiple": "multiple"},
}
}
but, even though a multiselect input actually appears on the webpage, only the first value is saved.
EIDT 05/13/2017
By playing a bit with flask-admin I found two possible (partial-)solution for my question, both of them with specific drawbacks.
1) The first deals with the use of Select2TagsField
from flask_admin.form.fields import Select2TagsField
...
form_extra_fields = {
'muri': Select2TagsField()
}
With this method is it possible to easily implement select menu for normal input text, even though at present I do not understand how to pass choices to Select2TagsField. It works well as a sort of multiple free text input. However, as far as I understand, it is not possible to pair Select2TagsField and form_choices
2) The second is a bit longer but it offers some more control on code (at least I presume).
Still it implies the use of form_choices, but this time paired with on_model_change
form_args = {
'FIELD': {
'render_kw': {"multiple": "multiple"},
}
}
form_choices = {'FIELD': [
('1', 'M1'), ('2', 'M2'), ('3', 'M3'), ('4', 'M4')
]}
...
def on_model_change(self, form, model, is_created):
if len(form.FIELD.raw_data) > 1:
model.FIELD = ','.join(form.FIELD.raw_data)
This solution, despite the former one, allows to map choices and works well when adding data to the model, but in editing it gives some problems. Any time I open the edit dialog the FIELD is empty. If I look at the data sent to the form (with on_form_prefill by printing form.FIELD.data) I get a comma separated string in the terminal but nothing appear in the pertinent select field on the webpage.

Maybe this is already outdated but I managed to change it and make it work with a multiple choice array field from postgres.
To make it work I extended Select2Field to know how to deal with the list:
class MultipleSelect2Field(Select2Field):
"""Extends select2 field to make it work with postgresql arrays and using choices.
It is far from perfect and it should be tweaked it a bit more.
"""
def iter_choices(self):
"""Iterate over choices especially to check if one of the values is selected."""
if self.allow_blank:
yield (u'__None', self.blank_text, self.data is None)
for value, label in self.choices:
yield (value, label, self.coerce(value) in self.data)
def process_data(self, value):
"""This is called when you create the form with existing data."""
if value is None:
self.data = []
else:
try:
self.data = [self.coerce(value) for value in value]
except (ValueError, TypeError):
self.data = []
def process_formdata(self, valuelist):
"""Process posted data."""
if not valuelist:
return
if valuelist[0] == '__None':
self.data = []
else:
try:
self.data = [self.coerce(value) for value in valuelist]
except ValueError:
raise ValueError(self.gettext(u'Invalid Choice: could not coerce'))
def pre_validate(self, form):
"""Validate sent keys to make sure user don't post data that is not a valid choice."""
sent_data = set(self.data)
valid_data = {k for k, _ in self.choices}
invalid_keys = sent_data - valid_data
if invalid_keys:
raise ValueError('These values are invalid {}'.format(','.join(invalid_keys)))
and to use it do this on the ModelView
class SomeView(ModelView):
form_args = dict(FIELD=dict(render_kw=dict(multiple="multiple"), choices=CHOICES_TUPLE))
form_overrides = dict(FIELD=MultipleSelect2Field)

For this approach to work you would need to use a column that can store a list of elements. At least with sqlite this is not possible using Flask-Admin. However it would be better for you to store your choices in a separate data model and use constraints to link the two models. See a working example here.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
app = Flask(__name__)
app.config['SECRET_KEY'] = '8e12c91677b3b3df266a770b22c82f2f'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLAlchemy(app)
admin = Admin(app)
item_tag_relation = db.Table('item_tag_relation',
db.Column('item_id', db.Integer, db.ForeignKey('item.id')),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)
class Item(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String())
tags = db.relationship("Tag",
secondary=item_tag_relation,
backref="items")
def __repr__(self):
return self.name
class Tag(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String())
def __repr__(self):
return self.name
class ItemModelView(ModelView):
pass
db.create_all()
admin.add_view(ItemModelView(Item, db.session))
admin.add_view(ModelView(Tag, db.session))
if __name__ == '__main__':
app.run(debug=True)

Related

Disabling options in django-autocomplete-light

Just started using django-autocomplete-light (autocomplete.ModelSelect2) and while I have managed to get it working, I wondered if it is possible to pass disabled options?
I have a list of customers to choose from but some, for various reasons, shouldn't be selected they shouldn't be able to use them. I know I could filter these non-selectable customers out, but this wouldn't be very usable as the user might think that the customer isn't in the database. If so, could someone point me in the right direction as I'm not sure where to start.
It says in the Select2 documentation that disabling options should be possible. Presumably if I could also send a 'disabled':true within the returned json response that might do it.
OK, so here is what I came up with and it works.
view.py
The Select2ViewMixin is subclassed and then a 'disabled' attribute is added to the customer details. This value provided by the ParentAutocomplete view.
from dal import autocomplete
from dal_select2.views import Select2ViewMixin
from dal.views import BaseQuerySetView
class CustomSelect2ViewMixin(Select2ViewMixin):
def get_results(self, context):
return [
{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
'disabled': self.is_disabled_choice(result), # <-- this gets added
} for result in context['object_list']
]
class CustomSelect2QuerySetView(CustomSelect2ViewMixin, BaseQuerySetView):
"""Adds ability to pass a disabled property to a choice."""
class ParentAutocomplete(CustomSelect2QuerySetView):
def get_queryset(self):
qs = Customer.objects.all()
if self.q:
qs = qs.filter(org_name__icontains=self.q)
return qs.order_by('org_name', 'org_city')
def get_result_label(self, item):
return item.selector_name
def get_selected_result_label(self, item):
return item.selector_name
def is_disabled_choice(self, item): # <-- this is where we determine if the record is selectable or not.
customer_id = self.forwarded.get('customer_id', None)
return not (item.can_have_children and not str(item.pk) == customer_id)
form.py
The form is then used as normal.
from dal import autocomplete
class CustomerBaseForm(forms.ModelForm):
customer_id= forms.IntegerField(required=False, widget=forms.HiddenInput)
class Meta:
model = Customer
widgets = {
'parent':autocomplete.ModelSelect2(
url='customer:parent-autocomplete',
forward=['customer_id'],
)
}
Hopefully this might be useful to someone.

Django FilterSet set initial value

We are using Django_Filterset in our project. I have been asked to set a default filter value for a foreign key column in the model
class RegFilter(django_filters.FilterSet):
class Meta:
model = models.Reg
fields = {
'id': ['exact'],
'nom_id': ['icontains'],
'nom_name': ['icontains'],
'product__name': ['icontains']
}
The product name should default to a product already in the db when the initial screen is displayed - any idea how this can be achieved? Appreciate your help.
I built on the answer given by #Gayathri and fixed a few small issues. He had a typo in his code, and the code wouldn't work with multiple choice fields, since they require the MultiValueDict functionality for multiple entries.
Given a rather regular view, here is the full code (in Python3 syntax):
class BookListView(FilterView):
model = Book
filterset_class = BookFilter
def get_filterset_kwargs(self, filterset_class):
kwargs = super().get_filterset_kwargs(filterset_class)
if kwargs['data'] is None:
filter_values = MultiValueDict()
else:
filter_values = kwargs['data'].copy()
if not filter_values:
# we need to use `setlist` for multi-valued fields to emulate this coming from a query dict
filter_values.setlist('status', ['new', 'ready'])
filter_values['sorting'] = '-created'
kwargs['data'] = filter_values
return kwargs
I managed to solve this one and thought the solution might help someone else in a situation similar to me.
In the views.py, override the FilterView method
def get_filterset_kwargs(self, filterset_class):
kwargs = super(RegFilter, self).get_filterset_kwargs(filterset_class)
if kwargs['data'] is None:
request_dict = {}
else:
request_dict = kwargs['data'].dict()
# This default will not be populated if any other filter options are chosen to restrict the query set
if not request_dict:
request_dict.update({
'product__name': 'ABC Product'
})
request_dict = kwargs['data']
return kwargs
This should now set the default product as 'ABC product' when no other options are passed to restrict the data fetched.

Pseudo-form in Django admin that generates a json object on save

I have a model with a field for a json object. This object is used on the site to control some css variables, among other things.
Right now in the admin, I have a text field where a user can save a json object. I'd like to show a form with all the attributes that, upon saving, will generate a json object.
Basically, the user sees, and the data is stored, like this:
{
"name":"hookedonwinter",
"user-id":123,
"basics":{
"height":150,
"weight":150
}
}
And I'd rather have the user see this:
Name: <input field>
User Id: <input field>
Height: <input field>
Weight: <input field>
and the data still be stored in json.
Any guidance would be appreciated. Links to docs that explain this, doubly appreciated.
Thanks!
Idea
Basically what you need to do is render your JSON into fields.
Create field for your model that stores JSON data.
Create form field
Create widget that:
Renders fields as multiple inputs
Takes data from POST/GET and transforms it back into JSON
You can also skip steps 1, 2 by overriding widget for TextField.
Documentation links
Widgets: https://docs.djangoproject.com/en/1.3/ref/forms/widgets/
Django code for reference how to create widgets: https://code.djangoproject.com/browser/django/trunk/django/forms/widgets.py
Proof of concept
I tried coding this solution and here is solution that worked for me without some edge cases.
fields.py
import json
from django.db import models
from django import forms
from django import utils
from django.utils.translation import ugettext_lazy as _
class JSONEditableField(models.Field):
description = _("JSON")
def formfield(self, **kwargs):
defaults = {'form_class': JSONEditableFormField}
defaults.update(kwargs)
return super(JSONEditableField, self).formfield(**defaults)
class JSONEditableWidget(forms.Widget):
def as_field(self, name, key, value):
""" Render key, value as field """
attrs = self.build_attrs(name="%s__%s" % (name, key))
attrs['value'] = utils.encoding.force_unicode(value)
return u'%s: <input%s />' % (key, forms.util.flatatt(attrs))
def to_fields(self, name, json_obj):
"""Get list of rendered fields for json object"""
inputs = []
for key, value in json_obj.items():
if type(value) in (str, unicode, int):
inputs.append(self.as_field(name, key, value))
elif type(value) in (dict,):
inputs.extend(self.to_fields("%s__%s" % (name, key), value))
return inputs
def value_from_datadict(self, data, files, name):
"""
Take values from POST or GET and convert back to JSON..
Basically what this does is it takes all data variables
that starts with fieldname__ and converts
fieldname__key__key = value into json[key][key] = value
TODO: cleaner syntax?
TODO: integer values don't need to be stored as string
"""
json_obj = {}
separator = "__"
for key, value in data.items():
if key.startswith(name+separator):
dict_key = key[len(name+separator):].split(separator)
prev_dict = json_obj
for k in dict_key[:-1]:
if prev_dict.has_key(k):
prev_dict = prev_dict[k]
else:
prev_dict[k] = {}
prev_dict = prev_dict[k]
prev_dict[dict_key[-1:][0]] = value
return json.dumps(prev_dict)
def render(self, name, value, attrs=None):
# TODO: handle empty value (render text field?)
if value is None or value == '':
value = '{}'
json_obj = json.loads(value)
inputs = self.to_fields(name, json_obj)
# render json as well
inputs.append(value)
return utils.safestring.mark_safe(u"<br />".join(inputs))
class JSONEditableFormField(forms.Field):
widget = JSONEditableWidget
models.py
from django.db import models
from .fields import JSONEditableField
class Foo(models.Model):
text = models.TextField()
json = JSONEditableField()
Hope this helps and here is how it looks:
I had similar task. I resolved it by creating Django form widget. You can try it for yours applications django-SplitJSONWidget-form
Interesting question! I'd like to see good and elegant solution for it :)
But it seems to me, that django-admin is not suitable for your task. I'd try to play with Forms. Smth like this:
class HmmForm(forms.Form):
name = forms.CharField(max_length = 128)
user_id = forms.IntegerField()
height = forms.IntegerField()
weight = forms.IntegerField()
def test(request, pk):
form = HmmForm()
if pk > 0:
hmm = Hmm.objects.get(pk = pk)
form = HmmForm( initial = {"name": hmm.name} )
return render_to_response("test/test.html", {"form": form})
And then simple render form in template, as you wish:
{{ form.as_table }} or {{ form.as_p }}
It's looks simple like this:
#Creating custom form
class MyCoolForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = ('field_that_stores_json', )
#field_that_shows_json1 = forms.CharField()
#field_that_shows_jsons = forms.CharField()
def __init__(self, *args, **kwargs):
#Deserizlize field that stores json here
def save(self, *args, **kwargs):
#Serialize fields that shows json here
After all, just set this form as a form for admin.
P.S.: Also you can write your own widget for form, that transforms json object into fields on js level and has textarea underneath.
Basically it sounds like you want a custom widget for your text field. The snippet on this page gives an example on how to render json key-value pairs. Even if it doesn't suit your needs entirely, especially as your nested json adds some complexity, it perhaps can give you some ideas.
As for the pure storage and retrieval of json objects into Python dicts, a few reusable JSONField implementations exist, like this one. You might want to add it to the mix.
Try using YAML as the format for user input, and then deserialize the object and serialize it back to json in the back end. Django already has serializers for that.
django-submodel may help you, although it cannot represent layered key-value now.
It's a pity to miss such HUGE bounty =p

Django: how to use custom manager in get_previous_by_FOO()?

I have a simple model MyModel with a date field named publication_date. I also have a custom manager that filters my model based on this date field.
This custom manager is accessible by .published and the default one by .objects.
from datetime import date, datetime
from django.db import models
class MyModelManager(models.Manager):
def get_query_set(self):
q = super(MyModelManager, self).get_query_set()
return q.filter(publication_date__lte=datetime.now())
class MyModel(models.Model):
...
publication_date = models.DateField(default=date.today())
objects = models.Manager()
published = MyModelManager()
This way, I got access to all objects in the admin but only to published ones in my views (using MyModel.published.all() queryset).
I also have
def get_previous(self):
return self.get_previous_by_publication_date()
def get_next(self):
return self.get_next_by_publication_date()
which I use in my templates: when viewing an object I can link to the previous and next object using
{{ object.get_previous }}
The problem is: this returns the previous object in the default queryset (objects) and not in my custom one (published).
I wonder how I can do to tell to this basic model functions (get_previous_by_FOO) to use my custom manager.
Or, if it's not possible, how to do the same thing with another solution.
Thanks in advance for any advice.
Edit
The view is called this way in my urlconf, using object_detail from the generic views.
(r'^(?P<slug>[\w-]+)$', object_detail,
{
'queryset': MyModel.published.all(),
'slug_field': 'slug',
},
'mymodel-detail'
),
I'm using Django 1.2.
In fact, get_next_or_previous_by_FIELD() Django function (which is used by get_previous_by_publication_date...) uses the default_manager.
So I have adapted it to reimplement my own utility function
def _own_get_next_or_previous_by_FIELD(self, field, is_next):
if not self.pk:
raise ValueError("get_next/get_previous cannot be used on unsaved objects.")
op = is_next and 'gt' or 'lt'
order = not is_next and '-' or ''
param = smart_str(getattr(self, field.attname))
q = Q(**{'%s__%s' % (field.name, op): param})
q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk})
qs = MyModel.published.filter(q).order_by('%s%s' % (order, field.name), '%spk' % order)
try:
return qs[0]
except IndexError:
def get_previous(self):
return self._own_get_next_or_previous_by_FIELD(MyModel._meta.fields[4], False)
def get_next(self):
return self._own_get_next_or_previous_by_FIELD(MyModel._meta.fields[4], True)
This is not a very clean solution, as I need to hardcode the queryset and the field used, but at least it works.

Django: How to make a query for on object based on an M2M field (multiple selections for field on search form)

I need help coming up with an efficient way to do a search query for a set of objects, based on a M2M field. My search form is going to look something like Blue Cross Blue Shield's | eg: this image
Now, let's suppose my model looks like this:
# models.py
class Provider(models.Model)
title = models.CharField(max_length=150)
phone = PhoneNumberField()
services_offered = models.ManyToManyField(ServiceType)
def __unicode__(self):
return self.title
class ServiceCategory(models.Model):
service_category = models.CharField(max_length=30)
def __unicode__(self):
return self.service_category
class Meta(object):
verbose_name_plural = "Service Categories"
class ServiceType(models.Model):
service_type = models.CharField(max_length=30)
service_category = models.ForeignKey(ServiceCategory)
def __unicode__(self):
return u'%s | %s' % (self.service_category, self.service_type
Also, we have to keep in mind that the options that we select are subject to change, since how they display on the form is dynamic (new ServiceCategories and ServiceTypes can be added at anytime). *How should I go about constructing a query for the Provider objects, given that a person using the search form can select multiple Services_Offered?*
This is currently my HIGHLY INEFFICIENT METHOD:
#managers.py
from health.providers.models import *
from django.db.models import Q
class Query:
def __init__(self):
self.provider_objects=Provider.objects.all()
self.provider_object=Provider.objects
self.service_object=ServiceType.objects
self.category_objects=ServiceCategory.objects.all()
def simple_search_Q(self, **kwargs): #matt's learning note: **kwargs passes any dictionary
return self.provider_objects.filter(
Q(services_offered__service_type__icontains=kwargs['service']),
Q(title__icontains=kwargs['title']),
Q(state=kwargs['state']),
).distinct().order_by('title')
====================
#views.py
from django.shortcuts import render_to_response
from health.providers.models import *
from health.search.forms import *
from health.search.managers import Query #location of the query sets
from django.core.paginator import Paginator, InvalidPage, EmptyPage
from django.template import RequestContext
def simple_search(request):
if request.method == 'POST':
SimpleSearch_form = SimpleSearch(request.POST)
if SimpleSearch_form.is_valid():
request.session["provider_list"] = None
kwargs = {'title': request.POST['title'],
'service': request.POST['service'], 'state': request.POST['state'] }
provider_list = Query().simple_search_Q(**kwargs)
return pagination_results(request, provider_list)
else:
SimpleSearch_form = SimpleSearch()
return render_to_response('../templates/index.html', { 'SimpleSearch_form': SimpleSearch_form},
context_instance=RequestContext(request))
How can I make my query:
Obtain Provider objects based on selecting multiple request.POST['service']
More efficient
Thanks for any help in advanced.
Best Regards,
Matt
1: for multiple request.POST['service'], I assume you mean these are CheckBoxes.
I'd make the CheckBox values ID's, not names, and do a PK lookup.
'services_offered__pk__in': request.POST.getlist('service')
That would return all Provider objects that have ALL of the services selected.
PS: You are also using CapitalCase for instances which is very confusing. If you want your code to be readable, I highly recommend some changes to your style (don't use CapitalCase for instances or variables) and make your variables more descriptive.
SimpleSearch_form = SimpleSearch() # what is SimpleSearch?
simplesearch_form = SimpleSearchForm() # now, it's very clear what the class SimpleSearchForm is
# and the form instance is clearly a for instance.
2: making it more efficient? You could get rid of a lot of code and code separation by remove your whole Query class. Also, I don't know why you are using Q objects since you are not doing anything that would require it (like OR or OR + AND).
def simple_search(request):
if request.method == 'POST':
searchform = SimpleSearchForm(request.POST)
if searchform.is_valid():
request.session['provider_list'] = None
post = request.POST
providers = Provider.objects.filter(services_offered__pk__in=post.getlist('services'),
title=post['title'], state=post['state'])
return pagination_results(request, provider_list)
else:
searchform = SimpleSearchForm()
return direct_to_template(request, '../templates/index.html', { 'searchform': searchform})