django rest-framework singleton ViewSet - python

I need to pass a set of config-settings (key-value pairs) through the django rest_framework to an api-enpoint. Read-only is fine. Django 1.7, Python3 and rest-framework v3.0.5.
I have pip installed django-solo, and I can access to it in the admin section, so I assume it works. I have set up a route which works, and now I need to make the 'View-like-thing' that actually returns the data.
This is as far as I have gotten (definitely wrong):
class ConfigViewSet(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
model = SiteConfiguration
permission_classes = (IsAuthenticatedOrReadOnly,)
def get_serializer_class(self):
# What goes here? I want _all_ the settings
def get_object(self):
obj = self.model.get_solo()
self.check_object_permissions(self.request, obj)
return obj
def list(self, *args, **kwargs):
return self.retrieve(*args, **kwargs)
Any help and hints appreciated.
PS! This is the config/models.py that has the settings:
from django.db import models
from solo.models import SingletonModel
class SiteConfiguration(SingletonModel):
site_name = models.CharField(max_length=255, default='Site Name')
maintenance_mode = models.BooleanField(default=False)
def __str__(self):
return u"Site Configuration"
class Meta:
verbose_name = "Site Configuration"

Oki, here goes:
1) pip-install 'django-solo'.
2) Make a new app with manage.py startapp config.
2a) File config/models.py:
from django.db import models
from solo.models import SingletonModel
class SiteConfiguration(SingletonModel):
site_name = models.CharField(max_length=255, default='Site Name')
maintenance_mode = models.BooleanField(default=False)
def __str__(self):
return u"Site Configuration"
class Meta:
verbose_name = "Site Configuration"
2b) File config/views.py:
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import SiteConfiguration
config = SiteConfiguration.get_solo()
class SiteConfiguration(APIView):
permission_classes = []
def get(self, request, format=None):
"""
Return site configuration key-values.
"""
return Response({
'name': config.site_name
})
3) The next problem is adding the view to the router. Using the DefaultRouter, one cannot register APIviews, so this guy had a simple HybridRouter solution [https://stackoverflow.com/a/23321478/1008905].
3a) Make a custom_routers.py in your project folder (where your main urls.py-file is located), with this content:
from rest_framework import routers, views, reverse, response
class HybridRouter(routers.DefaultRouter):
def __init__(self, *args, **kwargs):
super(HybridRouter, self).__init__(*args, **kwargs)
self._api_view_urls = {}
def add_api_view(self, name, url):
self._api_view_urls[name] = url
def remove_api_view(self, name):
del self._api_view_urls[name]
#property
def api_view_urls(self):
ret = {}
ret.update(self._api_view_urls)
return ret
def get_urls(self):
urls = super(HybridRouter, self).get_urls()
for api_view_key in self._api_view_urls.keys():
urls.append(self._api_view_urls[api_view_key])
return urls
def get_api_root_view(self):
# Copy the following block from Default Router
api_root_dict = {}
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
api_view_urls = self._api_view_urls
class APIRoot(views.APIView):
_ignore_model_permissions = True
def get(self, request, format=None):
ret = {}
for key, url_name in api_root_dict.items():
ret[key] = reverse.reverse(url_name, request=request, format=format)
# In addition to what had been added, now add the APIView urls
for api_view_key in api_view_urls.keys():
ret[api_view_key] = reverse.reverse(api_view_urls[api_view_key].name, request=request, format=format)
return response.Response(ret)
return APIRoot.as_view()
3b) In your main urls.py do this:
from .custom_routers import HybridRouter
# The rest is from the `rest-framework` polls-tutorial.
rest_router = HybridRouter()
rest_router.register(r'users', UserViewSet)
rest_router.register(r'polls', PollViewSet)
rest_router.add_api_view("config", url(r'^config/$', configViews.SiteConfiguration.as_view(), name='site_configuration'))
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url(r'^', include(rest_router.urls), name='rest_api'),
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^admin/', include(admin.site.urls), name='admin'),
]
All which seems to work for me.

