diff --git a/README.md b/README.md new file mode 100644 index 0000000..3581cac --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# C3LF System3 + +the third try to automate lost&found organization for chaos events. not a complete rewrite, but instead building on top +of the web frontend of version 2. everything else is new but still API compatible. now with more monorepo. + +## Architecture + +C3LF System3 integrates a Django-Rest-Framework + WebSocket backend, Vue.js frontend SPA and a minimal LMTP mail server +integrated with the Django backend. It is additionally deployed with a Postfix mail server as Proxy in front of the +LMTP socket, a MariaDB database, a Redis cache and an Nginx reverse proxy that serves the static SPA frontend, proxies +the API requests to the backend and serves the media files in cooperation with the Django backend using the +`X-Accel-Redirect` header. + +The production deployment is automated using Ansible and there are some Docker Compose configurations for development. + +## Project Structure + +- `core/` Contains the Django backend with database models, API endpoints, migrations, API tests, and mail server + functionalities. +- `web/` Contains the Vue.js frontend application. +- `deploy/` Contains deployment configurations and Docker scripts for various development modes. + +For more information, see the README.md files in the respective directories. + +## Development Modes + +There are currently 4 development modes for this Project: + +- Frontend-Only +- Backend-API-Only +- Full-Stack-Lite 'dev' (docker) +- **[WIP]** Full-Stack 'testing' (docker) + +*Choose the one that is most suited to the feature you want to work on or ist easiest for you to set up ;)* + +For all modes it is assumed that you have `git` installed, have cloned the repository and are in the root directory of +the project. Use `git clone https://git.hannover.ccc.de/c3lf/c3lf-system-3.git` to get the official upstream repository. +The required packages for each mode are listed separately and also state the specific package name for Debian 12. + +### Frontend-Only + +This mode is for developing the frontend only. It uses the vue-cli-service (webpack) to serve the frontend and watches +for changes in the source code to provide hot reloading. The API requests are proxied to the staging backend. + +#### Requirements + +* Node.js (~20.19.0) (`nodejs`) +* npm (~9.2.0) (`npm`) + +*Note: The versions are not strict, but these are tested. Other versions might work as well.* + +#### Steps + +```bash +cd web +npm intall +npm run serve +``` + +Now you can access the frontend at `localhost:8080` and start editing the code in the `web` directory. +For more information, see the README.md file in the `web` directory. + +### Backend-API-Only + +This mode is for developing the backend API only. It also specifically excludes most WebSockets and mail server +functionalities. Use this mode to focus on the backend API and Database models. + +#### Requirements + +* Python (~3.11) (`python3`) +* pip (`python3-pip`) +* virtualenv (`python3-venv`) + +*Note: The versions are not strict, but these are tested. Other versions might work as well.* + +#### Steps + +``` +python -m venv venv +source venv/bin/activate +pip install -r core/requirements.dev.txt +cd core +python manage.py test +``` + +The tests should run successfully to start and you can now start the TDD workflow by adding new failing tests. +For more information about the backend and TDD, see the README.md file in the `core` directory. + +### Full-Stack-Lite 'dev' (docker) + +This mode is for developing the both frontend and backend backend at the same time in a containerized environment. It +uses the `docker-compose` command to build and run the application in a container. It specifically excludes all mail +server and most WebSocket functionalities. + +#### Requirements + +* Docker (`docker.io`) +* Docker Compose (`docker-compose`) + +*Note: Depending on your system, the `docker compose` command might be included in general `docker` or `docker-ce` +package, or you might want to use podman instead.* + +#### Steps + +```bash +docker-compose -f deploy/dev/docker-compose.yml up --build +``` + +The page should be available at [localhost:8080](http://localhost:8080) +This Mode provides a minimal set of testdata, including a user `testuser` with password `testuser`. The test dataset is +defined in deploy/testdata.py and can be extended there. + +You can now edit code in `/web` and `/core` and changes will be applied to the running page as soon as the file is +saved. + +For details about each part, read `/web/README.md` and `/core/README.md` respectively. To execute commands in the +container context use 'exec' like + +```bash +docker exec -it c3lf-sys3-dev-core-1 ./manage.py test` +``` + +### Full-Stack 'testing' (docker) + +**WORK IN PROGRESS** + +*will include postfix, mariadb, redis, nginx and the ability to test sending mails, receiving mail and websocket based +realiteme updates in the frontend. the last step in verification before deploying to the staging system using ansible* + +## Online Instances + +These are deployed using `deploy/ansible/playbooks/deploy-c3lf-sys3.yml` and follow a specific git branch. + +### 'live' + +| URL | [c3lf.de](https://c3lf.de) | +|----------------|----------------------------| +| **Branch** | live | +| **Host** | polaris.lab.or.it | +| **Debug Mode** | off | + +This is the **'production' system** and should strictly follow the staging system after all changes have been validated. + +### 'staging' + +| URL | [staging.c3lf.de](https://staging.c3lf.de) | +|----------------|--------------------------------------------| +| **Branch** | testing | +| **Host** | andromeda.lab.or.it | +| **Debug Mode** | on | + +This system ist automatically updated by [git.hannover.ccc.de](https://git.hannover.ccc.de/c3lf/c3lf-system-3/) whenever +a commit is pushed to the 'testing' branch and the backend tests passed. + +**WARNING: allthough this is the staging system, it is fully functional and contains a copy of the 'production' data, so +do not for example reply to tickets for testing purposes as the system WILL SEND AN EMAIL to the person who originally +created it. If you want to test something like that, first create you own test ticket by sending an email to +`@staging.c3lf.de`** \ No newline at end of file diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..f9780e0 --- /dev/null +++ b/core/README.md @@ -0,0 +1,68 @@ +# Core + +This directory contains the backend of the C3LF System3 project, which is built using Django and Django Rest Framework. + +## Modules + +- `authentication`: Handles user authentication and authorization. +- `files`: Manages file uploads and related operations. +- `inventory`: Handles inventory management, including events, containers and items. +- `mail`: Manages email-related functionalities, including sending and receiving emails. +- `notify_sessions`: Handles real-time notifications and WebSocket sessions. +- `tickets`: Manages the ticketing system for issue tracking. + +## Modules Structure + +Most modules follow a similar structure, including the following components: + +- `/models.py`: Contains the database models for the module. +- `/serializers.py`: Contains the serializers for the module models. +- `/api_.py`: Contains the API views and endpoints for the module. +- `/migrations/`: Contains database migration files. Needs to contain an `__init__.py` file to be recognized as + a Python package and automatically migration creation to work. +- `/tests//test_.py`: Contains the test cases for the module. + +## Development Setup + +follow the instructions under 'Backend-API-Only' or 'Fullstack-Lite' in the root level `README.md` to set up a +development environment. + +## Test-Driven Development (TDD) Workflow + +The project follows a TDD workflow to ensure code quality and reliability. Here is a step-by-step guide to the TDD +process: + +1. **Write a Test**: Start by writing a test case for the new feature or bug fix. Place the test case in the appropriate + module within the `/tests//test_.py` file. + +2. **Run the Test**: Execute the test to ensure it fails, confirming that the feature is not yet implemented or the bug + exists. + ```bash + python manage.py test + ``` + +3. **Write the Code**: Implement the code required to pass the test. Write the code in the appropriate module within the + project. + +4. **Run the Test Again**: Execute the test again to ensure it passes. + ```bash + python manage.py test + ``` + +5. **Refactor**: Refactor the code to improve its structure and readability while ensuring that all tests still pass. + +6. **Repeat**: Repeat the process for each new feature or bug fix. + +## Measuring Test Coverage + +The project uses the `coverage` package to measure test coverage. To generate a coverage report, run the following +command: + +```bash +coverage run --source='.' manage.py test +coverage report +``` + +## Additional Information + +For more detailed information on the project structure and development modes, refer to the root level `README.md`. \ No newline at end of file diff --git a/core/core/settings.py b/core/core/settings.py index 90e026b..805a27b 100644 --- a/core/core/settings.py +++ b/core/core/settings.py @@ -70,7 +70,6 @@ INSTALLED_APPS = [ 'inventory', 'mail', 'notify_sessions', - 'shipments' ] REST_FRAMEWORK = { diff --git a/core/core/urls.py b/core/core/urls.py index 55093e9..2386891 100644 --- a/core/core/urls.py +++ b/core/core/urls.py @@ -30,7 +30,6 @@ urlpatterns = [ path('api/2/', include('mail.api_v2')), path('api/2/', include('notify_sessions.api_v2')), path('api/2/', include('authentication.api_v2')), - path('api/2/', include('shipments.api_v2')), path('api/', get_info), path('', include('django_prometheus.urls')), ] diff --git a/core/shipments/README.md b/core/shipments/README.md deleted file mode 100644 index 4666acc..0000000 --- a/core/shipments/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Shipment Management -## Functional Description -This module handles shipments that are processed by the lost&found team. - -**Feature List** -- Shipment can contain *n* (often one, but a lostee can also be sent multiple items) items -- Shipment is linked to *n* tickets (for informative purposes) -- Shipment holds the address of the parcel -- Shipment holds the dhl voucher used -- On creation of a shipment, the agent can activate the features "adress retrieval", "item validation" and/or tracking - - **address retrieval**: the lostee recieves a email with publicly accessible link, on this page the user can enter his address. - - address sanitation - - address validation - - automatic state change after successful address entry (-> waiting for shipping) - - **item validation**: the lostee recieves a email with publicly accessible link, on this page the user see the images and confirm the items - - automatic state change after confirmation - - **tracking**: the lostee recieves a email with publicly accessible link, on this page the user can enter his address -- Returning parcels could be managed to (e.g. if a shipment is marked as returned, the items will be *unfound* again) - -## State Model -The shipment process follows a defined state model to ensure proper workflow and validation: - -### States -1. **CREATED** (Initial State) - - Shipment is created without address, items, or tickets - - No validation requirements - -2. **PENDING_REVIEW** - - Items have been added to the shipment - - Waiting for requester to verify items and/or address - - Requires specification of what needs to be reviewed: - - Items only - - Address only - - Both items and address - - Validation requirements depend on review type - -3. **READY_FOR_SHIPPING** - - All required reviews completed - - All required fields validated - - Ready for shipping process - -4. **REJECTED** - - Shipment was reviewed and rejected - - Terminal state (no further transitions possible) - -5. **SHIPPED** - - Package has been shipped - - Requires complete address information - - Timestamp of shipping is recorded - -6. **COMPLETED** - - Package has been delivered - - Terminal state (no further transitions possible) - - Timestamp of completion is recorded - -### State Transitions -- CREATED → PENDING_REVIEW (when items are added, requires specification of review type) -- PENDING_REVIEW → READY_FOR_SHIPPING (when approved and all required reviews completed) -- PENDING_REVIEW → REJECTED (when rejected) -- READY_FOR_SHIPPING → SHIPPED (when marked as shipped) -- SHIPPED → COMPLETED (when marked as delivered) - -### Validation Rules -- Review requirement must be specified when entering PENDING_REVIEW state -- Address fields are required for shipping-related states (READY_FOR_SHIPPING, SHIPPED, COMPLETED) -- At least one item is required for any state beyond CREATED -- Invalid state transitions will raise validation errors -- Terminal states (REJECTED, COMPLETED) cannot transition to other states -- Approval requires completion of all specified review requirements: - - For ITEMS review: items must be added - - For ADDRESS review: all address fields must be complete - - For BOTH review: both items and complete address are required \ No newline at end of file diff --git a/core/shipments/__init__.py b/core/shipments/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/shipments/api_v2.py b/core/shipments/api_v2.py deleted file mode 100644 index 0c68d0c..0000000 --- a/core/shipments/api_v2.py +++ /dev/null @@ -1,205 +0,0 @@ -from django.urls import re_path -from rest_framework import routers, viewsets, status -from rest_framework.decorators import api_view, permission_classes, action -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework import serializers -from django.shortcuts import get_object_or_404 - -from .models import Shipment -from inventory.models import Item -from tickets.models import IssueThread - - -class ShipmentSerializer(serializers.ModelSerializer): - related_items = serializers.PrimaryKeyRelatedField( - many=True, - queryset=Item.objects.all(), - required=False - ) - related_tickets = serializers.PrimaryKeyRelatedField( - many=True, - queryset=IssueThread.objects.all(), - required=False - ) - - class Meta: - model = Shipment - fields = [ - 'id', 'public_secret', 'state', 'review_requirement', - 'recipient_name', 'address_supplements', 'street_address', - 'city', 'state_province', 'postal_code', 'country', - 'related_items', 'related_tickets', - 'created_at', 'updated_at', 'shipped_at', 'completed_at' - ] - read_only_fields = ['id', 'public_secret', 'created_at', 'updated_at', 'shipped_at', 'completed_at'] - - -class PublicShipmentSerializer(serializers.ModelSerializer): - related_items = serializers.PrimaryKeyRelatedField( - many=True, - read_only=True - ) - - class Meta: - model = Shipment - fields = [ - 'id', 'public_secret', 'state', 'review_requirement', - 'recipient_name', 'address_supplements', 'street_address', - 'city', 'state_province', 'postal_code', 'country', - 'related_items', 'created_at' - ] - read_only_fields = ['id', 'public_secret', 'state', 'review_requirement', 'created_at'] - - -class ShipmentAddressUpdateSerializer(serializers.ModelSerializer): - class Meta: - model = Shipment - fields = [ - 'recipient_name', 'address_supplements', 'street_address', - 'city', 'state_province', 'postal_code', 'country' - ] - - -class ShipmentViewSet(viewsets.ModelViewSet): - serializer_class = ShipmentSerializer - queryset = Shipment.objects.all() - permission_classes = [IsAuthenticated] - - @action(detail=True, methods=['post']) - def add_items(self, request, pk=None): - shipment = self.get_object() - review_requirement = request.data.get('review_requirement') - item_ids = request.data.get('item_ids', []) - - try: - items = Item.objects.filter(id__in=item_ids) - shipment.related_items.add(*items) - shipment.add_items(review_requirement) - return Response(ShipmentSerializer(shipment).data) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=True, methods=['post']) - def approve(self, request, pk=None): - shipment = self.get_object() - try: - shipment.approve() - return Response(ShipmentSerializer(shipment).data) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=True, methods=['post']) - def reject(self, request, pk=None): - shipment = self.get_object() - try: - shipment.reject() - return Response(ShipmentSerializer(shipment).data) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=True, methods=['post']) - def mark_shipped(self, request, pk=None): - shipment = self.get_object() - try: - # Validate state transition before attempting to mark as shipped - if shipment.state != Shipment.READY_FOR_SHIPPING: - return Response( - {'error': f'Invalid state transition: Cannot mark shipment as shipped from {shipment.state} state. Shipment must be in READY_FOR_SHIPPING state.'}, - status=status.HTTP_400_BAD_REQUEST - ) - shipment.mark_shipped() - return Response(ShipmentSerializer(shipment).data) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=True, methods=['post']) - def mark_completed(self, request, pk=None): - shipment = self.get_object() - try: - # Validate state transition before attempting to mark as completed - if shipment.state != Shipment.SHIPPED: - return Response( - {'error': f'Invalid state transition: Cannot mark shipment as completed from {shipment.state} state. Shipment must be in SHIPPED state.'}, - status=status.HTTP_400_BAD_REQUEST - ) - shipment.mark_completed() - return Response(ShipmentSerializer(shipment).data) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - -# Public API endpoints for shipment recipients -@api_view(['GET']) -@permission_classes([AllowAny]) -def public_shipment_detail(request, public_secret): - """ - Retrieve shipment details using public_secret. - Only accessible when shipment is in PENDING_REVIEW state. - """ - try: - shipment = Shipment.objects.get(public_secret=public_secret) - - # Only allow access to shipments in PENDING_REVIEW state - if shipment.state != Shipment.PENDING_REVIEW: - return Response(status=status.HTTP_404_NOT_FOUND) - - serializer = PublicShipmentSerializer(shipment) - return Response(serializer.data) - except Shipment.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - - -@api_view(['POST']) -@permission_classes([AllowAny]) -def public_shipment_update(request, public_secret): - """ - Update shipment with address information and transition state. - Only accessible when shipment is in PENDING_REVIEW state. - """ - try: - shipment = Shipment.objects.get(public_secret=public_secret) - - # Only allow updates to shipments in PENDING_REVIEW state - if shipment.state != Shipment.PENDING_REVIEW: - return Response(status=status.HTTP_404_NOT_FOUND) - - serializer = ShipmentAddressUpdateSerializer(shipment, data=request.data, partial=True) - if serializer.is_valid(): - # Update address fields - for field, value in serializer.validated_data.items(): - setattr(shipment, field, value) - - # Update the shipment state based on review requirement - try: - if shipment.review_requirement == Shipment.REVIEW_ADDRESS: - # If only address review was required, we can approve directly - shipment.approve() - elif shipment.review_requirement == Shipment.REVIEW_BOTH: - # If both items and address review were required, we need to check if items exist - if not shipment.related_items.exists(): - return Response( - {'error': 'Items must be added before approval'}, - status=status.HTTP_400_BAD_REQUEST - ) - shipment.approve() - else: - # For ITEMS review requirement, we just save the address but don't change state - shipment.save() - - return Response(PublicShipmentSerializer(shipment).data) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Shipment.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - - -router = routers.SimpleRouter() -router.register(r'shipments', ShipmentViewSet, basename='shipments') - -urlpatterns = router.urls + [ - re_path(r'^public/shipments/(?P[0-9a-f-]+)/$', public_shipment_detail, name='public-shipment-detail'), - re_path(r'^public/shipments/(?P[0-9a-f-]+)/update/$', public_shipment_update, name='public-shipment-update'), -] diff --git a/core/shipments/migrations/0001_initial.py b/core/shipments/migrations/0001_initial.py deleted file mode 100644 index eb889e2..0000000 --- a/core/shipments/migrations/0001_initial.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.7 on 2025-03-15 21:37 - -from django.db import migrations, models -import django.db.models.manager -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('inventory', '0007_rename_container_item_container_old_itemplacement_and_more'), - ('tickets', '0013_alter_statechange_state'), - ] - - operations = [ - migrations.CreateModel( - name='Shipment', - fields=[ - ('is_deleted', models.BooleanField(default=False)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('public_secret', models.UUIDField(default=uuid.uuid4)), - ('created_at', models.DateTimeField(auto_now_add=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ('related_items', models.ManyToManyField(to='inventory.item')), - ('related_tickets', models.ManyToManyField(to='tickets.issuethread')), - ], - options={ - 'abstract': False, - }, - managers=[ - ('all_objects', django.db.models.manager.Manager()), - ], - ), - ] diff --git a/core/shipments/migrations/0002_shipment_address_supplements_shipment_city_and_more.py b/core/shipments/migrations/0002_shipment_address_supplements_shipment_city_and_more.py deleted file mode 100644 index f5df569..0000000 --- a/core/shipments/migrations/0002_shipment_address_supplements_shipment_city_and_more.py +++ /dev/null @@ -1,68 +0,0 @@ -# Generated by Django 4.2.7 on 2025-04-11 15:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('shipments', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='shipment', - name='address_supplements', - field=models.TextField(blank=True, default='', max_length=255), - ), - migrations.AddField( - model_name='shipment', - name='city', - field=models.CharField(blank=True, default='', max_length=100), - ), - migrations.AddField( - model_name='shipment', - name='completed_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='shipment', - name='country', - field=models.CharField(blank=True, default='', max_length=100), - ), - migrations.AddField( - model_name='shipment', - name='postal_code', - field=models.CharField(blank=True, default='', max_length=20), - ), - migrations.AddField( - model_name='shipment', - name='recipient_name', - field=models.CharField(blank=True, default='', max_length=255), - ), - migrations.AddField( - model_name='shipment', - name='review_requirement', - field=models.CharField(blank=True, choices=[('ITEMS', 'Items Review Required'), ('ADDRESS', 'Address Review Required'), ('BOTH', 'Both Items and Address Review Required')], max_length=10, null=True), - ), - migrations.AddField( - model_name='shipment', - name='shipped_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='shipment', - name='state', - field=models.CharField(choices=[('CREATED', 'Created'), ('PENDING_REVIEW', 'Pending Review'), ('READY_FOR_SHIPPING', 'Ready for Shipping'), ('REJECTED', 'Rejected'), ('SHIPPED', 'Shipped'), ('COMPLETED', 'Completed')], default='CREATED', max_length=20), - ), - migrations.AddField( - model_name='shipment', - name='state_province', - field=models.CharField(blank=True, default='', max_length=100), - ), - migrations.AddField( - model_name='shipment', - name='street_address', - field=models.CharField(blank=True, default='', max_length=255), - ), - ] diff --git a/core/shipments/migrations/__init__.py b/core/shipments/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/shipments/models.py b/core/shipments/models.py deleted file mode 100644 index b98f88d..0000000 --- a/core/shipments/models.py +++ /dev/null @@ -1,162 +0,0 @@ -import uuid -from django.db import models -from django.core.exceptions import ValidationError -from django.utils import timezone -from django_softdelete.models import SoftDeleteModel, SoftDeleteManager -from inventory.models import Item -from tickets.models import IssueThread - -class Shipment(SoftDeleteModel): - # State choices - CREATED = 'CREATED' - PENDING_REVIEW = 'PENDING_REVIEW' - READY_FOR_SHIPPING = 'READY_FOR_SHIPPING' - REJECTED = 'REJECTED' - SHIPPED = 'SHIPPED' - COMPLETED = 'COMPLETED' - - SHIPMENT_STATES = [ - (CREATED, 'Created'), - (PENDING_REVIEW, 'Pending Review'), - (READY_FOR_SHIPPING, 'Ready for Shipping'), - (REJECTED, 'Rejected'), - (SHIPPED, 'Shipped'), - (COMPLETED, 'Completed'), - ] - - # Review requirement choices - REVIEW_ITEMS = 'ITEMS' - REVIEW_ADDRESS = 'ADDRESS' - REVIEW_BOTH = 'BOTH' - - REVIEW_REQUIREMENTS = [ - (REVIEW_ITEMS, 'Items Review Required'), - (REVIEW_ADDRESS, 'Address Review Required'), - (REVIEW_BOTH, 'Both Items and Address Review Required'), - ] - - id = models.AutoField(primary_key=True) - public_secret = models.UUIDField(default=uuid.uuid4) - state = models.CharField( - max_length=20, - choices=SHIPMENT_STATES, - default=CREATED - ) - review_requirement = models.CharField( - max_length=10, - choices=REVIEW_REQUIREMENTS, - null=True, - blank=True - ) - - # Shipping address fields - recipient_name = models.CharField(max_length=255, blank=True, default='') - address_supplements = models.TextField(max_length=255, blank=True, default='') - street_address = models.CharField(max_length=255, blank=True, default='') - city = models.CharField(max_length=100, blank=True, default='') - state_province = models.CharField(max_length=100, blank=True, default='') - postal_code = models.CharField(max_length=20, blank=True, default='') - country = models.CharField(max_length=100, blank=True, default='') - - related_items = models.ManyToManyField(Item) - related_tickets = models.ManyToManyField(IssueThread) - - created_at = models.DateTimeField(null=True, auto_now_add=True) - updated_at = models.DateTimeField(blank=True, null=True) - shipped_at = models.DateTimeField(blank=True, null=True) - completed_at = models.DateTimeField(blank=True, null=True) - - all_objects = models.Manager() - - def __str__(self): - return f'[{self.id}] {self.state}' - - def clean(self): - """Validate state transitions and required fields""" - if not self.pk: # New instance - return - - old_instance = Shipment.objects.get(pk=self.pk) - - # Validate state transitions - valid_transitions = { - self.CREATED: [self.PENDING_REVIEW], - self.PENDING_REVIEW: [self.READY_FOR_SHIPPING, self.REJECTED], - self.READY_FOR_SHIPPING: [self.SHIPPED], - self.SHIPPED: [self.COMPLETED], - self.REJECTED: [], # No transitions from rejected - self.COMPLETED: [], # No transitions from completed - } - - if (self.state != old_instance.state and - self.state not in valid_transitions[old_instance.state]): - raise ValidationError(f'Invalid state transition from {old_instance.state} to {self.state}') - - # Validate review requirement when entering PENDING_REVIEW - if self.state == self.PENDING_REVIEW and old_instance.state == self.CREATED: - if not self.review_requirement: - raise ValidationError('Review requirement must be specified when entering PENDING_REVIEW state') - - # Validate required fields based on state and review requirement - if self.state in [self.READY_FOR_SHIPPING, self.SHIPPED, self.COMPLETED]: - if not all([self.recipient_name, self.street_address, self.city, - self.state_province, self.postal_code, self.country]): - raise ValidationError('All address fields are required for shipping') - - if self.state != self.CREATED and not self.related_items.exists(): - raise ValidationError('Shipment must have at least one item') - - def add_items(self, review_requirement): - """Move shipment to PENDING_REVIEW after items are added""" - if self.state == self.CREATED and self.related_items.exists(): - if not review_requirement: - raise ValidationError('Review requirement must be specified when adding items') - if review_requirement not in [self.REVIEW_ITEMS, self.REVIEW_ADDRESS, self.REVIEW_BOTH]: - raise ValidationError(f'Invalid review requirement: {review_requirement}') - - self.review_requirement = review_requirement - self.state = self.PENDING_REVIEW - self.save() - - def approve(self): - """Approve shipment for shipping""" - if self.state == self.PENDING_REVIEW: - # Validate that all required reviews are completed - if self.review_requirement == self.REVIEW_ITEMS and not self.related_items.exists(): - raise ValidationError('Items must be added before approval') - elif self.review_requirement == self.REVIEW_ADDRESS and not all([self.recipient_name, self.street_address, self.city, - self.state_province, self.postal_code, self.country]): - raise ValidationError('Address must be complete before approval') - elif self.review_requirement == self.REVIEW_BOTH: - if not self.related_items.exists(): - raise ValidationError('Items must be added before approval') - if not all([self.recipient_name, self.street_address, self.city, - self.state_province, self.postal_code, self.country]): - raise ValidationError('Address must be complete before approval') - - self.state = self.READY_FOR_SHIPPING - self.save() - - def reject(self): - """Reject shipment""" - if self.state == self.PENDING_REVIEW: - self.state = self.REJECTED - self.save() - - def mark_shipped(self): - """Mark shipment as shipped""" - if self.state == self.READY_FOR_SHIPPING: - self.state = self.SHIPPED - self.shipped_at = timezone.now() - self.save() - - def mark_completed(self): - """Mark shipment as completed""" - if self.state == self.SHIPPED: - self.state = self.COMPLETED - self.completed_at = timezone.now() - self.save() - - def save(self, *args, **kwargs): - self.clean() - super().save(*args, **kwargs) \ No newline at end of file diff --git a/core/shipments/tests/__init__.py b/core/shipments/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/shipments/tests/v2/__init__.py b/core/shipments/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/shipments/tests/v2/test_api.py b/core/shipments/tests/v2/test_api.py deleted file mode 100644 index a6ad468..0000000 --- a/core/shipments/tests/v2/test_api.py +++ /dev/null @@ -1,595 +0,0 @@ -from django.test import TestCase, Client -from django.contrib.auth.models import Permission -from knox.models import AuthToken -from django.utils import timezone - -from authentication.models import ExtendedUser -from inventory.models import Event, Item, Container -from tickets.models import IssueThread -from shipments.models import Shipment - - -class ShipmentApiTest(TestCase): - def setUp(self): - super().setUp() - # Create a test user with all permissions - self.user = ExtendedUser.objects.create_user('testuser', 'test@example.com', 'test') - self.user.user_permissions.add(*Permission.objects.all()) - self.user.save() - - # Create authentication token - self.token = AuthToken.objects.create(user=self.user) - self.client = Client(headers={'Authorization': 'Token ' + self.token[1]}) - - # Create test event and container - self.event = Event.objects.create(slug='test-event', name='Test Event') - self.container = Container.objects.create(name='Test Container') - - # Create test items with container - self.item1 = Item.objects.create(description='Test Item 1', event=self.event, container=self.container) - self.item2 = Item.objects.create(description='Test Item 2', event=self.event, container=self.container) - - # Create test ticket - self.ticket = IssueThread.objects.create(name='Test Ticket') - - def test_list_shipments_empty(self): - """Test listing shipments when none exist""" - response = self.client.get('/api/2/shipments/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_create_shipment(self): - """Test creating a new shipment""" - data = { - 'recipient_name': 'John Smith', - 'street_address': '123 Oxford Street', - 'city': 'London', - 'state_province': 'Greater London', - 'postal_code': 'W1D 2HG', - 'country': 'United Kingdom', - 'related_items': [], - 'related_tickets': [] - } - response = self.client.post('/api/2/shipments/', data, content_type='application/json') - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json()['recipient_name'], 'John Smith') - self.assertEqual(response.json()['state'], Shipment.CREATED) - self.assertEqual(response.json()['review_requirement'], None) - - # Retrieve the created shipment and verify its details - shipment_id = response.json()['id'] - get_response = self.client.get(f'/api/2/shipments/{shipment_id}/') - self.assertEqual(get_response.status_code, 200) - self.assertEqual(get_response.json()['id'], shipment_id) - self.assertEqual(get_response.json()['recipient_name'], 'John Smith') - self.assertEqual(get_response.json()['street_address'], '123 Oxford Street') - self.assertEqual(get_response.json()['city'], 'London') - self.assertEqual(get_response.json()['state_province'], 'Greater London') - self.assertEqual(get_response.json()['postal_code'], 'W1D 2HG') - self.assertEqual(get_response.json()['country'], 'United Kingdom') - self.assertEqual(get_response.json()['state'], Shipment.CREATED) - self.assertEqual(get_response.json()['review_requirement'], None) - - def test_get_shipment(self): - """Test retrieving a specific shipment""" - # Create a shipment first - shipment = Shipment.objects.create( - recipient_name='Maria Garcia', - street_address='Calle Gran Via 28', - city='Madrid', - state_province='Madrid', - postal_code='28013', - country='Spain' - ) - - response = self.client.get(f'/api/2/shipments/{shipment.id}/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['id'], shipment.id) - self.assertEqual(response.json()['recipient_name'], 'Maria Garcia') - - def test_update_shipment(self): - """Test updating a shipment""" - # Create a shipment first - shipment = Shipment.objects.create( - recipient_name='Hans Mueller', - street_address='Kurfürstendamm 216', - city='Berlin', - state_province='Berlin', - postal_code='10719', - country='Germany' - ) - - data = { - 'recipient_name': 'Hans-Jürgen Mueller', - 'address_supplements': 'Apartment 4B' - } - response = self.client.patch(f'/api/2/shipments/{shipment.id}/', data, content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['recipient_name'], 'Hans-Jürgen Mueller') - self.assertEqual(response.json()['address_supplements'], 'Apartment 4B') - - def test_delete_shipment(self): - """Test deleting a shipment""" - # Create a shipment first - shipment = Shipment.objects.create( - recipient_name='Sophie Dubois', - street_address='15 Rue de Rivoli', - city='Paris', - state_province='Île-de-France', - postal_code='75001', - country='France' - ) - - response = self.client.delete(f'/api/2/shipments/{shipment.id}/') - self.assertEqual(response.status_code, 204) - - # Verify it's soft deleted - use all_objects manager to see deleted items - self.assertTrue(Shipment.all_objects.filter(id=shipment.id).exists()) - # Check if it's marked as deleted - shipment.refresh_from_db() - self.assertIsNotNone(shipment.deleted_at) - - def test_add_items(self): - """Test adding items to a shipment""" - # Create a shipment first - shipment = Shipment.objects.create( - recipient_name='Alessandro Romano', - street_address='Via del Corso 123', - city='Rome', - state_province='Lazio', - postal_code='00186', - country='Italy' - ) - - data = { - 'item_ids': [self.item1.id, self.item2.id], - 'review_requirement': Shipment.REVIEW_ITEMS - } - response = self.client.post(f'/api/2/shipments/{shipment.id}/add_items/', data, content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW) - self.assertEqual(response.json()['review_requirement'], Shipment.REVIEW_ITEMS) - - # Verify items were added - shipment.refresh_from_db() - self.assertEqual(shipment.related_items.count(), 2) - - def test_approve_shipment(self): - """Test approving a shipment""" - # Create a shipment and add items - shipment = Shipment.objects.create( - recipient_name='Yuki Tanaka', - street_address='1-1-1 Shibuya', - city='Tokyo', - state_province='Tokyo', - postal_code='150-0002', - country='Japan' - ) - shipment.related_items.add(self.item1) - shipment.review_requirement = Shipment.REVIEW_ITEMS - shipment.state = Shipment.PENDING_REVIEW - shipment.save() - - response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING) - - def test_reject_shipment(self): - """Test rejecting a shipment""" - # Create a shipment and add items - shipment = Shipment.objects.create( - recipient_name='Carlos Rodriguez', - street_address='Avenida Insurgentes Sur 1602', - city='Mexico City', - state_province='CDMX', - postal_code='03940', - country='Mexico' - ) - shipment.related_items.add(self.item1) - shipment.review_requirement = Shipment.REVIEW_ITEMS - shipment.state = Shipment.PENDING_REVIEW - shipment.save() - - response = self.client.post(f'/api/2/shipments/{shipment.id}/reject/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.REJECTED) - - def test_mark_shipped(self): - """Test marking a shipment as shipped""" - # Create a shipment and set it to READY_FOR_SHIPPING - shipment = Shipment.objects.create( - recipient_name='Anna Kowalski', - street_address='ul. Marszałkowska 126', - city='Warsaw', - state_province='Mazowieckie', - postal_code='00-008', - country='Poland', - state=Shipment.READY_FOR_SHIPPING - ) - shipment.related_items.add(self.item1) - - response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_shipped/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.SHIPPED) - self.assertIsNotNone(response.json()['shipped_at']) - - def test_mark_completed(self): - """Test marking a shipment as completed""" - # Create a shipment and set it to SHIPPED - shipment = Shipment.objects.create( - recipient_name='Lars Andersen', - street_address='Strøget 1', - city='Copenhagen', - state_province='Capital Region', - postal_code='1095', - country='Denmark', - state=Shipment.SHIPPED, - shipped_at=timezone.now() - ) - shipment.related_items.add(self.item1) - - response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_completed/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.COMPLETED) - self.assertIsNotNone(response.json()['completed_at']) - - def test_invalid_state_transition(self): - """Test that invalid state transitions are rejected""" - # Create a shipment in CREATED state - shipment = Shipment.objects.create( - recipient_name='Ahmed Hassan', - street_address='26 July Street', - city='Cairo', - state_province='Cairo Governorate', - postal_code='11511', - country='Egypt' - ) - - # Try to mark it as shipped directly (invalid transition) - response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_shipped/') - self.assertEqual(response.status_code, 400) - self.assertIn('error', response.json()) - self.assertIn('Invalid state transition', response.json()['error']) - - # Verify state hasn't changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.CREATED) - - def test_add_items_without_review_requirement(self): - """Test adding items without specifying review requirement""" - # Create a shipment first - shipment = Shipment.objects.create( - recipient_name='Wei Chen', - street_address='88 Century Avenue', - city='Shanghai', - state_province='Shanghai', - postal_code='200120', - country='China' - ) - - data = { - 'item_ids': [self.item1.id, self.item2.id] - } - response = self.client.post(f'/api/2/shipments/{shipment.id}/add_items/', data, content_type='application/json') - self.assertEqual(response.status_code, 400) - self.assertIn('error', response.json()) - - # Verify state hasn't changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.CREATED) - - def test_approve_without_items(self): - """Test approving a shipment without items""" - # Create a shipment with REVIEW_ITEMS requirement - shipment = Shipment.objects.create( - recipient_name='Raj Patel', - street_address='123 MG Road', - city='Mumbai', - state_province='Maharashtra', - postal_code='400001', - country='India', - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_ITEMS - ) - - response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/') - self.assertEqual(response.status_code, 400) - self.assertIn('error', response.json()) - - # Verify state hasn't changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.PENDING_REVIEW) - - def test_approve_without_address(self): - """Test approving a shipment without complete address""" - # Create a shipment with REVIEW_ADDRESS requirement - shipment = Shipment.objects.create( - recipient_name='Sofia Oliveira', - street_address='Avenida Paulista 1000', - city='São Paulo', - state_province='São Paulo', - postal_code='01310-100', - country='', # Missing country - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_ADDRESS - ) - shipment.related_items.add(self.item1) - - response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/') - self.assertEqual(response.status_code, 400) - self.assertIn('error', response.json()) - - # Verify state hasn't changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.PENDING_REVIEW) - - def test_public_shipment_detail_success(self): - """Test retrieving shipment details via public API with valid public_secret""" - # Create a shipment in PENDING_REVIEW state - shipment = Shipment.objects.create( - recipient_name='John Doe', - street_address='123 Main St', - city='Anytown', - state_province='State', - postal_code='12345', - country='Country', - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_ADDRESS - ) - shipment.related_items.add(self.item1) - - # Create a client without authentication - public_client = Client() - - # Test retrieving shipment details - response = public_client.get(f'/api/2/public/shipments/{shipment.public_secret}/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['id'], shipment.id) - self.assertEqual(response.json()['recipient_name'], 'John Doe') - self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW) - self.assertEqual(len(response.json()['related_items']), 1) - - def test_public_shipment_detail_not_found(self): - """Test retrieving shipment details with non-existent public_secret""" - # Create a client without authentication - public_client = Client() - - # Test with non-existent public_secret - response = public_client.get('/api/2/public/shipments/00000000-0000-0000-0000-000000000000/') - self.assertEqual(response.status_code, 404) - - def test_public_shipment_detail_wrong_state(self): - """Test retrieving shipment details when shipment is not in PENDING_REVIEW state""" - # Create a shipment in CREATED state - shipment = Shipment.objects.create( - recipient_name='Jane Smith', - street_address='456 Oak Ave', - city='Somewhere', - state_province='Province', - postal_code='67890', - country='Nation', - state=Shipment.CREATED - ) - - # Create a client without authentication - public_client = Client() - - # Test retrieving shipment details - response = public_client.get(f'/api/2/public/shipments/{shipment.public_secret}/') - self.assertEqual(response.status_code, 404) - - def test_public_shipment_update_success_address_only(self): - """Test updating shipment with address information when review_requirement is ADDRESS""" - # Create a shipment in PENDING_REVIEW state with ADDRESS review requirement - shipment = Shipment.objects.create( - recipient_name='Alice Johnson', - street_address='789 Pine Rd', - city='Nowhere', - state_province='Region', - postal_code='13579', - country='Land', - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_ADDRESS - ) - shipment.related_items.add(self.item1) - - # Create a client without authentication - public_client = Client() - - # Test updating shipment with address information - data = { - 'recipient_name': 'Alice Johnson', - 'street_address': '789 Pine Rd, Apt 4B', - 'city': 'Nowhere', - 'state_province': 'Region', - 'postal_code': '13579', - 'country': 'Land' - } - response = public_client.post( - f'/api/2/public/shipments/{shipment.public_secret}/update/', - data, - content_type='application/json' - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING) - self.assertEqual(response.json()['street_address'], '789 Pine Rd, Apt 4B') - - # Verify state has changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.READY_FOR_SHIPPING) - - def test_public_shipment_update_success_both_review(self): - """Test updating shipment with address information when review_requirement is BOTH""" - # Create a shipment in PENDING_REVIEW state with BOTH review requirement - shipment = Shipment.objects.create( - recipient_name='Bob Wilson', - street_address='321 Elm St', - city='Elsewhere', - state_province='Territory', - postal_code='24680', - country='Realm', - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_BOTH - ) - shipment.related_items.add(self.item1) - - # Create a client without authentication - public_client = Client() - - # Test updating shipment with address information - data = { - 'recipient_name': 'Bob Wilson', - 'street_address': '321 Elm St, Suite 7', - 'city': 'Elsewhere', - 'state_province': 'Territory', - 'postal_code': '24680', - 'country': 'Realm' - } - response = public_client.post( - f'/api/2/public/shipments/{shipment.public_secret}/update/', - data, - content_type='application/json' - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING) - self.assertEqual(response.json()['street_address'], '321 Elm St, Suite 7') - - # Verify state has changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.READY_FOR_SHIPPING) - - def test_public_shipment_update_success_items_only(self): - """Test updating shipment with address information when review_requirement is ITEMS""" - # Create a shipment in PENDING_REVIEW state with ITEMS review requirement - shipment = Shipment.objects.create( - recipient_name='Carol Davis', - street_address='654 Maple Dr', - city='Anywhere', - state_province='District', - postal_code='97531', - country='Kingdom', - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_ITEMS - ) - shipment.related_items.add(self.item1) - - # Create a client without authentication - public_client = Client() - - # Test updating shipment with address information - data = { - 'recipient_name': 'Carol Davis', - 'street_address': '654 Maple Dr, Unit 3', - 'city': 'Anywhere', - 'state_province': 'District', - 'postal_code': '97531', - 'country': 'Kingdom' - } - response = public_client.post( - f'/api/2/public/shipments/{shipment.public_secret}/update/', - data, - content_type='application/json' - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW) # State should not change - self.assertEqual(response.json()['street_address'], '654 Maple Dr, Unit 3') - - # Verify state has not changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.PENDING_REVIEW) - - def test_public_shipment_update_not_found(self): - """Test updating shipment with non-existent public_secret""" - # Create a client without authentication - public_client = Client() - - # Test with non-existent public_secret - data = { - 'recipient_name': 'Unknown Person', - 'street_address': '123 Fake St', - 'city': 'Fake City', - 'state_province': 'Fake State', - 'postal_code': '12345', - 'country': 'Fake Country' - } - response = public_client.post( - '/api/2/public/shipments/00000000-0000-0000-0000-000000000000/update/', - data, - content_type='application/json' - ) - self.assertEqual(response.status_code, 404) - - def test_public_shipment_update_wrong_state(self): - """Test updating shipment when it's not in PENDING_REVIEW state""" - # Create a shipment in CREATED state - shipment = Shipment.objects.create( - recipient_name='David Lee', - street_address='987 Cedar Ln', - city='Nowhere', - state_province='Province', - postal_code='13579', - country='Country', - state=Shipment.CREATED - ) - - # Create a client without authentication - public_client = Client() - - # Test updating shipment - data = { - 'recipient_name': 'David Lee', - 'street_address': '987 Cedar Ln, Apt 2C', - 'city': 'Nowhere', - 'state_province': 'Province', - 'postal_code': '13579', - 'country': 'Country' - } - response = public_client.post( - f'/api/2/public/shipments/{shipment.public_secret}/update/', - data, - content_type='application/json' - ) - self.assertEqual(response.status_code, 404) - - # Verify state hasn't changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.CREATED) - - def test_public_shipment_update_both_review_missing_items(self): - """Test updating shipment with BOTH review requirement when items are missing""" - # Create a shipment in PENDING_REVIEW state with BOTH review requirement but no items - shipment = Shipment.objects.create( - recipient_name='Eve Brown', - street_address='147 Birch Ave', - city='Somewhere', - state_province='Region', - postal_code='24680', - country='Land', - state=Shipment.PENDING_REVIEW, - review_requirement=Shipment.REVIEW_BOTH - ) - - # Create a client without authentication - public_client = Client() - - # Test updating shipment with address information - data = { - 'recipient_name': 'Eve Brown', - 'street_address': '147 Birch Ave, Suite 5', - 'city': 'Somewhere', - 'state_province': 'Region', - 'postal_code': '24680', - 'country': 'Land' - } - response = public_client.post( - f'/api/2/public/shipments/{shipment.public_secret}/update/', - data, - content_type='application/json' - ) - self.assertEqual(response.status_code, 400) - self.assertIn('error', response.json()) - self.assertIn('Items must be added before approval', response.json()['error']) - - # Verify state hasn't changed - shipment.refresh_from_db() - self.assertEqual(shipment.state, Shipment.PENDING_REVIEW) diff --git a/core/testdata.py b/core/testdata.py deleted file mode 100644 index e69de29..0000000 diff --git a/deploy/testdata.py b/deploy/testdata.py index 6cb3b33..dca385f 100644 --- a/deploy/testdata.py +++ b/deploy/testdata.py @@ -3,7 +3,7 @@ import os def setup(): from authentication.models import ExtendedUser, EventPermission - from inventory.models import Event, Container, Item + from inventory.models import Event from django.contrib.auth.models import Permission, Group permissions = ['add_item', 'view_item', 'view_file', 'delete_item', 'change_item'] if not ExtendedUser.objects.filter(username='admin').exists(): @@ -38,16 +38,6 @@ def setup(): start='2024-12-18 00:00:00.000000', end='2024-12-27 00:00:00.000000', pre_start='2024-12-31 00:00:00.000000', post_end='2025-01-04 00:00:00.000000')[ 0] - - container1 ,_= Container.objects.get_or_create(id=1, name='Box1') - container2 ,_= Container.objects.get_or_create(id=2, name='Box2') - - testitem1 ,_= Item.objects.get_or_create(id=1, event=event1, description="Test item 1",uid_deprecated=111) - testitem1.container = container1 - testitem2 ,_= Item.objects.get_or_create(id=2, event=event2, description="Test item 2",uid_deprecated=112) - testitem2.container = container1 - testitem3 ,_= Item.objects.get_or_create(id=3, event=event2, description="Test item 3",uid_deprecated=113) - testitem3.container = container2 # for permission in permissions: # EventPermission.objects.create(event=event_37c3, user=foo,