Skip to content

Commit

Permalink
Merge pull request #231 from NickolausDS/api-docs
Browse files Browse the repository at this point in the history
Add docs for Generic Class-Based views
  • Loading branch information
NickolausDS authored Oct 29, 2024
2 parents 2e28b76 + 8f6a3f1 commit 18e8e59
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 59 deletions.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ workflow.
reference/settings-example
reference/urls
reference/views
reference/generic-views
reference/local-settings
reference/deployment
reference/migration
Expand Down
46 changes: 46 additions & 0 deletions docs/source/reference/generic-views.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.. _generic_views_reference:

Generic Class-Based Views
=========================

Generic views allow for more fine-grained customization of Django Globus Portal Framework viwes.
They are built on Django Generic Class-Based views. DGPF generic views should be inherited with
specific desired functionality overridden. The URL conf also needs to be updated to override the
original DGPF views.

An example here shows usage of a class-based view which requires login and allows extending filters:

.. code-block:: python
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from globus_portal_framework.views.generic import SearchView
class MyCustomSearchView(LoginRequiredMixin, SearchView):
@property
def filters(self):
"""Allow custom default_filters per-index in settings.py"""
return super().filters + self.get_index_info().get('default_filters', [])
And here shows overriding the built-in DPGF Search View in ``urls.py``:

.. code-block:: python
# urls.py
from django.urls import path, include
import globus_portal_framework.urls # Allows index converter usage
from testportal.views import MyCustomSearchView
urlpatterns = [
path("<index:index>/", MyCustomSearchView.as_view(), name="search"),
# Note, you must define your custom view above the originals for Django to use it!
path("", include("globus_portal_framework.urls")),
]
.. automodule:: globus_portal_framework.views.generic
:members:
:member-order: bysource
:show-inheritance:
152 changes: 93 additions & 59 deletions globus_portal_framework/views/generic.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import logging
import typing as t
from urllib.parse import urlparse
import django
from django.views.generic import View
from django.conf import settings
from django.shortcuts import render
from django.contrib import messages
import globus_sdk
from globus_portal_framework.gsearch import (
get_template, get_search_filters, get_facets,
get_search_query, process_search_data, get_index, prepare_search_facets,
get_pagination, get_subject,
)
from globus_portal_framework.gclients import (
load_search_client
get_template,
get_search_filters,
get_facets,
get_search_query,
process_search_data,
get_index,
prepare_search_facets,
get_pagination,
get_subject,
)
from globus_portal_framework.gclients import load_search_client
import globus_portal_framework.exc


Expand All @@ -33,103 +40,122 @@ class SearchView(View):
rendered by the template.
"""

DEFAULT_TEMPLATE = 'globus-portal-framework/v2/search.html'
DEFAULT_TEMPLATE = "globus-portal-framework/v2/search.html"

def __init__(self, template=None, results_per_page=10):
def __init__(self, template: str = None, results_per_page: int = 10):
super().__init__()
self.template = template or self.DEFAULT_TEMPLATE
self.results_per_page = results_per_page

@property
def query(self):
def query(self) -> t.Mapping[str, str]:
"""Process the query using ``globus_portal_framework.gsearch.get_search_query``"""
return get_search_query(self.request)

@property
def filters(self):
def filters(self) -> t.Mapping[str, str]:
"""Get filters using ``globus_portal_framework.gsearch.get_search_filters``"""
return get_search_filters(self.request)

@property
def facets(self):
index = self.kwargs.get('index')
def facets(self) -> t.Mapping[str, str]:
"""Get facets using ``globus_portal_framework.gsearch.prepare_search_facets``"""
index = self.kwargs.get("index")
if index:
return prepare_search_facets(get_index(index).get('facets'))
return prepare_search_facets(get_index(index).get("facets"))
return []

@property
def page(self):
return self.request.GET.get('page', 1)
def page(self) -> int:
"""Get the query param for the user's desired page, or 1 if not specified"""
return self.request.GET.get("page", 1)

@property
def offset(self):
def offset(self) -> int:
"""Get the calculated offset based on the page number and configured
results-per-page. This value is sent in the request to Globus Search"""
return (int(self.page) - 1) * self.results_per_page

@property
def sort(self):
return self.get_index_info().get('sort', [])
def sort(self) -> list:
"""Get any index defined sort options defined per-index in settings.py. Default unsorted."""
return self.get_index_info().get("sort", [])

def get_index_info(self, index_uuid=None):
def get_index_info(self, index_uuid: str = None) -> t.Mapping[str, str]:
"""Fetch info on an index defined in settings.py"""
return get_index(index_uuid or self.kwargs.get('index'))
return get_index(index_uuid or self.kwargs.get("index"))

def get_search_client(self):
def get_search_client(self) -> globus_sdk.SearchClient:
"""Fetch a live search client, either loaded with user credentials or generic if not logged in."""
return load_search_client(self.request.user)

