#disfactory-notification

2026-01-21
github2 22:22:58
`<https://github.com/Disfactory/Disfactory/commit/d0db551b490c4545b37749f63d5d18b7bb0158e7|d0db551b>` - feat: support image upload to local file system `<https://github.com/Disfactory/Disfactory/commit/fb1b064be29b77faf59e20d857a66bddd87cd849|fb1b064b>` - feat: add deployment requirements variables `<https://github.com/Disfactory/Disfactory/commit/6bb7cc7ff462e6b35b0efcca11522db405c284cb|6bb7cc7f>` - feat: address reviewed issues `<https://github.com/Disfactory/Disfactory/commit/2660c1d66319b816ea9f6d674874eb1abd244d9c|2660c1d6>` - docs: add testing document `<https://github.com/Disfactory/Disfactory/commit/4ed2a1ca083fbc37d1d50298c2e332d0d11b1bdc|4ed2a1ca>` - Merge pull request #693 from Disfactory/feature/fs-image-upload-api
github2 22:22:58
`<https://github.com/Disfactory/Disfactory/commit/d0db551b490c4545b37749f63d5d18b7bb0158e7|d0db551b>` - feat: support image upload to local file system `<https://github.com/Disfactory/Disfactory/commit/fb1b064be29b77faf59e20d857a66bddd87cd849|fb1b064b>` - feat: add deployment requirements variables `<https://github.com/Disfactory/Disfactory/commit/6bb7cc7ff462e6b35b0efcca11522db405c284cb|6bb7cc7f>` - feat: address reviewed issues `<https://github.com/Disfactory/Disfactory/commit/2660c1d66319b816ea9f6d674874eb1abd244d9c|2660c1d6>` - docs: add testing document `<https://github.com/Disfactory/Disfactory/commit/4ed2a1ca083fbc37d1d50298c2e332d0d11b1bdc|4ed2a1ca>` - Merge pull request #693 from Disfactory/feature/fs-image-upload-api
github2 22:23:00
## Summary This PR implements a file system-based image upload API that provides an Imgur API-compatible interface, allowing the frontend to switch between Imgur and local storage seamlessly. ## Key Changes • Added a new `/api/upload` endpoint that stores images locally with validation for file type, size, and actual image content • Configured Docker volume for media file storage • Implemented comprehensive test coverage for the upload functionality ## Environment Variables The following existing environment variables are used by this feature: | Variable | Description | Example | | ----------------------------- | --------------------------------------------------------- | ----------------------------------------- | | DISFACTORY_BACKEND_MEDIA_ROOT | Media root path (already exists) | /home/deployer/Disfactory/backend/images/ | | DISFACTORY_BACKEND_DOMAIN | Backend domain for generating image URLs (already exists) | <https://disfactory.tw> | New variable for Docker deployment: | Variable | Description | Example | | -------------------- | -------------------------------------- | --------------------- | | DISFACTORY_MEDIA_DIR | Host directory for Docker volume mount | /var/disfactory/media | ## Setup ### Backend Setup Your existing `.env` should already have the required variables: DISFACTORY_BACKEND_MEDIA_ROOT="/home/deployer/Disfactory/backend/images/" DISFACTORY_BACKEND_DOMAIN="<https://disfactory.tw>" For Docker deployment, add the media directory mount: DISFACTORY_MEDIA_DIR=/path/to/media/storage ### Caddy Configuration (Production) The production Caddyfile already has the required configuration to serve uploaded media files: ``` file_server /media/* { root /home/deployer/Disfactory/backend } ``` This serves files from `MEDIA_ROOT` (e.g., `/home/deployer/Disfactory/backend/images/`) at URLs like `<https://api.disfactory.tw/media/uploads/abc123.jpg>`. *No Caddy changes required* - this is already configured. ### Frontend Setup To use the new backend upload instead of Imgur, update the frontend `.env`: VUE_APP_IMAGE_UPLOAD_PROVIDER=backend VUE_APP_IMAGE_UPLOAD_URL=<https://api.disfactory.tw/api/upload> ## API Response Format The endpoint returns an Imgur API-compatible response: { "data": { "link": "<https://api.disfactory.tw/media/uploads/abc123.jpg>", "deletehash": "abc123def456" }, "success": true, "status": 200 } ## Testing Run the tests using Docker Compose: docker compose -f docker-compose.dev.yml run --rm web pytest api/views/tests/test_image_upload.py -v ## Files Changed • `backend/api/views/image_upload.py` - New upload endpoint implementation • `backend/api/views/tests/test_image_upload.py` - Test suite • `backend/api/urls.py` - Route registration • `backend/docker-compose.yml` - Media volume configuration • `backend/.env.sample` - Environment variable documentation • `backend/.env.test` - Test environment configuration • `backend/docs/TESTING.md` - Testing documentation
github2 22:23:00
## Summary This PR implements a file system-based image upload API that provides an Imgur API-compatible interface, allowing the frontend to switch between Imgur and local storage seamlessly. ## Key Changes • Added a new `/api/upload` endpoint that stores images locally with validation for file type, size, and actual image content • Configured Docker volume for media file storage • Implemented comprehensive test coverage for the upload functionality ## Environment Variables The following existing environment variables are used by this feature: | Variable | Description | Example | | ----------------------------- | --------------------------------------------------------- | ----------------------------------------- | | DISFACTORY_BACKEND_MEDIA_ROOT | Media root path (already exists) | /home/deployer/Disfactory/backend/images/ | | DISFACTORY_BACKEND_DOMAIN | Backend domain for generating image URLs (already exists) | <https://disfactory.tw> | New variable for Docker deployment: | Variable | Description | Example | | -------------------- | -------------------------------------- | --------------------- | | DISFACTORY_MEDIA_DIR | Host directory for Docker volume mount | /var/disfactory/media | ## Setup ### Backend Setup Your existing `.env` should already have the required variables: DISFACTORY_BACKEND_MEDIA_ROOT="/home/deployer/Disfactory/backend/images/" DISFACTORY_BACKEND_DOMAIN="<https://disfactory.tw>" For Docker deployment, add the media directory mount: DISFACTORY_MEDIA_DIR=/path/to/media/storage ### Caddy Configuration (Production) The production Caddyfile already has the required configuration to serve uploaded media files: ``` file_server /media/* { root /home/deployer/Disfactory/backend } ``` This serves files from `MEDIA_ROOT` (e.g., `/home/deployer/Disfactory/backend/images/`) at URLs like `<https://api.disfactory.tw/media/uploads/abc123.jpg>`. *No Caddy changes required* - this is already configured. ### Frontend Setup To use the new backend upload instead of Imgur, update the frontend `.env`: VUE_APP_IMAGE_UPLOAD_PROVIDER=backend VUE_APP_IMAGE_UPLOAD_URL=<https://api.disfactory.tw/api/upload> ## API Response Format The endpoint returns an Imgur API-compatible response: { "data": { "link": "<https://api.disfactory.tw/media/uploads/abc123.jpg>", "deletehash": "abc123def456" }, "success": true, "status": 200 } ## Testing Run the tests using Docker Compose: docker compose -f docker-compose.dev.yml run --rm web pytest api/views/tests/test_image_upload.py -v ## Files Changed • `backend/api/views/image_upload.py` - New upload endpoint implementation • `backend/api/views/tests/test_image_upload.py` - Test suite • `backend/api/urls.py` - Route registration • `backend/docker-compose.yml` - Media volume configuration • `backend/.env.sample` - Environment variable documentation • `backend/.env.test` - Test environment configuration • `backend/docs/TESTING.md` - Testing documentation
github2 22:31:00
`<https://github.com/Disfactory/devops/commit/556a65288f312ce998be6bf5a99bec12cb392c58|556a6528>` - feat: init production migration plan, and ansible migration plan `<https://github.com/Disfactory/devops/commit/0a19b86c953f396c2602aea837d4eabbb7f8dc35|0a19b86c>` - chore: update docs `<https://github.com/Disfactory/devops/commit/2c8b72fa92c7beae56b010e898bb04fbc9fc7c58|2c8b72fa>` - chore: address reviewed issues to plan `<https://github.com/Disfactory/devops/commit/b764100cc0f0482d04f65be9248947d61188e5da|b764100c>` - chore: address more issues `<https://github.com/Disfactory/devops/commit/a69953f73fd43ef5651a72507df1c031e0a8f255|a69953f7>` - feat: simplify ansible roles `<https://github.com/Disfactory/devops/commit/1fbcb5e76d9f5eb9cfd4ec0ff709c848c3f401d0|1fbcb5e7>` - chore: remove imgur backup `<https://github.com/Disfactory/devops/commit/457352cbb388a84bb3dd1d91e06917a30069df9f|457352cb>` - docs: update ansible plan `<https://github.com/Disfactory/devops/commit/21ac096d606981796397027b704cfef7370d9591|21ac096d>` - docs: update implemnation docs `<https://github.com/Disfactory/devops/commit/3f697621fc0b0da32d1a293b59b2d23b031daaee|3f697621>` - docs: update docs `<https://github.com/Disfactory/devops/commit/573f85ea268ed03cb3444898b1b62c3f3c9d1450|573f85ea>` - feat: initial disfactory production ansible scripts implementation `<https://github.com/Disfactory/devops/commit/c53a7d4e73874937d4adb870f6b980f83f2eb6d1|c53a7d4e>` - feat: db restore and caddy file `<https://github.com/Disfactory/devops/commit/322629e6aaac54e318210689c5334ae393b17080|322629e6>` - feat: flatten production plan `<https://github.com/Disfactory/devops/commit/f33190ee5d5c73bcff6e2bab21833ef4c56b5138|f33190ee>` - feat: update playbook `<https://github.com/Disfactory/devops/commit/1d88f7b8ad0c06c39e1f17d21ddeba3b1f141a04|1d88f7b8>` - feat: migration done! switch to new host `<https://github.com/Disfactory/devops/commit/443a48e7b873b3ba2a7b2d996406cfec1572d7c7|443a48e7>` - feat: update cronjob setup `<https://github.com/Disfactory/devops/commit/eef620383d43ccd94dccf07292a6bf7849fa1644|eef62038>` - feat: bundle docs-exporter `<https://github.com/Disfactory/devops/commit/9cb8eaa51a77a82e78033662d84f0d490d8e4c6a|9cb8eaa5>` - feat: add spotdiff deployment config `<https://github.com/Disfactory/devops/commit/758c707a232aac18739dc7e2b499a5f0828ca29a|758c707a>` - Merge pull request #1 from Disfactory/feature/production-migration
github2 22:31:00
`<https://github.com/Disfactory/devops/commit/556a65288f312ce998be6bf5a99bec12cb392c58|556a6528>` - feat: init production migration plan, and ansible migration plan `<https://github.com/Disfactory/devops/commit/0a19b86c953f396c2602aea837d4eabbb7f8dc35|0a19b86c>` - chore: update docs `<https://github.com/Disfactory/devops/commit/2c8b72fa92c7beae56b010e898bb04fbc9fc7c58|2c8b72fa>` - chore: address reviewed issues to plan `<https://github.com/Disfactory/devops/commit/b764100cc0f0482d04f65be9248947d61188e5da|b764100c>` - chore: address more issues `<https://github.com/Disfactory/devops/commit/a69953f73fd43ef5651a72507df1c031e0a8f255|a69953f7>` - feat: simplify ansible roles `<https://github.com/Disfactory/devops/commit/1fbcb5e76d9f5eb9cfd4ec0ff709c848c3f401d0|1fbcb5e7>` - chore: remove imgur backup `<https://github.com/Disfactory/devops/commit/457352cbb388a84bb3dd1d91e06917a30069df9f|457352cb>` - docs: update ansible plan `<https://github.com/Disfactory/devops/commit/21ac096d606981796397027b704cfef7370d9591|21ac096d>` - docs: update implemnation docs `<https://github.com/Disfactory/devops/commit/3f697621fc0b0da32d1a293b59b2d23b031daaee|3f697621>` - docs: update docs `<https://github.com/Disfactory/devops/commit/573f85ea268ed03cb3444898b1b62c3f3c9d1450|573f85ea>` - feat: initial disfactory production ansible scripts implementation `<https://github.com/Disfactory/devops/commit/c53a7d4e73874937d4adb870f6b980f83f2eb6d1|c53a7d4e>` - feat: db restore and caddy file `<https://github.com/Disfactory/devops/commit/322629e6aaac54e318210689c5334ae393b17080|322629e6>` - feat: flatten production plan `<https://github.com/Disfactory/devops/commit/f33190ee5d5c73bcff6e2bab21833ef4c56b5138|f33190ee>` - feat: update playbook `<https://github.com/Disfactory/devops/commit/1d88f7b8ad0c06c39e1f17d21ddeba3b1f141a04|1d88f7b8>` - feat: migration done! switch to new host `<https://github.com/Disfactory/devops/commit/443a48e7b873b3ba2a7b2d996406cfec1572d7c7|443a48e7>` - feat: update cronjob setup `<https://github.com/Disfactory/devops/commit/eef620383d43ccd94dccf07292a6bf7849fa1644|eef62038>` - feat: bundle docs-exporter `<https://github.com/Disfactory/devops/commit/9cb8eaa51a77a82e78033662d84f0d490d8e4c6a|9cb8eaa5>` - feat: add spotdiff deployment config `<https://github.com/Disfactory/devops/commit/758c707a232aac18739dc7e2b499a5f0828ca29a|758c707a>` - Merge pull request #1 from Disfactory/feature/production-migration
github2 22:31:01
## Summary • capture current `disfactory` production inventory for reference • outline Ansible migration plan targeting Ubuntu 25.04 with updated Docker/`docker compose`/Caddy stack • document that the new `disfactory-production` host will have root access and SSH reachability back to `disfactory` ## Next Steps • build out baseline/infra roles per plan and validate on the new host • script rsync-based data transfer and database restore steps for cutover
github2 22:31:01
## Summary • capture current `disfactory` production inventory for reference • outline Ansible migration plan targeting Ubuntu 25.04 with updated Docker/`docker compose`/Caddy stack • document that the new `disfactory-production` host will have root access and SSH reachability back to `disfactory` ## Next Steps • build out baseline/infra roles per plan and validate on the new host • script rsync-based data transfer and database restore steps for cutover
github2 22:31:48
`<https://github.com/Disfactory/devops/commit/47ab35a06e49366233efe4cf1c33ba1c838443d5|47ab35a0>` - chore: update image upload path for production caddy
github2 22:31:48
`<https://github.com/Disfactory/devops/commit/47ab35a06e49366233efe4cf1c33ba1c838443d5|47ab35a0>` - chore: update image upload path for production caddy
github2 22:39:55
`<https://github.com/Disfactory/Disfactory/commit/12f40b292b6d0d902ee466efe3b28efe76b68d8d|12f40b29>` - chore: sync with production db version
github2 22:39:55
`<https://github.com/Disfactory/Disfactory/commit/12f40b292b6d0d902ee466efe3b28efe76b68d8d|12f40b29>` - chore: sync with production db version
github2 22:48:26
• *chore: use pip to install poetry* • *fix: poetry version for compatibiltiy*
github2 22:48:26
• *chore: use pip to install poetry* • *fix: poetry version for compatibiltiy*
github2 23:01:19
`<https://github.com/Disfactory/Disfactory/commit/a1e8d4ac7c0bca32c1682dc2a89f2895d1527f1c|a1e8d4ac>` - chore: use pip to install poetry `<https://github.com/Disfactory/Disfactory/commit/69a61cbb105114751d52d7c5ad7180b9d4b38dbf|69a61cbb>` - fix: poetry version for compatibiltiy `<https://github.com/Disfactory/Disfactory/commit/5718d445791a5b3f6b2d157cabf31456571dbb5c|5718d445>` - chore: fix caddy dockerfile `<https://github.com/Disfactory/Disfactory/commit/9e13307a27156df3800e2d41c9af09d95edba955|9e13307a>` - Merge pull request #694 from Disfactory/chore/fix-image-building
github2 23:01:19
`<https://github.com/Disfactory/Disfactory/commit/a1e8d4ac7c0bca32c1682dc2a89f2895d1527f1c|a1e8d4ac>` - chore: use pip to install poetry `<https://github.com/Disfactory/Disfactory/commit/69a61cbb105114751d52d7c5ad7180b9d4b38dbf|69a61cbb>` - fix: poetry version for compatibiltiy `<https://github.com/Disfactory/Disfactory/commit/5718d445791a5b3f6b2d157cabf31456571dbb5c|5718d445>` - chore: fix caddy dockerfile `<https://github.com/Disfactory/Disfactory/commit/9e13307a27156df3800e2d41c9af09d95edba955|9e13307a>` - Merge pull request #694 from Disfactory/chore/fix-image-building
github2 23:06:21
*Is your feature request related to a problem? Please describe.* 因應回報系統改版需求,附件檔案中的地號需要轉成經緯度 我們需要把這些案件(地號)的中心點,放到回報系統上 *Describe the solution you'd like* 使用<https://easymap.moi.gov.tw/Index|地籍圖資系統> <https://github.com/user-attachments/files/24770031/_disfactory.xlsx|違反土地使用個案_disfactory.xlsx>
github2 23:06:21
*Is your feature request related to a problem? Please describe.* 因應回報系統改版需求,附件檔案中的地號需要轉成經緯度 我們需要把這些案件(地號)的中心點,放到回報系統上 *Describe the solution you'd like* 使用<https://easymap.moi.gov.tw/Index|地籍圖資系統> <https://github.com/user-attachments/files/24770031/_disfactory.xlsx|違反土地使用個案_disfactory.xlsx>
github2 23:11:05
*Is your feature request related to a problem? Please describe.* 因應回報系統改版需求,附件檔案中的地號需要轉成經緯度 我們需要把這些案件(地號)的中心點,放到回報系統上 *Describe the solution you'd like* 使用<https://easymap.moi.gov.tw/Index|地籍圖資系統> <https://github.com/user-attachments/files/24770031/_disfactory.xlsx|違反土地使用個案_disfactory.xlsx>
github2 23:11:05
*Is your feature request related to a problem? Please describe.* 因應回報系統改版需求,附件檔案中的地號需要轉成經緯度 我們需要把這些案件(地號)的中心點,放到回報系統上 *Describe the solution you'd like* 使用<https://easymap.moi.gov.tw/Index|地籍圖資系統> <https://github.com/user-attachments/files/24770031/_disfactory.xlsx|違反土地使用個案_disfactory.xlsx>
github2 23:41:11
&gt; 根據新設計需要的 API 改動: &gt; &gt; --- &gt; &gt; # Factory Timeline API Extension Plan &gt; &gt; ## Summary &gt; &gt; Extend the factory API to support a new frontend displaying factory history/timeline with: &gt; &gt; 1. Display status transitions (狀態轉變) &gt; 2. User situation reports (使用者回報記錄) &gt; 3. Follow-ups for user (新狀況回報) - already implemented &gt; 4. Image upload batches (照片上傳批次) &gt; &gt; ## Approach: New `/timeline` Endpoint + Enhanced `report_records` &gt; &gt; ### Why a New Endpoint? &gt; &gt; • Keep existing API backward-compatible &gt; • Timeline data is expensive to compute (multiple joins) &gt; • Not all API consumers need timeline data &gt; • Better separation of concerns &gt; &gt; --- &gt; &gt; ## Implementation Plan &gt; &gt; ### 1. Enhance ReportRecordSerializer (backend/api/serializers.py) &gt; &gt; Add `action_type` and `action_body` fields to existing serializer: &gt; &gt; class ReportRecordSerializer(ModelSerializer): &gt; class Meta: &gt; model = ReportRecord &gt; fields = [ &gt; "id", &gt; "action_type", # NEW &gt; "action_body", # NEW &gt; "created_at", &gt; "others", &gt; ] &gt; &gt; *Impact:* Backward compatible (additive fields only) &gt; &gt; --- &gt; &gt; ### 2. Create New Timeline Serializers (backend/api/serializers.py) &gt; &gt; Add these new serializers: &gt; &gt; • `StatusTransitionSerializer` - Parse FollowUp notes containing " -&gt; " &gt; • `ImageBatchSerializer` - Group images by upload date &gt; • `FollowUpForUserSerializer` - Serialize for_user FollowUps (enhanced from existing inline) &gt; &gt; --- &gt; &gt; ### 3. Create New View (backend/api/views/factory_timeline_r.py) &gt; &gt; New endpoint: `GET /api/factories/&lt;factory_id&gt;/timeline` &gt; &gt; *Response structure:* &gt; &gt; { &gt; "factory_id": "uuid", &gt; "display_number": 123, &gt; "status_transitions": [ &gt; { &gt; "id": 1, &gt; "from_status": "已檢舉", &gt; "to_status": "已排程稽查", &gt; "created_at": "2023-01-15T10:30:00Z", &gt; "document_id": 456 &gt; } &gt; ], &gt; "user_reports": [ &gt; { &gt; "id": 1, &gt; "action_type": "UPDATE", &gt; "action_body": {"name": "...", "factory_type": "2-1"}, &gt; "created_at": "2023-01-10T08:00:00Z", &gt; "others": "備註" &gt; }, &gt; { &gt; "id": 2, &gt; "action_type": "POST_IMAGE", &gt; "action_body": {"url": "...", "DateTimeOriginal": "..."}, &gt; "created_at": "2023-01-11T09:00:00Z", &gt; "others": "" &gt; } &gt; ], &gt; "follow_ups_for_user": [ &gt; { &gt; "id": 1, &gt; "note": "進度回報內容", &gt; "created_at": "2023-01-20T14:00:00Z" &gt; } &gt; ], &gt; "image_batches": [ &gt; { &gt; "date": "2023-01-01", &gt; "count": 2, &gt; "images": [{"id": "...", "image_path": "...", "created_at": "..."}] &gt; } &gt; ] &gt; } &gt; &gt; *Query optimization:* Use `prefetch_related` with `Prefetch` objects to minimize N+1 queries (5 queries total regardless of data volume). &gt; &gt; --- &gt; &gt; ### 4. Update URL Configuration (backend/api/urls.py) &gt; &gt; Add new route: &gt; &gt; path("factories/&lt;factory_id&gt;/timeline", get_factory_timeline), &gt; &gt; --- &gt; &gt; ### 5. Update Views *init*.py &gt; &gt; Export the new view function. &gt; &gt; --- &gt; &gt; ## Files to Modify &gt; &gt; | File | Action | Description | &gt; | --------------------------------------- | ------ | -------------------------------------------------------------------------- | &gt; | backend/api/serializers.py | Modify | Add action_type/action_body to ReportRecordSerializer; add new serializers | &gt; | backend/api/views/factory_timeline_r.py | Create | New timeline endpoint view | &gt; | backend/api/views/__init__.py | Modify | Export new view | &gt; | backend/api/urls.py | Modify | Add timeline URL pattern | &gt; &gt; --- &gt; &gt; ## Key Implementation Details &gt; &gt; ### Status Transitions Detection &gt; &gt; FollowUps are auto-generated in `admin/document.py:239-247` with pattern: &gt; &gt; ``` &gt; "{old_status} -&gt; {new_status}" &gt; &gt; ``` &gt; &gt; Filter with: `FollowUp.objects.filter(note__contains=' -&gt; ')` &gt; &gt; ### Image Batching &gt; &gt; Group by `created_at` date (upload time), not `orig_time` (photo time): &gt; &gt; for date, group in groupby(sorted_images, key=lambda x: x.created_at.date()): &gt; batches.append({'date': date.isoformat(), 'count': len(list(group)), ...}) &gt; &gt; ### User Reports Filter &gt; &gt; Include `ReportRecord` with: &gt; &gt; • `action_type='UPDATE'` - User attribute changes (factory type, description, etc.) &gt; • `action_type='POST_IMAGE'` - Image uploads &gt; &gt; Exclude: &gt; &gt; • `action_type='POST'` - Factory creation (not a "situation report") &gt; &gt; Filter: `ReportRecord.objects.filter(action_type__in=['UPDATE', 'POST_IMAGE'])` &gt; &gt; --- &gt; &gt; ## Verification Steps &gt; &gt; 1. Create test factory with documents, follow-ups, images, and report records &gt; 2. `GET /api/factories/&lt;id&gt;/timeline` returns all four sections &gt; 3. Status transitions only show FollowUps with " -&gt; " pattern &gt; 4. User reports only show UPDATE action types &gt; 5. Image batches group correctly by date &gt; 6. `GET /api/factories/&lt;id&gt;/report_records` now includes `action_type` and `action_body` &gt; 7. Verify query count with Django Debug Toolbar (should be ~5 queries) _Originally posted by <https://github.com/Yukaii|@Yukaii> in <https://github.com/Disfactory/Disfactory/issues/655#issuecomment-3778952396|#655>_
github2 23:41:11
&gt; 根據新設計需要的 API 改動: &gt; &gt; --- &gt; &gt; # Factory Timeline API Extension Plan &gt; &gt; ## Summary &gt; &gt; Extend the factory API to support a new frontend displaying factory history/timeline with: &gt; &gt; 1. Display status transitions (狀態轉變) &gt; 2. User situation reports (使用者回報記錄) &gt; 3. Follow-ups for user (新狀況回報) - already implemented &gt; 4. Image upload batches (照片上傳批次) &gt; &gt; ## Approach: New `/timeline` Endpoint + Enhanced `report_records` &gt; &gt; ### Why a New Endpoint? &gt; &gt; • Keep existing API backward-compatible &gt; • Timeline data is expensive to compute (multiple joins) &gt; • Not all API consumers need timeline data &gt; • Better separation of concerns &gt; &gt; --- &gt; &gt; ## Implementation Plan &gt; &gt; ### 1. Enhance ReportRecordSerializer (backend/api/serializers.py) &gt; &gt; Add `action_type` and `action_body` fields to existing serializer: &gt; &gt; class ReportRecordSerializer(ModelSerializer): &gt; class Meta: &gt; model = ReportRecord &gt; fields = [ &gt; "id", &gt; "action_type", # NEW &gt; "action_body", # NEW &gt; "created_at", &gt; "others", &gt; ] &gt; &gt; *Impact:* Backward compatible (additive fields only) &gt; &gt; --- &gt; &gt; ### 2. Create New Timeline Serializers (backend/api/serializers.py) &gt; &gt; Add these new serializers: &gt; &gt; • `StatusTransitionSerializer` - Parse FollowUp notes containing " -&gt; " &gt; • `ImageBatchSerializer` - Group images by upload date &gt; • `FollowUpForUserSerializer` - Serialize for_user FollowUps (enhanced from existing inline) &gt; &gt; --- &gt; &gt; ### 3. Create New View (backend/api/views/factory_timeline_r.py) &gt; &gt; New endpoint: `GET /api/factories/&lt;factory_id&gt;/timeline` &gt; &gt; *Response structure:* &gt; &gt; { &gt; "factory_id": "uuid", &gt; "display_number": 123, &gt; "status_transitions": [ &gt; { &gt; "id": 1, &gt; "from_status": "已檢舉", &gt; "to_status": "已排程稽查", &gt; "created_at": "2023-01-15T10:30:00Z", &gt; "document_id": 456 &gt; } &gt; ], &gt; "user_reports": [ &gt; { &gt; "id": 1, &gt; "action_type": "UPDATE", &gt; "action_body": {"name": "...", "factory_type": "2-1"}, &gt; "created_at": "2023-01-10T08:00:00Z", &gt; "others": "備註" &gt; }, &gt; { &gt; "id": 2, &gt; "action_type": "POST_IMAGE", &gt; "action_body": {"url": "...", "DateTimeOriginal": "..."}, &gt; "created_at": "2023-01-11T09:00:00Z", &gt; "others": "" &gt; } &gt; ], &gt; "follow_ups_for_user": [ &gt; { &gt; "id": 1, &gt; "note": "進度回報內容", &gt; "created_at": "2023-01-20T14:00:00Z" &gt; } &gt; ], &gt; "image_batches": [ &gt; { &gt; "date": "2023-01-01", &gt; "count": 2, &gt; "images": [{"id": "...", "image_path": "...", "created_at": "..."}] &gt; } &gt; ] &gt; } &gt; &gt; *Query optimization:* Use `prefetch_related` with `Prefetch` objects to minimize N+1 queries (5 queries total regardless of data volume). &gt; &gt; --- &gt; &gt; ### 4. Update URL Configuration (backend/api/urls.py) &gt; &gt; Add new route: &gt; &gt; path("factories/&lt;factory_id&gt;/timeline", get_factory_timeline), &gt; &gt; --- &gt; &gt; ### 5. Update Views *init*.py &gt; &gt; Export the new view function. &gt; &gt; --- &gt; &gt; ## Files to Modify &gt; &gt; | File | Action | Description | &gt; | --------------------------------------- | ------ | -------------------------------------------------------------------------- | &gt; | backend/api/serializers.py | Modify | Add action_type/action_body to ReportRecordSerializer; add new serializers | &gt; | backend/api/views/factory_timeline_r.py | Create | New timeline endpoint view | &gt; | backend/api/views/__init__.py | Modify | Export new view | &gt; | backend/api/urls.py | Modify | Add timeline URL pattern | &gt; &gt; --- &gt; &gt; ## Key Implementation Details &gt; &gt; ### Status Transitions Detection &gt; &gt; FollowUps are auto-generated in `admin/document.py:239-247` with pattern: &gt; &gt; ``` &gt; "{old_status} -&gt; {new_status}" &gt; &gt; ``` &gt; &gt; Filter with: `FollowUp.objects.filter(note__contains=' -&gt; ')` &gt; &gt; ### Image Batching &gt; &gt; Group by `created_at` date (upload time), not `orig_time` (photo time): &gt; &gt; for date, group in groupby(sorted_images, key=lambda x: x.created_at.date()): &gt; batches.append({'date': date.isoformat(), 'count': len(list(group)), ...}) &gt; &gt; ### User Reports Filter &gt; &gt; Include `ReportRecord` with: &gt; &gt; • `action_type='UPDATE'` - User attribute changes (factory type, description, etc.) &gt; • `action_type='POST_IMAGE'` - Image uploads &gt; &gt; Exclude: &gt; &gt; • `action_type='POST'` - Factory creation (not a "situation report") &gt; &gt; Filter: `ReportRecord.objects.filter(action_type__in=['UPDATE', 'POST_IMAGE'])` &gt; &gt; --- &gt; &gt; ## Verification Steps &gt; &gt; 1. Create test factory with documents, follow-ups, images, and report records &gt; 2. `GET /api/factories/&lt;id&gt;/timeline` returns all four sections &gt; 3. Status transitions only show FollowUps with " -&gt; " pattern &gt; 4. User reports only show UPDATE action types &gt; 5. Image batches group correctly by date &gt; 6. `GET /api/factories/&lt;id&gt;/report_records` now includes `action_type` and `action_body` &gt; 7. Verify query count with Django Debug Toolbar (should be ~5 queries) _Originally posted by <https://github.com/Yukaii|@Yukaii> in <https://github.com/Disfactory/Disfactory/issues/655#issuecomment-3778952396|#655>_