How to filter a query by a list of ids in GraphQL using graphene-django? - django-filter

I'm trying to perform a GraphQL query using Django and Graphene. To query one single object using the id I did the following:
{
samples(id:"U2FtcGxlU2V0VHlwZToxMjYw") {
edges {
nodes {
name
}
}
}
}
And it just works fine. Problem arise when I try to query with more than one id, like the following:
{
samples(id_In:"U2FtcGxlU2V0VHlwZToxMjYw, U2FtcGxlU2V0VHlwZToxMjYx") {
edges {
nodes {
name
}
}
}
}
In the latter case I got the following error:
argument should be a bytes-like object or ASCII string, not 'list'
And this is a sketch of how defined the Type and Query in django-graphene
class SampleType(DjangoObjectType):
class Meta:
model = Sample
filter_fields = {
'id': ['exact', 'in'],
}
interfaces = (graphene.relay.Node,)
class Query(object):
samples = DjangoFilterConnectionField(SampleType)
def resolve_sample_sets(self, info, **kwargs):
return Sample.objects.all()

GlobalIDMultipleChoiceFilter from django-graphene kinda solves this issue, if you put "in" in the field name. You can create filters like
from django_filters import FilterSet
from graphene_django.filter import GlobalIDMultipleChoiceFilter
class BookFilter(FilterSet):
author = GlobalIDMultipleChoiceFilter()
and use it by
{
books(author: ["<GlobalID1>", "<GlobalID2>"]) {
edges {
nodes {
name
}
}
}
}
Still not perfect, but the need for custom code is minimized.

You can easily use a Filter just put this with your nodes.
class ReportFileFilter(FilterSet):
id = GlobalIDMultipleChoiceFilter()
Then in your query just use -
class Query(graphene.ObjectType):
all_report_files = DjangoFilterConnectionField(ReportFileNode, filterset_class=ReportFileFilter)
This is for relay implementation of graphql django.

None of the existing answers seemed to work for me as they were presented, however with some slight changes I managed to resolve my problem as follows:
You can create a custom FilterSet class for your object type, and filter the field by using the GlobalIDMultipleChoiceFilter. for example:
from django_filters import FilterSet
from graphene_django.filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter
class SampleFilter(FilterSet):
id = GlobalIDFilter()
id__in = GlobalIDMultipleChoiceFilter(field_name="id")
class Meta:
model = Sample
fields = (
"id_in",
"id",
)
Something I came cross is that you can not have filter_fields defined with this approach. Instead, you have to only rely on the custom FilterSet class exclusively, making your object type effectively look like this:
from graphene import relay
from graphene_django import DjangoObjectType
class SampleType(DjangoObjectType):
class Meta:
model = Sample
filterset_class = SampleFilter
interfaces = (relay.Node,)

I had trouble implementing the 'in' filter as well--it appears to be misimplemented in graphene-django right now and does not work as expected. Here are the steps to make it work:
Remove the 'in' filter from your filter_fields
Add an input value to your DjangoFilterConnectionField entitled 'id__in' and make it a list of IDs
Rename your resolver to match the 'samples' field.
Handle filtering by 'id__in' in your resolver for the field. For you this will look as follows:
from base64 import b64decode
def get_pk_from_node_id(node_id: str):
"""Gets pk from node_id"""
model_with_pk = b64decode(node_id).decode('utf-8')
model_name, pk = model_with_pk.split(":")
return pk
class SampleType(DjangoObjectType):
class Meta:
model = Sample
filter_fields = {
'id': ['exact'],
}
interfaces = (graphene.relay.Node,)
class Query(object):
samples = DjangoFilterConnectionField(SampleType, id__in=graphene.List(graphene.ID))
def resolve_samples(self, info, **kwargs):
# filter_field for 'in' seems to not work, this hack works
id__in = kwargs.get('id__in')
if id__in:
node_ids = kwargs.pop('id__in')
pk_list = [get_pk_from_node_id(node_id) for node_id in node_ids]
return Sample._default_manager.filter(id__in=pk_list)
return Sample._default_manager.all()
This will allow you to call the filter with the following api. Note the use of an actual array in the signature (I think this is a better API than sending a comma separated string of values). This solution still allows you to add other filters to the request and they will chain together correctly.
{
samples(id_In: ["U2FtcGxlU2V0VHlwZToxMjYw", "U2FtcGxlU2V0VHlwZToxMjYx"]) {
edges {
nodes {
name
}
}
}
}

