first commit
This commit is contained in:
42
.env
Normal file
42
.env
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Django Settings
|
||||||
|
DEBUG=1
|
||||||
|
SECRET_KEY=insta-django-Uq6bv1c4jcSjv50hA6sj6C8d18G3uHMowQz1EL5ljVvISPm1k8kqxZ1iMCCOy2ZPZCY23i
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,api.denizogur.com.tr,localhost:8000
|
||||||
|
CELERY_BROKER_URL=redis://default:gg7678290@10.80.80.70:6379/5
|
||||||
|
# Site URL - Production ve Development için buradan değiştirin
|
||||||
|
# SITE_URL=https://api.denizogur.com.tr
|
||||||
|
SITE_URL=http://localhost:8000 # Development için bu satırı aktif edin
|
||||||
|
|
||||||
|
# .env
|
||||||
|
USERNAME_SUFFIX_MIN=2
|
||||||
|
USERNAME_SUFFIX_MAX=999
|
||||||
|
|
||||||
|
# Database Settings (Mevcut PostgreSQL sunucunuz)
|
||||||
|
USE_POSTGRES=False
|
||||||
|
POSTGRES_DB=shop
|
||||||
|
POSTGRES_USER=cloud
|
||||||
|
POSTGRES_PASSWORD=gg7678290
|
||||||
|
POSTGRES_HOST=10.80.80.70
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# Social Auth (Google)
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-google-oauth2-key
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-google-oauth2-secret
|
||||||
|
|
||||||
|
# Social Auth (GitHub)
|
||||||
|
SOCIAL_AUTH_GITHUB_KEY=your-github-key
|
||||||
|
SOCIAL_AUTH_GITHUB_SECRET=your-github-secret
|
||||||
|
|
||||||
|
# Email Settings (Optional)
|
||||||
|
EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
EMAIL_HOST=212.64.215.243
|
||||||
|
EMAIL_PORT=1025
|
||||||
|
EMAIL_HOST_USER=''
|
||||||
|
EMAIL_HOST_PASSWORD=''
|
||||||
|
EMAIL_USE_TLS=False
|
||||||
|
EMAIL_USE_SSL=False
|
||||||
|
DEFAULT_FROM_EMAIL='noreply@localhost'
|
||||||
|
|
||||||
|
|
||||||
|
CELERY_BROKER_URL='redis://default:gg7678290@10.80.80.70:6379/3'
|
||||||
|
CELERY_RESULT_BACKEND='django-db'
|
||||||
33
.env.example
Normal file
33
.env.example
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Django Settings
|
||||||
|
DEBUG=1
|
||||||
|
SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
|
||||||
|
# Database Settings (PostgreSQL)
|
||||||
|
USE_POSTGRES=False
|
||||||
|
POSTGRES_DB=insta_db
|
||||||
|
POSTGRES_USER=postgres_user
|
||||||
|
POSTGRES_PASSWORD=postgres_password
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# Celery & Redis Settings
|
||||||
|
CELERY_BROKER_URL=redis://localhost:6379/5
|
||||||
|
|
||||||
|
# Social Auth (Google)
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=your-google-oauth2-key
|
||||||
|
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=your-google-oauth2-secret
|
||||||
|
|
||||||
|
# Social Auth (GitHub)
|
||||||
|
SOCIAL_AUTH_GITHUB_KEY=your-github-key
|
||||||
|
SOCIAL_AUTH_GITHUB_SECRET=your-github-secret
|
||||||
|
|
||||||
|
# Email Settings
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||||
|
EMAIL_HOST=localhost
|
||||||
|
EMAIL_PORT=1025
|
||||||
|
EMAIL_HOST_USER=
|
||||||
|
EMAIL_HOST_PASSWORD=
|
||||||
|
EMAIL_USE_TLS=False
|
||||||
|
EMAIL_USE_SSL=False
|
||||||
|
DEFAULT_FROM_EMAIL=noreply@localhost
|
||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
*.sqlite3
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env.DS_Store
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.log
|
||||||
|
db.sqlite3
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
567
GUIDE.md
Normal file
567
GUIDE.md
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
# Instagram Clone API - Detaylı Kılavuz
|
||||||
|
|
||||||
|
## 📋 İçindekiler
|
||||||
|
1. [Proje Yapısı](#proje-yapısı)
|
||||||
|
2. [Kurulum](#kurulum)
|
||||||
|
3. [Çalıştırma](#çalıştırma)
|
||||||
|
4. [Özellikler](#özellikler)
|
||||||
|
5. [API Endpoints](#api-endpoints)
|
||||||
|
6. [Veritabanı Modelleri](#veritabanı-modelleri)
|
||||||
|
7. [Celery & Otomasyonlar](#celery--otomasyonlar)
|
||||||
|
8. [Admin Paneli](#admin-paneli)
|
||||||
|
9. [Sorun Giderme](#sorun-giderme)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Proje Yapısı
|
||||||
|
|
||||||
|
```
|
||||||
|
insta/
|
||||||
|
├── core/ # Django ana ayarları
|
||||||
|
│ ├── settings.py # Proje konfigürasyonu
|
||||||
|
│ ├── urls.py # Ana URL router
|
||||||
|
│ ├── celery.py # Celery app tanımı
|
||||||
|
│ └── wsgi.py
|
||||||
|
├── accounts/ # Kullanıcı yönetimi
|
||||||
|
│ ├── models.py # CustomUser model (active_until ile)
|
||||||
|
│ ├── views.py # Auth views
|
||||||
|
│ ├── urls.py # Auth endpoints
|
||||||
|
│ ├── admin.py # Admin paneli
|
||||||
|
│ ├── middleware.py # Hesap süresi kontrol
|
||||||
|
│ ├── tasks.py # Celery görevleri
|
||||||
|
│ └── migrations/
|
||||||
|
├── namecreate/ # ML model eğitimi
|
||||||
|
│ ├── models.py # TrainingJob model
|
||||||
|
│ ├── views.py # Model eğitim endpoints
|
||||||
|
│ ├── tasks.py # Eğitim task'ı
|
||||||
|
│ ├── admin.py # Admin paneli
|
||||||
|
│ ├── urls.py # ML endpoints
|
||||||
|
│ └── migrations/
|
||||||
|
├── db.sqlite3 # Veritabanı (geliştirme)
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
└── .env # Ortam değişkenleri
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Kurulum
|
||||||
|
|
||||||
|
### 1. Virtual Environment Oluştur
|
||||||
|
```bash
|
||||||
|
cd /home/beyhan/Projeler/python/insta
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # Linux/Mac
|
||||||
|
# veya
|
||||||
|
.venv\Scripts\activate # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Dependencies Yükle
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Veritabanı Hazırla
|
||||||
|
```bash
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Admin Kullanıcısı Oluştur
|
||||||
|
```bash
|
||||||
|
python manage.py createsuperuser
|
||||||
|
# Email: admin@example.com
|
||||||
|
# Password: ***
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. .env Dosyası Oluştur
|
||||||
|
```bash
|
||||||
|
cat > .env << EOF
|
||||||
|
DEBUG=True
|
||||||
|
SECRET_KEY=your-secret-key-here
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
|
||||||
|
# Redis (Celery)
|
||||||
|
CELERY_BROKER_URL=redis://localhost:6379/5
|
||||||
|
|
||||||
|
# Go Servisi (opsiyonel)
|
||||||
|
GO_SERVICE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Çalıştırma
|
||||||
|
|
||||||
|
### Terminal 1: Django Development Server
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
python manage.py runserver
|
||||||
|
# Erişim: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 2: Celery Worker
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
celery -A core worker -l info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 3: Celery Beat (Scheduler)
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
celery -A core beat -l info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 4: Redis (Gerekli)
|
||||||
|
```bash
|
||||||
|
redis-server
|
||||||
|
# veya
|
||||||
|
docker run -d -p 6379:6379 redis:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⭐ Özellikler
|
||||||
|
|
||||||
|
### 1. **Admin-Only Kullanıcı Kaydı**
|
||||||
|
- Sadece admin kullanıcılar yeni hesap oluşturabilir
|
||||||
|
- `/api/v1/auth/users/` - POST (admin only)
|
||||||
|
- `/api/v1/auth/users/activation/` - POST (admin only)
|
||||||
|
- `/api/v1/auth/users/resend_activation/` - POST (admin only)
|
||||||
|
|
||||||
|
### 2. **Zamanlı Hesap Aktifliği**
|
||||||
|
- Kullanıcılara sınırlı kullanım süresi verilebilir
|
||||||
|
- `active_until` alanı ile kontrol edilir
|
||||||
|
- Otomatik pasife çekilir (middleware + Celery)
|
||||||
|
|
||||||
|
### 3. **Model Eğitimi (ML)**
|
||||||
|
- RandomForest modeli eğitilir
|
||||||
|
- ONNX formatına kaydedilir
|
||||||
|
- Metrikleri kaydedilir (accuracy, precision, recall, f1)
|
||||||
|
- Go servisine bildirilir
|
||||||
|
|
||||||
|
### 4. **Celery Otomasyonları**
|
||||||
|
- Süresi dolmuş hesapları günlük deaktif etme
|
||||||
|
- Model eğitimini arka planda yapma
|
||||||
|
- Database scheduler ile yönetim
|
||||||
|
|
||||||
|
### 5. **Admin Panelden Terminalsiz Uretim (2 Mod)**
|
||||||
|
- Terminal erisimi olmadan admin panelden kisi uretimi yapabilirsiniz.
|
||||||
|
- Yol: `Admin > Namecreate > Training jobs`
|
||||||
|
- Bir veya birden fazla `TrainingJob` secin, sonra `Action` menusunden secin:
|
||||||
|
- `Secili job(lar) icin 100 kisi uret (Istatistiksel)`
|
||||||
|
- `Secili job(lar) icin 100 kisi uret (LLM + fallback)`
|
||||||
|
- `Secili job(lar) icin 1000 kisi uret (Istatistiksel)`
|
||||||
|
- `Secili job(lar) icin 1000 kisi uret (LLM + fallback)`
|
||||||
|
- LLM servisi kapaliysa `LLM + fallback` secenegi otomatik olarak istatistiksel uretime duser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 API Endpoints
|
||||||
|
|
||||||
|
### **Kimlik Doğrulama (Djoser)**
|
||||||
|
|
||||||
|
#### Kayıt (Admin Only)
|
||||||
|
```bash
|
||||||
|
POST /api/v1/auth/users/
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "strongpass123",
|
||||||
|
"re_password": "strongpass123",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Yanıt (201 Created)
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"email": "user@example.com",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
```bash
|
||||||
|
POST /api/v1/auth/jwt/create/
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "strongpass123"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Yanıt
|
||||||
|
{
|
||||||
|
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Profil Bilgileri
|
||||||
|
```bash
|
||||||
|
GET /api/v1/auth/users/me/
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
|
||||||
|
# Yanıt
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"email": "user@example.com",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"is_active": true,
|
||||||
|
"date_joined": "2026-03-27T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **ML Model Eğitimi**
|
||||||
|
|
||||||
|
#### Eğitim Başlat
|
||||||
|
```bash
|
||||||
|
POST /api/v1/ml/train-model/
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
|
||||||
|
# Yanıt (Task başlatıldı)
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"message": "Model eğitim görevi başlatıldı.",
|
||||||
|
"task_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"celery_task_id": "abc123xyz"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Eğitim Durumunu Sorgula
|
||||||
|
```bash
|
||||||
|
GET /api/v1/ml/training-status/?task_id=550e8400-e29b-41d4-a716-446655440000
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
|
||||||
|
# Yanıt (Tamamlandı)
|
||||||
|
{
|
||||||
|
"task_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2026-03-27T12:00:00Z",
|
||||||
|
"started_at": "2026-03-27T12:00:05Z",
|
||||||
|
"completed_at": "2026-03-27T12:01:30Z",
|
||||||
|
"model_version": "2026-03-27T12-00-00",
|
||||||
|
"metrics": {
|
||||||
|
"accuracy": 0.96,
|
||||||
|
"precision": 0.97,
|
||||||
|
"recall": 0.95,
|
||||||
|
"f1_score": 0.96
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Veritabanı Modelleri
|
||||||
|
|
||||||
|
### **CustomUser (accounts/models.py)**
|
||||||
|
```python
|
||||||
|
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||||
|
email # Unique email
|
||||||
|
first_name # İsim
|
||||||
|
last_name # Soyisim
|
||||||
|
is_active # Aktif mi?
|
||||||
|
is_staff # Admin mi?
|
||||||
|
is_superuser # Superuser mi?
|
||||||
|
active_until # ⭐ Hesap bitiş tarihi
|
||||||
|
date_joined # Kayıt tarihi
|
||||||
|
last_login # Son giriş
|
||||||
|
|
||||||
|
# Metodlar:
|
||||||
|
is_expired() # Süresi doldu mu?
|
||||||
|
deactivate_if_expired() # Pasif yap
|
||||||
|
set_active_for_days(days) # N gün için aktif et
|
||||||
|
```
|
||||||
|
|
||||||
|
### **TrainingJob (namecreate/models.py)**
|
||||||
|
```python
|
||||||
|
class TrainingJob(models.Model):
|
||||||
|
task_id # Celery task ID
|
||||||
|
status # pending, running, completed, failed
|
||||||
|
model_type # Model tipi (RandomForest)
|
||||||
|
model_version # Timestamp (versiyonlama)
|
||||||
|
model_path # Dosya yolu
|
||||||
|
|
||||||
|
# Metrikleri
|
||||||
|
accuracy # Doğruluk
|
||||||
|
precision # Kesinlik
|
||||||
|
recall # Geri çağırma
|
||||||
|
f1_score # F1 skoru
|
||||||
|
|
||||||
|
# Zaman damgaları
|
||||||
|
created_at # Oluşturulma
|
||||||
|
started_at # Başlama
|
||||||
|
completed_at # Tamamlanma
|
||||||
|
|
||||||
|
# Go Servisi
|
||||||
|
go_service_notified # Bildirildi mi?
|
||||||
|
|
||||||
|
# Hata
|
||||||
|
error_message # Hata mesajı
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 Celery & Otomasyonlar
|
||||||
|
|
||||||
|
### **Otomatik Prosesler**
|
||||||
|
|
||||||
|
#### 1. Süresi Dolmuş Hesapları Deaktif Etme
|
||||||
|
**Periyodiklik:** Her 24 saatte 1 kez
|
||||||
|
**Task:** `accounts.tasks.deactivate_expired_users_task()`
|
||||||
|
**Veritabanı:** `active_until <= now()` olan users pasif olur
|
||||||
|
|
||||||
|
Manuel çalıştırmak:
|
||||||
|
```bash
|
||||||
|
python manage.py deactivate_expired_users
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Model Eğitimi
|
||||||
|
**Periyodiklik:** İsteğe bağlı (manual trigger)
|
||||||
|
**Task:** `namecreate.tasks.train_model_task(task_id)`
|
||||||
|
**İşler:**
|
||||||
|
1. Veriyi yükle (Iris dataset)
|
||||||
|
2. Modeli eğit (RandomForest)
|
||||||
|
3. Metrikleri hesapla
|
||||||
|
4. ONNX formatına kaydet (versiyonlu)
|
||||||
|
5. Go servisine bildir
|
||||||
|
|
||||||
|
### **Celery Settings (core/settings.py)**
|
||||||
|
```python
|
||||||
|
CELERY_BROKER_URL = 'redis://localhost:6379/5'
|
||||||
|
CELERY_RESULT_BACKEND = 'django-db'
|
||||||
|
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||||
|
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
'deactivate-expired-users-daily': {
|
||||||
|
'task': 'accounts.tasks.deactivate_expired_users_task',
|
||||||
|
'schedule': 60 * 60 * 24, # Her 24 saat
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Task Monitoring**
|
||||||
|
|
||||||
|
Celery Flower ile gerçek zamanlı monitoring:
|
||||||
|
```bash
|
||||||
|
pip install flower
|
||||||
|
celery -A core flower
|
||||||
|
# Erişim: http://localhost:5555
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨💼 Admin Paneli
|
||||||
|
|
||||||
|
### **Admin Erişimi**
|
||||||
|
```
|
||||||
|
http://localhost:8000/admin/
|
||||||
|
Email: admin@example.com
|
||||||
|
Şifre: ***
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Admin Sayfalarında Neleri Yönetebilirsin?**
|
||||||
|
|
||||||
|
#### 1. **Kullanıcılar** (`/admin/accounts/customuser/`)
|
||||||
|
- Yeni kullanıcı oluştur
|
||||||
|
- `active_until` tarihi belirle
|
||||||
|
- Hesabı aktif/pasif yap
|
||||||
|
- Filtreleme: İçinden çıkış tarihi, aktif durum, staff status
|
||||||
|
|
||||||
|
#### 2. **Eğitim Görevleri** (`/admin/namecreate/trainingjob/`)
|
||||||
|
- Tüm eğitim görevlerini görüntüle
|
||||||
|
- Status takibi: pending → running → completed/failed
|
||||||
|
- Metrikleri kontrol et (accuracy, f1, vb.)
|
||||||
|
- Error loglarını göster (başarısız task'lar)
|
||||||
|
- Go servisine bildirilip bildirilmediğini kontrol et
|
||||||
|
|
||||||
|
#### 3. **Periyodik Görevler** (`/admin/django_celery_beat/periodictask/`)
|
||||||
|
- Celery Beat task'larını yönet
|
||||||
|
- Schedule'ı özelleştir
|
||||||
|
- Yeni periyodik görev ekle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Sorun Giderme
|
||||||
|
|
||||||
|
### **Problem: Celery Worker hata veriyor**
|
||||||
|
|
||||||
|
**Çözüm 1:** Redis çalışıyor mu?
|
||||||
|
```bash
|
||||||
|
redis-cli ping
|
||||||
|
# Yanıt: PONG ise OK
|
||||||
|
```
|
||||||
|
|
||||||
|
**Çözüm 2:** Hata logu kontrol et
|
||||||
|
```bash
|
||||||
|
celery -A core worker -l debug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Çözüm 3:** Database migrations kontrol et
|
||||||
|
```bash
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Problem: Model eğitimi başlamıyor**
|
||||||
|
|
||||||
|
**Çözüm 1:** Task'ı manuel test et
|
||||||
|
```bash
|
||||||
|
python manage.py shell
|
||||||
|
>>> from namecreate.tasks import train_model_task
|
||||||
|
>>> task_id = 'test-123'
|
||||||
|
>>> from namecreate.models import TrainingJob
|
||||||
|
>>> job = TrainingJob.objects.create(task_id=task_id)
|
||||||
|
>>> result = train_model_task.delay(task_id)
|
||||||
|
>>> result.get()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Çözüm 2:** Worker loglarını kontrol et (`celery -A core worker -l info`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Problem: Admin panelinde TrainingJob görmüyorum**
|
||||||
|
|
||||||
|
**Çözüm:** Migration'ı çalıştır
|
||||||
|
```bash
|
||||||
|
python manage.py migrate namecreate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Problem: Djoser endpoints 403 Forbidden dönem**
|
||||||
|
|
||||||
|
**Sebep:** Admin değilsin (register/activation admin-only'dir)
|
||||||
|
- Admin kullanıcı ile yapabilirsin
|
||||||
|
- Veya superuser token'ı kullan
|
||||||
|
|
||||||
|
**Test:**
|
||||||
|
```bash
|
||||||
|
# Superuser ile
|
||||||
|
TOKEN=$(curl -X POST http://localhost:8000/api/v1/auth/jwt/create/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"admin@example.com","password":"***"}' | jq -r '.access')
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8000/api/v1/auth/users/ \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email":"newuser@example.com",
|
||||||
|
"password":"pass123",
|
||||||
|
"re_password":"pass123"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Örnek İş Akışı
|
||||||
|
|
||||||
|
### **Senaryo 1: Yeni Kullanıcı Ekleme (Admin)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Admin token al
|
||||||
|
curl -X POST http://localhost:8000/api/v1/auth/jwt/create/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"admin@example.com","password":"adminpass123"}' \
|
||||||
|
| jq .
|
||||||
|
|
||||||
|
# Response: access token vs refresh token
|
||||||
|
|
||||||
|
# 2. Yeni kullanıcı oluştur
|
||||||
|
curl -X POST http://localhost:8000/api/v1/auth/users/ \
|
||||||
|
-H "Authorization: Bearer <ACCESS_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email":"john@example.com",
|
||||||
|
"password":"johnpass123",
|
||||||
|
"re_password":"johnpass123",
|
||||||
|
"first_name":"John",
|
||||||
|
"last_name":"Doe"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 3. Admin panelinden active_until tarihi belirle
|
||||||
|
# http://localhost:8000/admin/accounts/customuser/
|
||||||
|
# John'ın profiline gir → active_until = 2026-04-27 (30 gün başlar)
|
||||||
|
|
||||||
|
# 4. 30 gün sonra otomatik pasif olur
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Senaryo 2: Model Eğitimi**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Eğitim başlat
|
||||||
|
curl -X POST http://localhost:8000/api/v1/ml/train-model/ \
|
||||||
|
-H "Authorization: Bearer <TOKEN>" \
|
||||||
|
| jq .
|
||||||
|
|
||||||
|
# Response: task_id = "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
|
||||||
|
# 2. Durum sorgula (5-10 saniye sonra)
|
||||||
|
curl http://localhost:8000/api/v1/ml/training-status/?task_id=550e8400-e29b-41d4-a716-446655440000 \
|
||||||
|
-H "Authorization: Bearer <TOKEN>" \
|
||||||
|
| jq .
|
||||||
|
|
||||||
|
# 3. Tamamlanınca:
|
||||||
|
# - Model: media/models/iris_model_2026-03-27_12-30-45.onnx
|
||||||
|
# - Metrikler: accuracy=0.96, f1=0.96
|
||||||
|
# - Go servisi bilgilendirildi (varsa)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notlar
|
||||||
|
|
||||||
|
- **Geliştirme:** SQLite kullanıyor. Production'da PostgreSQL kullan.
|
||||||
|
- **Redis:** Celery için gerekli. Docker'da `redis:latest` kullan.
|
||||||
|
- **Go Servisi:** `.env` dosyasında `GO_SERVICE_URL` tanımlarsan otomatik bildirim yapılır.
|
||||||
|
- **Emails:** Console backend kullanıyor. SMTP konfigürasyonu yapabilirsin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Faydalı Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Veritabanı işlemleri
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py flush # Tüm veriyi sil (DİKKAT!)
|
||||||
|
|
||||||
|
# Shell (Python repl)
|
||||||
|
python manage.py shell
|
||||||
|
|
||||||
|
# Superuser oluştur
|
||||||
|
python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Celery test
|
||||||
|
python manage.py celery -A core worker -l info
|
||||||
|
|
||||||
|
# Datab Shell (SQL)
|
||||||
|
python manage.py dbshell
|
||||||
|
|
||||||
|
# Yeni app oluştur
|
||||||
|
python manage.py startapp appname
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Sonraki Adımlar
|
||||||
|
|
||||||
|
- [ ] PostgreSQL kurulumu
|
||||||
|
- [ ] Production settings
|
||||||
|
- [ ] Docker containerization
|
||||||
|
- [ ] CI/CD pipeline (GitHub Actions)
|
||||||
|
- [ ] Monitoring & logging
|
||||||
|
- [ ] API Rate limiting
|
||||||
|
- [ ] WebSocket entegrasyonu (asenkron güncellemeler)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Hazırlandı:** 27 Mart 2026
|
||||||
|
**Versiyon:** 1.0
|
||||||
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
37
accounts/admin.py
Normal file
37
accounts/admin.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CustomUser)
|
||||||
|
class CustomUserAdmin(BaseUserAdmin):
|
||||||
|
"""
|
||||||
|
Custom admin panel configuration for CustomUser model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Fields to display in the user list
|
||||||
|
list_display = ('email', 'first_name', 'last_name', 'is_staff', 'is_active', 'active_until', 'date_joined')
|
||||||
|
list_filter = ('is_staff', 'is_superuser', 'is_active', 'active_until', 'date_joined')
|
||||||
|
search_fields = ('email', 'first_name', 'last_name')
|
||||||
|
ordering = ('-date_joined',)
|
||||||
|
|
||||||
|
# Fields to display on the user detail/edit page
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email', 'password')}),
|
||||||
|
(_('Personal info'), {'fields': ('first_name', 'last_name', 'active_until')}),
|
||||||
|
(_('Permissions'), {
|
||||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
|
||||||
|
}),
|
||||||
|
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fields to display when creating a new user
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email', 'password1', 'password2', 'first_name', 'last_name', 'active_until', 'is_staff', 'is_active'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ('date_joined', 'last_login')
|
||||||
5
accounts/apps.py
Normal file
5
accounts/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
name = 'accounts'
|
||||||
0
accounts/management/__init__.py
Normal file
0
accounts/management/__init__.py
Normal file
0
accounts/management/commands/__init__.py
Normal file
0
accounts/management/commands/__init__.py
Normal file
12
accounts/management/commands/deactivate_expired_users.py
Normal file
12
accounts/management/commands/deactivate_expired_users.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from accounts.tasks import deactivate_expired_users_task
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Deactivate users whose active period has expired.'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
updated_count = deactivate_expired_users_task()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Deactivated {updated_count} expired user(s).'))
|
||||||
54
accounts/middleware.py
Normal file
54
accounts/middleware.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Custom middleware for social authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import logout
|
||||||
|
from django.http import HttpResponseForbidden, JsonResponse
|
||||||
|
|
||||||
|
|
||||||
|
class SocialAuthExceptionMiddleware:
|
||||||
|
"""
|
||||||
|
Middleware to handle social auth exceptions and redirect properly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_exception(self, request, exception):
|
||||||
|
"""Handle social auth exceptions."""
|
||||||
|
from social_core.exceptions import AuthException
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
if isinstance(exception, AuthException):
|
||||||
|
return HttpResponseRedirect(f'/api/v1/auth/social/error/?error={str(exception)}')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AccountExpirationMiddleware:
|
||||||
|
"""
|
||||||
|
Deactivate users automatically when their access period has expired.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
|
||||||
|
if user and user.is_authenticated and hasattr(user, 'deactivate_if_expired'):
|
||||||
|
if user.deactivate_if_expired():
|
||||||
|
logout(request)
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return JsonResponse(
|
||||||
|
{'detail': 'Account expired. Please contact an administrator.'},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
return HttpResponseForbidden('Account expired. Please contact an administrator.')
|
||||||
|
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
37
accounts/migrations/0001_initial.py
Normal file
37
accounts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-11 21:31
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('email', models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True, verbose_name='email address')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
accounts/migrations/0002_customuser_active_until.py
Normal file
18
accounts/migrations/0002_customuser_active_until.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 19:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='active_until',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='If set, the account is automatically deactivated after this date.', null=True, verbose_name='active until'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
129
accounts/models.py
Normal file
129
accounts/models.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserManager(BaseUserManager):
|
||||||
|
"""
|
||||||
|
Custom user manager where email is the unique identifier
|
||||||
|
for authentication instead of username.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_user(self, email, password=None, **extra_fields):
|
||||||
|
"""
|
||||||
|
Create and save a regular user with the given email and password.
|
||||||
|
"""
|
||||||
|
if not email:
|
||||||
|
raise ValueError(_('The Email field must be set'))
|
||||||
|
|
||||||
|
email = self.normalize_email(email)
|
||||||
|
user = self.model(email=email, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, email, password=None, **extra_fields):
|
||||||
|
"""
|
||||||
|
Create and save a SuperUser with the given email and password.
|
||||||
|
"""
|
||||||
|
extra_fields.setdefault('is_staff', True)
|
||||||
|
extra_fields.setdefault('is_superuser', True)
|
||||||
|
extra_fields.setdefault('is_active', True)
|
||||||
|
|
||||||
|
if extra_fields.get('is_staff') is not True:
|
||||||
|
raise ValueError(_('Superuser must have is_staff=True.'))
|
||||||
|
if extra_fields.get('is_superuser') is not True:
|
||||||
|
raise ValueError(_('Superuser must have is_superuser=True.'))
|
||||||
|
|
||||||
|
return self.create_user(email, password, **extra_fields)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||||
|
"""
|
||||||
|
Custom user model where email is used instead of username.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- email: unique email address (used for login)
|
||||||
|
- first_name: user's first name
|
||||||
|
- last_name: user's last name
|
||||||
|
- is_staff: designates whether user can log into admin site
|
||||||
|
- is_active: designates whether user account is active
|
||||||
|
- date_joined: when the user account was created
|
||||||
|
"""
|
||||||
|
|
||||||
|
email = models.EmailField(
|
||||||
|
_('email address'),
|
||||||
|
unique=True,
|
||||||
|
error_messages={
|
||||||
|
'unique': _("A user with that email already exists."),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
first_name = models.CharField(_('first name'), max_length=150, blank=True)
|
||||||
|
last_name = models.CharField(_('last name'), max_length=150, blank=True)
|
||||||
|
is_staff = models.BooleanField(
|
||||||
|
_('staff status'),
|
||||||
|
default=False,
|
||||||
|
help_text=_('Designates whether the user can log into this admin site.'),
|
||||||
|
)
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
_('active'),
|
||||||
|
default=True,
|
||||||
|
help_text=_(
|
||||||
|
'Designates whether this user should be treated as active. '
|
||||||
|
'Unselect this instead of deleting accounts.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
active_until = models.DateTimeField(
|
||||||
|
_('active until'),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_('If set, the account is automatically deactivated after this date.'),
|
||||||
|
)
|
||||||
|
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||||
|
|
||||||
|
# Specify that we use email as the username field
|
||||||
|
USERNAME_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = [] # Email is already required by USERNAME_FIELD
|
||||||
|
|
||||||
|
objects = CustomUserManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('user')
|
||||||
|
verbose_name_plural = _('users')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
def get_full_name(self):
|
||||||
|
"""
|
||||||
|
Return the first_name plus the last_name, with a space in between.
|
||||||
|
"""
|
||||||
|
full_name = f'{self.first_name} {self.last_name}'
|
||||||
|
return full_name.strip()
|
||||||
|
|
||||||
|
def get_short_name(self):
|
||||||
|
"""
|
||||||
|
Return the short name for the user.
|
||||||
|
"""
|
||||||
|
return self.first_name
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
"""Return True when account usage period has ended."""
|
||||||
|
return bool(self.active_until and timezone.now() >= self.active_until)
|
||||||
|
|
||||||
|
def deactivate_if_expired(self, save=True):
|
||||||
|
"""Deactivate account if active_until has passed."""
|
||||||
|
if self.is_active and self.is_expired():
|
||||||
|
self.is_active = False
|
||||||
|
if save:
|
||||||
|
self.save(update_fields=['is_active'])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_active_for_days(self, days, save=True):
|
||||||
|
"""Enable user for a fixed number of days from now."""
|
||||||
|
self.active_until = timezone.now() + timezone.timedelta(days=days)
|
||||||
|
self.is_active = True
|
||||||
|
if save:
|
||||||
|
self.save(update_fields=['active_until', 'is_active'])
|
||||||
19
accounts/pipeline.py
Normal file
19
accounts/pipeline.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"""
|
||||||
|
Custom pipeline functions for Python Social Auth.
|
||||||
|
These functions are called during the social authentication process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def activate_user(strategy, details, user=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Custom pipeline step to ensure social auth users are active.
|
||||||
|
|
||||||
|
This ensures that users who register via social login don't need
|
||||||
|
email activation - they are automatically activated since the social
|
||||||
|
provider has already verified their email.
|
||||||
|
"""
|
||||||
|
if user and not user.is_active:
|
||||||
|
user.is_active = True
|
||||||
|
user.save(update_fields=['is_active'])
|
||||||
|
return {'user': user}
|
||||||
|
|
||||||
74
accounts/serializers.py
Normal file
74
accounts/serializers.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer
|
||||||
|
from djoser.serializers import UserSerializer as BaseUserSerializer
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserCreateSerializer(BaseUserCreateSerializer):
|
||||||
|
"""
|
||||||
|
Custom serializer for user registration.
|
||||||
|
Sets is_active=False by default so users must activate via email.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta(BaseUserCreateSerializer.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = ('id', 'email', 'password', 're_password', 'first_name', 'last_name')
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""
|
||||||
|
Override create to ensure is_active=False for email/password registrations.
|
||||||
|
Social auth users will have is_active=True set via pipeline.
|
||||||
|
"""
|
||||||
|
# Remove re_password as it's only for validation
|
||||||
|
validated_data.pop('re_password', None)
|
||||||
|
|
||||||
|
# Create user with is_active=False
|
||||||
|
user = CustomUser.objects.create_user(
|
||||||
|
email=validated_data['email'],
|
||||||
|
password=validated_data['password'],
|
||||||
|
first_name=validated_data.get('first_name', ''),
|
||||||
|
last_name=validated_data.get('last_name', ''),
|
||||||
|
is_active=False # Requires email activation
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserSerializer(BaseUserSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for user details.
|
||||||
|
Used for current user endpoint and user profile.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta(BaseUserSerializer.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = ('id', 'email', 'first_name', 'last_name', 'is_active', 'date_joined')
|
||||||
|
read_only_fields = ('id', 'email', 'is_active', 'date_joined')
|
||||||
|
|
||||||
|
|
||||||
|
class SocialLoginSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Serializer for social authentication.
|
||||||
|
Accepts provider name and access_token from frontend.
|
||||||
|
"""
|
||||||
|
provider = serializers.ChoiceField(
|
||||||
|
choices=['google-oauth2', 'github', 'facebook'],
|
||||||
|
help_text="Social auth provider name"
|
||||||
|
)
|
||||||
|
access_token = serializers.CharField(
|
||||||
|
help_text="Access token from the social provider"
|
||||||
|
)
|
||||||
|
id_token = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
help_text="ID token (optional, used by some providers like Google)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_provider(self, value):
|
||||||
|
"""Validate that the provider is supported."""
|
||||||
|
valid_providers = ['google-oauth2', 'github', 'facebook']
|
||||||
|
if value not in valid_providers:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Invalid provider. Must be one of: {', '.join(valid_providers)}"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
16
accounts/tasks.py
Normal file
16
accounts/tasks.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(name='accounts.tasks.deactivate_expired_users_task')
|
||||||
|
def deactivate_expired_users_task():
|
||||||
|
"""Deactivate active users whose active_until timestamp has passed."""
|
||||||
|
user_model = get_user_model()
|
||||||
|
now = timezone.now()
|
||||||
|
updated = user_model.objects.filter(
|
||||||
|
is_active=True,
|
||||||
|
active_until__isnull=False,
|
||||||
|
active_until__lte=now,
|
||||||
|
).update(is_active=False)
|
||||||
|
return updated
|
||||||
112
accounts/tests.py
Normal file
112
accounts/tests.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class AdminOnlyRegistrationEndpointsTests(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.admin_user = CustomUser.objects.create_superuser(
|
||||||
|
email='admin@example.com',
|
||||||
|
password='adminpass123',
|
||||||
|
)
|
||||||
|
self.regular_user = CustomUser.objects.create_user(
|
||||||
|
email='user@example.com',
|
||||||
|
password='userpass123',
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_register_endpoint_rejects_non_admin(self):
|
||||||
|
self.client.force_authenticate(user=self.regular_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/v1/auth/users/',
|
||||||
|
{
|
||||||
|
'email': 'new-user@example.com',
|
||||||
|
'password': 'strong-pass-123',
|
||||||
|
're_password': 'strong-pass-123',
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_register_endpoint_allows_admin(self):
|
||||||
|
self.client.force_authenticate(user=self.admin_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/v1/auth/users/',
|
||||||
|
{
|
||||||
|
'email': 'created-by-admin@example.com',
|
||||||
|
'password': 'strong-pass-123',
|
||||||
|
're_password': 'strong-pass-123',
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def test_activation_endpoint_rejects_non_admin(self):
|
||||||
|
self.client.force_authenticate(user=self.regular_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/v1/auth/users/activation/',
|
||||||
|
{'uid': 'invalid', 'token': 'invalid'},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_resend_activation_endpoint_rejects_non_admin(self):
|
||||||
|
self.client.force_authenticate(user=self.regular_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/v1/auth/users/resend_activation/',
|
||||||
|
{'email': self.regular_user.email},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_resend_activation_endpoint_allows_admin_access(self):
|
||||||
|
self.client.force_authenticate(user=self.admin_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/v1/auth/users/resend_activation/',
|
||||||
|
{'email': self.regular_user.email},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountExpiryTests(TestCase):
|
||||||
|
def test_user_is_deactivated_when_expired(self):
|
||||||
|
user = CustomUser.objects.create_user(
|
||||||
|
email='expired@example.com',
|
||||||
|
password='pass123456',
|
||||||
|
is_active=True,
|
||||||
|
active_until=timezone.now() - timezone.timedelta(days=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = user.deactivate_if_expired()
|
||||||
|
|
||||||
|
user.refresh_from_db()
|
||||||
|
self.assertTrue(changed)
|
||||||
|
self.assertFalse(user.is_active)
|
||||||
|
|
||||||
|
def test_user_stays_active_before_expiry(self):
|
||||||
|
user = CustomUser.objects.create_user(
|
||||||
|
email='active@example.com',
|
||||||
|
password='pass123456',
|
||||||
|
is_active=True,
|
||||||
|
active_until=timezone.now() + timezone.timedelta(days=3),
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = user.deactivate_if_expired()
|
||||||
|
|
||||||
|
user.refresh_from_db()
|
||||||
|
self.assertFalse(changed)
|
||||||
|
self.assertTrue(user.is_active)
|
||||||
22
accounts/urls.py
Normal file
22
accounts/urls.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import AdminRestrictedUserViewSet, SocialLoginView, SocialAuthCallbackView, SocialAuthSuccessView
|
||||||
|
|
||||||
|
|
||||||
|
auth_router = DefaultRouter()
|
||||||
|
auth_router.register('users', AdminRestrictedUserViewSet, basename='user')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Djoser endpoints (registration, activation, etc.)
|
||||||
|
# /api/v1/auth/users/ - POST: Register new user
|
||||||
|
# /api/v1/auth/users/activation/ - POST: Activate account with uid/token
|
||||||
|
# /api/v1/auth/users/me/ - GET: Get current user info
|
||||||
|
# /api/v1/auth/users/resend_activation/ - POST: Resend activation email
|
||||||
|
path('auth/', include(auth_router.urls)),
|
||||||
|
|
||||||
|
# Djoser JWT endpoints
|
||||||
|
# /api/v1/auth/jwt/create/ - POST: Login (get JWT tokens)
|
||||||
|
# /api/v1/auth/jwt/refresh/ - POST: Refresh access token
|
||||||
|
# /api/v1/auth/jwt/verify/ - POST: Verify token
|
||||||
|
path('auth/', include('djoser.urls.jwt')),
|
||||||
|
]
|
||||||
51
accounts/urls.py.bak
Normal file
51
accounts/urls.py.bak
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from .views import SocialLoginView, SocialAuthCallbackView, SocialAuthSuccessView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Python Social Auth URLs (MUST BE FIRST for OAuth redirect flow)
|
||||||
|
# /api/v1/social/login/github/ - GET: Start GitHub OAuth
|
||||||
|
# /api/v1/social/login/google-oauth2/ - GET: Start Google OAuth
|
||||||
|
# /api/v1/social/complete/github/ - GET: GitHub callback (handled by social-auth)
|
||||||
|
# /api/v1/social/complete/google-oauth2/ - GET: Google callback (handled by social-auth)
|
||||||
|
path('social/', include('social_django.urls', namespace='social')),
|
||||||
|
|
||||||
|
# SPA Test Page (Main app)
|
||||||
|
path('spa/', lambda request:
|
||||||
|
__import__('django.shortcuts').shortcuts.render(
|
||||||
|
request, 'spa_test/index.html'
|
||||||
|
), name='spa-test'),
|
||||||
|
|
||||||
|
# SPA Activation Page (Frontend route for email links)
|
||||||
|
path('spa/activate/<str:uid>/<str:token>/', lambda request, uid, token:
|
||||||
|
__import__('django.shortcuts').shortcuts.render(
|
||||||
|
request, 'spa_test/activate.html', {'uid': uid, 'token': token}
|
||||||
|
), name='spa-activate'),
|
||||||
|
|
||||||
|
# Django REST Framework browsable API auth
|
||||||
|
path('api-auth/', include('rest_framework.urls')),
|
||||||
|
|
||||||
|
# Djoser endpoints (registration, activation, etc.)
|
||||||
|
# /api/v1/auth/users/ - POST: Register new user
|
||||||
|
# /api/v1/auth/users/activation/ - POST: Activate account with uid/token
|
||||||
|
# /api/v1/auth/users/me/ - GET: Get current user info
|
||||||
|
# /api/v1/auth/users/resend_activation/ - POST: Resend activation email
|
||||||
|
path('auth/', include('djoser.urls')),
|
||||||
|
|
||||||
|
# Djoser JWT endpoints
|
||||||
|
# /api/v1/auth/jwt/create/ - POST: Login (get JWT tokens)
|
||||||
|
# /api/v1/auth/jwt/refresh/ - POST: Refresh access token
|
||||||
|
# /api/v1/auth/jwt/verify/ - POST: Verify token
|
||||||
|
path('auth/', include('djoser.urls.jwt')),
|
||||||
|
|
||||||
|
# Social authentication endpoints (Token-based - for mobile/SPA)
|
||||||
|
# /api/v1/auth/social/google-oauth2/ - POST: Login with Google (requires access_token)
|
||||||
|
# /api/v1/auth/social/github/ - POST: Login with GitHub (requires access_token)
|
||||||
|
# /api/v1/auth/social/facebook/ - POST: Login with Facebook (requires access_token)
|
||||||
|
path('auth/social/<str:provider>/', SocialLoginView.as_view(), name='social-login'),
|
||||||
|
|
||||||
|
# OAuth callback handler (after social-auth completes)
|
||||||
|
path('auth/social/callback/', SocialAuthCallbackView.as_view(), name='social-callback'),
|
||||||
|
|
||||||
|
# Success/Error pages
|
||||||
|
path('auth/social/success/', SocialAuthSuccessView.as_view(), name='social-success'),
|
||||||
|
]
|
||||||
283
accounts/views.py
Normal file
283
accounts/views.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views import View
|
||||||
|
from djoser.views import UserViewSet
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from social_django.utils import load_strategy, load_backend
|
||||||
|
from social_core.backends.oauth import BaseOAuth2
|
||||||
|
from social_core.exceptions import AuthException, AuthForbidden
|
||||||
|
from .serializers import SocialLoginSerializer, CustomUserSerializer
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRestrictedUserViewSet(UserViewSet):
|
||||||
|
"""
|
||||||
|
Restrict registration and activation-related endpoints to admin users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in {'create', 'activation', 'resend_activation'}:
|
||||||
|
return [IsAdminUser()]
|
||||||
|
return super().get_permissions()
|
||||||
|
|
||||||
|
|
||||||
|
class SocialLoginView(APIView):
|
||||||
|
"""
|
||||||
|
Social authentication endpoint.
|
||||||
|
Accepts access_token from social provider and returns JWT tokens.
|
||||||
|
|
||||||
|
POST /api/v1/auth/social/<provider>/
|
||||||
|
Body: { "access_token": "..." }
|
||||||
|
|
||||||
|
Supported providers: google-oauth2, github, facebook
|
||||||
|
"""
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
serializer_class = SocialLoginSerializer
|
||||||
|
|
||||||
|
def post(self, request, provider):
|
||||||
|
"""
|
||||||
|
Authenticate user with social provider token.
|
||||||
|
"""
|
||||||
|
# Validate provider
|
||||||
|
valid_providers = ['google-oauth2', 'github', 'facebook']
|
||||||
|
if provider not in valid_providers:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Invalid provider. Must be one of: {", ".join(valid_providers)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get access_token from request
|
||||||
|
access_token = request.data.get('access_token')
|
||||||
|
id_token = request.data.get('id_token', None)
|
||||||
|
|
||||||
|
if not access_token:
|
||||||
|
return Response(
|
||||||
|
{'error': 'access_token is required'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load social auth strategy and backend
|
||||||
|
strategy = load_strategy(request)
|
||||||
|
backend = load_backend(
|
||||||
|
strategy=strategy,
|
||||||
|
name=provider,
|
||||||
|
redirect_uri=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify token and get user
|
||||||
|
if isinstance(backend, BaseOAuth2):
|
||||||
|
# For OAuth2 providers, use access_token to get user info
|
||||||
|
user = backend.do_auth(access_token)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Unsupported authentication backend'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Authentication failed. Invalid token.'},
|
||||||
|
status=status.HTTP_401_UNAUTHORIZED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user is active
|
||||||
|
if not user.is_active:
|
||||||
|
# This shouldn't happen for social auth users, but just in case
|
||||||
|
user.is_active = True
|
||||||
|
user.save(update_fields=['is_active'])
|
||||||
|
|
||||||
|
# Generate JWT tokens
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
# Serialize user data
|
||||||
|
user_serializer = CustomUserSerializer(user)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'access': str(refresh.access_token),
|
||||||
|
'refresh': str(refresh),
|
||||||
|
'user': user_serializer.data
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except AuthForbidden:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Authentication forbidden. Email not provided by provider or permission denied.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
except AuthException as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Authentication error: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'An error occurred during authentication: {str(e)}'},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SocialAuthCallbackView(View):
|
||||||
|
"""
|
||||||
|
Callback view for OAuth flow completion.
|
||||||
|
After successful authentication, redirects to frontend with tokens.
|
||||||
|
"""
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = [] # No authentication required for callback
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""Handle OAuth callback and redirect to frontend with JWT tokens."""
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
# Get the authenticated user from the session
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if user and user.is_authenticated:
|
||||||
|
# Generate JWT tokens
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
# Redirect to SPA with tokens (for testing)
|
||||||
|
redirect_url = f"/api/v1/spa/?access={str(refresh.access_token)}&refresh={str(refresh)}"
|
||||||
|
|
||||||
|
print(f"[OAuth Callback] Redirecting to: {redirect_url}")
|
||||||
|
|
||||||
|
return HttpResponseRedirect(redirect_url)
|
||||||
|
else:
|
||||||
|
# Authentication failed
|
||||||
|
return HttpResponseRedirect("/api/v1/auth/social/error/?error=authentication_failed")
|
||||||
|
|
||||||
|
|
||||||
|
class SocialAuthSuccessView(APIView):
|
||||||
|
"""
|
||||||
|
Success page after social authentication.
|
||||||
|
Displays tokens for testing purposes.
|
||||||
|
"""
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = [] # No authentication required
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""Display success page with tokens."""
|
||||||
|
access_token = request.GET.get('access', '')
|
||||||
|
refresh_token = request.GET.get('refresh', '')
|
||||||
|
|
||||||
|
# Also check if user is in session
|
||||||
|
if not access_token and request.user.is_authenticated:
|
||||||
|
refresh = RefreshToken.for_user(request.user)
|
||||||
|
access_token = str(refresh.access_token)
|
||||||
|
refresh_token = str(refresh)
|
||||||
|
|
||||||
|
html_content = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Authentication Successful</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
.container {{
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}}
|
||||||
|
h1 {{
|
||||||
|
color: #28a745;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
.success-icon {{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
.token-box {{
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e1e4e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}}
|
||||||
|
.token-label {{
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}}
|
||||||
|
.token-value {{
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}}
|
||||||
|
.btn {{
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}}
|
||||||
|
.btn:hover {{
|
||||||
|
background: #5568d3;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="success-icon">✅</div>
|
||||||
|
<h1>Authentication Successful!</h1>
|
||||||
|
<p style="text-align: center; color: #666; margin-bottom: 30px;">
|
||||||
|
You have successfully authenticated with your social account.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="token-box">
|
||||||
|
<div class="token-label">Access Token:</div>
|
||||||
|
<div class="token-value" id="accessToken">{access_token}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="token-box">
|
||||||
|
<div class="token-label">Refresh Token:</div>
|
||||||
|
<div class="token-value" id="refreshToken">{refresh_token}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" onclick="copyTokens()">Copy Tokens to Clipboard</button>
|
||||||
|
<button class="btn" onclick="window.close()" style="background: #6c757d;">Close Window</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyTokens() {{
|
||||||
|
const tokens = {{
|
||||||
|
access: "{access_token}",
|
||||||
|
refresh: "{refresh_token}"
|
||||||
|
}};
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(tokens, null, 2))
|
||||||
|
.then(() => alert('Tokens copied to clipboard!'))
|
||||||
|
.catch(err => alert('Failed to copy: ' + err));
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
return HttpResponse(html_content)
|
||||||
BIN
celerybeat-schedule
Normal file
BIN
celerybeat-schedule
Normal file
Binary file not shown.
15
clery.txt
Normal file
15
clery.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# bir kere
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# worker
|
||||||
|
celery -A core worker -l info
|
||||||
|
|
||||||
|
# beat (schedule çalıştırıcı)
|
||||||
|
celery -A core beat -l info
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Request gönder ve task başlat
|
||||||
|
curl -X POST http://localhost:8000/api/v1/ml/train-model/
|
||||||
|
|
||||||
|
# Dönen task_id'i kopyala, sonra status sorgula:
|
||||||
|
curl "http://localhost:8000/api/v1/ml/training-status/?task_id=<task_id>"
|
||||||
3
core/__init__.py
Normal file
3
core/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ('celery_app',)
|
||||||
16
core/asgi.py
Normal file
16
core/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for core project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
10
core/celery.py
Normal file
10
core/celery.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
|
||||||
|
app = Celery('core')
|
||||||
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
|
app.autodiscover_tasks()
|
||||||
260
core/settings.py
Normal file
260
core/settings.py
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
"""
|
||||||
|
Django settings for core project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 6.0.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = config('SECRET_KEY', default='django-insecure-3rtts!rrr*3om)-um$5go^(-a@a2q#7he!+j&9caava+^%rw5n')
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = config('DEBUG', default=True, cast=bool)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = config('DJANGO_ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
# 3. Party
|
||||||
|
'rest_framework',
|
||||||
|
'djoser',
|
||||||
|
'drf_spectacular',
|
||||||
|
'drf_spectacular_sidecar',
|
||||||
|
'django_celery_results',
|
||||||
|
'django_celery_beat',
|
||||||
|
# My App
|
||||||
|
'accounts',
|
||||||
|
'namecreate',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'accounts.middleware.AccountExpirationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'core.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [BASE_DIR / 'templates']
|
||||||
|
,
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'core.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
|
|
||||||
|
USE_POSTGRES = config('USE_POSTGRES', default=False, cast=bool)
|
||||||
|
|
||||||
|
if USE_POSTGRES:
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': config('POSTGRES_DB', default='beyhan_blog'),
|
||||||
|
'USER': config('POSTGRES_USER', default='beyhan_blog'),
|
||||||
|
'PASSWORD': config('POSTGRES_PASSWORD', default='1923btO**'),
|
||||||
|
'HOST': config('POSTGRES_HOST', default='212.64.215.243'),
|
||||||
|
'PORT': config('POSTGRES_PORT', default='5432'),
|
||||||
|
'OPTIONS': {
|
||||||
|
'options': '-c search_path=public'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / 'static',
|
||||||
|
]
|
||||||
|
# Media files (User uploaded files)
|
||||||
|
MEDIA_URL = 'media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
'TITLE': 'Your Project API',
|
||||||
|
'DESCRIPTION': 'Your project description',
|
||||||
|
'VERSION': '1.0.0',
|
||||||
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
|
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
|
||||||
|
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||||
|
'REDOC_DIST': 'SIDECAR',
|
||||||
|
'SWAGGER_UI_SETTINGS': {
|
||||||
|
'persistAuthorization': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DJOSER = {
|
||||||
|
'LOGIN_FIELD': 'email',
|
||||||
|
'USER_ID_FIELD': 'id',
|
||||||
|
'USER_CREATE_PASSWORD_RETYPE': True,
|
||||||
|
'SERIALIZERS': {
|
||||||
|
'user_create': 'accounts.serializers.CustomUserCreateSerializer',
|
||||||
|
'current_user': 'accounts.serializers.CustomUserSerializer',
|
||||||
|
'user': 'accounts.serializers.CustomUserSerializer',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# EMAIL CONFIGURATION
|
||||||
|
# ==============================================================================
|
||||||
|
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
|
||||||
|
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||||
|
EMAIL_PORT = config('EMAIL_PORT', default=1025, cast=int)
|
||||||
|
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||||
|
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||||
|
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||||
|
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=False, cast=bool)
|
||||||
|
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='noreply@localhost')
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# CELERY & REDIS CONFIGURATION
|
||||||
|
# ==============================================================================
|
||||||
|
CELERY_BROKER_URL = config('CELERY_BROKER_URL', default='redis://127.0.0.1:6379/5')
|
||||||
|
CELERY_RESULT_BACKEND = 'django-db'
|
||||||
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
|
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
'deactivate-expired-users-daily': {
|
||||||
|
'task': 'accounts.tasks.deactivate_expired_users_task',
|
||||||
|
'schedule': 60 * 60 * 24,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
|
'LOCATION': config('CELERY_BROKER_URL', default='redis://127.0.0.1:6379/5'),
|
||||||
|
'OPTIONS': {
|
||||||
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
|
},
|
||||||
|
'KEY_PREFIX': 'insta',
|
||||||
|
'TIMEOUT': 300, # 5 dakika
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# CUSTOM USER MODEL
|
||||||
|
# ==============================================================================
|
||||||
|
AUTH_USER_MODEL = 'accounts.CustomUser'
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# GO SERVICE CONFIGURATION
|
||||||
|
# ==============================================================================
|
||||||
|
GO_SERVICE_URL = config('GO_SERVICE_URL', default=None)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# LLM GENERATOR CONFIGURATION
|
||||||
|
# ==============================================================================
|
||||||
|
# Ollama icin ornek: http://127.0.0.1:11434/api/chat
|
||||||
|
# OpenAI uyumlu endpoint icin ornek: https://api.openai.com/v1/chat/completions
|
||||||
|
LLM_API_URL = config('LLM_API_URL', default='http://127.0.0.1:11434/api/chat')
|
||||||
|
LLM_MODEL = config('LLM_MODEL', default='llama3.1:8b')
|
||||||
|
LLM_API_KEY = config('LLM_API_KEY', default='')
|
||||||
|
LLM_TIMEOUT = config('LLM_TIMEOUT', default=30, cast=int)
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# USERNAME REGENERATION SUFFIX RANGE
|
||||||
|
# ==============================================================================
|
||||||
|
# Yeniden uretimde rastgele sonek bu araliktan secilir.
|
||||||
|
# USERNAME_SUFFIX_MIN ve USERNAME_SUFFIX_MAX .env ile degistirilebilir.
|
||||||
|
USERNAME_SUFFIX_MIN = config('USERNAME_SUFFIX_MIN', default=2, cast=int)
|
||||||
|
USERNAME_SUFFIX_MAX = config('USERNAME_SUFFIX_MAX', default=999, cast=int)
|
||||||
35
core/urls.py
Normal file
35
core/urls.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for core project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/6.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/v1/', include('accounts.urls')),
|
||||||
|
path('api/v1/ml/', include('namecreate.urls')),
|
||||||
|
path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
|
# Optional UI:
|
||||||
|
path('api/v1/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
|
path('api/v1/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||||
|
]
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
16
core/wsgi.py
Normal file
16
core/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for core project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
22
manage.py
Executable file
22
manage.py
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
media/models/model_2026-03-27_19-57-58.onnx
Normal file
BIN
media/models/model_2026-03-27_19-57-58.onnx
Normal file
Binary file not shown.
0
namecreate/__init__.py
Normal file
0
namecreate/__init__.py
Normal file
250
namecreate/admin.py
Normal file
250
namecreate/admin.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import io
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import path, reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from .models import TrainingJob, NameVocab, GeneratedPerson
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratedPersonInline(admin.TabularInline):
|
||||||
|
model = GeneratedPerson
|
||||||
|
extra = 0
|
||||||
|
fields = (
|
||||||
|
'first_name', 'last_name', 'username', 'username_locked',
|
||||||
|
'birth_date', 'gender', 'confidence', 'generated_at'
|
||||||
|
)
|
||||||
|
readonly_fields = (
|
||||||
|
'first_name', 'last_name', 'username', 'username_locked',
|
||||||
|
'birth_date', 'gender', 'confidence', 'generated_at'
|
||||||
|
)
|
||||||
|
can_delete = False
|
||||||
|
show_change_link = True
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(TrainingJob)
|
||||||
|
class TrainingJobAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'id',
|
||||||
|
'task_id',
|
||||||
|
'status',
|
||||||
|
'model_type',
|
||||||
|
'model_file_exists',
|
||||||
|
'generated_person_count',
|
||||||
|
'accuracy',
|
||||||
|
'created_at',
|
||||||
|
'completed_at',
|
||||||
|
)
|
||||||
|
list_filter = ('status', 'model_type', 'created_at')
|
||||||
|
search_fields = ('task_id', 'error_message')
|
||||||
|
readonly_fields = (
|
||||||
|
'task_id',
|
||||||
|
'created_at',
|
||||||
|
'started_at',
|
||||||
|
'completed_at',
|
||||||
|
'model_version',
|
||||||
|
'model_file_exists',
|
||||||
|
'generated_person_count',
|
||||||
|
)
|
||||||
|
actions = (
|
||||||
|
'generate_100_statistical_action',
|
||||||
|
'generate_100_llm_action',
|
||||||
|
'generate_1000_statistical_action',
|
||||||
|
'generate_1000_llm_action',
|
||||||
|
)
|
||||||
|
inlines = (GeneratedPersonInline,)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Görev Bilgisi', {
|
||||||
|
'fields': ('task_id', 'status', 'created_at', 'started_at', 'completed_at')
|
||||||
|
}),
|
||||||
|
('Model', {
|
||||||
|
'fields': ('model_type', 'model_version', 'model_path', 'model_file_exists')
|
||||||
|
}),
|
||||||
|
('Uretilen Kayitlar', {
|
||||||
|
'fields': ('generated_person_count',)
|
||||||
|
}),
|
||||||
|
('Metrikler', {
|
||||||
|
'fields': ('accuracy', 'precision', 'recall', 'f1_score')
|
||||||
|
}),
|
||||||
|
('Go Servisi', {
|
||||||
|
'fields': ('go_service_notified',)
|
||||||
|
}),
|
||||||
|
('Hata', {
|
||||||
|
'fields': ('error_message',),
|
||||||
|
'classes': ('collapse',)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def model_file_exists(self, obj):
|
||||||
|
return bool(obj.model_path and __import__('os').path.exists(obj.model_path))
|
||||||
|
|
||||||
|
model_file_exists.boolean = True
|
||||||
|
model_file_exists.short_description = 'Model dosyasi var'
|
||||||
|
|
||||||
|
def generated_person_count(self, obj):
|
||||||
|
return obj.generated_persons.count()
|
||||||
|
|
||||||
|
generated_person_count.short_description = 'Uretilen kisi sayisi'
|
||||||
|
|
||||||
|
def _run_generate_persons(self, request, queryset, count, use_llm):
|
||||||
|
total_jobs = queryset.count()
|
||||||
|
ok_jobs = 0
|
||||||
|
|
||||||
|
for job in queryset:
|
||||||
|
try:
|
||||||
|
out = io.StringIO()
|
||||||
|
kwargs = {
|
||||||
|
'count': count,
|
||||||
|
'job_id': job.pk,
|
||||||
|
'stdout': out,
|
||||||
|
}
|
||||||
|
if use_llm:
|
||||||
|
kwargs['use_llm'] = True
|
||||||
|
else:
|
||||||
|
kwargs['no_llm'] = True
|
||||||
|
|
||||||
|
call_command('generate_persons', **kwargs)
|
||||||
|
ok_jobs += 1
|
||||||
|
except Exception as exc:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'Job id={job.pk} icin uretim hatasi: {exc}',
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
mode = 'LLM (fallback ile)' if use_llm else 'Istatistiksel'
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'Uretim tamamlandi. Mod: {mode}. Basarili job: {ok_jobs}/{total_jobs}. Her job icin {count} kayit.',
|
||||||
|
level=messages.SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.action(description='Secili job(lar) icin 100 kisi uret (Istatistiksel)')
|
||||||
|
def generate_100_statistical_action(self, request, queryset):
|
||||||
|
self._run_generate_persons(request, queryset, count=100, use_llm=False)
|
||||||
|
|
||||||
|
@admin.action(description='Secili job(lar) icin 100 kisi uret (LLM + fallback)')
|
||||||
|
def generate_100_llm_action(self, request, queryset):
|
||||||
|
self._run_generate_persons(request, queryset, count=100, use_llm=True)
|
||||||
|
|
||||||
|
@admin.action(description='Secili job(lar) icin 1000 kisi uret (Istatistiksel)')
|
||||||
|
def generate_1000_statistical_action(self, request, queryset):
|
||||||
|
self._run_generate_persons(request, queryset, count=1000, use_llm=False)
|
||||||
|
|
||||||
|
@admin.action(description='Secili job(lar) icin 1000 kisi uret (LLM + fallback)')
|
||||||
|
def generate_1000_llm_action(self, request, queryset):
|
||||||
|
self._run_generate_persons(request, queryset, count=1000, use_llm=True)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(NameVocab)
|
||||||
|
class NameVocabAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'name_type', 'gender', 'origin', 'frequency')
|
||||||
|
list_filter = ('origin', 'name_type', 'gender')
|
||||||
|
search_fields = ('name',)
|
||||||
|
ordering = ('origin', '-frequency')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(GeneratedPerson)
|
||||||
|
class GeneratedPersonAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'first_name', 'last_name', 'username', 'username_locked',
|
||||||
|
'birth_date', 'gender', 'confidence', 'generated_at', 'username_ops'
|
||||||
|
)
|
||||||
|
list_filter = ('gender', 'username_locked', 'generated_at')
|
||||||
|
search_fields = ('first_name', 'last_name', 'username')
|
||||||
|
readonly_fields = ('generated_at',)
|
||||||
|
actions = ('regenerate_username_action', 'lock_username_action', 'unlock_username_action')
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path(
|
||||||
|
'<int:person_id>/regenerate-username/',
|
||||||
|
self.admin_site.admin_view(self.regenerate_username_view),
|
||||||
|
name='namecreate_generatedperson_regenerate_username',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<int:person_id>/toggle-username-lock/',
|
||||||
|
self.admin_site.admin_view(self.toggle_username_lock_view),
|
||||||
|
name='namecreate_generatedperson_toggle_username_lock',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def regenerate_username_view(self, request, person_id):
|
||||||
|
person = self.get_object(request, person_id)
|
||||||
|
if person is None:
|
||||||
|
self.message_user(request, 'Kayit bulunamadi.', level=messages.ERROR)
|
||||||
|
return HttpResponseRedirect('../')
|
||||||
|
|
||||||
|
if person.username_locked:
|
||||||
|
self.message_user(request, 'Username kilitli. Once kilidi acin.', level=messages.WARNING)
|
||||||
|
return HttpResponseRedirect('../../')
|
||||||
|
|
||||||
|
old_username = person.username
|
||||||
|
person.regenerate_username(save=True)
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'Username guncellendi: {old_username} -> {person.username}',
|
||||||
|
level=messages.SUCCESS,
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect('../../')
|
||||||
|
|
||||||
|
def toggle_username_lock_view(self, request, person_id):
|
||||||
|
person = self.get_object(request, person_id)
|
||||||
|
if person is None:
|
||||||
|
self.message_user(request, 'Kayit bulunamadi.', level=messages.ERROR)
|
||||||
|
return HttpResponseRedirect('../')
|
||||||
|
|
||||||
|
person.username_locked = not person.username_locked
|
||||||
|
person.save(update_fields=['username_locked'])
|
||||||
|
state = 'kilitlendi' if person.username_locked else 'kilidi acildi'
|
||||||
|
self.message_user(request, f'Username {state}.', level=messages.SUCCESS)
|
||||||
|
return HttpResponseRedirect('../../')
|
||||||
|
|
||||||
|
def username_ops(self, obj):
|
||||||
|
regen_url = reverse('admin:namecreate_generatedperson_regenerate_username', args=[obj.pk])
|
||||||
|
lock_url = reverse('admin:namecreate_generatedperson_toggle_username_lock', args=[obj.pk])
|
||||||
|
lock_label = 'Kilidi ac' if obj.username_locked else 'Kilitle'
|
||||||
|
return format_html(
|
||||||
|
'<a class="button" href="{}">Yeniden uret</a> '
|
||||||
|
'<a class="button" href="{}">{}</a>',
|
||||||
|
regen_url,
|
||||||
|
lock_url,
|
||||||
|
lock_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
username_ops.short_description = 'Islemler'
|
||||||
|
|
||||||
|
@admin.action(description='Secili kayitlarda username yeniden uret')
|
||||||
|
def regenerate_username_action(self, request, queryset):
|
||||||
|
used_usernames = set(
|
||||||
|
GeneratedPerson.objects.exclude(username='').values_list('username', flat=True)
|
||||||
|
)
|
||||||
|
updated = 0
|
||||||
|
skipped = 0
|
||||||
|
for person in queryset:
|
||||||
|
if person.username_locked:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
used_usernames.discard(person.username)
|
||||||
|
person.regenerate_username(used_usernames=used_usernames, force=True, save=True)
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'{updated} kayitta username yeniden uretildi. Kilitli oldugu icin atlanan: {skipped}.'
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.action(description='Secili kayitlarda username kilitle')
|
||||||
|
def lock_username_action(self, request, queryset):
|
||||||
|
count = queryset.update(username_locked=True)
|
||||||
|
self.message_user(request, f'{count} kayitta username kilitlendi.')
|
||||||
|
|
||||||
|
@admin.action(description='Secili kayitlarda username kilidini ac')
|
||||||
|
def unlock_username_action(self, request, queryset):
|
||||||
|
count = queryset.update(username_locked=False)
|
||||||
|
self.message_user(request, f'{count} kayitta username kilidi acildi.')
|
||||||
5
namecreate/apps.py
Normal file
5
namecreate/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NamecreateConfig(AppConfig):
|
||||||
|
name = 'namecreate'
|
||||||
110
namecreate/llm_generator.py
Normal file
110
namecreate/llm_generator.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import json
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prompt(count, min_age):
|
||||||
|
max_year = date.today().year - min_age
|
||||||
|
return (
|
||||||
|
"Turkiye icin gercekci kisi verisi uret. "
|
||||||
|
"Turkce isim ve soyisim oncelikli olsun. "
|
||||||
|
f"{count} adet kayit uret. "
|
||||||
|
f"Her kaydin dogum yili en fazla {max_year} olsun (yani en az {min_age} yas). "
|
||||||
|
"Sadece JSON dondur. Baska metin yazma. "
|
||||||
|
"Format tam olarak su olsun: "
|
||||||
|
"{\"people\":[{\"first_name\":\"...\",\"last_name\":\"...\",\"gender\":\"E|K\",\"birth_date\":\"YYYY-MM-DD\"}]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_people(items, min_age):
|
||||||
|
valid = []
|
||||||
|
today = date.today()
|
||||||
|
for item in items:
|
||||||
|
first_name = str(item.get('first_name', '')).strip()
|
||||||
|
last_name = str(item.get('last_name', '')).strip()
|
||||||
|
gender = str(item.get('gender', '')).strip().upper()
|
||||||
|
birth_date_raw = str(item.get('birth_date', '')).strip()
|
||||||
|
|
||||||
|
if not first_name or not last_name:
|
||||||
|
continue
|
||||||
|
if gender not in {'E', 'K'}:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
birth_date = datetime.strptime(birth_date_raw, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
age = today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day))
|
||||||
|
if age < min_age:
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid.append({
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'gender': gender,
|
||||||
|
'birth_date': birth_date,
|
||||||
|
})
|
||||||
|
|
||||||
|
return valid
|
||||||
|
|
||||||
|
|
||||||
|
def generate_people_with_llm(count, min_age=20):
|
||||||
|
"""LLM API'den kisi verisi alir, validate edip dondurur."""
|
||||||
|
api_url = getattr(settings, 'LLM_API_URL', None)
|
||||||
|
model = getattr(settings, 'LLM_MODEL', None)
|
||||||
|
timeout = getattr(settings, 'LLM_TIMEOUT', 30)
|
||||||
|
api_key = getattr(settings, 'LLM_API_KEY', None)
|
||||||
|
|
||||||
|
if not api_url or not model:
|
||||||
|
raise RuntimeError('LLM_API_URL veya LLM_MODEL ayari eksik.')
|
||||||
|
|
||||||
|
prompt = _build_prompt(count=count, min_age=min_age)
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
if api_key:
|
||||||
|
headers['Authorization'] = f'Bearer {api_key}'
|
||||||
|
|
||||||
|
# OpenAI uyumlu endpoint
|
||||||
|
if '/v1/chat/completions' in api_url:
|
||||||
|
payload = {
|
||||||
|
'model': model,
|
||||||
|
'response_format': {'type': 'json_object'},
|
||||||
|
'messages': [
|
||||||
|
{'role': 'system', 'content': 'JSON disinda metin uretme.'},
|
||||||
|
{'role': 'user', 'content': prompt},
|
||||||
|
],
|
||||||
|
'temperature': 0.8,
|
||||||
|
}
|
||||||
|
resp = requests.post(api_url, headers=headers, json=payload, timeout=timeout)
|
||||||
|
resp.raise_for_status()
|
||||||
|
content = resp.json()['choices'][0]['message']['content']
|
||||||
|
else:
|
||||||
|
# Varsayilan: Ollama /api/chat
|
||||||
|
payload = {
|
||||||
|
'model': model,
|
||||||
|
'stream': False,
|
||||||
|
'format': 'json',
|
||||||
|
'messages': [
|
||||||
|
{'role': 'system', 'content': 'JSON disinda metin uretme.'},
|
||||||
|
{'role': 'user', 'content': prompt},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
resp = requests.post(api_url, headers=headers, json=payload, timeout=timeout)
|
||||||
|
resp.raise_for_status()
|
||||||
|
body = resp.json()
|
||||||
|
content = body.get('message', {}).get('content') or body.get('response')
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
raise RuntimeError('LLM bos cevap dondurdu.')
|
||||||
|
|
||||||
|
parsed = json.loads(content)
|
||||||
|
people = parsed.get('people') if isinstance(parsed, dict) else None
|
||||||
|
if not isinstance(people, list):
|
||||||
|
raise RuntimeError('LLM cevabi beklenen JSON formatinda degil.')
|
||||||
|
|
||||||
|
valid_people = _validate_people(people, min_age=min_age)
|
||||||
|
if not valid_people:
|
||||||
|
raise RuntimeError('LLM gecerli kisi verisi dondurmedi.')
|
||||||
|
|
||||||
|
return valid_people
|
||||||
0
namecreate/management/__init__.py
Normal file
0
namecreate/management/__init__.py
Normal file
0
namecreate/management/commands/__init__.py
Normal file
0
namecreate/management/commands/__init__.py
Normal file
204
namecreate/management/commands/generate_persons.py
Normal file
204
namecreate/management/commands/generate_persons.py
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
"""
|
||||||
|
NameVocab tablosundan ağırlıklı örneklemeyle kişi verisi üretir.
|
||||||
|
- Doğum tarihi: 20 yaş ve üstü (bugüne göre)
|
||||||
|
- Türkçe isimler frekans ağırlıklı olarak önce gelir
|
||||||
|
- Üretilen kayıtlar GeneratedPerson tablosuna kaydedilir
|
||||||
|
|
||||||
|
Kullanım:
|
||||||
|
python manage.py generate_persons # 1000 kişi
|
||||||
|
python manage.py generate_persons --count 500 # 500 kişi
|
||||||
|
python manage.py generate_persons --clear # önce tabloyu temizle
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import calendar
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from namecreate.models import NameVocab, GeneratedPerson, TrainingJob
|
||||||
|
from namecreate.username_utils import build_unique_username
|
||||||
|
from namecreate.llm_generator import generate_people_with_llm
|
||||||
|
|
||||||
|
# Bugün: 27 Mart 2026 — 20 yaş ve üstü → en geç 27 Mart 2006 doğum
|
||||||
|
MAX_BIRTH_YEAR = date.today().year - 20 # 2006
|
||||||
|
MIN_BIRTH_YEAR = 1940
|
||||||
|
|
||||||
|
|
||||||
|
def _weighted_pick(queryset):
|
||||||
|
"""Frequency değerlerine göre ağırlıklı rastgele seçim."""
|
||||||
|
names = list(queryset.values_list('name', 'frequency'))
|
||||||
|
if not names:
|
||||||
|
raise CommandError("NameVocab tablosu boş. Önce 'seed_name_vocab' komutunu çalıştırın.")
|
||||||
|
population = [n for n, _ in names]
|
||||||
|
weights = [f for _, f in names]
|
||||||
|
return random.choices(population, weights=weights, k=1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _random_birth_date():
|
||||||
|
"""20 yaş ve üstü rastgele doğum tarihi üretir."""
|
||||||
|
year = random.randint(MIN_BIRTH_YEAR, MAX_BIRTH_YEAR)
|
||||||
|
month = random.randint(1, 12)
|
||||||
|
# Ayın son gününü doğru hesapla (şubat, 30/31 gün farkları)
|
||||||
|
_, max_day = calendar.monthrange(year, month)
|
||||||
|
|
||||||
|
# 2006 doğumsa mart ayı sonrasında doğanlar henüz 20 yaşında değil
|
||||||
|
if year == MAX_BIRTH_YEAR:
|
||||||
|
today = date.today()
|
||||||
|
if month > today.month:
|
||||||
|
month = random.randint(1, today.month)
|
||||||
|
if month == today.month:
|
||||||
|
_, max_day = calendar.monthrange(year, month)
|
||||||
|
max_day = min(max_day, today.day)
|
||||||
|
|
||||||
|
_, max_day = calendar.monthrange(year, month)
|
||||||
|
day = random.randint(1, max_day)
|
||||||
|
return date(year, month, day)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '1000 kişilik sahte isim/soyisim/doğum tarihi verisi üretir (20 yaş ve üstü)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--count', type=int, default=1000,
|
||||||
|
help='Üretilecek kişi sayısı (varsayılan: 1000)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--clear', action='store_true',
|
||||||
|
help='Üretmeden önce mevcut GeneratedPerson kayıtlarını sil'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--job-id', type=int, default=None,
|
||||||
|
help='Bağlanacak TrainingJob ID (belirtilmezse en son tamamlanmış iş kullanılır)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--use-llm', action='store_true',
|
||||||
|
help='Uretimi LLM ile yapmayi dener; basarisiz olursa istatistiksel fallback yapar.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-llm', action='store_true',
|
||||||
|
help='LLM denemeyi kapatir, dogrudan istatistiksel uretim yapar.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--llm-strict', action='store_true',
|
||||||
|
help='LLM basarisiz olursa fallback yapma, komutu hata ile sonlandir.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
count = options['count']
|
||||||
|
|
||||||
|
if options['clear']:
|
||||||
|
deleted, _ = GeneratedPerson.objects.all().delete()
|
||||||
|
self.stdout.write(self.style.WARNING(f'{deleted} eski kayıt silindi.'))
|
||||||
|
|
||||||
|
# TrainingJob bağlantısını belirle
|
||||||
|
job_id = options.get('job_id')
|
||||||
|
if job_id:
|
||||||
|
try:
|
||||||
|
training_job = TrainingJob.objects.get(pk=job_id)
|
||||||
|
except TrainingJob.DoesNotExist:
|
||||||
|
raise CommandError(f'TrainingJob id={job_id} bulunamadı.')
|
||||||
|
else:
|
||||||
|
training_job = TrainingJob.objects.filter(status='completed').exclude(
|
||||||
|
model_path=None
|
||||||
|
).order_by('-completed_at').first()
|
||||||
|
if training_job is None:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'Tamamlanmış (model dosyalı) TrainingJob bulunamadı. training_job=None olarak üretilecek.'
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.stdout.write(f'TrainingJob kullanılıyor: id={training_job.pk} ({training_job.model_type})')
|
||||||
|
|
||||||
|
# Erkek / kadın isimlerini bir kez çek
|
||||||
|
male_first = NameVocab.objects.filter(name_type='first', gender='E')
|
||||||
|
female_first = NameVocab.objects.filter(name_type='first', gender='K')
|
||||||
|
last_names = NameVocab.objects.filter(name_type='last')
|
||||||
|
|
||||||
|
if not male_first.exists() or not female_first.exists() or not last_names.exists():
|
||||||
|
raise CommandError(
|
||||||
|
"NameVocab tablosu eksik. Önce 'python manage.py seed_name_vocab' komutunu çalıştırın."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tüm ağırlıklı listeleri RAM'e al (1000 kayıt için yeterli)
|
||||||
|
male_pool = list(male_first.values_list('name', 'frequency'))
|
||||||
|
female_pool = list(female_first.values_list('name', 'frequency'))
|
||||||
|
last_pool = list(last_names.values_list('name', 'frequency'))
|
||||||
|
|
||||||
|
def pick(pool):
|
||||||
|
names, weights = zip(*pool)
|
||||||
|
return random.choices(names, weights=weights, k=1)[0]
|
||||||
|
|
||||||
|
used_usernames = set(GeneratedPerson.objects.values_list('username', flat=True).exclude(username=''))
|
||||||
|
persons = []
|
||||||
|
source = 'statistical'
|
||||||
|
|
||||||
|
llm_rows = None
|
||||||
|
use_llm = options.get('use_llm') or not options.get('no_llm')
|
||||||
|
if use_llm:
|
||||||
|
try:
|
||||||
|
llm_rows = generate_people_with_llm(count=count, min_age=20)
|
||||||
|
source = 'llm'
|
||||||
|
except Exception as e:
|
||||||
|
if options.get('llm_strict'):
|
||||||
|
raise CommandError(f'LLM uretimi basarisiz: {e}')
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
f'LLM uretimi basarisiz ({e}). Istatistiksel fallback kullaniliyor.'
|
||||||
|
))
|
||||||
|
|
||||||
|
if llm_rows:
|
||||||
|
for row in llm_rows[:count]:
|
||||||
|
first = row['first_name']
|
||||||
|
last = row['last_name']
|
||||||
|
gender = row['gender']
|
||||||
|
birth = row['birth_date']
|
||||||
|
username = build_unique_username(first, last, birth, used_usernames)
|
||||||
|
persons.append(GeneratedPerson(
|
||||||
|
first_name=first,
|
||||||
|
last_name=last.upper(),
|
||||||
|
username=username,
|
||||||
|
birth_date=birth,
|
||||||
|
gender=gender,
|
||||||
|
confidence=None,
|
||||||
|
training_job=training_job,
|
||||||
|
))
|
||||||
|
|
||||||
|
missing = count - len(persons)
|
||||||
|
for _ in range(missing):
|
||||||
|
gender = random.choice(['E', 'K'])
|
||||||
|
first = pick(male_pool if gender == 'E' else female_pool)
|
||||||
|
last = pick(last_pool)
|
||||||
|
birth = _random_birth_date()
|
||||||
|
username = build_unique_username(first, last, birth, used_usernames)
|
||||||
|
persons.append(GeneratedPerson(
|
||||||
|
first_name=first,
|
||||||
|
last_name=last.upper(), # soyisim büyük harf
|
||||||
|
username=username,
|
||||||
|
birth_date=birth,
|
||||||
|
gender=gender,
|
||||||
|
confidence=None,
|
||||||
|
training_job=training_job,
|
||||||
|
))
|
||||||
|
|
||||||
|
GeneratedPerson.objects.bulk_create(persons)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n{count} kişi başarıyla üretildi ve kaydedildi.'
|
||||||
|
))
|
||||||
|
self.stdout.write(f' Kaynak: {source}')
|
||||||
|
self.stdout.write(
|
||||||
|
f" Erkek: {sum(1 for p in persons if p.gender == 'E')} | "
|
||||||
|
f"Kadın: {sum(1 for p in persons if p.gender == 'K')}"
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
f" Doğum aralığı: {MIN_BIRTH_YEAR} – {MAX_BIRTH_YEAR} (20 yaş ve üstü)"
|
||||||
|
)
|
||||||
|
# Örnek 5 kayıt göster
|
||||||
|
self.stdout.write('\nİlk 5 örnek:')
|
||||||
|
for p in persons[:5]:
|
||||||
|
age = date.today().year - p.birth_date.year - (
|
||||||
|
(date.today().month, date.today().day) < (p.birth_date.month, p.birth_date.day)
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
f" {p.first_name} {p.last_name} (@{p.username}) | {p.birth_date} | "
|
||||||
|
f"{p.get_gender_display()} | {age} yaş"
|
||||||
|
)
|
||||||
125
namecreate/management/commands/seed_name_vocab.py
Normal file
125
namecreate/management/commands/seed_name_vocab.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Türkçe kökenli isimler öncelikli olarak NameVocab tablosunu doldurur.
|
||||||
|
Kullanım: python manage.py seed_name_vocab
|
||||||
|
"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from namecreate.models import NameVocab
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Türkçe kökenli isimler — en yüksek öncelik
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
TURKCE_ERKEK = [
|
||||||
|
("Alp", 80), ("Alpay", 60), ("Alperen", 100), ("Altan", 70),
|
||||||
|
("Aydın", 90), ("Baran", 85), ("Barış", 120), ("Batuhan", 95),
|
||||||
|
("Berk", 75), ("Berkay", 80), ("Burak", 140), ("Çağan", 50),
|
||||||
|
("Çağrı", 65), ("Deniz", 110), ("Doğan", 70), ("Doruk", 55),
|
||||||
|
("Emre", 160), ("Enes", 90), ("Eren", 130), ("Erhan", 75),
|
||||||
|
("Furkan", 85), ("Görkem", 60), ("Güven", 45), ("Haluk", 50),
|
||||||
|
("İlker", 70), ("Kaan", 95), ("Kadir", 80), ("Kerem", 100),
|
||||||
|
("Koral", 45), ("Korhan", 50), ("Mert", 130), ("Oğuz", 75),
|
||||||
|
("Onur", 90), ("Orkun", 55), ("Selim", 85), ("Sercan", 70),
|
||||||
|
("Serdar", 80), ("Soner", 65), ("Tarık", 75), ("Tuna", 50),
|
||||||
|
("Tunahan", 60), ("Uğur", 85), ("Umut", 100), ("Ufuk", 55),
|
||||||
|
("Volkan", 80), ("Yiğit", 90), ("Yunus", 95), ("Zafer", 60),
|
||||||
|
]
|
||||||
|
|
||||||
|
TURKCE_KADIN = [
|
||||||
|
("Aslı", 110), ("Aylin", 100), ("Aynur", 75), ("Ayşen", 65),
|
||||||
|
("Banu", 70), ("Bahar", 90), ("Başak", 80), ("Belgin", 55),
|
||||||
|
("Bengü", 50), ("Berrak", 60), ("Burcu", 95), ("Büşra", 85),
|
||||||
|
("Cansu", 100), ("Ceren", 120), ("Çiğdem", 80), ("Deniz", 90),
|
||||||
|
("Ebru", 95), ("Elçin", 65), ("Elif", 150), ("Esra", 110),
|
||||||
|
("Ezgi", 100), ("Gizem", 90), ("Gül", 75), ("Gülşen", 55),
|
||||||
|
("Güneş", 50), ("Hande", 85), ("İlayda", 75), ("İpek", 80),
|
||||||
|
("Melike", 90), ("Meltem", 95), ("Merve", 130), ("Nilay", 80),
|
||||||
|
("Nur", 70), ("Özge", 100), ("Pınar", 90), ("Seda", 85),
|
||||||
|
("Selin", 100), ("Sibel", 80), ("Simge", 75), ("Tuğba", 85),
|
||||||
|
("Tülay", 60), ("Ülkü", 50), ("Yasemin", 110), ("Zeynep", 120),
|
||||||
|
("Zümra", 55),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Türkçe soyisimler
|
||||||
|
TURKCE_SOYISIM = [
|
||||||
|
("Yılmaz", 200), ("Kaya", 180), ("Demir", 170), ("Çelik", 150),
|
||||||
|
("Şahin", 140), ("Yıldız", 130), ("Arslan", 120), ("Doğan", 115),
|
||||||
|
("Kılıç", 110), ("Aslan", 105), ("Çetin", 100), ("Bulut", 95),
|
||||||
|
("Aydın", 90), ("Özdemir", 90), ("Demirci", 85), ("Güler", 80),
|
||||||
|
("Erdoğan", 75), ("Çakır", 75), ("Polat", 70), ("Koç", 70),
|
||||||
|
("Acar", 65), ("Kurt", 65), ("Yavuz", 65), ("Ateş", 60),
|
||||||
|
("Güneş", 60), ("Işık", 60), ("Karaca", 55), ("Türk", 55),
|
||||||
|
("Özkan", 55), ("Bay", 50), ("Toker", 50), ("Şimşek", 50),
|
||||||
|
("Akay", 45), ("Boz", 45), ("Deniz", 45), ("Ercan", 45),
|
||||||
|
("Güçlü", 40), ("Kaplan", 40), ("Savaş", 40), ("Turan", 40),
|
||||||
|
("Baş", 35), ("Çam", 35), ("Kara", 35), ("Taş", 35),
|
||||||
|
("Dağ", 30), ("Duman", 30), ("Gür", 30), ("Köse", 30),
|
||||||
|
("Uçar", 30), ("Yurt", 30),
|
||||||
|
]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Batı kökenli isimler — ikinci öncelik
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
BATI_ERKEK = [
|
||||||
|
("Can", 110), ("Cem", 90), ("Cenk", 75), ("Sarp", 60),
|
||||||
|
("Alper", 70), ("Enver", 55),
|
||||||
|
]
|
||||||
|
|
||||||
|
BATI_KADIN = [
|
||||||
|
("Ece", 100), ("Derya", 85), ("Sera", 60), ("Lara", 70),
|
||||||
|
("Nisa", 80), ("Sena", 75),
|
||||||
|
]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Arapça kökenli isimler — son öncelik (küçük liste)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
ARAPCA_ERKEK = [
|
||||||
|
("Ahmet", 160), ("Ali", 150), ("Mehmet", 170), ("Hasan", 100),
|
||||||
|
("Hüseyin", 95), ("İbrahim", 90), ("Mustafa", 140), ("Ömer", 80),
|
||||||
|
]
|
||||||
|
|
||||||
|
ARAPCA_KADIN = [
|
||||||
|
("Fatma", 120), ("Ayşe", 130), ("Hatice", 90), ("Havva", 70),
|
||||||
|
("Meryem", 80), ("Rabia", 65),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _bulk_create(entries, name_type, gender, origin):
|
||||||
|
objs = []
|
||||||
|
for name, freq in entries:
|
||||||
|
objs.append(NameVocab(
|
||||||
|
name=name,
|
||||||
|
name_type=name_type,
|
||||||
|
gender=gender,
|
||||||
|
origin=origin,
|
||||||
|
frequency=freq,
|
||||||
|
))
|
||||||
|
# ignore_conflicts: aynı kayıt varsa atla
|
||||||
|
NameVocab.objects.bulk_create(objs, ignore_conflicts=True)
|
||||||
|
return len(objs)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'NameVocab tablosunu Türkçe kökenli isimler öncelikli olarak doldurur'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
# Türkçe — birinci öncelik
|
||||||
|
total += _bulk_create(TURKCE_ERKEK, 'first', 'E', 'turkce')
|
||||||
|
total += _bulk_create(TURKCE_KADIN, 'first', 'K', 'turkce')
|
||||||
|
total += _bulk_create(TURKCE_SOYISIM, 'last', 'U', 'turkce')
|
||||||
|
|
||||||
|
# Batı — ikinci öncelik
|
||||||
|
total += _bulk_create(BATI_ERKEK, 'first', 'E', 'bati')
|
||||||
|
total += _bulk_create(BATI_KADIN, 'first', 'K', 'bati')
|
||||||
|
|
||||||
|
# Arapça — son öncelik
|
||||||
|
total += _bulk_create(ARAPCA_ERKEK, 'first', 'E', 'arapca')
|
||||||
|
total += _bulk_create(ARAPCA_KADIN, 'first', 'K', 'arapca')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'{total} isim işlendi. '
|
||||||
|
f'Türkçe: {NameVocab.objects.filter(origin="turkce").count()}, '
|
||||||
|
f'Batı: {NameVocab.objects.filter(origin="bati").count()}, '
|
||||||
|
f'Arapça: {NameVocab.objects.filter(origin="arapca").count()}'
|
||||||
|
))
|
||||||
40
namecreate/migrations/0001_initial.py
Normal file
40
namecreate/migrations/0001_initial.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 19:47
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TrainingJob',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('task_id', models.CharField(help_text='Celery task ID', max_length=255, unique=True)),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Beklemede'), ('running', 'Eğitiliyor'), ('completed', 'Tamamlandı'), ('failed', 'Başarısız')], default='pending', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('started_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('model_type', models.CharField(default='RandomForest', max_length=100)),
|
||||||
|
('model_version', models.DateTimeField(default=django.utils.timezone.now, help_text='Model versiyonu (timestamp)')),
|
||||||
|
('model_path', models.FilePathField(blank=True, null=True)),
|
||||||
|
('accuracy', models.FloatField(blank=True, null=True)),
|
||||||
|
('precision', models.FloatField(blank=True, null=True)),
|
||||||
|
('recall', models.FloatField(blank=True, null=True)),
|
||||||
|
('f1_score', models.FloatField(blank=True, null=True)),
|
||||||
|
('error_message', models.TextField(blank=True, null=True)),
|
||||||
|
('go_service_notified', models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Eğitim Görevi',
|
||||||
|
'verbose_name_plural': 'Eğitim Görevleri',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 19:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('namecreate', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trainingjob',
|
||||||
|
name='feature_count',
|
||||||
|
field=models.PositiveIntegerField(blank=True, help_text='Özellik (sütun) sayısı — ONNX tipi için kullanılır', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trainingjob',
|
||||||
|
name='features',
|
||||||
|
field=models.JSONField(blank=True, help_text='2D liste: her satır bir örnek, her sütun bir özellik', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trainingjob',
|
||||||
|
name='labels',
|
||||||
|
field=models.JSONField(blank=True, help_text='1D liste: her örneğin sınıf etiketi', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trainingjob',
|
||||||
|
name='sample_count',
|
||||||
|
field=models.PositiveIntegerField(blank=True, help_text='Eğitim verisi satır sayısı', null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
49
namecreate/migrations/0003_generatedperson_namevocab.py
Normal file
49
namecreate/migrations/0003_generatedperson_namevocab.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 20:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('namecreate', '0002_trainingjob_feature_count_trainingjob_features_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GeneratedPerson',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('first_name', models.CharField(max_length=100)),
|
||||||
|
('last_name', models.CharField(max_length=100)),
|
||||||
|
('birth_date', models.DateField()),
|
||||||
|
('gender', models.CharField(choices=[('E', 'Erkek'), ('K', 'Kadın')], max_length=1)),
|
||||||
|
('confidence', models.FloatField(blank=True, help_text='Modelin seçim güven skoru (0-1)', null=True)),
|
||||||
|
('generated_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('training_job', models.ForeignKey(blank=True, help_text='Bu kişiyi üreten model versiyonu', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='generated_persons', to='namecreate.trainingjob')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Üretilen Kişi',
|
||||||
|
'verbose_name_plural': 'Üretilen Kişiler',
|
||||||
|
'ordering': ['-generated_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NameVocab',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='İsim veya soyisim', max_length=100)),
|
||||||
|
('name_type', models.CharField(choices=[('first', 'İsim'), ('last', 'Soyisim')], max_length=5)),
|
||||||
|
('gender', models.CharField(choices=[('E', 'Erkek'), ('K', 'Kadın'), ('U', 'Unisex')], default='U', max_length=1)),
|
||||||
|
('origin', models.CharField(choices=[('turkce', 'Türkçe'), ('bati', 'Batı'), ('diger', 'Diğer'), ('arapca', 'Arapça')], default='turkce', help_text='Türkçe kökenli isimler varsayılan ve önceliklidir', max_length=10)),
|
||||||
|
('frequency', models.PositiveIntegerField(default=1, help_text='Veri setindeki görülme sıklığı — ağırlıklı seçimde kullanılır')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'İsim Sözlüğü',
|
||||||
|
'verbose_name_plural': 'İsim Sözlüğü',
|
||||||
|
'ordering': ['origin', '-frequency', 'name'],
|
||||||
|
'unique_together': {('name', 'name_type', 'gender')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
namecreate/migrations/0004_alter_trainingjob_model_path.py
Normal file
18
namecreate/migrations/0004_alter_trainingjob_model_path.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 20:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('namecreate', '0003_generatedperson_namevocab'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='trainingjob',
|
||||||
|
name='model_path',
|
||||||
|
field=models.CharField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
namecreate/migrations/0005_generatedperson_username.py
Normal file
18
namecreate/migrations/0005_generatedperson_username.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 20:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('namecreate', '0004_alter_trainingjob_model_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='generatedperson',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(blank=True, db_index=True, max_length=150),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-27 20:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('namecreate', '0005_generatedperson_username'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='generatedperson',
|
||||||
|
name='username_locked',
|
||||||
|
field=models.BooleanField(default=False, help_text='Aciksa username yeniden uretilmez (force ile degistirilebilir).'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
namecreate/migrations/__init__.py
Normal file
0
namecreate/migrations/__init__.py
Normal file
179
namecreate/models.py
Normal file
179
namecreate/models.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import random
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from .username_utils import build_unique_username
|
||||||
|
|
||||||
|
|
||||||
|
class NameVocab(models.Model):
|
||||||
|
"""Eğitim ve üretim için isim/soyisim sözlüğü. Türkçe kökenli isimler önceliklidir."""
|
||||||
|
|
||||||
|
ORIGIN_CHOICES = [
|
||||||
|
('turkce', 'Türkçe'), # Birinci öncelik
|
||||||
|
('bati', 'Batı'),
|
||||||
|
('diger', 'Diğer'),
|
||||||
|
('arapca', 'Arapça'), # Son öncelik
|
||||||
|
]
|
||||||
|
|
||||||
|
NAME_TYPE_CHOICES = [
|
||||||
|
('first', 'İsim'),
|
||||||
|
('last', 'Soyisim'),
|
||||||
|
]
|
||||||
|
|
||||||
|
GENDER_CHOICES = [
|
||||||
|
('E', 'Erkek'),
|
||||||
|
('K', 'Kadın'),
|
||||||
|
('U', 'Unisex'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100, help_text='İsim veya soyisim')
|
||||||
|
name_type = models.CharField(max_length=5, choices=NAME_TYPE_CHOICES)
|
||||||
|
gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='U')
|
||||||
|
origin = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=ORIGIN_CHOICES,
|
||||||
|
default='turkce',
|
||||||
|
help_text='Türkçe kökenli isimler varsayılan ve önceliklidir',
|
||||||
|
)
|
||||||
|
frequency = models.PositiveIntegerField(
|
||||||
|
default=1,
|
||||||
|
help_text='Veri setindeki görülme sıklığı — ağırlıklı seçimde kullanılır'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['origin', '-frequency', 'name']
|
||||||
|
verbose_name = 'İsim Sözlüğü'
|
||||||
|
verbose_name_plural = 'İsim Sözlüğü'
|
||||||
|
unique_together = [('name', 'name_type', 'gender')]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.get_name_type_display()}, {self.get_gender_display()}, {self.get_origin_display()})"
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratedPerson(models.Model):
|
||||||
|
"""Modelin ürettiği kişi kaydı."""
|
||||||
|
|
||||||
|
GENDER_CHOICES = [
|
||||||
|
('E', 'Erkek'),
|
||||||
|
('K', 'Kadın'),
|
||||||
|
]
|
||||||
|
|
||||||
|
training_job = models.ForeignKey(
|
||||||
|
'TrainingJob',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='generated_persons',
|
||||||
|
help_text='Bu kişiyi üreten model versiyonu',
|
||||||
|
)
|
||||||
|
first_name = models.CharField(max_length=100)
|
||||||
|
last_name = models.CharField(max_length=100)
|
||||||
|
username = models.CharField(max_length=150, blank=True, db_index=True)
|
||||||
|
username_locked = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Aciksa username yeniden uretilmez (force ile degistirilebilir).'
|
||||||
|
)
|
||||||
|
birth_date = models.DateField()
|
||||||
|
gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
|
||||||
|
confidence = models.FloatField(
|
||||||
|
null=True, blank=True,
|
||||||
|
help_text='Modelin seçim güven skoru (0-1)'
|
||||||
|
)
|
||||||
|
generated_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-generated_at']
|
||||||
|
verbose_name = 'Üretilen Kişi'
|
||||||
|
verbose_name_plural = 'Üretilen Kişiler'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.first_name} {self.last_name} ({self.birth_date})"
|
||||||
|
|
||||||
|
def regenerate_username(self, used_usernames=None, force=False, save=True):
|
||||||
|
"""Kayit icin yeni bir username uretir. Mevcut username her zaman degisir."""
|
||||||
|
if self.username_locked and not force:
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
if used_usernames is None:
|
||||||
|
used_usernames = set(
|
||||||
|
GeneratedPerson.objects.exclude(pk=self.pk).exclude(username='').values_list('username', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mevcut username'i yasak listesine ekle ve rastgele sonek ile basla
|
||||||
|
# Boylece ayni base'e hic donulmez (ping-pong olmaz)
|
||||||
|
if self.username:
|
||||||
|
used_usernames.add(self.username)
|
||||||
|
|
||||||
|
suffix_min = getattr(settings, 'USERNAME_SUFFIX_MIN', 2)
|
||||||
|
suffix_max = getattr(settings, 'USERNAME_SUFFIX_MAX', 999)
|
||||||
|
self.username = build_unique_username(
|
||||||
|
self.first_name,
|
||||||
|
self.last_name,
|
||||||
|
self.birth_date,
|
||||||
|
used_usernames,
|
||||||
|
_force_suffix=random.randint(suffix_min, suffix_max),
|
||||||
|
)
|
||||||
|
if save:
|
||||||
|
self.save(update_fields=['username'])
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
|
class TrainingJob(models.Model):
|
||||||
|
"""Makine öğrenme model eğitim görevinin kaydı."""
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', 'Beklemede'),
|
||||||
|
('running', 'Eğitiliyor'),
|
||||||
|
('completed', 'Tamamlandı'),
|
||||||
|
('failed', 'Başarısız'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Temel bilgiler
|
||||||
|
task_id = models.CharField(max_length=255, unique=True, help_text='Celery task ID')
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
started_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
completed_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
# Model bilgileri
|
||||||
|
model_type = models.CharField(max_length=100, default='RandomForest')
|
||||||
|
model_version = models.DateTimeField(default=timezone.now, help_text='Model versiyonu (timestamp)')
|
||||||
|
model_path = models.CharField(max_length=500, null=True, blank=True)
|
||||||
|
|
||||||
|
# Eğitim verisi
|
||||||
|
features = models.JSONField(
|
||||||
|
null=True, blank=True,
|
||||||
|
help_text='2D liste: her satır bir örnek, her sütun bir özellik'
|
||||||
|
)
|
||||||
|
labels = models.JSONField(
|
||||||
|
null=True, blank=True,
|
||||||
|
help_text='1D liste: her örneğin sınıf etiketi'
|
||||||
|
)
|
||||||
|
feature_count = models.PositiveIntegerField(
|
||||||
|
null=True, blank=True,
|
||||||
|
help_text='Özellik (sütun) sayısı — ONNX tipi için kullanılır'
|
||||||
|
)
|
||||||
|
sample_count = models.PositiveIntegerField(
|
||||||
|
null=True, blank=True,
|
||||||
|
help_text='Eğitim verisi satır sayısı'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Metrikler
|
||||||
|
accuracy = models.FloatField(null=True, blank=True)
|
||||||
|
precision = models.FloatField(null=True, blank=True)
|
||||||
|
recall = models.FloatField(null=True, blank=True)
|
||||||
|
f1_score = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
|
# Hata handling
|
||||||
|
error_message = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
# Go servisine sinyal
|
||||||
|
go_service_notified = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created_at']
|
||||||
|
verbose_name = 'Eğitim Görevi'
|
||||||
|
verbose_name_plural = 'Eğitim Görevleri'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.model_type} - {self.status} - {self.created_at}"
|
||||||
129
namecreate/tasks.py
Normal file
129
namecreate/tasks.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
|
from celery import shared_task
|
||||||
|
from django.conf import settings
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.datasets import load_iris
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
|
||||||
|
from skl2onnx import convert_sklearn
|
||||||
|
from skl2onnx.common.data_types import FloatTensorType
|
||||||
|
from namecreate.models import TrainingJob
|
||||||
|
|
||||||
|
|
||||||
|
def notify_go_service(model_path, metrics):
|
||||||
|
"""Go servisine model yüklenmiş olduğunu bildirir."""
|
||||||
|
try:
|
||||||
|
go_service_url = settings.GO_SERVICE_URL
|
||||||
|
if not go_service_url:
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model_path": model_path,
|
||||||
|
"metrics": metrics,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{go_service_url}/reload-model",
|
||||||
|
json=payload,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Go servisi bildirimi başarısız: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(name='namecreate.tasks.train_model_task')
|
||||||
|
def train_model_task(task_id):
|
||||||
|
"""
|
||||||
|
Makine öğrenme modelini arka planda eğitir ve ONNX olarak kaydeder.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
job = TrainingJob.objects.get(task_id=task_id)
|
||||||
|
job.status = 'running'
|
||||||
|
job.started_at = datetime.now()
|
||||||
|
job.save(update_fields=['status', 'started_at'])
|
||||||
|
|
||||||
|
# 1. Veri Seti Yükleme
|
||||||
|
if job.features and job.labels:
|
||||||
|
# Kullanıcının gönderdiği veri
|
||||||
|
X = np.array(job.features, dtype=np.float32)
|
||||||
|
y = np.array(job.labels, dtype=np.int32)
|
||||||
|
else:
|
||||||
|
# Demo: Iris dataset
|
||||||
|
iris = load_iris()
|
||||||
|
X, y = iris.data.astype(np.float32), iris.target
|
||||||
|
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(
|
||||||
|
X, y, test_size=0.2, random_state=42
|
||||||
|
)
|
||||||
|
feature_count = X.shape[1]
|
||||||
|
|
||||||
|
# 2. Model Eğitimi
|
||||||
|
model = RandomForestClassifier(n_estimators=10, random_state=42)
|
||||||
|
model.fit(X_train, y_train)
|
||||||
|
|
||||||
|
# 3. Metrikleri Hesapla
|
||||||
|
y_pred = model.predict(X_test)
|
||||||
|
accuracy = accuracy_score(y_test, y_pred)
|
||||||
|
precision = precision_score(y_test, y_pred, average='weighted')
|
||||||
|
recall = recall_score(y_test, y_pred, average='weighted')
|
||||||
|
f1 = f1_score(y_test, y_pred, average='weighted')
|
||||||
|
|
||||||
|
# 4. ONNX Formatına Dönüştür (feature_count dinamik)
|
||||||
|
initial_type = [('float_input', FloatTensorType([None, feature_count]))]
|
||||||
|
onx = convert_sklearn(model, initial_types=initial_type)
|
||||||
|
|
||||||
|
# 5. Dosyaya Kaydet (Versiyonlu - Timestamp ile)
|
||||||
|
timestamp = job.model_version.strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
model_filename = f"model_{timestamp}.onnx"
|
||||||
|
model_path = os.path.join(settings.MEDIA_ROOT, 'models', model_filename)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
||||||
|
with open(model_path, "wb") as f:
|
||||||
|
f.write(onx.SerializeToString())
|
||||||
|
|
||||||
|
# 6. Go Servisine Bilder
|
||||||
|
metrics = {
|
||||||
|
'accuracy': float(accuracy),
|
||||||
|
'precision': float(precision),
|
||||||
|
'recall': float(recall),
|
||||||
|
'f1_score': float(f1),
|
||||||
|
}
|
||||||
|
go_notified = notify_go_service(model_path, metrics)
|
||||||
|
|
||||||
|
# 7. Veritabanına Kaydet
|
||||||
|
job.status = 'completed'
|
||||||
|
job.completed_at = datetime.now()
|
||||||
|
job.model_path = model_path
|
||||||
|
job.accuracy = accuracy
|
||||||
|
job.precision = precision
|
||||||
|
job.recall = recall
|
||||||
|
job.f1_score = f1
|
||||||
|
job.go_service_notified = go_notified
|
||||||
|
job.save()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'task_id': task_id,
|
||||||
|
'model_path': model_path,
|
||||||
|
'go_service_notified': go_notified,
|
||||||
|
'metrics': metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
job = TrainingJob.objects.get(task_id=task_id)
|
||||||
|
job.status = 'failed'
|
||||||
|
job.error_message = str(e)
|
||||||
|
job.save(update_fields=['status', 'error_message'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'task_id': task_id,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
3
namecreate/tests.py
Normal file
3
namecreate/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
8
namecreate/urls.py
Normal file
8
namecreate/urls.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('train-model/', views.train_and_export_model, name='train-model'),
|
||||||
|
path('training-status/', views.get_training_status, name='training-status'),
|
||||||
|
path('training-jobs/', views.list_training_jobs, name='training-jobs'),
|
||||||
|
]
|
||||||
60
namecreate/username_utils.py
Normal file
60
namecreate/username_utils.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_for_username(value):
|
||||||
|
"""Turkce karakterleri ASCII'ye cevirip username-safe hale getirir."""
|
||||||
|
tr_map = str.maketrans({
|
||||||
|
'c': 'c',
|
||||||
|
'C': 'c',
|
||||||
|
'g': 'g',
|
||||||
|
'G': 'g',
|
||||||
|
'i': 'i',
|
||||||
|
'I': 'i',
|
||||||
|
'o': 'o',
|
||||||
|
'O': 'o',
|
||||||
|
's': 's',
|
||||||
|
'S': 's',
|
||||||
|
'u': 'u',
|
||||||
|
'U': 'u',
|
||||||
|
'ç': 'c',
|
||||||
|
'Ç': 'c',
|
||||||
|
'ğ': 'g',
|
||||||
|
'Ğ': 'g',
|
||||||
|
'ı': 'i',
|
||||||
|
'İ': 'i',
|
||||||
|
'ö': 'o',
|
||||||
|
'Ö': 'o',
|
||||||
|
'ş': 's',
|
||||||
|
'Ş': 's',
|
||||||
|
'ü': 'u',
|
||||||
|
'Ü': 'u',
|
||||||
|
})
|
||||||
|
value = value.translate(tr_map).lower()
|
||||||
|
return re.sub(r'[^a-z0-9]+', '', value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_unique_username(first_name, last_name, birth_date, used_usernames, _force_suffix=None):
|
||||||
|
"""
|
||||||
|
ad.soyadYY formatinda username uretir, cakisma olursa sonek ekler.
|
||||||
|
_force_suffix: verilirse bare base denenmez, bu sayidan itibaren baslar
|
||||||
|
(regeneration ping-pong onlemek icin kullanilir).
|
||||||
|
"""
|
||||||
|
first = normalize_for_username(first_name or '')
|
||||||
|
last = normalize_for_username(last_name or '')
|
||||||
|
yy = str(birth_date.year)[-2:]
|
||||||
|
base = f"{first}.{last}{yy}" if first and last else f"user{yy}"
|
||||||
|
|
||||||
|
if _force_suffix is not None:
|
||||||
|
# Yeniden uretim: bare base'i hic deneme, rastgele bir sonek ile basla
|
||||||
|
counter = _force_suffix
|
||||||
|
candidate = f"{base}{counter}"
|
||||||
|
else:
|
||||||
|
candidate = base
|
||||||
|
counter = 1
|
||||||
|
|
||||||
|
while candidate in used_usernames:
|
||||||
|
counter += 1
|
||||||
|
candidate = f"{base}{counter}"
|
||||||
|
|
||||||
|
used_usernames.add(candidate)
|
||||||
|
return candidate
|
||||||
241
namecreate/views.py
Normal file
241
namecreate/views.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
|
from namecreate.models import TrainingJob
|
||||||
|
from namecreate.tasks import train_model_task
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_json_data(request):
|
||||||
|
"""
|
||||||
|
JSON body'den veriyi çeker.
|
||||||
|
Beklenen format:
|
||||||
|
{
|
||||||
|
"features": [[1.2, 3.4, 5.6, 7.8], [2.1, 4.3, 6.5, 8.7], ...],
|
||||||
|
"labels": [0, 1, 2, ...]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
body = json.loads(request.body)
|
||||||
|
features = body.get('features')
|
||||||
|
labels = body.get('labels')
|
||||||
|
|
||||||
|
if not features or not labels:
|
||||||
|
raise ValueError("'features' ve 'labels' alanları zorunludur.")
|
||||||
|
if len(features) != len(labels):
|
||||||
|
raise ValueError("'features' ve 'labels' eleman sayısı eşit olmalıdır.")
|
||||||
|
if len(features) < 10:
|
||||||
|
raise ValueError("En az 10 eğitim örneği gönderiniz.")
|
||||||
|
|
||||||
|
# Tip güvenliği: tüm değerler sayısal olmalı
|
||||||
|
for i, row in enumerate(features):
|
||||||
|
if not all(isinstance(v, (int, float)) for v in row):
|
||||||
|
raise ValueError(f"features[{i}] içinde sayısal olmayan değer var.")
|
||||||
|
if not all(isinstance(v, (int, float)) for v in labels):
|
||||||
|
raise ValueError("labels listesi sadece sayısal değer içermelidir.")
|
||||||
|
|
||||||
|
return features, [int(v) for v in labels]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_csv_data(file):
|
||||||
|
"""
|
||||||
|
CSV dosyasından veriyi çeker.
|
||||||
|
Beklenen format — son sütun label, geri kalanlar feature:
|
||||||
|
1.2,3.4,5.6,7.8,0
|
||||||
|
2.1,4.3,6.5,8.7,1
|
||||||
|
...
|
||||||
|
Header satırı varsa otomatik atlanır.
|
||||||
|
"""
|
||||||
|
content = file.read().decode('utf-8')
|
||||||
|
reader = csv.reader(io.StringIO(content))
|
||||||
|
|
||||||
|
features, labels = [], []
|
||||||
|
for lineno, row in enumerate(reader, start=1):
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
# Header satırını atla
|
||||||
|
try:
|
||||||
|
values = [float(v) for v in row]
|
||||||
|
except ValueError:
|
||||||
|
if lineno == 1:
|
||||||
|
continue # Başlık satırı
|
||||||
|
raise ValueError(f"CSV satır {lineno}: sayısal olmayan değer.")
|
||||||
|
|
||||||
|
if len(values) < 2:
|
||||||
|
raise ValueError(f"CSV satır {lineno}: en az 2 sütun (özellik + etiket) gerekli.")
|
||||||
|
|
||||||
|
features.append(values[:-1])
|
||||||
|
labels.append(int(values[-1]))
|
||||||
|
|
||||||
|
if len(features) < 10:
|
||||||
|
raise ValueError("En az 10 eğitim örneği gönderiniz.")
|
||||||
|
|
||||||
|
return features, labels
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def train_and_export_model(request):
|
||||||
|
"""
|
||||||
|
Modeli arka planda eğitmek için Celery task'ı başlatır.
|
||||||
|
|
||||||
|
Veri gönderme yöntemleri:
|
||||||
|
|
||||||
|
1) JSON body:
|
||||||
|
POST /api/v1/ml/train-model/
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"features": [[1.2, 3.4, 5.6, 7.8], ...],
|
||||||
|
"labels": [0, 1, 2, ...]
|
||||||
|
}
|
||||||
|
|
||||||
|
2) CSV dosyası:
|
||||||
|
POST /api/v1/ml/train-model/
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
Form alanı: data=<csv_dosyası>
|
||||||
|
(Son sütun label, geri kalanlar feature)
|
||||||
|
|
||||||
|
3) Veri göndermezseniz yerleşik Iris demo verisi kullanılır.
|
||||||
|
"""
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({'error': 'Sadece POST destekleniyor.'}, status=405)
|
||||||
|
|
||||||
|
try:
|
||||||
|
features, labels = None, None
|
||||||
|
source = 'demo'
|
||||||
|
|
||||||
|
# --- Yöntem 1: JSON body ---
|
||||||
|
ct = request.content_type or ''
|
||||||
|
if 'application/json' in ct and request.body:
|
||||||
|
features, labels = _parse_json_data(request)
|
||||||
|
source = 'json'
|
||||||
|
|
||||||
|
# --- Yöntem 2: CSV dosyası ---
|
||||||
|
elif 'data' in request.FILES:
|
||||||
|
features, labels = _parse_csv_data(request.FILES['data'])
|
||||||
|
source = 'csv'
|
||||||
|
|
||||||
|
# --- Yöntem 3: Demo (Iris) ---
|
||||||
|
# features ve labels None kalır, task default veriyi kullanır
|
||||||
|
|
||||||
|
feature_count = len(features[0]) if features else None
|
||||||
|
sample_count = len(features) if features else None
|
||||||
|
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
TrainingJob.objects.create(
|
||||||
|
task_id=task_id,
|
||||||
|
status='pending',
|
||||||
|
features=features,
|
||||||
|
labels=labels,
|
||||||
|
feature_count=feature_count,
|
||||||
|
sample_count=sample_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
celery_task = train_model_task.delay(task_id)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'status': 'queued',
|
||||||
|
'message': 'Model eğitim görevi başlatıldı.',
|
||||||
|
'task_id': task_id,
|
||||||
|
'celery_task_id': celery_task.id,
|
||||||
|
'data_source': source,
|
||||||
|
'sample_count': sample_count,
|
||||||
|
'feature_count': feature_count,
|
||||||
|
})
|
||||||
|
|
||||||
|
except (ValueError, KeyError) as e:
|
||||||
|
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
def get_training_status(request):
|
||||||
|
"""Task'ın durumunu sorgular."""
|
||||||
|
task_id = request.GET.get('task_id')
|
||||||
|
|
||||||
|
if not task_id:
|
||||||
|
return JsonResponse({
|
||||||
|
"error": "task_id gerekli"
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
job = TrainingJob.objects.get(task_id=task_id)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"task_id": job.task_id,
|
||||||
|
"status": job.status,
|
||||||
|
"created_at": job.created_at.isoformat() if job.created_at else None,
|
||||||
|
"started_at": job.started_at.isoformat() if job.started_at else None,
|
||||||
|
"completed_at": job.completed_at.isoformat() if job.completed_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Task tamamlandığında metrikleri ekle
|
||||||
|
if job.status == 'completed':
|
||||||
|
response.update({
|
||||||
|
"model_version": job.model_version.isoformat(),
|
||||||
|
"metrics": {
|
||||||
|
"accuracy": job.accuracy,
|
||||||
|
"precision": job.precision,
|
||||||
|
"recall": job.recall,
|
||||||
|
"f1_score": job.f1_score,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Task başarısız olmuşsa hata mesajı ekle
|
||||||
|
if job.status == 'failed':
|
||||||
|
response["error_message"] = job.error_message
|
||||||
|
|
||||||
|
return JsonResponse(response)
|
||||||
|
|
||||||
|
except TrainingJob.DoesNotExist:
|
||||||
|
return JsonResponse({
|
||||||
|
"error": "Task bulunamadı"
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@authentication_classes([SessionAuthentication, JWTAuthentication])
|
||||||
|
@permission_classes([IsAdminUser])
|
||||||
|
def list_training_jobs(request):
|
||||||
|
"""TrainingJob kayıtlarını listeler."""
|
||||||
|
status_filter = request.GET.get('status')
|
||||||
|
limit = request.GET.get('limit', '50')
|
||||||
|
|
||||||
|
try:
|
||||||
|
limit = max(1, min(int(limit), 200))
|
||||||
|
except ValueError:
|
||||||
|
return JsonResponse({"error": "limit sayısal olmalıdır"}, status=400)
|
||||||
|
|
||||||
|
jobs = TrainingJob.objects.all().order_by('-created_at')
|
||||||
|
if status_filter:
|
||||||
|
jobs = jobs.filter(status=status_filter)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for job in jobs[:limit]:
|
||||||
|
model_exists = bool(job.model_path and os.path.exists(job.model_path))
|
||||||
|
items.append({
|
||||||
|
'id': job.pk,
|
||||||
|
'task_id': job.task_id,
|
||||||
|
'status': job.status,
|
||||||
|
'model_type': job.model_type,
|
||||||
|
'created_at': job.created_at.isoformat() if job.created_at else None,
|
||||||
|
'started_at': job.started_at.isoformat() if job.started_at else None,
|
||||||
|
'completed_at': job.completed_at.isoformat() if job.completed_at else None,
|
||||||
|
'sample_count': job.sample_count,
|
||||||
|
'feature_count': job.feature_count,
|
||||||
|
'model_path': job.model_path,
|
||||||
|
'model_exists': model_exists,
|
||||||
|
'error_message': job.error_message,
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'count': len(items),
|
||||||
|
'results': items,
|
||||||
|
})
|
||||||
86
requirements.txt
Normal file
86
requirements.txt
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
amqp==5.3.1
|
||||||
|
asgiref==3.11.1
|
||||||
|
attrs==26.1.0
|
||||||
|
billiard==4.2.4
|
||||||
|
celery==5.6.3
|
||||||
|
certifi==2026.2.25
|
||||||
|
cffi==2.0.0
|
||||||
|
charset-normalizer==3.4.6
|
||||||
|
click==8.3.1
|
||||||
|
click-didyoumean==0.3.1
|
||||||
|
click-plugins==1.1.1.2
|
||||||
|
click-repl==0.3.0
|
||||||
|
cron-descriptor==1.4.5
|
||||||
|
cryptography==46.0.6
|
||||||
|
defusedxml==0.7.1
|
||||||
|
Django==6.0.3
|
||||||
|
django-appconf==1.2.0
|
||||||
|
django-autoslug==1.9.9
|
||||||
|
django-celery-beat==2.9.0
|
||||||
|
django-cleanup==9.0.0
|
||||||
|
django-colorfield==0.14.0
|
||||||
|
django-cors-headers==4.9.0
|
||||||
|
django-cropper-image==1.0.5
|
||||||
|
django-imagekit==6.1.0
|
||||||
|
django-redis==6.0.0
|
||||||
|
django-stubs==6.0.1
|
||||||
|
django-stubs-ext==6.0.1
|
||||||
|
django-timezone-field==7.2.1
|
||||||
|
django_celery_results==2.6.0
|
||||||
|
djangorestframework==3.17.1
|
||||||
|
djangorestframework-stubs==3.16.8
|
||||||
|
djangorestframework_simplejwt==5.5.1
|
||||||
|
djoser==2.3.3
|
||||||
|
drf-spectacular==0.29.0
|
||||||
|
drf-spectacular-sidecar==2026.3.1
|
||||||
|
flower==2.0.1
|
||||||
|
hiredis==3.3.1
|
||||||
|
humanize==4.15.0
|
||||||
|
idna==3.11
|
||||||
|
inflection==0.5.1
|
||||||
|
joblib==1.5.3
|
||||||
|
jsonschema==4.26.0
|
||||||
|
jsonschema-specifications==2025.9.1
|
||||||
|
kombu==5.6.2
|
||||||
|
ml_dtypes==0.5.4
|
||||||
|
numpy==2.4.3
|
||||||
|
oauthlib==3.3.1
|
||||||
|
onnx==1.20.1
|
||||||
|
packaging==26.0
|
||||||
|
pilkit==3.0
|
||||||
|
pillow==12.1.1
|
||||||
|
prometheus_client==0.24.1
|
||||||
|
prompt_toolkit==3.0.52
|
||||||
|
protobuf==7.34.1
|
||||||
|
psycopg2-binary==2.9.11
|
||||||
|
pycparser==3.0
|
||||||
|
PyJWT==2.12.1
|
||||||
|
python-crontab==3.3.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-decouple==3.8
|
||||||
|
python3-openid==3.2.0
|
||||||
|
pytz==2026.1.post1
|
||||||
|
PyYAML==6.0.3
|
||||||
|
redis==7.4.0
|
||||||
|
referencing==0.37.0
|
||||||
|
requests==2.33.0
|
||||||
|
requests-oauthlib==2.0.0
|
||||||
|
rpds-py==0.30.0
|
||||||
|
scikit-learn==1.8.0
|
||||||
|
scipy==1.17.1
|
||||||
|
six==1.17.0
|
||||||
|
skl2onnx==1.20.0
|
||||||
|
social-auth-app-django==5.7.0
|
||||||
|
social-auth-core==4.8.5
|
||||||
|
sqlparse==0.5.5
|
||||||
|
threadpoolctl==3.6.0
|
||||||
|
tornado==6.5.5
|
||||||
|
types-PyYAML==6.0.12.20250915
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
tzdata==2025.3
|
||||||
|
tzlocal==5.3.1
|
||||||
|
uritemplate==4.2.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
vine==5.1.0
|
||||||
|
wcwidth==0.6.0
|
||||||
|
whitenoise==6.12.0
|
||||||
129
schema.yml
Normal file
129
schema.yml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Your Project API
|
||||||
|
version: 1.0.0
|
||||||
|
description: Your project description
|
||||||
|
paths:
|
||||||
|
/api/v1/auth/jwt/create/:
|
||||||
|
post:
|
||||||
|
operationId: create_create
|
||||||
|
description: |-
|
||||||
|
Takes a set of user credentials and returns an access and refresh JSON web
|
||||||
|
token pair to prove the authentication of those credentials.
|
||||||
|
tags:
|
||||||
|
- create
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenObtainPair'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenObtainPair'
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenObtainPair'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenObtainPair'
|
||||||
|
description: ''
|
||||||
|
/api/v1/auth/jwt/refresh/:
|
||||||
|
post:
|
||||||
|
operationId: refresh_create
|
||||||
|
description: |-
|
||||||
|
Takes a refresh type JSON web token and returns an access type JSON web
|
||||||
|
token if the refresh token is valid.
|
||||||
|
tags:
|
||||||
|
- refresh
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenRefresh'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenRefresh'
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenRefresh'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenRefresh'
|
||||||
|
description: ''
|
||||||
|
/api/v1/auth/jwt/verify/:
|
||||||
|
post:
|
||||||
|
operationId: verify_create
|
||||||
|
description: |-
|
||||||
|
Takes a token and indicates if it is valid. This view provides no
|
||||||
|
information about a token's fitness for a particular use.
|
||||||
|
tags:
|
||||||
|
- verify
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenVerify'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenVerify'
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenVerify'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TokenVerify'
|
||||||
|
description: ''
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
TokenObtainPair:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
writeOnly: true
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
writeOnly: true
|
||||||
|
access:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
refresh:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- access
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
- refresh
|
||||||
|
TokenRefresh:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
access:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
refresh:
|
||||||
|
type: string
|
||||||
|
writeOnly: true
|
||||||
|
required:
|
||||||
|
- access
|
||||||
|
- refresh
|
||||||
|
TokenVerify:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
writeOnly: true
|
||||||
|
required:
|
||||||
|
- token
|
||||||
Reference in New Issue
Block a user