Metadata-Version: 2.3
Name: smoothglue_calendar
Version: 1.0.1
Summary: SmoothGlue Calendar: A reusable Django app for calendar functionality.
License: Proprietary
Keywords: Interoperability,API Integration,Edge Computing,Application Framework,Data-Driven Development,Django,Calendar
Author: BrainGu
Author-email: smoothglue@braingu.com
Requires-Python: >=3.12,<4.0
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: Django (>=4.2.11)
Requires-Dist: djangorestframework (>=3.14.0,<4.0.0)
Requires-Dist: drf-spectacular (>=0.28.0,<0.29.0)
Requires-Dist: smoothglue-django-core (>=1.3.1,<2.0.0)
Project-URL: Homepage, https://braingu.com/solutions/smoothglue/
Project-URL: issues, https://braingu.com/support
Description-Content-Type: text/markdown

# SmoothGlue Calendar

`smoothglue_calendar` is a Django app that provides a flexible and extensible calendar solution for the SmoothGlue ecosystem. It allows you to create and manage calendar events, define custom event types, and link events to any other Django model.

By attaching any object to an `CalendarEvent` object approach, the event start-end times will be centralized in a single model/table, making it easier and faster to query the event data. This approach allows faster deconfliction of scheduling data since everything is in the same table.

## Features

- **Calendar Events**: Create, retrieve, update, and delete calendar events with start and end times, a name, and a description.
- **Custom Event Types**: Define your own event types to categorize events.
- **Generic Relations**: Link calendar events to any other Django model using generic foreign keys.
- **Dynamic Serializers**: Specify a serializer for each event type to control the representation of the related object.
- **Filtering**: Filter events by `event_type` and for overlapping date ranges.



## Model ERD for SmoothGlue Calendar


```mermaid
erDiagram
    CalendarEvent {
        UUID id PK
        string event_name
        datetime start_time
        datetime end_time
        UUID event_type_id FK
        integer content_type_id FK
        UUID object_id
    }
    EventType {
        UUID id PK
        string name
    }    EventType ||--|{ CalendarEvent : "is of type"
    CalendarEvent }o--|| Meeting : "can point to"
    CalendarEvent }o--|| PlatformUser : "can point to"
    CalendarEvent }o--|| Conference : "can point to"
    CalendarEvent }o--|| AnyOtherModel : "can point to"
```


## Installation

1.  Add `smoothglue_calendar` to your `INSTALLED_APPS` in your Django settings file:

    ```python
    INSTALLED_APPS = [
        ...
        'smoothglue.calendar',
        ...
    ]
    ```

2.  Include the calendar's URLs in your project's `urls.py`:

    ```python
    from django.urls import include, path

    urlpatterns = [
        ...
        path('api/calendar/', include('smoothglue_calendar.urls')),
        ...
    ]
    ```

3.  Run migrations to create the calendar models:

    ```bash
    python manage.py migrate smoothglue_calendar
    ```

## Usage

### API Endpoints

- `api/calendar/calendar-events/`:
  - `GET`: List all calendar events.
    - Query Parameters:
      - `event_type`: Filter by the UUID of an `EventType`.
      - `range_start`: The start of a date range (ISO 8601 format).
      - `range_end`: The end of a date range (ISO 8601 format).
      - `object_id`: Filter by the UUID of a related object.
      - `year`: Filter by year on either the `start_time` or `end_time`.
      - `month`: Filter by month (1-12) on either the `start_time` or `end_time`.
      - `day`: Filter by day (1-31) on either the `start_time` or `end_time`.
        *The `range_start` and `range_end` parameters will return any events that overlap with the specified date range.*
  - `POST`: Create a new calendar event.
- `api/calendar/calendar-events/{id}/`:
  - `GET`: Retrieve a specific calendar event.
  - `PUT`/`PATCH`: Update a specific calendar event.
  - `DELETE`: Delete a specific calendar event.
  - `POST /archive/`: Archives the event.
  - `POST /unarchive/`: Unarchives the event.
- `api/calendar/event-types/`:
  - `GET`: List all event types.
  - `POST`: Create a new event type.