Another way is to tell the Relay filter of graphene_django to also deals with a list. This filter is register in a mixin in graphene_django and applied to any filter you define.
So here my solution:
from graphene_django.filter.filterset import (
GlobalIDFilter,
GrapheneFilterSetMixin,
)
from graphql_relay import from_global_id
class CustomGlobalIDFilter(GlobalIDFilter):
"""Allow __in lookup for IDs"""
def filter(self, qs, value):
if isinstance(value, list):
value_lst = [from_global_id(v)[1] for v in value]
return super(GlobalIDFilter, self).filter(qs, value_lst)
else:
return super().filter(qs, value)
# Fix the mixin defaults
GrapheneFilterSetMixin.FILTER_DEFAULTS.update({
AutoField: {"filter_class": CustomGlobalIDFilter},
OneToOneField: {"filter_class": CustomGlobalIDFilter},
ForeignKey: {"filter_class": CustomGlobalIDFilter},
})

Related

SQLALchemy update ARRAY column [duplicate]

I'm working on a project using Flask and a PostgreSQL database, with SQLAlchemy.
I have Group objects which have a list of User IDs who are members of the group. For some reason, when I try to add an ID to a group, it will not save properly.
If I try members.append(user_id), it doesn't seem to work at all. However, if I try members += [user_id], the id will show up in the view listing all the groups, but if I restart the server, the added value(s) is (are) not there. The initial values, however, are.
Related code:
Adding group to the database initially:
db = SQLAlchemy(app)
# ...
g = Group(request.form['name'], user_id)
db.session.add(g)
db.session.commit()
The Group class:
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.postgresql import ARRAY
class Group(db.Model):
__tablename__ = "groups"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
leader = db.Column(db.Integer)
# list of the members in the group based on user id
members = db.Column(ARRAY(db.Integer))
def __init__(self, name, leader):
self.name = name
self.leader = leader
self.members = [leader]
def __repr__(self):
return "Name: {}, Leader: {}, Members: {}".format(self.name, self.leader, self.members)
def add_user(self, user_id):
self.members += [user_id]
My test function for updating the Group:
def add_2_to_group():
g = Group.query.all()[0]
g.add_user(2)
db.session.commit()
return redirect(url_for('show_groups'))
Thanks for any help!
As you have mentioned, the ARRAY datatype in sqlalchemy is immutable. This means it isn’t possible to add new data into array once it has been initialised.
To solve this, create class MutableList.
from sqlalchemy.ext.mutable import Mutable
class MutableList(Mutable, list):
def append(self, value):
list.append(self, value)
self.changed()
#classmethod
def coerce(cls, key, value):
if not isinstance(value, MutableList):
if isinstance(value, list):
return MutableList(value)
return Mutable.coerce(key, value)
else:
return value
This snippet allows you to extend a list to add mutability to it. So, now you can use the class above to create a mutable array type like:
class Group(db.Model):
...
members = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
...
You can use the flag_modified function to mark the property as having changed. In this example, you could change your add_user method to:
from sqlalchemy.orm.attributes import flag_modified
# ~~~
def add_user(self, user_id):
self.members += [user_id]
flag_modified(self, 'members')
To anyone in the future: so it turns out that arrays through SQLAlchemy are immutable. So, once they're initialized in the database, they can't change size. There's probably a way to do this, but there are better ways to do what we're trying to do.
This is a hacky solution, but what you can do is:
Store the existing array temporarily
Set the column value to None
Set the column value to the existing temporary array
For example:
g = Group.query.all()[0]
temp_array = g.members
g.members = None
db.session.commit()
db.session.refresh(g)
g.members = temp_array
db.session.commit()
In my case it was solved by using the new reference for storing a object variable and assiging that new created variable in object variable.so, Instead of updating the existing objects variable it will create a new reference address which reflect the changes.
Here in Model,
Table: question
optional_id = sa.Column(sa.ARRAY(sa.Integer), nullable=True)
In views,
option_list=list(question.optional_id if question.optional_id else [])
if option_list:
question.optional_id.clear()
option_list.append(obj.id)
question.optional_id=option_list
else:
question.optional_id=[obj.id]

Is there a way to set the id of an existing instance as the value of a nested serializer in DRF?

