first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:26:46 +03:00
commit 2be3a313ad
55 changed files with 3609 additions and 0 deletions

42
.env Normal file
View 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
View 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
View 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
View 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
View File

37
accounts/admin.py Normal file
View 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
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

View File

View File

View 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
View 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)

View 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',
},
),
]

View 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'),
),
]

View File

129
accounts/models.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

15
clery.txt Normal file
View 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
View File

@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

16
core/asgi.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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()

Binary file not shown.

0
namecreate/__init__.py Normal file
View File

250
namecreate/admin.py Normal file
View 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>&nbsp;'
'<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
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class NamecreateConfig(AppConfig):
name = 'namecreate'

110
namecreate/llm_generator.py Normal file
View 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

View File

View 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ş"
)

View 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), ("ı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()}'
))

View 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'],
},
),
]

View File

@@ -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),
),
]

View 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')},
},
),
]

View 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),
),
]

View 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),
),
]

View File

@@ -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).'),
),
]

View File

179
namecreate/models.py Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
namecreate/urls.py Normal file
View 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'),
]

View 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
View 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
View 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
View 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