[ADD] HORILLA_API: Add swagger

This commit is contained in:
Horilla
2025-12-23 15:01:24 +05:30
parent 641a4d4842
commit f4bedaca4d
21 changed files with 1049 additions and 707 deletions

View File

@@ -1,16 +1,16 @@
"""
ASGI config for horilla project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "horilla.settings")
application = get_asgi_application()
"""
ASGI config for horilla project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "horilla.settings")
application = get_asgi_application()

View File

@@ -106,6 +106,7 @@ NO_PERMISSION_MODALS = [
"recruitmentgeneralsetting",
"resume",
"recruitmentmailtemplate",
"profileeditfeature",
]
if settings.env("AWS_ACCESS_KEY_ID", default=None):
@@ -115,7 +116,6 @@ if settings.env("AWS_ACCESS_KEY_ID", default=None):
AWS_S3_REGION_NAME = settings.env("AWS_S3_REGION_NAME")
DEFAULT_FILE_STORAGE = settings.env("DEFAULT_FILE_STORAGE")
AWS_S3_ADDRESSING_STYLE = settings.env("AWS_S3_ADDRESSING_STYLE")
AWS_S3_ENDPOINT_URL = settings.env("AWS_S3_ENDPOINT_URL", default=None)
settings.AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID
settings.AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY
@@ -123,7 +123,6 @@ if settings.env("AWS_ACCESS_KEY_ID", default=None):
settings.AWS_S3_REGION_NAME = AWS_S3_REGION_NAME
settings.DEFAULT_FILE_STORAGE = DEFAULT_FILE_STORAGE
settings.AWS_S3_ADDRESSING_STYLE = AWS_S3_ADDRESSING_STYLE
settings.AWS_S3_ENDPOINT_URL = AWS_S3_ENDPOINT_URL
if settings.env("AWS_ACCESS_KEY_ID", default=None) and "storages" in INSTALLED_APPS:

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ REST_FRAMEWORK_SETTINGS = {
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"DEFAULT_AUTHENTICATION_CLASSES": (
"horilla_api.auth.RejectBasicAuthentication",
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"PAGE_SIZE": 20,
@@ -32,13 +33,9 @@ SWAGGER_SETTINGS = {
"name": "Authorization",
"in": "header",
"description": "Enter your Bearer token here",
},
"Basic": {
"type": "basic",
"description": "Basic authentication. Enter your username and password.",
},
}
},
"SECURITY": [{"Bearer": []}, {"Basic": []}],
"SECURITY": [{"Bearer": []}],
}
# Inject the REST framework settings into the Django project settings
setattr(settings, "REST_FRAMEWORK", REST_FRAMEWORK_SETTINGS)

View File