An alternate approach that doesn't require outside dependencies:
# models.py
from django.db import models
class MySingleton(models.Model):
def save(self, *args, **kwargs):
self.pk = 1
return super().save(*args, **kwargs)
#classmethod
def singleton(cls):
obj, _ = cls.objects.get_or_create(pk=1)
return obj
# serializers.py
from rest_framework import serializers
from . import models
class MySingletonSerializer(serializers.ModelSerializer):
class Meta:
model = models.MySingleton
fields = "__all__"
# views.py
from rest_framework import generics
from . import models
from . import serializers
class SingletonView(generics.RetrieveAPIView):
serializer_class = serializers.MySingletonSerializer
def get_object(self):
return models.MySingleton.singleton()

Related

ViewSet and additional retrieve URL

I have a django model Donation that I expose as a ViewSet. Now I want to add an additional URL to a second model Shop where a related instance of Donation can be retrieved via the parameter order_id and custom actions can be executed.
# models.py
class Donation(models.Model):
id = models.AutoField(primary_key=True)
order_id = models.StringField(help_text='Only unique in combination with field `origin`')
origin = models.ForeignKey('Shop', on_delete=models.PROTECT)
class Shop(models.Model):
id = models.AutoField(primary_key=True)
# views.py
class DonationViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
def retrieve(self, request, *args, **kwargs):
if kwargs['pk'].isdigit():
return super(DonationViewSet, self).retrieve(request, *args, **kwargs)
else:
shop_id = self.request.query_params.get('shop_id', None)
order_id = self.request.query_params.get('order_id', None)
if shop_id is not None and order_id is not None:
instance = Donations.objects.filter(origin=shop_id, order_id=order_id).first()
if instance is None:
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(self.get_serializer(instance).data)
return Response(status=status.HTTP_404_NOT_FOUND)
#action(methods=['post'], detail=True)
def custom_action(self, request, *args, **kwargs):
pass
class ShopViewSet(viewsets.ModelViewSet):
pass
# urls.py
router = routers.DefaultRouter()
router.register(r'donations', DonationViewSet)
router.register(r'shops', ShopViewSet)
router.register(r'shops/(?P<shop_id>[0-9]+)/donations/(?P<order_id>[0-9]+)', DonationViewSet)
My goal is to have http://localhost:8000/donations point at the entire DonationViewSet. Also I would like to lookup an individual donation, by its combination of shop_id and order_id like follows http://localhost:8000/shops/123/donations/1337/ and also executing the custom action like follows http://localhost:8000/shops/123/donations/1337/custom_action/. The problem I have is that the second url returns an entire queryset, not just a single instance of the model.
You can also use drf-nested-routers, which will have something like this:
from rest_framework_nested import routers
from django.conf.urls import url
# urls.py
router = routers.SimpleRouter()
router.register(r'donations', DonationViewSet, basename='donations')
router.register(r'shops', ShopViewSet, basename='shops')
shop_donations_router = routers.NestedSimpleRouter(router, r'', lookup='shops')
shop_donations_router.register(
r'donations', ShopViewSet, basename='shop-donations'
)
# views.py
class ShopViewSet(viewsets.ModelViewSet):
def retrieve(self, request, pk=None, donations_pk=None):
# pk for shops, donations_pk for donations
#action(detail=True, methods=['PUT'])
def custom_action(self, request, pk=None, donations_pk=None):
# pk for shops, donations_pk for donations
This is not tested! But in addition to what you already have, this will support:
donations/
donations/1337/
shops/123/donations/1337/
shops/123/donations/1337/custom_action
You can add urls by simply appending to the router's urls in the config like so. If all you want to do is add a single action from a view for one specifc url, and dont need all of the actions/urls for the viewset
# urls.py
urlpatterns = [
path('some_path', my_lookup_view)),
] # typical django url convention
urlpatterns += router.urls
# views.py
#api_view(['POST'])
def my_looup_view(request, shop_id, order_id):
# ...some lookup...
pass
You'll want to
derive from GenericViewSet since you're using models anyway
override get_object() instead with your custom lookup logic:
from rest_framework import mixins
from rest_framework.generics import get_object_or_404
from rest_framework.viewsets import GenericViewSet
class MyModelViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
lookup_value = self.kwargs["pk"]
if lookup_value.isdigit():
obj = get_object_or_404(queryset, pk=lookup_value)
else:
obj = get_object_or_404(queryset, third_party_service_id=lookup_value)
self.check_object_permissions(self.request, obj)
return obj
#action(methods=["post"], detail=True)
def custom_action(self, request, *args, **kwargs):
thing = self.get_object()
# urls.py
router.register(r"mymodels", MyModelViewSet)
This should let you do
mymodels/ to list models,
mymodels/123/ to get a model by PK,
mymodels/kasjdfg/ to get a model by third-party service id,
mymodels/123/custom_action/ to run custom_action on a model by PK
mymodels/kasjdfg/custom_action/ to run custom_action on a model by service id,

