Compare commits
12 Commits
starfields
...
starfields
| Author | SHA1 | Date | |
|---|---|---|---|
| bc9a7d3832 | |||
| 9715122c6c | |||
| acd766ea29 | |||
| 051bae28ce | |||
| 71573ca44e | |||
| 986d4302a7 | |||
| 1099250ca5 | |||
| 9565b07d55 | |||
| 2c58bc955f | |||
| 75e9a60b87 | |||
| 0f26c16b15 | |||
| c1fd01a6d8 |
73
README.md
73
README.md
@@ -3,4 +3,77 @@ This repository holds the django library that StarFields uses for the django-res
|
||||
# Differences with the DRF generic views
|
||||
|
||||
It changes the generic lifecycles of all the CRUD operations to fit within them automated caching functionality. Caching and deleting cache keys is handled by the library in a way that the cache keys have no duplicates. The generic views offered include single item CRUD and list-based CRUD.
|
||||
|
||||
To manage automated caching this the library replaces (and appends to) the DRF filters. These filters need a get_unique_dict method in order to avoid the duplicate cache keys problem.
|
||||
|
||||
# Usage
|
||||
### Ensure that the module is in the INSTALLED_APPS in settings.py:
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'starfields_drf_generics',
|
||||
]
|
||||
```
|
||||
|
||||
### Making views in views.py:
|
||||
```python
|
||||
from starfields_drf_generics import generics
|
||||
from starfields_drf_generics import filters as libfilters
|
||||
|
||||
class CategoriesView(generics.CachedListRetrieveAPIView):
|
||||
"""
|
||||
This view lists all the categories. Usually this API is called on the shop initialization and the result is saved on the front-end for use shop-wide.
|
||||
"""
|
||||
cache_prefix = "shop.products.categories"
|
||||
cache_vary_on_user = False
|
||||
cache_timeout_mins = ShopSettings.get_solo().cache_timeout
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
paged = False
|
||||
filter_backends = []
|
||||
logger = logger
|
||||
cache = cache
|
||||
|
||||
class SearchView(generics.CachedListRetrieveAPIView):
|
||||
"""
|
||||
This view lists the gallery pictures with extensive searching and filtering features. You can use this API to get the latest pictures, perform picture searches among others.
|
||||
"""
|
||||
cache_prefix = "gallery.search"
|
||||
cache_vary_on_user = False
|
||||
cache_timeout_mins = GallerySettings.get_solo().cache_timeout
|
||||
queryset = Picture.objects.filter(published=True)
|
||||
serializer_class = PictureSerializer
|
||||
ordering_fields = ('similarity','date_added','updated')
|
||||
category_class = Category
|
||||
search_fields = ['name','slug','tag__name','tag__slug']
|
||||
paged = True
|
||||
default_page_size = 20
|
||||
filter_backends = (libfilters.CategoryFilter,
|
||||
libfilters.TrigramSearchFilter,
|
||||
libfilters.OrderingFilter,
|
||||
)
|
||||
logger = logger
|
||||
cache = cache
|
||||
```
|
||||
|
||||
### New class attributes that are used
|
||||
```python
|
||||
cache_prefix = defines the prefix that the module will use when saving values in the cache
|
||||
cache_vary_on_user = defines whether keys saved in the cache are different for each user, in which case extra user information will be added to the cache prefix
|
||||
cache_timeout_mins = the cache key timeout
|
||||
filter_backends = the filters that you want the view to have, each can be configured with view class attributes
|
||||
ordering_fields = if you use the OrderingFilter you must indicate what fields the user can order by, the first element is used as the default order
|
||||
search_fields = if you use the TrigramSearchFilter you must indicate the fields to search through
|
||||
paged = your generic view can have a pager for the user to choose pages or it can be a full listing
|
||||
default_page_size = the default size of the pages if a user has not indicated a page size
|
||||
logger = you should register a logger in order to get error feedback in your deployments
|
||||
cache = the main feature of this module is automated and organized caching, you should register your cache here
|
||||
```
|
||||
|
||||
### Extras
|
||||
The source code is similar to the django-rest-framework's generic classes and related objects, it should be eminently readable. As with the rest framework's generics multiple inheritance is used through mixing to organize the behavior of each class in a standalone setting.
|
||||
|
||||
# Duplicate Cache Keys Problem
|
||||
The problem arises in the way cache keys are created, the naive method is to just use the information from the url of the request and just save it to the cache. This creates a problem in that a request such as https://example.com/api/v1/pictures/search?search=mypic&category=mycat and a request https://example.com/api/v1/pictures/search?category=mycat&search=mypic contain the same information in their cache values. So the order of each filter or the order within the filters (such as the facet filter I made for e-commerce APIs) affects the caching behavior and creates more work for our APIs.
|
||||
|
||||
The way this module fixes the duplicate cache keys problem is by systematically ordering the filters through each filter's get_unique_dict method that are called in cache_mixins.py and then running the sorted_params_string utility function in the resulting dict.
|
||||
|
||||
@@ -20,3 +20,9 @@ classifiers = [
|
||||
[project.urls]
|
||||
"Homepage" = "https://git.vickys-corner.xyz/ace/starfields-drf-generics"
|
||||
|
||||
[options.packages.find]
|
||||
where = starfields_drf_generics
|
||||
|
||||
[options.package_data]
|
||||
templates.filters =
|
||||
*.html
|
||||
@@ -1,4 +1,4 @@
|
||||
from libraries.utils import sorted_params_string
|
||||
from starfields_drf_generics.utils import sorted_params_string
|
||||
|
||||
# TODO classes below that involve create, update, destroy don't delete the caches properly, they need a regex cache delete
|
||||
# TODO put more reasonable asserts and feedback
|
||||
@@ -2,7 +2,6 @@ from django_filters import rest_framework as filters
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import CharField
|
||||
from shop.models.product import Product, Facet
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
import operator
|
||||
from django.template import loader
|
||||
@@ -89,7 +88,7 @@ class CategoryFilter(BaseFilterBackend):
|
||||
"""
|
||||
This filter assigns the view.category object for later use, in particular for filters that depend on this one.
|
||||
"""
|
||||
template = './filters/categories.html'
|
||||
template = 'starfields_drf_generics/templates/filters/categories.html'
|
||||
category_field = 'category'
|
||||
|
||||
def get_category_class(self, view, request):
|
||||
@@ -203,7 +202,7 @@ class FacetFilter(BaseFilterBackend):
|
||||
"""
|
||||
This filter requires CategoryFilter to be ran before it. It assigns the view.facets which includes all the facets applicable to the current category.
|
||||
"""
|
||||
template = './filters/facets.html'
|
||||
template = 'starfields_drf_generics/templates/filters/facets.html'
|
||||
|
||||
def get_facet_class(self, view, request):
|
||||
return getattr(view, 'facet_class', None)
|
||||
@@ -216,13 +215,20 @@ class FacetFilter(BaseFilterBackend):
|
||||
|
||||
def assign_view_facets(self, request, view):
|
||||
if not hasattr(view, 'facets'):
|
||||
if hasattr(view, 'facet_class'):
|
||||
self.facet_class = self.get_facet_class(view, request)
|
||||
|
||||
assert self.facet_class is not None, (
|
||||
f"{view.__class__.__name__} should include a `facet_class` attribute"
|
||||
)
|
||||
|
||||
if view.category:
|
||||
if view.category.tn_ancestors_pks:
|
||||
view.facets = Facet.objects.filter(Q(category__id=view.category.id) | Q(category__id__in=view.category.tn_ancestors_pks.split(','))).prefetch_related('facet_tags')
|
||||
view.facets = self.facet_class.objects.filter(Q(category__id=view.category.id) | Q(category__id__in=view.category.tn_ancestors_pks.split(','))).prefetch_related('facet_tags')
|
||||
else:
|
||||
view.facets = Facet.objects.filter(category__id=view.category.id).prefetch_related('facet_tags')
|
||||
view.facets = self.facet_class.objects.filter(category__id=view.category.id).prefetch_related('facet_tags')
|
||||
else:
|
||||
view.facets = Facet.objects.filter(category__tn_level=1).prefetch_related('facet_tags')
|
||||
view.facets = self.facet_class.objects.filter(category__tn_level=1).prefetch_related('facet_tags')
|
||||
|
||||
def get_filters_dict(self, request, view):
|
||||
"""
|
||||
@@ -423,7 +429,7 @@ class TrigramSearchFilter(BaseFilterBackend):
|
||||
# TODO misunderstood the urlconf stuff of the RUD methods, this is probably unnecessary
|
||||
class SlugSearchFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the search.
|
||||
template = './filters/slug.html'
|
||||
template = 'starfields_drf_generics/templates/filters/slug.html'
|
||||
slug_title = _('Slug Search')
|
||||
slug_description = _("The instance's slug.")
|
||||
slug_field = 'slug'
|
||||
@@ -10,7 +10,7 @@ from rest_framework import views
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from libraries import mixins
|
||||
from starfields_drf_generics import mixins
|
||||
|
||||
|
||||
# Concrete view classes that provide method handlers
|
||||
@@ -8,7 +8,7 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework import mixins
|
||||
from libraries.cache_mixins import CacheGetMixin, CacheSetMixin, CacheDeleteMixin
|
||||
from starfields_drf_generics.cache_mixins import CacheGetMixin, CacheSetMixin, CacheDeleteMixin
|
||||
|
||||
|
||||
# Mixin classes to be included in the generic classes
|
||||
14
starfields_drf_generics/templates/filters/categories.html
Normal file
14
starfields_drf_generics/templates/filters/categories.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% load rest_framework %}
|
||||
{% load i18n %}
|
||||
<h2>{% trans "Categories" %}</h2>
|
||||
<div class="list-group">
|
||||
{% for key, label in options %}
|
||||
{% if key == current %}
|
||||
<a href="{% add_query_param request param key %}" class="list-group-item active">
|
||||
<span class="glyphicon glyphicon-ok" style="float: right" aria-hidden="true"></span> {{ label }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% add_query_param request param key %}" class="list-group-item">{{ label }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
23
starfields_drf_generics/templates/filters/facets.html
Normal file
23
starfields_drf_generics/templates/filters/facets.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% load rest_framework %}
|
||||
{% load i18n %}
|
||||
<h2>{% trans "Facets" %}</h2>
|
||||
{% for facet_tag_slug_key, facet_tag_slug_values in options.items %}
|
||||
{% for facet_slug, facet_name in facet_slug_names.items %}
|
||||
{% if facet_slug == facet_tag_slug_key %}
|
||||
<h3>{{ facet_name }}</h3>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="list-group">
|
||||
{% for tag_slug in facet_tag_slug_values %}
|
||||
{% if tag_slug.0 in current %}
|
||||
{# TODO this does not remove parameters #}
|
||||
<a href="{% add_query_param request facet_tag_slug_key tag_slug.0 %}" class="list-group-item active">
|
||||
<span class="glyphicon glyphicon-ok" style="float: right" aria-hidden="true"></span> {{ tag_slug.1 }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% add_query_param request facet_tag_slug_key tag_slug.0 %}" class="list-group-item">{{ tag_slug.1 }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
12
starfields_drf_generics/templates/filters/slug.html
Normal file
12
starfields_drf_generics/templates/filters/slug.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% load i18n %}
|
||||
<h2>{% trans "Slug" %}</h2>
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" style="width: 350px" name="{{ param }}" value="{{ term }}">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span> {% trans "Slug" %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
Reference in New Issue
Block a user