I'm developing a chat application. I have a serializer like this:
class PersonalChatRoomSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalChatRoom
fields = '__all__'
user_1 = UserSerializer(read_only=True)
user_2 = UserSerializer()
the user_1 field is auto-populated but the client should provide the user_2 field in order to create a personal chat room with another user.
My problem is, when creating a new chat room, the serializer tries to create a new user object from the input data thus giving me validation errors. What I really want it to do is to accept a user id and set the value of user_2 field to an existing user instance that is currently available in the database and if the user is not found, simply return a validation error. (the exact behavior of PrimaryKeyRelatedField when creating a new object)
I want my input data to look like this:
{
'user_2': 1 // id of the user
}
And when I retrieve my PersonalChatRoom object, I want the serialized form of the user object for my user_2 field:
{
...,
'user_2': {
'username': ...,
'the_rest_of_the_fields': ...
}
}
How can I achieve this?
views.py
class GroupChatRoomViewSet(viewsets.ModelViewSet):
permission_classes = [IsUserVerified, IsGroupOrIsAdminOrReadOnly]
serializer_class = GroupChatRoomSerializer
def get_queryset(self):
return self.request.user.group_chat_rooms.all()
def perform_create(self, serializer):
return serializer.save(owner=self.request.user)
I finally figured out how to do it. I just needed to override the to_representation method and serialize the object there. Here is the code I ended up with:
class PersonalChatRoomSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalChatRoom
fields = '__all__'
read_only_fields = ['user_1']
def to_representation(self, chat_room):
""" Serialize user instances when outputing the results """
obj = super().to_representation(chat_room)
for field in obj.keys():
if field.startswith('user_'):
obj[field] = UserSerializer(User.objects.get(pk=obj[field])).data
return obj

Adding Additional Data to a Serialize Response in Django

Updated
I changed my simplified question into a real example.
I've created a working post response of data from the model using ModelSerialzer, which I call from a post method in a view class. I would like to add additional data to the response. This is the pertinent code from my CBV:
def post(self, request, format=None):
user_profile = UserProfiles.objects.get(user=request.user.id)
service_id = user_profile.service_id
rec_filter = Recommendations.objects.values_list('resource')
if service_id > 0:
service_name = Services.objects.get(pk=service_id)
programs = Programs.objects.filter(services=service_id)
resources_filtered = Resources.objects.filter(program__in=programs).exclude(id__in=rec_filter)
else:
service_name = 'All Services'
resources_filtered = Resources.objects.exclude(id__in=rec_filter)
serializer = ResourceSerializer(resources_filtered, many=True)
#serializer.data["service"] = service_name
return Response(serializer.data)
The commented out line was my attempt to add data base on a similar post here. I get a 500 response in my API call. What is the correct way to do it? The response data is JSON if that's necessary to mention.
This is the ModelSerializer:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail'
]
Test of the answer
Heres the updated code based on the answer with a correction to fix and error:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
service = serializers.SerializerMethodField()
def get_service(self, obj):
return "All Services"
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail',
'service'
]
The problem with this approach is that the value "All Services" is repeated in every row serialized. It's only needed once. I'd also like to keep the data transmitted minimized.
The problem with the original attempt is that serializer.data is immutable. It's necessary to make a copy and add to it.
serializer = ResourceSerializer(resources_filtered, many=True)
augmented_serializer_data = list(serializer.data)
augmented_serializer_data.append({'service': 'All Services'})
return Response(augmented_serializer_data)
This answer is based on one given by #andre-machado in this question.
This code here is an example to coincide with the other answer given.
You can do it in serializer itself. Define the new field required and add it in fields. Mark all the fields in serializer from resource model.
class ResourceSerializer(serializers.ModelSerializer):
service = serializers.SerializerMethodField()
def get_service(self):
return "All Services"
class Meta :
model = Resources
fields = ('service') #Mark all the fields required here from resource model
You can do it from the serilaizer. In this case i was adding the field isOpen to the response and this is how i did it .timeDifference is the name of the function that was to generate data for the extra field . I hope it helps
class ShopSearchSerializer(serializers.ModelSerializer):
isOpen = serializers.SerializerMethodField('timeDifference')
def timeDifference(self,*args):
requestTime = datetime.now()
return requestTime
class Meta:
model = Shop
fields =['name','city','street','house','opening_time','closing_time','isOpen']

Passing variable to a django queryset

