drf-endpoint-scaffold
> متى تُستعمل: "DRF endpoints under /api/v1/: serializer, view, selector, RBAC, tests. Trigger: DRF, endpoint, REST API, APIView. نقطة API جديدة، serializer، api/urls.py"
SKILL.md.claude/skills/drf-endpoint-scaffold/SKILL.md
GitHubمتى تُستعمل: "DRF endpoints under /api/v1/: serializer, view, selector, RBAC, tests. Trigger: DRF, endpoint, REST API, APIView. نقطة API جديدة، serializer، api/urls.py"
بناءُ نقطة REST في SchoolOS
الغرض: كلُّ نقطةٍ جديدة تولد بصلاحيّة دورٍ صريحة، ومقيّدةً بمدرسة المستخدم، بعددِ استعلاماتٍ ثابت، ومع اختبارٍ يثبت الثلاثة — وتعبر حرّاسَ CI من أوّل دفع.
السببُ الحيّ: مراجعةُ 2026-09-13 وجدت نقاطاً تكتفي بـ«مسجَّل الدخول» فبلغ الطالبُ سجلَّ حضور المدرسة ودرجاتِ زملائه (المصدر: tests/test_every_route_is_guarded.py، tests/test_borrowings_api_scope.py).
متى تُستعمل ومتى لا
- نعم: نقطةٌ جديدة في
/api/v1/(المسجّلة فيshschool/urls.pyباسمapi_v1)، أو إنشاءٌ يُضاف لنقطةٍ قائمة. - لا:
/api/students/search/وأخواتُها فيoperations/api_urls.py(نقاطٌ داخليّةٌ للشاشات)، ولا شاشاتُ HTML.
الإجراء
- اقرأ الأعراف في
references/00-api-conventions.md(الصلاحيّات، الترقيم، الوسائط، سقوفُ الملفّات). - ولّد الهيكل من جذر شجرتك (بايثون المضيف يكفي؛ لا يحتاج Django):
للإنشاء:python .claude/skills/drf-endpoint-scaffold/scripts/scaffold_endpoint.py --app transport --model BusRoute \ --name bus-routes --fields id,area_name --school-path bus__school --order-by area_name,id \ --permission IsStaffMember --readonly--write-fields a,b --unique-key bبدل--readonly. يتحقّق من وجود التطبيق والنموذج والصلاحيّة، ويرفض__all__وschoolفي حقول الإنشاء، وينذر بغياب قيدٍ فريدٍ أو ترتيبٍ أو حقلschool. - وزّع الكتل من
<app>/_scaffold_<name>.py: الـselector إلى ملفّ selectors المجال، والخدمةُ إلى خدمات التطبيق، والـserializer والـview إلى وحدةٍ جديدةapi/views_<name>.py(السابقةapi/views_erasure.py)، والمسارُ إلىapi/urls.py. ثمّ احذف ملفَّ التجهيز — يتجاهله git (_*.py) فلن يذكّرك بهgit status. - اكتب ما عليه
NotImplementedErrorفيtests/test_<name>_api.py(make_row، وpayloadللإنشاء) — الاختبارُ يسقط حتّى تكتبه، عمداً. - أضِف
select_related/prefetch_relatedلكلّ علاقةٍ يقرؤها الـserializer حتّى يخضرّ اختبارُ الثبات (مهارة nplus1-hunter). - شغّل في حاوية جلستك: اختبارَك، ثمّ
tests/test_every_route_is_guarded.pyوtests/test_api_contract.pyوtests/test_file_size.py(بأمر الاختبار فيCLAUDE.md)، ثمّ ruff بإصدار CI. التفصيل فيreferences/02-testing.md.
القواعد وأسبابها
- صلاحيّةُ دورٍ من
api/permissions.py، لاIsAuthenticatedوحدها. الحارسُtests/test_every_route_is_guarded.pyيسقط أيَّ مسارٍ صلاحيّتُه «مسجَّل الدخول» أو «الجميع» ما لم يُسمَّ فيGUARDED_INSIDE/OPEN_BY_DESIGNبسببه. وpermission_classes = [IsTeacherOrAdmin]تستبدل الافتراضيَّ (IsAuthenticatedفيREST_FRAMEWORK) وهي تفحص المصادقةَ بنفسها. - المدرسةُ من المستخدم لا من الطلب.
_school(request)=request.user.get_school()(api/views.py)، والاستعلامُ يبدأ منها بالمسار الصحيح للنموذج (school، أوclass_group__school، أوbus__school). ولاschoolفي حقول الكتابة: من يرسلها يكتب في مدرسةٍ أخرى. - حقولٌ صريحة، لا
__all__. الإضافةُ اللاحقة لحقلٍ شخصيٍّ في النموذج تخرج في الـAPI بلا مراجعة؛ القائمةُ الصريحة تجعل كلَّ عرضٍ قراراً (مهارة pdppl-pii-audit). - الإنشاءُ idempotent بقيدٍ في القاعدة.
get_or_createبلاUniqueConstraintعلى المفتاح يسبق فيه طلبان متزامنان فيتكرّر الصفّ (وثائق Django:get_or_create). مثالُ الإصدار السابق (Subjectعلىcode) بلا قيدٍ كهذا، وcodeفيهblank=True— فالمفتاحُ الفارغ يدمج موادَّ مختلفة (operations/models/schedule.py). - ترتيبٌ حتميٌّ لكلّ قائمةٍ مرقّمة. نموذجٌ بلا
Meta.ordering(مثلBusRoute) يكرّر صفوفاً ويُسقط أخرى بين الصفحات؛ مرّر--order-by. - لا تكبر
api/views.py: 915 سطراً وسقفُ الملفّ الجديد 1000 (tests/file_size_ratchet.py، 2026-09-28) — النقطةُ الجديدة في وحدتها. - ما يزيد عن 300ms مهمّةٌ خلفيّة؛ والتصديرُ بالآليّة المركزيّة (ADR-0007،
core/exports/) لا في طلب API.
فخاخٌ حقيقيّة
- خطأ:
client.force_authenticate(user)أوAPIClientفي الاختبار. الصواب:Client().force_login(user)—SchoolPermissionMiddlewareيفحص جلسةَ Django ويردّ/api/بـ401 قبل DRF (core/middleware.py)؛ مُثبَت: الاختبارُ القديم أخذ 401 حيث توقّع 200 و403. - خطأ: ملفُّ الاختبار في
<app>/tests/. الصواب:tests/test_<name>_api.py— fixtures الأدوار فيtests/conftest.pyلا تُرى خارجه (الاختبارُ القديم: «fixture 'teacher_user' not found»). - خطأ:
django_assert_num_queries(4)برقمٍ «يُضبط بعد أوّل تشغيل». الصواب: صفٌّ واحد مقابل ثلاثة بعد إحماء (نمطtests/test_n_plus_one_queries.py). - خطأ:
assert r.status_code in (201, 400)«مؤقّتاً». الصواب:NotImplementedErrorحتّى تُكتب الحمولة — القيمةُ المؤقّتة يجب أن تُفشل (ذاكرةfeedback_placeholders_must_fail.md). - خطأ:
fields = "__all__"في serializer الكتابة. الصواب: قائمةٌ بلاschool، واختبارُtest_school_cannot_be_injected. - خطأ:
.order_by("-created_at")لكلّ نموذج. الصواب: كثيرٌ من النماذج بلاcreated_at(Subject،BusRoute) —Meta.orderingأو--order-by. - خطأ: «النماذجُ ترث
SchoolScopedModel». الصواب: أربعةُ نماذج فقط ترثه؛ أغلبُها حقلُschoolصريح أو مدرسةٌ عبر علاقة. - خطأ: «المسار الوحيد
D:\shschool_mvp». الصواب: اعمل في شجرتك (CLAUDE.md، القاعدة رقم 1).
المراجع
| الملف | متى تقرأه |
|---|---|
references/00-api-conventions.md |
قبل التصميم: المصادقة والصلاحيّات والترقيم والوسائط والحرّاس التي تمرّ عليها كلُّ نقطة |
references/01-layers.md |
حين توزّع الكتل: ما يذهب إلى selector وخدمةٍ وserializer وview، ومثالٌ كاملٌ مصحَّح |
references/02-testing.md |
حين تكتب الاختبار أو تفهم سقوطه، وما يشغّله CI على النقطة |
references/99-test-cases.md |
عند تعديل الوصف أو الجسم أو السكربت |