How can I embed extra column/external URLs in Flask-appbuilder list/detail model view? - flask-appbuilder

Flask-appbuilder's ModelView can display list and detail for a model. Very handy and save many times for CURD operations.
Sometimes the application demands more features with extra column(s) besides CURD operations. For example, in a IoT related Device ModelView, besides CRUD, I want to link to anther realtime gauge web page, or call Web API offered by device server to send command to device.
In other Python framework, like Tornado/Cyclone, I will manually designed a template page (with extra buttons) and (embed extra) javascript code. But I am still not familiar with FAB's structure.
I can make these extra operations as external links to other exposed methods. And add these links to models as data fields. But I think these design is quite ugly. And its URL is too long to display as well.
Any better ideas? Which methods should be overriden?

I found a solution from FAB's issues site on Github. In models.py, you can define a method, then use the method in views.py. Then the resource list page will treat the method as addtional column. This solution has a drawback, you have to write HTML in a model method.
Here is my code.
models.py
class Device(Model):
id = Column(Integer, primary_key = True)
snr = Column(String(256), unique = True)
name = Column(String(128))
addr = Column(String(256))
latitude = Column(Float)
longitude = Column(Float)
status = Column(Enum('init','normal','transfer','suspend'), default = 'init')
owner_id = Column(Integer, ForeignKey('account.id'))
owner = relationship("Account")
def __repr__(self):
return self.name
def get_gauge_url(self):
btn = "<i class=\"fa fa-dashboard\">".format(self.id)
return btn
views.py
class DeviceView(ModelView):
datamodel = SQLAInterface(Device)
related_views = [PermitView, EventView]
label_columns = {'snr':'SNR',
'owner_id':'Owner',
'get_gauge_url':'Gauge'}
list_columns = ['name','snr','addr','owner','get_gauge_url']
edit_columns = ['name','snr','owner','addr','latitude','longitude','status',]
add_columns = edit_columns
show_fieldsets = [
('Summary',
{'fields':['name','snr','owner']}
),
('Device Info',
{'fields':['addr','latitude','longitude','status'],'expanded':True}
),
]

Related

Flask-Appbuilder - User security role required in View

If the current user role = admin then show all the records in the table. If not, then limit the rows by created user.
I can get the user name if I define a function in the View Class, but need it before the list is constructed. See source code below.
from flask_appbuilder.models.sqla.filters import FilterEqualFunction
from app import appbuilder, db
from app.models import Language
from wtforms import validators, TextField
from flask import g
from flask_appbuilder.security.sqla.models import User
def get_user():
return g.user
class LanguageView(ModelView):
datamodel = SQLAInterface(Language)
list_columns = ["id", "name"]
base_order = ("name", "asc")
page_size = 50
#This is the part that does not work - unable to import app Error: Working outside of application context
#If the user role is admin show all, if not filter only to the specific user
if g.user.roles != "admin":
base_filters = [['created_by', FilterEqualFunction, get_user]]
This is the error I'm getting:
Was unable to import app Error: Working outside of application context.
This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context(). See the
documentation for more information.
In this case it is better to create two different ModelViews, one for users with base_filters = [['created_by', FilterEqualFunction, get_user]] and the second for admins only without any filtering.
And do not forget to specify correct permissions for both.
In my case, I created new filter FilterStartsWithFunction by coping FilterStartsWith(You can find the source code in Flask-Appbuilder pack easily. ). see the codes
from flask_appbuilder.models.sqla.filters import get_field_setup_query
class FilterStartsWithFunction(BaseFilter):
name = "Filter view with a function"
arg_name = "eqf"
def apply(self, query, func):
query, field = get_field_setup_query(query, self.model, self.column_name)
return query.filter(field.ilike(func() + "%"))
def get_user():
if 'Admin' in [r.name for r in g.user.roles]:
return ''
else:
return g.user.username
...
...
base_filters = [['created_by',FilterStartsWithFunction,get_user]]

Should objects know of the objects they're used in?