So I have this urlset below. And in the urlset there are two urls that take paramters into the url and passes it into the apiviw.
urlpatterns = [
url(r'^([0-9]{4})/([0-9]{1,2})/test/$', views.DuperView.as_view()),
url(r'^test/([0-9]{4})/([0-9]{2})/$', views.SuperView.as_view()),
url(r'^test/', views.TestView.as_view()),
]
Here is the APIView that everything is passed into.
class DuperView(APIView):
queryset = models.DuperModel.dupers.duperdeduper()
def get(self, request, year, month, format=None):
an_apiview = [
year,
month,
]
return Response({'http_method': 'GET', 'api_view': an_apiview})
the first parameter that is defined in the url as ([0-9]{4}) is passed into the get method as year and the second defined as ([0-9]{1,2}) is passed as month.
Here is my model, model manager and model queryset for the model this all corresponds to.
class DuperQuerySet(models.QuerySet):
def duperdeduper(self):
return self.filter(year='2000')
class DuperModelManager(models.Manager):
def get_queryset(self):
return DuperQuerySet(self.model, using=self._db)
def duperdeduper(self):
return self.get_queryset().authors()
class DuperModel(models.Model):
year = models.ForeignKey(YearModel)
month = models.ForeignKey(MonthModel)
name = models.CharField(max_length=255)
dupers = DuperModelManager()
I was wondering if there is a way to pass the year and month parameters to the model somehow, that way I can use it when I process the data in the models?
SELECT * FROM dupertable, yeartable WHERE year='variablevaluepassedon'
Is this kind of functionality possible? I know I can use the .filter() method on my queryset, but my problem is getting the variable to the queryset to make this query work.
As Daniel mentionnned, you should just override the get_queryset method from your view. From there you can pass your parameters you got from the url.
class DuperView(GenericApiView, ListModelMixin):
def get_queryset(self):
year = self.kwargs.get('year')
month = self.kwargs.get('month')
return DuperModel.dupers.filter(month=month, year=year)
I have used the GenericApiView to add the get_queryset method. You can then add the mixins you need for your API. In this case, I added the ListModelMixin, that implement a get method to retrieve a list of elements.
Also you should reference your parameter in your urls :
urlpatterns = [
url(r'^(?P<year>[0-9]{4})/(?P<month>[0-9][0-9]{1,2})/test/$',
]

Play Scala Anorm One to Many relationship

Coming from play anorm, create a model from json without passing anorm PK value in the json I'm trying to add Seq[Address] to case class like
case class User(
id: Pk[Long] = NotAssigned,
name: String = "",
email: String = "",
addresses: Seq[Address])
Address is a simple object/class with three Strings. An user can have more than 1 address, how do I get all the addresses along with the User in findAll.
def findAll(): Seq[User] = {
Logger.info("select * from pt_users")
DB.withConnection { implicit connection =>
SQL(
"""
select * from pt_users where name like {query} limit {size} offset {offset}
""").as(User.list *)
}
}
A side note about something I have found useful: if you're not sure you will always want to fetch the addresses for a user, you can avoid adding that relation as a field and instead use tuples or other data structures for representing them. This would allow you to have a method like this:
def allUsersWithAddresses(): Map[User, Seq[Address])] = ...
But still have methods that return only users without having the joined data.
To read a join (or subselect) you will have to parse the combined output with a parser, something like this:
.as(User ~ Address *).groupBy(_._1)
If you really want to put the addresses inside of user, you'd have to make the address list empty from the user parser and then map each distinct user into one with the addresses:
.as(User ~ Address *).groupBy(_._1).map {
case (user, addresses) => user.copy(addresses = addresses)
}
Note, the examples are just pointers to an approximate solution, not copy-paste-and-compile ready code.
Hope it helped!
This will work
/** Parses a `Blog` paired with an optional `Post` that can be later be collapsed into one object. */
val parser: RowParser[(Blog, Option[Post])] = {
simple ~ Post.parser.? map {
case blog~post => (blog, post)
}
}
def list: List[Blog] = {
DB.withConnection { implicit c =>
SQL("""
SELECT * FROM blogs b
LEFT OUTER JOIN posts p ON(p.blog_id = b.id)
""").as(parser *)
.groupBy(_._1)
.mapValues(_.map(_._2).flatten)
.toList
.map{ case (blog, posts) => blog.copy(posts = posts) }
}
}
Copied from https://index.scala-lang.org/mhzajac/anorm-relational/anorm-relational/0.3.0?target=_2.12