Get detail by Unique ID but not PK in Django Rest Framework URLs

I am trying to create Rest API using DRF. Wanted to get detail by using UniqueId. I can use PK and get the output but wanna use unique id (token_id in my jobs Model) created in the model field.
Models.py
from django.db import models
from rest_api.util import unique_slug_generator
from django.urls import reverse
# Create your models here.
class Jobs(models.Model):
token_id = models.CharField(max_length=64, unique=True)
name = models.CharField(max_length=100)
url = models.URLField()
environment = models.CharField(max_length=100, null=True)
runtestnow = models.BooleanField(default=False)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('token_id', kwargs={'token_id':self.token_id})
class Queue(models.Model):
tokenid = models.ForeignKey(Jobs, on_delete=models.CASCADE)
date = models.DateField(auto_now=True)
def __str__(self):
return self.tokenid
class VM(models.Model):
vm_count = models.IntegerField(default=120)
def __str__(self):
return f"VM Count: {self.vm_count}"
Urls.py
from django.urls import path, include
from . import views
from .views import (RegisterTestMethodView,
RegisterTestMethodViewDetail,
CheckStatusView,
ReleaseTokenView
)
from rest_framework import routers
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.urlpatterns import format_suffix_patterns
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
router = routers.DefaultRouter()
router.register('jobs', views.JobsView)
urlpatterns = [
path('', include(router.urls)),
path('registertestmethod/', RegisterTestMethodView.as_view()),
path('registertestmethod/<int:pk>/',
RegisterTestMethodViewDetail.as_view()),
path('checkstatus/<int:pk>', CheckStatusView.as_view()),
path('checkstatus/<token_id>', CheckStatusView.as_view()),
path('releasetoken/<int:pk>', ReleaseTokenView.as_view()),
]
Serializers.py
from rest_framework import serializers
from .models import Jobs
from django.utils.crypto import get_random_string
class JobsSerializers(serializers.HyperlinkedModelSerializer):
token_id = serializers.CharField(default=get_random_string(length=25))
class Meta:
model = Jobs
fields = ('id', 'name', 'url','runtestnow','token_id')
class CheckStatusSerializers(serializers.HyperlinkedModelSerializer):
class Meta:
model = Jobs
fields = ('id','runtestnow')
class RegisterTestMethodSerializers(serializers.HyperlinkedModelSerializer):
class Meta:
model = Jobs
fields = ('id', 'name', 'url', 'environment', 'runtestnow', 'token_id')
Views.py
from rest_framework import viewsets, permissions, authentication
from .models import Jobs, VM, Queue
from .serializers import (JobsSerializers,
RegisterTestMethodSerializers,
CheckStatusSerializers)
import json
import datetime
import collections
collections.deque()
#3rd time
from rest_framework import generics
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authentication import (SessionAuthentication,
BasicAuthentication,
TokenAuthentication)
from rest_framework.permissions import IsAuthenticated
from django.utils.crypto import get_random_string
with open('greet/static/greet/static/config.json', 'r') as
data_config:
data_ready = json.load(data_config)
totalVM = int(data_ready['totalVM'])
max_vm = int(data_ready['max_vm_count'])
grid_name = (data_ready['GridNameForDev'])
min_vm = int(data_ready['min_vm_count'])
class RegisterTestMethodView(APIView):
# authentication_classes = [SessionAuthentication,
TokenAuthentication, BasicAuthentication]
# permission_classes = [IsAuthenticated] # No access (not even
read if not authorized)
def get(self, request):
snippets = Jobs.objects.all()
serializer = RegisterTestMethodSerializers(snippets,
many=True)
return Response(serializer.data)
def post(self, request):
queue = VM.objects.all()
id_token = get_random_string(length=25)
if not queue:
queue = VM(vm_count=totalVM)
queue.save()
else:
for queue_obj in queue:
queue = queue_obj
if queue.vm_count > min_vm:
queue.vm_count -= max_vm
queue.save()
request.data["token_id"] = id_token
request.data["runtestnow"] = True
else:
request.data["token_id"] = id_token
request.data["runtestnow"] = False
serializer = RegisterTestMethodSerializers(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'TokenId': serializer.data['token_id'],
'RunTestNow': serializer.data['runtestnow'],
'VmCount': queue.vm_count,
'GridName': grid_name, 'Vm_left':
queue.vm_count}, status=status.HTTP_201_CREATED)
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
class JobsView(viewsets.ModelViewSet):
queryset = Jobs.objects.all()
serializer_class = JobsSerializers
lookup_field = 'token_id'
class CheckStatusView(APIView):
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk, token_id):
try:
return Jobs.objects.get(pk=pk)
except Jobs.DoesNotExist:
raise Http404
def get(self, request, token_id):
pk = request.GET.get('pk')
print(pk)
queue = VM.objects.get()
job_list = Jobs.objects.exclude(runtestnow=True)
filtered = Jobs.objects.filter(id=pk)
next_q = job_list.order_by('id').first()
waitlist = 1
return Response(
{"tokenid": token_id, "Runtestnow": False, "VMcount":
queue.vm_count,
'GridName': grid_name, 'waitlist #': waitlist,
'Vm_left':
queue.vm_count}, status=status.HTTP_201_CREATED)
def post(self, request, pk):
queue = VM.objects.get()
vm_count = queue.vm_count
job_list = Jobs.objects.exclude(runtestnow=True)
filtered = Jobs.objects.filter(id=pk)
next_q = job_list.order_by('id').first()
waitlist = int(pk-next_q.id + 1)
if next_q:
print(next_q.id)
if next_q.id == pk and queue.vm_count > min_vm:
queue.vm_count -= max_vm
filtered.update(runtestnow=True)
queue.save()
vm_used = max_vm
else:
filtered.update(runtestnow=False)
queue.save()
vm_used = 0
snippet = self.get_object(pk)
serializer = RegisterTestMethodSerializers(snippet)
return Response({"tokenid": serializer.data["id"],
"Runtestnow": serializer.data['runtestnow'], "VMcount":
vm_used,
'GridName': grid_name, 'waitlist #': waitlist ,
'Vm_left': queue.vm_count},
status=status.HTTP_201_CREATED)
class ReleaseTokenView(APIView):
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk):
try:
return Jobs.objects.get(pk=pk)
except Jobs.DoesNotExist:
raise Http404
def delete(self, request, pk, format=None):
queue = VM.objects.get()
if not queue:
queue = VM(vm_count=totalVM)
if not self.get_object(pk):
print("Not Method Called...")
return
if queue.vm_count < totalVM :
queue.vm_count += max_vm
queue.save()
elif queue.vm_count + max_vm > totalVM:
queue.vm_count = totalVM
queue.save()
snippet = self.get_object(pk)
snippet.delete()
return Response(data={'Released': True},
status=status.HTTP_204_NO_CONTENT)
I can get information using but I wanna user token_id. I can do that using Serializers as it is in jobs.
If I do
localhost/jobs/xJcn8XxF2g9DmmwQwGS0Em754. # --> I get the output but I
# wanna use and I am aware
#that this will return all CRUD methods but how do I apply the
#business logic in Serializers.
localhost/checkstatus/xJcn8XxF2g9DmmwQwGS0Em754 . # --> I wanna
# apply business logic before getting the output. Which
# returns Response related to the PK as well.
What is the best way to do it?
Do I add it on serializer.py(how) or views.py?
I would appreciate it if you provide any helpful documents.
You should set lookup_field as token_id in your serializer and viewset.
Here is answer Django Rest Framework: Access item detail by slug instead of ID
Actually I was able to do it by some research. It seems like I have to pass a unique id (token_id) in URL and query using the same unique id (token_id) on the views.py. I was aware that there is modelviewset that does it effortlessly as mentioned by Ishak, but I wanted to use APIView and on top of that I wanted some business logic to be added. I probably have to do some more research on how to add logic to modelviewset. Here is my Solution.
Views.py
def get(self, request, token_id):
get_job = Jobs.objects.get(token_id=token_id)
pk = get_job.id
job_list = Jobs.objects.exclude(runtestnow=True)
next_q = job_list.order_by('id').first()
queue = VM.objects.get()
waitlist = int(pk) - int(next_q.id)
if waitlist == 1:
waitlist = 'You are next on the queue. :)'
return Response(
{"tokenid": token_id, "Runtestnow": False, "VMcount":
queue.vm_count,
'GridName': grid_name, 'waitlist #': waitlist, 'Vm_left':
queue.vm_count}, status=status.HTTP_201_CREATED)
Urls.py
path('checkstatus/<token_id>', CheckStatusView.as_view()),
We can always use the slug field, but I really wanted token_id as input. This should work fine for me as of now.
There might be some other way as well. Feel free to share.

