This project is YuanPeiProFile(YPPF), a student and room management platform. This project contains two parts, the website version front-end and back-end and the backend API for Wechat Miniprogram. The website is implemented using Django + template view, with a small portion using DjangoREST. The miniapp backend is implemented entirely with DjangoREST. The front-end of the miniapp can be found at https://github.com/HelloWorldZTR/YPPF-mini
Prefer the repository's Dev Container setup. The host only needs Docker with
docker compose subcommand. The container image is based on Python 3.11 and
installs requirements.txt; Compose also starts a MySQL 8 server, waits for it
to become healthy, mounts the repository at /workspace, and sets
YPPF_DEBUG=true plus the development database connection variables. The
optional .devcontainer/dev_requirements.txt is installed automatically by a
Dev Container client, but currently only adds IPython and is not required for
tests.
From the repository root, create a local configuration only when it does not
already exist. Never overwrite an existing config.json, because it is
ignored by Git and may contain developer secrets. The script fills in the
Compose database defaults (yppf, root, secret, and host mysql):
test -f config.json || bash scripts/default_config.shWhen using the VS Code / Cursor Dev Container (.devcontainer/),
postCreateCommand / postStartCommand run
scripts/devcontainer_ensure_db.sh after ensuring config.json exists:
- Empty database:
migrate→ import repository-rootdev_sample.sql. - Populated database: keep existing data (no DROP); only
migrate. - Superusers are not created automatically; create one manually when
/admin/is needed (python scripts/create_dev_superuser.pyorpython manage.py createsuperuser).
Website sample accounts in the dump use password test (for example
S000001). To wipe and reload the sample dump after a dump fix or when the
MySQL volume still holds old data, run inside the app container:
bash scripts/devcontainer_reset_sample_db.shHost-only docker compose ... up --build does not run those hooks; use
the commands below (or open the Dev Container) instead.
Build and start the development containers, then run Django commands inside
the yppf service:
docker compose -f .devcontainer/docker-compose.yml up -d --build
docker compose -f .devcontainer/docker-compose.yml exec -T yppf python manage.py migrate
docker compose -f .devcontainer/docker-compose.yml exec -T yppf python manage.py testUse exec -T for non-interactive LLM tool calls. After model changes, create
and apply migrations before rerunning tests:
docker compose -f .devcontainer/docker-compose.yml exec -T yppf python manage.py makemigrations
docker compose -f .devcontainer/docker-compose.yml exec -T yppf python manage.py migrateInspect failures and stop the environment with:
docker compose -f .devcontainer/docker-compose.yml logs mysql yppf
docker compose -f .devcontainer/docker-compose.yml downDo not add -v to down unless intentionally deleting the persistent
development database. The CI baseline is the same Django test runner,
python manage.py test.
boot/settings.py is the authoritative Django application registry
(INSTALLED_APPS), and boot/urls.py is the root URL configuration. When
adding a Django app, register it in INSTALLED_APPS; when exposing HTTP
routes, add the app's urls.py to boot/urls.py. The main source directories
are listed below. For the mini-program backend's internal layout and routing
rules, read api/AGENTS.md.
| Directory | Responsibility | Route registration |
|---|---|---|
boot/ |
Django project bootstrap: settings, root configuration loading, root URLs, and WSGI entry point. Database settings prefer DB_* environment variables and otherwise read config.json. |
ROOT_URLCONF points to boot/urls.py; this is the top-level router. |
generic/ |
Shared identity and authentication layer. Defines the custom User, permission blacklist, credit/YQPoint records, WeChat profiles, login/logout, redirects, and health check. |
generic/urls.py is included at /, providing /, /login/, /logout/, /healthcheck/, and /redirect/. |
app/ |
Core website and domain app. Contains people and organizations, positions, activities and participation, notifications, courses, academic map/Q&A, comments, YQPoint prize pools, homepage images, their admin pages, forms, jobs, and template views. Large features are split into files such as activity_views.py, org_views.py, course_views.py, and academic_views.py; shared models remain in app/models.py. |
app/urls.py is included both at / and /yppf/ for legacy compatibility. Add website routes in this file and place the implementation in the matching feature view module. |
api/ |
Django REST Framework backend for the WeChat mini-program. See its scoped AGENTS.md for module responsibilities and development rules. |
boot/urls.py mounts its root router at /api/. |
Appointment/ |
Room reservation subsystem (historically YPUnderground): rooms, regular and long-term appointments, checkout/review, violations/card checks, hardware-facing door/camera/display APIs, summaries, and appointment scheduling/reminders. Lower-level appointment state logic is in Appointment/appoint/. |
Appointment/urls.py is namespaced as Appointment and mounted at /underground/. |
yp_library/ |
Library/book-room subsystem: readers, books, lending records, searches, synchronization with an external library database, and related jobs/commands. | yp_library/urls.py is mounted at /yplibrary/. |
questionnaire/ |
Generic survey engine with surveys, questions, choices, answer sheets/text, serializers, object permissions, and result-export command. | Its DRF router in questionnaire/urls.py is mounted at /questionnaire/. |
dormitory/ |
Dormitory data, agreements, assignments, assignment algorithm/import commands, routine-QA and result pages, plus read-only REST viewsets. | dormitory/urls.py is mounted at /dormitory/. |
feedback/ |
Feedback types and feedback records, website views, admin integration, utilities, and scheduled processing. | feedback/urls.py is included at /, producing routes such as /feedback/. |
record/ |
Page/module event tracking and the project logging facade in record/log/. Runtime log files are not source code and go in the top-level log/ directory. |
record/urls.py is included at /, exposing /eventTrackingFunc/ and /logs/. |
achievement/ |
Achievement definitions, unlock records, unlock APIs used by other apps, scheduled unlock jobs, and initialization/upload management commands. | No independent URL configuration; callers use achievement/api.py. |
semester/ |
Semester types and current/next-semester helpers, administration, and rollover jobs. | No independent URL configuration; callers use semester/api.py. |
scheduler/ |
Shared APScheduler integration, persistent Django job store, decorators/add/cancel helpers, RPC-backed executor, and scheduler health/collection management commands. Other apps declare periodic work in their own jobs.py. |
No HTTP URLs. Start the worker with python manage.py runscheduler; commands are under scheduler/management/commands/. |
dm/ |
Data-management utilities and fixtures for importing/exporting people, organizations, semesters, appointments, graduate status, and annual summaries. | No HTTP URLs; use its Django management commands such as dump and load. |
utils/ |
Framework-level reusable code: configuration access, hashing, HTTP/auth wrappers, model managers/query helpers, permissions, context managers, and base views. It should not contain feature-specific business logic. | No routes or models of its own. |
extern/ |
Cross-application integrations, chiefly WeChat communication, external logging, configuration, and multithreading helpers. Feature-specific external integrations may instead live in <app>/extern/. |
No routes. |
Across Django apps, models.py, admin.py, apps.py, and migrations/ have
their standard Django meanings. management/commands/<name>.py registers a
python manage.py <name> command. Tests may be in tests.py, test/, or
tests/; run them through the project-wide Django test command rather than
assuming a single layout. A feature's jobs.py contains scheduler-discovered
jobs, extern/ contains feature-scoped external services, and utils.py or a
utils/ package contains internal helpers. In app/, view/ contains base
view classes while the top-level *_views.py files contain feature endpoints.
Repository support and presentation directories are:
| Directory | Contents |
|---|---|
templates/ |
Shared Django templates, grouped by feature (activity, course, dormitory, feedback, Appointment, and others). |
static/ |
Versioned CSS, JavaScript, fonts, images, and third-party front-end assets. |
media/ |
Development/user-uploaded content such as avatars, activity photos, prizes, and wallpapers; served by Django only in development via boot/urls.py. |
log/ |
Generated runtime and per-user logs; do not place application source here. |
scripts/ |
Shell/Python maintenance helpers, including development config.json generation and test-data/migration cleanup scripts. |
docs/ |
Sphinx documentation source, translations, and build scripts. |
.devcontainer/ and Dockerfile |
Reproducible Python/MySQL development environment described above. |
.github/ |
GitHub Actions test/deployment workflows and their CI-specific Compose configuration. |
Route prefixes currently registered in boot/urls.py are /admin/,
/api-auth/, /api/, /yppf/, /underground/, /yplibrary/,
/questionnaire/, and /dormitory/, plus root-level routes from generic,
record, app, and feedback. Before adding a route, check both the root
router and the target app router to avoid duplicate names or paths. REST
module details are documented in api/AGENTS.md.
generic.User is the authentication/account layer and is the configured
AUTH_USER_MODEL. It extends Django's AbstractUser and owns credentials,
sessions, Django groups and permissions, staff/superuser flags, credit,
YQPoint, display/search fields, and the coarse utype. Always import this
model from generic.models (or from an application models.py that
deliberately re-exports it); do not import Django's built-in User or create
a second account model.
The account is not, by itself, the domain entity shown on profile pages. The current domain mapping is:
| Layer | Representation | Meaning |
|---|---|---|
| Account | generic.User |
Login identity and cross-application principal. username is normally a student/staff ID or an organization account ID. |
| Person | app.NaturalPerson |
One-to-one profile through person_id. Person is only a type alias for NaturalPerson, not another table or subclass. |
| Person kind | NaturalPerson.identity |
Distinguishes student from teacher/staff. Teachers, activity reviewers, organization supervisors, course auditors, and accommodation instructors are not separate user subclasses; they are natural people selected by identity, status, configuration, a relation such as OrganizationType.incharge/Activity.examine_teacher, or a feature permission. |
| Person status | NaturalPerson.status |
In-study/in-service, graduated/retired, accommodation instructor, postponed graduation, or leave of absence. These values are business state, not authentication classes. |
| Organization | app.Organization |
A separate one-to-one profile through organization_id; it has its own User account and can become the current session identity. |
| Membership/role | app.Position |
Connects a natural person to an organization for a year and semester. pos is rank, is_admin grants account-switch/management responsibility, and status records in-service/departed membership. It is not an authentication account. |
| Pre-registration | app.Freshman |
Staging data used during freshman registration. It is not a User and is no longer the authoritative person record after registration. |
The inheritance and containment relations are summarized below. Dashed arrows are aliases or registration-time transitions, not database inheritance or foreign keys.
classDiagram
direction TB
class AbstractUser {
<<Django>>
}
class User {
username
utype
is_active
active
}
class NaturalPerson {
person_id
identity
status
}
class Person {
<<type alias>>
}
class Organization {
organization_id
otype
status
}
class OrganizationType {
incharge
}
class Position {
person
org
pos
is_admin
status
year
semester
}
class Freshman {
sid
status
}
AbstractUser <|-- User : Python inheritance
NaturalPerson "0..1" --> "1" User : person_id (OneToOne)
Organization "0..1" --> "1" User : organization_id (OneToOne)
Person ..> NaturalPerson : TypeAlias
Position "*" --> "1" NaturalPerson : person (ForeignKey)
Position "*" --> "1" Organization : org (ForeignKey)
Organization "*" --> "1" OrganizationType : otype (ForeignKey)
OrganizationType "*" --> "1" NaturalPerson : incharge (ForeignKey)
Freshman ..> User : registration creates account
Freshman ..> NaturalPerson : registration creates profile
note for NaturalPerson "Student, teacher/staff, reviewer, auditor, and accommodation-instructor distinctions are fields, relations, configuration, or permissions—not subclasses."
User.Type.PERSON, STUDENT, and TEACHER are all treated as person account
types by User.Type.Persons() and User.is_person(). Historical data and
callers use both the broad PERSON value and the more specific student or
teacher values. Consequently, use user.is_person(), user.is_org(), and
NaturalPerson.identity/is_teacher() according to the question being
asked; do not assume utype == PERSON, and do not assume utype alone is the
authoritative teacher role. User.is_valid() has a narrower meaning: the
account type is neither SPECIAL nor UNAUTHORIZED, so application code may
look up its corresponding person or organization profile. It does not mean
that the account is active, authenticated, or authorized for a feature.
Resolve a domain profile through app.utils.get_classified_user() (the legacy
alias is get_person_or_org()) or the profile manager's get_by_user().
These interfaces understand all person-type utype values and support
update=True for row locking and activate=True for domain-active filtering.
When adding another one-to-one account profile, implement the common
get_type(), get_user(), and get_display_name() interface, add
get_by_user() and activated() manager behavior, and register the new type
with the classification helpers instead of scattering type branches.
Several similarly named flags answer different questions and must not be substituted for one another:
User.is_activeis Django's inherited authentication/permission flag. This project usesAllowAllUsersModelBackend, so it deliberately does not reject an account at password-authentication time solely becauseis_activeis false; Django permission and admin behavior may still depend on it.User.activeis YPPF's business-level account eligibility flag. For a person it is intended to mean not graduated/retired or otherwise disabled; for an organization it is intended to mean not dissolved. Existing flows check it for operations such as applying, selecting courses, appointments, search results, and YQPoint use. It does not prevent session login by itself. The natural-person admin actions currently synchronize it for graduation, leave, postponement, instructor, and in-study/in-service transitions; code must not assume every direct status edit does so.NaturalPerson.objects.activated()excludes onlyGRADUATED. In particular, it currently includes leave-of-absence people even though the admin normally sets theirUser.activefalse.NaturalPerson.is_teacher()additionally treats a retired teacher as inactive by default.Organization.statusmeans that the organization is online/not taken offline, andOrganization.objects.activated()filters that field. It is a separate stored value from the organization'sUser.active.Position.objects.activated()means an in-service position in the selected current semester/year (or the requested non-current range). Other models'activated()methods define their own domain- and time-specific scopes; the method name has no project-wide universal predicate.- An
activate=Trueargument on profile lookup means "apply that profile manager'sactivated()filter". It does not mutate or activate anything. Likewiseactive_scoreis a usage metric andHomepageImage.activatedis content publication state; neither is an account flag.
There are also two independent permission systems. Django permissions are
checked with user.has_perm('app_label.codename'); BlacklistBackend
subtracts PermissionBlacklist entries from permissions supplied by the
backend. NaturalPerson.permissions is a JSON-backed set of feature-policy
flags such as course selection, appointment, and course-credit eligibility,
accessed through has_permission() and its grant/revoke helpers. Do not mix
the naming or APIs of these systems, and do not infer either one from
User.active unless that feature explicitly defines such a rule.
The normal website uses Django session authentication. The login view is
generic.views.Index at / and /login/: it accepts a username/password,
also resolves an organization display name to its account username, calls
django.contrib.auth.authenticate() and login(), performs the valid-profile
and first-login flow, and records organization accounts that the natural
person may switch to. A responsible member switches identity through the
existing app.utils.user_login_org() and
update_related_account_in_session() flow; never authorize an organization
action merely because a person belongs to the organization. The current
session principal must be the organization account, and the switch must have
been allowed by an active Position with is_admin=True.
Protect new template-based function views with both login and YPPF profile checks, in this order:
@login_required(redirect_field_name='origin')
@utils.check_user_access(redirect_url='/logout/')
def featurePage(request):
...login_required establishes that request.user is authenticated and sends
the visitor to LOGIN_URL with the origin return parameter.
check_user_access then rejects SPECIAL/UNAUTHORIZED accounts and
enforces the agreement and initial-password flow for is_newuser. Pages that
implement one of those onboarding steps, such as the agreement page, must
follow the existing exceptions rather than create a redirect loop. Login is
only the first gate: add explicit person/organization, active-state,
relationship, feature-policy, and object-ownership checks required by the
operation. Never use assert as the only authorization check because Python
can disable assertions; return a redirect/403 or raise the appropriate
permission exception for caller-controlled access failures.
For class-based website pages, prefer ProfileTemplateView or
ProfileJsonView when the page requires a valid, onboarded YPPF profile.
Their ProfileView base builds on SecureView, whose default is
login_required = True, and applies the valid-account and first-login checks.
Use plain SecureTemplateView/SecureJsonView only when those profile checks
are intentionally inappropriate. SecureView.perms_required accepts Django
permission strings and requires all of them; business or object-level rules
still belong in a narrow explicit check near the operation. Set
login_required = False only for deliberately public endpoints such as the
login page or health check.
Normal website views should not implement their own password comparison or
accept mini-program JWTs. The REST API separately supports session and JWT
authentication, and the WeChat webview bridge exchanges a JWT for a
single-use ticket before generic.views.redirect_to_webview() creates a
website session. Keep that ticket flow confined to the existing /redirect/
entry point rather than placing credentials or JWTs in website URLs.
Apply project rules in this order when existing code demonstrates more than one style:
- Security, authorization, data integrity, and transaction correctness.
- Backward compatibility of documented routes, public Python interfaces, persisted data, and client-visible response contracts.
- The repository-wide and nearest scoped
AGENTS.mdinstructions. - The local module's established formatting and naming style.
Higher-priority rules override lower-priority precedent. In particular, do
not copy a bare except, authorization assert, wildcard import,
render(..., locals()), state-changing GET handler, or unlocked read-modify-
write sequence merely because an older function in the same file uses it.
These rules are prospective: make touched code safe enough to support the
requested change, but do not combine a focused change with wholesale cleanup
or formatting of unrelated legacy code.
Use the existing feature modules instead of introducing another parallel architecture:
- A website or DRF view owns HTTP concerns: authentication, authorization, request parsing, calling domain operations, choosing status/redirect, and rendering or serialization. It should not implement a long multi-model workflow.
- Feature business operations belong in the matching feature utility module,
such as
activity_utils.py,course_utils.py,org_utils.py, or a feature-localutils.py. A new substantial feature may add one matching<feature>_utils.py; do not place feature policy in the top-levelutils/package. - A model method owns behavior intrinsic to one entity and its invariant. A
chainable database selection belongs on a custom
QuerySet; expose it from the manager withManager.from_queryset()or an equivalent typed manager. Managers may additionally own named constructors and atomic mutation entry points. Cross-model workflows belong in the feature utility layer, not in a view or an unrelated model. - External network and platform integration belongs in
extern/or<app>/extern/. Scheduled entry points belong injobs.pyand should call the same domain operation used by synchronous callers instead of duplicating its rules. - Do not introduce Django signals for core state transitions, accounting, notification delivery, or other behavior whose ordering matters. Call such behavior explicitly from the domain operation. Signals are acceptable only for truly decoupled integration where duplicate execution is safe and the sender does not depend on the result.
For new website class-based pages, ProfileTemplateView and
ProfileJsonView are the default when a valid, onboarded profile is required;
use the Secure*View bases for authenticated pages that intentionally do not
require a classified profile. Use a function view only for a small endpoint
or when extending a legacy function-view feature. Do not create another
authentication/view-base hierarchy.
New fields and relations must make their domain contract explicit:
- Use
TextChoicesorIntegerChoicesfor persisted finite states. Do not compare a stored state to an unexplained literal elsewhere in the code. - Choose
null,blank, and a default deliberately. Usenull=Trueonly when absence is a real domain state; avoid representing the same absence as bothNULLand an empty string. - Every relation must choose
on_deletefrom the real lifecycle. Use a semanticrelated_namewhenever application code will traverse the reverse relation; avoid adding another ambiguous default<model>_setinterface. - Enforce uniqueness and row-local invariants with
UniqueConstraintandCheckConstraintwhere the database can express them. Python validation may improve the error message but must not be the only protection against a concurrent write. - A custom
activated(),current(), or similar QuerySet method must document its exact predicate, including time/semester behavior. It must remain chainable and must not silently mutate records. - Shape collection queries deliberately. Use
select_related()for required single-valued relations andprefetch_related()for collections that will be traversed; do not issue a predictable query per row from a template, serializer, or loop. Paginate externally visible collections that can grow without a small domain bound. - Use
User.objects.create_user()/create_superuser()for accounts, neverUser.objects.create(). Use a model's named manager constructor when one exists so required initialization and records are not bypassed.
Do not call full_clean() automatically from every save(); Django does not
normally do so and bulk operations bypass it. Validate caller input in forms
or serializers, enforce durable invariants in the database, and keep any
model save() override narrow and documented.
Treat any operation that reads current state and then writes dependent state as a single transaction. This includes balances, credit/YQPoint, capacity, signup, check-in, appointment, approval, account switching, and status transitions.
- Enter
transaction.atomic()before the authoritative read. Fetch mutable rows withselect_for_update()and re-check permissions, capacity, balance, and source status after locking; a pre-transaction check is only an early user-friendly rejection. - Lock multiple rows in a deterministic order, normally by primary key, to reduce deadlocks. Keep transactions short and do not render templates, perform file/network I/O, or wait for external services while holding locks.
- Use
F()expressions for independent counters and balances where possible. Usesave(update_fields=[...])when only known fields changed. After a QuerySetupdate(), do not rely on already-loaded Python objects without refreshing or updating them deliberately. - A state field with notifications, audit records, related flags, balances, or
transition restrictions must have one named domain transition function.
Views, admin actions, commands, jobs, and APIs must call it instead of
assigning the field or using
QuerySet.update()directly. Direct bulk update is allowed only for a side-effect-free field and must preserve all documented invariants. - Create database records and their audit/accounting records in the same
transaction. Schedule email, WeChat calls, cache invalidation, and scheduler
work with
transaction.on_commit()so rolled-back changes do not leak to external systems. Make retryable jobs and callbacks idempotent. - Catch
IntegrityErroronly around the smallest operation that is expected to conflict. Do not catch an exception inside a broken atomic block and continue issuing queries in that block.
HTTP methods carry behavioral meaning. GET and HEAD must be safe and must
not create, update, delete, send, approve, check in, or consume a one-time
credential. Protect each new mutating function view with @require_POST or a
specific @require_http_methods; class-based views must restrict
http_method_names.
Global CsrfViewMiddleware is currently disabled, so every new
session-authenticated website mutation must apply explicit CSRF protection.
The canonical function-view decorator order is:
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_POST
@csrf_protect
@login_required(redirect_field_name='origin')
@utils.check_user_access(redirect_url='/logout/')
@require_POST
def updateFeature(request):
...If the feature uses @logger.secure_view(), place it immediately above the
function so it wraps business handling but not the CSRF, login, profile, or
method gates. Protect a mutating class-based view explicitly as well:
from django.utils.decorators import method_decorator
@method_decorator(csrf_protect, name='dispatch')
class UpdateFeature(ProfileJsonView):
http_method_names = ['post']
...Include {% csrf_token %} in HTML forms; JavaScript clients must send the
corresponding token rather than exempting the view. csrf_exempt is permitted
only for a verified non-browser integration that uses a separate
authentication mechanism, and its threat model must be documented in the
view docstring. When modifying an existing session-authenticated mutation,
add the same method and CSRF protection unless a verified legacy caller would
break; document such a compatibility exception next to the route.
Build template contexts explicitly. New or substantially changed views must
pass a named context/render_context dictionary or use
get_context_data(); never pass locals() to a template. Only include values
the template is intended to access, especially around credentials, forms,
uploaded files, and user profiles. New pages should extend the nearest shared
base template and reuse shared static assets rather than copying navbar,
message, or WebView-detection markup.
Use a Django Form for non-trivial website input and a DRF Serializer for
API input. Do not spread manual type conversion and required-field checks
through a view. Validate uploaded file type, size, and ownership before
persisting it. Any caller-controlled redirect target such as next, origin,
or to must be validated with Django's URL safety helpers and restricted to
an allowed local host/path; startswith('/') alone also accepts a
protocol-relative URL and is not sufficient.
For a new website JSON endpoint, prefer ProfileJsonView or SecureJsonView
and the existing warn_code/warn_message message fields when a user-facing
message is needed. Also use a meaningful HTTP status: 2xx for success, 400 for
invalid input or a rejected business precondition, 403 for an authenticated
user lacking permission, 404 for an object not visible in the caller-scoped
queryset, 409 for a genuine concurrent/state conflict, and 500 for an
unexpected server failure. Do not create a second business status code that
contradicts the HTTP status. Preserve an existing AJAX response shape when
changing a legacy endpoint and document that shape in the view docstring.
Mini-program DRF endpoints additionally follow api/AGENTS.md.
USE_TZ is intentionally false. Domain and database datetimes are therefore
naive local datetimes in the deployment's configured timezone; use
datetime.now() consistently for those comparisons. Capture now once near
the start of an operation instead of making repeated calls whose values may
cross a boundary.
Use timezone-aware UTC only at an explicit protocol boundary that requires
it, such as JWT claims or an external API. Convert at that boundary and never
compare or store a mixture of aware UTC and naive domain datetimes. Tests for
deadlines, semesters, expiry, or scheduler behavior must use a fixed injected
or mocked time instead of depending on the wall clock. Do not change
USE_TZ as part of an unrelated feature; doing so is a project-wide data
migration.
Every model change requires a checked-in migration. Never edit, renumber, or
delete a migration already present on the shared develop history; create a
new migration that moves the schema or data forward. Review generated
migrations rather than accepting them blindly, especially column removal,
type conversion, defaults, indexes, and constraint names. The ultimate purpose
is to prevent overwriting an executed migration file. Therefore, you can only
merge migration files when they are not commited yet.
Use historical models from apps.get_model() inside RunPython; do not
import current application models. Make data migrations deterministic and
safe for the expected table size, use batches when necessary, and provide a
reverse function when reversal is safe. If reversal would destroy or
misinterpret data, use RunPython.noop and explain why in a migration
comment. Separate a large data backfill from a constraint-enforcement
migration when existing rows must be repaired first.
After model changes, run makemigrations, inspect the generated file, apply
it, and run:
docker compose -f .devcontainer/docker-compose.yml exec -T yppf \
python manage.py makemigrations --check --dry-runTests are part of a behavior change, not a follow-up. A new feature or bug fix must cover the successful path and the relevant failure boundaries: invalid input, unauthenticated access, insufficient permission, person versus organization behavior, inactive/domain-invalid state, and cross-user object access. A bug fix must include a regression test that fails without the fix.
Use Django TestCase for normal database behavior and
TransactionTestCase only when real commit/rollback, locking, or concurrent
connections are part of the behavior. Prefer factories or small explicit
setup helpers local to the test module; do not depend on developer database
contents or execution order. Mock WeChat, email, library, hardware, scheduler,
and other external effects, while asserting that the correct call is arranged
after commit when relevant.
Run the narrowest useful test target while iterating. Before handoff, run the
entire affected Django app; run the full python manage.py test suite for
changes to shared models, authentication, settings, configuration, scheduler
infrastructure, migrations, or cross-application utilities. Test-only changes
must not weaken production validation merely to make fixtures easier to
create.
A task is Done only when every applicable condition below is satisfied. Writing the requested code is implementation progress, not completion by itself. If a condition cannot be verified, report the task as implemented but not fully verified; do not describe it as Done.
- The requested behavior and explicit acceptance criteria are implemented end to end. All affected entry points—website, API, admin, command, job, or external integration—use the same intended domain rules.
- Important edge cases and failure paths are handled, including the relevant person/organization distinction, inactive state, permission boundary, invalid input, missing object, duplicate request, and concurrent state change.
- No required work is represented by a placeholder, commented-out branch, fabricated value, silent fallback, or ambiguous TODO. A genuine external dependency may remain only when it is recorded with the required TODO metadata and explicitly reported as a blocker, in which case the task is not Done.
- The change stays within the requested scope. Unrelated user changes are preserved, and unrelated cleanup, reformatting, generated artifacts, and debug code are absent from the final diff.
- Authentication, authorization, object ownership, account type, active state, HTTP method, CSRF, and redirect checks follow the rules above. The happy path is not accepted while a caller can bypass the same operation through another route or account type.
- Input is validated through the appropriate form or serializer, predictable errors have the correct response/exception semantics, and unexpected errors remain diagnosable without exposing secrets or personal data.
- Multi-row or read-modify-write behavior is atomic and concurrency-safe. State transitions use their canonical domain entry point; audit/accounting records share the transaction, and external effects occur after commit.
- New configuration contains safe defaults or placeholders, never real credentials. New exports, logs, uploads, and responses expose only the data required by the authorized consumer.
- Every model change has an inspected migration, applies successfully, and
leaves
makemigrations --check --dry-runclean. Required data migration, reversal behavior, constraint ordering, and deployment compatibility are addressed. - Existing documented routes, response shapes, public imports, configuration keys, persisted meanings, and mini-program behavior remain compatible, or the intentional breaking change is explicitly requested and documented with its migration/rollout steps.
- Relevant docstrings,
__all__, configuration templates, scopedAGENTS.md, README/docs, OpenAPI schema annotations, and operational notes are updated when their contract changed. Documentation must describe the resulting behavior, not the implementation plan that preceded it.
- New behavior has tests at the level where it can fail. Bug fixes have a regression test, permission-sensitive changes cover allowed and denied callers, and transaction-sensitive changes cover the relevant conflict or idempotency behavior.
- The narrow tests used during development pass, followed by the affected app's tests. The full Django suite passes whenever the testing rules above require it. Model changes additionally pass migration checks; API changes pass schema generation; template/static changes receive a proportional render or browser smoke check in each affected ordinary/WebView context.
- A required check that was not run because Docker, MySQL, credentials, an external service, or another dependency was unavailable is not silently waived. Report the exact command, reason, and remaining risk, and describe the result as verification incomplete.
- A failing required check means the task is not Done. A demonstrably unrelated pre-existing failure may be separated only when it is reproduced or otherwise evidenced, the affected tests pass, and the failure is clearly reported in the handoff.
- Review
git statusand the final diff before handoff. Confirm that no secret, localconfig.json, runtime log, uploaded media, temporary file, editor artifact, or accidental migration is included. - The final report states what behavior changed, the important files, the verification commands and results, and any migration, configuration, deployment, compatibility, or manual follow-up required. Do not claim tests passed when they were not run.
- Commit, push, PR creation, deployment, and external messages are part of Done only when the user requested them. Otherwise the local verified change and a complete handoff are sufficient; do not perform those external-state actions merely to satisfy this checklist.
Use the following status language consistently:
- Done: every applicable condition above is satisfied.
- Implemented, verification incomplete: code/document changes are present, but at least one required verification could not be completed.
- Blocked: a required decision, permission, credential, service, or external change prevents meaningful completion.
- Not done: required implementation, correctness work, tests, migrations, documentation, or failure handling remains.
Follow the rule precedence above. This project predates some current conventions, so preserve harmless local formatting when editing legacy code, but do not reproduce a prohibited correctness or security pattern. Keep ordinary Python lines below 90 characters (prefer 80); URL configuration may use up to 100 characters. Use hanging indentation for long calls, collections, imports, and expressions, and break long expressions at semantic boundaries rather than introducing opaque temporary abbreviations.
The repository currently has no authoritative autoformatter or linter configuration. Do not run a repository-wide formatter or reformat unrelated lines. Adding Ruff, Black, mypy, pre-commit, or another enforced tool requires a separate configuration change that establishes its scope and reconciles it with these line-length and string rules; do not assume personal editor defaults are project policy.
- Use
CapWordsfor classes and exceptions,lower_snake_casefor ordinary functions and variables, andUPPER_SNAKE_CASEonly for values that are genuinely immutable for the lifetime of the process. - Names must communicate domain meaning and quantity. In particular, use
singular/plural names to distinguish one object from a collection. Do not
add type prefixes such as
i_,dict_,m_, org_; use type annotations when type information matters. - Never use
l,O, orIas a one-character name because they are easily confused with1and0. - Module and HTML template names use lowercase
snake_case. Template names should describe context, audience, model, content, function, and layout as needed, for exampleorg_account_setting.htmloruser_left_navbar.html. Preserve established domain abbreviations such asYQPointandQAwhere the surrounding package already uses them. - Website view functions may use
lowerCamelCaseand should match the route's meaning. Existing website URL path components also uselowerCamelCase. For DRF APIs, follow the naming convention already used by that API module and its router instead of renaming existing endpoints. - Prefer single quotes for short, internal, or user-visible strings in new code. If an older file consistently uses another quote style, keep that file consistent. Triple double quotes remain acceptable for docstrings.
- Prefix private module members, functions, and classes with
_. Reserve__for members that must not be exported, and use it sparingly. - View modules (
*views.py) and utility modules (*utils.py) should declare__all__for their supported public interface. Any module intended to supportfrom module import *must declare__all__. Importers must not depend on names omitted from__all__and must never import__-prefixed names.models.pyonly needs__all__when wildcard import is supported; model names do not need privacy prefixes. - Do not add wildcard imports. Existing dependency aggregators such as
app.views_dependencyandapp.utils_dependencyare legacy compatibility interfaces; new modules must use explicit imports, and edits to existing modules must import newly required names explicitly rather than expanding a wildcard surface. Remove a wildcard import when doing so is small and does not create unrelated churn. - Before adding a helper or wrapper, search the related models, utilities, and dependency modules for an existing implementation. Reuse and improve the shared interface rather than creating a near-duplicate or bypassing it.
- Keep imports grouped as standard library, third-party packages, and local project code. Within local imports, put foundational utilities/dependency modules before other applications and the current feature. Respect the existing dependency direction and avoid circular imports.
Do not hard-code values that may vary by deployment, semester, institution, policy, or integration. This includes credentials, service URLs, feature flags, timeouts, limits, dates, organization names, filesystem locations, and other operational parameters. Put such values in the existing hierarchical configuration system instead of scattering constants through business logic.
Each feature should own an <app>/config.py module. Define a Config
subclass whose attributes are LazySetting descriptors, construct one shared
instance from boot.config.ROOT_CONFIG, and import that instance in callers:
from boot.config import ROOT_CONFIG
from utils.config import Config, LazySetting
__all__ = ['CONFIG']
class FeatureConfig(Config):
enabled = LazySetting('enabled', default=False, type=bool)
endpoint = LazySetting('endpoint', type=str)
batch_size = LazySetting('batch_size', int, default=500)
CONFIG = FeatureConfig(ROOT_CONFIG, 'feature')The corresponding hierarchy belongs in config_template.json under the same
prefix, while real local values belong only in the ignored config.json:
{
"feature": {
"enabled": false,
"endpoint": "$FEATURE_ENDPOINT$",
"batch_size": 500
}
}Follow these rules when adding or consuming settings:
- Access settings as
CONFIG.setting_name; do not readconfig.jsondirectly or indexROOT_CONFIGfrom feature code. Use a descriptive exported instance name such asCONFIG,scheduler_config, orlibrary_config, matching the surrounding application. - Give optional settings a safe development default. Required settings should
omit the default and declare an explicit
type, so a missing or malformed value raisesImproperlyConfiguredwhen resolved. Never provide a usable default credential or commit a real secret, token, app ID, or API key. - Declare
type=even when a default or conversion function can infer it if doing so makes the contract clearer.LazySettingperforms only top-levelisinstancevalidation for parameterized containers; it does not validate every list or dictionary element. - Use the
trans_fnargument for normalization or conversion rather than repeating conversion at call sites. Reuse helpers fromutils.config.cast, such asmapping,optional, andstr_to_time. A setting may also derive from anotherLazySettingwhen the dependency is intentional, as in the existing WeChat configuration. - For a nested section with several related settings, create a nested
Configobject in the parent config's__init__, followingProfileConfigandYQPointConfig, instead of encoding the hierarchy into unrelated globals. - Configuration values are resolved lazily and cached on first access. Do not
assume that editing
config.jsonchanges an already running process; restart it. Callactivate_all()only when early validation of the entire config tree is intentionally required. - Environment-variable overrides are reserved for deployment/bootstrap
settings already designed for them, such as
YPPF_DEBUGand theDB_*values inboot/settings.py. Do not introduce ad hocos.getenv()calls in feature modules; extend the centralized bootstrap settings only when an environment override is genuinely required. - Update
config_template.json, development setup scripts when necessary, documentation, and relevant tests together with a new setting. Preserve an existingconfig.json; it may contain local secrets and must never be added to Git.
- Comments must explain intent, constraints, non-obvious domain behavior, or why an approach is necessary; do not narrate obvious syntax. Update nearby comments whenever behavior changes.
- Prefer a comment on its own line. Use inline comments only when they add real value, separated from the statement by at least two spaces. Write comments as complete sentences where practical.
- Add docstrings to public modules, functions, classes, and methods. A private
function may instead have a short comment immediately after its
defline. Multi-line docstring closing quotes belong on their own line. - A view docstring should summarize the page/API purpose, authorized users, behavior differences by user type, objects mutated, and important model or helper dependencies; mention the template when it helps navigation.
- A general function docstring should state its purpose and any important preconditions, side effects, exception contract, parameters, and return value. Model documentation should cover significant methods, manager/query helpers, and subclass requirements. Do not invent author or owner metadata when it is not known.
- Make condition semantics explicit when
None, an empty value, andFalsehave different meanings. Know the expected input range, document uncertain assumptions for review, useisinstance()for type checks, and use identity checks forNone(value is Noneorvalue is not None). - Do not exploit the value-returning behavior of
and/oras an implicit conditional expression. Use an explicit conditional expression or control flow so the intended result is clear. - Validate predictable conditions directly. Keep each
tryblock narrow and catch specific exceptions; do not wrap a whole function or use a bareexcept. An error path must either recover completely or return/raise enough information for its caller to handle it. - Log unexpected events at the highest useful layer, avoiding duplicate logs
in frequently called low-level helpers. Choose severity by impact:
ERRORis for failures requiring urgent administrator action, recoverable failures should normally beWARNINGor lower, and caller-owned invalid input should generally not exceedINFO. Include the parameters, state, and exception details needed to diagnose and restore normal operation, without recording secrets or unnecessary personal data. - At an HTTP boundary, preserve the distinction between invalid input, authentication, permission, absence, state conflict, and server failure. Do not turn every exception into a 200 response, a generic redirect, or a 400. For website HTML, a redirect with a user message is acceptable after a recoverable POST, but it must not conceal an authentication/authorization failure or an unexpected server error.
Do not leave ambiguous unfinished code. When work is intentionally blocked on another task and the required metadata is known, place the marker on its own line in this form:
# TODO: task <task-id> <contact> <YYYY-MM-DD> <blocked work or dependency>For a multi-line unfinished region, use matching start/end TODO comments and
describe the expected integration steps. Keep TODO text independent of nearby
code so it can be found with TODO: task <task-id>. Never fabricate a task ID,
contact, or date merely to satisfy the format.
Use a Conventional Commits-style message compatible with Husky/commitlint workflows:
<type>: <concise summary>
<more details when needed>
Files changed:
- path/to/file.py: Describe the behavior or documentation changed here.
- path/to/other_file.py: Describe the change made here.
Use an appropriate short type such as feat, fix, refactor, test,
docs, style, build, ci, or chore. Keep the summary specific and
imperative. Every commit message must list the files changed and briefly state
what changed in each file. Use the body to explain motivation, important
behavioral changes, migration or compatibility concerns, and related issue
numbers when applicable. Each commit should represent one coherent logical
change; squash incidental or fragmented local commits before submitting the
branch.
Contributions must be developed in a personal fork, not directly on the main repository:
- Fork the main repository and configure it as the contributor's fork
(normally
origin). Configure the main repository asupstream. - Fetch
upstreamand start from the latestupstream/develop. - Create a dedicated working branch from that commit. Use a descriptive
<type>/<topic>name, for examplefeat/add_sth,fix/login_redirect, ordocs/agent_guidance. - Push the working branch to the personal fork and open a pull request from
that branch to the main repository's
developbranch.
The main repository's develop branch must remain a single linear,
fast-forwardable history. Working branches must therefore contain no merge
commits. Before the PR is merged, fetch the latest upstream/develop and
rebase the working branch onto it; resolve conflicts during the rebase and
rerun the relevant tests. After rebasing a branch that was already pushed,
update only the contributor's fork branch with --force-with-lease, never
plain --force. Do not merge develop into the working branch. The final PR
head must be directly fast-forwardable from the current develop head.
Check the above restrictions prior to committing, if the requirements are not satisfied, DO NOT proceed to commit and tell the user what to do instead.
Never include real APP_ID (appid) or APP_SECRET (secret) values in
config_template.json or a commit; the template may contain placeholders
only. Configure the real values only in config.json, which is ignored by
Git. Contact the project manager for these credentials when debugging the
WeChat mini-program backend.
Treat passwords, session IDs, JWTs, tickets, signing values, raw WeChat codes,
openid, API tokens, and configuration credentials as secrets. Never put
them in URLs, logs, exception messages, fixtures committed to Git, analytics,
or screenshots. Do not log complete request headers or bodies on an
authentication or binding endpoint.
Student/staff IDs, names, phone numbers, email addresses, dormitory data, birthdays, avatars, uploaded files, and library/appointment/feedback records are personal data even when they are not authentication secrets. Query and serialize only the fields required by the feature, scope object access before retrieval, and log an internal primary key or a deliberately masked identifier instead of a full personal profile. New exports and bulk-management commands must enforce an explicit permission, avoid predictable public filenames, and document where the output is stored and who may receive it.