@@ -79,6 +79,7 @@ MIDDLEWARE = [
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"horilla_api.middleware.RejectBasicAuthMiddleware",
"corsheaders.middleware.CorsMiddleware",
"simple_history.middleware.HistoryRequestMiddleware",
"django.middleware.locale.LocaleMiddleware",

View File

@@ -37,6 +37,7 @@ urlpatterns = [
path("", include("horilla_views.urls")),
path("employee/", include("employee.urls")),
path("horilla-widget/", include("horilla_widgets.urls")),
path("api/", include("horilla_api.urls")),
re_path(
"^inbox/notifications/", include(notifications.urls, namespace="notifications")
),

View File

@@ -1,16 +1,16 @@
"""
WSGI config for horilla project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "horilla.settings")
application = get_wsgi_application()
"""
WSGI config for horilla project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "horilla.settings")
application = get_wsgi_application()

View File

@@ -0,0 +1,79 @@
# Horilla API Documentation
This document provides information on how to use and maintain the API documentation for Horilla HRMS.
## Accessing API Documentation
The API documentation is available at the following URLs:
- **Swagger UI**: `/api/swagger/` - Interactive documentation with testing capabilities
- **ReDoc**: `/api/redoc/` - Clean, responsive documentation for easier reading
- **JSON Schema**: `/api/swagger.json` - Raw schema for integration with other tools
## Features
- **Interactive Documentation**: Test API endpoints directly from the browser
- **Module Organization**: Endpoints are grouped by module (employee, attendance, etc.)
- **Authentication Support**: Bearer (JWT) only. Basic authentication is disabled.
- **Request/Response Examples**: Clear examples of data formats
- **Versioning**: API versions are clearly indicated
## For Developers: Adding Documentation to New Endpoints
### Using the `document_api` Decorator
```python
from horilla_api.docs import document_api
class MyAPIView(APIView):
@document_api(
operation_description="Description of what this endpoint does",
request_body=MyRequestSerializer,
responses={
200: MyResponseSerializer,
400: "Bad request error description"
},
tags=['Module Name']
)
def get(self, request):
# Your view logic here
pass
```
### Common Parameters
For paginated list views:
```python
@document_api(
operation_description="List all items with pagination",
responses={200: MySerializer(many=True)},
query_params='paginated'
)
```
## Maintaining Documentation
- Documentation automatically updates when API endpoints change
- Security schemes (Bearer/JWT) are configured in `rest_conf.py`
- Basic authentication has been removed across all interfaces`
- Module tags are defined in `horilla_api/docs.py`
## Testing Documentation
1. Start the development server
2. Navigate to `/api/swagger/` or `/api/redoc/`
3. Verify all endpoints are properly documented
4. Test authentication flows
5. Check that all modules are properly organized
## Troubleshooting
If endpoints are not appearing in documentation:
- Ensure the URL is included in the `patterns` list in `horilla_api/urls.py`
- Check that the view is using proper decorators
- Verify the serializer is correctly defined
If you see a Basic authorization option in Swagger:
- Clear your browser cache and refresh `/api/swagger/`
- Confirm `SWAGGER_SETTINGS` only contains the `Bearer` scheme

View File

@@ -2,3 +2,6 @@ from horilla.settings import INSTALLED_APPS
INSTALLED_APPS.append("geofencing")
INSTALLED_APPS.append("facedetection")
# Import Swagger settings to ensure they're applied
from . import swagger_settings

View File

@@ -12,3 +12,10 @@ class GetEmployeeSerializer(serializers.ModelSerializer):
def get_full_name(self, obj):
return obj.get_full_name()
class LoginRequestSerializer(serializers.Serializer):
"""Simple request body for the login endpoint."""
username = serializers.CharField()
password = serializers.CharField()

View File

@@ -1,12 +1,54 @@
from django.contrib.auth import authenticate
from drf_yasg import openapi
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from ...api_serializers.auth.serializers import GetEmployeeSerializer
from horilla_api.docs import document_api
from ...api_serializers.auth.serializers import (
GetEmployeeSerializer,
LoginRequestSerializer,
)
class LoginAPIView(APIView):
@document_api(
operation_description="Authenticate user and return JWT access token with employee info",
request_body=LoginRequestSerializer,
responses={
200: openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"employee": openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"id": openapi.Schema(type=openapi.TYPE_INTEGER),
"full_name": openapi.Schema(type=openapi.TYPE_STRING),
"employee_profile": openapi.Schema(
type=openapi.TYPE_STRING,
description="Profile image URL",
),
},
),
"access": openapi.Schema(
type=openapi.TYPE_STRING, description="JWT access token"
),
"face_detection": openapi.Schema(type=openapi.TYPE_BOOLEAN),
"face_detection_image": openapi.Schema(
type=openapi.TYPE_STRING,
description="Face detection image URL",
nullable=True,
),
"geo_fencing": openapi.Schema(type=openapi.TYPE_BOOLEAN),
"company_id": openapi.Schema(
type=openapi.TYPE_INTEGER, nullable=True
),
},
),
},
tags=["auth"],
)
def post(self, request):
if "username" and "password" in request.data.keys():
username = request.data.get("username")

View File

@@ -0,0 +1,29 @@
"""
Example API view with documentation
"""
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from horilla_api.docs import document_api
class ExampleDocumentedView(APIView):
"""
Example view demonstrating API documentation
"""
@document_api(
operation_description="Get API documentation example",
responses={
200: "Example response with documentation",
404: "Not found error"
},
tags=['Documentation Example']
)
def get(self, request):
"""
Example GET method with documentation
"""
return Response(
{"message": "API documentation is working correctly"},
status=status.HTTP_200_OK
)

View File

@@ -1,3 +1,5 @@
from typing import Any
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend
@@ -163,7 +165,7 @@ class DepartmentView(APIView):
departments = Department.objects.all()
paginator = PageNumberPagination()
page = paginator.paginate_queryset(departments, request)
page: list[Any] | None = paginator.paginate_queryset(departments, request)
serializer = self.serializer_class(page, many=True)
return paginator.get_paginated_response(serializer.data)

View File

@@ -6,11 +6,8 @@ class HorillaApiConfig(AppConfig):
name = "horilla_api"
def ready(self):
from django.urls import include, path
from horilla.urls import urlpatterns
urlpatterns.append(
path("api/", include("horilla_api.urls")),
)
super().ready()
"""
Initialize API documentation when the app is ready
"""
# Import and register API documentation components
import horilla_api.schema # noqa

39
horilla_api/auth.py Normal file
View File

@@ -0,0 +1,39 @@
"""
Authentication utilities for the API
"""
from rest_framework import authentication
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.exceptions import AuthenticationFailed
class SwaggerAuthentication(authentication.BaseAuthentication):
"""
Custom authentication class for Swagger UI
"""
def authenticate(self, request):
# Get the authentication header
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
# Try JWT authentication first
if auth_header.startswith('Bearer '):
jwt_auth = JWTAuthentication()
try:
return jwt_auth.authenticate(request)
except:
pass
# Fall back to session authentication
if request.user and request.user.is_authenticated:
return (request.user, None)
return None
class RejectBasicAuthentication(authentication.BaseAuthentication):
"""
Explicitly reject HTTP Basic Auth across the API with a clear error message.
"""
def authenticate(self, request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if auth_header.startswith('Basic '):
raise AuthenticationFailed('Basic authentication is disabled. Use Bearer token (JWT) in the Authorization header.')
return None

20
horilla_api/decorators.py Normal file
View File

@@ -0,0 +1,20 @@
"""
Decorators for API views
"""
from functools import wraps
from rest_framework.response import Response
from rest_framework import status
def api_authentication_required(view_func):
"""
Decorator to ensure API views require authentication
"""
@wraps(view_func)
def wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated:
return Response(
{"detail": "Authentication credentials were not provided."},
status=status.HTTP_401_UNAUTHORIZED
)
return view_func(request, *args, **kwargs)
return wrapped_view

88
horilla_api/docs.py Normal file
View File

@@ -0,0 +1,88 @@
"""
Documentation helpers for API views
"""
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework import authentication
# Module tags for organizing endpoints
MODULE_TAGS = {
'auth': 'Authentication',
'asset': 'Asset Management',
'base': 'Base',
'employee': 'Employee Management',
'notifications': 'Notifications',
'payroll': 'Payroll',
'attendance': 'Attendance',
'leave': 'Leave Management',
}
# Common response schemas
error_response = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'error': openapi.Schema(type=openapi.TYPE_STRING),
'detail': openapi.Schema(type=openapi.TYPE_STRING),
}
)
success_response = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'success': openapi.Schema(type=openapi.TYPE_BOOLEAN),
'message': openapi.Schema(type=openapi.TYPE_STRING),
}
)
# Common parameters
pagination_params = [
openapi.Parameter('page', openapi.IN_QUERY, description="Page number", type=openapi.TYPE_INTEGER),
openapi.Parameter('page_size', openapi.IN_QUERY, description="Number of results per page", type=openapi.TYPE_INTEGER),
]
def document_api(
operation_description=None,
request_body=None,
responses=None,
query_params=None,
tags=None,
manual_parameters=None,
**kwargs
):
"""
Decorator for documenting API views with authentication
Example usage:
@document_api(
operation_description="List all employees",
responses={200: EmployeeSerializer(many=True)},
tags=['Employee']
)
def get(self, request):
...
"""
# Add pagination parameters for list views
if manual_parameters is None and query_params == 'paginated':
manual_parameters = pagination_params
# Add common error responses
if responses and 400 not in responses:
responses[400] = error_response
if responses and 401 not in responses:
responses[401] = error_response
if responses and 403 not in responses:
responses[403] = error_response
# Add security requirement (Bearer only)
security = [{'Bearer': []}]
return swagger_auto_schema(
operation_description=operation_description,
request_body=request_body,
responses=responses,
manual_parameters=manual_parameters,
tags=tags,
security=security,
**kwargs
)

23
horilla_api/middleware.py Normal file
View File

@@ -0,0 +1,23 @@
from django.http import JsonResponse
class RejectBasicAuthMiddleware:
"""
Middleware that rejects HTTP Basic Authentication globally with a consistent message.
This ensures endpoints that override DRF authentication classes still reject Basic.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if isinstance(auth_header, str) and auth_header.startswith('Basic '):
return JsonResponse(
{
"error": "Basic authentication is disabled",
"detail": "Use Bearer token (JWT) in the Authorization header."
},
status=401
)
return self.get_response(request)

86
horilla_api/schema.py Normal file
View File

@@ -0,0 +1,86 @@
"""
Schema configuration for API documentation
"""
from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import swagger_auto_schema
from drf_yasg.generators import OpenAPISchemaGenerator
class ModuleTaggingAutoSchema(SwaggerAutoSchema):
"""
Custom schema generator that automatically tags operations based on their module
"""
def get_tags(self, operation_keys):
# Extract module name from the operation keys
if len(operation_keys) > 1:
# Use the first part of the URL path as the tag (e.g., 'employee', 'attendance')
return [operation_keys[0]]
return super().get_tags(operation_keys)
class OrderedTagSchemaGenerator(OpenAPISchemaGenerator):
"""
Custom schema generator to enforce tag ordering.
Places 'auth' first, followed by remaining tags sorted alphabetically.
"""
def get_schema(self, request=None, public=False):
schema = super().get_schema(request=request, public=public)
# Collect all tag names used in operations
tag_names = set()
for path_item in schema.paths.values():
for method_name in ("get", "put", "post", "delete", "options", "head", "patch", "trace"):
operation = getattr(path_item, method_name, None)
if operation and getattr(operation, "tags", None):
for t in operation.tags:
if t:
tag_names.add(t)
# Desired order: 'auth' first, then others alphabetically
ordered_names = ["auth"] + sorted([t for t in tag_names if t != "auth"])
# Build top-level tags list in the specified order
schema.tags = [{"name": name} for name in ordered_names]
return schema
def api_doc(**kwargs):
"""
Decorator for documenting API views
Example usage:
@api_doc(
responses={200: EmployeeSerializer(many=True)},
operation_description="List all employees",
tags=['Employee']
)
def get(self, request):
...
"""
return swagger_auto_schema(**kwargs)
# Common response schemas
error_response = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'error': openapi.Schema(type=openapi.TYPE_STRING),
'detail': openapi.Schema(type=openapi.TYPE_STRING),
}
)
success_response = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'success': openapi.Schema(type=openapi.TYPE_BOOLEAN),
'message': openapi.Schema(type=openapi.TYPE_STRING),
}
)
# Common parameters
pagination_params = [
openapi.Parameter('page', openapi.IN_QUERY, description="Page number", type=openapi.TYPE_INTEGER),
openapi.Parameter('page_size', openapi.IN_QUERY, description="Number of results per page", type=openapi.TYPE_INTEGER),
]
# Security definitions are already configured in rest_conf.py

View File

@@ -0,0 +1,30 @@
"""
Custom Swagger settings for the API
"""
from django.conf import settings
# Define security definitions for Swagger UI
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header',
'description': 'JWT Token Authentication: Enter your token with the "Bearer " prefix, e.g. "Bearer abcde12345"'
}
},
'USE_SESSION_AUTH': False,
'DEFAULT_UI_SETTINGS': {
# Keep tag order as defined in the generated spec
'tagsSorter': 'none'
},
'SECURITY_REQUIREMENTS': [
{'Bearer': []}
],
}
# Apply settings
if hasattr(settings, 'SWAGGER_SETTINGS'):
settings.SWAGGER_SETTINGS.update(SWAGGER_SETTINGS)
else:
setattr(settings, 'SWAGGER_SETTINGS', SWAGGER_SETTINGS)

View File

@@ -1,6 +1,39 @@
from django.conf import settings
from django.urls import include, path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
from horilla_api.schema import OrderedTagSchemaGenerator
# Create schema view for Swagger and ReDoc
schema_view = get_schema_view(
openapi.Info(
title="Horilla API",
default_version="v1",
description="API documentation for Horilla HRMS. Click the 'Authorize' button at the top to authenticate.",
terms_of_service="https://www.horilla.com/terms/",
contact=openapi.Contact(email="contact@horilla.com"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
generator_class=OrderedTagSchemaGenerator,
)
urlpatterns = [
# API Documentation URLs
path(
"swagger<format>/", schema_view.without_ui(cache_timeout=0), name="schema-json"
),
path(
"swagger/",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
path("docs/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-docs"),
# API Endpoints (static configuration)
path("auth/", include("horilla_api.api_urls.auth.urls")),
path("asset/", include("horilla_api.api_urls.asset.urls")),
path("base/", include("horilla_api.api_urls.base.urls")),