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