Metadata-Version: 2.4
Name: django-infra-core
Version: 0.2.2
Summary: Reusable Django building blocks for multi-app deployments
Author: DjangoInfra Team
License-Expression: MIT
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: Django>=5.2

# django-infra-core

Birden çok Django uygulaması (vbs, takibat, examapply, holidayjournal) arasında
paylaşılan mimari çekirdek.

## Ne sağlar

- **Tenancy**: hostname bazlı `Tenant` çözümlemesi, tek deployment'ın birden çok
  müşteriye hizmet etmesi
- **Yetkilendirme kapsamı**: `Membership` ile tenant admini / kurum yöneticisi rolleri
- **Admin mixin'leri**: kapsam dışı kayıtların listede, formda ve doğrudan URL'de
  görünmemesi
- **Soyut modeller**: `BaseInstitution`, `BaseStudent`, `BaseMembership`
- **Form yardımcıları**: kurum türüne göre daralan `grade`/`level` seçenekleri
- **Ayar ve OIDC yardımcıları**

## Kurulum

```bash
pip install django-infra-core==0.2.0
```

## 0.1.x'ten 0.2.0'a geçiş (BREAKING)

0.2.0, tek pod'un tek müşteriye hizmet ettiği varsayımını kaldırır.

| Değişiklik | Etki |
|---|---|
| `INSTITUTION_TYPE` env değişkeni artık `grade`/`level` seçeneklerini belirlemiyor | Bir pod artık hem ortaokul hem hafızlık verisi tutabilir. Seçenek daraltma model yerine form katmanında yapılır. |
| `BaseInstitution.tenant` FK eklendi (`null=True`) | Migration gerekli. Backfill yapılana kadar tenant'sız kayıtlar hiçbir tenant'ta **görünmez** (fail-closed). |
| `BaseInstitution.manager` (O2O) DEPRECATED | `Membership` ile değiştirin. Geçiş dönemi için hâlâ okunuyor. |
| `InstitutionScopedAdminMixin` kapsam tabanlı yeniden yazıldı | `institution_fk_name` artık nokta yolu kabul eder (`student__institution`). |

### Geçiş adımları

```bash
# 1. Somut Membership modelini tanımla (aşağıdaki örnek)
# 2. Migration üret
python manage.py makemigrations infra_core <core_app>
# 3. Tenant kayıtlarını oluştur (admin veya data migration)
# 4. Mevcut Institution kayıtlarını tenant'a bağla (data migration)
# 5. Backfill tamamlanınca tenant FK'sını null=False yapabilirsin
```

## Kurulum sözleşmesi

```python
# settings.py
INSTALLED_APPS = [
    # ...
    "infra_core",
    "core",
]

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "infra_core.tenancy.TenantMiddleware",          # AuthenticationMiddleware'den ÖNCE
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "infra_core.auth.InstitutionContextMiddleware",  # TenantMiddleware'den SONRA
]

# Somut Membership modelinin yolu — zorunlu
INFRA_MEMBERSHIP_MODEL = "core.Membership"

# Tenant çözümlemesinden muaf yollar (sağlık kontrolleri, statik)
INFRA_TENANT_EXEMPT_PATHS = ["/healthz", "/readyz", "/static/", "/media/"]
```

`ALLOWED_HOSTS` gevşek bırakılabilir (`*`) çünkü gerçek kapı `TenantMiddleware`'dir:
bilinmeyen host 404 döner. Ancak admin POST'ları için `CSRF_TRUSTED_ORIGINS`
hâlâ gereklidir:

```python
CSRF_TRUSTED_ORIGINS = ["https://*.bilgikampusu.com", "https://vbs.hisaregitim.org"]
```

## Modelleri somutlaştırma

```python
# core/models.py
from django.db import models
from infra_core.models import BaseInstitution, BaseMembership, BaseStudent


class Institution(BaseInstitution):
    pass


class Student(BaseStudent):
    institution = models.ForeignKey(
        Institution, on_delete=models.CASCADE, related_name="students"
    )


class Membership(BaseMembership):
    # Tenant admini için boş bırakılır, kurum yöneticisi için zorunludur.
    institution = models.ForeignKey(
        Institution, on_delete=models.CASCADE, null=True, blank=True,
        related_name="memberships",
    )
```