Why django rest api root does not list classic endpoints

I have registerd some of my endpoints through routers and written others in classic style, by classic style I mean that they are not registerd through router but directly written in path. Now problem is that api root lists only the endpoints that are registered through router and i want both to be listed.
app/views.py
from uuid import UUID
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from event.models import Event
from event.serializers import EventSerializer
from food_album.models import FoodAlbums
from food_album.serializers import FoodAlbumsSerializer
from restaurant.models import RestaurantProfile
from restaurant.serializers import RestaurantProfileSerializer
from .serializers import *
customer_favourites = {
0: [RestaurantProfile, RestaurantProfileSerializer],
1: [Event, EventSerializer],
2: [FoodAlbums, FoodAlbumsSerializer]
}
class CustomerProfileViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions
"""
queryset = CustomerProfile.objects.all()
serializer_class = CustomerProfileSerializer
lookup_field = 'public_id'
def get_serializer_class(self):
if self.action in ['create', 'update']:
return CustomerProfileCreateSerializer
return self.serializer_class
class RewardTypesViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions
"""
queryset = RewardTypes.objects.all()
serializer_class = RewardTypesSerializer
lookup_field = 'public_id'
class RewardActionsViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions
"""
queryset = RewardActions.objects.all()
serializer_class = RewardActionsSerializer
lookup_field = 'public_id'
class ReportItemViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions
"""
queryset = ReportItem.objects.all()
serializer_class = ReportItemSerializer
lookup_field = 'public_id'
class CustomerFavouritesViewSet(viewsets.ModelViewSet):
"""
A viewset that provides the standard actions
"""
serializer_class = CustomerFavouritesSerializer
permission_classes = (IsAuthenticated,)
lookup_field = 'public_id'
def perform_create(self, serializer):
serializer.save(customer=self.request.user.customer)
def get_queryset(self):
user = self.request.user
if user.customer:
return CustomerFavourites.objects.filter(customer=user.customer)
else:
return None
class UserFeedsViewSet(viewsets.ViewSet):
"""
A viewset that provides the standard actions
"""
serializer_class = RestaurantProfileSerializer
def retrieve(self, request):
r = RestaurantProfile.objects.all()
serializer = RestaurantProfileSerializer(r, many=True)
return Response(serializer.data)
class CustomerBookmarkViewSet(viewsets.ViewSet):
"""
A viewset that provides the standard actions
"""
permission_classes = (IsAuthenticated,)
def list(self, request, reference_type):
bookmarks = CustomerFavourites.objects.filter(type=0,
reference_type=reference_type).values_list(
'reference_id', flat=True)
public_ids = [UUID(id_) for id_ in bookmarks]
r = customer_favourites.get(reference_type)[0].objects.filter(
public_id__in=public_ids).all()
serializer = customer_favourites.get(reference_type)[1](r, many=True)
return Response(serializer.data)
class CustomerFavouriteViewSet(viewsets.ViewSet):
"""
A viewset that provides the standard actions
"""
permission_classes = (IsAuthenticated,)
def list(self, request, reference_type):
favs = CustomerFavourites.objects.filter(type=1,
reference_type=reference_type).values_list(
'reference_id', flat=True)
public_ids = [UUID(id_) for id_ in favs]
r = customer_favourites.get(reference_type)[0].objects.filter(
public_id__in=public_ids).all()
serializer = customer_favourites.get(reference_type)[1](r, many=True)
return Response(serializer.data)
app/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'profile', views.CustomerProfileViewSet,
base_name='profile')
router.register('reward-actions', views.RewardActionsViewSet, base_name='reward_actions')
router.register('reward-types', views.RewardTypesViewSet, base_name='reward_types')
router.register('report-item', views.ReportItemViewSet, base_name='report_item')
router.register('toggle-favourites', views.CustomerFavouritesViewSet, base_name='toggle_favourites')
urlpatterns = [
path('customer/', include(router.urls)),
path('customer/user-feeds/',
views.UserFeedsViewSet.as_view({'get': 'retrieve'})),
path('customer/favourites/<int:reference_type>/',
views.CustomerFavouriteViewSet.as_view({'get': 'list'})),
path('customer/bookmarks/<int:reference_type>/',
views.CustomerBookmarkViewSet.as_view({'get': 'list'}))
]
You need to append router.urls to the list of existing views as
urlpatterns += router.urls. For more information, kindly look into - https://www.django-rest-framework.org/api-guide/routers/