class Item:
def __init__(self, box, description):
self._box = box
self._description = description
class Box:
def __init__(self):
self.item_1 = Item(self, 'A picture')
self.item_2 = Item(self, 'A pencil')
#etc
old_stuff = Box()
print(old_stuff.item_1.box.item_1.box.item_2.box.item_1)
Above is shown an example piece of code which demonstrates my problem better than I ever could with plain text. Is there a better way to find in what box something is? (In what box is the picture?) Since I am not particularly fond of the above solution because it allows for this weird up and down calling which could go on forever. Is there a better way to solve this problem or is this just a case of: If it's stupid and it works, it ain't stupid.
Note: this trick isn't python specific. It's doable in all object-oriented programming laguages.
There is no right or wrong way to do this. The solution depends on how you want to use the object.
If your use-case requires that an item know in which box it is stored, then you need a reference to the box; if not, then you don't need the association.
Similarly, if you need to which items are in a given box, then you need references to the items in the box object.
The immediate requirement (that is, the current context) always dictates how one designs a class model; for example, one models an item or a box differently in a UI layer from how one would model it in a service layer.
You must introduce new class - ItemManager or simply dict or other external structure to store information about which box contain your item:
class Item:
def __init__(self, description):
self.description = description
class Box:
def __init__(self, item_1, item_2):
self.item_1 = item_1
self.item_2 = item_2
class ItemManager:
def __init__(self):
self.item_boxes = {}
def register_item(self, item, box):
self.item_boxes[item] = box
def deregister_item(self, item):
del self.item_boxes[item]
def get_box(self, item):
return self.item_boxes.get(item, None)
item_manager = ItemManager()
item_1 = Item("A picture")
item_2 = Item("A pencil")
item_3 = Item("A teapot")
old_stuff = Box(item_1, item_2)
item_manager.register_item(item_1, old_stuff)
item_manager.register_item(item_2, old_stuff)
new_stuff = Box(item_3, None)
item_manager.register_item(item_3, new_stuff)
box_with_picture = item_manager.get_box(item_2)
print box_with_picture.item_1.description
Also see SRP: an item should not know which box contains it.

POST a list to the API, update or create depending on the existence of that instance

I have a view which allows me to post multiple entries to a model. Currently if I add all new entries, they are added successfully. But if I add any entry for which the pk already exists, it naturally throws a serializer error and nothing gets updated.
I wish to write a method which will let me post multiple entries, but automatically either update an existing one OR add a new one successfully depending the existence of that instance.
The idea of a customized ListSerialzer is the closest thing I came across to achieve this but still not clear if I can do this.
Has anyone ever implemented anything like this ?
In views.py:
def post(self,request,format=None):
data = JSONParser().parse(request)
serializer = PanelSerializer(data=data,many=True)
if serializer.is_valid():
serializer.save()
return JsonResponse({"success":"true","content":serializer.data}, status=201)
return JsonResponse({'success': "false",'errorCode':"1",'errorMessage':serializer.errors}, status=400)
in serializers.py:
class PanelSerializer(serializers.ModelSerializer):
class Meta:
model = Panel
fields = ('pId','pGuid','pName', 'pVoltage', 'pAmperage','pPermission', 'pPresent', 'pSelf','pInfo')
def create(self, validated_data):
logger.info('Information incoming_1!')
print ("Atom")
return Panel.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.pId = validated_data.get('pId', instance.pId)
instance.pGuid = validated_data.get('pId', instance.pGuid)
instance.pName = validated_data.get('pName', instance.pName)
instance.pVoltage = validated_data.get('pVoltage', instance.pVoltage)
instance.pAmperage = validated_data.get('pAmperage', instance.pAmperage)
instance.pPermission = validated_data.get('pPermission', instance.pPermission)
instance.pPresent = validated_data.get('pPresent', instance.pPresent)
instance.pSelf = validated_data.get('pSelf', instance.pSelf)
instance.pInfo = validated_data.get('pInfo', instance.pInfo)
instance.save()
return instance
This is how the code stands as of now. I believe I will mainly need to either work on the update method of my serializer or first change it to a ListSerializer and write some custom logic again in the update method.

Ruby on Rails 5 Action Cable: stream for current model instance (URL-based subscriptions)