`Tenant` somut bir modeldir ve `infra_core` içinde yaşar; onu tanımlamana gerek yok.

## Admin

```python
# core/admin.py
from django.contrib import admin
from infra_core.admin import (
    BaseMembershipAdmin, InstitutionScopedAdminMixin, TenantScopedAdminMixin,
)
from .models import Institution, Membership, Student


@admin.register(Student)
class StudentAdmin(InstitutionScopedAdminMixin, admin.ModelAdmin):
    institution_fk_name = "institution"


@admin.register(Institution)
class InstitutionAdmin(InstitutionScopedAdminMixin, admin.ModelAdmin):
    institution_fk_name = ""          # modelin kendisi Institution


@admin.register(Membership)
class MembershipAdmin(BaseMembershipAdmin):
    pass
```

Dolaylı bağlı modellerde yolu tam ver — yoksa kapsam uygulanmaz:

```python
class ReportAdmin(InstitutionScopedAdminMixin, admin.ModelAdmin):
    institution_fk_name = "student__institution"
```

Bu mixin `get_queryset`, `has_*_permission`, `formfield_for_foreignkey` ve
`save_model`'i birlikte kapatır; yalnızca `get_queryset` override etmek yeterli
değildir çünkü açılır listeler ve doğrudan URL'ler ayrı sızıntı yüzeyleridir.

**`admin.ModelAdmin` alt sınıflarında kapsam mixin'ini eklemeyi unutmak, sessiz bir
veri sızıntısıdır.** Yeni bir admin eklerken mixin'i eklediğini doğrula.

## Roller ve kapsam

| Rol | Kapsam |
|---|---|
| `superuser` | Sınırsız (tüm tenant'lar) |
| `tenant_admin` | Tenant'ın tüm kurumları |
| `institution_manager` | Tek kurum |
| (yetkisiz) | Hiçbir kayıt |

Kapsam host'a bağlıdır: bir kullanıcı A tenant'ının adminiyse, B tenant'ının
host'undan giriş yaptığında kapsamı **boştur**.

```python
from infra_core.tenancy import resolve_scope, scope_queryset

scope = resolve_scope(request.user, tenant=request.tenant)
queryset = scope_queryset(Student.objects.all(), scope)
```

View'larda `request.scope` ve `request.institution` `InstitutionContextMiddleware`
tarafından doldurulur.

## Kurum türüne göre seçenekler

`grade`/`level` alanları model seviyesinde tüm değerleri kabul eder; daraltma
formda yapılır:

```python
from infra_core.forms import InstitutionTypeAwareFormMixin

class StudentForm(InstitutionTypeAwareFormMixin, forms.ModelForm):
    class Meta:
        model = Student
        fields = "__all__"
```

`BaseStudent.clean()` kurumun `effective_institution_type` değerine göre doğrulama
yapmaya devam eder (kurumda tür boşsa tenant'ınki kullanılır).

## BaseInstitution alanları

`name`, `code`, `address`, `location`, `phone`, `email`, `gender`,
`institution_type`, `tenant`, `manager` (deprecated), `is_active`,
`created_at`, `updated_at`

## BaseStudent alanları

`first_name`, `last_name`, `grade`, `level`, `status`, `created_at`, `updated_at`

## Import/Export

- `BaseModelResource`: ortak resource ayarları
- `ImportExportAdminMixin`: ortak import/export admin mixin'i

## Testler

```bash
python -m venv .venv-test
.venv-test/bin/pip install "Django>=5.2" pytest pytest-django
.venv-test/bin/python -m pytest
```

`tests/` altında gerçek bir Django projesi kurulur ve izolasyon hem kapsam
fonksiyonları hem de canlı admin istekleri üzerinden doğrulanır.
