Compare commits
4 commits
clarity/in
...
testing
Author | SHA1 | Date | |
---|---|---|---|
9def22a836 | |||
8d45fef627 | |||
756fe4aaad | |||
51ddc8edc3 |
16 changed files with 227 additions and 1152 deletions
158
README.md
Normal file
158
README.md
Normal file
|
@ -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
|
||||||
|
`<event>@staging.c3lf.de`**
|
68
core/README.md
Normal file
68
core/README.md
Normal file
|
@ -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:
|
||||||
|
|
||||||
|
- `<module>/models.py`: Contains the database models for the module.
|
||||||
|
- `<module>/serializers.py`: Contains the serializers for the module models.
|
||||||
|
- `<module>/api_<api_version>.py`: Contains the API views and endpoints for the module.
|
||||||
|
- `<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.
|
||||||
|
- `<module>/tests/<api_version>/test_<feature_model_or_testcase>.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 `<module>/tests/<api_version>/test_<feature_model_or_testcase>.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`.
|
|
@ -70,7 +70,6 @@ INSTALLED_APPS = [
|
||||||
'inventory',
|
'inventory',
|
||||||
'mail',
|
'mail',
|
||||||
'notify_sessions',
|
'notify_sessions',
|
||||||
'shipments'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
|
|
|
@ -30,7 +30,6 @@ urlpatterns = [
|
||||||
path('api/2/', include('mail.api_v2')),
|
path('api/2/', include('mail.api_v2')),
|
||||||
path('api/2/', include('notify_sessions.api_v2')),
|
path('api/2/', include('notify_sessions.api_v2')),
|
||||||
path('api/2/', include('authentication.api_v2')),
|
path('api/2/', include('authentication.api_v2')),
|
||||||
path('api/2/', include('shipments.api_v2')),
|
|
||||||
path('api/', get_info),
|
path('api/', get_info),
|
||||||
path('', include('django_prometheus.urls')),
|
path('', include('django_prometheus.urls')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
|
|
@ -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<public_secret>[0-9a-f-]+)/$', public_shipment_detail, name='public-shipment-detail'),
|
|
||||||
re_path(r'^public/shipments/(?P<public_secret>[0-9a-f-]+)/update/$', public_shipment_update, name='public-shipment-update'),
|
|
||||||
]
|
|
|
@ -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()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -3,7 +3,7 @@ import os
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
from authentication.models import ExtendedUser, EventPermission
|
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
|
from django.contrib.auth.models import Permission, Group
|
||||||
permissions = ['add_item', 'view_item', 'view_file', 'delete_item', 'change_item']
|
permissions = ['add_item', 'view_item', 'view_file', 'delete_item', 'change_item']
|
||||||
if not ExtendedUser.objects.filter(username='admin').exists():
|
if not ExtendedUser.objects.filter(username='admin').exists():
|
||||||
|
@ -39,16 +39,6 @@ def setup():
|
||||||
pre_start='2024-12-31 00:00:00.000000', post_end='2025-01-04 00:00:00.000000')[
|
pre_start='2024-12-31 00:00:00.000000', post_end='2025-01-04 00:00:00.000000')[
|
||||||
0]
|
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:
|
# for permission in permissions:
|
||||||
# EventPermission.objects.create(event=event_37c3, user=foo,
|
# EventPermission.objects.create(event=event_37c3, user=foo,
|
||||||
# permission=Permission.objects.get(codename=permission))
|
# permission=Permission.objects.get(codename=permission))
|
||||||
|
|
Loading…
Add table
Reference in a new issue