Django/Wagtail Rest Framework API Ordering/Pagination - django

I am using wagtail. I have serialized my API. I want to order them by -first_published_at, when someone hit my API url api/v2/pages they will see an ordered API without filtering it via URL. here is my api.py code:
class ProdPagesAPIViewSet(BaseAPIViewSet):
renderer_classes = [JSONRenderer]
filter_backends = [FieldsFilter,
ChildOfFilter,
AncestorOfFilter,
DescendantOfFilter,
OrderingFilter,
TranslationOfFilter,
LocaleFilter,
SearchFilter,]
meta_fields = ["type","seo_title","search_description","first_published_at"]
body_fields = ["id","type","seo_title","search_description","first_published_at","title"]
listing_default_fields = ["type","seo_title","search_description","first_published_at","id","title","alternative_title","news_slug","blog_image","video_thumbnail","categories","blog_authors","excerpt","content","content2","tags",]
nested_default_fields = []
ordered_queryset= []
name = "pages"
model = AddStory
api_router.register_endpoint("pages", ProdPagesAPIViewSet)
I have tried ordered_queryset= [AddStory.objects.order_by('-first_published_at')]
But it's not ordered by the newest published stories. How should I do the query?
Here is my API response
{
"meta": {
"total_count": 6
},
"items": [
{
"id": 4,
"meta": {
"type": "blog.AddStory",
"seo_title": "",
"search_description": "",
"first_published_at": "2022-08-30T11:05:11.341355Z"
},
{
"id": 6,
"meta": {
"type": "blog.AddStory",
"seo_title": "",
"search_description": "",
"first_published_at": "2022-08-30T11:13:47.114889Z"
},
{
"id": 7,
"meta": {
"type": "blog.AddStory",
"seo_title": "",
"search_description": "",
"first_published_at": "2022-08-31T11:13:47.114889Z"
},

Solved after using get_queryset instead of order_queryset
#api.py
class ProdPagesAPIViewSet(BaseAPIViewSet):
renderer_classes = [JSONRenderer]
filter_backends = [FieldsFilter,
ChildOfFilter,
AncestorOfFilter,
DescendantOfFilter,
OrderingFilter,
TranslationOfFilter,
LocaleFilter,
SearchFilter,]
meta_fields = ["type","seo_title","search_description","first_published_at"]
body_fields = ["id","type","seo_title","search_description","first_published_at","title"]
listing_default_fields = ["type","seo_title","search_description","first_published_at","id","title","alternative_title","news_slug","blog_image","video_thumbnail","categories","blog_authors","excerpt","content","content2","tags",]
nested_default_fields = []
def get_queryset(self):
return self.model.objects.all().order_by("-first_published_at")
name = "pages"
model = AddStory
api_router.register_endpoint("pages", ProdPagesAPIViewSet)

Related

Django rest framework - serializer for dictionary structured payload

I'm trying to create a Serializer for a payload that looks something like this -
{
"2fd08845-9b21-4972-87ed-2e7fd03448c5": {
"operation": "Create",
"operationId": "356f6501-a117-4c8d-98ce-dcb4344d481b",
"user": "superuser",
"immediate": "true"
},
"fe6d0c85-0021-431e-9955-e8e1b1ebc414": {
"operation": "Create",
"operationId": "adcedb2f-c751-441f-8108-2c29667ea9cf",
"user": "employee",
"immediate": "false"
}
}
I thought of using DictField, but my problem is that there isn't a field name. it's only a dictionary of keys and values.
I tried something like:
class UserOperationSerializer(serializers.Serializer):
operation = serializers.ChoiceField(choices=["Create", "Delete"])
operationId = serializers.UUIDField()
user = serializers.CharField()
immediate = serializers.BooleanField()
class UserOperationsSerializer(serializers.Serializer):
test = serializers.DictField(child=RelationshipAuthorizeObjectSerializer())
But again, there isn't a 'test' field.
I think your easiest path forward would be to flatten the payload to the following format:
[
{
"request_id": "2fd08845-9b21-4972-87ed-2e7fd03448c5",
"operation": "Create",
"operationId": "356f6501-a117-4c8d-98ce-dcb4344d481b",
"user": "superuser",
"immediate": "true"
},
{
"request_id": "fe6d0c85-0021-431e-9955-e8e1b1ebc414",
"operation": "Create",
"operationId": "adcedb2f-c751-441f-8108-2c29667ea9cf",
"user": "employee",
"immediate": "false"
}
]
And then serialize it. Otherwise, you'd be creating custom fields/serializers which is not pretty.
The way I finally solved it was to add a dynamic 'body' field that contains the real payload of the request.
class UserOperationSerializer(serializers.Serializer):
operation = serializers.ChoiceField(choices=["Create", "Delete"])
operationId = serializers.UUIDField()
user = serializers.CharField()
immediate = serializers.BooleanField()
class UserOperationsSerializer(serializers.Serializer):
body = serializers.DictField(child=UserOperationSerializer())
def __init__(self, *args, **kwargs):
kwargs['data'] = {'body': kwargs['data']}
super().__init__(*args, **kwargs)
Then, in the View, I will use that data as serializer.validated_data['body']
That did the work for me.

list indices must be integers or slices when appending a dict to a serializer in django

I have an already created serializer object, I am trying to add a new object to the serializer but I keep getting the error
list indices must be integers or slices, not str
I am not able to trace where I am going wrong with the creation of the new object. Here is my code below and more explanations.
class ClusterFunctionView(generics.ListAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = FunctionListSerializer
def get_queryset(self):
//returns serializer
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
user = self.request.user
cluster = Cluster.objects.filter(user_id=user.id, id=self.kwargs["cluster_id"]).first()
schedule = Schedule.objects.filter(clusters__in=[cluster]).values().first() # I am getting the new object here
print('schedule', type(schedule)) # I checked the type, it is a dict
response.data['schedule'] = schedule # doesn't seem to be appending to the existing serializer.
return response
The following is an example of the output of the schedule object, I printed using print('schedule', schedule):
schedule {'id': 7, 'user_id': 3, 'creation_time': datetime.datetime(2020, 5, 25, 15, 44, 39, 875485), 'name': 'mandard_1', 'is_active': True, 'comment': 'extract mardard premier batch', 'cron_expression': '#once'}
A sample of the existing serializer on which I should add the above object is:
[
{
"id": 1,
"function": "connections",
"max_concurrency": 1,
"mandatory_params": {},
"public_params": {
"cluster": {
"account": true,
"max_pages": {
"max": 100,
"default": 100
},
"profiles_per_page": {
"max": 25,
"default": 25
}
}
},
"params": {
"max_pages": 100,
"account_function": "user_account",
"alchemy_directory": "connections",
"unique_result_obj_attribute": "connection_id"
}
}
]
I am expecting a serializer with a schedules object, a result like :
[
{
"id": 1,
"function": "connections",
"max_concurrency": 1,
"mandatory_params": {},
"public_params": {
"cluster": {
"account": true,
"max_pages": {
"max": 100,
"default": 100
},
"profiles_per_page": {
"max": 25,
"default": 25
}
}
},
"params": {
"max_pages": 100,
"account_function": "user_account",
"alchemy_directory": "connections",
"unique_result_obj_attribute": "connection_id"
}
},
"schedule": {} # this should be added as a result
]
What could the problem be, and what solution could I undertake? Thanks
I was able to solve the question, realizing it was a small mistake that I did.
In the def list function,
This line was the one causing the error :
response.data['schedule'] = schedule
I realised that the serializer produced was of a list result, and I was initially trying to append the schedule : {}, directly into a list, hence the error. It should be appended into the first object. Hence I needed to access the object in the list using index, so changing it to below solved the problem:
response.data[0]['schedule'] = schedule

Flatten json return by DRF

I have json API returned as below format.
But I want to return json API decomposing namingzone key as specified below.
Could anyone tell me how I can revise serializer to achieve this?
serializer.py is also specified below.
For models.py and views.py, please refer to my previous post.
current
{
"zone": {
"zone": "office_enclosed",
"namingzone": [
{
"naming": "moffice"
}
]
},
"lpd": 11.9,
"sensor": true
},
{
"zone": {
"zone": "office_open",
"namingzone": [
{
"naming": "off"
},
{
"naming": "office"
}
]
},
"lpd": 10.5,
"sensor": true
}
Target
{
"zone": "office_enclosed",
"naming": "moffice",
"lpd": 11.9,
"sensor": true
},
{
"zone": "office_open",
"naming": "off",
"lpd": 10.5,
"sensor": true
},
{
"zone": "office_open",
"naming": "office",
"lpd": 10.5,
"sensor": true
}
serializer.py
class namingNewSerializer(serializers.ModelSerializer):
class Meta:
model=Naming
fields=('naming',)
class zoneSerializer(serializers.ModelSerializer):
namingzone=namingNewSerializer(many=True)
class Meta:
model=Zone
fields = ('zone','namingzone')
class lightSerializer(serializers.ModelSerializer):
zone = zoneSerializer()
class Meta:
model=Light
fields = ('zone','lpd','sensor')
class namingSerializer(serializers.ModelSerializer):
zone=zoneSerializer()
class Meta:
model=Naming
fields=('zone','naming')
I would say using Serializer might complicate the implementations. Rather, you can take an pythonic approach. Try like this:
class SomeView(APIView):
...
def get(self, request, *args, **kwargs):
data = lightSerializer(Light.objects.all(), many=True).data
data = list(data) # convert lazy object to list
updated_data = list()
for item in data:
newdict = dict()
zone = item['zone']
newdict.update({'zone':zone['zone'], 'lpd': item['lpd'], 'sensor':item['sensor']})
for naming_zone in zone.get('namingzone'):
naming_zone.update(newDict)
updated_data.append(naming_zone)
return Response(updated_data, status=status.HTTP_200_OK)
See DRF Field document about source. It will help you.
https://www.django-rest-framework.org/api-guide/fields/#source

django rest framework - get two models

I have multiple models that are associated by Foreign Keys. I can export them all separately using django rest framework on a one to one basis, and I can also export multiple ones nested. However I want to be able to essentially "concatenate" them together into a single json/xml export.
The models in the example below are joined by a one to one foreign key on jobdtl_id. I have some where it's one to many but I'm hoping I can figure that out when I know how to get a view that will link 2 separate models like I want below -
Here's an example of what I want the json to look like by hitting a single URL like
http://localhost/job/4/
{
"job": {
"-id": "9878",
"-name": "This is the job",
"-master": "blahserver",
"-dbversion": "234",
"-xmlversion": "1",
"jobmst": {
"jobmst_id": "9878",
"jobmst_type": "2",
"jobmst_prntid": "234",
"jobmst_active": "Y",
"jobmst_name": "This is the job",
"jobmst_owner": "Owner",
"jobdtl_id": "9878",
"jobmst_lstchgtm": {
"-date": "Y",
"#text": "2013-10-23 09:22:08.0"
},
"jobmst_prntname": "Parent",
"jobmst_alias": "9878"
},
"jobdtl": {
"jobdtl_id": "9878",
"jobdtl_cmd": "blah.exe",
"jobdtl_failalarm": "NULL",
"nodmst_id": "NULL",
"nodlstmst_id": "NULL",
"jobdtl_inhevent": "Y",
"jobdtl_inhoptions": "Y",
"jobdtl_inhagent": "Y",
"jobdtl_inhrepeat": "Y",
"jobdtl_inhtime": "Y",
"jobdtl_timewin": "NULL",
"jobdtl_saveoutput": "Y",
"jobdtl_outputname": "NULL",
"jobdtl_trackmethod": "1",
"jobdtl_trackcmd": "NULL",
"jobdtl_deplogic": "1",
"jobdtl_rerun": "NULL",
"jobdtl_params": "--blah --ok"
},
"jobdep": [
{
"jobdep_id": "79670",
"jobmst_id": "9878",
"jobdep_type": "1",
"jobdep_jobmst": "another job",
"varmst_id": "NULL"
},
{
"-num": "2",
"jobdep_id": "83783",
"jobmst_id": "9878",
"jobdep_type": "1",
"jobdep_jobmst": "and another",
"varmst_id": "NULL"
}
],
"trgjob": [
{
"trgjob_id": "22286",
"trgmst_id": "23455",
"jobmst_id": "9878"
},
{
"-num": "2",
"trgjob_id": "28980",
"trgmst_id": "23521",
"jobmst_id": "9878"
},
{
"-num": "3",
"trgjob_id": "28981",
"trgmst_id": "9237",
"jobmst_id": "9878"
}
]
}
}
The models are basically like this -
class Jobdtl(models.Model):
jobdtl_id = models.IntegerField(primary_key=True)
jobdtl_cmd = models.TextField(blank=True)
....
jobdtl_duration = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'jobdtl'
class Jobmst(models.Model):
jobmst_id = models.IntegerField(primary_key=True)
jobmst_type = models.SmallIntegerField()
....
jobdtl_id = models.ForeignKey('Jobdtl', db_column='jobdtl_id', related_name='mstdtl', blank=True, null=True)
def __unicode__(self):
return self.jobmst_name
class Meta:
managed = False
db_table = 'jobmst'
end caveat I'm converting the json from how the XML looks for the existing legacy app which is like so -
<?xml version="1.0"?>
<job id="9878" name="This is the job" master="blahserver" dbversion="532" xmlversion="1">
<jobmst>
<jobmst_id>9878</jobmst_id>
<jobmst_type>2</jobmst_type>
<jobmst_prntid>234</jobmst_prntid>
<jobmst_active>Y</jobmst_active>
<jobmst_name>This is the job</jobmst_name>
<jobmst_owner>Owner</jobmst_owner>
<jobdtl_id>9878</jobdtl_id>
<jobmst_lstchgtm date="Y">2013-10-23 09:22:08.0</jobmst_lstchgtm>
<jobmst_prntname>Parent</jobmst_prntname>
<jobmst_alias>9878</jobmst_alias>
</jobmst>
<jobdtl>
<jobdtl_id>9878</jobdtl_id>
<jobdtl_cmd>blah.exe</jobdtl_cmd>
<jobdtl_failalarm>NULL</jobdtl_failalarm>
<nodmst_id>NULL</nodmst_id>
<nodlstmst_id>NULL</nodlstmst_id>
<jobdtl_inhevent>Y</jobdtl_inhevent>
<jobdtl_inhoptions>Y</jobdtl_inhoptions>
<jobdtl_inhagent>Y</jobdtl_inhagent>
<jobdtl_inhrepeat>Y</jobdtl_inhrepeat>
<jobdtl_inhtime>Y</jobdtl_inhtime>
<jobdtl_timewin>NULL</jobdtl_timewin>
<jobdtl_saveoutput>Y</jobdtl_saveoutput>
<jobdtl_outputname>NULL</jobdtl_outputname>
<jobdtl_trackmethod>1</jobdtl_trackmethod>
<jobdtl_trackcmd>NULL</jobdtl_trackcmd>
<jobdtl_deplogic>1</jobdtl_deplogic>
<jobdtl_rerun>NULL</jobdtl_rerun>
<jobdtl_params>--blah --ok</jobdtl_params>
</jobdtl>
<jobdep>
<jobdep_id>79670</jobdep_id>
<jobmst_id>9878</jobmst_id>
<jobdep_type>1</jobdep_type>
<jobdep_jobmst>another job</jobdep_jobmst>
<varmst_id>NULL</varmst_id>
</jobdep>
<jobdep num="2">
<jobdep_id>83783</jobdep_id>
<jobmst_id>9878</jobmst_id>
<jobdep_type>1</jobdep_type>
<jobdep_jobmst>and another</jobdep_jobmst>
<varmst_id>NULL</varmst_id>
</jobdep>
<trgjob>
<trgjob_id>22286</trgjob_id>
<trgmst_id>23455</trgmst_id>
<jobmst_id>9878</jobmst_id>
</trgjob>
<trgjob num="2">
<trgjob_id>28980</trgjob_id>
<trgmst_id>23521</trgmst_id>
<jobmst_id>9878</jobmst_id>
</trgjob>
<trgjob num="3">
<trgjob_id>28981</trgjob_id>
<trgmst_id>9237</trgmst_id>
<jobmst_id>9878</jobmst_id>
</trgjob>
</job>
class MSTSerializer(serializers.HyperlinkedModelSerializer):
jobdtl_id = DTLSerializer()
class Meta:
model = Jobmst
fields = ('id', 'url', 'jobdtl_id'...)
class DTLSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Jobdtl
fields = ('id', 'url', ...)
Would result in a more correct data structure of
{
"jobmst_id": 4,
"jobmst_type": 1,
"jobdtl_id": {
"jobdtl_id": 4,
"jobdtl_cmd": null,
"jobdtl_duration": 1379
},
}
I found the solution by doing the following in my views.py
...
#csrf_exempt
def tesxml_test(request, pk):
"""
Retrieve, update or delete a code snippet.
"""
try:
mst = Jobmst.objects.using('database1').get(jobmst_id=pk)
dtl = Jobdtl.objects.using('database1').get(jobdtl_id=pk)
dep = Jobdep.objects.using('database2').filter(jobmst_id=pk).order_by('jobdep_id')
trg = Trgjob.objects.using('database1').filter(jobmst_id=pk).order_by('trgjob_order')
except Jobmst.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
#Get String Results of 4 queries
jobmststring = JobmstSerializer(mst)
jobdtlstring = JobdtlSerializer(dtl)
jobdepstring = JobdepSerializer(dep)
trgjobstring = TrgjobSerializer(trg)
#Get serialized Results of 4 queries
jobmst_serialized = {'jobmst': jobmststring.data}
jobdtl_serialized = {'jobdtl': jobdtlstring.data}
jobdep_serialized = {'jobdep': jobdepstring.data}
trgjob_serialized = {'trgjob': trgjobstring.data}
jobgroup = jobmst_serialized, jobdtl_serialized, jobdep_serialized, trgjob_serialized,
jobgroupresponse = TESXMLResponse(jobgroup)
return jobgroupresponse
...
It's not perfect but it puts me on the next step of my problem which is customizing the renderer to get the data in the root fields which I have another SO question for :)

Django Haystack - How to force exact attribute match without stemming?

I'm using Django 1.5 with django-haystack 2.0 and an elasticsearch backend. I'm trying to search by an exact attribute match. However, I'm getting "similar" results even though I'm using both the __exact operator and the Exact() class. How can I prevent this behavior?
For example:
# models.py
class Person(models.Model):
name = models.TextField()
# search_indexes.py
class PersonIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
name = indexes.CharField(model_attr="name")
def get_model(self):
return Person
def index_queryset(self, using=None):
return self.get_model().objects.all()
# templates/search/indexes/people/person_text.txt
{{ object.name }}
>>> p1 = Person(name="Simon")
>>> p1.save()
>>> p2 = Person(name="Simons")
>>> p2.save()
$ ./manage.py rebuild_index
>>> person_sqs = SearchQuerySet().models(Person)
>>> person_sqs.filter(name__exact="Simons")
[<SearchResult: people.person (name=u'Simon')>
<SearchResult: people.person (name=u'Simons')>]
>>> person_sqs.filter(name=Exact("Simons", clean=True))
[<SearchResult: people.person (name=u'Simon')>
<SearchResult: people.person (name=u'Simons')>]
I only want the search result for "Simons" - the "Simon" result should not show up.
Python3, Django 1.10, Elasticsearch 2.4.4.
TL;DR: define custom tokenizer (not filter)
Verbose explanation
a) use EdgeNgramField:
# search_indexes.py
class PersonIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.EdgeNgramField(document=True, use_template=True)
...
b) template:
# templates/search/indexes/people/person_text.txt
{{ object.name }}
c) create custom search backend:
# backends.py
from django.conf import settings
from haystack.backends.elasticsearch_backend import (
ElasticsearchSearchBackend,
ElasticsearchSearchEngine,
)
class CustomElasticsearchSearchBackend(ElasticsearchSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(CustomElasticsearchSearchBackend, self).__init__(
connection_alias, **connection_options)
setattr(self, 'DEFAULT_SETTINGS', settings.ELASTICSEARCH_INDEX_SETTINGS)
class CustomElasticsearchSearchEngine(ElasticsearchSearchEngine):
backend = CustomElasticsearchSearchBackend
d) define custom tokenizer (not filter!):
# settings.py
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'apps.persons.backends.CustomElasticsearchSearchEngine',
'URL': 'http://127.0.0.1:9200/',
'INDEX_NAME': 'haystack',
},
}
ELASTICSEARCH_INDEX_SETTINGS = {
"settings": {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "custom_ngram_tokenizer",
"filter": ["asciifolding", "lowercase"]
},
"edgengram_analyzer": {
"type": "custom",
"tokenizer": "custom_edgengram_tokenizer",
"filter": ["asciifolding", "lowercase"]
}
},
"tokenizer": {
"custom_ngram_tokenizer": {
"type": "nGram",
"min_gram": 3,
"max_gram": 12,
"token_chars": ["letter", "digit"]
},
"custom_edgengram_tokenizer": {
"type": "edgeNGram",
"min_gram": 2,
"max_gram": 12,
"token_chars": ["letter", "digit"]
}
}
}
}
}
HAYSTACK_DEFAULT_OPERATOR = 'AND'
e) use AutoQuery (more versatile):
# views.py
search_value = 'Simons'
...
person_sqs = \
SearchQuerySet().models(Person).filter(
content=AutoQuery(search_value)
)
f) reindex after changes:
$ ./manage.py rebuild_index
I was facing a similar problem. if you change the settings of your haystacks elasticsearch back end like:
DEFAULT_SETTINGS = {
'settings': {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["haystack_ngram", "lowercase"]
},
"edgengram_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["haystack_edgengram", "lowercase"]
}
},
"tokenizer": {
"haystack_ngram_tokenizer": {
"type": "nGram",
"min_gram": 6,
"max_gram": 15,
},
"haystack_edgengram_tokenizer": {
"type": "edgeNGram",
"min_gram": 6,
"max_gram": 15,
"side": "front"
}
},
"filter": {
"haystack_ngram": {
"type": "nGram",
"min_gram": 6,
"max_gram": 15
},
"haystack_edgengram": {
"type": "edgeNGram",
"min_gram": 6,
"max_gram": 15
}
}
}
}
}
Then it will tokenize only when the query is more than 6 character.
If you want results like "xyzsimonsxyz", then you would need to use ngram analyzer instead of EdgeNGram or you could use both depending on your requirements. EdgeNGram generates tokens only from the beginning.
with NGram 'simons' will be one of the generated tokens for term xyzsimonsxyz assuming max_gram >=6 and you will get expected results, also search_analyzer needs to be different or you will get weird results.
Also index size might get pretty big with ngram if you have huge chunk of text
Not use CharField use EdgeNgramField.
# search_indexes.py
class PersonIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
name = indexes.EdgeNgramField(model_attr="name")
def get_model(self):
return Person
def index_queryset(self, using=None):
return self.get_model().objects.all()
And not user filter, user autocomplete
person_sqs = SearchQuerySet().models(Person)
person_sqs.autocomplete(name="Simons")
source: http://django-haystack.readthedocs.org/en/v2.0.0/autocomplete.html