def post_search(self, client, index_uuid, search_client_data):
def post_search(
self,
client: globus_sdk.SearchClient,
index_uuid: str,
search_client_data: t.Mapping[str, str],
) -> t.Mapping[str, str]:
"""If you want to inject or modify any parameters in the
globus_sdk.SearchClient.post_search function, you can override this
function. """
function."""
return client.post_search(index_uuid, search_client_data)

def set_search_session_data(self, index):
def set_search_session_data(self, index: str):
"""Set some metadata about the search in the user's session. This will
record some data about their last search to fill in some basic DGPF
context, such as the 'Back To Search' link on a result detail page.
"""
self.request.session['search'] = {
'full_query': urlparse(self.request.get_full_path()).query,
'query': self.query,
'filters': self.filters,
'index': index,
self.request.session["search"] = {
"full_query": urlparse(self.request.get_full_path()).query,
"query": self.query,
"filters": self.filters,
"index": index,
}

def process_result(self, index_info, search_result):
def process_result(
self, index_info: t.Mapping[str, str], search_result: t.Mapping[str, str]
) -> t.Mapping[str, str]:
"""
Process the result from Globus Search into data ready to be rendered
into search templates.
"""
return {
'search': {
'search_results': process_search_data(
index_info.get('fields', []), search_result.data['gmeta']),
'facets': get_facets(
search_result, index_info.get('facets', []), self.filters,
index_info.get('filter_match'),
index_info.get('facet_modifiers')),
'pagination': get_pagination(
search_result.data['total'], search_result.data['offset']),
'count': search_result.data['count'],
'offset': search_result.data['offset'],
'total': search_result.data['total'],
"search": {
"search_results": process_search_data(
index_info.get("fields", []), search_result.data["gmeta"]
),
"facets": get_facets(
search_result,
index_info.get("facets", []),
self.filters,
index_info.get("filter_match"),
index_info.get("facet_modifiers"),
),
"pagination": get_pagination(
search_result.data["total"], search_result.data["offset"]
),
"count": search_result.data["count"],
"offset": search_result.data["offset"],
"total": search_result.data["total"],
}
}

def get_context_data(self, index):
def get_context_data(self, index: str) -> t.Mapping[str, str]:
"""calls post_search and process_result. If there is an error, returns
a context with a single 'error' var and logs the exception."""
data = {
'q': self.query,
'filters': self.filters,
'facets': self.facets,
'offset': self.offset,
'sort': self.sort,
'limit': self.results_per_page,
"q": self.query,
"filters": self.filters,
"facets": self.facets,
"offset": self.offset,
"sort": self.sort,
"limit": self.results_per_page,
}
try:
index_info = self.get_index_info(index)
result = self.post_search(self.get_search_client(),
index_info['uuid'], data)
result = self.post_search(
self.get_search_client(), index_info["uuid"], data
)
return self.process_result(index_info, result)
except globus_portal_framework.exc.ExpiredGlobusToken:
# Don't catch this exception. Middleware will automatically
Expand All @@ -139,10 +165,12 @@ def get_context_data(self, index):
if settings.DEBUG:
raise
log.exception(e)
return {'error': 'There was an error in your search, please try a '
'different query or contact your administrator.'}
return {
"error": "There was an error in your search, please try a "
"different query or contact your administrator."
}

def get(self, request, index, *args, **kwargs):
def get(self, request: django.http.HttpRequest, index: str, *args, **kwargs):
"""
Fetches the context, then renders a page. Calls 'get_template', which
checks to see if there is an overridden page for a given index. If
Expand All @@ -154,24 +182,30 @@ def get(self, request, index, *args, **kwargs):
"""
context = self.get_context_data(index)
self.set_search_session_data(index)
error = context.get('error')
error = context.get("error")
if error:
messages.error(request, error)
template = get_template(index, self.template)
log.debug(f'Using template {template}')
log.debug(f"Using template {template}")
return render(request, template, context)


class DetailView(View):
DEFAULT_TEMPLATE = 'globus-portal-framework/v2/detail-overview.html'
"""Show a single detail page to a user."""

DEFAULT_TEMPLATE = "globus-portal-framework/v2/detail-overview.html"

def __init__(self, template=None):
super().__init__()
self.template = template or self.DEFAULT_TEMPLATE

def get_context_data(self, index, subject):
def get_context_data(self, index: str, subject: str) -> t.Mapping[str, str]:
"""Call globus_portal_framework.gsearch.get_subject using the index, subject, and user
and return the result."""
return get_subject(index, subject, self.request.user)

def get(self, request, index, subject):
def get(self, request: django.http.HttpRequest, index: str, subject: str):
"""Get context data, and return a rendered search view, selecting the template with
globus_portal_framework.gsearch.get_template."""
context = self.get_context_data(index, subject)
return render(request, get_template(index, self.template), context)

0 comments on commit 18e8e59

Please sign in to comment.