How can I add a link to download a file in a Django admin detail page?

I want to create a file containing a string and allow the user to download the file when they click a button in the admin detail page. Any ideas?
Probably add html to the form? But how can I do that? I am new to Django.
You can go along the following lines:
class YourAdmin(ModelAdmin):
# add the link to the various fields attributes (fieldsets if necessary)
readonly_fields = ('download_link',)
fields = (..., 'download_link', ...)
# add custom view to urls
def get_urls(self):
urls = super(YourAdmin, self).get_urls()
urls += [
url(r'^download-file/(?P<pk>\d+)$', self.download_file,
name='applabel_modelname_download-file'),
]
return urls
# custom "field" that returns a link to the custom function
def download_link(self, obj):
return format_html(
'Download file',
reverse('admin:applabel_modelname_download-file', args=[obj.pk])
)
download_link.short_description = "Download file"
# add custom view function that downloads the file
def download_file(self, request, pk):
response = HttpResponse(content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="whatever.txt"')
# generate dynamic file content using object pk
response.write('whatever content')
return response
In your models.py field for that application, add the following piece of code
from django.utils.safestring import mark_safe
def fieldname_download(self):
return mark_safe('<a href="/media/{0}" download>{1}</a>'.format(
self.fieldname, self.fieldname))
fieldname_download.short_description = 'Download Fieldname'
Then in your admin.py, add this field to your readonly_fields for that model
readonly_fields = ('fieldname_download', )
In your settings.py file you need to specify a root path to a directory where to serve the files from and a base url for accessing them:
MEDIA_ROOT=(str, 'path/to/your/media/directory/'),
MEDIA_URL=(str,'/media/'),
There are two answer about add download link as new field to details page which is easyer than add download link inside AdminFileWidget. I write this answer in case someone need add download link inside AdminFileWidget.
The final result like this:
The way to achieve this is:
1 models.py:
class Attachment(models.Model):
name = models.CharField(max_length=100,
verbose_name='name')
file = models.FileField(upload_to=attachment_file,
null=True,
verbose_name='file ')
2 views.py:
class AttachmentView(BaseContextMixin, DetailView):
queryset = Attachment.objects.all()
slug_field = 'id'
def get(self, request, *args, **kwargs):
instance = self.get_object()
if settings.DEBUG:
response = HttpResponse(instance.file, content_type='application/force-download')
else:
# x-sendfile is a module of apache,you can replace it with something else
response = HttpResponse(content_type='application/force-download')
response['X-Sendfile'] = instance.file.path
response['Content-Disposition'] = 'attachment; filename={}'.format(urlquote(instance.filename))
return response
3 urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('attachment/<int:pk>/', AttachmentView.as_view(), name='attachment'),
]
4 admin.py
from django.urls import reverse
from django.contrib import admin
from django.utils.html import format_html
from django.contrib.admin import widgets
class DownloadFileWidget(widgets.AdminFileWidget):
id = None
template_name = 'widgets/download_file_input.html'
def __init__(self, id, attrs=None):
self.id = id
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
print(self, name, value, attrs, self.id)
context['download_url'] = reverse('attachment', kwargs={'pk': self.id})
return context
class AttachmentAdmin(admin.ModelAdmin):
list_display = ['id', 'name', '_get_download_url']
search_fields = ('name',)
my_id_for_formfield = None
def get_form(self, request, obj=None, **kwargs):
if obj:
self.my_id_for_formfield = obj.id
return super(AttachmentAdmin, self).get_form(request, obj=obj, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
if self.my_id_for_formfield:
if db_field.name == 'file':
kwargs['widget'] = DownloadFileWidget(id=self.my_id_for_formfield)
return super(AttachmentAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def _get_download_url(self, instance):
return format_html('{}', reverse('attachment', kwargs={'pk': instance.id}), instance.filename)
_get_download_url.short_description = 'download'
admin.site.register(Attachment, AttachmentAdmin)
5 download_file_input.html
{% include "admin/widgets/clearable_file_input.html" %}
Download {{ widget.value }}
That's all!

Django-haystack generic SearchView - no results

I'm tryinig to get haystack working with a class-based generic view according to the documentation here. I can get results from a SearchQuerySet in the shell, so the models are being indexed. But I can't get the view to return a result on the page.
The main reason for using the generic view is that I want to extend later with more SQS logic.
I'm probably missing something obvious...
views.py :
from haystack.query import SearchQuerySet
from haystack.generic_views import SearchView
from .forms import ProviderSearchForm
from .models import Provider
class ProviderSearchView(SearchView):
template_name = 'search/provider_search.html'
form_class = ProviderSearchForm
def get_context_data(self, *args, **kwargs):
""" Extends context to include data for services."""
context = super(ProviderSearchView, self).get_context_data(*args, **kwargs)
context['body_attr'] = 'id="provider-search"'
return context
def get_queryset(self):
queryset = super(ProviderSearchView, self).get_queryset()
return queryset.filter(is_active=True)
search_indexes.py:
from haystack import indexes
from .models import Provider
class ProviderIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
title = indexes.CharField(model_attr='name')
created = indexes.DateTimeField(model_attr='created')
def get_model(self):
return Provider
def index_queryset(self, using=None):
"Used when the entire index for model is updated."
return self.get_model().objects.all()
forms.py
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Submit
from crispy_forms.bootstrap import FieldWithButtons
from haystack.forms import SearchForm
from .models import Provider
class ProviderSearchForm(SearchForm):
""" Override the form with crispy styles """
models = [ Provider ]
def __init__(self, *args, **kwargs):
super(ProviderSearchForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.disable_csrf = True
self.helper.form_tag = False
self.helper.form_show_labels = False
self.helper.layout = Layout (
FieldWithButtons(
Field('q', css_class='form-control input-lg', placeholder="Search providers..."),
Submit('','Search', css_class='btn btn-lg btn-primary'))
)
def get_models(self):
return self.models
def search(self):
sqs = super(ProviderSearchForm, self).search().models(*self.get_models())
return sqs
def no_query_found(self):
return self.searchqueryset.all()
The problem was that my page template was using the wrong variable in the for loop.
The documentation suggests:
for result in page_object.object_list
It should be:
for result in page_obj.object_list
note the template variable is page_obj.
See issue post on GitHub

Categories