Skip to content
Snippets Groups Projects
Verified Commit 51b8c1a9 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'master' into calendar-object-feeds

parents b75f59ff 2c3d1518
No related branches found
No related tags found
1 merge request!1148Calendar events and iCal feeds
Pipeline #137875 canceled
...@@ -24,6 +24,7 @@ Added ...@@ -24,6 +24,7 @@ Added
* [Dev] Options for filtering and sorting of GraphQL queries at the server. * [Dev] Options for filtering and sorting of GraphQL queries at the server.
* [Dev] Managed models for instances handled by other apps. * [Dev] Managed models for instances handled by other apps.
* [Dev] Upload slot sytem for out-of-band uploads in GraphQL clients * [Dev] Upload slot sytem for out-of-band uploads in GraphQL clients
* Generic endpoint for retrieving objects as JSON
Changed Changed
~~~~~~~ ~~~~~~~
......
...@@ -608,6 +608,11 @@ class RegistryObject: ...@@ -608,6 +608,11 @@ class RegistryObject:
return cls.registered_objects_dict.get(name) return cls.registered_objects_dict.get(name)
class ObjectAuthenticator(RegistryObject):
def authenticate(self, request, obj):
raise NotImplementedError()
class CalendarEventMixin(RegistryObject): class CalendarEventMixin(RegistryObject):
"""Mixin for calendar feeds. """Mixin for calendar feeds.
...@@ -755,3 +760,6 @@ class CalendarEventMixin(RegistryObject): ...@@ -755,3 +760,6 @@ class CalendarEventMixin(RegistryObject):
def valid_feed_names(cls): def valid_feed_names(cls):
"""Return a list of valid feed names.""" """Return a list of valid feed names."""
return [feed.name for feed in cls.valid_feeds] return [feed.name for feed in cls.valid_feeds]
def get_object_by_name(cls, name):
return cls.registered_objects_dict.get(name)
...@@ -42,6 +42,21 @@ urlpatterns = [ ...@@ -42,6 +42,21 @@ urlpatterns = [
), ),
path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")), path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("system_status/", views.SystemStatusAPIView.as_view(), name="system_status_api"), path("system_status/", views.SystemStatusAPIView.as_view(), name="system_status_api"),
path(
"o/<str:app_label>/<str:model>/<int:pk>/",
views.ObjectRepresentationView.as_view(),
name="object_representation_with_pk",
),
path(
"o/<str:app_label>/<str:model>/",
views.ObjectRepresentationView.as_view(),
name="object_representation_with_model",
),
path(
"o/",
views.ObjectRepresentationView.as_view(),
name="object_representation_anonymous",
),
path("", include("django_prometheus.urls")), path("", include("django_prometheus.urls")),
path( path(
"django/", "django/",
......
...@@ -101,6 +101,7 @@ from .mixins import ( ...@@ -101,6 +101,7 @@ from .mixins import (
AdvancedDeleteView, AdvancedDeleteView,
AdvancedEditView, AdvancedEditView,
CalendarEventMixin, CalendarEventMixin,
ObjectAuthenticator,
SuccessNextMixin, SuccessNextMixin,
) )
from .models import ( from .models import (
...@@ -1556,6 +1557,78 @@ class LoggingGraphQLView(GraphQLView): ...@@ -1556,6 +1557,78 @@ class LoggingGraphQLView(GraphQLView):
return result return result
class ObjectRepresentationView(View):
"""View with unique URL to get a JSON representation of an object."""
def get_model(self, request: HttpRequest, app_label: str, model: str):
"""Get the model by app label and model name."""
try:
return apps.get_model(app_label, model)
except LookupError:
raise Http404()
def get_object(self, request: HttpRequest, app_label: str, model: str, pk: int):
"""Get the object by app label, model name and primary key."""
if getattr(self, "model", None) is None:
self.model = self.get_model(request, app_label, model)
try:
return self.model.objects.get(pk=pk)
except self.model.DoesNotExist:
raise Http404()
def get(
self,
request: HttpRequest,
app_label: Optional[str] = None,
model: Optional[str] = None,
pk: Optional[int] = None,
*args,
**kwargs,
) -> HttpResponse:
if app_label and model:
self.model = self.get_model(request, app_label, model)
else:
self.model = None
if app_label and model and pk:
self.object = self.get_object(request, app_label, model, pk)
else:
self.object = None
authenticators = request.GET.get("authenticators", "").split(",")
if authenticators == [""]:
authenticators = list(ObjectAuthenticator.registered_objects_dict.keys())
self.authenticate(request, authenticators)
if hasattr(self.object, "get_json"):
res = self.object.get_json(request)
else:
res = {"id": self.object.id}
res["_meta"] = {
"model": self.object._meta.model_name,
"app": self.object._meta.app_label,
}
return JsonResponse(res)
def authenticate(self, request: HttpRequest, authenticators: list[str]) -> bool:
"""Authenticate the request against the given authenticators."""
for authenticator in authenticators:
authenticator_class = ObjectAuthenticator.get_object_by_name(authenticator)
if not authenticator_class:
continue
obj = authenticator_class().authenticate(request, self.object)
if obj:
if self.object is None:
self.object = obj
elif obj != self.object:
raise BadRequest("Ambiguous objects identified")
return True
raise PermissionDenied()
class ICalFeedView(PermissionRequiredMixin, View): class ICalFeedView(PermissionRequiredMixin, View):
"""View to generate an iCal feed for a calendar.""" """View to generate an iCal feed for a calendar."""
......
...@@ -259,7 +259,7 @@ export default defineConfig({ ...@@ -259,7 +259,7 @@ export default defineConfig({
directoryIndex: null, directoryIndex: null,
navigateFallbackAllowlist: [ navigateFallbackAllowlist: [
new RegExp( new RegExp(
"^/(?!(django|admin|graphql|__icons__|oauth/authorize))[^.]*$" "^/(?!(django|admin|graphql|__icons__|oauth/authorize|o))[^.]*$"
), ),
], ],
additionalManifestEntries: [ additionalManifestEntries: [
...@@ -274,7 +274,7 @@ export default defineConfig({ ...@@ -274,7 +274,7 @@ export default defineConfig({
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp( urlPattern: new RegExp(
"^/(?!(django|admin|graphql|__icons__|oauth/authorize))[^.]*$" "^/(?!(django|admin|graphql|__icons__|oauth/authorize|o))[^.]*$"
), ),
handler: "CacheFirst", handler: "CacheFirst",
}, },
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment