HH/docs/workflows/appreciations.md
2026-07-12 11:18:20 +03:00

27 KiB

Appreciations — Workflow & Lifecycle

Source of truth: apps/appreciation/. This is an outbound recognition/recognition module (recognition sent to staff/physicians), fundamentally different from inbound case-management. This document describes the current implementation only. Every claim is cited as file:line.

⚠ Read first — two important findings:

  1. VALID_APPRECIATION_TRANSITIONS (models.py:28-34) is defined but never referenced anywhere (dead code). The state machine is enforced only by runtime ValueError guards inside the model transition methods.
  2. The REST API create and complaint→appreciation conversion call appreciation.send() on a freshly-created DRAFT, but send() requires ACTIVATED/AI_ANALYZED — both would raise ValueError (likely bugs). The working path is the UI activation-gate flow.

1. Purpose

Appreciations = BOTH inbound patient feedback AND outbound staff recognition, but the dominant designed workflow is OUTBOUND recognition FROM PX/hospital staff TO staff/physicians.

Evidence:

  • apps/appreciation/models.py:2, __init__.py:2, README.md:3, IMPLEMENTATION_SUMMARY.md:4: "Send and track appreciation to users and physicians."
  • The outbound character: appreciation_create is gated to PX/hospital staff (ui_views.py:155); the email subject is "Staff Appreciation" (signals.py:72); the body "A staff member in your department has received an appreciation. Please acknowledge their good work." (signals.py:179); tasks.py:18 "Send email + SMS to champion, manager, and appreciated staff."
  • The inbound patient-feedback channel: public_appreciation_submit (ui_views.py:1196) — patients/public submit a message, created as DRAFT, then PX staff triage and activate/send. Plus complaint→appreciation conversion (complaints/views.py:2076).

So: a recognition engine whose originating actors can be (a) PX/hospital staff sending peer/leadership recognition, (b) public/patient submissions awaiting staff review, or (c) appreciation-type complaints being formally converted. The terminal recipient is always a staff member or physician.


2. How a Case Starts

2.1 Who can create

Channel View/Serializer Permission File:line
Internal UI form appreciation_create + AppreciationForm @login_required; PX/Hosp-Admin/PX-Mgmt/PX-Employee forms.py:8; ui_views.py:153
Public portal public_appreciation_submit No auth (@csrf_exempt); rate-limited (5/5min/IP) ui_views.py:1196
External API (HIS integration) integrations/api_views.py create API-key scoped to a hospital integrations/api_views.py:606
REST API create AppreciationCreateSerializer IsAuthenticated views.py:55, serializers.py:132
Complaint → Appreciation conversion convert_to_appreciation Edit permission on the complaint complaints/views.py:2075

2.2 Creation channels in detail

(a) Internal UI form (forms.py:8): ModelForm exposing only ["hospital", "department", "message_en"] (forms.py:13); department restricted to active departments (forms.py:34). On POST: sender = request.user, status = DRAFT (ui_views.py:173). Category/recipient/visibility are NOT set here — decided later during activate/send.

(b) Public portal (ui_views.py:1196): Inputs contact_name, contact_phone, message, hospital, optional staff_name/department/section. Required: name, phone, message, hospital (ui_views.py:1224). Creates with status=DRAFT, visibility=PUBLIC, is_anonymous=False, category=None, metadata {source:"public_form", ...} (ui_views.py:1239). Fires notify_staff_new_item.delay("appreciation", ...) (ui_views.py:1258). Returns reference_number.

(c) External API (integrations/api_views.py:606): Creates DRAFT, visibility PUBLIC, metadata {source:"external_api", ...}. Returns the canonical reference_number generated by save() (APR-YYYYMM-NNNN); the same value is used to retrieve the record via GET /api/v1/external/appreciations/<reference_number>/. Fires notify_staff_new_item.

(d) REST API create (views.py:143, serializer serializers.py:132): Required: recipient_type (['user','staff']), recipient_id (UUID), message_en, hospital_id. Optional: category_id, message_ar, visibility (default PRIVATE), is_anonymous, department_id. Validation: recipient must belong to hospital; category hospital-compatible. AppreciationViewSet.create resolves a User/Staff via ContentType, creates the record (status DRAFT), then immediately calls appreciation.send() (views.py:198).

(e) Complaint → Appreciation conversion (complaints/views.py:2076): Guards — complaint active (is_active_status, complaints/views.py:2087), complaint_type == "appreciation" (:2096), not already converted (:2103). Defaults: message_encomplaint.description; message_arcomplaint.short_description_ar; visibility="private"; is_anonymous=True (:2113). Creates DRAFT (:2171), then appreciation.send() (:2183). Links back via metadata.appreciation_id (:2186); optionally closes complaint (:2196). Default category "Patient Feedback Appreciation" (code patient_feedback, create_patient_feedback_category.py).

2.3 What happens immediately after submission

  • Reference number generated in Appreciation.save() via generate_reference("APR", hospital) (models.py:243) → APR-YYYYMM-NNNN (global monthly sequence). Stored on reference_number (models.py:198), comment "internal-only, not publicly trackable".
  • Default status = DRAFT (models.py:193).
  • post_save signal handle_appreciation_sent (signals.py:27) only acts when status == SENT and not notification_sent — does nothing at creation (DRAFT).
  • For public/external: PX-admin notification task notify_staff_new_item("appreciation", id) fired (ui_views.py:1259, api_views.py:625) → emails/new_appreciation_notification.html.

2.4 Why DRAFT + activation gate?

Mirrors the complaints activation gate. A DRAFT appreciation is untriaged: no recipient set (internal form), no AI analysis, no decision to publish at chosen visibility. Activation (ui_views.py:195) is the PX-staff gate that confirms content, triggers AI analysis, stamps activated_at/activated_by, emits audit. Only after activation may it be sent (models.py:335).

The draft count is surfaced to reviewers via core/context_processors.py:74 (draft_appreciation_count) for PX/Hosp-Admin/PX-Mgmt.


3. Complete Lifecycle

3.1 The declared state machine — VALID_APPRECIATION_TRANSITIONS (models.py:28-34) — verified

DRAFT        → {ACTIVATED}
ACTIVATED    → {AI_ANALYZED, SENT}
AI_ANALYZED  → {SENT}
SENT         → {ACKNOWLEDGED}
ACKNOWLEDGED → set()   (terminal)

3.2 Where enforced — NOWHERE via the constant

VALID_APPRECIATION_TRANSITIONS is never imported or referenced anywhere else (grep confirmed). The model transition methods each re-check the current status and raise ValueError:

  • activate() (models.py:309) — requires DRAFT.
  • mark_ai_analyzed(analysis_data) (models.py:320) — requires ACTIVATED.
  • send() (models.py:331) — requires ACTIVATED or AI_ANALYZED.
  • acknowledge() (models.py:344) — requires SENT.

3.3 clean() and DB CheckConstraints

  • No clean() method on Appreciation.
  • No CheckConstraints in any migration. Only unique_together on category (models.py:84) and AppreciationStats (models.py:517), unique=True on reference_number/badge code.

Conclusion: status integrity enforced only at runtime in Python methods, not at DB layer.


4. Status Definitions

Status Constant When entered Who advances Next Key code
DRAFT AppreciationStatus.DRAFT (models.py:21) On creation (model default) PX/hospital staff via appreciation_activate ACTIVATED created; ref number generated; PX admins notified (public/external only)
ACTIVATED ACTIVATED (models.py:22) Staff approves/triages draft; runs AI AI step (auto) or staff appreciation_send/send_to AI_ANALYZED, SENT activated_at/activated_by stamped (models.py:316, ui_views.py:211)
AI_ANALYZED AI_ANALYZED (models.py:23) mark_ai_analyzed() called by activate view after a successful AI call Staff via send/send_to SENT ai_analyzed_at + ai_analysis JSON (models.py:326, ui_views.py:254)
SENT SENT (models.py:24) send() invoked The recipient (User/Staff) via acknowledge ACKNOWLEDGED sent_at stamped (models.py:339); triggers post_save cascade (notifications, stats, badges)
ACKNOWLEDGED ACKNOWLEDGED (models.py:25) Recipient acknowledges (terminal) none acknowledged_at (models.py:352)

4.1 The AI_ANALYZED status — what AI analysis happens

