Problem description
Ok, I am trying to add a function defined inside a model (called Highway) to the HttpResponse of the model (Highway) after serializing with geojson serializer without success.
I'm trying to find a solution by going through the source code as no errors are passed and the property does not appear in the HttpResponse. I might however be complicating things and hopefully can get another eyes on this. I'm open to other suggestions, maybe to update the Highway each time a Location is added/modified.
The item appears correctly when passing it to the admin site and all other fields (not shown below) work as intended.
PS. I'm quite new to the whole Django system. Thank you!
Django version: 2.1
Relevant links:
https://docs.djangoproject.com/en/2.1/ref/contrib/gis/serializers/
https://github.com/django/django/blob/master/django/contrib/gis/serializers/geojson.py
Minified code
geom.models.py
class Highway(models.Model):
name = models.CharField(max_length=50, unique=True)
length = models.IntegerField("Length in meters")
mline = models.MultiLineStringField("Geometry", srid=4326)
def cameras_per_km(self):
#THIS IS THE FUNCTION I AM TRYING TO SEND TO GEOJSON SERIALIZER
count = self.location_set.filter(location__icontains="camera").count()
return round(count/(self.length/1000),1)
cameras_per_km.short_description = "Cameras/km"
class Location(models.Model):
location = models.CharField(max_length=30, unique=True)
highway = models.ForeignKey(Highway, null=True, on_delete=models.SET_NULL)
point = models.PointField()
geom.admin.py (when passing the func to list_display the item appears correclty in admin)
class HighwayAdmin(LeafletGeoAdmin):
list_display = ('name', 'cameras_per_km')
admin.site.register(
Highway,
HighwayAdmin,
)
geom.views.py (this doesn't)
def geojson_highway_all(request):
geojsonformat = serialize('geojson',
Highway.objects.all(),
geometry_field='mline',
fields = (
'pk',
'cameras_per_km' # <-- I want this to show
)
)
return HttpResponse(geojsonformat)
geom.urls.py
from django.urls import path
from . import views
app_name = 'geom'
urlpatterns = [
path('highways.geojson', views.geojson_highway_all, name='highways.geojson'),
]
Update: I (Anton vBR) have now rewritten this answer completely but think #ruddra deserves some credit. I am however open for alternative solutions. Hopefullly this answer can helps someone out in the future.
Create a new Serializer based on geojson serializer
geom.views.py
from django.contrib.gis.serializers.geojson import Serializer
class CustomSerializer(Serializer):
def end_object(self, obj):
for field in self.selected_fields:
if field == self.geometry_field or field == 'pk':
continue
elif field in self._current.keys():
continue
else:
try:
self._current[field] = getattr(obj, field)()
except AttributeError:
pass
super(CustomSerializer, self).end_object(obj)
geojsonformat = CustomSerializer().serialize(
Highway.objects.all(),
geometry_field='mline',
fields = (
'pk',
'cameras_per_km'
)
Related
I have a database with cars, and each car has an id. To get all cars I would go at this route api/cars/, now I am trying to implement getting a single car which has id 1 and this is my urls:
urlpatterns = [
path('api/cars/', CarsAPI.as_view()),
path('api/cars/:id/', CarAPI.as_view()),
path('api/tours/ongoing/', CarListOngoingAPI.as_view())
]
And this is my views for first path and second,
class CarsAPI(generics.ListCreateAPIView):
queryset = Car.objects.all()
serializer_class = CarsSerializer
# GET single car with id
class CarAPI(generics.RetrieveAPIView):
queryset = Car.objects.all()
serializer_class = CarsSerializer
class CarListOngoingAPI(generics.ListAPIView):
queryset = Car.objects.all()
serializer_class = CarsSerializer
And here is my Car model:
class Car(models.Model):
make = models.CharField(max_length=100, blank=True)
description = models.CharField(max_length=500, blank=True)
ongoing = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
First class returs a list of all car models. Now I need to find a way to implement two other types, one where an argument is passed in, in my case id api/cars/:id/ and the second api/cars/ongoing/, I should say that these are just hypothethetical cases, just for learning purposes and any help is greatly appreciated.
I'm quite bad at Django myself, from what what I've gathered you can simple do something like so:
def car_id(request, pk):
"""url: api/car/pk"""
try:
car = models.Car.objects.get(id=pk)
except models.Car.DoesNotExist:
# Whoopsie
return HttpResponseNotFound(
json.dumps({"ERR": f"car with id {id} not found"}),
content_type="application/json",
)
# Serialise your car or do something with it
return JsonResponse(CarSerializer(car).data)
(Assuming you have your api's urlpatterns defined like so)
urlpatterns = [
...
r"api/car/(?P<id>\d+)/", views.car_id
]
For the first part of my problem, I just had to fix the urls to:
urlpatterns = [
path('api/cars/', CarsAPI.as_view()),
path('api/cars/<int:pk>/', CarAPI.as_view()),
path('api/tours/ongoing/', CarListOngoingAPI.as_view())
]
And for the second, I learned how to filter a query set by overriding get_queryset()
class CarListOngoingAPI(generics.ListAPIView):
# queryset = Car.objects.all()
serializer_class = CarsSerializer
def get_queryset(self):
return Car.objects.filter(ongoing=True)
I am using a view in django rest framework. In this view it takes an argument city to then fetch a list a neighborhoods in that city.
the example of the url looks like this:
http://127.0.0.1:8000/api/neighborhood-list/chicago/
the url code looks like this:
url(r'neighborhood-list/(?P<city>[a-zA-Z]+)/', VenueFilterOptionsView.as_view()),
the view:
class NeighborhoodListView(generics.ListAPIView):
lookup_field = 'city'
def list(self, request, city):
self.city = city
queryset = Neighborhood.objects.filter(city=self.city)
serializer = NeighborhoodSerializer(queryset, many=True)
the serializer:
class NeighborhoodSerializer(serializers.ModelSerializer):
class Meta:
model = Neighborhood
fields = 'neighborhood'
the model:
class Neighborhood(models.Model):
city = models.ForeignKey(City, null=True)
neighborhood = models.CharField(max_length=150, null=False)
what I don't understand is I set the lookup field to city, unless that only works for instances not lists? And even so I am using the listAPIView generic
the exception location is here:
/home/rickus/211hospitality/suitsandtables/backend/venv/local/lib/python2.7/site-packages/django/db/models/fields/__init__.py in get_prep_value, line 966
and the code on line 966 is the following:
def get_prep_value(self, value):
value = super(AutoField, self).get_prep_value(value)
if value is None:
return None
return int(value)
the return value of this method in the init folder being referenced by the stack trace is being cast as an int every time. SO I guess now the question is how do we override this nonsense or work around it.
so now I am working my way back trying to figure out what is going on.
anyone have any ideas?
Update - My original answer was incorrect. List view doesn't actually work with the lookup_field and lookup_url_kwarg attributes, those attributes are used by Rest Frameworks DetailView in the get_object(self) method to retrieve a single instance using those lookup fields.
I've updated the answer so it overrides the get_queryset(self) method to return the correctly filtered list. This is how ListView should be customised.
It looks like you haven't defined your ListView properly. The problem seems to be that you are trying to filter on the Cities Primary Key, which is an integer field, using a string that can't be parsed as an integer. I'll write up how I think your view should look presuming your trying to do your filtering based on some field on the City model.
# models.py
class City(models.Model):
name = models.CharField(max_length=32)
class Neighbourhood(models.Model):
city = models.ForeignKey(City)
# views.py
class NeighbourhoodListView(generics.ListAPIView):
queryset = Neighbourhood.objects.all()
serializer_class = NeighbourhoodSerializer
def get_queryset(self):
return self.queryset.filter(city__name=self.kwargs.get('city')
# urls.py
urlpatterns = [
url(
r'^neighbourhood-list/(?P<city>[a-zA-Z]+)/$',
NeighbourhoodListView.as_view(),
name='neighbourhood-list',
)
]
This will filter your Neighbourhoods by the Cities name. If you want to filter Neighbourhoods by the cities Primary Key, then you should use:
# views.py
class NeighbourhoodListView(generics.ListAPIView):
queryset = Neighbourhood.objects.all()
serializer_class = NeighbourhoodSerializer
def get_queryset(self):
return self.queryset.filter(city=self.kwargs.get('city'))
# urls.py
urlpatterns = [
url(
r'^neighbourhood-list/(?P<city>\d+)/$',
NeighbourhoodListView.as_view(),
name='neighbourhood-list',
)
]
This fixes the view and url's to filter Neighbourhoods by the Cities Primary Key. This would be more performant because it doesn't need to perform a join between City and Neighbourhood.
I have what I hope is a simple query for the stack overflow community.
Given the following configuration, I have a fairly simple int field on my "Totals" model and can't seem to get filtering to work on that field.
Here is the object from models.py:
class TotalType(Enum):
daily_sum = 1
weekly_sum = 2
class Total(models.Model):
TOTAL_TYPES = (
(TotalType.daily_sum, 'Daily Sum'),
(TotalType.weekly_sum, 'Weekly Sum')
)
location = models.ForeignKey(Location, db_column='LocationId')
ci_timestamp = models.DateTimeField(db_column='ci_TimeStamp', blank=False, null=False)
amount = models.DecimalField(max_digits=12, decimal_places=2, blank=False, null=False)
total_type = models.IntegerField(max_length=5, blank=False, choices=TOTAL_TYPES)
Here is the router info from urls.py:
router = DefaultRouter()
router.register(r'totals', TotalViewSet)
urlpatterns = patterns('',
url(r'^', include(router.urls))
)
Here is my object serializer:
class TotalSerializer(serializers.HyperlinkedModelSerializer):
location = serializers.HyperlinkedRelatedField(view_name="location-detail", many=False)
class Meta:
model = Total
fields = ('id', 'location', 'ci_timestamp', 'amount', 'total_type')
Finally, here is the view configuration:
class TotalViewSet(viewsets.ReadOnlyModelViewSet):
filter_fields = ('total_type', 'location')
queryset = Total.objects.all()
serializer_class = TotalSerializer
The problem:
A request for all "totals" works fine: GET /totals returns all.
A request for "totals" by location works fine: GET /totals?location=1 returns all totals for location 1.
A request for totals by total_type returns 0 results, incorrectly: GET /totals?total_type=1 returns 0 results. No error thrown.
Looking at the debug toolbar, I can see that no query is executed using the Total model. It makes the queries against django_Session and auth_user and that is it. These queries return as expected.
If I call with both params (location and total_type), I can see that it makes the query with only the location in the WHERE clause, but the API still returns no results...even the the query does (although erroneously).
Any ideas??
Django 1.6.4
Python 2.7
djangorestframework 2.3.13
Answer (which struck me 2 minutes after I posted the question):
I needed to use the enum values in my choices, not the actual objects that represent the name/value pairs.
Like this:
TOTAL_TYPES = (
(TotalType.daily_sum.value, 'Daily Sum'),
(TotalType.weekly_sum.value, 'Weekly Sum')
)
I'm surprised that everything else worked except for the REST filtering params before I made this change.
I have such model:
class Place(models.Model):
name = models.CharField(max_length=80, db_index=True)
city = models.ForeignKey(City)
address = models.CharField(max_length=255, db_index=True)
# and so on
Since I'm importing them from many sources, and users of my website are able to add new Places, I need a way to merge them from an admin interface. Problem is, name is not very reliable since they can be spelled in many different ways, etc
I'm used to use something like this:
class Place(models.Model):
name = models.CharField(max_length=80, db_index=True) # canonical
city = models.ForeignKey(City)
address = models.CharField(max_length=255, db_index=True)
# and so on
class PlaceName(models.Model):
name = models.CharField(max_length=80, db_index=True)
place = models.ForeignKey(Place)
query like this
Place.objects.get(placename__name='St Paul\'s Cathedral', city=london)
and merge like this
class PlaceAdmin(admin.ModelAdmin):
actions = ('merge', )
def merge(self, request, queryset):
main = queryset[0]
tail = queryset[1:]
PlaceName.objects.filter(place__in=tail).update(place=main)
SomeModel1.objects.filter(place__in=tail).update(place=main)
SomeModel2.objects.filter(place__in=tail).update(place=main)
# ... etc ...
for t in tail:
t.delete()
self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main)
merge.short_description = "Merge places"
as you can see, I have to update all other models with FK to Place with new values. But it's not very good solution since I have to add every new model to this list.
How do I "cascade update" all foreign keys to some objects prior to deleting them?
Or maybe there are other solutions to do/avoid merging
If anyone intersted, here is really generic code for this:
def merge(self, request, queryset):
main = queryset[0]
tail = queryset[1:]
related = main._meta.get_all_related_objects()
valnames = dict()
for r in related:
valnames.setdefault(r.model, []).append(r.field.name)
for place in tail:
for model, field_names in valnames.iteritems():
for field_name in field_names:
model.objects.filter(**{field_name: place}).update(**{field_name: main})
place.delete()
self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main)
Two libraries now exist with up-to-date model merging functions that incorporate related models:
Django Extensions' merge_model_instances management command.
Django Super Deduper
Tested on Django 1.10. Hope it can serve.
def merge(primary_object, alias_objects, model):
"""Merge 2 or more objects from the same django model
The alias objects will be deleted and all the references
towards them will be replaced by references toward the
primary object
"""
if not isinstance(alias_objects, list):
alias_objects = [alias_objects]
if not isinstance(primary_object, model):
raise TypeError('Only %s instances can be merged' % model)
for alias_object in alias_objects:
if not isinstance(alias_object, model):
raise TypeError('Only %s instances can be merged' % model)
for alias_object in alias_objects:
# Get all the related Models and the corresponding field_name
related_models = [(o.related_model, o.field.name) for o in alias_object._meta.related_objects]
for (related_model, field_name) in related_models:
relType = related_model._meta.get_field(field_name).get_internal_type()
if relType == "ForeignKey":
qs = related_model.objects.filter(**{ field_name: alias_object })
for obj in qs:
setattr(obj, field_name, primary_object)
obj.save()
elif relType == "ManyToManyField":
qs = related_model.objects.filter(**{ field_name: alias_object })
for obj in qs:
mtmRel = getattr(obj, field_name)
mtmRel.remove(alias_object)
mtmRel.add(primary_object)
alias_object.delete()
return True
Based on the snippet provided in the comments in the accepted answer, I was able to develop the following. This code does not handle GenericForeignKeys. I don't ascribe to their use as I believe it indicates a problem with the model you are using.
I used list a lot of code to do this in this answer, but I have updated my code to use django-super-deduper mentioned here. At the time, django-super-deduper did not handle unmanaged models in a good way. I submitted an issue, and it looks like it will be corrected soon. I also use django-audit-log, and I don't want to merge those records. I kept the signature and the #transaction.atomic() decorator. This is helpful in the event of a problem.
from django.db import transaction
from django.db.models import Model, Field
from django_super_deduper.merge import MergedModelInstance
class MyMergedModelInstance(MergedModelInstance):
"""
Custom way to handle Issue #11: Ignore models with managed = False
Also, ignore auditlog models.
"""
def _handle_o2m_related_field(self, related_field: Field, alias_object: Model):
if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
return super()._handle_o2m_related_field(related_field, alias_object)
def _handle_m2m_related_field(self, related_field: Field, alias_object: Model):
if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
return super()._handle_m2m_related_field(related_field, alias_object)
def _handle_o2o_related_field(self, related_field: Field, alias_object: Model):
if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
return super()._handle_o2o_related_field(related_field, alias_object)
#transaction.atomic()
def merge(primary_object, alias_objects):
if not isinstance(alias_objects, list):
alias_objects = [alias_objects]
MyMergedModelInstance.create(primary_object, alias_objects)
return primary_object
I was looking for a solution to merge records in Django Admin, and found a package that is doing it (https://github.com/saxix/django-adminactions).
How to use:
Install package:
pip install django-adminactions
Add adminactions to your INSTALLED_APPS:
INSTALLED_APPS = (
'adminactions',
'django.contrib.admin',
'django.contrib.messages',
)
Add actions to admin.py:
from django.contrib.admin import site
import adminactions.actions as actions
actions.add_to_site(site)
Add service url to your urls.py: url(r'^adminactions/', include('adminactions.urls')),
Tried it just now, it works for me.
I just stumbled upon the hardest problem I ever had with Django Rest Framework. Let me give you my models first, and then explain:
class Stampcardformat(models.Model):
workunit = models.ForeignKey(
Workunit,
on_delete=models.CASCADE
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
limit = models.PositiveSmallIntegerField(
default=10
)
category = models.CharField(
max_length=255
)
class Stampcard(models.Model):
stampcardformat = models.ForeignKey(
Stampcardformat,
on_delete=models.CASCADE
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
class Stamp(models.Model):
stampcardformat = models.ForeignKey(
Stampcardformat,
on_delete=models.CASCADE
)
stampcard = models.ForeignKey(
Stampcard,
on_delete=models.CASCADE,
blank=True,
null=True
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
These models describe a simple stampcard model. A stampcard is considered full, when it has as many stamps via foreignkey associated to it as it's stampcardformat's limit number dictates.
I need to write view that does the following:
The view takes in a list of stamps (see below) consisting of their
uuid's.
It then needs to find the right stampcardformat for each given
stamp.
Next it needs to check, whether the requests user has a stampcard
with the corresponding stampcardformat.
a) If it has, it needs to check, if the stampcard is full or not.
i) If it is full, it needs to create a new stampcard of the given format
and update the stamps stampcard-foreignkey to the created stampcard.
ii) If it isn't full, it needs update the stamps stampcard-foreignkey
to the found stampcard
b) If the user hasn't got a stampcard of the given
stampcardformat, it needs to create a new stampcard and update the
stamps stampcard-foreignkey to the created stampcard.
Here is the request body list of stamps:
[
{
"stamp_uuid": "62c4070f-926a-41dd-a5b1-1ddc2afc01b2"
},
{
"stamp_uuid": "4ad6513f-5171-4684-8377-1b00de4d6c87"
},
...
]
The class based views don't seem to support this behaviour. I tried modifying the class based views, to no avail. I fail besides many points, because the view throws the error:
AssertionError: Expected view StampUpdate to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
Edit
For additional context: I need the url to be without pk, slug or anything.
So the url should just be something like:
/api/stampcards/stamps/
and do a put (or any request that has a body and works) to it.
The route I wrote is:
url(r'^stamps/$', StampUpdate.as_view(), name='stamp-api-update'),
Edit:
HUGE update. So I managed to cheese together a view that works.
First I updated the stampcard model like this (I did add anew field 'done' to track if it is full):
class Stampcard(models.Model):
stampcardformat = models.ForeignKey(
Stampcardformat,
on_delete=models.CASCADE
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
done = models.BooleanField(default=False)
Then I wrote the view like this:
class StampUpdate(APIView):
permission_classes = (IsAuthenticated,)
def get_object(self, uuid):
try:
return Stamp.objects.get(uuid=uuid)
except Stamp.DoesNotExist():
raise Http404
def put(self, request, format=None):
for stamp_data in request.data:
stamp = self.get_object(stamp_data['stamp_uuid'])
if stamp.stampcard==None:
user_stampcard = self.request.user.stampcard_set.exclude(done=True).filter(stampcardformat=stamp.stampcardformat)
if user_stampcard.exists():
earliest_stampcard = user_stampcard.earliest('timestamp')
stamp.stampcard = earliest_stampcard
stamp.save()
if earliest_stampcard.stamp_set.count() == earliest_stampcard.stampcardformat.limit:
earliest_stampcard.done=True
earliest_stampcard.save()
else:
new_stampcard = Stampcard(stampcardformat=stamp.stampcardformat, user=self.request.user)
new_stampcard.save()
stamp.stampcard = new_stampcard
stamp.save()
new_stampcards = Stampcard.objects.exclude(done=True).filter(user=self.request.user)
last_full_stampcard = Stampcard.objects.filter(user=self.request.user).filter(done=True)
if last_full_stampcard.exists():
last_full_stampcard_uuid=last_full_stampcard.latest('updated').uuid
last_full_stampcard = Stampcard.objects.filter(uuid=last_full_stampcard_uuid)
stampcards = new_stampcards | last_full_stampcard
else:
stampcards = new_stampcards
print(stampcards)
stampcard_serializer = StampcardSerializer(stampcards, many=True)
return Response(stampcard_serializer.data)
But I have two issues with this code:
My intuition tells me that the parts where is just call save() on the model instance (e.g. stamp.save()) are very unsafe for an api. I couldn't get it to work to serialize the data first. My question is: Is this view okay like this? Or can I improve anything? It doesn't use generic class based used for example, but I don't know how to use them here...
I would also love to return the stampcard, if it got filled up by this method. But I also want to exclude all non-relevant stampcards, which is why I called .exclude(done=True). A stampcard that got filled up unfortunately has done=True though! How can I add stampcards that got filled up in the process to the return value?
I don't think it's unsafe to have stamp.save() in PUT method because by definition it supposes to alter object's value.
For returning only relevant stampcards, you could just add stampcard to a set like this
class StampUpdateView(APIView):
def get_object(self, uuid):
try:
return Stamp.objects.get(uuid=uuid)
except Stamp.DoesNotExist():
raise Http404
def put(self, request, *args, **kwargs):
stampcard_set = set()
for stamp_data in request.data:
stamp = self.get_object(stamp_data['stamp_uuid'])
user_stampcard = request.user.stampcard_set.exclude(done=True).filter(stampcardformat=stamp.stampcardformat)
if user_stampcard.exists():
stampcard = user_stampcard.earliest('timestamp')
else:
stampcard = Stampcard(stampcardformat=stamp.stampcardformat, user=request.user)
stampcard.save()
stamp.stampcard = stampcard
stamp.save()
if stampcard.stamp_set.count() == stampcard.stampcardformat.limit:
stampcard.done = True
stampcard.save()
stampcard_set.add(stampcard)
stampcard_serializer = StampcardSerializer(stampcard_set, many=True)
return Response(stampcard_serializer.data)
This way it doesn't matter if returning stampcards are already done or not.
Also note that I move down limit checking lines in your code to be after when stamp was saved because if limit was set to 1, stampcard will have to be set as done immediately after a stamp was added.