### Creating Custom Event Types with Fixtures

You can create custom event types by loading a fixture file. This is useful for pre-populating your database with the event types your application needs.

To allow other applications to link their models to calendar events, you can specify a `content_type_serializer` for an `EventType`. This serializer will be used to represent the related object in the `CalendarEvent` API responses.

**Example Fixture (`event_types.json`):**

```json
[
  {
    "model": "smoothglue_calendar.eventtype",
    "pk": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "fields": {
      "name": "Meeting",
      "description": "A standard meeting event.",
      "content_type_serializer": "some_app.serializers.MeetingSerializer"
    }
  },
  {
    "model": "smoothglue_calendar.eventtype",
    "pk": "b2c3d4e5-f6a7-8901-2345-67890abcdef1",
    "fields": {
      "name": "Exercise",
      "description": "A training exercise event."
    }
  }
]
```

To load the fixture, run the following command:

```bash
python manage.py loaddata event_types.json
```

This will create the "Meeting" and "Exercise" event types. The "Meeting" event type is configured to use the `MeetingSerializer` to represent its related objects, allowing for rich, domain-specific data to be included in the calendar API.

## Advanced Configuration

### Pluggable Queryset

The `CalendarEventViewSet` supports a pluggable `get_queryset` implementation, allowing you to customize the queryset based on your application's needs, such as applying custom permissions or prefetching related data.

To use a custom queryset function, define `SG_CALENDAR_SETTINGS` in your Django settings file:

```python
SG_CALENDAR_SETTINGS = {
    "CALENDAR_VIEWSET_QUERYSET_FN": "path.to.your.custom_queryset_function"
}
```

Your custom function must accept two arguments: the `request` object and the `default_queryset`. It should return a `QuerySet` object.

**Example Custom Function:**

```python
def get_events_for_user(request, default_queryset):
    return default_queryset.filter(created_by=request.user)
```

### Pluggable Permissions

Similar to the queryset, you can inject custom permission classes into the `CalendarEventViewSet`. This allows you to enforce specific access controls, such as role-based access or object-level permissions.

To use custom permission classes, update `SG_CALENDAR_SETTINGS` in your Django settings file:

```python
SG_CALENDAR_SETTINGS = {
    "CALENDAR_VIEWSET_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
        "path.to.your.CustomPermission",
    ]
}
```

**Example Custom Permission:**

```python
from rest_framework import permissions

class CalendarEventPermission(permissions.BasePermission):
    """
    A single, consolidated permission class for the CalendarEvent model.
    """

    def has_permission(self, request, view):
        """
        Handles model-level permissions (for list/create actions).
        """
        if not request.user or not request.user.is_authenticated:
            return False

        if view.action == "list":
            return request.user.has_perm("your_app.view_calendar")

        if view.action == "create":
            event_type_name = request.data.get("event_type_name")
            if event_type_name == "Personnel":
                self.message = "You do not have permission to add new personnel events."
                return request.user.has_perm("your_app.add_personnel_event")
            
            return request.user.has_perm("your_app.add_event")

        return True

    def has_object_permission(self, request, view, obj):
        """
        Dispatches to the appropriate helper method based on the action.
        """
        action_handlers = {
            "retrieve": self._can_retrieve,
            "update": self._can_update,
            "partial_update": self._can_update,
            "destroy": self._can_archive,
            "archive": self._can_archive,
            "unarchive": self._can_archive,
        }

        handler = action_handlers.get(view.action)
        if handler:
            return handler(request, obj)

        return False

    def _can_retrieve(self, request, obj):
        return request.user.has_perm("your_app.view_calendar", obj)

    def _can_update(self, request, obj):
        if obj.event_type.name == "Personnel":
            return request.user.has_perm("your_app.change_personnel_event", obj)
        return request.user.has_perm("your_app.change_event", obj)

    def _can_archive(self, request, obj):
        if obj.event_type.name == "Personnel":
            return request.user.has_perm("your_app.archive_personnel_event", obj)
        return request.user.has_perm("your_app.archive_event", obj)
```