Performed synchronously inside appreciation_activate (ui_views.py:230-258), immediately after DRAFT→ACTIVATED. Uses apps.core.ai_service.AIService.chat_completion (NOT analyze_complaint):

  • Prompt asks for JSON: summary_en, summary_ar, themes (list), tone (warm|formal|casual), suggested_response_en, suggested_response_ar (ui_views.py:234-246).
  • Parsed and passed to appreciation.mark_ai_analyzed(analysis_data) (ui_views.py:251).
  • On failure: logged + swallowed; appreciation stays ACTIVATED (does NOT auto-advance) — still sendable (ui_views.py:255).
  • AIService.chat_completion (ai_service.py:685) → OpenRouter /chat/completions, model google/gemini-2.5-flash-lite default (ai_service.py:41).

So AI_ANALYZED is a "content enrichment" step (summary/themes/tone/draft reply), not a sentiment/severity gate. The appreciation can still be SENT directly from ACTIVATED without ever becoming AI_ANALYZED (models.py:30).


5. Workflow Actions

5.1 UI actions (ui_views.py)

Action Function URL name Status gate Permission
List appreciation_list :37 appreciation_list @login_required; hospital-filtered
Detail appreciation_detail :92 appreciation_detail login; PX/Hosp-Admin or same-hospital
Create (internal) appreciation_create :153 appreciation_create creates DRAFT PX/Hosp-Admin/PX-Mgmt/PX-Employee
Activate (DRAFT→ACTIVATED + AI) appreciation_activate :193 appreciation_activate must be DRAFT (:198) PX/Hosp-Admin/PX-Mgmt/PX-Employee
Send (form) appreciation_send :264 appreciation_send ACTIVATED or AI_ANALYZED (:269) PX/Hosp-Admin/PX-Mgmt/PX-Employee
Send-to (AJAX, person/department) appreciation_send_to :320 appreciation_send_to ACTIVATED or AI_ANALYZED (:344) PX/Hosp-Admin/Dept-Manager/PX-Mgmt/PX-Employee
Acknowledge (SENT→ACKNOWLEDGED) appreciation_acknowledge :485 appreciation_acknowledge must be SENT (:500) recipient only (User ContentType match, :492)
Public submit public_appreciation_submit :1196 public_appreciation_submit creates DRAFT none
Restore deleted appreciation_restore :1036 admin_appreciation_restore PX/Hosp-Admin
PDF appreciation_pdf :1276 appreciation_pdf @login_required
Leaderboard leaderboard_view :515 leaderboard_view login
My Badges my_badges_view :580 my_badges_view login
Category CRUD :666-843 category_* PX/Hosp-Admin only
Badge CRUD :850-1033 badge_* PX/Hosp-Admin only
AJAX: users/staff/physicians/departments :1057-1147 ajax/* login

Detail view exposes can_activate and can_send flags (ui_views.py:142).

5.2 REST API (views.py)

AppreciationCategoryViewSet (:33), AppreciationViewSet (:55, actions: acknowledge :210, my_appreciations :232, sent_by_me :263, summary :277), AppreciationStatsViewSet (:363), AppreciationBadgeViewSet (:394, PX/Hosp-Admin), UserBadgeViewSet (:416), LeaderboardView (:445).

acknowledge verifies the requester is the recipient via ContentType+id (views.py:216), else HTTP 403.

⚠ Note: there is no activate or ai_analyze REST action — those are UI-only.

⚠ Gap — REST create vs status: AppreciationViewSet.create (views.py:143) creates the record and immediately calls appreciation.send() (views.py:198). Because send() requires ACTIVATED/AI_ANALYZED (models.py:335), this raises ValueError on a DRAFT.


6. Decision Points

  1. Activation gate (DRAFT→ACTIVATED) (ui_views.py:195). Mirrors the complaints activation gate.
  2. AI analysis path vs direct-send — AI attempted at appreciation_activate (ui_views.py:230). If it succeeds → AI_ANALYZED; if it throws → stays ACTIVATED and is still sendable. From ACTIVATED the send step is allowed (models.py:30), so AI is optional to progressing.
  3. Visibility/audience decisionAppreciationVisibility (models.py:37): PRIVATE/DEPARTMENT/HOSPITAL/PUBLIC. Default PRIVATE (models.py:190). Public portal forces PUBLIC (ui_views.py:1248); external API forces PUBLIC (api_views.py:613); complaint conversion defaults "private" (complaints/views.py:2115). Read-access filtered in AppreciationViewSet.get_queryset (views.py:78).
  4. Recipient/target selection — done at send time, not creation, in appreciation_send_to: recipient_type ∈ {person, department} (ui_views.py:350). If department: auto-targets champion+manager via get_champion_and_manager. Optional staff_id sets recipient to a Staff.
  5. Badge awarding decisionsignals.check_and_award_badges (signals.py:306) runs whenever an appreciation reaches SENT.

7. Assignment Flow — RECIPIENTS

There is no "case assignment to an agent". The analogous concept is the recipient (who the recognition is for) plus the visibility audience.

7.1 Recipient model

  • Recipient is a GenericForeignKey (recipient_content_type + recipient_object_id + recipient) (models.py:132). Eligible concrete types: accounts.User and organizations.Staff (incl. physicians as staff_type='physician', ui_views.py:1110).
  • Sender: models.ForeignKey("accounts.User") (models.py:127).
  • AppreciationForm (internal) does not set a recipient — only hospital, department, message_en. Recipient bound at send step (ui_views.py:427) for the department path, or via recipient_type/recipient_id in REST/conversion.
  • The "owner" (for routing) is computed by get_owner() (models.py:250) — a cascade over section/department role-holders. ⚠ Gap: get_owner() is defined but not invoked by any code in the appreciation app (verify external callers).

7.2 Visibility

AppreciationVisibility (models.py:37): PRIVATE, DEPARTMENT, HOSPITAL, PUBLIC. Read-side enforcement in AppreciationViewSet.get_queryset (views.py:78): a user sees their sent items, those received by their User/Staff profile, DEPARTMENT-vis in their dept, HOSPITAL-vis in their hospital, and all PUBLIC.

7.3 Organization context

hospital (required, models.py:139), department (nullable, models.py:173), section (nullable, models.py:165), plus three DEPRECATED legacy fields (legacy_location, legacy_main_section, legacy_subsection, models.py:140-163).


8. Investigation Process — N/A

Appreciations have no investigation phase. No investigator field, no investigation status, no evidence model. The closest analog is the AI_ANALYZED step, which is automated content enrichment (summary/themes/tone/suggested reply), not a human investigation. Why N/A: appreciations are positive recognition, not problems requiring root-cause analysis.


9. Communication Flow — notifications

9.1 On reaching SENT — post_save cascade

signals.handle_appreciation_sent (signals.py:27) fires when status == SENT and not notification_sent. It calls:

  • send_appreciation_notification (signals.py:49): builds HTML email ("Staff Appreciation"); resolves recipient email/phone via get_recipient_email/get_recipient_phone (models.py:287); sends email + SMS; sender display "Anonymous" if is_anonymous; calls _send_department_head_notification (signals.py:140) emailing department.manager + Staff(department=dept, is_head=True); calls _send_cc_notifications (signals.py:203); sets notification_sent=True/notification_sent_at.
  • update_appreciation_stats (signals.py:227) — §12.
  • check_and_award_badges (signals.py:306) — §Gamification.

9.2 The send_appreciation_notifications Celery task

tasks.py:9 (@shared_task) — a separate, richer notification path used by the UI appreciation_send_to department branch (ui_views.py:442). Requires a department; get_champion_and_manager(department); per target sends email + SMS; if staff_id provided, emails/SMS the Staff "You've Been Appreciated!".

⚠ Two notification codepathssignals.py (sync, fires on every SENT) and tasks.py (async, fires only from UI send_to department branch). Both can run for the same appreciation if sent via send_to, potentially double-notifying.

9.3 On creation of a public/external DRAFT

notify_staff_new_item("appreciation", id) emails PX admins/on-call roster (complaints/tasks.py:2489), template emails/new_appreciation_notification.html.

9.4 On badge award

signals.check_and_award_badges emails the recipient user "You earned a badge: !" (signals.py:357).


10. Escalation Flow — N/A

No SLA timers, no escalation levels, no overdue fields in Appreciation (models.py:110-237). No celery beat task references appreciation SLA. Why N/A: appreciation is non-urgent recognition; urgency/SLA semantics belong to complaints.


11. Resolution Process — "acknowledgment"

The analog of resolution is the acknowledge transition (SENT→ACKNOWLEDGED).

  • Model method: acknowledge() (models.py:344) — sets status=ACKNOWLEDGED, acknowledged_at=now().
  • UI: appreciation_acknowledge (ui_views.py:485) — POST; only the recipient (matched by User ContentType + id, ui_views.py:492); rejects if not SENT (ui_views.py:500).
  • REST: AppreciationViewSet.acknowledge (views.py:210) — same recipient check, HTTP 403 otherwise.

"Acknowledgment" = the recipient has confirmed/thanked — it is the recipient's terminal action. There is no sender-side resolution.


12. Closure Process

12.1 Terminal state & reopen

ACKNOWLEDGED is terminal: VALID_APPRECIATION_TRANSITIONS[ACKNOWLEDGED] = set() (models.py:33), and acknowledge() leaves no forward method. There is no reopen action/view anywhere. Once ACKNOWLEDGED, permanently complete.

12.2 Soft delete / restore

Appreciation extends SoftDeleteModel (models.py:110); soft-deletable + restorable via appreciation_restore (ui_views.py:1036, uses Appreciation.all_objects + restore()). Restore does not change status.

12.3 Statistics aggregation

AppreciationStats (models.py:476) — monthly per recipient. Unique key (recipient_content_type, recipient_object_id, year, month) (models.py:517). Fields: received_count, sent_count, acknowledged_count, hospital_rank, department_rank, category_breakdown (JSON). Aggregated in signals.update_appreciation_stats (signals.py:227) when an appreciation reaches SENT: get_or_create the month row; atomically increment counts via F('...') + 1; update category JSON; recalculate_rankings (signals.py:269) re-numerates ranks by -received_count.

12.4 Leaderboard

LeaderboardView (views.py:445): reads year/month (defaults current month); filters AppreciationStats by hospital; enumerates rank; attaches earned badges. UI equivalent leaderboard_view (ui_views.py:515).

12.5 Summary

AppreciationViewSet.summary (views.py:277) and appreciation_summary_ajax (ui_views.py:1150) compute total/this-month received & sent, badges earned, top category, hospital rank.


13. Exception Flows

13.1 Complaint → Appreciation conversion

complaints/views.py:2076. Guards: is_active_status, complaint_type == "appreciation", metadata.appreciation_id absent. Default category "Patient Feedback Appreciation" (code patient_feedback).

13.2 ⚠ INCONSISTENCY (likely bug): conversion and REST create call send() on DRAFT

  • Conversion: creates status=DRAFT (complaints/views.py:2171) then appreciation.send() (:2183).
  • REST create: creates DRAFT then appreciation.send() (views.py:198).
  • send() (models.py:335) requires ACTIVATED/AI_ANALYZED, else ValueError.

Both paths would raise ValueError at runtime. Neither wraps send() in try/except. This strongly indicates the primary, exercised path is the UI activation-gate flow, and these two callers are out of sync with the gate model.

13.3 Draft abandonment

A DRAFT has no expiry task and no cleanup. Remains queryable as DRAFT (counted in core/context_processors.py:79, ui_views.py:71). No auto-close/activation.

13.4 Visibility changes

visibility is a plain CharField (models.py:190) with no transition guard — editable at any status via admin or direct mutation. Public/external forced PUBLIC at creation, not otherwise change-protected.

13.5 Badge revocation

No revoke/un-award path. UserBadge (models.py:432) has only create + read. badge_delete (ui_views.py:1004) refuses to delete a badge with any UserBadge (ui_views.py:1018) — earned badges are protected. A badge, once awarded, persists.

13.6 Duplicate appreciation

No deduplication on creation. The only dedup is per-badge-award (signals.py:326) and per-stats-month (unique_together models.py:517, guarded by get_or_create).

13.7 Restore from soft-delete

appreciation_restore (ui_views.py:1036) — PX/Hosp-Admin only; redirects to config:deleted_items.

13.8 AI-failure handling

In appreciation_activate, an AI exception is logged but swallowed (ui_views.py:255) — appreciation stays ACTIVATED and is still sendable. At infra level, AIService._notify_ai_failure (ai_service.py:172) emails PX admins (debounced hourly) on HTTP 401/402.


14. Department-Response Sub-Flow — NOT used (confirmed)

Appreciations do not use the inbound send-to-department → department-responds → resolve cycle. There is no responded/department_action status, no department-side response model, no SLA on department reply. The send_to step (ui_views.py:320) is outbound only — it delivers the appreciation to a department's champion+manager+staff and marks the appreciation SENT; the department does not "respond back" through the system (their only return action is the recipient's acknowledge).

Why: this module is outbound recognition. The "send to department" verb here means deliver recognition to that department, not open a case for the department to handle.


15. End-to-End Example

Patient submits appreciation via public form (mentions Nurse A)
  ↓ (ui_views.py:1196)  → status=DRAFT, ref APR-202607-0001,
                          visibility=PUBLIC, PX admins notified
PX staff reviews + activates
  ↓ (ui_views.py:193)  → status=ACTIVATED, activated_at/by
                         AI analysis runs synchronously
                         [DECISION: AI succeeds → AI_ANALYZED, or stays ACTIVATED]
  ↓ (ui_views.py:230)  → ai_analysis JSON stored (summary/themes/tone/suggested reply)
PX sends to Department X (target Nurse A / champion / manager)
  ↓ (ui_views.py:320)  → recipient=Staff(Nurse A), status=SENT, sent_at
                         [DECISION: visibility PRIVATE/DEPARTMENT/HOSPITAL/PUBLIC]
                         post_save cascade fires:
                           - email+SMS to recipient ("Staff Appreciation")
                           - email department manager + dept heads
                           - cc notifications
                           - update_appreciation_stats (received/sent count, rank)
                           - check_and_award_badges (e.g. "First Appreciation")
                             → badge email to recipient
Nurse A (the recipient) acknowledges
  ↓ (ui_views.py:485)  → status=ACKNOWLEDGED, acknowledged_at
                         [TERMINAL — no reopen, no further actions]

Appendix A — Gamification (Badges)

A.1 AppreciationBadge (models.py:360) — definition

Fields: hospital (null = system-wide), code (unique), bilingual name_en/ar/description_en/ar, icon, color, order. criteria_type (models.py:389): received_count, received_month, streak_weeks, diverse_senders. criteria_value (int threshold). is_active.

A.2 Seed data (seed_appreciation_data.py)

Categories: excellent_care, team_player, innovation, leadership, mentorship, going_extra_mile, reliability, positive_attitude.

Badges (seed file is authoritative — README.md differs):

code name_en criteria_type value
first_appreciation First Appreciation received_count 1
appreciated_5 Rising Star received_count 5
appreciated_10 Shining Star received_count 10
appreciated_25 Super Star received_count 25
appreciated_50 Legendary received_count 50
monthly_champion Monthly Champion received_month 10
streak_4_weeks Consistent streak_weeks 4
diverse_appreciation Well-Loved diverse_senders 10

A.3 UserBadge (models.py:432) — award record

GenericFK recipient, FK badge, earned_at (auto_now_add), appreciation_count (count-at-award), metadata JSON. No unique constraint — awarder dedups manually (signals.py:326).

A.4 Awarding logic — signals.check_and_award_badges (signals.py:306)

Triggered from handle_appreciation_sent when SENT. Iterates active badges for the hospital/system-wide; skip if already earned; check_badge_criteria (signals.py:383): received_count (total SENT to recipient), received_month (this month), streak_weeks (check_appreciation_streak signals.py:498 — Mon-start weeks; gap breaks streak), diverse_senders (distinct senders). If qualifies: UserBadge.objects.create, email recipient.

A.5 Badge progress UI — my_badges_view (ui_views.py:580)

Computes per-badge progress % toward threshold. Template appreciation/my_badges.html.


Appendix B — Flagged gaps / ambiguities

  1. VALID_APPRECIATION_TRANSITIONS is dead code — defined but never enforced via lookup; transitions rely solely on per-method ValueError. Constant and methods agree today but nothing keeps them in sync.
  2. REST create and complaint conversion call send() on DRAFT — would raise ValueError (views.py:198, complaints/views.py:2183 vs guard models.py:335). Likely bug/stale code; UI activation flow is the working path.
  3. AppreciationForm does not capture recipient/category/visibility — decided later (at send-time for UI path). An internally-created DRAFT has no recipient until send_to runs.
  4. Two notification codepathssignals.py (sync, every SENT) and tasks.py (async, UI send_to dept branch). Potential double-notification.
  5. No badge revocation, no reopen, no draft expiry — by design.
  6. README badge list vs seed file badge list differ — seed file is authoritative.
  7. get_owner() (models.py:250) defined but not invoked in the appreciation app — verify external callers.