I have searched the web far and wide (including reading many code examples for ActionCable) for what seems to be an answer to a very basic question. Alas, I have not solved my problem.
Suppose, I have a model Search and I have navigated to the URL /searches/1.
I would also have the search.coffee file under javascripts/channels/ which starts with:
App.instance = App.cable.subscriptions.create 'SearchChannel'
and a SearchChannel class that looks like this:
class SearchChannel < ApplicationCable::Channel
def subscribed
search = Search.find(params[:id])
stream_for search
end
def unsubscribed
end
end
Naturally, the code above produces an error because params[id] is nil.
Here are my questions:
How do I subscribe to the correct Search instance based on the URL?
How do I avoid trying to subscribe to SearchChannel if I am on another URL that doesn't require a subscription, e.g. /searches/1/results?
Thank you for help!
If you look at implementation of ActionCable.Subscriptions#create, it can be called in two ways.
Option 1 (with channelName):
App.cable.subscriptions.create 'SearchChannel'
Option 2 (with channel Object):
App.cable.subscriptions.create channel: 'SearchChannel', id: 'Best Room'
If you go with Option 1, internally, it gets converted into channel Object.
So, if you want to capture id on the server side, you need to use the 2nd option and pass the id of the search, then you should be able to capture it on the server side as you described:
class SearchChannel < ApplicationCable::Channel
def subscribed
search = Search.find(params[:id])
stream_for search
end
def unsubscribed
end
end
Refer to Client-Server Interactions and Subsciptions for more info.
Hello if you need to access the variables from the URL for server side handling of the connections, here is what you can do. In your search.coffee.erb file.
string = window.location.href
cut_string = "/"
if string.indexOf(cut_string) != -1
id = string.split(cut_string)[4]
else
id = // something else
App.instance = App.cable.subscriptions.create { channel: "SearchChannel" , id: id } , ...
In your channel file, you can succesfully use it.
class SearchChannel < ApplicationCable::Channel
def subscribed
search = Search.find(params[:id])
stream_for search
end
def unsubscribed
end
end
How do I subscribe to the correct Search instance based on the URL?
You can change the coffeescript code and make it so that it only creates an actioncable connection if the desired page is browsed.
string = window.location.href
cut_string = "/"
if string.indexOf(cut_string) != -1
id = string.split(cut_string)[4] IF BLOCK CREATE CONNECTION
App.instance = App.cable.subscriptions.create { channel: "SearchChannel" , id: id } , ...
How do I avoid trying to subscribe to SearchChannel if I am on another
URL that doesn't require a subscription, e.g. /searches/1/results
Add multiple conditions to the if block in the coffescript code.

django Autocomplete-light how to choose a specific method from a mode

I am new at django and autocomplete-light. I try to get a different fields of the model from autocomplete-light, but it always return the same field. And the reason is because def in the Model defined one field. So I created another def, but can not make autocomplete-light to call that specific def. Here is my code.
models.py:
class Item(models.Model):
...
serial_number=models.CharField(max_length=100, unique=True)
barcode=models.CharField(max_length=25, unique=True)
def __unicode__(self):
return self.serial_number
def bar(self):
return self.barcode
.......
autocomplete_light_registry.py
autocomplete_light.register(Item,
name='AutocompleteItemserial',
search_fields=['serial_number'],
)
autocomplete_light.register(Item,
name='AutocompleteItembarcode',
search_fields=['barcode'],
)
Here is the issue: when I try to get the barcodes from the autocomplete-light, it returns serial_numbers. No matter what I try to get from the Item model, it always returns the serial number. I really appreciate for the answers. Thank you.
Just in case, here is the form.py
forms.py
class ItemForm(forms.ModelForm):
widgets = {
'serial_number': autocomplete_light.TextWidget('AutocompleteItemserial'),
'barcode': autocomplete_light.TextWidget('AutocompleteItembarcode'),
}
Although this is an old post but as I just faced the same issue therefore I am sharing my solution.
The reason autocomplete is returning serial_number is because django-autocomplete-light uses the __unicode__ method of the model to show the results. In your AutocompleteItembarcode all that is being done is autocomplete-light is searching by barcode field of Item.
Try the following.
In app/autocomplete_light_registry.py
from django.utils.encoding import force_text
class ItemAutocomplete(autocomplete_light.AutocompleteModelBase):
search_fields = ['serial_number']
model = Item
choices = Item.objects.all()
def choice_label(self, choice):
"""
Return the human-readable representation of a choice.
"""
barcode = Item.objects.get(pk=self.choice_value(choice)).barcode
return force_text(barcode)
autocomplete_light.register(ItemAutocomplete)
For more help you can have a look at the source code.