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
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,
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.
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/
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!
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